This commit was merged in pull request #111.
This commit is contained in:
@@ -21,7 +21,11 @@ export type CostEvent =
|
|||||||
export type CommentaryEvent =
|
export type CommentaryEvent =
|
||||||
| { type: "agent_commentary"; agentId: string; jobId: string; text: string };
|
| { type: "agent_commentary"; agentId: string; jobId: string; text: string };
|
||||||
|
|
||||||
export type BusEvent = JobEvent | SessionEvent | DebateEvent | CostEvent | CommentaryEvent;
|
// External agent state changes (e.g. Kimi, Perplexity picking up or completing tasks)
|
||||||
|
export type AgentExternalEvent =
|
||||||
|
| { type: "agent:external_state"; agentId: string; state: string; taskSummary?: string };
|
||||||
|
|
||||||
|
export type BusEvent = JobEvent | SessionEvent | DebateEvent | CostEvent | CommentaryEvent | AgentExternalEvent;
|
||||||
|
|
||||||
class EventBus extends EventEmitter {
|
class EventBus extends EventEmitter {
|
||||||
emit(event: "bus", data: BusEvent): boolean;
|
emit(event: "bus", data: BusEvent): boolean;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const DEFAULT_TIMMY: TimmyState = {
|
|||||||
|
|
||||||
const _state: WorldState = {
|
const _state: WorldState = {
|
||||||
timmyState: { ...DEFAULT_TIMMY },
|
timmyState: { ...DEFAULT_TIMMY },
|
||||||
agentStates: { alpha: "idle", beta: "idle", gamma: "idle", delta: "idle" },
|
agentStates: { alpha: "idle", beta: "idle", gamma: "idle", delta: "idle", kimi: "idle", perplexity: "idle" },
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,8 +34,10 @@ export function setAgentStateInWorld(agentId: string, agentState: string): void
|
|||||||
_deriveTimmy();
|
_deriveTimmy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WORKSHOP_AGENTS = ["alpha", "beta", "gamma", "delta"];
|
||||||
|
|
||||||
function _deriveTimmy(): void {
|
function _deriveTimmy(): void {
|
||||||
const states = Object.values(_state.agentStates);
|
const states = WORKSHOP_AGENTS.map(id => _state.agentStates[id] ?? "idle");
|
||||||
if (states.includes("working")) {
|
if (states.includes("working")) {
|
||||||
_state.timmyState.activity = "working";
|
_state.timmyState.activity = "working";
|
||||||
_state.timmyState.mood = "focused";
|
_state.timmyState.mood = "focused";
|
||||||
|
|||||||
@@ -269,6 +269,21 @@ function translateEvent(ev: BusEvent): object | null {
|
|||||||
text: ev.text,
|
text: ev.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── External agent state (Kimi, Perplexity) (#11) ─────────────────────────
|
||||||
|
case "agent:external_state": {
|
||||||
|
updateAgentWorld(ev.agentId, ev.state);
|
||||||
|
void logWorldEvent(
|
||||||
|
`agent:${ev.state}`,
|
||||||
|
`${ev.agentId} is now ${ev.state}${ev.taskSummary ? `: ${ev.taskSummary.slice(0, 80)}` : ""}`,
|
||||||
|
ev.agentId,
|
||||||
|
);
|
||||||
|
const msgs: object[] = [{ type: "agent_state", agentId: ev.agentId, state: ev.state }];
|
||||||
|
if (ev.taskSummary) {
|
||||||
|
msgs.push({ type: "agent_task_summary", agentId: ev.agentId, summary: ev.taskSummary });
|
||||||
|
}
|
||||||
|
return msgs;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,18 +5,27 @@
|
|||||||
* unused (x, z) position. No other file needs to be edited.
|
* unused (x, z) position. No other file needs to be edited.
|
||||||
*
|
*
|
||||||
* Fields:
|
* Fields:
|
||||||
* id — unique string key used in WebSocket messages and state maps
|
* id — unique string key used in WebSocket messages and state maps
|
||||||
* label — display name shown in the 3D HUD and chat panel
|
* label — display name shown in the 3D HUD and chat panel
|
||||||
* color — hex integer (0xRRGGBB) used for Three.js materials and lights
|
* color — hex integer (0xRRGGBB) used for Three.js materials and lights
|
||||||
* role — human-readable role string shown under the label sprite
|
* role — human-readable role string shown under the label sprite
|
||||||
* direction — cardinal facing direction (for future mesh orientation use)
|
* specialization — optional capability description shown in agent inspect card
|
||||||
* x, z — world-space position on the horizontal plane (y is always 0)
|
* direction — cardinal facing direction (for future mesh orientation use)
|
||||||
|
* x, z — world-space position on the horizontal plane (y is always 0)
|
||||||
*/
|
*/
|
||||||
export const AGENT_DEFS = [
|
export const AGENT_DEFS = [
|
||||||
{ id: 'alpha', label: 'ALPHA', color: 0x00ff88, role: 'orchestrator', direction: 'north', x: 0, z: -6 },
|
{ id: 'alpha', label: 'ALPHA', color: 0x00ff88, role: 'orchestrator', direction: 'north', x: 0, z: -6 },
|
||||||
{ id: 'beta', label: 'BETA', color: 0x00aaff, role: 'worker', direction: 'east', x: 6, z: 0 },
|
{ id: 'beta', label: 'BETA', color: 0x00aaff, role: 'worker', direction: 'east', x: 6, z: 0 },
|
||||||
{ id: 'gamma', label: 'GAMMA', color: 0xff6600, role: 'validator', direction: 'south', x: 0, z: 6 },
|
{ id: 'gamma', label: 'GAMMA', color: 0xff6600, role: 'validator', direction: 'south', x: 0, z: 6 },
|
||||||
{ id: 'delta', label: 'DELTA', color: 0xaa00ff, role: 'monitor', direction: 'west', x: -6, z: 0 },
|
{ id: 'delta', label: 'DELTA', color: 0xaa00ff, role: 'monitor', direction: 'west', x: -6, z: 0 },
|
||||||
|
{
|
||||||
|
id: 'kimi', label: 'KIMI', color: 0x00d4ff, role: 'analyst',
|
||||||
|
specialization: 'Long Context Analysis', direction: 'northwest', x: -10, z: -10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perplexity', label: 'PERPLEXITY', color: 0xff6b9d, role: 'researcher',
|
||||||
|
specialization: 'Real-time Research', direction: 'northeast', x: 10, z: -10,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ const CRYSTAL_POS = new THREE.Vector3(0.6, 1.15, -4.1);
|
|||||||
|
|
||||||
const agentStates = Object.fromEntries(AGENT_DEFS.map(d => [d.id, 'idle']));
|
const agentStates = Object.fromEntries(AGENT_DEFS.map(d => [d.id, 'idle']));
|
||||||
|
|
||||||
|
// Workshop agents that drive Timmy's mood (excludes external agents Kimi/Perplexity)
|
||||||
|
const WORKSHOP_AGENT_IDS = ['alpha', 'beta', 'gamma', 'delta'];
|
||||||
|
|
||||||
function deriveTimmyState() {
|
function deriveTimmyState() {
|
||||||
if (agentStates.gamma === 'working') return 'working';
|
if (agentStates.gamma === 'working') return 'working';
|
||||||
if (agentStates.beta === 'thinking' || agentStates.alpha === 'thinking') return 'thinking';
|
if (agentStates.beta === 'thinking' || agentStates.alpha === 'thinking') return 'thinking';
|
||||||
if (Object.values(agentStates).some(s => s !== 'idle')) return 'active';
|
if (WORKSHOP_AGENT_IDS.some(id => agentStates[id] !== 'idle')) return 'active';
|
||||||
return 'idle';
|
return 'idle';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +100,108 @@ function _pickMouthGeo(smileAmount) {
|
|||||||
|
|
||||||
// ── Build Timmy ───────────────────────────────────────────────────────────────
|
// ── Build Timmy ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ── External agent bodies (Kimi, Perplexity) ──────────────────────────────────
|
||||||
|
const _extBodies = {};
|
||||||
|
|
||||||
export function initAgents(sceneRef) {
|
export function initAgents(sceneRef) {
|
||||||
scene = sceneRef;
|
scene = sceneRef;
|
||||||
timmy = buildTimmy(scene);
|
timmy = buildTimmy(scene);
|
||||||
|
_initKimiBody(scene);
|
||||||
|
_initPerplexityBody(scene);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initKimiBody(sc) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.position.set(-10, 1.2, -10);
|
||||||
|
|
||||||
|
const mat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x00d4ff, emissive: 0x004466, emissiveIntensity: 0.4,
|
||||||
|
roughness: 0.15, metalness: 0.4,
|
||||||
|
});
|
||||||
|
const core = new THREE.Mesh(new THREE.OctahedronGeometry(0.38, 0), mat);
|
||||||
|
group.add(core);
|
||||||
|
|
||||||
|
const ringMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x00d4ff, emissive: 0x0088aa, emissiveIntensity: 0.6,
|
||||||
|
roughness: 0.1, metalness: 0.6, transparent: true, opacity: 0.7,
|
||||||
|
});
|
||||||
|
const ring1 = new THREE.Mesh(new THREE.TorusGeometry(0.60, 0.025, 6, 32), ringMat);
|
||||||
|
ring1.rotation.x = Math.PI / 3;
|
||||||
|
group.add(ring1);
|
||||||
|
|
||||||
|
const ring2 = new THREE.Mesh(new THREE.TorusGeometry(0.76, 0.018, 6, 32), ringMat.clone());
|
||||||
|
ring2.rotation.x = Math.PI / 2;
|
||||||
|
ring2.rotation.z = Math.PI / 4;
|
||||||
|
group.add(ring2);
|
||||||
|
|
||||||
|
const light = new THREE.PointLight(0x00d4ff, 0.5, 8);
|
||||||
|
group.add(light);
|
||||||
|
|
||||||
|
sc.add(group);
|
||||||
|
_extBodies.kimi = { group, core, ring1, ring2, light, mat, pulsePhase: Math.random() * Math.PI * 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initPerplexityBody(sc) {
|
||||||
|
const group = new THREE.Group();
|
||||||
|
group.position.set(10, 1.2, -10);
|
||||||
|
|
||||||
|
const mat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xff6b9d, emissive: 0x660033, emissiveIntensity: 0.4,
|
||||||
|
roughness: 0.2, metalness: 0.3,
|
||||||
|
});
|
||||||
|
const core = new THREE.Mesh(new THREE.IcosahedronGeometry(0.32, 0), mat);
|
||||||
|
group.add(core);
|
||||||
|
|
||||||
|
const scanMat = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xff6b9d, emissive: 0xaa2255, emissiveIntensity: 0.7,
|
||||||
|
roughness: 0.1, metalness: 0.5, transparent: true, opacity: 0.65,
|
||||||
|
});
|
||||||
|
const scanRings = [0, Math.PI / 3, -Math.PI / 3].map(angle => {
|
||||||
|
const r = new THREE.Mesh(new THREE.TorusGeometry(0.55, 0.022, 6, 28), scanMat.clone());
|
||||||
|
r.rotation.x = Math.PI / 2 + angle;
|
||||||
|
r.rotation.z = angle * 0.5;
|
||||||
|
group.add(r);
|
||||||
|
return r;
|
||||||
|
});
|
||||||
|
|
||||||
|
const light = new THREE.PointLight(0xff6b9d, 0.5, 8);
|
||||||
|
group.add(light);
|
||||||
|
|
||||||
|
sc.add(group);
|
||||||
|
_extBodies.perplexity = { group, core, scanRings, light, mat, pulsePhase: Math.random() * Math.PI * 2 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateExtBodies(t) {
|
||||||
|
_updateExtBody('kimi', t);
|
||||||
|
_updateExtBody('perplexity', t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateExtBody(id, t) {
|
||||||
|
const body = _extBodies[id];
|
||||||
|
if (!body) return;
|
||||||
|
const state = agentStates[id] || 'idle';
|
||||||
|
const isActive = state === 'working' || state === 'active';
|
||||||
|
const isThinking = state === 'thinking';
|
||||||
|
|
||||||
|
const speedMult = isActive ? 2.5 : isThinking ? 1.5 : 0.6;
|
||||||
|
const emissI = isActive ? 1.2 : isThinking ? 0.7 : 0.25;
|
||||||
|
const lightI = isActive ? 1.2 : isThinking ? 0.6 : 0.2;
|
||||||
|
const bobAmp = isActive ? 0.10 : 0.04;
|
||||||
|
|
||||||
|
body.group.position.y = 1.2 + Math.sin(t * 0.0008 + body.pulsePhase) * bobAmp;
|
||||||
|
body.mat.emissiveIntensity = emissI;
|
||||||
|
body.light.intensity = lightI;
|
||||||
|
|
||||||
|
if (id === 'kimi') {
|
||||||
|
body.core.rotation.y += 0.008 * speedMult;
|
||||||
|
body.core.rotation.x += 0.003 * speedMult;
|
||||||
|
body.ring1.rotation.z += 0.012 * speedMult;
|
||||||
|
body.ring2.rotation.x += 0.007 * speedMult;
|
||||||
|
} else {
|
||||||
|
body.core.rotation.y += 0.006 * speedMult;
|
||||||
|
body.core.rotation.z += 0.009 * speedMult;
|
||||||
|
body.scanRings.forEach((r, i) => { r.rotation.y += (0.015 + i * 0.008) * speedMult; });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTimmy(sc) {
|
function buildTimmy(sc) {
|
||||||
@@ -417,6 +519,7 @@ export function updateAgents(time) {
|
|||||||
const t = time * 0.001;
|
const t = time * 0.001;
|
||||||
const dt = _lastFrameTime > 0 ? Math.min((time - _lastFrameTime) * 0.001, 0.05) : 0.016;
|
const dt = _lastFrameTime > 0 ? Math.min((time - _lastFrameTime) * 0.001, 0.05) : 0.016;
|
||||||
_lastFrameTime = time;
|
_lastFrameTime = time;
|
||||||
|
_updateExtBodies(time);
|
||||||
|
|
||||||
const vs = deriveTimmyState();
|
const vs = deriveTimmyState();
|
||||||
const pulse = Math.sin(t * 1.8 + timmy.pulsePhase);
|
const pulse = Math.sin(t * 1.8 + timmy.pulsePhase);
|
||||||
@@ -889,5 +992,19 @@ export function disposeAgents() {
|
|||||||
timmy.bubbleTex?.dispose();
|
timmy.bubbleTex?.dispose();
|
||||||
timmy.bubbleMat?.dispose();
|
timmy.bubbleMat?.dispose();
|
||||||
timmy = null;
|
timmy = null;
|
||||||
|
|
||||||
|
// Dispose external agent bodies
|
||||||
|
for (const body of Object.values(_extBodies)) {
|
||||||
|
body.group.traverse(obj => {
|
||||||
|
if (obj.geometry) obj.geometry.dispose();
|
||||||
|
if (obj.material) {
|
||||||
|
const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
|
||||||
|
mats.forEach(m => m.dispose());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (scene) scene.remove(body.group);
|
||||||
|
}
|
||||||
|
for (const k of Object.keys(_extBodies)) delete _extBodies[k];
|
||||||
|
|
||||||
scene = null;
|
scene = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { colorToCss } from './agent-defs.js';
|
import { colorToCss, AGENT_DEFS } from './agent-defs.js';
|
||||||
|
|
||||||
|
// Specialization lookup built once from AGENT_DEFS
|
||||||
|
const _specializations = Object.fromEntries(
|
||||||
|
AGENT_DEFS.filter(d => d.specialization).map(d => [d.id, d.specialization])
|
||||||
|
);
|
||||||
|
|
||||||
const _proj = new THREE.Vector3();
|
const _proj = new THREE.Vector3();
|
||||||
let _camera = null;
|
let _camera = null;
|
||||||
@@ -20,6 +25,7 @@ let _labels = []; // { el, worldPos: THREE.Vector3, id }
|
|||||||
|
|
||||||
// ── State cache (updated from WS) ────────────────────────────────────────────
|
// ── State cache (updated from WS) ────────────────────────────────────────────
|
||||||
const _states = {};
|
const _states = {};
|
||||||
|
const _lastTasks = {};
|
||||||
|
|
||||||
// ── Inspect popup ─────────────────────────────────────────────────────────────
|
// ── Inspect popup ─────────────────────────────────────────────────────────────
|
||||||
let _inspectEl = null;
|
let _inspectEl = null;
|
||||||
@@ -100,6 +106,10 @@ function _makeLabel(container, id, name, role, color, worldPos) {
|
|||||||
return { el, worldPos, id, color };
|
return { el, worldPos, id, color };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setLabelLastTask(id, summary) {
|
||||||
|
_lastTasks[id] = summary;
|
||||||
|
}
|
||||||
|
|
||||||
export function setLabelState(id, state) {
|
export function setLabelState(id, state) {
|
||||||
_states[id] = state;
|
_states[id] = state;
|
||||||
const entry = _labels.find(l => l.id === id);
|
const entry = _labels.find(l => l.id === id);
|
||||||
@@ -118,13 +128,17 @@ export function showInspectPopup(id, screenX, screenY) {
|
|||||||
|
|
||||||
const state = _states[id] || 'idle';
|
const state = _states[id] || 'idle';
|
||||||
const uptime = Math.floor(performance.now() / 1000);
|
const uptime = Math.floor(performance.now() / 1000);
|
||||||
|
const spec = _specializations[id];
|
||||||
|
const lastTask = _lastTasks[id];
|
||||||
_inspectEl.innerHTML = `
|
_inspectEl.innerHTML = `
|
||||||
<div style="color:${entry.color};font-weight:bold;letter-spacing:2px;font-size:12px;margin-bottom:6px;">
|
<div style="color:${entry.color};font-weight:bold;letter-spacing:2px;font-size:12px;margin-bottom:6px;">
|
||||||
${id.toUpperCase()}
|
${id.toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
|
${spec ? `<div style="color:${entry.color}99;margin-bottom:4px;font-size:10px;letter-spacing:1px;">⬡ ${spec}</div>` : ''}
|
||||||
<div style="color:#aaa;margin-bottom:2px;">state : <span style="color:${entry.color}">${state}</span></div>
|
<div style="color:#aaa;margin-bottom:2px;">state : <span style="color:${entry.color}">${state}</span></div>
|
||||||
<div style="color:#aaa;margin-bottom:2px;">uptime : ${uptime}s</div>
|
<div style="color:#aaa;margin-bottom:2px;">uptime : ${uptime}s</div>
|
||||||
<div style="color:#aaa;">network: <span style="color:#44ff88">connected</span></div>
|
<div style="color:#aaa;margin-bottom:2px;">network: <span style="color:#44ff88">connected</span></div>
|
||||||
|
${lastTask ? `<div style="color:#888;font-size:9px;margin-top:4px;border-top:1px solid #333;padding-top:4px;">last: ${lastTask.slice(0, 60)}</div>` : ''}
|
||||||
`;
|
`;
|
||||||
_inspectEl.style.left = `${screenX}px`;
|
_inspectEl.style.left = `${screenX}px`;
|
||||||
_inspectEl.style.top = `${screenY}px`;
|
_inspectEl.style.top = `${screenY}px`;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { scene } from './world.js'; // Import the scene
|
|||||||
import { setAgentState, setSpeechBubble, applyAgentStates, setMood, TIMMY_WORLD_POS } from './agents.js';
|
import { setAgentState, setSpeechBubble, applyAgentStates, setMood, TIMMY_WORLD_POS } from './agents.js';
|
||||||
import { appendSystemMessage, appendDebateMessage, showCostTicker, updateCostTicker } from './ui.js';
|
import { appendSystemMessage, appendDebateMessage, showCostTicker, updateCostTicker } from './ui.js';
|
||||||
import { sentiment } from './edge-worker-client.js';
|
import { sentiment } from './edge-worker-client.js';
|
||||||
import { setLabelState } from './hud-labels.js';
|
import { setLabelState, setLabelLastTask } from './hud-labels.js';
|
||||||
import { createJobIndicator, dissolveJobIndicator } from './effects.js';
|
import { createJobIndicator, dissolveJobIndicator } from './effects.js';
|
||||||
import { getPubkey } from './nostr-identity.js';
|
import { getPubkey } from './nostr-identity.js';
|
||||||
|
|
||||||
@@ -122,11 +122,19 @@ function handleMessage(msg) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'agent_task_summary': {
|
||||||
|
if (msg.agentId && msg.summary) {
|
||||||
|
setLabelLastTask(msg.agentId, msg.summary);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'job_completed': {
|
case 'job_completed': {
|
||||||
if (jobCount > 0) jobCount--;
|
if (jobCount > 0) jobCount--;
|
||||||
if (msg.agentId) {
|
if (msg.agentId) {
|
||||||
setAgentState(msg.agentId, 'idle');
|
setAgentState(msg.agentId, 'idle');
|
||||||
setLabelState(msg.agentId, 'idle');
|
setLabelState(msg.agentId, 'idle');
|
||||||
|
setLabelLastTask(msg.agentId, `job ${(msg.jobId || '').slice(0, 8)} completed`);
|
||||||
}
|
}
|
||||||
appendSystemMessage(`job ${(msg.jobId || '').slice(0, 8)} complete`);
|
appendSystemMessage(`job ${(msg.jobId || '').slice(0, 8)} complete`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user