Files
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

304 lines
10 KiB
Go

package agentruntime
import (
"context"
"sync"
"time"
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
"github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
)
const (
claudeSDKSidecarCommandEnv = "TUTTI_CLAUDE_SDK_SIDECAR_COMMAND"
claudeSDKSidecarEntryPathEnv = "TUTTI_CLAUDE_SDK_SIDECAR_ENTRY_PATH"
claudeSDKSidecarTestDriverEnv = "TUTTI_CLAUDE_SDK_SIDECAR_TEST_DRIVER"
claudeSDKAppNodeEnv = "TUTTI_APP_NODE"
claudeSDKAppRuntimeRootEnv = "TUTTI_APP_RUNTIME_ROOT"
claudeSDKAppRuntimeCacheEnv = "TUTTI_APP_RUNTIME_CACHE_ROOT"
claudeSDKSidecarAdapterName = "claude-agent-sdk"
claudeSDKSidecarDefaultNodeArg = "--experimental-strip-types"
claudeSDKAuthRefreshLogPrefix = "CLAUDE_CODE_AUTH_REFRESH_DEBUG"
claudeSDKCancelLogPrefix = "CLAUDE_CODE_CANCEL_DIAGNOSTIC"
claudeSDKProviderTurnLogPrefix = "CLAUDE_CODE_PROVIDER_TURN_DIAGNOSTIC"
)
type ClaudeCodeSDKAdapter struct {
transport ProcessTransport
preparer ProviderLaunchPreparer
mu sync.Mutex
sessions map[string]*claudeSDKAdapterSession
retiredSessions map[string][]*claudeSDKAdapterSession
terminalInteractions terminalInteractiveDispositionStore
interactiveDispositionSink InteractiveDispositionSink
commandSink CommandSnapshotSink
eventSink SessionEventSink
promptImageMaterializer providerPromptImageMaterializer
interactiveAckTimeout time.Duration
inputUnits *providerInputUnitTracker
lifecycleMu sync.Mutex
lifecycleLocks map[string]*claudeSDKSessionLock
}
type claudeSDKSessionLock struct {
mu sync.Mutex
refs int
}
type claudeSDKAdapterSession struct {
// dispatchMu is the exact-session commit axis. Event projection may run
// before it, but canonical publication and registry replacement must hold
// this mutex so a stale reader cannot commit after a new generation wins.
dispatchMu sync.Mutex
conn ProcessConnection
reader *claudeSDKLineReader
session Session
providerSessionID string
resumeCursor map[string]any
childSessions map[string]claudeSDKChildSession
assistantMessages map[string]string
thinkingMessages map[string]string
compactMessages map[string]claudeSDKCompactMessage
pendingRequests map[string]*pendingInteractiveRequest
pendingResponses map[string]chan claudeSDKSidecarEvent
turns map[string]*claudeSDKTurnWaiter
// turnNormalizers owns each Claude turn's event lifecycle the same way
// Codex/ACP use acpTurnNormalizer: track open tool calls while the turn is
// live, and Finish* closes dangling calls when the turn reaches a terminal
// state. Guarded by the adapter mutex.
turnNormalizers map[string]*acpTurnNormalizer
liveState claudeSDKLiveState
sendMu sync.Mutex
readerStarted bool
// invalid is guarded by the adapter mutex. Once set, a stale Resume
// attempt must never put this session back into the live registry.
invalid bool
// lifecycleSeq numbers the adapter's TurnLifecycle snapshots (ADR 0008):
// monotonically increasing per session so consumers receiving snapshots
// over different channels (the Exec emit closure and the session event
// sink) can drop stale ones. Guarded by the adapter mutex.
lifecycleSeq uint64
// diagnosticEventSeq orders the bounded Claude SDK lifecycle diagnostics
// written by the Go adapter. It never participates in event projection or
// durable turn state. Guarded by the adapter mutex.
diagnosticEventSeq uint64
// settledTurns remembers turn IDs whose terminal event already left this
// adapter, so a late Cancel re-states the settled snapshot instead of
// fabricating a competing terminal transition. Guarded by the adapter
// mutex.
settledTurns map[string]string
// rootTurnID remains the canonical WorkspaceAgentTurn across SDK-native
// synthetic continuations. Provider turn ids are aliases, never new root
// WorkspaceAgentTurn ids.
rootTurnID string
rootProviderTurns map[string]struct{}
// providerAcceptanceOutcomes retains the durable Host acceptance result by
// exact canonical Turn. Cancel dispatches to the provider first and consults
// this latch only when the sidecar confirms a provider-active cancellation.
// Guarded by the adapter mutex.
providerAcceptanceOutcomes map[string]*claudeSDKProviderAcceptanceOutcome
// goalArmTurnID is the sidecar turn carrying a queued /goal set command
// that has not settled yet; until it does, other turns settling must not
// be read as goal completion. Guarded by the adapter mutex.
goalArmTurnID string
// goalClearControlTurns identifies the provider turns created only to carry
// a native /goal clear command. Claude emits an assistant acknowledgement
// for those turns, but goal control is thread metadata rather than
// transcript content, so their assistant/thinking projection is suppressed
// before persistence. Guarded by the adapter mutex.
goalClearControlTurns map[string]struct{}
// Durable goal identity associated with the arm/continuation lifecycle.
// It is deliberately independent of goalArmTurnID.
goalOperationID string
goalRevision int64
goalRepairEpoch int64
fencedGoalIdentities map[goalOperationIdentity]struct{}
fencedGoalTurns map[string]claudeSDKGoalTurnFenceState
goalTurnBindings map[string]claudeSDKGoalTurnBinding
pendingGoalCommands map[string]claudeSDKPendingGoalCommand
}
type claudeSDKProviderAcceptanceOutcome struct {
done chan struct{}
once sync.Once
err error
}
type claudeSDKGoalTurnFenceState uint8
const (
claudeSDKGoalTurnFenceSuppress claudeSDKGoalTurnFenceState = iota + 1
claudeSDKGoalTurnFenceSettle
)
type claudeSDKGoalTurnBinding struct {
turnID string
providerTurnID string
origin string
identity goalOperationIdentity
published bool
publicationDone chan struct{}
}
type claudeSDKPendingGoalCommand struct {
identity goalOperationIdentity
action string
previousGoal map[string]any
started bool
}
type claudeSDKCompactMessage struct {
messageID string
active bool
// terminalStatus makes the first explicit or synthesized terminal update
// authoritative. A late sidecar result must not overwrite a canceled turn.
// Guarded by the adapter mutex.
terminalStatus string
}
type claudeSDKChildSession struct {
Key string
AgentSessionID string
ParentToolUseID string
TurnID string
RootAgentSessionID string
RootTurnID string
ParentAgentSessionID string
ParentTurnID string
TaskID string
AgentID string
Description string
Status string
Summary string
LastToolName string
Async bool
StartedAtUnixMS int64
UpdatedAtUnixMS int64
CompletedAtUnixMS int64
}
type claudeSDKTurnWaiter struct {
mu sync.Mutex
turnID string
emit EventSink
events []activityshared.Event
done chan claudeSDKTurnResult
completed bool
}
type claudeSDKTurnResult struct {
events []activityshared.Event
err error
}
type claudeSDKLineReader struct {
conn ProcessConnection
buffer string
stderrDiagnosticBuffer string
trackInputUnits bool
lines []claudeSDKBufferedLine
// stderrTail keeps only a bounded, sanitized classification of sidecar
// diagnostics. Raw stderr may contain prompts, paths, credentials, or stack
// traces and must never enter durable activity or user-visible errors.
stderrTail []byte
}
type claudeSDKBufferedLine struct {
value string
unit ProviderInputUnit
}
func providerInputUnitsEnabled(conn ProcessConnection) bool {
_, ok := conn.(ProviderInputUnitCompletion)
return ok
}
func newClaudeSDKLineReader(
conn ProcessConnection,
trackInputUnits bool,
) *claudeSDKLineReader {
return &claudeSDKLineReader{
conn: conn,
trackInputUnits: trackInputUnits,
}
}
func NewClaudeCodeSDKAdapter(transport ProcessTransport) *ClaudeCodeSDKAdapter {
return &ClaudeCodeSDKAdapter{
transport: transport,
sessions: make(map[string]*claudeSDKAdapterSession),
retiredSessions: make(map[string][]*claudeSDKAdapterSession),
interactiveAckTimeout: claudeSDKInteractiveAckTimeout,
inputUnits: providerInputUnitTrackerForTransport(transport),
lifecycleLocks: make(map[string]*claudeSDKSessionLock),
}
}
func (*ClaudeCodeSDKAdapter) Provider() string {
return ProviderClaudeCode
}
func (*ClaudeCodeSDKAdapter) ConnectorCapabilities(
context.Context,
Session,
) (ConnectorCapabilities, error) {
return ConnectorCapabilities{HTTPMCP: true}, nil
}
func (*ClaudeCodeSDKAdapter) UsesRootProviderTurnLifecycle() bool {
return true
}
func (a *ClaudeCodeSDKAdapter) SetProviderLaunchPreparer(preparer ProviderLaunchPreparer) {
if a == nil {
return
}
a.preparer = preparer
}
func (a *ClaudeCodeSDKAdapter) SessionState(session Session) SessionStateSnapshot {
adapterSession := a.getSession(session.AgentSessionID)
return SessionStateSnapshot{
RoomID: session.RoomID,
AgentSessionID: session.AgentSessionID,
Provider: session.Provider,
ProviderSessionID: session.ProviderSessionID,
Status: session.Status,
TurnLifecycle: cloneRuntimeTurnLifecycle(session.TurnLifecycle),
SubmitAvailability: cloneRuntimeSubmitAvailability(session.SubmitAvailability),
PermissionModeID: session.PermissionModeID,
Settings: cloneOptionalSessionSettings(session.Settings),
Capabilities: canonical.NewCapabilitySnapshot(claudeSDKCapabilities(session)),
RuntimeContext: claudeSDKRuntimeContext(session, adapterSession),
PendingInteractive: a.claudeSDKPendingInteractive(adapterSession),
UpdatedAtUnixMS: session.UpdatedAtUnixMS,
}
}
func (a *ClaudeCodeSDKAdapter) SetCommandSnapshotSink(sink CommandSnapshotSink) {
if a == nil {
return
}
a.mu.Lock()
a.commandSink = sink
a.mu.Unlock()
}
func (a *ClaudeCodeSDKAdapter) SetSessionEventSink(sink SessionEventSink) {
if a == nil {
return
}
a.mu.Lock()
a.eventSink = sink
a.mu.Unlock()
}
func (a *ClaudeCodeSDKAdapter) SessionCommandSnapshot(session Session) (AgentSessionCommandSnapshot, bool) {
adapterSession := a.getSession(session.AgentSessionID)
if adapterSession == nil {
return AgentSessionCommandSnapshot{}, false
}
return adapterSession.commandSnapshot(session.AgentSessionID)
}