Files
open-multi-agent-open-multi…/packages/core/tests/multi-perspective-code-review.test.ts
Jack Chen ffb2905017 feat(agent): orchestrate external coding agents over ACP (#204) (#360)
* feat(agent): orchestrate external coding agents over ACP (#204)

Add an AgentBackend seam so an Agent can run on either the LLM AgentRunner
or an external agent. First backend: an ACP client that drives a coding CLI
(e.g. Claude Code via its ACP adapter) as a first-class team member — same
task DAG, shared memory, cascade, and token budget as LLM agents.

- Extract AgentBackend ({ run, stream }) that AgentRunner already implements;
  Agent.getBackend() branches on AgentConfig.backend.
- AcpBackend spawns the agent over stdio JSON-RPC and maps session updates to
  a RunResult (text, tool calls, usage); handles permissions and abort.
- ACP SDK as a lazy optional peer; new /acp subpath export.
- docs/external-agents.md, a hybrid-team example, and tests (in-process fake
  agent + subprocess integration).

Experimental: ACP is young; token accounting maps ACP's single context
figure and client-side fs proxying is not implemented yet.

* fix(agent): harden ACP backend after review (#204)

Follow-up fixes on the ACP external-agent backend (PR #360):

- Token accounting: usage_update.used is a cumulative context-size gauge, not a
  per-turn delta. Record each turn's increment so summing across a reused session
  telescopes to the latest figure instead of double-counting.
- systemPrompt: ACP has no system-prompt field, so prepend the agent's systemPrompt
  to the session's first turn (once). It was previously dropped and only fed
  coordinator routing.
- Abort: when the signal is already aborted before a turn starts, return empty
  output without prompting (no in-flight turn to cancel).
- Startup: clear the cached start() promise on failure so a later run retries
  instead of re-throwing the same rejection forever.
- Permissions: auto-approve now prefers least-privilege allow_once over a
  session-wide allow_always.

Docs (external-agents.md, CLAUDE.md) updated to match. ACP tests 10 to 15;
lint + full suite (1135) green.
2026-07-10 23:26:40 +08:00

89 lines
2.7 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { z } from 'zod'
import { Agent } from '../src/agent/agent.js'
import { AgentRunner } from '../src/agent/runner.js'
import { ToolRegistry } from '../src/tool/framework.js'
import { ToolExecutor } from '../src/tool/executor.js'
import type { AgentConfig, LLMAdapter, LLMResponse } from '../src/types.js'
const ReviewFindings = z.array(
z.object({
priority: z.enum(['critical', 'high', 'medium', 'low']),
category: z.enum(['security', 'performance', 'style']),
issue: z.string(),
fix_hint: z.string(),
}),
)
function mockAdapter(responses: string[]): LLMAdapter {
let callIndex = 0
return {
name: 'mock',
async chat() {
const text = responses[callIndex++] ?? ''
return {
id: `mock-${callIndex}`,
content: [{ type: 'text' as const, text }],
model: 'mock-model',
stop_reason: 'end_turn',
usage: { input_tokens: 10, output_tokens: 20 },
} satisfies LLMResponse
},
async *stream() {
/* unused in this test */
},
}
}
function buildMockAgent(config: AgentConfig, responses: string[]): Agent {
const adapter = mockAdapter(responses)
const registry = new ToolRegistry()
const executor = new ToolExecutor(registry)
const agent = new Agent(config, registry, executor)
const runner = new AgentRunner(adapter, registry, executor, {
model: config.model,
systemPrompt: config.systemPrompt,
maxTurns: config.maxTurns,
maxTokens: config.maxTokens,
temperature: config.temperature,
agentName: config.name,
})
;(agent as any).backend = runner
return agent
}
describe('multi-perspective code review example', () => {
it('returns structured findings that match the issue schema', async () => {
const config: AgentConfig = {
name: 'synthesizer',
model: 'mock-model',
systemPrompt: 'Return structured review findings.',
outputSchema: ReviewFindings,
}
const findings = [
{
priority: 'critical',
category: 'security',
issue: 'User input is interpolated directly into the SQL query.',
fix_hint: 'Use parameterized queries for all database writes.',
},
{
priority: 'medium',
category: 'style',
issue: 'Error responses use inconsistent wording across branches.',
fix_hint: 'Standardize error messages and response payload structure.',
},
]
const agent = buildMockAgent(config, [JSON.stringify(findings)])
const result = await agent.run('Synthesize the reviewers into structured findings.')
expect(result.success).toBe(true)
expect(Array.isArray(result.structured)).toBe(true)
expect(ReviewFindings.parse(result.structured)).toEqual(findings)
})
})