import type { ModelTokenUsage, TokenUsage } from "@skaile/workspaces/types"; import type { DriverTarget } from "@skaile/workspaces/plugin-registry"; import { type ModelEntry } from "../models.js"; import { type AgentConfig, AgentDriver, type DriverInfo, type SlashCommandInfo } from "../types.js"; /** One entry of the SDK result's `modelUsage` map (`ModelUsage` in the SDK's `sdk.d.ts`). */ type SdkModelUsage = { inputTokens?: number; outputTokens?: number; cacheReadInputTokens?: number; cacheCreationInputTokens?: number; costUSD?: number; contextWindow?: number; }; /** * Turn the SDK's run-cumulative `modelUsage` into this turn's per-model share. * * Each counter is the difference against `previous` (the map from the last * result of the same SDK run); `previous === null` means the run just started, * so the whole map is the turn. A model whose counter went backwards is * re-baselined the way the cost delta treats a decrease in `total_cost_usd`: * its current values are the turn. Models whose counters did not move are * dropped, and `contextWindow` is carried as-is since it is a property, not a * counter. Returns `null` when no model moved. */ export declare function diffModelUsage(current: Record, previous: Record | null): Record | null; export declare class ClaudeSdkDriver extends AgentDriver { readonly driverInfo: DriverInfo; private readonly log; private config; private abortController; private running; private sdk; private toolIdToName; private prevText; /** * Count of sanitized characters already streamed for the current message. * Lets {@link handleStreamEvent} emit only the newly-stable sanitized suffix * as `_textDelta`, so a leaked marker that spans deltas is never half-sent. */ private emittedSanitizedLen; /** Set once the streaming text of the current message classifies as a self-healable auth render. */ private suppressingAuthStream; /** * The auth render this attempt dropped on the promise that a self-heal would * retry — the evidence {@link handleResultErrorPayload} otherwise lacks when * the SDK reports the same 401 as an `is_error` result with an empty * `errors[]`. Written only through {@link stashSuppressedAuthRender}, which * applies a stricter test than suppression does; see its docblock. Cleared at * both ends of {@link runPrompt}, so it can only ever describe the turn that * is failing. */ private suppressedAuthRenderText; /** Active query — stays alive between turns (streaming input mode). */ private query; /** * The live query's prompt stream. Created in {@link startQuery}, fed one user * message per turn, and ended wherever the query is dropped — an ended stream * with a live query means the CLI's stdin is closed and the query can never * accept another turn. */ private inputStream; /** * `true` once any SDK message arrived after the current turn's watchdog was * armed — flips the stall budget from the first-message tier to the mid-turn * ceiling. See {@link armTurnStallWatchdog}. */ private firstSdkMessageSeen; /** When the live query was spawned — the origin for the first-message timing. */ private queryStartedAt; /** * One-shot: armed at each spawn, disarmed by the first message of THAT spawn. * Deliberately not `firstSdkMessageSeen`, which the stall watchdog resets every * turn — a resident query serves turns 2..N without re-spawning, so reusing it * would measure from the original spawn and report the idle time between turns * as SDK latency. */ private awaitingFirstMessageOfSpawn; /** What the spawn actually resumed, captured before a fallback can clear it. */ private spawnResumed; /** * `num_turns` reported by the most recent result message. Feeds * {@link shouldRecycleQuery}; reset on every fresh spawn. */ private lastNumTurns; /** * OS pid of the CLI subprocess backing `query`, captured via * `spawnClaudeCodeProcess` in {@link startQuery}. Feeds * {@link snapshotDescendantPids}; see `_devlog/notes/2026-08-12-abort-kills-tool-subprocess.md`. */ private cliProcessPid; /** Resolves when the current turn completes. */ private turnResolve; /** Rejects when the current turn errors. */ private turnReject; /** * Settled tail for complete logical prompts. Rejections are swallowed only on * the tail so one failed turn cannot wedge later callers. */ private promptTail; /** Guards against duplicate agent_end emissions per turn. */ private turnCompleted; /** `Date.now()` of the most recent SDK message — the watchdog's liveness signal. */ private lastSdkMessageAt; /** Armed while a turn is in flight; trips when the SDK goes silent for too long. */ private turnStallTimer; /** * Armed only while an `is_error` result waits for its follow-up iterator * throw; fails the turn if that throw never lands. Cleared at every turn * start and settle site so it can never fire into a later turn. */ private errorResultGraceTimer; /** * Mirror of the in-flight {@link prompt} call's auth retry budget: `false` * while the `onAuthError` self-heal is still available, `true` once it has * been spent. Read by {@link failTurn} to decide whether to defer the * `agent-event: error` emission for a self-healable auth error. * * Set unconditionally at the top of every {@link runPrompt} attempt; never * reset elsewhere — the next call's set is the only legitimate transition. */ private authSelfHealUsed; /** * Mirror of the in-flight turn's seat-failover budget — same contract as * {@link authSelfHealUsed}, for the `onLimitBlocked` path. */ private limitFailoverUsed; /** * Set when THIS attempt saw an upstream usage-limit rejection: either the * SDK's `rate_limit_event` telemetry with `status: "rejected"`, or an error * result the classifier calls `rate_limit`. * * The telemetry is the load-bearing half. A subscription ceiling arrives as * `{"status":"rejected","type":"five_hour"}` followed by an error result * whose text ("You've hit your session limit · resets 9:50am") matches none * of the classifier's rate-limit keywords — so without this flag the * rejection classifies as `unknown` and no failover is reachable at all. * * Reset at the top of every {@link runPrompt} attempt, so the replay after a * failover observes its own outcome rather than inheriting the first one's. */ private limitRejectionSeen; /** * True when {@link handleResultErrorPayload} rejected this turn ONLY so the * seat-failover branch could run — a turn that, before this existed, would * have emitted its error and resolved. If the failover then declines, the * rejection is undone so a limit block with no peer available looks exactly * as it always has downstream (one error event, one `agent_end`, a resolved * promise) rather than also drawing the runner's fatal "prompt failed". */ private limitTurnRejectedForFailover; /** * Whether this attempt already reported a provider rejection. One upstream * failure routinely reaches the driver twice — `rejected` telemetry followed * by the error result, or an `is_error` result followed by the iterator throw * carrying the real text — and these observations feed counters, so counting * one rejection twice would overstate the seat's pressure. */ private providerResponseReported; /** Limit-window metadata from this attempt's rejection telemetry, if any. */ private lastLimitInfo; /** * The `agent-event: error` {@link failTurn} withheld because a recovery * (auth self-heal or seat failover) was about to run. The recovery emits it * if it declines, and drops it if it retries — so a recovered turn shows the * user nothing and a failed one shows exactly one error. */ private deferredTurnError; /** Tracks whether a session has been started (for continue: true). */ private hasSession; /** Session ID from the SDK — used for streamInput messages. */ private sessionId; /** * Set while the SDK is paused inside `canUseTool` on an `AskUserQuestion` * call, waiting for the user's answer. The SDK delivers every sub-question in * one call (one `toolUseID`) and the turn stays paused until we resolve with * a {@link PermissionResult}. We accumulate `answers[questionText] = reply` — * the same keying the SDK expects back — and resolve `allow` once every entry * in `questionTexts` has one. On abort/hibernate the request is denied so the * SDK writes a clean tool_result (no dangling tool_use to poison the resumed * transcript). Null when no question is pending. */ private pendingQuestion; /** Cached slash commands discovered from the SDK. */ private cachedCommands; /** Token usage from the most recent completed turn. */ private _lastTokens; /** * Prompt size of the most recent API request — the real context fill. Kept * apart from `_lastTokens`, whose result-message usage is cumulative over * every API call in the turn and so cannot answer "how full is the context". */ private _lastContextTokens; /** Context window size from the most recent result's modelUsage. */ private _contextWindow; /** * Model id of the last main-thread assistant message. `modelUsage` also * holds subagent and housekeeping models, so this keys the main window. */ private _lastMainModel; /** Per-model usage of the most recent completed turn, derived in `captureResultUsage`. */ private _lastModelUsage; /** * The raw `modelUsage` map from the most recently seen result message. Like * `total_cost_usd` it is cumulative over the whole SDK run, so a turn's share * is the per-model difference against this snapshot. */ private _lastCumulativeModelUsage; /** * The SDK's `total_cost_usd` from the most recently seen result message — * cumulative over the whole SDK run/session, not per-turn. Diffed against * the next result's total to derive the per-turn delta. */ private _lastCumulativeCostUsd; /** Per-turn cost delta derived from `total_cost_usd`, emitted as the `result` event's `costUsd`. */ private _lastTurnCostUsd; /** Whether the last result carried `total_cost_usd`; 0 alone cannot tell a free turn from an unreported one. */ private _lastCostReported; /** * Bumped by every `startQuery()` spawn. Lets `captureResultUsage` detect a * fresh CLI process deterministically, instead of only inferring it from * `total_cost_usd` happening to decrease. */ private _querySpawnGeneration; /** Spawn generation `_lastCumulativeCostUsd` was captured under. */ private _costTrackingGeneration; /** * `true` when the active credential is a Claude Code OAuth subscription * (no API key in config) rather than a raw `ANTHROPIC_API_KEY`. Set on * every `startQuery` because the credential source can be reconfigured * at runtime via `configure`. Threaded into `classifyClaudeSdkError` so * upstream auth-shaped failures get a quota-aware hint when warranted. */ private usingOauthCredential; /** * @param config - Driver configuration. Claude SDK-relevant fields: * - `cwd` — working directory; SDK session files and agent definitions are read from here. * - `model` — Anthropic model identifier (e.g. `"claude-sonnet-4-5"`). Defaults to `"sonnet"`. * - `apiKeys.anthropic` / `env.ANTHROPIC_API_KEY` — API key forwarded to the SDK query * (`cloud: default` only; no key falls back to OAuth credentials). * - `cloud` / `cloudConfig` / `cloudSecrets` — non-default clouds (bedrock/vertex/azure/ * gateway) route through `buildClaudeCloudEnv` instead: exactly one `CLAUDE_CODE_USE_*` * flag plus the resolved credential bundle, never `queryArgs.apiKey`. * - `agentName` — name of a `.claude/agents/.md` agent definition to use as identity. * - `mcpServers` — in-process MCP server instances injected as native `tool_use` tools. * - `systemPrompt` — appended to the `claude_code` preset system prompt. * - `resumeSessionId` — SDK session UUID to resume from a prior driver lifetime. * - `maxTurns` — maximum agentic turns per `prompt()` call (default: 15). */ constructor(config: AgentConfig); get runtimeSessionId(): string | undefined; reconfigure(patch: Partial>): void; getModel(): string | undefined; /** * Dynamically imports `@anthropic-ai/claude-agent-sdk`. * * @throws {Error} When the SDK package is not installed. Install it with * `bun add @anthropic-ai/claude-agent-sdk`. */ start(): Promise; /** * Sends a user message to the Claude Agent SDK and resolves when the turn completes. * * If a live SDK query already exists (from a prior turn in this session) the message * is pushed into its persistent prompt stream (streaming input mode). Otherwise a new * query is started applying the resume chain described in the class-level remarks. * * @param message - Plain-text user prompt. * @throws {Error} When the SDK reports a fatal error that cannot be auto-recovered. * * @remarks * A single automatic retry is performed when the SDK throws * "No conversation found with session ID …" — a recoverable stale-resume error that * occurs after a container crash that prevented the SDK from flushing its session file. */ prompt(message: string): Promise; /** * Runs one complete logical prompt inside its acquired FIFO slot. * * Retries recurse here rather than through {@link prompt}, so they cannot * enqueue behind themselves and deadlock. */ private runPrompt; /** * Arm the per-turn inactivity watchdog. Self-rescheduling rather than a single * absolute timer so any mid-turn SDK message pushes the deadline out; the * timer is `unref`'d so an armed watchdog never holds the process open. * * Two budget tiers: until the first SDK message of the turn arrives the * (small) {@link firstMessageStallMs} budget applies — silence there means * the query is deaf, not busy. Once anything arrives, the (large) * {@link turnStallMs} ceiling takes over, because a long-running tool call * legitimately produces no SDK messages. The budget is re-read every tick, so * the first message flips tiers without re-arming. */ private armTurnStallWatchdog; private disarmTurnStallWatchdog; /** * Bound the wait for the follow-up throw an `is_error` result implies. The * throw carries the real, richly-classified error, so the deferral stays — * this only caps it, turning a permanently pending turn into a bounded * failure. Never arms onto an already-settled turn: an auth / account / limit * payload has run `failTurn` by the time this is reached, and the same guard * is re-checked inside the callback because the throw normally lands in * between. Note 2026-06-08-claude-sdk-error-result-handling. */ private armErrorResultGrace; /** * The retry helpers that null `turnResolve`/`turnReject` before recursing * need no call: they run only from `runPrompt`'s catch, which is reached only * because `failTurn` or `rejectTurnSilently` rejected — and both disarm first. */ private disarmErrorResultGrace; /** * The SDK went silent mid-turn. Tear the query down so the *next* prompt * starts a fresh one (a live-but-deaf query would otherwise swallow every * subsequent `streamInput`), then reject the turn with an actionable message. */ private onTurnStalled; /** * Drop a query that will not unwind on its own, keeping the session intact. * * Deliberately does NOT clear `sessionId` / `hasSession` — the next * `startQuery()` must resume the same SDK session so the conversation * survives. That is what separates this from {@link kill}. * * Does NOT sweep the CLI's descendant processes — callers needing that must * snapshot via {@link snapshotDescendantPids} *before* this (and any other * abort signal) runs, then {@link killPids} after. */ private hardResetQuery; /** * End the persistent prompt stream (the SDK responds by closing the CLI's * stdin) and forget it. Called wherever the query is dropped so a stale * stream can never be pushed into. Safe to call with no stream. */ private endInputStream; /** * `maxTurns` counts agentic turns, but whether the CLI resets that count per * user turn or accumulates it across a resident streaming session is * undocumented. This guard is correct under either semantics: once the last * result's reported `num_turns` is within {@link RECYCLE_TURNS_FRACTION} * headroom of the ceiling, the resident query is dropped at the next turn * boundary and the prompt respawns with `--resume` (which restarts the * count). Under per-turn semantics this fires only when a single prompt * nearly exhausted the budget by itself — where a recycle is harmless. */ private shouldRecycleQuery; /** * Gracefully drop the resident query between turns so the next prompt starts * a fresh CLI resuming the same session. Deliberately does NOT sweep the * CLI's descendant processes: a between-turns recycle mirrors the legacy * spawn-per-turn behavior, where the CLI's background tasks (`run_in_background` * dev servers) outlive the turn boundary. Note they become orphans no later * sweep can reach — snapshotDescendantPids walks from `cliProcessPid`, which * the next spawn overwrites — exactly as in legacy spawn-per-turn. */ private recycleQueryForNextTurn; /** * Capture every current OS descendant of the CLI subprocess (SIGKILLed * later via {@link killPids}). **Call BEFORE `interrupt()` / * `abortController.abort()` / `query.close()` — never after**: once the CLI * exits, Linux reparents its children immediately, so a later walk finds * nothing. Linux-only; `[]` if there's no captured pid or no `/proc`. Full * rationale: `_devlog/notes/2026-08-12-abort-kills-tool-subprocess.md`. */ private snapshotDescendantPids; /** SIGKILL every pid in a snapshot from {@link snapshotDescendantPids}. */ private killPids; /** Direct children of `pid` via Linux procfs — no external binary required. */ private childPids; /** Every descendant of `rootPid` (excluding `rootPid` itself), breadth-first. */ private collectDescendantPids; /** * Side-effects of the stale-resume recovery path: emit the resume-outcome and * non-fatal error events, then reset session state so the recursive prompt() * starts a fresh session. The caller owns the decision to recurse — this only * performs the state mutation, never control flow. */ private prepareStaleResumeRetry; /** * Side-effects of the poisoned-transcript recovery path: scrub the on-disk * transcript and, when something was repaired, emit recovery events and reset * query state. Returns `true` when the scrub changed the transcript (the * caller should recurse) and `false` otherwise. The caller owns the decision * to recurse — this only performs the scrub + state mutation, never control * flow. */ private attemptPoisonScrubRetry; /** * Side-effects of the auth-error recovery path: invoke the `onAuthError` * refresh callback. On success, reset query state and return `true` (the * caller should recurse). On failure, emit the deferred `agent-event: error` * (paired with the deferral in {@link failTurn}) and return `false`. The * caller owns the decision to recurse and the subsequent rethrow — this only * performs the refresh + state mutation, never control flow. */ private attemptAuthSelfHeal; /** * Side-effects of the seat-failover path: ask the host to re-resolve the AI * provider config behind this session. On `switched: true` drop the query and * return `true` (the caller recurses onto a fresh CLI); otherwise emit the * deferred error and return `false`. Mirrors {@link attemptAuthSelfHeal} — * this performs the state mutation, the caller owns the control flow. * * Dropping `this.query` is the whole point of the retry, not bookkeeping: a * resident CLI caches "blocked until " and short-circuits the next * prompt locally with no API attempt at all (skaile-ai/workspaces#566), so a * rewritten credentials file alone would change nothing. Only a fresh spawn * clears it. `sessionId` / `hasSession` are preserved so the new query * resumes the same conversation. */ private attemptLimitFailover; /** * Resolve the Claude Code config directory — the parent of `projects/` — from * the driver config, the process environment, or the `~/.claude` default. */ private resolveClaudeConfigDir; /** * Preventively repair the on-disk SDK transcript before a resume. * * The reactive {@link scrubPoisonedTranscript} pass in `prompt()` only fires * *after* a turn has already failed with a `400 invalid_request_error`, * costing a wasted round-trip and surfacing a scary (if non-fatal) error to * the user. A transcript poisoned in a prior driver lifetime — most commonly * an image block whose `media_type` does not match its bytes, produced by the * Claude Code `Read` tool on a PDF with embedded JPEGs (anthropics/claude-code * #55338) — would otherwise 400 on the very first resumed turn. * * Running the same magic-byte scrub *before* handing the transcript to the * SDK means a known poison class never reaches the API, so recovery is * invisible. This is regex-free (unlike the reactive gate) and idempotent: a * clean transcript is left byte-for-byte untouched. The reactive path remains * the safety net for poison introduced mid-turn within the current lifetime. */ private preventivelyScrubTranscript; /** * Resolve the effective cloud transport for this query. Unrecognized * non-default values fall back to `default` (today's behavior) with a * warning — old-runner tolerance per the cloud-provider contract; the * platform gates runner versions for real non-default sessions. */ private resolveCloud; private startQuery; getSlashCommands(): SlashCommandInfo[]; /** * Permission callback wired into every query. Only `AskUserQuestion` is * intercepted: the turn pauses here until {@link answerQuestion} resolves it * (or {@link denyPendingQuestion} cancels it). Every other tool auto-approves * — under `bypassPermissions` the SDK never even invokes this callback for * them, but we approve verbatim as a defensive default. * * Arrow property so the bound `this` survives being passed as * `options.canUseTool` by value. */ private handleCanUseTool; hasPendingQuestion(): boolean; answerQuestion(answer: string, question?: string): boolean; /** * Resolve any pending {@link handleCanUseTool} promise with a `deny` so the * SDK records a clean tool_result and the turn unwinds. No-op when nothing is * pending. Used on abort/kill/reset and when a question is superseded. */ private denyPendingQuestion; /** * Compose the MCP servers passed to the SDK query. Merges the legacy * `config.mcpServers` (connectors / workspace plugin / declarative skaile.yaml * entries) with a synthetic `skaile-capabilities` server built from the * runner-provided {@link BridgeCapabilityHooks} when present. * * Returns an empty options patch when neither source is configured, so legacy * v1 sessions remain unaffected. */ private buildEffectiveMcpServers; /** * Build the synthetic MCP server that exposes registered capabilities to the * Claude Agent SDK. Lazy-imports `createSdkMcpServer` from the SDK and `zod` * (an SDK peer) since both are optional peers of this bridge. * * Returns null when the SDK or zod is unavailable, or when the registry * exposes no tools — leaving the legacy `mcpServers` untouched. */ private buildCapabilityMcpServer; /** Fetch and cache slash commands from the SDK query. */ private fetchSlashCommands; /** * Background loop — consumes the query's async generator continuously. * The generator stays alive between turns; streamInput() feeds new messages. * If it ends unexpectedly, query is nulled so the next prompt() restarts. */ private consumeMessages; /** Capture the SDK session id from a message (first one assigns hasSession). */ private captureSessionId; /** * Handle an error thrown by the query's async generator. Aborts are logged * silently; everything else is classified and routed through {@link failTurn} * (auth-shaped failures promoted to {@link AuthError}). */ private handleConsumerError; /** * Signal the current turn as done — idempotent per turn. * * Same defensive ordering as {@link failTurn}: settle the turn promise * before emitting `agent_end` so a misbehaving listener cannot trap the * promise in a pending state. See failTurn's docblock for the production * incident that motivated this pattern. */ private completeTurn; /** Emit the turn-end signal, swallowing a throwing listener. */ private emitAgentEnd; /** * Reject the current turn WITHOUT emitting an `agent-event: error`. * * The one legitimate use is a turn that is about to be replayed: the host * must not see an error for a failure the driver is itself recovering from * (the same reasoning as `failTurn`'s auth self-heal deferral). Ordering * mirrors {@link failTurn} — settle the promise before touching anything * else — so a turn can never be left pending. * * Anything a caller should actually learn about goes through `failTurn`. */ private rejectTurnSilently; /** * Signal the current turn as failed. When a structured `detail` is * supplied, the AgentEvent carries the category + hint so downstream * consumers (normalizer → runner → platform → frontend) can surface a * useful diagnosis instead of the raw upstream string. * * Reject ordering: `turnReject()` runs FIRST, then the agent-event is * emitted. `EventEmitter#emit` calls listeners synchronously; if any * listener throws (transport.send on a closed socket, normalizer bug, * compaction tracking error, etc.) the throw would otherwise propagate * out of failTurn before `turnReject` can run, leaving `await * turnPromise` pending forever and silently hanging `driver.prompt(...)` * — which in turn prevents the runner's `onAuthError` callback from * firing on stale-token 401s. Settling the promise first guarantees the * caller transitions even if the listener chain misbehaves; the emit is * wrapped in try/catch so a listener failure cannot leak out. * * Self-heal deferral: when the failure is an {@link AuthError} and the auth * self-heal budget is still available (`!authSelfHealUsed`) AND the caller * wired an `onAuthError` callback, the downstream `agent-event: error` * emission is deferred. The rejected turn promise still travels to * {@link prompt}'s catch block where the self-heal runs; if the retry * succeeds the user never sees a 401, if the retry fails the second * `failTurn` call (now with `authSelfHealUsed === true`) emits the error * normally. This stops the historical "401 flashes in the UI even though * self-heal worked" misbehaviour where the bridge emitted the error event * before the retry decision was known. * * Spec: `_devlog/specs/2026-05-07-unified-credential-mediation.md` * § "Runner-side handling on 401" (the AI 401 mediation path that * exposed this hang). */ private failTurn; /** * Whether a usage-limit block on the current attempt still has a seat * failover available. Evaluated identically by {@link failTurn} (to decide * whether to withhold the error event) and by {@link handleResultErrorPayload} * (to decide whether to reject the turn at all) — one predicate so the two * can never disagree and strand a deferred event. */ private willFailoverOnLimit; /** Emit one `agent-event: error`, swallowing a throwing listener. */ private emitTurnErrorEvent; /** * Emit the error {@link failTurn} withheld, exactly once. Called by a * recovery path that decided NOT to retry, so every failure produces one * downstream error event and a recovered one produces none. `fallback` covers * the paths that reject without going through `failTurn`. */ private emitDeferredTurnError; private handleSdkMessage; /** * Surface subscription rate-limit telemetry as a structured warning log, and * — on `status: "rejected"` — arm the seat failover and report the rejection * to the host for per-seat health attribution. * * Returns `true` when the message was a rate-limit event (and fully handled), * so the caller can stop dispatching. * * This used to only log. That is what left a session pinned to a parked seat: * the rejection was fully handled here and nothing propagated, so the * platform's re-resolution — which was ready and working — was reachable only * through a token refresh that happened to be due (skaile-ai/platform#2976). */ private handleRateLimitEvent; /** * Report an observed 401 / 429 to the host so the owning AI provider config's * live health counters can be bumped (skaile-ai/workspaces#368). Strictly * fire-and-forget: a throwing host callback must never fail the turn that * produced the observation. */ private reportProviderResponse; private handleAssistantMessage; private isSuppressibleAuthRender; /** * True when this text is the CLI's own render of a 401 a pending self-heal * will retry. Shared by both suppression sites so they cannot disagree — * why that is forced: `_devlog/notes/2026-06-08-auth-error-text-suppress.md`. */ private isSuppressibleAuthRenderText; /** Whether a 401 on this attempt can still be self-healed and retried. */ private authSelfHealAvailable; /** * Stash a dropped auth render as evidence for {@link handleResultErrorPayload}. * * Deliberately stricter than the suppression predicate. Suppression only hides * a message, but this text can reach the result path and construct an * `AuthError` — which refreshes a credential, bumps that seat's health * counters, and replays the turn. `classifyClaudeSdkError` buckets a bare * "401" or "unauthorized" as auth, so ordinary prose about authentication * qualifies; requiring a marker only the CLI itself emits keeps that prose out. */ private stashSuppressedAuthRender; /** Flatten an assistant message's text blocks; non-text blocks contribute nothing. */ private assistantTextOf; private textIsAuthRender; private handleStreamEvent; private handleUserMessage; private handleResultMessage; /** * Capture the per-call context size from an assistant message. Its `usage` * describes exactly one API request, so `input + cache_creation + cache_read` * is the prompt that occupied the window on that call — unlike the result * message's usage, which sums every call in the turn. All three are summed * rather than `contextTokens()`-style max'd: on a per-call Anthropic usage the * fields are disjoint slices of one prompt. */ private captureAssistantContext; /** * Capture token usage + context-window size from a result message. * Anthropic separates fresh prompt input from cached/cache-creation * tokens — surface all four to consumers that want a complete picture. */ private captureResultUsage; /** Optional usage fields shared by the success and error `result` events. */ private turnUsageFields; /** Emit the message_end (if any) + success result event for a clean turn. */ private handleSuccessResult; /** * Handle the non-clean branch (legacy `SDKResultError`, or a `success` * subtype carrying `is_error: true`). Read via the loose shape since the * second case omits `errors` on the typed variant. See devlog note * 2026-06-08-claude-sdk-error-result-handling. */ private handleErrorResult; /** * Classify + route an `is_error` result payload: auth-shaped errors throw * via `failTurn`; populated non-auth errors surface a normal `error` event; * empty errors defer to the consumer-error catch under a bounded grace timer. * * An empty `errors[]` falls back to the auth render this attempt suppressed: * handed nothing, the classifier can only answer `unknown`, stranding a 401 * the render path already recognised on the same text. */ private handleResultErrorPayload; /** Build an SDKUserMessage for the persistent prompt stream. */ private buildUserMessage; private mapAssistantMessage; private mapToolResults; /** * Requests the current SDK query to stop processing via `query.interrupt()` and * signals the underlying `AbortController`. The driver remains usable after abort. * * Also SIGKILLs every OS descendant of the CLI subprocess (e.g. a running * Bash command) on either path the turn ends — see * {@link snapshotDescendantPids}'s doc for why that's needed. * * @remarks Best-effort: if `interrupt()` throws (e.g. query already ended) the * error is silently swallowed and the abort signal is still sent. A query * whose generator refuses to unwind within {@link ABORT_GRACE_MS} is torn * down outright — see {@link hardResetQuery}. */ abort(): Promise; /** `true` while the driver is processing a `prompt()` call. */ get isRunning(): boolean; /** * Closes the active SDK query and resets all session state. * * The driver instance must not be reused after `kill()`. Session IDs are cleared, * so a new `ClaudeSdkDriver` will start a fresh session. * * Also SIGKILLs every OS descendant of the CLI subprocess (e.g. a running * Bash command) — see {@link snapshotDescendantPids}'s doc for why that * needs to be snapshotted before `query.close()` runs, not after. */ kill(): void; listModels(): Promise; getTokenUsage(): TokenUsage | null; getContextTokens(): number | null; getContextWindow(): number | null; resetSession(): Promise; /** * Build allowedTools / disallowedTools options from AgentConfig.tools. * * When an allowlist is active, automatically prepend a wildcard entry for * every configured MCP server (e.g. `mcp__skaile-connectors__*`) so that * connector tools are never silently blocked by agent.yaml restrictions. */ private buildToolRestrictions; /** * Map the simple string thinking config to SDK ThinkingConfig objects. * This keeps the platform-layer SDK-agnostic while the bridge handles the mapping. */ private mapThinkingConfig; /** * Find the Claude Code CLI executable. * * Resolution order: * 1. CLAUDE_CODE_PATH env var (set in Docker containers where cli.js is * extracted from the SDK during build — bun-compiled binaries can't * access the SDK's bundled cli.js via $bunfs) * 2. `which claude` on PATH (dev machines with claude installed globally) * 3. undefined — let the SDK resolve its own bundled cli.js (works when * not running inside a bun-compiled binary) */ private findClaudeBinary; } /** * `DriverTarget` wrapper for the claude-sdk driver. Registered into the plugin * registry by `registerBuiltinDrivers()`. The Claude Agent SDK is lazy-imported * inside `start()` — this target export never pulls it at module top. */ export declare const claudeSdkDriverTarget: DriverTarget; export {}; //# sourceMappingURL=claude-sdk.d.ts.map