一键导入
adapter-architecture-pattern
How the multi-backend adapter system works — AgentAdapter interface, ACP protocol, session resume, and how to add new provider adapters
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
How the multi-backend adapter system works — AgentAdapter interface, ACP protocol, session resume, and how to add new provider adapters
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
When debugging why an agent has the wrong provider/model, or when modifying agent creation or delegation flows.
How to get agents to use group chats for peer coordination in flightdeck-based crews. Covers hub-and-spoke anti-pattern, auto-grouping triggers, and file-lock-linked groups. Use when setting up multi-agent crews with 3+ agents or diagnosing why agents aren't collaborating directly.
When fetching from the `/coordination/activity` API and you only need specific action types (e.g., `progress_update`, `task_completed`, `delegated`).
When fetching activity data for UI display, or debugging why activity-based features show empty/stale data.
Architecture decisions and patterns from Flightdeck development (Phases 2–4). Covers feature architecture, state management, component patterns, and API design.
Known bugs and root causes encountered during Flightdeck development. Check this list before debugging — you may be hitting a known issue.
| name | adapter-architecture-pattern |
| description | How the multi-backend adapter system works — AgentAdapter interface, ACP protocol, session resume, and how to add new provider adapters |
Flightdeck supports multiple AI CLI providers (Copilot, Claude, Gemini, Cursor, Codex, OpenCode) through a unified adapter system. All providers use the ACP (Agent Communication Protocol) over stdio. Understanding this architecture is essential when adding new providers, debugging adapter issues, or modifying the agent spawn pipeline.
AdapterFactory.createAdapterForProvider(config)
│
├── provider='mock' → MockAdapter (testing)
└── all providers → AcpAdapter (subprocess via ACP stdio)
Two adapter backends implement the same AgentAdapter interface:
| Backend | Transport | Process Model | Session Resume | Providers |
|---|---|---|---|---|
| AcpAdapter | ACP protocol over stdio/ndjson | Subprocess (spawned CLI) | loadSession() — throws on failure so the UI can surface the error | Copilot, Claude, Gemini, OpenCode, Cursor, Codex |
| MockAdapter | In-memory | In-process | N/A | Testing only |
packages/server/src/adapters/
├── types.ts # AgentAdapter interface (THE contract)
├── AdapterFactory.ts # Factory: resolveBackend() + createAdapterForProvider()
├── AcpAdapter.ts # Subprocess adapter — the single production adapter
├── MockAdapter.ts # Test adapter
├── presets.ts # Provider presets (binary, args, env, supportsResume)
├── ModelResolver.ts # Cross-provider model resolution + tier aliases
├── RoleFileWriter.ts # Per-provider agent file writers
├── index.ts # Barrel exports
└── __tests__/ # Test suites
Every adapter must implement this interface (defined in types.ts):
interface AgentAdapter extends EventEmitter {
readonly type: string; // 'acp' or 'mock'
readonly isConnected: boolean;
readonly isPrompting: boolean;
readonly promptingStartedAt: number | null;
readonly currentSessionId: string | null;
readonly supportsImages: boolean;
start(opts: AdapterStartOptions): Promise<string>; // Returns sessionId
prompt(content: PromptContent, opts?: PromptOptions): Promise<PromptResult>;
cancel(): Promise<void>;
terminate(): void;
resolvePermission(approved: boolean): void;
}
'connected' (sessionId) — adapter ready'text' (string) — assistant output text'thinking' (string) — reasoning/thinking output'tool_call' (ToolCallInfo) — tool execution started'tool_call_update' (ToolUpdateInfo) — tool execution completed/failed'usage' (UsageInfo) — token usage report'prompting' (boolean) — prompt in progress'prompt_complete' (StopReason) — prompt finished'response_start' — new response beginning'permission_request' (PermissionRequest) — needs user approval'idle' — ready for next prompt'exit' (code) — adapter terminatedSession resume is handled via the ACP protocol. When loadSession() fails, the error is propagated so the caller can surface it to the user:
// AcpAdapter — throws on failure for UI visibility
try {
const result = await connection.loadSession({ sessionId });
} catch (err) {
// Emits 'session_resume_failed' for observability, then throws.
// Does NOT fall back to newSession() — the UI should show the error.
throw new Error(`Session resume failed: ${err instanceof Error ? err.message : String(err)}`);
}
Whether resume succeeds depends on the CLI provider's support. Some providers persist full conversation history, others don't. When resume fails, the error is surfaced to the user rather than silently creating a fresh session.
The SessionResumeManager (in packages/server/src/agents/) orchestrates resume on server startup. It reads the agent roster, checks preset.supportsResume, and passes sessionId through AdapterStartOptions.
All adapters share the same concurrent prompt handling:
async prompt(content, opts?) {
if (this._isPrompting) {
// Queue the prompt — only one active prompt at a time
if (opts?.priority) {
this.promptQueue.splice(this.promptQueuePriorityCount, 0, content);
this.promptQueuePriorityCount++;
} else {
this.promptQueue.push(content);
}
return { stopReason: 'end_turn' };
}
// ... execute prompt, then drainQueue() on completion
}
AcpAdapter translates the CLI's permission model to a common flow:
'permission_request' eventadapter.resolvePermission(approved) calledIn autopilot mode, permissions are auto-approved without user interaction.
Adding a new ACP-compatible provider requires ~30 lines of configuration:
presets.ts:myProvider: {
id: 'myProvider',
name: 'My Provider',
binary: 'my-cli',
args: ['--acp', '--stdio'],
transport: 'stdio',
supportsResume: false,
modelFlag: '--model',
defaultModel: 'my-model-v1',
}
ProviderId union typeRoleFileWriter if the provider uses agent filesModelResolver.ts if neededNo adapter code changes are needed — the AcpAdapter handles all providers via presets.
resolveBackend() + presets, not if/else chains