This commit was merged in pull request #94.
This commit is contained in:
@@ -376,6 +376,72 @@ Respond ONLY with valid JSON: {"accepted": true/false, "reason": "..."}`,
|
||||
outputTokens: totalOutput,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a short, character-appropriate commentary line for an agent during
|
||||
* a given phase of the job lifecycle. Uses Haiku (evalModel) with a 60-token
|
||||
* cap so replies are always a single sentence. Errors are swallowed.
|
||||
*
|
||||
* In STUB_MODE returns a canned string so the full flow can be exercised
|
||||
* without an Anthropic API key.
|
||||
*/
|
||||
async generateCommentary(agentId: string, phase: string, context?: string): Promise<string> {
|
||||
const STUB_COMMENTARY: Record<string, Record<string, string>> = {
|
||||
alpha: {
|
||||
routing: "Routing job to Gamma for execution.",
|
||||
complete: "Job complete. Returning to standby.",
|
||||
rejected: "Request rejected by Beta. Standing down.",
|
||||
},
|
||||
beta: {
|
||||
evaluating: "Reviewing your request for clarity and ethics.",
|
||||
assessed: "Evaluation complete.",
|
||||
},
|
||||
gamma: {
|
||||
starting: "Analysing the task. Ready to work.",
|
||||
working: "Working on your request now.",
|
||||
done: "Work complete. Delivering output.",
|
||||
},
|
||||
delta: {
|
||||
eval_paid: "⚡ Eval payment confirmed.",
|
||||
work_paid: "⚡ Work payment confirmed. Unlocking execution.",
|
||||
},
|
||||
};
|
||||
|
||||
if (STUB_MODE) {
|
||||
return STUB_COMMENTARY[agentId]?.[phase] ?? `${agentId}: ${phase}`;
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPTS: Record<string, string> = {
|
||||
alpha: "You are Alpha, the orchestrator AI. You give ultra-brief status updates (max 10 words) about job routing and lifecycle. Be direct and professional.",
|
||||
beta: "You are Beta, the evaluator AI. You give ultra-brief status updates (max 10 words) about evaluating a request. Be analytical.",
|
||||
gamma: "You are Gamma, the worker AI. You give ultra-brief status updates (max 10 words) about executing a task. Be focused and capable.",
|
||||
delta: "You are Delta, the payment AI. You give ultra-brief status updates (max 10 words) about Lightning payment confirmations. Start with ⚡",
|
||||
};
|
||||
|
||||
const systemPrompt = SYSTEM_PROMPTS[agentId];
|
||||
if (!systemPrompt) return "";
|
||||
|
||||
try {
|
||||
const client = await getClient();
|
||||
const message = await client.messages.create({
|
||||
model: this.evalModel,
|
||||
max_tokens: 60,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: `Narrate your current phase: ${phase}${context ? `. Context: ${context}` : ""}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
const block = message.content[0];
|
||||
if (block?.type === "text") return block.text!.trim();
|
||||
return "";
|
||||
} catch (err) {
|
||||
logger.warn("generateCommentary failed", { agentId, phase, err: String(err) });
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const agentService = new AgentService();
|
||||
|
||||
@@ -18,7 +18,10 @@ export type DebateEvent =
|
||||
export type CostEvent =
|
||||
| { type: "cost:update"; jobId: string; sats: number; phase: "eval" | "work" | "session"; isFinal: boolean };
|
||||
|
||||
export type BusEvent = JobEvent | SessionEvent | DebateEvent | CostEvent;
|
||||
export type CommentaryEvent =
|
||||
| { type: "agent_commentary"; agentId: string; jobId: string; text: string };
|
||||
|
||||
export type BusEvent = JobEvent | SessionEvent | DebateEvent | CostEvent | CommentaryEvent;
|
||||
|
||||
class EventBus extends EventEmitter {
|
||||
emit(event: "bus", data: BusEvent): boolean;
|
||||
|
||||
@@ -257,6 +257,15 @@ function translateEvent(ev: BusEvent): object | null {
|
||||
isFinal: ev.isFinal,
|
||||
};
|
||||
|
||||
// ── Agent commentary (#1) ─────────────────────────────────────────────────
|
||||
case "agent_commentary":
|
||||
return {
|
||||
type: "agent_commentary",
|
||||
agentId: ev.agentId,
|
||||
jobId: ev.jobId,
|
||||
text: ev.text,
|
||||
};
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -389,5 +398,50 @@ export function attachWebSocketServer(server: Server): void {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Global commentary listener (set up once per server, not per socket) ────
|
||||
// Watches job lifecycle events and fires Haiku commentary to all clients.
|
||||
eventBus.on("bus", (ev: BusEvent) => {
|
||||
let agentId: string | null = null;
|
||||
let phase: string | null = null;
|
||||
let jobId: string | null = null;
|
||||
|
||||
if (ev.type === "job:state") {
|
||||
jobId = ev.jobId;
|
||||
if (ev.state === "evaluating") {
|
||||
// Beta evaluating + Alpha routing
|
||||
void (async () => {
|
||||
const [betaText, alphaText] = await Promise.all([
|
||||
agentService.generateCommentary("beta", "evaluating"),
|
||||
agentService.generateCommentary("alpha", "routing"),
|
||||
]);
|
||||
if (betaText) broadcastToAll(wss, { type: "agent_commentary", agentId: "beta", jobId, text: betaText });
|
||||
if (alphaText) broadcastToAll(wss, { type: "agent_commentary", agentId: "alpha", jobId, text: alphaText });
|
||||
})();
|
||||
return;
|
||||
}
|
||||
if (ev.state === "executing") {
|
||||
agentId = "gamma"; phase = "starting";
|
||||
} else if (ev.state === "complete") {
|
||||
agentId = "alpha"; phase = "complete";
|
||||
} else if (ev.state === "rejected") {
|
||||
agentId = "alpha"; phase = "rejected";
|
||||
}
|
||||
} else if (ev.type === "job:paid") {
|
||||
jobId = ev.jobId;
|
||||
agentId = "delta";
|
||||
phase = ev.invoiceType === "eval" ? "eval_paid" : "work_paid";
|
||||
}
|
||||
|
||||
if (agentId && phase && jobId) {
|
||||
const capturedAgentId = agentId;
|
||||
const capturedPhase = phase;
|
||||
const capturedJobId = jobId;
|
||||
void (async () => {
|
||||
const text = await agentService.generateCommentary(capturedAgentId, capturedPhase);
|
||||
if (text) broadcastToAll(wss, { type: "agent_commentary", agentId: capturedAgentId, jobId: capturedJobId, text });
|
||||
})();
|
||||
}
|
||||
});
|
||||
|
||||
logger.info("WebSocket server attached at /api/ws");
|
||||
}
|
||||
|
||||
@@ -151,6 +151,15 @@ function handleMessage(msg) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'agent_commentary': {
|
||||
// Agent narration during job lifecycle
|
||||
if (msg.text) {
|
||||
setSpeechBubble(msg.text);
|
||||
appendSystemMessage(`${msg.agentId}: ${(msg.text || '').slice(0, 80)}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'agent_count':
|
||||
case 'visitor_count':
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user