UNPKG

@agentproto/driver-agent-cli

Version:

@agentproto/driver-agent-cli — AIP-45 AGENT-CLI.md reference implementation. Multi-protocol runner that loads an AGENT-CLI.md manifest, spawns the binary, and dispatches turns through an ACP / MCP / proprietary arm. ACP arm delegates to @agentproto/acp; e

991 lines (960 loc) • 49.9 kB
import { A as AgentCliHandle, a as AgentCliRuntime, b as AgentCliDefinition, c as AgentCliRuntimeSession, d as AgentCliClient, S as SetModelResult, e as AgentCliPrintConfig, f as AgentCliSessionSkill, R as RuntimeConfig, C as ContinuationStrategyId, g as AgentCliStartOptions, T as TurnContext, h as ContinuationKeyScope } from './schema-_N8GL_uH.js'; export { i as AgentCliAuth, j as AgentCliCapabilities, k as AgentCliConnectOptions, l as AgentCliContinuation, m as AgentCliFrontmatter, n as AgentCliFsZones, o as AgentCliInstallMethod, p as AgentCliMcpBlock, q as AgentCliMode, r as AgentCliModelEntry, s as AgentCliModels, t as AgentCliOption, u as AgentCliOptionType, v as AgentCliPinnedSessionTuning, w as AgentCliPresetDeclaration, x as AgentCliProtocol, y as AgentCliRouteSelection, z as AgentCliSession, B as AgentCliSessionMode, D as AgentCliSetupPersist, E as AgentCliSetupSkipIf, F as AgentCliSetupStep, G as AgentCliSmallModel, H as AgentCliStateHome, I as AgentCliUpgrade, J as AgentCliVersionCheck, K as RuntimeConfigInput, L as SetEffortResult, M as SetSessionModeResult, N as SteerOutcome, O as agentCliFrontmatterSchema, P as runtimeConfigSchema } from './schema-_N8GL_uH.js'; import { StreamEvent, AcpMcpServer } from '@agentproto/acp'; export { StreamEvent } from '@agentproto/acp'; import { ChildProcess } from 'node:child_process'; import { SandboxMode } from '@agentproto/command-sandbox'; export { SessionConfigOption, SessionMode } from '@agentproto/acp/client'; import 'zod'; declare const defineAgentCli: (def: AgentCliDefinition) => Readonly<AgentCliDefinition>; /** * A manifest `bin: "node"` means "run with the node that's driving THIS * process" (the daemon's own `process.execPath`), not "look up `node` on * the child's PATH". A bare `spawn("node", ...)` depends on PATH containing * a `node` entry — true in an interactive shell (nvm etc.) but NOT * guaranteed under launchd, which starts the daemon with a minimal PATH. * Resolving to `process.execPath` sidesteps PATH entirely for the one bin * value that always means "this runtime". * * `npx` / `npm` get the same treatment when possible: every Node install * layout (nvm, fnm, homebrew, system, Volta) ships them as siblings of the * `node` binary, so a daemon whose PATH lacks that dir (launchd again — * observed live 2026-08-30 as `spawn npx ENOENT` on a codex `agent_start`) * can still resolve them relative to its own `process.execPath`. Falls back * to the bare name (PATH lookup, prior behavior) when no sibling exists — * e.g. a trimmed container image exposing npx elsewhere on PATH. * * Every other bin (hermes, gemini, …) is left untouched — those are meant * to be resolved off PATH. */ declare function resolveSpawnBin(bin: string, deps?: { execPath?: string; exists?: (p: string) => boolean; }): string; declare function createAgentCliRuntime(definition: AgentCliHandle): AgentCliRuntime; /** * The canonical postures this driver enforces as a real boundary at * spawn/restart through the adapter's own isolated config — claude-code's * `permissions.defaultMode`, opencode's per-session `permission` block — * independently of any advertised ACP mode. Empty for every other adapter. * Surfaced on the session (`AgentCliRuntimeSession.spawnEnforcedPostures`) so * the runtime can report such a posture as enforced rather than as the * advisory preamble its mode registry alone would suggest. */ declare function spawnEnforcedPostures(definition: Pick<AgentCliDefinition, "id">): readonly string[]; /** * Live-session control surface, built once from a connected protocol arm. * * `AgentCliRuntimeSession` requires six members beyond * `send`/`cancel`/`close`: the three mid-session switches (`setModel`, * `setSessionMode`, `setEffort`, SPEC §3.9/§3.4a) and the three * read-surface snapshots (`availableConfigOptions`, `availableModes`, * `currentModeId`). All six are pure delegation to the arm — none of * them touch the child process, the spawn, or the transport. * * They used to live inline inside `createAgentCliRuntime`, which made * them unreachable for any host that builds its OWN session over a * different transport. Those hosts exist and are the whole point of * `createAcpProtocolArm` being exported: guilde runs one runtime whose * child lives in an e2b sandbox and another whose child lives on a * remote daemon behind a WS tunnel, both driving the same ACP arm. Every * such host had to hand-copy this block, and silently broke the day the * interface grew a seventh member. * * So it lives here, exported, and `createAgentCliRuntime` consumes it * like everyone else — one implementation, one place to extend. */ /** * The slice of `AgentCliRuntimeSession` {@link createArmSessionControls} * supplies. Spread it into the session literal alongside the * transport-specific `sessionId`/`send`/`cancel`/`close`. */ type ArmSessionControls = Pick<AgentCliRuntimeSession, "setModel" | "setSessionMode" | "setEffort" | "availableConfigOptions" | "availableModes" | "currentModeId">; /** * Build the control surface from a CONNECTED arm. * * Call this after `arm.connect()` has resolved: the read-surface fields * are snapshots, so an ACP arm's captured `newSession`/`loadSession` * response has to already be populated. Arms that don't model this * (print, proprietary) leave the getters undefined and are defaulted * here to the same empty/absent shape their `setModel`-style * counterparts use for "not supported". * * Every switch resolves — none throws, none tears down the session, even * when the underlying agent rejects the request. See * {@link SetModelResult} and friends for the reason vocabulary. */ declare function createArmSessionControls(arm: AgentCliClient, definition: AgentCliHandle): ArmSessionControls; /** See {@link MODEL_SWITCH_ACK_RE}. */ declare function isModelSwitchAcknowledgement(evt: unknown): boolean; /** * Extract the target model id from a plain-text turn that opens with a * `/model <id>` control command — whether it arrives through * `applyModelCommand`'s dedicated control turn or as an ORDINARY * conversational prompt (see {@link MODEL_SWITCH_ACK_RE}'s doc for why the * latter matters: it's the case the hermes spawn recipe actually produces). */ declare function parseModelSwitchCommand(text: string): string | undefined; /** * Switch the active model via a `/model <id>` control turn, for adapters * whose ACP session config doesn't select the model (`models.apply: * "command"`, e.g. hermes). The turn is fully drained so the switch * completes before the caller's next real turn. Best-effort: a transport * failure or a missing acknowledgement is warned and reported as * `{applied:false, reason}`, never thrown — the session simply continues * on whatever model it already had. Shared by the spawn-time apply * (return value ignored there) and the mid-session `setModel("command")` * path (return value surfaced to the caller). */ declare function applyModelCommand(arm: AgentCliClient, modelId: string): Promise<SetModelResult>; /** * Send a turn and yield the arm's events for it. * * Re-attaches the recent stderr tail to error events. The ACP layer * surfaces a terse `{message: "Invalid params"}`; the child's stderr * almost always has a more useful line ("npx claude-agent-acp: not * authenticated, run `claude login`"). Hosts read `error.data` when * present, falling back to `message` for older payloads. * * When `stderrTurnError` is supplied, a line it parses into a message * (see {@link parseStderrStreamError}) is surfaced as THIS turn's error: * the turn is cancelled and ended with `reason:"error"` instead of hanging * forever on a `prompt` the server never resolves. Only armed for adapters * whose transport retries silently (opencode); every other adapter keeps * the exact prior behavior. */ declare function promptTurn(arm: AgentCliClient, turnId: string, message: unknown, stderrTurnError?: (line: string) => string | undefined): AsyncIterable<StreamEvent>; /** * Agent CLI as a model port — turn ANY AIP-45 agent CLI runtime into a generic * `complete({system?, prompt}) → {result}` executor. This is the one bridge that * lets the SAME registry (hermes, claude-code, opencode, codex, openclaw, …) * back any prompt→completion seam: a report's chapter writer, a corpus * distiller, a Mastra tool's judgment step. The executor swaps, the prompts * (built by each seam's engine) don't. * * It drives the CLI over **ACP**: per `complete()` it spawns a fresh session, * sends one user turn, concatenates the `text-delta` stream until `turn-end`, * then closes. Lives here (not in a product) so seams BELOW the products — * `@agentproto/corpus` distill, the CLI — can consume it without importing * upward. The runner is injectable so assembly is unit-tested without a binary. */ /** * The minimal structural model port every seam consumes. Anything with this * `complete` shape satisfies the corpus `ReportModelPort` / `DistillPort`-backing * model / an AIP `ModelPort` — duck-typed, no nominal coupling. */ interface ModelLike { complete(req: { system?: string; prompt: string; [k: string]: unknown; }): Promise<{ result: string; }>; } /** * The "file-reading sub-agent" tier — opt-in across every file-IO-capable * executor. When `datasetDir` is set the executor is rooted there and granted * its own read tools, so instead of being limited to the excerpts a seam quotes * in the prompt it can open the primary sources directly and ground first-hand. */ interface FileReadingOptions { /** Absolute path to the dataset/source root the executor may read from. */ datasetDir?: string; } /** The instruction prepended to a completion when {@link FileReadingOptions.datasetDir} is set. */ declare function datasetPreamble(datasetDir: string): string; /** Compose system + prompt with an optional source-access preamble (shared by every executor). */ declare function composePrompt(system: string | undefined, prompt: string, datasetDir?: string): string; /** Provider keys an agent CLI may route through, forwarded from the parent env by default. */ declare const PROVIDER_KEY_ENV: readonly ["OPENROUTER_API_KEY", "ANTHROPIC_API_KEY", "OPENAI_API_KEY"]; interface AgentCliModelOptions extends FileReadingOptions { /** * Env handed to the spawned CLI process — the provider key it routes through. * Defaults to forwarding {@link PROVIDER_KEY_ENV} from the parent process env. */ env?: Record<string, string>; /** Working directory for the CLI process. Defaults to `datasetDir` when set. */ cwd?: string; /** Timeout per completion (ms). Default 5 min. */ timeoutMs?: number; /** * Injectable runner (composed prompt → completion text). Defaults to driving * the runtime over ACP. Override in tests to avoid the CLI. */ run?: (prompt: string) => Promise<string>; } /** Build the env for the spawned CLI: explicit override, else forwarded provider keys. */ declare function resolveCliEnv(opts: Pick<AgentCliModelOptions, "env">): Record<string, string>; /** * Build a {@link ModelLike} backed by an AIP-45 agent-CLI runtime. Hand it any * `*Runtime()` from the registry (Hermes, claude-code, opencode, …); per-CLI * presets are one-liners over this. */ declare function makeAgentCliModel(runtime: AgentCliRuntime, opts?: AgentCliModelOptions): ModelLike; /** * AIP-45 protocol arm: `protocol: "acp"`. * * Wraps a subprocess's stdio in an `@agentproto/acp` client and * exposes the `AgentCliClient` shape the runner consumes. Each * `send()` call delegates to a single AcpClientSession.prompt() and * forwards the resulting StreamEvent stream. */ /** * Outcome the host returns to the agent on a permission request. * * Mirrors the upstream ACP `RequestPermissionResponse` shape EXACTLY — * note the doubly-nested `outcome` field: the outer is the response * envelope, the inner is the discriminator (string literal, not a * `{ type: ... }` object). Get this wrong and the agent silently * interprets the response as a refusal ("User refused permission to * run tool"), so we type it strictly here. * * { outcome: { outcome: "selected", optionId: "..." } } * { outcome: { outcome: "cancelled" } } */ type AcpPermissionOutcome = { outcome: { outcome: "selected"; optionId: string; }; } | { outcome: { outcome: "cancelled"; }; }; interface AcpPermissionRequestParams { sessionId?: string; toolCall?: { toolCallId?: string; title?: string | null; kind?: string | null; rawInput?: unknown; }; options?: Array<{ optionId: string; name?: string; kind?: string; }>; } type AcpPermissionHandler = (params: AcpPermissionRequestParams) => Promise<AcpPermissionOutcome> | AcpPermissionOutcome; /** * Default handler: pick the first option whose `kind` starts with * `allow_` (Claude Code offers `allow_once` and `allow_always`). * Falls back to the first option if none match — keeps the agent * unblocked rather than throwing on an unfamiliar option set. * * Mirrors the trust model of in-process Mastra agents: the user * approved the operator binding once; subsequent tool calls don't * re-prompt by default. Hosts that want explicit per-call gating * pass their own `onPermissionRequest`. * * NOT safe to use as-is for a `mode: "plan"` session — see * `planModePermissionHandler` below. */ declare const autoAllowPermissionHandler: AcpPermissionHandler; /** * Permission handler for a session spawned with `mode: "plan"`. * * `autoAllowPermissionHandler` picks the first `allow_*` option, but for * the plan-mode-exit request that option is always an escalation away * from plan (`auto`, `acceptEdits`, or `bypassPermissions` depending on * what the wrapper offers) — auto-approving it silently defeats the * mode the caller asked for (see the regression this fixes: a `plan` * session that wrote a file when prompted to). Noted as a known gap in * a naive auto-allow handler when #143 shipped the `CLAUDE_CONFIG_DIR` * override that makes `mode: "plan"` take effect in the first place; * this closes it. * * Behaves exactly like `autoAllowPermissionHandler` for every other * request. For the plan-mode-exit request specifically, it selects the * offered `reject_once` option ("No, keep planning") so the wrapper * denies the exit with a clean, structured outcome — the tool call * fails, the agent stays in plan mode and can keep reasoning or tell * the user it's blocked, instead of the turn throwing on a `cancelled` * outcome. Falls back to `cancelled` if no `reject_once` option is * offered (shouldn't happen with the current wrapper, but a safe * default beats guessing an allow). * * This is a deliberate one-way gate: a plan-mode session driven by this * handler can never escalate itself out of plan mode. A caller that * wants a human (or a policy engine) to approve that escalation must * supply its own `onPermissionRequest` — this handler is only the * default for callers that don't. */ declare const planModePermissionHandler: AcpPermissionHandler; interface AcpProtocolOptions { child: ChildProcess; cwd: string; clientInfo?: { name: string; title?: string; version?: string; }; /** Called when the agent asks for permission to run a tool (Write, * Bash, ...). Defaults to `autoAllowPermissionHandler`, or * `planModePermissionHandler` when `requestedMode === "plan"`. Pass a * custom handler to plumb requests through a UI / governance * policy. Throwing or returning a rejected promise bubbles to the * agent as an internal error. */ onPermissionRequest?: AcpPermissionHandler; /** * AIP-45 mode requested at spawn (`opts.config.mode` in * `define-agent-cli.ts`'s `start()`), if any. Used to pick the default * permission handler when `onPermissionRequest` is omitted — see * `defaultPermissionHandlerForMode`. */ requestedMode?: string; /** * When true, permission requests are surfaced as `agent-prompt` StreamEvents * and HELD in the daemon's permission inbox rather than auto-answered — * forwarded verbatim to `createAcpClient({ permissionHold })`. The * `onPermissionRequest` / `requestedMode` handler is bypassed while this is * set. Default false (unchanged: requests are auto-answered in-arm). */ permissionHold?: boolean; } declare function createAcpProtocolArm(options: AcpProtocolOptions): AgentCliClient; /** * AIP-45 protocol arm: `protocol: "print"`. * * Drives a headless CLI as one fresh subprocess per turn — no * long-lived ACP connection. Simpler than the ACP arm and immune to * the stale-proxy race condition: the session object itself never dies * between turns; only the per-turn child does. * * ## Configuration * * The adapter's `print` manifest block declares the CLI's one-shot * surface (flags, output format, event taxonomy). When the block is * absent, Claude Code defaults are applied for backward compatibility. * * ## Session tracking * * The `sessionId` property starts empty (or pre-seeded from * `resumeSessionId`) and is updated after the first successful turn * from the wire event that carries the session / thread identifier * (Claude: `result.session_id`; Mastra: `result.threadId`). * Subsequent turns pass the appropriate resume flag so the CLI * rehydrates the conversation. */ interface PrintArmOptions { bin: string; /** Base argv passed to the binary BEFORE print flags and prompt. * Typically permission-mode and model overrides from `composeSpawn`. */ baseArgs: string[]; cwd: string; env: Record<string, string>; /** Pre-seed from `resumeSessionId` so the first turn reattaches. */ resumeSessionId?: string; /** Adapter-declared print surface config. Omit for Claude defaults. */ printConfig?: AgentCliPrintConfig; /** * MCP servers to mount into the agent's session. For the `mastracode` * print arm (`event_schema: "mastra-jsonl"`) these are written to * `<cwd>/.mastracode/mcp.json` before the first turn's spawn (the * mastracode CLI has no CLI flag / env var / config-dir override for * MCP config — this was exhaustively verified against its source) and * restored/removed on `close()`. Ignored for other print schemas * (e.g. Claude Code), which don't load MCP config from this path. */ mcpServers?: AcpMcpServer[]; /** OS-level confinement for the per-turn spawn — see * `AgentCliStartOptions.commandSandbox` in `../types.ts` and * `wrapAgentCliSpawn`'s doc for the fail-closed contract. */ commandSandbox?: SandboxMode; /** Extra write-capable paths beyond the default toolchain set, e.g. the * per-session `CLAUDE_CONFIG_DIR` temp dir set up by the caller. */ extraWritePaths?: string[]; /** Extra READ-only paths beyond the confinement boundary — the exact-file * AGENTS.md grant (`AgentCliStartOptions.additionalReadPaths`). READ * exceptions only; never widens writes. */ extraReadPaths?: string[]; /** See `AgentCliStartOptions.fsZones`. */ fsZones?: { readOnly: string[]; writable: string[]; hidden?: string[]; }; /** * The explicitly requested model id — when set, a wire event that * truthfully reports which model the agent ACTUALLY started on (today: * the jcode-ndjson `start` line's `model` field) is checked against it, * and a mismatch aborts the turn loudly (child killed, error event) * instead of letting the agent run on a model the operator didn't name. * Set by `define-agent-cli.ts` only for `routeSelection: * "derived-from-model"` adapters with an explicit `model` option — the * print-protocol counterpart of its ACP-arm guard. The bug this closes: * jcode's CLI silently falls back to its own default on an unknown * `--model` id (observed live: `--model totally-bogus-xyz` → * `{"type":"start","model":"gpt-5.6-sol","provider":"OpenAI"}`), which * for a derived-from-model adapter is a different provider on a * different bill. Schemas that never report the started model are * unaffected. Compared via {@link printModelMismatch} (basename, * `@suffix`/`provider/`-prefix tolerant). */ expectedModel?: string; } declare function createPrintSession(opts: PrintArmOptions): AgentCliRuntimeSession; /** * Mutable per-stream state for the jcode NDJSON mapper. Tool arguments * arrive as JSON-string `tool_input` deltas between `tool_start` and * `tool_exec`, so the pending call accumulates here until complete. */ interface JcodeMapperState { pending?: { id: string; name: string; input: string; }; } declare function createJcodeMapperState(): JcodeMapperState; /** * Maps a single `jcode run --ndjson` line to this repo's * {@link StreamEvent} taxonomy. Wire shape verified against a live * `jcode run --ndjson` (2026-08-14): * * {type:"start", session_id, model, provider} → session capture * {type:"status_detail"|"connection_phase"| * "connection_type"|"message_end", …} → dropped (noise) * {type:"text_delta", text} → text-delta * {type:"tool_start", id, name} → open pending call * {type:"tool_input", delta} → accumulate args * {type:"tool_exec", id, name} → tool-call * {type:"tool_done", id, name, output, error} → tool-result * {type:"tokens", input, output, cache_read_input, …} → usage_update * {type:"done", text, session_id, usage} → turn-end * * `tokens` fires once per upstream API call (a tool round-trip produces * several), so usage arrives as successive updates, not one total. */ declare function mapJcodeEvent(evt: Record<string, unknown>, sessionId: string, state: JcodeMapperState): StreamEvent | StreamEvent[] | null; /** * Mutable per-stream state for the Mastra Code event mapper. * Mastra Code sends the FULL accumulated text on each `message_update`, * so we track the previous length to emit only the new portion. * * Exported so other Mastra Code protocol arms (e.g. the in-process * `proprietary` arm in `@agentproto/adapter-mastracode-inprocess`) can * reuse the same event mapper instead of reimplementing it — the wire * shape here is `AgentControllerEvent` regardless of whether it arrived * over a JSONL subprocess pipe or as a live in-process object. */ interface MastraMapperState { lastTextLength: number; } declare function createMastraMapperState(): MastraMapperState; /** * Maps a single Mastra Code `AgentControllerEvent` (already a plain * object — JSON-parsed from a subprocess line, or handed over directly * by an in-process arm) to this repo's {@link StreamEvent} taxonomy. * `stderrLines` enriches `error` events for arms that have a real * subprocess to report on; pass `[]` when there is none (e.g. the * in-process arm). */ declare function mapMastraEvent(evt: Record<string, unknown>, sessionId: string, stderrLines: string[], state: MastraMapperState): StreamEvent | null; /** * AIP-45 protocol arm: `protocol: "proprietary"`. * * Loads an NPM adapter package (named in the manifest's `adapter` * field) implementing `AgentCliClient`. The package translates the * vendor's REPL/proprietary stream into the canonical StreamEvent * taxonomy. * * Unlike the `acp` / `print` arms (built into this package, driving a * spawned subprocess), a proprietary arm may not involve a subprocess * at all — e.g. an in-process SDK integration. `createAgentCliRuntime` * skips the subprocess spawn entirely for `protocol: "proprietary"` * (see `define-agent-cli.ts`); this loader has no opinion on whether * the loaded package owns a child process, a socket, or nothing. * * ## Adapter package contract * * The named package's default export path (its `main`/`exports["."]`) * must export a factory: * * export function createAgentCliClient( * definition: AgentCliHandle, * ): AgentCliClient | Promise<AgentCliClient> * * A default export function is accepted as a fallback for adapters * that only have one thing to export. The factory receives the full * manifest handle so adapter-specific config (models, capabilities, * metadata) is available without a second lookup — mirrors the ACP * arm receiving `clientInfo` derived from the manifest. */ interface ProprietaryProtocolOptions { /** NPM package name to load — from manifest.adapter. */ adapter: string; /** Full manifest handle, forwarded verbatim to the loaded factory. */ definition: AgentCliHandle; } declare function createProprietaryProtocolArm(options: ProprietaryProtocolOptions): Promise<AgentCliClient>; /** Adapters with a verified switch for excluding host instruction files. */ declare function agentCliSupportsHostContextIsolation(adapterId: string): boolean; /** * `claudeMdExcludes` entries covering every instruction file Claude Code * would load from a STRICT ancestor of `cwd` (the host monorepo around an * installed app). `cwd`'s own CLAUDE.md is deliberately left loadable — it * is the app's. Both the lexical and the symlink-resolved ancestor chains * are covered, since the SDK may walk either. */ declare function hostContextExcludes(cwd: string): string[]; /** * Isolated adapter state home for OS-confined spawns. * * Some CLIs keep mutable runtime state in a home directory under `$HOME` * (codex: `~/.codex` — a sqlite state db, `tmp/arg0` PATH aliases, session * rollouts). The OS sandbox denies `$HOME` outright, so a confined spawn of * such a CLI dies at startup ("failed to initialize sqlite state runtime"). * * Granting the REAL home writable would let a confined agent rewrite the * operator's own config (codex `config.toml`: MCP servers, sandbox mode) for * every later, unconfined run, and read every other session's transcripts. * Instead the confined spawn gets its own home — the per-session-lineage * `configDir` when the host keys one (so native resume finds its rollouts * after a respawn), else a throwaway temp dir — pointed at by the CLI's home * env var. Only the files the definition lists in `share` (the login file) * are symlinked back to the real home, and only those exact files are * granted through the sandbox, so a token refresh written through the link * lands in the operator's real login rather than a diverging copy. */ /** Subdir of the session `configDir` holding a login-less data home. */ declare const CREDENTIAL_DATA_SUBDIR = "auth-data"; /** * Whole-tree teardown for a spawned adapter process. * * `child.kill("SIGTERM")` alone signals only the direct child — for most * adapters that is an `npx`/`npm exec` wrapper, and the wrapper can ignore * SIGTERM (observed live) while the real adapter, its MCP servers, and any * headless Chrome they started keep running as its descendants. Worse, once * the wrapper does die those descendants reparent to init and are no longer * findable from the child's pid at all. * * So the tree is snapshotted (`ps -axo pid=,ppid=`) BEFORE anything is * signalled, every member gets SIGTERM, and whatever is still alive after a * grace period — plus anything it spawned meanwhile — gets SIGKILL. */ interface ProcessRow { pid: number; ppid: number; } interface TerminateTreeOptions { /** How long SIGTERM gets before survivors are SIGKILLed. Default 3000ms. */ graceMs?: number; /** Liveness poll interval during the grace period. Default 100ms. */ pollMs?: number; /** Process table source — injectable for tests. Default `ps -axo pid=,ppid=`. */ listProcesses?: () => Promise<ProcessRow[]>; } interface TerminateTreeResult { /** Descendant pids found in the pre-signal snapshot (root excluded). */ descendants: number[]; /** Pids (root included) still alive after the grace period, SIGKILLed. */ killed: number[]; } /** Every pid below `rootPid` in `rows` (breadth-first, root excluded). */ declare function descendantsOf(rootPid: number, rows: readonly ProcessRow[]): number[]; /** * SIGTERM `child` and its whole process tree, then SIGKILL whatever survives * `graceMs`. Resolves once the tree is gone or the SIGKILLs are sent. * * The root is signalled through `child.kill()` and its liveness read off * `exitCode`/`signalCode`, never a raw `process.kill(child.pid)` — a child * whose spawn failed has no pid to signal (see the runtime's * `killChildIfSpawned`), and a root that already exited is a no-op, so this * never touches a pid it didn't verifiably spawn and still own. */ declare function terminateChildTree(child: ChildProcess, opts?: TerminateTreeOptions): Promise<TerminateTreeResult>; /** * Per-session skill delivery — the driver half of the skill-imports feature. * * The daemon resolves imported skills at spawn (see `@agentproto/runtime`'s * `resolveSessionSkills`) and hands them down as * {@link AgentCliStartOptions.sessionSkills}. This module copies each skill * folder into the session's isolated config dir, at a location the ADAPTER * declares so its harness discovers the skills natively under its own * isolated home: * * <adapterConfigDir>/<dir>/<alias>/SKILL.md * * The location and any env var naming it come from the adapter manifest's * `metadata.sessionSkills` field ({@link SessionSkillsMetadata}) — an adapter * that declares none gets no delivery (the field is the opt-in). The whole * `<dir>` tree is wiped first, so a respawn never leaves a stale skill behind. * * For opencode this is the only route that survives `lean` mode: the mode sets * `OPENCODE_DISABLE_EXTERNAL_SKILLS=1`, which gates opencode's AUTO-DISCOVERY * of `~/.claude/skills`, `~/.agents/skills`, and project-ancestor `.claude`/ * `.agents` dirs — but NOT its own config-dir scan. Pointing `OPENCODE_CONFIG_DIR` * at the session dir (whose `skills/` we populate) loads the skills regardless * of that flag, because opencode's `Skill.discovery` scans every config dir * (including `$OPENCODE_CONFIG_DIR`) unconditionally. Verified against the * installed opencode 1.18.x/1.19.x binary; see the report. */ /** Placeholder an adapter manifest uses in `metadata.sessionSkills.env` values * for "the absolute session config dir" (the base that holds `dir`). */ declare const SESSION_SKILLS_ABS_DIR = "<abs dir>"; /** Shape of an adapter manifest's `metadata.sessionSkills`. */ interface SessionSkillsMetadata { /** * Skill dir RELATIVE to the session config dir — the copy target is * `<adapterConfigDir>/<dir>/<alias>/`. claude-code and opencode both use * `"skills"` (the flat one-subdir-per-skill shape). */ dir: string; /** * Env vars to point at the session config dir so the harness discovers the * delivered skills under its isolated home. The literal * {@link SESSION_SKILLS_ABS_DIR} in a value is substituted with the absolute * session config dir. opencode needs `OPENCODE_CONFIG_DIR`; claude-code * needs none (`CLAUDE_CONFIG_DIR` is already the session dir). */ env?: Record<string, string>; } /** * Read + validate `metadata.sessionSkills` from an adapter manifest. Returns * `undefined` when absent or malformed (an adapter that opts out, or a manifest * typo) — delivery is purely additive and must never break a spawn. */ declare function readSessionSkillsMetadata(metadata: Record<string, unknown> | undefined): SessionSkillsMetadata | undefined; interface ApplySessionSkillsArgs { skills: readonly AgentCliSessionSkill[]; meta: SessionSkillsMetadata; /** The absolute session config dir that holds `meta.dir`. */ baseDir: string; /** Mutated in place with the resolved `meta.env` entries. */ env: Record<string, string>; } interface ApplySessionSkillsResult { /** The delivered skills dir — an extra write path for a confined spawn. */ targetDir: string; /** Non-fatal per-skill problems (a bad alias, a copy failure). Delivery is * additive and must NEVER sink the spawn — one broken skill is reported and * skipped, the rest still land. */ warnings: string[]; } /** * Copy each skill folder into `<baseDir>/<dir>/<alias>/`, wiping `<dir>` first. * A copy (not a symlink) so the delivered tree survives the source moving, and * a recursive copy so multi-file skills (`scripts/`, references) come along. * Per-skill failures are collected as warnings, never thrown. */ declare function applySessionSkills(args: ApplySessionSkillsArgs): ApplySessionSkillsResult; /** * Shared converter from this repo's generic `AcpMcpServer[]` * (`{ name, transport: "stdio"|"http"|"sse", ref? }`) to the * `Record<string, McpServerConfig>` shape mastracode expects — both * for the in-process SDK config (`MastraCodeConfig.mcpServers`) and * for the file-based `.mastracode/mcp.json` the print arm writes. * * The target type is structurally identical in both contexts: * - stdio → `{ command, args?, env? }` * - http → `{ url, headers? }` * - sse → `{ url }` (mastracode's `McpHttpServerConfig` doc says * it handles SSE through the same `url` field, no separate * discriminator) * * Neither `McpServerConfig`/`McpStdioServerConfig`/`McpHttpServerConfig` * is exported from `mastracode`'s public surface (only `.`, `./tui`, * `./acp`, `./headless`, `./plugin` subpaths are exported — no `./mcp`), * so we define a local, structurally-matching type here. TS structural * typing means a plain object literal shaped correctly type-checks fine * against `MastraCodeConfig.mcpServers` without a named import. * * Mirrors EXACTLY the same transport→field semantics the ACP arm uses * in `toAcpMcpServer` (`@agentproto/acp/client`), just flattened to * mastracode's keyed-record shape instead of the ACP wire array. */ /** * mastracode's per-server config entry (structural — not imported from * `mastracode`, which doesn't export these types from its public * surface). Detected structurally by mastracode: `command` present → * stdio, `url` present → http. Both keys present → entry is skipped * (mastracode's own validation). */ type MastracodeMcpServerConfig = { command: string; args?: string[]; env?: Record<string, string>; } | { url: string; headers?: Record<string, string>; }; /** * Convert the repo's compact `AcpMcpServer[]` into mastracode's * `Record<string, McpServerConfig>` keyed by server name. Returns * `undefined` for an empty/missing input so callers can spread the * result conditionally (`...(cond ? { mcpServers } : {})`). * * On key collision the caller's entry simply overwrites — this matches * mastracode's own "higher precedence overrides lower, by server name" * merge semantics for both the SDK config field and the file-based * load-order. */ declare function toFileBasedMcpServers(servers: readonly AcpMcpServer[] | undefined): Record<string, MastracodeMcpServerConfig> | undefined; /** * Thrown when `RuntimeConfig` references manifest entries that don't * exist or violates a declared constraint (type / enum / bounds). The * `code` field lets callers map to friendly UI messages without * inspecting the message string. */ declare class RuntimeConfigError extends Error { readonly code: "unknown_mode" | "unknown_option" | "option_type_mismatch" | "option_enum_violation" | "option_bounds_violation" | "unsupported_continuation" | "model_denied" | "missing_auth_credential"; readonly path: string; constructor(code: RuntimeConfigError["code"], path: string, message: string); } /** * Resolved spawn args + env. The runtime's `start()` impl merges * `env` into the host process env / sandbox env before exec. */ interface ComposedSpawn { binArgs: string[]; env: Record<string, string>; /** * Env keys the active mode AND active options declared for deletion * (AgentCliMode.env_unset / AgentCliOption.env_unset). Surfaced * pure-side so the runtime's `start()` — which owns the process.env * merge — can apply the deletions at the one point where the ambient * env is actually present (compose starts from `{}` and can't delete * what isn't there yet). Empty when nothing declares any. */ envUnset: string[]; } /** * Optional spawn-time hints that are outside the pure runtime config. * Today the only consumer is auth-aware codex model handling: the * composer can be told to skip `models.apply:"arg"` when the caller has * already determined that the current auth mode must preserve the CLI's * default model. */ interface ComposeSpawnOptions { skipModelArg?: boolean; } /** * Compose the final spawn args from a manifest + per-call config. * Pure — no I/O, no sandbox access, no global state. Safe to call * repeatedly with the same input. * * `config` may be undefined or an empty object — both fall back to * the manifest's defaults verbatim (manifest `bin_args`, no mode * patch, no option patches). Continuation is not applied here — the * `ContinuationStrategy` registry handles that — but we DO validate * `config.continuation` against `manifest.continuation.supported` so * a misconfigured operator fails fast at compose time, not at the * point the strategy tries to acquire a session. */ declare function composeSpawn(handle: AgentCliHandle, config?: RuntimeConfig, opts?: ComposeSpawnOptions): ComposedSpawn; /** * Resolve the continuation strategy id to use for this call. Operator * config wins; manifest default is the fallback; "none" if neither * declares anything (preserves pre-AIP-45-extension behaviour). */ declare function resolveContinuationStrategy(handle: AgentCliHandle, config?: RuntimeConfig): ContinuationStrategyId; /** * Legacy AIP-45 mode-id classification — the SINGLE SOURCE for recognizing the * gateway (route) and permission (posture) ids that used to be declared as * `modes[]` entries before the SPEC §3.4a route/posture extraction. * * Those entries are now deleted from the adapter manifests: route is derived * from the model catalog's `@route` route-identity and posture comes from the * harness's own ACP mode registry (`SessionModeState.availableModes`) plus the * canonical-posture layer (step 2c). But a caller may STILL pass one of the old * ids as `config.mode` — a persisted OPERATOR.md `runtime.config.mode`, a * `defaults.adapters.<slug>` binding, or an in-flight request written against * the pre-migration manifest. Both the compose path (`composeSpawn`) and the * daemon-boundary shim (`@agentproto/runtime`'s `decomposeMode`) therefore need * to recognize the legacy vocabulary. `@agentproto/runtime` imports this module * rather than re-hardcoding the list, so the id→axis classification stays * single-sourced across the driver/runtime boundary (runtime → driver only; the * driver stays lean, never depending back on the runtime). * * This module classifies onto the AXIS only (`route`/`posture`/`context`); the * canonical-posture value mapping (`bypass-permissions` → `bypass`, …) is a * daemon concept and lives in `@agentproto/runtime`, keyed by * {@link LEGACY_POSTURE_MODE_IDS}. */ /** Gateway-preset ids that were declared as `kind:"route"` modes. */ declare const LEGACY_GATEWAY_MODE_IDS: ReadonlySet<string>; /** * Permission-profile ids that were declared as `kind:"posture"` modes across * the claude-code / codex / opencode adapters (the superset over adapters — * some ids are another adapter's vocabulary: codex's `full-access`, opencode's * `build`). */ declare const LEGACY_POSTURE_MODE_IDS: ReadonlySet<string>; /** * Classify a legacy mode id onto its orthogonal axis. Gateway ids ⇒ `"route"`, * known permission ids ⇒ `"posture"`, everything else ⇒ `"context"` — the * least-privilege default, so a truly-unknown id never silently grants a * posture or reroutes billing (SPEC R4). */ declare function inferLegacyModeKind(modeId: string): "posture" | "route" | "context"; /** * True when `modeId` is a KNOWN legacy route-or-posture id that used to be a * `modes[]` entry but was extracted (SPEC §3.4a) — i.e. it classifies as * `route` or `posture`, not `context` and not a genuinely-unknown id. * `composeSpawn` degrades such an id to a soft no-op (no env/argv patch: route * now resolves from the catalog, posture from the ACP registry / canonical * layer) instead of throwing `unknown_mode`; a truly-unknown id — for which * this returns `false` — still throws. */ declare function isLegacyExtractedModeId(modeId: string): boolean; /** * AIP-45 ContinuationStrategy interface. * * A continuation strategy decides HOW prior conversation turns reach a * spawned CLI on subsequent invocations. Built-ins handle: * * - `none` — fresh session per call (current pre-AIP-45 behaviour) * - `pinned-session` — keep the spawned child alive, reuse across turns * - `transcript` — caller-supplied preamble prepended to each turn * - `native-resume` — pass a session id to the CLI's own `--resume` flag * * Adapter packages MAY register custom strategies via the registry — * for example, a Goose-specific strategy that uses MCP-side session * load semantics. Custom strategy ids require a follow-up AIP that * opens the `ContinuationStrategyId` enum (so the manifest schema can * validate them). * * The strategy owns the SESSION LIFECYCLE: `acquire` returns a session * the caller can `send()` against, and `release` decides whether the * session lives on (pinned-session) or closes immediately (none). The * runner / generation strategy MUST go through `acquire`/`release` — * it MUST NOT call `runtime.start()` / `session.close()` directly when * a strategy is active. */ /** * Per-acquire context the strategy gets. The runner builds this from * the manifest, the operator config, and the current turn's identity. */ interface AcquireContext { runtime: AgentCliRuntime; /** Already-composed start options (cwd / env / signal / config / * turnCtx). Strategies MAY override individual fields when they * call `runtime.start(...)` themselves. */ startOptions: AgentCliStartOptions; /** Per-call config (already validated against the manifest). */ config: RuntimeConfig; /** Identity context for key derivation. */ turnCtx: TurnContext; } /** * Strategies can hint to the runner about how the turn played out so * the strategy can decide whether to keep the session alive (normal * end), reset its TTL but keep it (transient error), or evict it * (dead-process error). */ type ReleaseOutcome = { kind: "completed"; } | { kind: "cancelled"; } | { kind: "error"; reason: "transient" | "fatal"; message: string; }; interface ReleaseContext { session: AgentCliRuntimeSession; outcome: ReleaseOutcome; turnCtx: TurnContext; } /** * The strategy contract. `acquire` returns a session ready for the * caller to `send()` against — fresh OR reused. `release` decides * the session's fate. Strategies MAY hold internal state (e.g. * the pinned-session map) keyed by `turnCtx`. */ interface ContinuationStrategy { readonly id: ContinuationStrategyId; acquire(ctx: AcquireContext): Promise<AgentCliRuntimeSession>; release(ctx: ReleaseContext): Promise<void>; } /** * Derive a stable pin key from `turnCtx` according to the manifest's * `pinned_session.key_scope`. Missing scope fields downgrade to a * less-specific key with a warning — pinning still works, it just * collides more. * * Returns `null` when EVERY scope field the manifest asked for is * missing (no key derivable; strategy falls back to per-spawn). */ declare function deriveKeyFromScope(scope: ContinuationKeyScope[], turnCtx: TurnContext): string | null; /** * Continuation strategy registry. * * Module-scoped singleton — all built-ins are registered at import * time, custom strategies (from adapter packages) register via * `registerContinuationStrategy`. The runner looks up by id; * unregistered ids throw at acquire-time so the caller fails fast * with a clear "this strategy id isn't registered" message. */ /** * Register (or replace) a continuation strategy. Adapter packages * MAY register custom strategies on import — but they MUST first * land an AIP that opens the `ContinuationStrategyId` enum so the * manifest schema accepts the new id. */ declare function registerContinuationStrategy(strategy: ContinuationStrategy): void; declare function getContinuationStrategy(id: ContinuationStrategyId): ContinuationStrategy; declare function listContinuationStrategies(): ContinuationStrategyId[]; /** * `native-resume` strategy — reattach to the agent's own session by id * (ACP `loadSession`, MCP equivalent, or argv-style `--resume`). * * Two-step lifecycle: * 1. **acquire**: look up a persisted sessionId for `turnCtx` via * `loadHook`. If found, pass it to `runtime.start` as * `resumeSessionId` so the protocol arm reattaches. If not, spawn * fresh — and after the session is up, capture the new id from * `session.sessionId` and persist it via `saveHook`. * 2. **release**: close the spawned process. The session lives in * the agent's storage layer (e.g. Claude Code's JSONL files); * cold-start resume on the next acquire reads from that store. * * Hooks are registered once per host process via * `configureNativeResume({ load, save })`. Without hooks the strategy * degrades to per-spawn behaviour with a one-line warning — it's not * fatal because the spawn still works, just without continuity. * * Requires the manifest to declare `capabilities.resumable: true` * AND the agent to advertise the matching protocol capability (e.g. * ACP `loadSession: true`). The schema enforces the manifest side at * validation; runtime capability mismatch surfaces from the protocol * arm as the agent's own error. */ interface NativeResumeHooks { /** Look up a persisted sessionId for the given identity scope. Return * undefined when no prior session exists — strategy spawns fresh. */ load: (turnCtx: TurnContext) => Promise<string | undefined>; /** Persist a freshly-established sessionId so the next cold start * can resume. Called once per "fresh spawn" acquire (not per turn). * Idempotent / upsert semantics expected on the host side. */ save: (turnCtx: TurnContext, sessionId: string) => Promise<void>; /** Optional: drop the persisted entry when a session is detected as * unresumable (agent rejected loadSession with a hard error). */ forget?: (turnCtx: TurnContext) => Promise<void>; } /** * Register the host's session-id load/save callbacks. Call once at * boot. Subsequent calls overwrite — useful in tests; in prod treat * the registration as exclusive. */ declare function configureNativeResume(hooks: NativeResumeHooks): void; /** * @agentproto/driver-agent-cli — AIP-45 AGENT-CLI.md `defineAgentCli` * reference impl. * * Spec: https://agentproto.sh/docs/aip-45 * * Authoring paths: * - TS: `defineAgentCli({...})` → `AgentCliHandle` * - MD: `parseAgentCliManifest(src) → agentCliFromManifest({...})` → `AgentCliHandle` * * Runtime: `createAgentCliRuntime(handle)` → spawn binary, dispatch * turns through the protocol arm (acp / mcp / proprietary), normalise * events to {@link StreamEvent}. */ declare const SPEC_NAME: "agentcli-interactive/v1"; declare const SPEC_VERSION: "0.1.0-alpha"; export { type AcpPermissionHandler, type AcpPermissionOutcome, type AcpPermissionRequestParams, type AcquireContext, AgentCliClient, AgentCliDefinition, AgentCliHandle, type AgentCliModelOptions, AgentCliPrintConfig, AgentCliRuntime, AgentCliRuntimeSession, AgentCliSessionSkill, AgentCliStartOptions, type ApplySessionSkillsArgs, type ApplySessionSkillsResult, type ArmSessionControls, CREDENTIAL_DATA_SUBDIR, type ComposedSpawn, ContinuationKeyScope, type ContinuationStrategy, ContinuationStrategyId, type FileReadingOptions, type JcodeMapperState, LEGACY_GATEWAY_MODE_IDS, LEGACY_POSTURE_MODE_IDS, type MastraMapperState, type MastracodeMcpServerConfig, type ModelLike, type NativeResumeHooks, PROVIDER_KEY_ENV, type PrintArmOptions, type ProcessRow, type ProprietaryProtocolOptions, type ReleaseContext, type ReleaseOutcome, RuntimeConfig, RuntimeConfigError, SESSION_SKILLS_ABS_DIR, SPEC_NAME, SPEC_VERSION, type SessionSkillsMetadata, SetModelResult, type TerminateTreeOptions, type TerminateTreeResult, TurnContext, agentCliSupportsHostContextIsolation, applyModelCommand, applySessionSkills, autoAllowPermissionHandler, composePrompt, composeSpawn, configureNativeResume, createAcpProtocolArm, createAgentCliRuntime, createArmSessionControls, createJcodeMapperState, createMastraMapperState, createPrintSession, createProprietaryProtocolArm, datasetPreamble, defineAgentCli, deriveKeyFromScope, descendantsOf, getContinuationStrategy, hostContextExcludes, inferLegacyModeKind, isLegacyExtractedModeId, isModelSwitchAcknowledgement, listContinuationStrategies, makeAgentCliModel, mapJcodeEvent, mapMastraEvent, parseModelSwitchCommand, planModePermissionHandler, promptTurn, readSessionSkillsMetadata, registerContinuationStrategy, resolveCliEnv, resolveContinuationStrategy, resolveSpawnBin, spawnEnforcedPostures, terminateChildTree, toFileBasedMcpServers };