Claude through the Agent SDK
Claude uses an SDK query stream rather than app-server RPC; T3 holds live query context and deferred interactions while normalizing assistant, tool, task, plan, usage, and result observations into one product contract.
What this chapter resolves
- Follow Claude query configuration resume steering interruption and cleanup
- Map permissions input tools TodoWrite tasks and results
- Separate provider-owned stream state from durable T3 history
- Explain skills commands and live usage without cross-provider overclaim
Claude reaches T3 through the Agent SDK query/message stream, not Codex app-server and not ACP. The adapter owns a live context per T3 thread: pending approval/input deferreds, turn bookkeeping, task state, model/mode, resume metadata, and last-known context telemetry. That creates a coherent product surface; it is not a durable cross-provider scheduler or replay log of SDK traffic.
Configure a query, then preserve enough to resume
const runtimeModeToPermission: Record<string, PermissionMode> = {
"auto-accept-edits": "acceptEdits",
auto: "auto",
"full-access": "bypassPermissions",
};
const permissionMode = runtimeModeToPermission[input.runtimeMode];
const settings = {
...(typeof thinking === "boolean" ? { alwaysThinkingEnabled: thinking } : {}),
...(fastMode ? { fastMode: true } : {}),
...(ultracode ? { ultracode: true } : {}),
};
const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId);
// The attachments dir grant lets the agent Read/copy pasted images at
// the paths ProviderService injects into the turn text, without an
// approval prompt. It is a leaf directory holding only attachment
// files; siblings like secrets/ and state.sqlite stay ungranted.
const additionalDirectories = [
...(input.cwd ? [input.cwd] : []),
serverConfig.attachmentsDir,
];
const queryOptions: ClaudeQueryOptions = {
...(input.cwd ? { cwd: input.cwd } : {}),
...(apiModelId ? { model: apiModelId } : {}),
pathToClaudeCodeExecutable: claudeBinaryPath,
systemPrompt: { type: "preset", preset: "claude_code" },
settingSources: [...CLAUDE_SETTING_SOURCES],
// `ultracode` is a Claude Code setting, not an API effort level. It is
// normalized to `xhigh` above and paired with `settings.ultracode`.
...(effectiveEffort
? {
effort: effectiveEffort as unknown as NonNullable<ClaudeQueryOptions["effort"]>,
}
: {}),
...(permissionMode ? { permissionMode } : {}),
...(permissionMode === "bypassPermissions"
? { allowDangerouslySkipPermissions: true }
: {}),
...(Object.keys(settings).length > 0 ? { settings } : {}),
...(existingResumeSessionId ? { resume: existingResumeSessionId } : {}),
...(newSessionId ? { sessionId: newSessionId } : {}),
includePartialMessages: true,
canUseTool,
env: claudeEnvironment,
additionalDirectories,
...(Object.keys(extraArgs).length > 0 ? { extraArgs } : {}),
...(mcpSession
? {
mcpServers: {
"t3-code": {
type: "http",
url: mcpSession.endpoint,
headers: {
Authorization: mcpSession.authorizationHeader,
},
},
},
}
: {}),
}; const queryRuntime = yield* Effect.try({
try: () =>
createQuery({
prompt,
options: queryOptions,
}),
catch: (cause) =>
new ProviderAdapterProcessError({
provider: PROVIDER,
threadId,
detail: "Failed to start Claude runtime session.",
cause,
}),The adapter derives native SDK permission behavior from T3 runtime policy:
full-access maps to bypassPermissions, auto-accept-edits to acceptEdits, and
auto to auto. approval-required has no mapping, so permissionMode is omitted
and the SDK default applies. Instance-bound model selection controls effort, fast,
and thinking options only where the selected model supports them.
Resume is metadata, not a replayed T3 transcript. The adapter validates a persisted
cursor containing the T3 thread id, a provider-native resume identifier (while
accepting legacy sessionId), optional resumeSessionAt, and a turn count, then
supplies those hints to a fresh query. T3 durable history remains an independent
orchestration record.
Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.
Text equivalent
A T3 thread starts or resumes a Claude SDK query. The adapter keeps a live context containing pending approval and input deferreds, task and plan state, turn metadata, model selection, resume ids, and last-known token usage. SDK messages map to canonical content, item, plan, task, request, completion, error, and token-usage events. Runtime ingestion dispatches internal commands. The event store and projections are durable; query objects, deferred responses, stream position, and adapter maps are volatile.
apps/server/src/provider/Layers/ClaudeAdapter.ts:4138–4196 ↗apps/server/src/provider/Layers/ClaudeAdapter.ts:4223–4235 ↗apps/server/src/provider/Layers/ClaudeAdapter.ts:656–689 ↗apps/server/src/provider/Layers/ClaudeAdapter.ts:2051–2112 ↗apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:1490–1874 ↗A second send while work is live is a steer
When a turn is active, a later sendTurn continues that live provider turn instead
of creating a second canonical one. This is an adapter semantic grounded in Claude
query behavior—not a claim that all providers accept concurrent prompts identically.
Interruption uses native terminal-result classification, distinguishing explicit
aborts and cancellation from real provider failures.
Tools, requests, plans, and tasks are separate translations
const respondToRequest: ClaudeAdapterShape["respondToRequest"] = Effect.fn("respondToRequest")(
function* (threadId, requestId, decision) {
const context = yield* requireSession(threadId);
const pending = context.pendingApprovals.get(requestId);
if (!pending) {
return yield* new ProviderAdapterRequestError({
provider: PROVIDER,
method: "item/requestApproval/decision",
detail: `Unknown pending approval request: ${requestId}`,
});
}
context.pendingApprovals.delete(requestId);
yield* Deferred.succeed(pending.decision, decision);
},
);
const respondToUserInput: ClaudeAdapterShape["respondToUserInput"] = Effect.fn(
"respondToUserInput",
)(function* (threadId, requestId, answers) {
const context = yield* requireSession(threadId);
const pending = context.pendingUserInputs.get(requestId);
if (!pending) {
return yield* new ProviderAdapterRequestError({
provider: PROVIDER,
method: "item/tool/respondToUserInput",
detail: `Unknown pending user-input request: ${requestId}`,
});
}
context.pendingUserInputs.delete(requestId);
yield* Deferred.succeed(pending.answers, answers);
});Tool activity maps to canonical item lifecycle. For approval or structured input, the adapter stores a deferred, emits a canonical request observation, and resumes the native interaction after the client response. Unknown ids are typed request errors. The pending maps are not durable inboxes; restart empties them until the provider flow creates another request.
TodoWrite and agent/task-shaped messages are interpreted into product plan steps, task activity, completion, and selected per-task usage. This feeds T3 work views, but does not make a T3 plan control Claude’s scheduler or establish the same task vocabulary for every adapter.
Classify one Claude SDK observation
Choose an event family to see the native message, its product projection, and the point where durable truth begins.
Selected Query configured
Query configured
- Native SDK meaning
- The SDK query receives cwd, model, effort, permission mode, and optional resume metadata.
- Canonical product meaning
- A ProviderSession records a live Claude binding and a resumable cursor shape.
- Boundary to keep honest
- The SDK session and query stream are provider-owned runtime state; a stored cursor is not a replay of every transient message.
All message families
- Query configured
The SDK query receives cwd, model, effort, permission mode, and optional resume metadata.
A ProviderSession records a live Claude binding and a resumable cursor shape.
The SDK session and query stream are provider-owned runtime state; a stored cursor is not a replay of every transient message.
- Assistant + tool
Assistant text and tool-use/result messages stream from the Agent SDK.
Content and item lifecycle events are normalized for thread projections.
The canonical item taxonomy is T3’s contract; it is not an assertion that all SDK message fields persist.
- Permission / question
A tool request or structured input pauses on an SDK-side deferred response.
request.opened or user-input.requested gives clients a canonical action.
The deferred is volatile. The durable record appears only after runtime ingestion’s internal command commits.
- Todo / task
TodoWrite and agent/task-shaped SDK messages provide planning and work signals.
turn.plan.updated and task/activity events drive product work views.
These are adapter normalizations, not a cross-provider task scheduler or durable execution queue.
- Result / error
A terminal SDK result carries success, cancellation, error, and sometimes usage fields.
turn completion plus optional thread token usage or runtime error reaches T3.
Live context telemetry is not transcript accounting, and an SDK result still crosses a hot delivery bridge before durable projection.
Skills and commands are discovered native configuration
The Claude driver probes account/capability information and scans native slash-command and skill paths with normalized path/preference rules. That enriches a configured provider instance. It is distinct from adapter message normalization: discovery does not prove a skill loaded for a particular query, and query activity is not proof that every installed skill was discoverable.
Live context telemetry is not historical accounting
Claude result and selected message usage can update a per-thread token/context snapshot, including input/cache/output/reasoning/tool/duration fields where present. This supports the live context meter. Chapter 20’s Usage page independently scans provider-owned transcripts, de-duplicates them, and prices where possible; it does not use this runtime event as its source of truth.
At the pinned revision only Codex and Claude emit this live telemetry. A generic runtime schema can represent future data, but that is not evidence Cursor, Grok, or OpenCode emit it now.
stopSession closes a Claude context and emits an exit observation; session listing
and finalization operate on in-memory contexts. Tests cover permission modes,
model/effort options, assistant/tool mapping, steering, TodoWrite plans, resume,
approval/input, usage, and cleanup. They do not prove crash recovery of a pending
SDK deferred or replay of an event published but not yet ingested.