源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
# Claude Code SDK Runtime
|
||||
|
||||
Claude Code has one supported runtime path: the daemon starts the
|
||||
`@tutti-os/claude-sdk-sidecar` package, and the sidecar talks to the Claude Agent
|
||||
SDK.
|
||||
|
||||
The product-fidelity and acceptance contract is maintained in
|
||||
[Claude Code SDK Refactor Fidelity Requirements](./claude-code-sdk-requirements.md).
|
||||
|
||||
## Boundary
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Service["tuttid agent services"] --> Adapter["Go Claude SDK adapter"]
|
||||
Adapter <-->|"versioned NDJSON over stdio"| Sidecar["TypeScript SDK sidecar"]
|
||||
Sidecar --> SDK["Claude Agent SDK"]
|
||||
Adapter --> Activity["normalized agent activity"]
|
||||
Activity --> Consumers["Agent GUI and other consumers"]
|
||||
```
|
||||
|
||||
The public boundary is normalized agent activity and capability data. The GUI
|
||||
must branch on capabilities or event semantics, not on the Claude Code provider
|
||||
name. Claude-specific SDK messages, tool shapes, session cursors, and transport
|
||||
details stay behind the daemon adapter and sidecar.
|
||||
|
||||
## Daemon ownership
|
||||
|
||||
The Go runtime under `packages/agent/daemon/runtime` is split by responsibility:
|
||||
|
||||
| Module | Ownership |
|
||||
| ------------------------------ | -------------------------------------------------------- |
|
||||
| `claude_sdk_adapter.go` | Adapter construction, shared state, and sink wiring |
|
||||
| `claude_sdk_lifecycle.go` | Session start, resume, close, and live-session release |
|
||||
| `claude_sdk_execution.go` | Prompt validation, execution, guidance, and cancellation |
|
||||
| `claude_sdk_settings.go` | Live settings and permission-mode application |
|
||||
| `claude_sdk_transport.go` | Sidecar process and NDJSON transport |
|
||||
| `claude_sdk_protocol.go` | Protocol version and envelope validation |
|
||||
| `claude_sdk_session.go` | Session storage, command/env helpers, and state payloads |
|
||||
| `claude_sdk_events.go` | Sidecar event dispatch and lifecycle routing |
|
||||
| `claude_sdk_turn.go` | Per-turn event lifecycle via shared `acpTurnNormalizer` |
|
||||
| `claude_sdk_activity.go` | Conversion into normalized activity events |
|
||||
| `claude_sdk_live_state.go` | Live message, task, and usage state reconciliation |
|
||||
| `claude_sdk_interactive.go` | Approval and interactive prompt handling |
|
||||
| `claude_sdk_goal.go` | Goal and plan lifecycle projection |
|
||||
| `normalized_session_events.go` | Protocol-neutral session activity projection |
|
||||
|
||||
The descriptor selects the SDK runtime kind; these adapter modules own the
|
||||
process/session lifecycle and normalization behind that selection. They must not expose
|
||||
raw SDK envelopes to services or GUI packages. Service-layer provider catalogs,
|
||||
composer profiles, targets, status probes, and identity projections consume the
|
||||
same `ProviderDescriptor` instead of re-registering Claude Code locally.
|
||||
Provider preparation may inject system-prompt and plugin paths that exist only
|
||||
in the sidecar runtime filesystem. The Go adapter treats those environment
|
||||
values as opaque and forwards them unchanged; `options.ts` reads and validates
|
||||
the referenced files after the sidecar starts. Daemon code must not inspect
|
||||
those paths because remote transports do not share its filesystem.
|
||||
Claude modules do not call ACP _protocol_ helpers (session-update decoding,
|
||||
standard ACP tool-call envelopes, and similar). Open tool-call bookkeeping and
|
||||
turn `Finish*` settlement reuse the shared adapter turn lifecycle
|
||||
(`acpTurnNormalizer`), the same entity Codex and standard ACP already use, so
|
||||
cancel/fail/complete close dangling tool cards instead of leaving them
|
||||
in progress. Protocol-neutral session and interactive activity projection have
|
||||
their own modules, while Claude goal, command, usage, and interaction decoding
|
||||
stay inside the Claude SDK boundary.
|
||||
Claude Goal state comes only from provider-owned Goal observations. The
|
||||
sidecar normalizes both SDK `active_goal` messages and native `/goal`
|
||||
`goal_status` transcript attachments into one `goal_observed` event. Local
|
||||
Claude Agent SDK `0.3.220` streams omit those attachment records from the
|
||||
public `SDKMessage` iterator. The root `system/init` message supplies the
|
||||
provider Session ID and cwd; `goalTranscript.ts` mirrors the pinned SDK's local
|
||||
project-key algorithm to locate its JSONL. Restore starts from the already
|
||||
known provider Session ID, while a generation-scoped `SessionStart` callback
|
||||
can still supply the SDK-provided exact `transcript_path`. The reader consumes
|
||||
only rows appended after observation began, drains once before the root result
|
||||
settles, and continues following the file because Claude's native evaluator
|
||||
may flush the final `goal_status` just after the SDK result. Starting at the
|
||||
existing end of file prevents a restored session from replaying an old active
|
||||
or completed Goal. A
|
||||
non-null `active_goal` or `goal_status.met=false` keeps the condition active;
|
||||
`goal_status.met=true` completes it. A null `active_goal` is interpreted as
|
||||
explicit clear or completion using the exact command action and previous Goal
|
||||
observation. Ordinary Turn completion has no Goal semantics.
|
||||
Command-consumption evidence travels as the internal `goal.control_applied`
|
||||
event to the Host Goal lane and must never be embedded in session runtime
|
||||
context.
|
||||
New normalized session updates use `sessionUpdateKind`; the former ACP-named
|
||||
metadata key is accepted only while reading imported or durable historical
|
||||
events.
|
||||
|
||||
Context usage is published only when the provider reports a context-window
|
||||
limit or the live Session already has a provider-reported limit for the same
|
||||
model. Token deltas from a new Session do not synthesize a 200k or 1M
|
||||
denominator from the model name; AgentGUI keeps usage hidden until the first
|
||||
authoritative window arrives. Restore/resume must not await that snapshot
|
||||
before `session_started`: Host Resume/Send completes on the start handshake,
|
||||
and usage may refresh asynchronously afterward.
|
||||
|
||||
Model selection preserves requested and resolved state separately. The model
|
||||
config option's `currentValue` remains the user's selection, including
|
||||
`default`, while `effectiveValue` is the concrete model reported by the
|
||||
per-user SDK runtime. The sidecar begins consuming the Query after
|
||||
`session_started` so the SDK's `system/init.model` can publish that fact before
|
||||
the first prompt; root assistant messages continuously reconcile it. A live
|
||||
switch back to `default` clears the previous effective value until the SDK
|
||||
reports the newly resolved model. Hosts and renderers must not derive the
|
||||
effective model from the advertised Default option's label or description.
|
||||
|
||||
Claude credential-sensitive operations share the process-wide gate owned by
|
||||
`services/tuttid/service/claudecode`. Real session startup, hidden model
|
||||
discovery, and `claude auth status` acquire this same gate. The AgentGUI's
|
||||
initial provider demand only checks local availability; Anthropic usage is
|
||||
queried after the user opens a usage surface or explicitly refreshes it.
|
||||
|
||||
Composer model discovery uses an account-level scope made from provider,
|
||||
agent-target identity, and a non-secret auth fingerprint. It intentionally does
|
||||
not include workspace or caller cwd for Claude Code, so switching workspaces
|
||||
cannot create duplicate discovery processes. The process runs from the
|
||||
daemon-owned discovery directory. A composer request waits at most 20 seconds,
|
||||
but that wait does not cancel the SDK initialization: the background lifecycle
|
||||
may continue for up to ten minutes so OAuth refresh state can be persisted.
|
||||
Transient failures before a session starts remain retryable. Auth invalidation
|
||||
marks in-flight results superseded and clears caches, but never closes a hidden
|
||||
discovery session early; the original ten-minute lifecycle remains responsible
|
||||
for teardown so credential refresh has time to finish persisting.
|
||||
The descriptor's `CLAUDE_CONFIG_DIR` root override is honored consistently by
|
||||
runtime endpoint discovery, status/custom-config inspection, and auth watching.
|
||||
|
||||
## Sidecar ownership
|
||||
|
||||
`packages/agent/claude-sdk-sidecar/src/main.ts` is only the stdio server and
|
||||
request router. `sessionRuntime.ts` coordinates a session through focused
|
||||
collaborators:
|
||||
|
||||
- `protocol.ts` and `eventSink.ts`: versioned wire envelopes and event emission.
|
||||
- `sessionConfiguration.ts`, `sessionSettings.ts`, and `options.ts`: SDK query
|
||||
configuration and mutable session settings.
|
||||
- `promptQueue.ts` and `turnLifecycle.ts`: prompt ordering and turn ownership.
|
||||
- `goalExecQueue.ts`: queued Goal command ordering and supersession.
|
||||
- `goalProjection.ts` and `goalTranscript.ts`: provider Goal normalization and
|
||||
narrow recovery of SDK-omitted transcript evidence.
|
||||
- `queryGeneration.ts` and `queryHooks.ts`: one SDK Query execution generation,
|
||||
its prompt/abort resources, and generation-scoped SDK hooks.
|
||||
- `messageRouter.ts`, `messageProjection.ts`, `assistantStream.ts`, and
|
||||
`sdkMessages.ts`: SDK message routing and assistant output projection.
|
||||
- `toolActivity.ts`, `toolEvents.ts`, `toolActivityTypes.ts`, `taskPlan.ts`, and
|
||||
`taskNotification.ts`: normalized tool, delegated-task, and plan activity.
|
||||
- `interactive.ts` and `interactiveTurnResolver.ts`: approvals, interactive
|
||||
questions, and canonical Turn ownership resolution.
|
||||
- `compaction.ts` and `usage.ts`: context compaction and usage reporting.
|
||||
Restore/resume publishes `session_started` before refreshing context usage;
|
||||
`getContextUsage()` runs in the background and must not gate Host Resume.
|
||||
- `authDiagnostics.ts`, `errors.ts`, and `runtimeValues.ts`: diagnostics and
|
||||
small runtime value helpers.
|
||||
- `testDriver.ts`: isolated deterministic sidecar-driver behavior used by
|
||||
runtime integration tests.
|
||||
|
||||
The dependency direction is from `main.ts` to `sessionRuntime.ts`, then to these
|
||||
collaborators. Collaborators must not import `main.ts` or own the stdio loop.
|
||||
Projection modules emit typed sidecar events; they do not call daemon or GUI
|
||||
code.
|
||||
|
||||
The outbound root user UUID is correlation evidence, not provider identity.
|
||||
Normally the live SDK iterator returns the persisted root user message and the
|
||||
sidecar binds its UUID before projecting the provider Turn. A successful query
|
||||
may omit that echo and begin with assistant output or an interactive tool.
|
||||
Every root assistant, stream, tool, approval, user-input, and result path enters
|
||||
one shared single-flight identity barrier before projection. The barrier reads
|
||||
the official provider transcript, resolves exactly one root user message by the
|
||||
opaque correlation UUID, and emits `provider_turn_identity_resolved` with the
|
||||
latest persisted checkpoint. A bounded cancellable retry handles short
|
||||
transcript write delay; absence at the deadline and ambiguity fail explicitly.
|
||||
|
||||
The Go adapter converts the resolved identity into a durable acceptance
|
||||
receipt. Its event reader blocks until the Host atomically persists the
|
||||
canonical Turn, provider Session, and provider Turn mapping. The canonical
|
||||
`root_provider_turn.started` is published exactly once before streaming or
|
||||
interactive activity is released. Provider start, checkpoint, and completion
|
||||
never derive identity from the outbound UUID alone.
|
||||
|
||||
Raw sidecar stderr is never copied into activity, logs, or user-visible errors.
|
||||
The Go transport retains only a bounded failure classification; explicitly
|
||||
prefixed auth diagnostics are separately sanitized before structured logging.
|
||||
|
||||
SDK Query lifetime is narrower than durable provider-session lifetime. The
|
||||
sidecar owns Query termination; an RPC caller deadline alone is never treated
|
||||
as cancellation because it cannot stop an already-dispatched SDK control
|
||||
request. A user cancel carries the exact canonical Turn, a cooperative
|
||||
interrupt budget, and a consumer-drain budget, and returns one structured disposition:
|
||||
`pre_accept`, `provider_active`, `absent`, or `mismatch`. An undispatched queue
|
||||
entry can be removed locally. Once the prompt has been dispatched, cancel first
|
||||
revokes the current Query generation and requests the SDK interrupt. If Claude
|
||||
Code acknowledges within the cooperative budget, shutdown remains graceful. If
|
||||
the acknowledgment is missing or fails, the sidecar closes the owned SDK Query
|
||||
transport; the pinned SDK then ends stdin and escalates its Claude Code child
|
||||
from `SIGTERM` to `SIGKILL`. The sidecar waits only for the separate drain budget
|
||||
for its consumer to settle. A drain timeout is an explicit bounded failure and
|
||||
never leaves the request pending. Revocation fences message routing, hooks, and
|
||||
`canUseTool` before permission-mode handling, including `bypassPermissions`;
|
||||
background-task notifications from the canceled generation therefore cannot
|
||||
start a synthetic continuation or execute another tool. Every Turn already
|
||||
handed to the retired Query's prompt queue settles after that same authoritative
|
||||
shutdown boundary, including a drained Goal command that can no longer execute;
|
||||
a Goal command still in the sidecar's deferred queue is independent and remains
|
||||
eligible for exact local removal. For a
|
||||
`provider_active` response, the Go adapter also waits for the exact Turn's
|
||||
durable provider-acceptance outcome before confirming cancellation. `absent`
|
||||
is the only authoritative not-found result; mismatches, unknown dispositions,
|
||||
drain failures, and acceptance failures remain fail-closed. The next real
|
||||
user prompt creates a fresh Query with
|
||||
`resume: providerSessionId` and generation-local prompt/abort resources. If the
|
||||
SDK replays the canceled generation's terminal task notification and paired
|
||||
result during resume, the new generation consumes that tail without attaching
|
||||
it to the new canonical Turn. Session close remains permanent and closes the
|
||||
current generation without creating a resumable successor.
|
||||
|
||||
The Go adapter also tracks each accepted-but-not-started typed Goal command by
|
||||
its operation, revision, repair epoch, action, and previous optimistic Goal
|
||||
mirror. A Goal-generation fence targets both those pending commands and Turns
|
||||
that already have provider bindings. When the sidecar confirms an exact pending
|
||||
Goal cancellation, the adapter removes its bookkeeping and restores the
|
||||
previous mirror only while that same identity is still current, so a stale
|
||||
cancellation cannot overwrite a newer Goal. `goal_command_started` records
|
||||
provider progress but does not commit the optimistic mirror; an authoritative
|
||||
Goal observation or successful command terminal commits it, while cancellation,
|
||||
failure, or supersession closes the exact pending transaction.
|
||||
|
||||
Guidance preempts a provider response without ending its canonical Turn. The
|
||||
sidecar captures the already-active Query and calls its SDK `interrupt()` before
|
||||
the guidance handler first yields; it never waits for query creation or pending
|
||||
configuration while the old response continues. As soon as that interrupt
|
||||
succeeds, the sidecar emits `guidance_interrupted`, and only then queues the
|
||||
guidance prompt. The daemon uses that response boundary to settle open thinking,
|
||||
assistant, and tool projections without emitting a Turn terminal. Claude's
|
||||
later matching `error_during_execution` is bookkeeping only. Output produced
|
||||
for the guidance then starts a fresh response projection on the same canonical
|
||||
Turn.
|
||||
|
||||
Background-task lifecycle uses the SDK's `background_tasks_changed` system
|
||||
message as a level signal. Its `tasks` array fully replaces the previous live
|
||||
set. An empty set means the background children have quiesced; it does not mean
|
||||
the root Turn is complete. When a successful ordinary root result arrives while
|
||||
a background continuation is already pending, the sidecar retains the original
|
||||
root Turn until session idle instead of emitting a terminal/start pair. If the
|
||||
root result already settled before the pending signal, the sidecar reserves a
|
||||
synthetic continuation. It remains in the existing running phase,
|
||||
and results with `origin.kind = task-notification` confirm background
|
||||
follow-up output without settling it by notification/result count. The SDK's
|
||||
`session_state_changed: idle` event is the authoritative turn-over edge after
|
||||
the held-back result and background-agent loop drain. The sidecar does not
|
||||
start a post-result settlement timer: queued follow-ups may legitimately begin
|
||||
several seconds apart, and interrupting that queue fabricates provider errors.
|
||||
If root output does not begin within 30 seconds, the sidecar emits the
|
||||
`continuation_delayed` warning, completes the synthetic reservation with
|
||||
`background_agent_continuation_timeout`, interrupts the pending SDK query, and
|
||||
rejects that continuation's late output so it cannot attach to a later Turn.
|
||||
|
||||
Exact task terminal events and the level signal are not ordered by contract.
|
||||
The sidecar therefore waits for a short quiescence grace after the live set
|
||||
becomes empty. Exact terminal edges received during that window win; after the
|
||||
window, the daemon marks only still-unresolved asynchronous child Turns as
|
||||
interrupted. Background-level diagnostics record aggregate counts for tasks
|
||||
observed by the SDK level signal and exact delegated-task states known to Tutti.
|
||||
This makes missing terminal edges visible in logs while leaving the GUI's
|
||||
existing running/processing presentation unchanged.
|
||||
|
||||
The background-task level tracker owns its quiescence timer and continuation
|
||||
reservation state. Query cancellation clears both before interrupting the SDK.
|
||||
A failed or canceled root result fences later empty-level and task-notification
|
||||
signals from opening a synthetic continuation, while those signals may still
|
||||
report diagnostics or settle their exact child lifecycle.
|
||||
|
||||
The daemon resolves one owner for each Claude tool event before both closed-Turn
|
||||
admission and activity projection. A delegation call belongs to the Session
|
||||
that launched the child; an ordinary tool executed by a child belongs to that
|
||||
child. Settling the child therefore cannot suppress a later completion of its
|
||||
still-open parent Agent call, and repeated parent updates cannot move a terminal
|
||||
child back to running.
|
||||
|
||||
The SDK may also report one completed child twice: first as `task_updated` with
|
||||
only its task description, then as `task_notification` with the actual result.
|
||||
The first edge settles the child lifecycle; the later notification updates the
|
||||
same child assistant-message snapshot and marks its root continuation pending.
|
||||
Several queued notifications may be coalesced into fewer follow-up results, so
|
||||
they share the currently active root provider Turn or one reserved synthetic
|
||||
Turn. Result origin identifies those follow-ups; session idle, rather than a
|
||||
cardinality comparison, terminates the shared continuation.
|
||||
|
||||
## Protocol and compatibility
|
||||
|
||||
The daemon and sidecar exchange newline-delimited JSON over standard input and
|
||||
output. Every request and event carries the current protocol version. Missing
|
||||
or unsupported versions fail explicitly. Change both
|
||||
`claude_sdk_protocol.go` and `src/protocol.ts` together and cover the change on
|
||||
both sides. Version 10 makes the cooperative-interrupt and consumer-drain
|
||||
budgets required cancel-request fields. Version 9 makes the cancel disposition, exact canonical Turn ID,
|
||||
provider Turn ID, and dispatch phase correctness-required response fields.
|
||||
|
||||
Capability and composer contracts are intentionally stable across this runtime
|
||||
split. Imported historical metadata may still be read for display compatibility
|
||||
without affecting runtime selection.
|
||||
|
||||
Desktop packaging runs the deterministic sidecar protocol smoke twice: once
|
||||
after production dependencies are vendored, and again from the final Electron
|
||||
Resources directory after symlinks have been replaced. Both checks must complete
|
||||
`start`, `exec`, and `close` without reading repository sources.
|
||||
|
||||
The vendored bundle excludes the native `claude` executable (it still carries
|
||||
the SDK's JS, type metadata, and `manifest.json`). The binary the SDK spawns
|
||||
is provisioned at runtime by tuttid from the CDN (npm mirrors as fallback),
|
||||
pinned and verified against the vendored SDK's `manifest.json` (see
|
||||
`services/tuttid/service/agentstatus/claude_binary.go`). The sidecar picks the
|
||||
executable in `src/executablePath.ts`: an explicit `CLAUDE_CODE_EXECUTABLE`
|
||||
always wins, a native package next to the SDK (dev tree) comes next, and
|
||||
`TUTTI_CLAUDE_CODE_FALLBACK_EXECUTABLE` — the provisioned binary or a
|
||||
PATH-installed claude, chosen by `runtimeprep.ClaudeCodePreparer` — covers the
|
||||
packaged app.
|
||||
|
||||
`close` is a request/ack boundary, not a fire-and-forget signal. The sidecar
|
||||
awaits SDK query shutdown before replying `ok`; the daemon only then closes
|
||||
stdin and the process. Transport reads are context-aware so a request timeout
|
||||
can stop waiting without implicitly killing a provider process.
|
||||
|
||||
## Code health and validation
|
||||
|
||||
Production business files follow the repository limit of at most 800 lines.
|
||||
Before adding another responsibility to a file near that limit, extract a
|
||||
named collaborator with one owner and a directed dependency. Tests are grouped
|
||||
by session, assistant streaming, delegated tasks, nested tasks, interaction,
|
||||
and lifecycle behavior rather than collected in one fixture file.
|
||||
|
||||
Run these focused checks after changing the runtime:
|
||||
|
||||
```sh
|
||||
cd packages/agent/claude-sdk-sidecar
|
||||
pnpm typecheck
|
||||
pnpm test
|
||||
pnpm exec oxlint src
|
||||
|
||||
cd ../daemon
|
||||
go test ./runtime
|
||||
golangci-lint run ./runtime/...
|
||||
```
|
||||
|
||||
Also run `pnpm check:changed` from the repository root for cross-package
|
||||
contracts and packaging checks.
|
||||
Reference in New Issue
Block a user