42 KiB
Agent Adapters
Parent: spec.md · Siblings: architecture.md · skills-protocol.md · new-agent-runtime-acp.md · modes.md
The adapter layer is OD's most load-bearing design decision. We delegate the entire agent loop — model calls, tool use, context management, permission handling, resume, cancel — to the user's existing code agent CLI. OD's job is to detect it, feed it a skill + prompt + working directory, and stream its output back to the web UI.
If you're adding a new ACP-backed runtime, start with new-agent-runtime-acp.md for the expected stdio transport, JSON-RPC message flow, and process lifecycle contract.
Thesis: The code agent space has already converged on strong implementations (Claude Code, Codex, Devin for Terminal, Cursor Agent, OpenCode, Qoder CLI, and others). Reimplementing another one is worse than talking to all of them.
Inspiration: multica (PATH-scan detection + daemon architecture) and cc-switch (per-agent config format knowledge + symlink-based skill distribution).
1. Adapter contract: a data spec, not a class
An adapter is not a class that implements the agent loop. It is a plain data object — one RuntimeAgentDef object literal per CLI — that declares how to talk to that CLI: which binary to probe, how to build its argv, how it streams, what it can do. A generic engine reads those fields and does the detecting, launching, invoking, and stream-parsing for every agent uniformly. There is no per-agent subclass and no run() / cancel() method to implement.
Where the pieces live (all under apps/daemon/src/):
- The contract (the data spec):
runtimes/types.ts— theRuntimeAgentDeftype. - One def per CLI:
runtimes/defs/*.ts—claude.ts,codex.ts,cursor-agent.ts,devin.ts, … each exports a single object literal. - The registry (a unique-id array):
runtimes/registry.ts—BASE_AGENT_DEFScollects every def intoAGENT_DEFS; a boot-time loop throws on any duplicateid. - The generic engine (zero per-agent code):
detection.ts,capabilities.ts,executables.ts/resolution.ts,launch.ts,invocation.ts,env.ts,mcp.ts,models.ts,prompt-budget.tsunderruntimes/, plus the stream dispatch inserver.tsthat routes each def'sstreamFormat/eventParserto the matching*-stream.tsparser. - The public barrel:
agents.tsre-exportsAGENT_DEFS,getAgentDef,detectAgents,resolveAgentLaunch, … fromruntimes/. It defines nothing itself — import from it for convenience, but readruntimes/for the contract.
Adding a CLI is a one-file change. Drop a new
runtimes/defs/<cli>.tsexporting oneRuntimeAgentDef, add it to theBASE_AGENT_DEFSarray inregistry.ts, and the engine detects, launches, invokes, and (for an existingstreamFormat) streams it — no engine edits, no new class, no method overrides. The def is config; the loop is shared. A genuinely new wire format is the only case that also adds an engine file (a new*-stream.tsand astreamFormatvalue).
The data spec (RuntimeAgentDef, abbreviated)
The full type lives in runtimes/types.ts; the load-bearing fields:
type RuntimeAgentDef = {
id: string; // unique key, e.g. "claude" | "codex" — the registry dedupes on it
name: string; // display name
bin: string; // CLI executable to probe on PATH
fallbackBins?: string[]; // alternate executable names
versionArgs: string[]; // args for the version / detection probe
fallbackModels: RuntimeModelOption[];
// How to invoke: build the argv for one turn from the composed prompt.
buildArgs: (
prompt: string,
imagePaths: string[],
extraAllowedDirs?: string[],
options?: RuntimeBuildOptions,
runtimeContext?: RuntimeContext,
) => string[];
// How it talks back: the engine dispatches these to the matching parser.
streamFormat: string; // e.g. "claude-stream-json" | "acp-json-rpc" | "plain"
eventParser?: string; // named parser, e.g. "codex" | "cursor-agent" | "opencode"
// How the prompt is delivered.
promptViaStdin?: boolean;
promptViaFile?: boolean;
promptInputFormat?: 'text' | 'stream-json';
// Optional capability / integration declarations (all data, no behavior).
supportsImagePaths?: boolean;
externalMcpInjection?: 'claude-mcp-json' | 'acp-merge' | 'opencode-env-content';
authProbe?: { args: string[]; timeoutMs?: number };
listModels?: RuntimeListModels; // dynamic model discovery
// …~30 more optional fields, every one data or a pure arg-builder.
};
Every field is data or a pure arg-builder — there is no run(), no cancel(), no subclass. Capabilities, detection, cancellation, and streaming are the engine's job, driven off these declarations, which is why a new agent needs only a new object rather than a new code path.
A concrete def (shape)
// runtimes/defs/acme.ts (illustrative — a made-up CLI, not a shipped def)
export const acmeAgentDef: RuntimeAgentDef = {
id: 'acme',
name: 'Acme CLI',
bin: 'acme',
versionArgs: ['--version'],
fallbackModels: [{ id: 'acme-pro', label: 'Acme Pro' }],
streamFormat: 'claude-stream-json', // reuse an existing parser — no engine change
promptViaStdin: true,
buildArgs: (prompt, imagePaths, extraDirs, opts) => [
'--output-format', 'stream-json',
/* … */
],
};
The registry (a unique-id array)
// runtimes/registry.ts
const BASE_AGENT_DEFS: RuntimeAgentDef[] = [
claudeAgentDef, codexAgentDef, devinAgentDef, cursorAgentDef,
/* … one entry per CLI (roughly two dozen today) … */
];
// boot-time invariant: no two defs may share an id
const ids = new Set<string>();
for (const def of AGENT_DEFS) {
if (ids.has(def.id)) throw new Error(`Duplicate agent definition id: ${def.id}`);
ids.add(def.id);
}
AGENT_DEFS = BASE_AGENT_DEFS plus any user-defined local profiles (readLocalAgentProfileDefs), and getAgentDef(id) is the lookup the rest of the daemon uses. The event set the *-stream.ts parsers emit onto the UI stream (thinking / tool-call / tool-result / text-delta / file-write / error / done) is defined by those parsers, not by the def — see §11 for where they live and server.ts for the dispatch.
2. Detection strategy
detectAgents() and detectAgentsStream() probe all registered definitions in
parallel whenever they are invoked. The daemon warms detection at startup, and
agent-list/run paths invoke it again when they need fresh availability or model
data; there is no persisted 24-hour detection result.
For each definition, detection:
- Calls
resolveAgentLaunch()so it probes the same configured, fallback, or packaged executable path that a run will actually spawn—not merely the first PATH-visible shim. - Runs the definition's version probe. An OS-level missing or non-executable result marks the adapter unavailable; a CLI that launches but rejects its version flag remains available with no version string.
- After availability is established, runs help/capability, model-discovery,
and declared auth probes concurrently. Definitions without
authProbeare not assigned a synthetic auth failure from a config-directory guess.
Each adapter probe is fault-isolated so one broken executable cannot empty the whole picker. Capability flags and recently discovered live models are retained in process for invocation/model validation, and each detection pass refreshes them.
3. Shipped adapter catalog
The authoritative list is BASE_AGENT_DEFS in
runtimes/registry.ts. The shipped
definitions currently group by transport as follows:
| Stream format | Runtime ids |
|---|---|
claude-stream-json |
claude, amp, codebuddy |
json-event-stream |
codex, cursor-agent, opencode, mimo, byok-opencode |
copilot-stream-json |
copilot |
qoder-stream-json |
qoder |
acp-json-rpc |
amr (Vela), devin, hermes, kimi, kiro, kilo, reasonix, trae-cli, vibe |
pi-rpc |
pi |
dsh-profile-jsonl |
deepseek-harness |
plain |
aider, antigravity, atomcode, deepseek, grok-build, qwen |
byok-opencode is the API-backed OpenCode-compatible profile rather than an
additional local executable. User-defined local profiles may extend the base
registry at runtime. Gemini remains available as a BYOK provider and MCP client
target, but its local generation runtime was retired and is not an adapter id.
4. Skill composition and staging
Skill delivery is shared daemon behavior; individual runtime definitions do not select a native, prompt, or project-instruction strategy.
For each run the daemon:
- Resolves the primary project/request skill, any additional
skillIdsselected through@mentions, and applicable plugin-provided skills. - Composes the selected
SKILL.mdbodies into the system prompt. It does not automatically inline everyreferences/*.mdfile. Instead, the skill-root preamble tells the agent where the staged skill lives and identifies side files referenced by the body so the agent can read them when needed. - Attempts to copy each selected skill directory into
<project-cwd>/.od-skills/<basename>-<source-path-hash>/. These are real, dereferenced project-private copies—not symlinks or junctions—so a tool that edits the staged tree cannot mutate the source skill. The copy path includes a recursive stream-copy fallback for recoverable cross-filesystem failures. - Supplies both the cwd-relative staged path and the absolute source fallback in the prompt. Definitions that accept additional directories may also receive external skill/design-system roots as CLI flags or system-prompt hints, according to their declared arg builder.
This is why RuntimeAgentDef has no nativeSkillLoading or
skillInjectionStrategy field, and why the daemon does not create
.cursorrules or install selected skills into an agent's home directory as part
of a run. See skills-protocol.md for the skill format;
the active-run staging implementation is in
server.ts,
skills.ts, and
cwd-aliases.ts.
5. Per-adapter notes
5.1 Claude Code (reference implementation)
- Invocation starts with
claude -p --input-format stream-json --output-format stream-json --verbose; model, capability-gated partial-message/extra-dir flags, native session--resumeor--session-id, and--permission-mode bypassPermissionsare appended when applicable. The process itself is spawned in the effective project cwd; there is no--cwd <artifact-dir>prompt invocation. - The composed prompt is written as one JSONL
usermessage. Stdin remains open so the daemon can forward additional user messages mid-turn, then is closed after a clean terminalturn_end/usagerather than at a mid-tooltool_usepause. runtimes/claude-stream.tstranslates Claude's structured stdout into the shared event stream. The generic run lifecycle owns process termination and cancellation.- Selected skills use the shared composition/staging path in §4; a run does
not symlink them into
~/.claude/skills/.
5.2 BYOK OpenCode
- The former direct-Anthropic fallback was replaced by the
byok-opencodeprofile. API-mode provider credentials and model selection are translated into OpenCode configuration, while the installedopencode-cli/opencodeprocess still owns the model/tool loop. - Invocation is
opencode run --format jsonwith the composed prompt on stdin and the selected provider/model passed through OpenCode's model syntax. The profile shares the OpenCode JSON-event parser and external MCP injection path. - There is no daemon-owned fallback loop or daemon implementation of
Read/Write/Edittools, and this profile is not an automatic recovery target for failed local agents.
5.3 Codex
- The default transport runs the installed CLI's built-in
codex app-serverover stdio JSON-RPC; no separate app-server installation or patched Codex binary is required. New sessions usethread/start, follow-up turns usethread/resume, and prompts arrive throughturn/start. - Official file previews use
item/fileChange/patchUpdated. The daemon enablesfeatures.apply_patch_streaming_eventsper thread only when the running server's initialize response identifies a stable version >= 0.123.0. Upstream 0.123.0 first wired the event through app-server (#18289); 0.122.0 only had the internal event. Older, unknown, and prerelease versions keep the existing completed-file behavior without receiving this feature override. This is a preview capability floor, not a new minimum version for running Codex. - A patch preview creates one lightweight Write/Edit row per file as soon as
its path is available, before the full patch finishes generating. Final
events settle the same row with diff statistics and execution status.
This covers
apply_patch, not arbitrary shell command arguments. The upstream feature remains opt-in; absence of preview events does not block completion. See patch-stream validation. OD_CODEX_TRANSPORT=exec-jsonselects the legacy transport:codex exec --json --skip-git-repo-checkwith the effective sandbox, create-only-C/--add-dir, and optional model/reasoning overrides. This transport does not carry early patch previews.runtimes/json-event-stream.tsparses Codex's structured JSON events; this is not a regex-based plain-text adapter. The parser capturesthread.started.thread_id.- Legacy follow-up turns use
codex exec resume --json ... <thread-id>. Resume uses-c sandbox_mode=...because Codex rejects create-only--sandbox,-C, and--add-dirflags onexec resume. - Detection uses
codex login statusfor auth andcodex debug modelsfor live model discovery, with static model hints as a fallback. Skills use the shared composition/staging path in §4 rather than version-gated loading from~/.codex/skills/.
5.4 Devin for Terminal
- Invocation:
devin --permission-mode dangerous --respect-workspace-trust false acp. - Install/update: macOS/Linux/WSL users can install with
curl -fsSL https://cli.devin.ai/install.sh | bash; rundevin updatefor existing installs. - Version requirement: requires a Devin CLI build with the
devin acpsubcommand (verified withdevin 2026.5.1-1). Check withdevin acp --help; if the subcommand is missing, update or reinstall Devin for Terminal. - Streaming: Agent Client Protocol JSON-RPC over stdio, handled by the daemon's shared
acp-json-rpctransport. - Skills: selected skills use the shared composition/staging path in §4; no Devin-specific skill installation is required for a run.
- Surgical edits: Devin's own edit/write tools handle targeted changes.
- Permission:
--permission-mode dangerousavoids headless approval prompts in the web UI;--respect-workspace-trust falseensures Devin doesn't block on trust prompts for newly created project dirs. Org/team-level policies still apply inside Devin.
5.5 Cursor Agent
- Invocation uses
cursor-agent --print --output-format stream-json --stream-partial-output --force, capability-gated--trust,--workspace <project-cwd>, and an optional model. The composed prompt is delivered on stdin rather than as a positional argument. - Cursor's JSONL is parsed by the shared
json-event-streamdispatcher with thecursor-agentparser. Detection usescursor-agent statusfor auth andcursor-agent modelsfor live model discovery. - Selected skills use §4's prompt composition and
.od-skillscopies. The daemon does not generate a.cursorrulesfile. --workspacechooses the starting workspace;--forceand optional--trustare part of the non-interactive authority posture described in §10, not a filesystem sandbox supplied by OpenDesign.
5.6 OpenCode
- OpenCode runs as
opencode run --format jsonwith the prompt on stdin. Newer builds that advertise--dangerously-skip-permissionsfromopencode run --helpreceive that flag; older builds keep the compatible argv without it. - The adapter discovers models with
opencode models, parses structured JSON events, and captures OpenCode'ssessionID. Follow-up turns continue the native session with-s <session-id>. - External MCP configuration is supplied per invocation through
OPENCODE_CONFIG_CONTENT; selected skills still use the shared §4 path.
5.7 GitHub Copilot CLI
- Invocation is
copilot --allow-all-tools --output-format jsonwith optional model and repeated--add-dirarguments. The daemon omits-pentirely and pipes the composed prompt to stdin;-p -would be interpreted as the literal prompt-. --allow-all-toolsis required for non-interactive execution; without it Copilot can block waiting for a tool-approval prompt.--add-dirwidens the CLI's path access to explicitly supplied external roots.- Streaming:
--output-format jsonemits JSONL with the same expressive shape as Claude Code's stream-json (assistant.reasoning_delta,assistant.message_delta,tool.execution_start/complete,result).apps/daemon/src/copilot-stream.tsmaps these onto the same UI events asclaude-stream.ts. - Skills use the shared composition/staging path in §4.
- Surgical edits: dedicated
edittool. - Copilot declares no proactive
authProbe; detection proves that the binary is invocable, while login and account scope remain owned by the CLI.
5.8 Qoder CLI
- Invocation is
qodercli -p --output-format stream-json --yolo -w <project-cwd>with optional model, repeated absolute--add-dir, and one--attachment <absolute-image-path>per image. The composed prompt is delivered over stdin, and print mode exits after the turn. - Streaming:
--output-format stream-jsonemits JSONL records such assystem/init,assistant, andresult.apps/daemon/src/runtimes/qoder-stream.tsmaps assistant content blocks to text deltas, maps assistant errors without text to typed error events, and preserves result usage, model usage, cost, duration, stop reason, and unknown records as raw events. - Models: ships fallback hints for
default,lite,efficient,auto,performance, andultimate. Selectingdefaultomits--modelso Qoder's own CLI configuration remains authoritative. - Skills use the shared §4 path.
--add-diris repeatable for absolute external roots; relative entries are not forwarded. - Permission:
--yoloavoids headless approval prompts. Treat this as the same trust posture as running Qoder directly with that flag in the selected project directory. - Gotcha: Detection only proves
qodercli --versioncan run. Qoder owns login/account scope; persistedqodercli loginstate or an inheritedQODER_PERSONAL_ACCESS_TOKENis outside daemon management. Run failures are surfaced instead of triggering a daemon login flow.
5.9 Trae CLI
- Invocation:
traecli acp serve --yolo, using the daemon's shared ACP JSON-RPC transport. The adapter follows Trae CLI's public ACP entrypoint documented at https://www.volcengine.com/docs/86677/2227861?lang=zh. - Streaming:
acp-json-rpc; the daemon uses the same ACP event path as the other ACP-backed adapters. - Models: dynamic via the ACP handshake. If model discovery fails, the picker falls back to the CLI's default configuration rather than requiring CI or startup detection to log in to Trae CLI.
- Skills use the shared §4 path. External MCP servers can be forwarded through the ACP launch descriptor with the existing
acp-mergepath. - Permission:
--yoloavoids headless approval prompts in the web UI. This follows the adapter catalog's existing non-interactive permission posture for CLIs such as Devin, Copilot, Qoder, and DeepSeek: the daemon runs agent CLIs without a TTY, so it must not rely on an interactive tool-approval prompt to make progress. - Gotcha: Detection only proves
traecli --versionand model discovery can run in the current environment. Trae CLI owns login, account scope, and model entitlement; the daemon does not run login flows or edit Trae CLI configuration.
5.10 Pi
- Invocation:
pi --mode rpc [--model <id>] [--thinking <level>] [--append-system-prompt <dir> …], with the composed prompt delivered over stdin via JSON-RPC. The daemon sends apromptcommand (optionally withimagesfor multimodal input) and pi streams back typed events untilagent_end. Pi's RPC process stays alive afteragent_end(designed for multi-prompt sessions); the daemon closes stdin and SIGTERMs after a grace period since/api/chatis single-shot. - Streaming:
pi-rpcJSON-RPC over stdio. Events includeagent_start,turn_start/end,message_update(text deltas, thinking deltas, tool calls),tool_execution_start/end,compaction_start,auto_retry_start/end, andextension_error.apps/daemon/src/agent-protocol/pi-rpc/session.tsmaps them into the shared UI event stream. Errors fromextension_errorand exhaustedauto_retry_endgo through the daemon's normal stream-error and empty-output handling. - Models: dynamic —
pi --list-modelsprints a TSV table to stdout that the daemon parses into provider/model picker entries. Fallback hints for the most common providers/models are shipped for when the list command times out. - Images: pi's RPC
promptcommand supports animagesfield (base64-encodedImageContentobjects). The daemon reads validatedimagePathsat session attach time and includes them in the prompt command. Unreadable images are skipped rather than failing the run. - Skills use §4's composed prompt and staged
.od-skillscopies. Absolute external roots inextraAllowedDirsare also forwarded through repeated--append-system-prompt; this is a path hint, not a filesystem grant or sandbox flag. - Thinking: the daemon exposes pi's
--thinkinglevels (off,minimal,low,medium,high,xhigh) in the Settings model picker. - Resume: after
agent_end, the transport captures the single changed.pi/sessions/*.jsonlpath. A later turn can sendnew_sessionwith that file asparentSession; ambiguous concurrent changes are not associated with the conversation. - Extension UI: auto-resolved. pi's RPC protocol can request user dialogs (
select,confirm,input,editor) and fire-and-forget notifications (setStatus,setWidget,notify,setTitle,set_editor_text). Dialog methods are auto-approved (confirm → true, select → first option) and fire-and-forget methods are silently consumed because the web UI has no surface for them. - Gotcha: pi's RPC
promptresponse is asynchronous —success: trueonly means the prompt was accepted, not that the agent finished. Agent failures after acceptance surface through the normal event stream (extension_error,auto_retry_endwithsuccess: false) and the empty-output guard.
5.11 DeepSeek TUI
- Invocation is
deepseek exec --auto [--model <id>] "<prompt>"through the dispatcher.codewhaleis an argv-compatible fallback binary after the upstream rename. The companiondeepseek-tui/codewhale-tuiruntime is not probed directly because it does not accept this invocation shape. - Streaming: plain text deltas to stdout in non-
--jsonmode (tool-call notifications go to stderr). Skipping--jsonis intentional —deepseek exec --jsonbatches the entire run into one trailing summary object instead of streaming, which would freeze the chat UI until end-of-turn. - Auto-approval:
--autoenables agentic mode with the YOLO permission posture. The daemon runs every CLI without a TTY, so the interactive approval prompt would otherwise hang the run. - Skills use the shared composition/staging path in §4; the adapter does not rely on DeepSeek's own skill-directory scan.
- Prompt delivery: positional argv (no stdin sentinel; clap declares
prompt: Stringas a required field). This means very large composed prompts can hit Windows' ~32 KBCreateProcesslimit; for typical chat prompts this is non-issue. Upstream support for a-stdin sentinel would let us flip this topromptViaStdin: truelike the other adapters. To avoid surfacing oversized prompts as a genericspawn ENAMETOOLONG/E2BIG, the adapter declaresmaxPromptArgBytes(currently 30,000) and/api/chatenforces it through three complementary guards: a fast pre-bin-resolutioncheckPromptArgvBudgetagainst the raw composed prompt bytes, a post-buildArgscheckWindowsCmdShimCommandLineBudgetthat — when the resolved binary is a Windows.cmd/.batshim — recomputes the would-becmd.exe /d /s /c "<inner>"command line using the same per-arg quote-doubling the platform layer applies on Windows, and a siblingcheckWindowsDirectExeCommandLineBudgetthat — when the resolved binary is a non-shim Windows install (e.g. a cargo-builtdeepseek.exe) — recomputes the same command line using libuv'squote_cmd_argrules (every"becomes\", backslashes adjacent to a quote are doubled). The two Windows guards are mutually exclusive on a given resolution: the cmd-shim guard owns.cmd/.bat, the direct-exe guard owns everything else. Together they catch quote-heavy prompts (code blocks, JSON-shaped skill seeds) that fit under the raw byte budget but expand past CreateProcess's 32_767-charlpCommandLinecap on either install path. All three guards emit the same actionableAGENT_PROMPT_TOO_LARGESSE error telling the user to reduce skills/design-system context, shorten the conversation, or pick an adapter with stdin support, and all three are unit-tested (oversized + short-prompt branches, quote-heavy regressions for both Windows paths, and a mutual-exclusivity check) so the guards can't silently regress. - Models: ships
deepseek-v4-proanddeepseek-v4-flashas fallback hints (1M-token context windows, native thinking-mode streaming). Users can paste any other id (e.g.nvidia-nim/deepseek-v4-pro,fireworks/deepseek-v4-flash) via the Settings dialog's custom-model input. - Gotcha — no proactive auth probe. Detection reports binary availability,
but DeepSeek TUI reads its API key from
~/.deepseek/config.tomlorDEEPSEEK_API_KEY. If a run fails with a recognized auth signature, the shared failure classifier emits DeepSeek-specific guidance for those two configuration paths instead of returning the raw non-actionable error.
5.12 DeepSeek Harness
-
OpenDesign launches the user's official
dshinstallation; it does not bundle Harness or Node. Install the tested DSH release first and useDSH_BINonly when its executable is outside the daemon's PATH. OpenDesign publishes checksum-verifying bootstrap installers for users who do not already have the compatible Node, DSH, and pnpm toolchain. They place an OD-discoverable launcher in the user's local bin directory and open the Harness Web UI for provider setup after installation:curl -fsSL 'https://open-design.ai/install-dsh.sh?version=1' | sh& ([scriptblock]::Create((irm 'https://open-design.ai/install-dsh.ps1?version=1')))From Windows Command Prompt, the equivalent bootstrap is:
curl -fsSL "https://open-design.ai/install-dsh.cmd?version=1" -o "%TEMP%\install-dsh.cmd" && call "%TEMP%\install-dsh.cmd"Pass
--no-launchto the downloaded POSIX script or-NoLaunchto the downloaded PowerShell script for unattended installation. The installers pin the exact versions in the adapter's compatibility policy and do not use a global npm install. -
The adapter also requires an OpenDesign-owned Harness profile named
open-design. The package source lives atpackages/dsh-runtime. Packaged OD builds embed an exact tarball and SHA-256 manifest for this thin component; they do not depend on a public npm release at setup time. Repository developers may pack and install the same source manually:pnpm --filter @open-design/dsh-runtime build pnpm -C packages/dsh-runtime pack --pack-destination <temporary-directory> dsh plugin --profile open-design add <temporary-directory>/open-design-dsh-runtime-0.1.0.tgz dsh --profile open-design --probe dsh --profile open-design --models -
Detection first checks
dsh --version, then requires the profile's strict protocol-generation handshake. Whendshexists but the profile is missing or incompatible, DeepSeek Harness stays in the normal Your CLIs list with a setup-required state. Selecting it opens an explicit confirmation dialog; confirmation installs the embedded component through the user'sdsh, rescans, selects, and connection-tests it. Cancelling changes nothing. Only a missingdshexecutable belongs in the installable-agent group. -
Each OD run starts a fresh
dsh --profile open-design --stdioprocess. The JSONL profile protocol creates a Harness session on the first turn and cold resumes that exact session on later turns. This is profile-stdio resume, not a CLI resume flag and not ACP. -
Text, thinking, tool calls/results, usage, cancellation, and terminal status are structured. Harness writes ordinary files in the OD project cwd, so the existing watcher and artifact preview own delivery.
-
Phase one uses credentials already configured for Harness or inherited as
DEEPSEEK_API_KEY; OpenDesign neither stores nor reads back the secret. -
Model detection comes from
dsh --profile open-design --models. Each model may expose its own reasoning-effort choices; OD validates and forwards only one of the choices advertised for that selected model.
5.13 Plain stream artifact handoff
Adapters with streamFormat: 'plain' do not expose structured file-write tool calls to the daemon. Their stdout is still a valid artifact handoff when the model emits Anthropic-style source blocks:
<artifact identifier="landing-page" type="text/html" title="Landing page">
<!doctype html>
<html>...</html>
</artifact>
At run completion, the daemon scans the captured plain stdout for <artifact> blocks with supported text types and writes them through the normal project artifact path:
| Artifact type | Project file |
|---|---|
text/html or html |
<identifier>.html |
text/css or css |
<identifier>.css |
image/svg+xml or svg |
<identifier>.svg |
text/markdown, text/x-markdown, markdown, or md |
<identifier>.md |
The identifier is slugged before use, collisions receive -2, -3, etc., and outputs without a supported <artifact> block are left unchanged. This daemon-side extraction keeps headless runs and web-attached runs aligned: the project file exists even when no browser is present to parse the chat stream.
6. Runtime metadata and UI
There is no public agents.capabilities() method and no generalized
surgicalEdit/streaming/resume feature-gate table.
RuntimeAgentDefdeclares invocation and transport facts such as prompt delivery, stream parser, image support, external MCP injection, native session resume, model/reasoning choices, and prompt budgets.runtimes/capabilities.tscontains only an in-process map of flags found in an installed CLI's help output. Arg builders use those flags to avoid passing options that an older executable does not recognize—for example Claude's partial messages or Cursor's--trust./api/agentsreturnsAgentInfo: availability, resolved path, version, auth status when probed, diagnostics, models/model source, reasoning options, and MCP metadata. The picker and Settings render controls from those concrete fields.
Run-time behavior such as native-session recovery is handled by the shared engine from the selected definition; it is not toggled by a separate web capability API.
7. Agent switching
The picker writes the selected id to AppConfig.agentId, persists the local
preference, and syncs it through PUT /api/app-config. Model and reasoning
choices are stored separately under agentModels[agentId].
Each chat/run request carries its agentId, and the daemon resolves that
definition when it creates the run. Changing the picker affects subsequent
runs; it does not rebind or mutate a child process that is already running.
Cancel the existing run separately if it should stop. There is no
POST agents.setActive endpoint or capability-refresh handshake.
8. Selection and failure recovery
OpenDesign does not implement an ordered cross-agent fallback chain. A chat request explicitly names its agent, and a crash, auth failure, timeout, or invalid invocation remains a failure for that run. The user can select another agent and send the request again, but the daemon does not silently—or through a dedicated one-click fallback action—move the request to Claude, another detected CLI, or BYOK OpenCode.
The generic run-creation endpoint has a narrower pre-run defaulting rule when
agentId is omitted: use the configured agent if it is currently available,
otherwise use the first available definition. This chooses an agent before a
run starts; it is not failure recovery. Separately, when a stored native
session has expired, the daemon may clear that same agent's stale session and
reseed the turn with the full transcript. That recovery never changes agent
families.
9. Detection UX
The daemon warms detection during startup, but the user-facing inventory comes
from /api/agents. Its ?stream=1 form emits one agent SSE event as each
probe settles, followed by done, so Settings can paint cards without waiting
for the slowest CLI. The non-streaming form returns the complete array.
Settings groups installed and unavailable agents, shows version/path and declared auth/model information, renders typed diagnostics with fix actions, and exposes a rescan action. The compact picker lists available agents and keeps unavailable entries disabled. Auth is only asserted for definitions that declare a working probe; other definitions leave auth unset/unknown rather than guessing it from a config directory. The old illustrative terminal transcript and “skills dir linked” status are not emitted by the current startup path.
10. Authorization boundaries
The daemon delegates policy enforcement to each CLI, but its headless arg builders intentionally choose non-interactive permission modes. The effective project cwd is an execution root, not a uniform OpenDesign sandbox, and external-directory flags can widen a CLI's reach.
- Claude runs with
--permission-mode bypassPermissions; Cursor runs with--forceand capability-gated--trust. - Devin uses
--permission-mode dangerous --respect-workspace-trust false; Qoder and Trae use--yolo; Copilot uses--allow-all-tools; DeepSeek uses--auto. Other definitions have their own explicit headless posture (for example Amp's--dangerously-allow-all). - OpenCode receives
--dangerously-skip-permissionsonly when its help probe advertises that flag, so older compatible builds are not given an unknown option. - Codex defaults to
workspace-writewith network access on supported macOS and Linux hosts. Windows, WSL, or an explicitOD_CODEX_SANDBOX=danger-full-accessoperator override usesdanger-full-accessbecause the workspace-write path cannot support the required shell execution there.
Provider/org policy and any sandbox internal to the CLI still apply, but users
must treat these runs as trusted agent execution with the authority shown by
the selected definition. There is no direct-Anthropic daemon loop with a
daemon-owned Read/Write/Edit whitelist; BYOK generation delegates that
loop to OpenCode as described in §5.2.
11. Adapter source layout
The contract, the per-CLI defs, and most stream parsers live under
apps/daemon/src/runtimes/; the JSON-RPC transports live under
apps/daemon/src/agent-protocol/. The agents.ts public barrel,
copilot-stream.ts, and the server.ts spawn/dispatch glue are the main
runtime-facing entry points directly under apps/daemon/src/.
apps/daemon/src/
├── agents.ts # public barrel — re-exports AGENT_DEFS / getAgentDef / detectAgents / … from runtimes/ (defines nothing)
├── runtimes/
│ ├── types.ts # the RuntimeAgentDef contract (the data spec) + shared runtime types
│ ├── registry.ts # BASE_AGENT_DEFS array → AGENT_DEFS + unique-id guard + getAgentDef()
│ ├── defs/ # one object literal per CLI — the file you add for a new agent
│ │ ├── claude.ts
│ │ ├── codex.ts
│ │ ├── cursor-agent.ts
│ │ ├── devin.ts
│ │ ├── … # ~two dozen defs (opencode, hermes, qoder, copilot,
│ │ │ # amp, pi, kiro, kilo, vibe, deepseek, aider, antigravity, qwen,
│ │ │ # grok-build, kimi, reasonix, codebuddy, trae-cli, …)
│ │ └── shared.ts # helpers reused across defs (not a registered agent)
│ ├── detection.ts # resolved-launch/version/help/model/auth probes (detectAgents / …Stream)
│ ├── capabilities.ts # in-process help-probe flag map consumed by arg builders
│ ├── executables.ts # PATH resolution · resolution.ts — bin resolution
│ ├── launch.ts # generic launch descriptor (resolveAgentLaunch / applyAgentLaunchEnv)
│ ├── invocation.ts # generic argv/prompt invocation from a def's buildArgs
│ ├── env.ts # per-agent spawn env · mcp.ts — external-MCP injection per def
│ ├── models.ts # live/fallback models · prompt-budget.ts — argv size guards
│ ├── local-profiles.ts # user-defined local agent profiles merged into AGENT_DEFS
│ ├── claude-stream.ts # streamFormat="claude-stream-json": stream-json JSONL → UI events
│ ├── qoder-stream.ts # streamFormat="qoder-stream-json": stream-json JSONL → UI events
│ ├── json-event-stream.ts# streamFormat="json-event-stream": generic JSONL → UI events
│ └── plain-stream.ts # streamFormat="plain": scans stdout for <artifact> blocks → project files
├── copilot-stream.ts # streamFormat="copilot-stream-json" — the one stream parser that sits flat at src/
├── agent-protocol/ # JSON-RPC transports, dispatched via agent-protocol/index.ts (attachAcpSession / attachPiRpcSession)
│ ├── index.ts # barrel: attachAcpSession / attachPiRpcSession / mapPiRpcEvent
│ ├── acp/ # streamFormat="acp-json-rpc": shared transport for AMR, Devin, Hermes, Kimi, Kiro, Kilo, Reasonix, Trae CLI, and Vibe
│ ├── pi-rpc/ # streamFormat="pi-rpc": pi's JSON-RPC-over-stdio transport
│ └── core/ # shared JSON-line stream helpers
└── server.ts # spawn pipeline + stream dispatch: routes def.streamFormat/eventParser to a parser
The engine is agent-agnostic: it iterates AGENT_DEFS and reads fields. A community contribution adds a new agent by dropping one runtimes/defs/<cli>.ts and appending it to BASE_AGENT_DEFS — detection, launch, invocation, and (for an existing streamFormat) parsing come for free, with no change to core daemon code.
12. Open questions
- Nested agents. Structured streams can carry a child agent's messages
inline. For example, Claude Task frames have a non-null
parent_tool_use_id; the parser surfaces their content but prevents the child'sturn_endfrom completing the parent run. The UI still does not expose an independent nested-run tree. - Usage and cost coverage. Parsers preserve
usageand reported cost when a CLI exposes them (for example Claude, Codex, OpenCode, and Qoder), and those events feed persisted run messages and lifecycle analytics. Coverage is runtime-dependent; OpenDesign does not invent token or billing data when a CLI omits it. - Windows support. PATH scanning and
spawnsemantics differ on Windows. Definitions that accept stdin should setpromptViaStdin; argv-only definitions must declare and enforce a prompt budget so Windows' command-line limit fails observably before spawn. - Docker-contained agents. Some users run Claude Code in a container. Adapter needs a "remote" mode — probably same interface but talks over SSH. Phase 2+.
AMR compaction continuation (ACP extension v1)
The shared contract lives in packages/contracts/src/api/amr-continuation.ts.
An AMR initialize response must advertise both loadSession and
agentCapabilities._meta["com.open-design.nativeSessionContinue"].version = 1.
A structured OPENCODE_COMPACTION_CONTINUATION_INCOMPLETE prompt error can
supply the durable session ID and exact user/assistant message cursor only
when every observed tool result is terminal. The daemon independently checks
its tool frames before closing unfinished rows.
The daemon waits for the previous process tree to become quiescent, loads the
same durable session in a new process, and sends _session/continue with only
that cursor. Vela calls OpenCode's guarded POST /session/:sessionID/continue;
OpenCode rechecks persisted history and committed tools under its serialized
runner and continues the model loop without inserting a user prompt. This
requires matching Vela and OpenCode implementations; ordinary session/load
alone is insufficient.
At most one native continuation is allowed per physical Run. Cancellation,
missing or changed history, outstanding/unknown tools, unsupported versions,
and exhausted recovery stop with failure. Legacy compaction failure wording
is classified but never authorizes reseeding or full prompt replay. The same
daemon behavior serves web and od callers through the existing run API.