Skip to content

Latest commit

 

History

History
133 lines (98 loc) · 9.39 KB

File metadata and controls

133 lines (98 loc) · 9.39 KB

Feature: ClaudeThread Run Flow

Links: Architecture: docs/Architecture/Overview.md Modules: ClaudeThread.cs, ClaudeExec.cs, ThreadEventParser.cs ADRs: 001-claude-cli-wrapper.md, 002-protocol-parsing-and-thread-serialization.md


Purpose

Provide deterministic thread-based execution over Claude Code CLI so C# consumers can run turns, stream events, and resume existing conversations safely.

Runtime source of truth for this feature is the observed non-interactive Claude Code protocol:

  • claude -p --output-format json
  • claude -p --output-format stream-json --verbose

The upstream reference repository anthropics/claude-code is tracked in this repo as a submodule for change monitoring, but the SDK contract follows actual CLI behavior first.


Scope

In scope

  • Turn execution (RunAsync, RunStreamedAsync) for plain-text prompts and IReadOnlyList<UserInput> requests that currently normalize text-only input.
  • Conversion of Claude JSONL stream into typed ThreadEvent/ThreadItem models.
  • ClaudeThread identity tracking across thread.started and resume flows.
  • Failure/cancellation handling and typed structured output.

Out of scope

  • Network transport reimplementation of Claude protocol (SDK uses CLI process).
  • Image/file upload transport in print mode.
  • Multi-thread merge semantics between separate ClaudeThread instances.

