# Claude Agent Runtime Provider

## IRON LAW — Do not take over Claude lifecycle

Cradle **must not** own Claude Agent SDK turn/session scheduling. The long-lived SDK `Query` + `AsyncIterable` is the authority for turns, coalescing, `result` boundaries, interrupt / `still_queued`, and per-UUID cancel. Cradle only **projects** SDK events into UI Runs, messages, SSE, queue rows, and observability.

**Forbidden failure mode (do not reintroduce):** closing the active user UI run on an empty/early top-level `result`, clearing `currentTurn`, then opening an `origin: system` synthetic run so the same Claude work continues under a second Cradle run id. That is lifecycle takeover disguised as projection.

If a change requires Cradle to schedule Claude turns again: stop and redesign. Prefer deleting wrong scheduling seams over adding more lifecycle ownership.

---

Owns the Claude Agent SDK adapter for Chat Runtime. This provider translates Cradle `UIMessage` turns into Claude Agent SDK streaming input and maps SDK output back into AI SDK `UIMessageChunk` events.

Selected chat Skills arrive as Cradle-owned `data-cradle-skill` message parts. When the provider creates a Query, it removes them from the text/image input blocks and merges their names into Claude Agent SDK `queryOptions.skills` unless the profile already enables `skills: "all"`. Skills, configured effort, tools, disallowed tools, and MCP servers are fixed for that Query's lifetime. Only permission mode, the ultracode effort flag, and a pending model switch synchronize live.

Claude SDK persistence is enabled for API-key main chat turns, and that SDK config root is pinned to Cradle-owned runtime data through `CLAUDE_CONFIG_DIR`. The path follows the Codex app-server convention: `CRADLE_DATA_DIR/runtimes/claude-agent`, then `dirname(CRADLE_DB_PATH)/runtimes/claude-agent`, then `~/.cradle/runtimes/claude-agent`. This lets the SDK own native JSONL transcripts for resume without writing new sessions into the user's `~/.claude/projects` namespace.

Official Claude.ai subscription auth intentionally does not set Cradle's `CLAUDE_CONFIG_DIR`. If the user's shell provides a custom Claude config directory, Cradle preserves it; otherwise the SDK and CLI read the user's normal Claude login state from the default Claude config / secure-storage path. Main turns in this auth mode set `persistSession: false`, so Cradle does not ask the SDK to write native resumable transcripts into the user's `~/.claude/projects`. A fresh, non-resuming Query receives bounded Cradle-owned history once; later turns reuse that Query's in-memory transcript. A future SDK `SessionStore` integration could restore native resume while keeping auth-root and transcript-root ownership separate.

Claude session titles are read from SDK session metadata with `getSessionInfo()` after a provider session id is known, then reported through Chat Runtime's title callback. SDK session metadata reads and `renameSession()` writes are scoped to the same Cradle-owned SDK config root and the active SDK project `cwd`. Cradle owns the final `sessions.title` write.

Agent-scoped Claude Agent sessions use `~/.cradle/agents/{agentId}` as SDK `cwd`. The original project workspace remains explicit through SDK `additionalDirectories` and `CRADLE_WORKSPACE_PATH`; agent context is also passed through `CRADLE_AGENT_ID` and `CRADLE_AGENT_HOME`. The agent home is initialized by the Skills module, including `.agents/skills` and `.claude/skills` links to the Cradle-owned agent `skills/` directory.

Stored Cradle chats with a provider session id pass SDK `resume`, allowing Claude Agent to restore its native transcript. Only a fresh, non-resuming Query receives bounded full history in its first prompt. A live or resumed Query does not replay normal Cradle user/assistant messages because it has already consumed them; it receives only recent Cradle-local context such as local bang-command output that may not exist in the SDK transcript.

Query creation passes the resolved model through SDK query options and model alias environment variables. A pending model switch is applied to the live Query through SDK `setModel()` regardless of auth mode, then cleared from the provider snapshot.

