Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions docs/AGENT-CANDIDATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,31 +144,32 @@ image builds `pnpm deploy` one package, and every driver package would need
its own build, typecheck and test wiring. Revisit if third-party drivers
become a goal.

**Do give the driver SDK a shape inside the package.** The boundary already
holds — drivers import only shared host modules, never each other, and only
`main.ts` names them — but the SDK is ~1,100 lines spread flat over `src/`,
and `main.ts` knows every agent through two `switch` statements (loading and
warm-up), with agent-specific environment handling in them. Target layout:
**Do give the driver SDK a shape inside the package** (done). The layout:

```
src/
host/ main.ts, host.ts — the process, framing, routing
sdk/ driver.ts, types.ts, transcript, tools, commands, mcp,
provider, net, executable — the only thing a driver imports
install/ agentInstall, lockfilePins, generated/ — pinned runtimes
acp/ the ACP client and a generic ACP session/driver base
(extracted from drivers/deepseek)
drivers/<agent>/ each exports one DriverModule
host/ main.ts, host.ts — the process, framing, routing;
modules.ts — the list of driver modules
sdk/ driver.ts, module.ts, types.ts, transcript, tools, commands,
mcp, provider, net, executable — the driver SDK
install/ agentInstall, lockfilePins — pinned runtimes
generated/ driver-protocol types and lockfile pins (generated; stays
put, since the Rust generator and CI's drift check name it)
acp/ (next) the ACP client and a generic ACP session/driver base,
extracted from drivers/deepseek
drivers/<agent>/ each exports one DriverModule from module.ts
```

A `DriverModule` is the agent's whole registration: its id and label, how
to build the driver from the environment, and its runtime as an install
descriptor (a pinned binary, a pinned package tree, or "found on PATH").
`main.ts` keeps a list of modules instead of a `switch`; loading, the
warm-up mode and install-on-demand all iterate that list, so adding an
agent really is one folder plus one line. A test fails the build when a
driver imports anything outside `sdk/`, `install/`, `acp/` or its own
folder.
A `DriverModule` (`sdk/module.ts`) is the agent's whole registration: its
id and label, the environment variables that are its alone (taken out of
the environment every agent process inherits, before any driver is built),
how to build the driver, and its runtime — what the machine already has,
and how to install the pinned one. `host/modules.ts` keeps the list;
loading and the warm-up mode iterate it, and install on demand will too, so
adding an agent is one folder plus one line. `host/__tests__/layout.test.ts`
fails the build when a driver imports anything outside `sdk/`, `install/`,
`generated/` (and later `acp/`) or its own folder, or when anything but the
module list names a driver.

## Order

Expand Down
23 changes: 14 additions & 9 deletions docs/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,20 +527,25 @@ restart.
### Adding an agent

1. Write a driver in `packages/agent-host/src/drivers/<agent>/` implementing
`Driver` (`src/driver.ts`): `info()` advertises the agent (modes, efforts,
`supports`, credentials); `startSession()` returns a `DriverSession` and
reports through the `SessionContext` it is handed — `emit()` session
events, `requestPermission()` / `askQuestion()` / `requestPlanApproval()`
when the user must decide.
`Driver` (`src/sdk/driver.ts`): `info()` advertises the agent (modes,
efforts, `supports`, credentials); `startSession()` returns a
`DriverSession` and reports through the `SessionContext` it is handed —
`emit()` session events, `requestPermission()` / `askQuestion()` /
`requestPlanApproval()` when the user must decide.
2. Translate the agent's own events into typed `OutputEntry` values in the
driver — nothing agent-specific may reach the bridge.
3. Register it in `src/main.ts` (it is enabled through
`CODEDECK_AGENT_HOST_DRIVERS`).
3. Export a `DriverModule` (`src/sdk/module.ts`) from the folder's
`module.ts`: the agent's id, how its driver is built from the
environment, and its runtime (what the machine already has, and how to
install the pinned one). Add it to the list in `src/host/modules.ts`; it
is enabled through `CODEDECK_AGENT_HOST_DRIVERS`.
4. Test it beside the driver, in `src/drivers/<agent>/__tests__/`, like
`drivers/claude/__tests__/claudeDriver.test.ts` and
`drivers/opencode/__tests__/opencodeDriver.test.ts` do, with the
recording `SessionContext` in `src/__tests__/context.ts`. Everything the
driver owns — its permission policy included — lives in that folder too.
recording `SessionContext` in `src/sdk/__tests__/context.ts`. Everything
the driver owns — its permission policy included — lives in that folder
too. A driver imports only `src/sdk/`, `src/install/`, `src/generated/`
and its own folder; `src/host/__tests__/layout.test.ts` enforces it.

