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

803 lines (778 loc) • 39.9 kB
import { A as AgentCliHandle, a as AgentCliRuntime, b as AgentCliDefinition, c as AgentCliRuntimeSession, d as AgentCliClient, S as SetModelResult, e as AgentCliPrintConfig, R as RuntimeConfig, C as ContinuationStrategyId, f as AgentCliStartOptions, T as TurnContext, g as ContinuationKeyScope } from './schema-CKrBhnuF.js'; export { h as AgentCliAuth, i as AgentCliCapabilities, j as AgentCliConnectOptions, k as AgentCliContinuation, l as AgentCliFrontmatter, m as AgentCliFsZones, n as AgentCliInstallMethod, o as AgentCliMcpBlock, p as AgentCliMode, q as AgentCliModelEntry, r as AgentCliModels, s as AgentCliOption, t as AgentCliOptionType, u as AgentCliPinnedSessionTuning, v as AgentCliPresetDeclaration, w as AgentCliProtocol, x as AgentCliRouteSelection, y as AgentCliSession, z as AgentCliSessionMode, B as AgentCliSetupPersist, D as AgentCliSetupSkipIf, E as AgentCliSetupStep, F as AgentCliVersionCheck, G as RuntimeConfigInput, H as SetEffortResult, I as SetSessionModeResult, J as SteerOutcome, K as agentCliFrontmatterSchema, L as runtimeConfigSchema } from './schema-CKrBhnuF.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>; declare function createAgentCliRuntime(definition: AgentCliHandle): AgentCliRuntime; /** * 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. */ declare function promptTurn(arm: AgentCliClient, turnId: string, message: unknown): 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[]; /** * 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, AgentCliStartOptions, type ArmSessionControls, 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 ProprietaryProtocolOptions, type ReleaseContext, type ReleaseOutcome, RuntimeConfig, RuntimeConfigError, SPEC_NAME, SPEC_VERSION, SetModelResult, TurnContext, agentCliSupportsHostContextIsolation, applyModelCommand, autoAllowPermissionHandler, composePrompt, composeSpawn, configureNativeResume, createAcpProtocolArm, createAgentCliRuntime, createArmSessionControls, createJcodeMapperState, createMastraMapperState, createPrintSession, createProprietaryProtocolArm, datasetPreamble, defineAgentCli, deriveKeyFromScope, getContinuationStrategy, hostContextExcludes, inferLegacyModeKind, isLegacyExtractedModeId, isModelSwitchAcknowledgement, listContinuationStrategies, makeAgentCliModel, mapJcodeEvent, mapMastraEvent, parseModelSwitchCommand, planModePermissionHandler, promptTurn, registerContinuationStrategy, resolveCliEnv, resolveContinuationStrategy, toFileBasedMcpServers };