Claude Agent sessions own runtime settings through the native SDK `permissionMode` axis (`default`, `acceptEdits`, `bypassPermissions`, `plan`). A new SDK Query starts in the session's configured mode; later changes use `setPermissionMode()` on the live Query. Cradle stores that value directly in session config; legacy `accessMode` × `interactionMode` pairs are migrated to `permissionMode` on read. Composer UI, queue items, and desktop bridge pass provider-native settings without collapsing them into the Codex 2D model. Hard structural denies (ExitPlanMode capture and AskUserQuestion being unavailable in bypass mode) are enforced by a `PreToolUse` hook in every permission mode; interactive approvals remain on `canUseTool` and are mode-dependent by design.

Cradle's canonical `ultra` thinking choice maps to the Claude SDK's session-scoped `ultracode` setting, which enables `xhigh` effort with standing dynamic-workflow orchestration. A live SDK query receives the change through `applyFlagSettings`; switching away clears the flag again. The SDK remains the authority for account, workflow, and model eligibility.

Claude SDK `EnterPlanMode` is intercepted as a provider-owned request to switch the session `permissionMode` to `plan` through Chat Runtime settings, so composer state and the active SDK permission mode share one owner. Claude SDK `ExitPlanMode` remains available in SDK plan mode as the provider-owned signal for submitting a proposed plan. Cradle captures that tool input into its existing tool chunk envelope, stores the latest captured plan in the Claude Agent provider snapshot, exposes it through the runtime-neutral `plan` UI slot, projects a synthetic `plan_implementation` approval so the renderer can submit `PLEASE IMPLEMENT THIS PLAN:` as an ordinary follow-up, denies the native exit action, and keeps runtime permission state changes owned by Chat Runtime settings (`permissionMode` back to the user's prior full-access or approval mode).

Claude Agent SubAgent output is provider-thread-first. Active child SDK messages carrying `parent_tool_use_id` or task lifecycle `tool_use_id` are mapped with the normal Claude Agent chunk mapper but published through Chat Runtime provider-thread events, not through the parent chat queue. Parent `Agent` rows remain tool lifecycle/reference rows; full child transcripts hydrate and stream through provider-thread APIs. Claude SDK `canUseTool` approvals from a SubAgent carry `agentID`; when the provider can resolve that agent id to the parent tool-call provider thread, the approval chunks are published to the child provider-thread stream so the Browser Panel detail view can approve or deny without leaking child activity into the parent transcript.

Assistant SDK timestamps are preserved in message metadata and own usage-event occurrence time, with receive time used only for older emitters. The SDK's structured `tool_result_meta` sidecar classifies denied, interrupted, and cancelled non-executions as terminal tool errors without parsing provider prose. Structured `subagent_retry` progress updates the existing crew call and exposes the agent as `retrying`, including attempt, retry limit, delay, status, and category in the shared crew UI slot.

Claude Workflow observation is a dedicated provider submodule under [`workflow/`](./workflow/README.md). It owns execution projection, declaration discovery, JSONL/final-event normalization, state reduction, artifact watching, and cleanup separately from generic SubAgent threads.

## Native Session Ownership

One Cradle chat session owns one long-lived Claude SDK `Query` and one continuous `AsyncIterable<SDKUserMessage>`. The Query remains alive across top-level `result` messages: `result` closes a native turn, not the session. Claude owns native input queueing, priority coalescing, background continuation, interrupt, and per-UUID cancellation.

Input submission is independent from output projection. User sends, live Guide messages, and durable queue submissions push directly into the SDK input stream with an explicit native priority. They never wait for a Cradle UI Run, synthetic projection Run, transcript write, or queue-row transition. The pump continues consuming SDK messages in arrival order; top-level output after a prior **non-empty** `result` opens a new system-origin UI Run when needed, but that Run is only an SSE/persistence container and has no input authority. An empty/early top-level `result` must **not** clear the active user `currentTurn` — subsequent Query output stays on the same user UI run (see IRON LAW above). Do not finalize that deferred empty turn because the input stream is waiting for the next prompt pull (`isWaitingForPull`); that race clears `currentTurn` before follow-on assistant frames arrive and forces a system synthetic run. Empty turns with no follow-on close on the next user `streamTurn`, Query teardown, or user `cancelTurn` (abort — interrupt alone cannot settle a turn whose native `result` already arrived).

Durable queue rows are submitted-input records. A live submission stamps the queue row UUID onto `SDKUserMessage.uuid`. Claude Code's `msg_lifecycle_v1` protocol emits `command_lifecycle` events carrying that UUID as `command_uuid`, with `queued`, `started`, and terminal `completed` / `failed` / `cancelled` states. Those events are the only completion authority; `result` remains only a native turn boundary. Multiple command UUIDs may complete around one coalesced native turn without inventing one-row-one-turn Runs. There is no user-echo assumption, prompt-text matching, settle timer, or adopt path.

Native durable submit is rejected when the CLI does not advertise or emit `msg_lifecycle_v1`; Cradle does not create an unfinishable submitted row. On Query teardown, an input observed only as `queued` is safe for drain to submit through a replacement Query. An input observed as `started` (or pushed without any lifecycle acknowledgement) is marked failed when no terminal lifecycle arrives, preventing automatic duplicate execution after an ambiguous process loss. The SDK 0.3.207 type declarations do not yet include the live `command_lifecycle` wire message, so the provider owns a narrow observed-wire type until the SDK snapshot catches up.

Stopping a current turn settles an unprojected or deferred-empty UI turn locally with `{ type: 'abort', reason: 'user' }`, issues `interrupt()` as a best-effort control request without awaiting it, and closes the live Query. This is the cancel-path detach: the next prompt builds a fresh Query from the last provider session id, so late cancellation fallout cannot settle or zombie the next turn. When output has already been projected, Chat Runtime's cancel path owns the terminal projection while the provider still tears down the Query. UUIDs reported in `still_queued` are logged as facts; after detach, only inputs still observed as native `queued` remain safe for drain to resubmit on a replacement Query. Cancelling one submitted row calls `cancelAsyncMessage(uuid)` first; Cradle cancels the durable row only when Claude returns `true`.

Chat Runtime releases the product active-run claim and publishes the abort notification before awaiting provider `cancelTurn` / `interrupt`, so session admission and Stop UI are not pinned on the Claude control ack.

Claude Agent uses Synara-aligned **queue-fallback steer**. Capability is `steer: 'queue-fallback'`: Cmd/Ctrl+Enter while busy enqueues a durable front-of-queue item labeled `continuation.mode = 'steer'` and interrupts the live turn so that item drains next. There is **no** product mid-turn SDK input push for Composer steer or Enter=queue — `enqueueSessionQueueItem` never calls live `submitNativeInput`. Codex remains the only `steer: 'native'` live same-turn path.

Explicit durable queue follow-ups (plain Enter while busy) use Cradle's session queue and drain a new Run after the active turn completes. Low-level Claude UUID input-stream helpers remain on the live registry for tests and explicit native cancel, but product enqueue does not auto-submit them.

The long-lived SDK query may continue emitting messages after the parent Cradle run reaches its first top-level `result`, for example when a background Agent completes. Post-turn events are split by explicit SDK lineage: messages with `parent_tool_use_id` remain provider-thread output, `task_started`/`task_progress`/`task_notification` update provider-owned crew or task state without claiming conversation ownership, and later top-level assistant output starts a system-origin Cradle run that uses the normal active-run replay/SSE stream before becoming a durable assistant message. A background task's `task_id` is lifecycle identity, not a substitute for `parent_tool_use_id`.

Claude SDK `TodoWrite` remains a Claude-owned tool, but the adapter also projects the latest normalized todo list into the Claude Agent provider snapshot and exposes it as the runtime-neutral `progress` UI slot. Claude SDK `TaskCreate`, `TaskUpdate`, and `TaskList` also feed the same progress slot when the SDK provides structured task input/output such as `tool_use_result`; the adapter does not parse human-facing task result strings for IDs. The tool result payload still carries `result.pluginState.todos` for TodoWrite transcript rendering; the provider snapshot is the source for composer-adjacent live progress state.

## Testing

Unit tests with a fake SDK `query` factory own provider-local construction and projection assertions: query options, effort mapping, MCP servers, Skills, and snapshot writers. Tests under `integration/` use one shared model API simulator and the real bundled Claude CLI for behavior that crosses the SDK/process/wire boundary: history payloads, permission gating, cancel teardown, project settings, and HTTP request shapes. New scheduling or lifecycle claims belong in integration tests unless the test is explicitly limited to provider-local state.

## Files

- `provider.ts`: Thin Claude Agent `ChatRuntime` facade and owner of long-lived query lifecycle, turn streaming, native input, cancellation, and disposal.
- `provider-internals.ts`: Shared provider-private lifecycle records passed explicitly to extracted owners.
- `message-dispatch.ts`: Routes SDK session messages and command lifecycle events to active, provider-thread, and synthetic turn projections.
- `projection/provider-thread-turns.ts`: Owns provider-thread and synthetic-turn creation, publication, completion, approval events, and usage projection.
- `presentation.ts`: Owns slash-command presentation, runtime UI slot projection, context usage reads, and compact-state caching.
- `live-settings.ts`: Synchronizes live query permission mode and ultracode settings with runtime settings.
- `session-artifacts.ts`: Owns session titles, account snapshots, provider-thread filesystem reads, and transcript projection.
- `quick-question.ts`: Runs isolated non-persistent quick questions through the SDK.
- `sdk-augmentation.d.ts`: Narrow declaration merge for the real SDK `Query.cancelAsyncMessage()` method missing from 0.3.207 types.
- `provider.test.ts`: Regression tests for Claude Agent SDK options, title projection, MCP forwarding, history projection, streaming, non-blocking input, native steer, UUID coalescing/cancellation, post-result projection Runs, attachments, model switching, and tool chunk mapping.
- `metadata.ts`: Claude Agent runtime kind, catalog metadata, static capabilities, slash-command presentation projection, and static runtime UI slots. Slash commands are served from the live Query and replaced on `commands_changed`; only a session without a live Query spawns a short-lived Query for presentation discovery.
- `types.ts`: Claude Agent provider-private content and session-info types shared by package modules.
- `runtime-context.ts`: Resolves per-session Claude Agent cwd, agent home, project workspace path, SDK additional directories, and the Cradle-owned SDK config root used by API-key mode.
- `input-projector.ts`: Projects Cradle message input, history, selected Skills, provider config, and environment into Claude Agent SDK content and query options.
- `context-usage-projector.ts`: Projects Claude Agent SDK context usage control responses into Chat Runtime context usage details and compact UI slot state.
- `async-input-stream.ts`: Claude Agent SDK async user-message input stream built on shared provider queue infrastructure.
- `permission-bridge.ts`: Provider-owned bridge between SDK permission callbacks and Chat Runtime semantics, including AskUserQuestion user-input requests, plan-mode denial, pending tool approvals, and SubAgent `agentID` metadata forwarding.
- `state-projector.ts`: Projects Claude Agent provider snapshot state such as pending resumed-session model switches, captured plan UI slot state, captured TodoWrite/Task progress state, and crew/subagent provider-thread ids.
- `event-to-chunk-mapper.ts`: Maps Claude Agent SDK messages into AI SDK `UIMessageChunk` events.
- `event-to-chunk-mapper.test.ts`: Mapper-level regression tests.
- `tools/`: Claude Code tool identity, todo/task progress projection, and tool envelope mapping.