Business Rules

  • Only one active turn per ClaudeThread instance.
  • RunAsync returns only completed items and latest assistant text as FinalResponse.
  • RunAsync<TResponse> returns RunResult<TResponse> with deserialized TypedResponse; typed runs require an output schema via either direct outputSchema overload parameter or TurnOptions.OutputSchema.
  • Typed run API supports both concise overloads (RunAsync<TResponse>(..., outputSchema, ...)) and full options overloads (RunAsync<TResponse>(..., turnOptions)); for AOT-safe typed deserialization pass JsonTypeInfo<TResponse>.
  • Convenience typed overloads without JsonTypeInfo<TResponse> are explicitly marked as AOT-unsafe with RequiresDynamicCode and RequiresUnreferencedCode.
  • result events with is_error: true must surface as TurnFailedEvent, and RunAsync(...) must raise ThreadRunException.
  • For Claude structured output turns, typed JSON may arrive via the structured_output field on the final result event even when textual result is empty; SDK typed runs must deserialize that JSON payload.
  • Invalid JSONL event lines must fail fast with parse context.
  • Protocol tokens are parsed via constants, not inline literals.
  • Optional ILogger (Microsoft.Extensions.Logging) receives process lifecycle diagnostics (start/success/failure/cancellation).
  • Structured output uses typed StructuredOutputSchema models (including DTO property selectors) that are serialized inline to --json-schema.
  • LocalImageInput exists in the SDK model layer but is currently rejected in Claude print mode with NotSupportedException.
  • TurnOptions.ReplayUserMessages is currently rejected because the SDK only supports text input, not Claude stream-json input mode.
  • Claude executable resolution returns an immutable CliLaunchCommand through ClaudeClient.GetCliLaunchCommand(). The SDK resolves one absolute executable plus literal prefix arguments from the same effective environment used by execution and metadata probes. Native executables remain direct launches; a Windows npm .cmd/.bat shim is never invoked through a shell. The resolver accepts only the exact @anthropic-ai/claude-code package manifest and launches its supported native executable target directly or its JavaScript entry with node.exe.
  • Thread options map current Claude Code print-mode flags (--model, --permission-mode, tool allow/deny lists, system prompts, MCP config, resume/session flags, budget, settings, plugins, betas), plus raw AdditionalCliArguments passthrough for future non-transport flags. SDK-managed transport flags are reserved and rejected if passed manually.
  • Child processes inherit the parent environment by default, with configured ClaudeOptions.EnvironmentVariables overlaid. Set ClaudeOptions.InheritEnvironmentVariables = false to start from only explicitly configured values (plus the SDK's explicit API-key/base-URL settings); the same option is available through ClaudeChatClientOptions.ClaudeOptions.
  • SDK print-mode sessions persist by default because the SDK only emits --no-session-persistence when the caller explicitly sets ThreadOptions.NoSessionPersistence = true.
  • Persisted print-mode sessions are stored by Claude under project-scoped directories in ~/.claude/projects/.../<session-id>.jsonl; external CLI/App resume should use the same working directory or project scope that created the session.
  • The SDK guarantees persisted session ids plus resume-by-id; whether a non-interactive print-mode session appears in Claude's default history or resume picker depends on Claude CLI/App behavior rather than an SDK-specific storage format.
  • Execution failures are surfaced to the caller with the raw Claude event context preserved in exception chains where available.
  • New event types are surfaced as UnknownEvent with their raw JSON payload, and unknown content blocks retain their raw JSON in MessageContentBlock.Raw; consumers can continue processing streams when Claude adds protocol records.

User Flows

Primary flows

  1. Start and run turn
  • Actor: SDK consumer
  • Trigger: StartThread().RunAsync(...)
  • Steps: build claude -p args -> execute Claude Code CLI -> parse stream-json output -> collect result
  • Result: RunResult with items, usage, final assistant response
  1. Start and run typed structured turn
  • Actor: SDK consumer
  • Trigger: StartThread().RunAsync<TResponse>(..., outputSchema, ...) (or TurnOptions variant)
  • Steps: run regular turn -> deserialize final JSON response to TResponse using provided JsonTypeInfo<TResponse> when needed
  • Result: RunResult<TResponse> with typed payload in TypedResponse
  1. Resume existing thread
  • Actor: SDK consumer
  • Trigger: ResumeThread(id).RunAsync(...)
  • Steps: include --resume <id> args -> parse events
  • Result: turn executes in existing Claude conversation

Edge cases

  • Malformed JSON line -> InvalidOperationException with raw line context
  • result event with is_error: true -> ThreadRunException
  • cancellation token triggered -> execution interrupted and surfaced to caller
  • Cancellation requests stop the CLI process tree and await root-process exit plus natural stdout and stderr EOF, bounded by ClaudeOptions.ProcessTerminationTimeout (five seconds by default). If any condition cannot be confirmed, the SDK surfaces a failure instead of returning cancellation. A detached descendant that retains either pipe can therefore turn cancellation into a bounded failure; the SDK does not attest that external effects or detached descendants have stopped. Each redirected stream has an independent cumulative ClaudeOptions.MaximumProcessOutputCharacters budget (1,048,576 characters by default); stdout line buffering shares the stdout budget, and exceeding either budget fails the run. Metadata settings files are also bounded by ClaudeOptions.CliMetadataMaximumFileCharacters (1,048,576 characters by default); oversized files fail metadata inspection. Concurrent metadata subprocesses share a single bounded process lease; waiting for it is limited by ClaudeOptions.CliMetadataProbeLeaseTimeout (two minutes by default), separately from each subprocess's CliMetadataProbeTimeout. An incomplete pipe cleanup blocks later probes immediately.

Diagrams

flowchart LR
  Caller["Caller"] --> ClaudeThread["ClaudeThread.RunAsync / RunStreamedAsync"]
  ClaudeThread --> ExecArgs["ClaudeExecArgs"]
  ExecArgs --> Cli["claude -p --output-format stream-json"]
  Cli --> Stream["JSONL events"]
  Stream --> Parser["ThreadEventParser"]
  Parser --> Result["RunResult / streamed events"]
Loading

Verification

Test commands

  • build: dotnet build ManagedCode.ClaudeCodeSharpSDK.slnx -c Release -warnaserror
  • test: dotnet test --solution ManagedCode.ClaudeCodeSharpSDK.slnx -c Release
  • format: dotnet format ManagedCode.ClaudeCodeSharpSDK.slnx
  • coverage: dotnet test --solution ManagedCode.ClaudeCodeSharpSDK.slnx -c Release -- --coverage --coverage-output-format cobertura --coverage-output coverage.cobertura.xml

Test mapping


Definition of Done

  • Public thread APIs stay aligned with current Claude Code CLI contracts and documented in repository feature/architecture docs.
  • All listed tests pass.
  • Typed structured output API keeps explicit AOT-safe (JsonTypeInfo<TResponse>) and convenience overload contracts documented and covered by tests.
  • Docs remain aligned with code and CI workflows.