P2c: in-app choir agent with proposals and approval

This commit is contained in:
Emil
2026-11-01 02:27:44 +03:00
parent 0df07577a1
commit 2ecb8822de
20 changed files with 2236 additions and 30 deletions
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env node
// Mock choir: ask, get a proposal, approve it, see the tempo move.
const ws = new WebSocket('ws://127.0.0.1:4317');
const pending = new Map();
const waiters = [];
let proposal = null;
let resolved = null;
let history = null;
const waitFor = (predicate) => new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('timeout')), 20000);
waiters.push({ predicate, resolve: (value) => { clearTimeout(timer); resolve(value); } });
const existing = messages.find(predicate);
if (existing) resolve(existing);
});
const messages = [];
ws.onopen = () => ws.send(JSON.stringify({ type: 'hello', protocol: 1, token: 'dev', client: 'agent-smoke' }));
ws.onmessage = (message) => {
if (typeof message.data !== 'string') return;
const parsed = JSON.parse(message.data);
messages.push(parsed);
if (parsed.type === 'agent_history') history = parsed.messages;
if (parsed.type === 'event' && parsed.event?.kind === 'agent_proposal') proposal = parsed.event;
if (parsed.type === 'event' && parsed.event?.kind === 'agent_resolved') resolved = parsed.event;
if (parsed.type === 'result') pending.set(parsed.id, parsed.result);
for (const waiter of [...waiters]) {
if (waiter.predicate(parsed)) { waiters.splice(waiters.indexOf(waiter), 1); waiter.resolve(parsed); }
}
};
const send = (id, command) => ws.send(JSON.stringify({ type: 'command', id, command }));
await waitFor((message) => message.type === 'snapshot');
send(1, { type: 'agent_ask', text: 'raise the tempo a little' });
await waitFor((message) => message.type === 'event' && message.event?.kind === 'agent_proposal');
if (!proposal) throw new Error('no proposal');
if (!proposal.commands.some((command) => command.type === 'set_tempo')) throw new Error('expected a tempo proposal');
const before = pending.get(1)?.state?.transport?.bpm;
send(2, { type: 'agent_approve', id: proposal.id });
await waitFor((message) => message.type === 'event' && message.event?.kind === 'agent_resolved');
const state = pending.get(2)?.state;
if (!resolved?.applied) throw new Error('proposal was not applied');
if (state?.transport?.bpm !== 132) throw new Error(`tempo did not move: ${state?.transport?.bpm}`);
const userLines = messages.filter((message) => message.type === 'event' && message.event?.kind === 'agent_message' && message.event.message.role === 'user').length;
const agentLines = messages.filter((message) => message.type === 'event' && message.event?.kind === 'agent_message' && message.event.message.role === 'agent').length;
console.log(JSON.stringify({
history: history?.length ?? 0,
proposal: { id: proposal.id, commands: proposal.commands, description: proposal.description },
resolved,
bpm: state.transport.bpm,
userLines,
agentLines,
journal: state.journal_len
}, null, 2));
ws.close();
console.log('agent smoke passed');