feat(coder): v2.6 Phase 2 — warm ACP backend for goose/qwen
WarmAcpBackend (AgentBackend) holds one persistent goose acp / qwen --acp child + ClientSideConnection + ACP session per (chat,agent); initialize+session/new once, reused across turns. Abort = session/cancel the prompt only (never kills the child); child exit -> agent_sessions.status='crashed' -> re-spawn next turn. Dispatcher routes goose/qwen chat-tab tasks to the pooled warm backend via pure shouldUseWarmBackend (needs session_id+chat_id); one-shot runExternalAgent kept as fallback for arena/MCP/new_task. handleSessionUpdate extracted to a shared pure acp-event-map.ts (one-shot path byte-identical). SDK: installed @agentclientprotocol/sdk@^0.22.1 has stable resumeSession/loadSession; resume moot in the warm hot path, deferred to Phase 3. 15 new tests (warm-acp-routing, acp-event-map); 180 coder tests pass; tsc + build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { shouldUseWarmBackend, isTurnOkForStopReason } from '../warm-acp-routing.js';
|
||||
|
||||
/**
|
||||
* Phase 2 routing predicate: which goose/qwen tasks go to the warm pool backend
|
||||
* vs the existing one-shot ACP path.
|
||||
*
|
||||
* The warm backend is keyed (chat_id, agent) — the persistent context unit (same
|
||||
* as opencode-server). A task only routes warm when it carries BOTH a session_id
|
||||
* and a chat_id, i.e. it originates from a real chat tab (the coder message route
|
||||
* stamps both). Session-less creators (arena, MCP-created, generic /api/tasks,
|
||||
* new_task) lack chat_id/session_id and keep the one-shot worktree-per-task path,
|
||||
* which never spawns a warm process.
|
||||
*/
|
||||
describe('shouldUseWarmBackend (Phase 2 routing)', () => {
|
||||
it('routes a chat-tab task (session_id + chat_id) to the warm backend', () => {
|
||||
expect(shouldUseWarmBackend({ agent: 'qwen', session_id: 's1', chat_id: 'c1' })).toBe(true);
|
||||
expect(shouldUseWarmBackend({ agent: 'goose', session_id: 's1', chat_id: 'c1' })).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps a session-less arena/MCP task on the one-shot path', () => {
|
||||
expect(shouldUseWarmBackend({ agent: 'qwen', session_id: null, chat_id: null })).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps a task with a session but no chat on the one-shot path', () => {
|
||||
// chat_id is the warm-key half; without it ensureSession would get a degenerate
|
||||
// (null, agent) key, so fall back to one-shot rather than synthesize a chat.
|
||||
expect(shouldUseWarmBackend({ agent: 'goose', session_id: 's1', chat_id: null })).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps a task with a chat but no session on the one-shot path', () => {
|
||||
expect(shouldUseWarmBackend({ agent: 'qwen', session_id: null, chat_id: 'c1' })).toBe(false);
|
||||
});
|
||||
|
||||
it('only applies to warm-capable agents (goose, qwen); others never warm here', () => {
|
||||
// opencode has its own dedicated warm path; native/claude/etc. are not ACP-warm.
|
||||
expect(shouldUseWarmBackend({ agent: 'opencode', session_id: 's1', chat_id: 'c1' })).toBe(false);
|
||||
expect(shouldUseWarmBackend({ agent: 'claude', session_id: 's1', chat_id: 'c1' })).toBe(false);
|
||||
expect(shouldUseWarmBackend({ agent: null, session_id: 's1', chat_id: 'c1' })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isTurnOkForStopReason (ACP stop-reason → ok/fail)', () => {
|
||||
it('treats normal completions as ok', () => {
|
||||
expect(isTurnOkForStopReason('end_turn')).toBe(true);
|
||||
expect(isTurnOkForStopReason('max_tokens')).toBe(true);
|
||||
expect(isTurnOkForStopReason('max_turn_requests')).toBe(true);
|
||||
});
|
||||
|
||||
it('treats refusal and cancelled as failures', () => {
|
||||
expect(isTurnOkForStopReason('refusal')).toBe(false);
|
||||
expect(isTurnOkForStopReason('cancelled')).toBe(false);
|
||||
});
|
||||
|
||||
it('defaults an absent stop reason to a successful end_turn', () => {
|
||||
expect(isTurnOkForStopReason(undefined)).toBe(true);
|
||||
expect(isTurnOkForStopReason(null)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user