@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
TypeScript
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 };