The bridge and the phone need no change: the new agent appears in the
catalog, and its entries render through the typed vocabulary above. Only a
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-host/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import { build } from 'esbuild';

await build({
entryPoints: ['src/main.ts'],
entryPoints: ['src/host/main.ts'],
bundle: true,
platform: 'node',
target: 'node22',
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-host/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "node build.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"gen:platform-packages": "vitest run -u src/__tests__/lockfilePins.test.ts"
"gen:platform-packages": "vitest run -u src/install/__tests__/lockfilePins.test.ts"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.283",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import type {
SdkSessionHandle,
SdkSessionOptions,
} from '../facade';
import type { StartSession } from '../../../types';
import { recordingContext, type Handlers } from '../../../__tests__/context';
import type { StartSession } from '../../../sdk/types';
import { recordingContext, type Handlers } from '../../../sdk/__tests__/context';

class ScriptedHandle implements SdkSessionHandle {
readonly pushed: string[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
PROVIDER_BASE_URL_ERROR,
sanitizeProviderBaseEnv,
} from '../env';
import type { ProviderBinding, StartSession } from '../../../types';
import type { ProviderBinding, StartSession } from '../../../sdk/types';

const KIMI_TOKEN = 'sk-kimi-TESTSECRET-000';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
*/
import { describe, it, expect } from 'vitest';
import { extractDiff, sdkMessageToEntries } from '../adapter';
import { newTranslateContext, type TranslateContext } from '../../../transcript';
import type { OutputEntry } from '../../../types';
import { newTranslateContext, type TranslateContext } from '../../../sdk/transcript';
import type { OutputEntry } from '../../../sdk/types';
import type {
SdkAssistantMessage,
SdkMessage,
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-host/src/drivers/claude/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
* hides the tool calls behind those cards so they do not also render as
* ordinary tool actions.
*/
import { todosOf, toolInput, toolKindOf, toolLocations, toolTitle } from '../../tools';
import { todosOf, toolInput, toolKindOf, toolLocations, toolTitle } from '../../sdk/tools';
import {
MAX_DIFF_LINES,
toDiffLines,
truncateToolResult,
type DiffPayload,
type KnownTask,
type TranslateContext,
} from '../../transcript';
import type { DiffLine, OutputEntry, Subagent, TaskKind, TaskStatus } from '../../types';
} from '../../sdk/transcript';
import type { DiffLine, OutputEntry, Subagent, TaskKind, TaskStatus } from '../../sdk/types';
import type {
SdkMessage,
SdkAssistantMessage,
Expand Down
14 changes: 7 additions & 7 deletions packages/agent-host/src/drivers/claude/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
*/
import * as os from 'node:os';
import * as path from 'node:path';
import type { Driver, DriverSession, McpManager, PluginManager, SessionContext, SessionMcpState } from '../../driver';
import { mcpStatus } from '../../mcp';
import type { HttpPost } from '../../net';
import type { Driver, DriverSession, McpManager, PluginManager, SessionContext, SessionMcpState } from '../../sdk/driver';
import { mcpStatus } from '../../sdk/mcp';
import type { HttpPost } from '../../sdk/net';
import { isBenignPlanDirWrite } from './policy';
import { PERMISSION_ALLOW, PERMISSION_ALLOW_ALWAYS, PERMISSION_DENY, toolKindOf, toolLocations, toolTitle } from '../../tools';
import { newTranslateContext } from '../../transcript';
import { slashCommand } from '../../commands';
import { PERMISSION_ALLOW, PERMISSION_ALLOW_ALWAYS, PERMISSION_DENY, toolKindOf, toolLocations, toolTitle } from '../../sdk/tools';
import { newTranslateContext } from '../../sdk/transcript';
import { slashCommand } from '../../sdk/commands';
import type {
AgentInfo,
ModelEntry,
Expand All @@ -29,7 +29,7 @@ import type {
SlashCommand,
StartSession,
UsageData,
} from '../../types';
} from '../../sdk/types';
import { sdkMessageToEntries } from './adapter';
import { ANTHROPIC_API_KEY_CREDENTIAL, buildClaudeEnv } from './env';
import {
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-host/src/drivers/claude/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* credentials and the session's provider binding, turned into the variables
* the CLI reads. The returned objects carry SECRETS — never log them.
*/
import { isValidProviderBaseUrl, PROVIDER_BASE_URL_ERROR } from '../../provider';
import type { ProviderBinding, StartSession } from '../../types';
import { isValidProviderBaseUrl, PROVIDER_BASE_URL_ERROR } from '../../sdk/provider';
import type { ProviderBinding, StartSession } from '../../sdk/types';

export const ANTHROPIC_API_KEY_CREDENTIAL = 'anthropic_api_key';

Expand Down
2 changes: 1 addition & 1 deletion packages/agent-host/src/drivers/claude/facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { findInDirs, findOnPath, isFile } from '../../executable';
import { findInDirs, findOnPath, isFile } from '../../sdk/executable';
import type {
CanUseTool,
Options,
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-host/src/drivers/claude/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
*/
import { createRequire } from 'node:module';
import * as path from 'node:path';
import type { PackagedBinary } from '../../agentInstall';
import { exeName, isFile, isMusl } from '../../executable';
import type { PackagedBinary } from '../../install/agentInstall';
import { exeName, isFile, isMusl } from '../../sdk/executable';

/** The SDK's platform package for a machine. */
export function claudePlatformPackage(
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-host/src/drivers/claude/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
import { readFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import * as path from 'node:path';
import type { McpManager, McpState } from '../../driver';
import { serverInfo } from '../../mcp';
import type { McpAction, McpServerAdd, McpServerInfo } from '../../types';
import type { McpManager, McpState } from '../../sdk/driver';
import { serverInfo } from '../../sdk/mcp';
import type { McpAction, McpServerAdd, McpServerInfo } from '../../sdk/types';
import { failureMessage, type CliRunner } from './plugins';

const CHANGE_TIMEOUT_MS = 60_000;
Expand Down
48 changes: 48 additions & 0 deletions packages/agent-host/src/drivers/claude/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Claude Code: the Agent SDK around the `claude` binary — the one on the
* machine (CODEDECK_CLAUDE_PATH, PATH, or the SDK's own platform package),
* else the pinned platform package installed on demand.
* CODEDECK_TEST_MODE=1 swaps the SDK for canned `/test-*` sessions.
*/
import { installBinary } from '../../install/agentInstall';
import type { DriverEnv, DriverModule } from '../../sdk/module';
import { httpPost } from '../../sdk/net';
import { ClaudeDriver } from './driver';
import { RealSdkFacade, resolveClaudeExecutable } from './facade';
import { bundledClaudeExecutable, claudeBinary } from './install';
import { TestModeSdkFacade } from './testModeFacade';

const testMode = (ctx: DriverEnv): boolean => ctx.env.CODEDECK_TEST_MODE === '1';

/** The `claude` this machine has, if any. */
function existing(ctx: DriverEnv): string | null {
return resolveClaudeExecutable(undefined, ctx.lookupEnv) ?? bundledClaudeExecutable();
}

function install(ctx: DriverEnv): Promise<string> {
return installBinary(claudeBinary(), { cacheDir: ctx.cacheDir, registry: ctx.registry, log: ctx.log });
}

export const claudeModule: DriverModule = {
id: 'claude-code',
label: 'Claude Code',
create(ctx) {
const test = testMode(ctx);
const claudePath = test ? null : existing(ctx);
return new ClaudeDriver({
facade: test ? new TestModeSdkFacade() : new RealSdkFacade(),
...(claudePath ? { claudePath } : {}),
// Nothing on the machine: the driver installs it in the background,
// so the agent is listed right away and the first session waits for it.
...(!test && !claudePath ? { installClaude: () => install(ctx) } : {}),
httpPost,
discoverModels: !test,
managePlugins: !test,
manageMcp: !test,
});
},
runtime: {
find: (ctx) => (testMode(ctx) ? 'test mode' : existing(ctx)),
install,
},
};
6 changes: 3 additions & 3 deletions packages/agent-host/src/drivers/claude/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
import { execFile } from 'node:child_process';
import { readFileSync } from 'node:fs';
import * as path from 'node:path';
import { shortDescription } from '../../commands';
import type { PluginManager, PluginState } from '../../driver';
import type { AvailablePlugin, InstalledPlugin, PluginAction, PluginMarketplace } from '../../types';
import { shortDescription } from '../../sdk/commands';
import type { PluginManager, PluginState } from '../../sdk/driver';
import type { AvailablePlugin, InstalledPlugin, PluginAction, PluginMarketplace } from '../../sdk/types';

export interface CliResult {
code: number;
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-host/src/drivers/claude/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* a window is only included when the SDK reports it; null inner values are
* preserved so the phone can decide whether to render the row.
*/
import type { UsageData, UsageWindow } from '../../types';
import type { UsageData, UsageWindow } from '../../sdk/types';

/** The slice of SDKControlGetUsageResponse we consume (experimental shape). */
interface RawUsageWindow {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/
import { describe, expect, it } from 'vitest';
import type { SessionUpdate } from '@agentclientprotocol/sdk';
import type { DiffLine, OutputEntry } from '../../../types';
import type { DiffLine, OutputEntry } from '../../../sdk/types';
import { deepseekUpdateToEntries, toolCallDiffs, type ToolCallMemory } from '../adapter';

type EntryOf<T extends OutputEntry['entryType']> = Extract<OutputEntry, { entryType: T }>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { createServer, type Socket } from 'node:net';
import { tmpdir } from 'node:os';
import * as path from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import { recordingContext, type RecordingContext } from '../../../__tests__/context';
import type { StartSession } from '../../../types';
import type { DriverSession } from '../../../driver';
import { recordingContext, type RecordingContext } from '../../../sdk/__tests__/context';
import type { StartSession } from '../../../sdk/types';
import type { DriverSession } from '../../../sdk/driver';
import { DeepSeekDriver } from '../driver';
import { BRIDGE_SOCKET_ENV, QUESTION_MARKER } from '../plugin';
import { DeepSeekMcp } from '../mcp';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* custom provider (a gateway), in the style of claudeEnv.test.ts.
*/
import { describe, expect, it } from 'vitest';
import { PROVIDER_BASE_URL_ERROR } from '../../../provider';
import type { ProviderBinding } from '../../../types';
import { PROVIDER_BASE_URL_ERROR } from '../../../sdk/provider';
import type { ProviderBinding } from '../../../sdk/types';
import {
DEEPSEEK_API_KEY_CREDENTIAL,
buildDeepSeekEnv,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import * as path from 'node:path';
import { describe, expect, it } from 'vitest';
import type { McpServerAdd } from '../../../types';
import type { McpServerAdd } from '../../../sdk/types';
import { DeepSeekMcp } from '../mcp';

const INITIAL_LAYER = `# Your patch layer for this dsh profile, applied after every bundle layer.
Expand Down
6 changes: 3 additions & 3 deletions packages/agent-host/src/drivers/deepseek/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
* from the name (src/tools.ts) exactly as they do for the other agents.
*/
import type { SessionUpdate } from '@agentclientprotocol/sdk';
import { todosOf, toolInput, toolKindOf, toolLocations, toolTitle } from '../../tools';
import { MAX_DIFF_LINES, truncateToolResult, toDiffLines, type DiffPayload } from '../../transcript';
import type { OutputEntry } from '../../types';
import { todosOf, toolInput, toolKindOf, toolLocations, toolTitle } from '../../sdk/tools';
import { MAX_DIFF_LINES, truncateToolResult, toDiffLines, type DiffPayload } from '../../sdk/transcript';
import type { OutputEntry } from '../../sdk/types';

/**
* The tools whose card *is* the exchange. The question tool asks through the
Expand Down
4 changes: 2 additions & 2 deletions packages/agent-host/src/drivers/deepseek/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
import { createHash } from 'node:crypto';
import * as path from 'node:path';
import { connect } from 'node:net';
import { slashCommand } from '../../commands';
import type { SlashCommand } from '../../types';
import { slashCommand } from '../../sdk/commands';
import type { SlashCommand } from '../../sdk/types';

/** How long one question may take. The plugin answers from memory; a command
* that does real work (a compaction asks a model) takes as long as it takes,
Expand Down
12 changes: 6 additions & 6 deletions packages/agent-host/src/drivers/deepseek/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@
* harness's result update carries no diff.
*/
import type { RequestPermissionRequest, RequestPermissionResponse, SessionConfigOption, SessionNotification } from '@agentclientprotocol/sdk';
import type { Driver, DriverSession, McpManager, PluginManager, SessionContext, SessionMcpState } from '../../driver';
import type { HttpGet } from '../../net';
import { mcpStatus as mcpStatusOf } from '../../mcp';
import { parseSlashCommand } from '../../commands';
import { PERMISSION_ALLOW, PERMISSION_DENY, now, toolKindOf, toolLocations, toolTitle } from '../../tools';
import type { Driver, DriverSession, McpManager, PluginManager, SessionContext, SessionMcpState } from '../../sdk/driver';
import type { HttpGet } from '../../sdk/net';
import { mcpStatus as mcpStatusOf } from '../../sdk/mcp';
import { parseSlashCommand } from '../../sdk/commands';
import { PERMISSION_ALLOW, PERMISSION_DENY, now, toolKindOf, toolLocations, toolTitle } from '../../sdk/tools';
import type {
AgentInfo,
McpStatus,
Expand All @@ -38,7 +38,7 @@ import type {
SlashCommand,
StartSession,
UsageData,
} from '../../types';
} from '../../sdk/types';
import { deepseekUpdateToEntries, type ToolCallMemory } from './adapter';
import { DEEPSEEK_API_KEY_CREDENTIAL, DEEPSEEK_API_KEY_ENV, DEEPSEEK_BASE_URL_ENV, buildDeepSeekEnv } from './env';
import { askPlugin, listSessionCommands, runSessionCommand, steerSession } from './bridge';
Expand Down
Loading
Loading