v2 phase 2: agent-runner — provider interface, poll loop, formatter

AgentProvider abstraction with Claude and Mock implementations.
Poll loop reads messages_in, formats by kind, queries provider,
writes results to messages_out. Concurrent polling pushes follow-up
messages into active queries.

- providers/types.ts: AgentProvider, AgentQuery, ProviderEvent
- providers/claude.ts: wraps Agent SDK with MessageStream, hooks,
  transcript archiving
- providers/mock.ts: canned responses with push() support
- providers/factory.ts: createProvider()
- formatter.ts: format by kind (chat/task/webhook/system), XML
  escaping, routing extraction
- poll-loop.ts: poll → format → query → write, concurrent polling
- mcp-tools.ts: MCP server with send_message tool
- index-v2.ts: new entry point (config from env, enters poll loop)
- 11 new tests, all 288 tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-04-08 23:36:55 +03:00
parent 3f0451b7b0
commit 5a0098edc9
10 changed files with 1045 additions and 1 deletions

View File

@@ -0,0 +1,66 @@
import type { AgentProvider, AgentQuery, ProviderEvent, QueryInput } from './types.js';
/**
* Mock provider for testing. Returns canned responses.
* Supports push() — queued messages produce additional results.
*/
export class MockProvider implements AgentProvider {
private responseFactory: (prompt: string) => string;
constructor(responseFactory?: (prompt: string) => string) {
this.responseFactory = responseFactory ?? ((prompt) => `Mock response to: ${prompt.slice(0, 100)}`);
}
query(input: QueryInput): AgentQuery {
const pending: string[] = [];
let waiting: (() => void) | null = null;
let ended = false;
let aborted = false;
const responseFactory = this.responseFactory;
const events: AsyncIterable<ProviderEvent> = {
async *[Symbol.asyncIterator]() {
yield { type: 'init', sessionId: `mock-session-${Date.now()}` };
// Process initial prompt
yield { type: 'result', text: responseFactory(input.prompt) };
// Process any pushed follow-ups
while (!ended && !aborted) {
if (pending.length > 0) {
const msg = pending.shift()!;
yield { type: 'result', text: responseFactory(msg) };
continue;
}
// Wait for push() or end()
await new Promise<void>((resolve) => {
waiting = resolve;
});
waiting = null;
}
// Drain remaining
while (pending.length > 0) {
const msg = pending.shift()!;
yield { type: 'result', text: responseFactory(msg) };
}
},
};
return {
push(message: string) {
pending.push(message);
waiting?.();
},
end() {
ended = true;
waiting?.();
},
events,
abort() {
aborted = true;
waiting?.();
},
};
}
}