Part IV · Five harnesses, one product modelClaude Agent SDK
Chapter 17source checked

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

apps/server/src/provider/Layers/ClaudeAdapter.ts:4138–4196 ↗verbatim · typescript · 0b8c0d48
      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,
                  },
                },
              },
            }
          : {}),
      };
Read this as: The adapter maps three T3 runtime modes to explicit SDK permission modes, then assembles options for cwd, model, effort, permissions, native resume/new-session identity, environment, allowed directories, and optional T3 MCP access.
apps/server/src/provider/Layers/ClaudeAdapter.ts:4223–4235 ↗verbatim · typescript · 293fa0c2
      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,
          }),
Read this as: The prepared prompt and queryOptions cross into createQuery here; a synchronous startup failure becomes a typed provider process error.

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.

Figure 17.1 · A Claude SDK stream is normalized, then durably re-enteredquery state and deferred responses are live memory
Claude Agent SDK query stream normalizationDiagram loading

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.

Claude Agent SDK query stream normalization
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.

Figure 17.1. T3 configures a Claude SDK query with model, mode, cwd, and optional resume metadata. Assistant, tool, TodoWrite/task, request, result, and error messages flow into the adapter. It emits canonical runtime events that ingestion turns into internal commands; only those command transactions become durable T3 events and projections.

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

apps/server/src/provider/Layers/ClaudeAdapter.ts:4497–4529 ↗verbatim · typescript · e41de225
  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);
  });
Read this as: Approval and structured-input responses resolve only known pending deferreds in a live Claude session context.

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.

Interactive stream classifier

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
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.