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

791 lines
38 KiB
Go

package agenthost
import (
"context"
"errors"
"fmt"
"strings"
"unicode/utf8"
"github.com/google/uuid"
storesqlite "github.com/tutti-os/tutti/packages/agent/store-sqlite"
)
// CreateSession reports at most one aggregated TerminalFailure for a failed
// command. Cleanup after a primary failure stays diagnostic.
func (h *Host) CreateSession(ctx context.Context, workspaceID string, input CreateSessionInput) (CreateSessionResult, error) {
clientSubmitID := firstNonEmptyTrimmed(input.ClientSubmitID, legacyClientSubmitID(input.Metadata))
activationID := strings.TrimSpace(input.ActivationID)
operationID := firstNonEmptyTrimmed(activationID, clientSubmitID, input.AgentSessionID)
ctx, command := h.beginCommand(ctx, commandTerminalFailureInput{
flow: "session_create", workspaceID: workspaceID, agentSessionID: input.AgentSessionID,
operationID: operationID, requestID: activationID, clientSubmitID: clientSubmitID, turnID: input.TurnID,
})
var result CreateSessionResult
err := h.withWorkspaceRuntimeOperationInfo(ctx, WorkspaceRuntimeOperationInfo{
WorkspaceID: workspaceID, OperationID: operationID, Kind: "session_create",
AgentSessionID: input.AgentSessionID, Source: "host.CreateSession",
}, func(operationCtx context.Context) error {
var createErr error
result, createErr = h.createSession(operationCtx, workspaceID, input)
return createErr
})
command.finish(ctx, h, err)
return result, err
}
func (h *Host) createSession(ctx context.Context, workspaceID string, input CreateSessionInput) (CreateSessionResult, error) {
workspaceID, input.AgentSessionID = strings.TrimSpace(workspaceID), strings.TrimSpace(input.AgentSessionID)
input.Provider, input.AgentTargetID = strings.TrimSpace(input.Provider), strings.TrimSpace(input.AgentTargetID)
if h == nil || h.runtime == nil || h.store == nil || workspaceID == "" || input.AgentSessionID == "" || input.Provider == "" {
return createSessionFailureResult(input, ErrInvalidArgument)
}
var err error
input.RailPlacement, err = normalizeRailPlacement(input.RailPlacement)
if err != nil {
return createSessionFailureResult(input, err)
}
ref := SessionRef{WorkspaceID: workspaceID, AgentSessionID: input.AgentSessionID}
normalized, promptText, err := normalizeOptionalPromptContent(input.InitialContent)
if err != nil {
return createSessionFailureResult(input, err)
}
typedGoal, isTypedGoal := ParseTypedGoalControl(normalized, false)
if input.InitialGoalControl != nil {
if len(normalized) != 0 {
return createSessionFailureResult(input, ErrInvalidArgument)
}
typedGoal, err = normalizeTypedGoalControl(*input.InitialGoalControl)
if err != nil {
return createSessionFailureResult(input, err)
}
isTypedGoal = true
}
metadata := submissionMetadata(input.Metadata, input.ClientSubmitID)
goalMetadata := clonePayload(metadata)
goalInput := GoalControlInput{
WorkspaceID: workspaceID, AgentSessionID: input.AgentSessionID,
Action: typedGoal.Action, Objective: typedGoal.Objective,
ClientSubmitID: input.ClientSubmitID, SubmissionMetadata: goalMetadata,
}
if isTypedGoal {
if replay, found, replayErr := h.replayInitialGoalCreate(ctx, input, goalInput); found || replayErr != nil {
return replay, replayErr
}
}
claimMetadata := metadata
if isTypedGoal || len(normalized) == 0 {
normalized = nil
claimMetadata = nil
}
if len(normalized) > 0 && strings.TrimSpace(input.TurnID) == "" {
input.TurnID = uuid.NewString()
}
claim, claimPending, err := h.prepareSubmitClaim(ctx, ref, claimMetadata, input.TurnID)
if err != nil {
if errors.Is(err, storesqlite.ErrSubmitClaimTurnConflict) {
return createSessionFailureResult(input, errors.Join(ErrSubmitDeliveryUnknown, err))
}
return createSessionFailureResult(input, err)
}
if claim.ClientSubmitID != "" && !claimPending {
if claim.Status != "accepted" && claim.Status != "rejected" {
return createSessionFailureResult(input, ErrSubmitDeliveryUnknown)
}
canonicalSession, _, readErr := h.store.GetSession(ctx, workspaceID, input.AgentSessionID)
if readErr != nil {
return createSessionFailureResult(input, readErr)
}
if !railPlacementMatchesSession(input.RailPlacement, canonicalSession) {
return createSessionFailureResult(input, ErrRailPlacementConflict)
}
runtimeSession, _ := h.runtime.Session(workspaceID, input.AgentSessionID)
return CreateSessionResult{Session: runtimeSession, Canonical: canonicalSession, TurnID: claim.TurnID, SessionStatus: CreateSessionStatusCreated, InitialGoalStatus: CreateSessionInitialGoalStatusNotRequested}, nil
}
defer func() {
if claimPending {
h.abandonSubmitClaim(ref, claim.ClientSubmitID)
}
}()
runtimePublisher, ok := h.runtime.(RuntimeSessionInitializationPublisher)
if !ok {
return createSessionFailureResult(input, ErrRuntimeSessionPublishUnavailable)
}
prepared := PreparedRuntime{Cwd: strings.TrimSpace(value(input.Cwd))}
if h.preparation != nil {
preparedAt := h.now()
prepared, err = h.preparation.Prepare(ctx, createPreparationInput(workspaceID, input))
// Only observe failures here. Service adapters already emit the success
// node_result for runtime_prepared before Host CreateSession; a success
// LifecycleStep would duplicate that analytics event.
if err != nil {
h.observeStep(ctx, "session_create", "runtime_prepared", workspaceID, input.AgentSessionID, input.Provider, preparedAt, err)
return createSessionFailureResult(input, err)
}
}
sessionLockHeld := false
cleanup := func(cause error, started bool, canonicalCreated bool) error {
return h.cleanupFailedCreate(ctx, ref, input.Provider, cause, failedCreateCleanupState{
RuntimeStarted: started, CanonicalCreated: canonicalCreated, SessionLockHeld: sessionLockHeld,
})
}
releaseSession, err := h.acquireSession(ctx, ref)
if err != nil {
return createSessionFailureResult(input, cleanup(err, false, false))
}
sessionLockHeld = true
defer func() {
if sessionLockHeld {
releaseSession()
}
}()
canonicalBeforeStart, canonicalExisted, err := h.store.GetSession(ctx, workspaceID, input.AgentSessionID)
if err != nil {
return createSessionFailureResult(input, cleanup(err, false, false))
}
if canonicalExisted && !railPlacementMatchesSession(input.RailPlacement, canonicalBeforeStart) {
return createSessionFailureResult(input, cleanup(ErrRailPlacementConflict, false, false))
}
if input.RailPlacement, prepared.Env, err = h.resolveCreateRuntimeRailEnvironment(ctx, workspaceID, input, prepared); err != nil {
return createSessionFailureResult(input, cleanup(err, false, false))
}
startedAt := h.now()
release, err := h.acquireStartup(ctx, input.Provider)
if err != nil {
h.observeStep(ctx, "session_create", "runtime_started", workspaceID, input.AgentSessionID, input.Provider, startedAt, err)
return createSessionFailureResult(input, cleanup(err, false, false))
}
startResult, err := func() (RuntimeStartResult, error) {
defer release()
runtimeTitle, initialTitleEstablished := initialGoalRuntimeTitle(value(input.Title), input.InitialDisplayPrompt, typedGoal, isTypedGoal)
return h.runtime.Start(ctx, RuntimeStartInput{
WorkspaceID: workspaceID, AgentSessionID: input.AgentSessionID, AgentTargetID: input.AgentTargetID,
Provider: input.Provider, Cwd: prepared.Cwd, Env: append([]string(nil), prepared.Env...),
MCPServers: cloneHostMCPServerBindings(prepared.MCPServers), Title: runtimeTitle, InitialTitleEstablished: initialTitleEstablished,
PermissionModeID: value(input.PermissionModeID), Model: value(input.Model), PlanMode: valueBool(input.PlanMode),
BrowserUse: input.BrowserUse, ComputerUse: input.ComputerUse, CodexSaverMode: valueBool(input.CodexSaverMode), RTKSaverMode: valueBool(input.RTKSaverMode),
ProviderTargetRef: cloneMap(firstMap(prepared.ProviderTargetRef, input.ProviderTargetRef)),
RuntimeContext: cloneMap(input.RuntimeContext), ReasoningEffort: value(input.ReasoningEffort),
Speed: value(input.Speed), ConversationDetailMode: strings.TrimSpace(input.ConversationDetailMode),
Visible: input.Visible, Provisional: len(normalized) > 0,
CanonicalInitPending: true,
})
}()
if err != nil {
h.observeStep(ctx, "session_create", "runtime_started", workspaceID, input.AgentSessionID, input.Provider, startedAt, err)
return createSessionFailureResult(input, cleanup(err, false, false))
}
session := startResult.Session
runtimeCreated := startResult.Created
h.observeStep(ctx, "session_create", "runtime_started", workspaceID, session.ID, session.Provider, startedAt, nil)
startedAt = h.now()
canonicalSession, err := h.store.InitializeRuntimeSession(ctx, runtimeSessionInitializationForCreate(session, input))
if err != nil {
h.observeStep(ctx, "session_create", "session_persisted", workspaceID, session.ID, session.Provider, startedAt, err)
return createSessionFailureResult(input, cleanup(err, runtimeCreated, false))
}
canonicalCreated := !canonicalExisted
if strings.TrimSpace(canonicalSession.ID) != strings.TrimSpace(session.ID) || strings.TrimSpace(canonicalSession.WorkspaceID) != workspaceID || strings.TrimSpace(canonicalSession.RailSectionKey) == "" {
identityErr := fmt.Errorf("initialize workspace agent session: persisted session identity mismatch")
h.observeStep(ctx, "session_create", "session_persisted", workspaceID, session.ID, session.Provider, startedAt, identityErr)
return createSessionFailureResult(input, cleanup(identityErr, runtimeCreated, canonicalCreated))
}
if !railPlacementMatchesSession(input.RailPlacement, canonicalSession) {
placementErr := ErrRailPlacementConflict
h.observeStep(ctx, "session_create", "session_persisted", workspaceID, session.ID, session.Provider, startedAt, placementErr)
return createSessionFailureResult(input, cleanup(placementErr, runtimeCreated, canonicalCreated))
}
h.observeStep(ctx, "session_create", "session_persisted", workspaceID, session.ID, session.Provider, startedAt, nil)
startedAt = h.now()
published, err := publishRuntimeSessionInitialization(
ctx,
runtimePublisher,
RuntimeSessionInitializationPublishInput{
WorkspaceID: workspaceID, AgentSessionID: session.ID,
},
)
if err != nil {
h.observeStep(ctx, "session_create", "session_published", workspaceID, session.ID, session.Provider, startedAt, err)
return createSessionFailureResult(input, cleanup(err, runtimeCreated, canonicalCreated))
}
if strings.TrimSpace(published.ID) != strings.TrimSpace(session.ID) ||
strings.TrimSpace(published.WorkspaceID) != workspaceID {
publishErr := errors.New("publish workspace agent session: runtime session identity mismatch")
h.observeStep(ctx, "session_create", "session_published", workspaceID, session.ID, session.Provider, startedAt, publishErr)
return createSessionFailureResult(input, cleanup(publishErr, runtimeCreated, canonicalCreated))
}
session = published
h.observeStep(ctx, "session_create", "session_published", workspaceID, session.ID, session.Provider, startedAt, nil)
if len(normalized) == 0 && !isTypedGoal {
return CreateSessionResult{Session: session, Canonical: canonicalSession, SessionStatus: CreateSessionStatusCreated, InitialGoalStatus: CreateSessionInitialGoalStatusNotRequested}, nil
}
if isTypedGoal {
releaseSession()
sessionLockHeld = false
goalInput.AgentSessionID = session.ID
goalResult, goalErr := h.goalControl(ctx, goalInput)
if goalErr != nil {
if goalControlResultPending(goalResult) {
if refreshed, ok := h.runtime.Session(workspaceID, session.ID); ok {
session = refreshed
}
return CreateSessionResult{
Session: session, Canonical: canonicalSession, Kind: "goalControl", GoalControl: &goalResult,
SessionStatus: CreateSessionStatusCreated, InitialGoalStatus: CreateSessionInitialGoalStatusUnknown,
}, goalErr
}
// A typed goal starts from a non-provisional, already published
// session. Preserve that canonical session on command failure just as
// the legacy Service did; rolling it back would leave subscribers with
// an unpaired session-created event.
return CreateSessionResult{Session: session, Canonical: canonicalSession, Kind: "goalControl", SessionStatus: CreateSessionStatusCreated, InitialGoalStatus: CreateSessionInitialGoalStatusFailed}, cleanup(goalErr, runtimeCreated, false)
}
if refreshed, ok := h.runtime.Session(workspaceID, session.ID); ok {
session = refreshed
}
return CreateSessionResult{Session: session, Canonical: goalResult.Canonical, Kind: "goalControl", GoalControl: &goalResult, SessionStatus: CreateSessionStatusCreated, InitialGoalStatus: CreateSessionInitialGoalStatusSucceeded}, nil
}
startedAt = h.now()
if err := h.runtime.ValidatePromptContent(ctx, RuntimeExecInput{WorkspaceID: workspaceID, AgentSessionID: session.ID, Content: normalized}); err != nil {
h.observeStep(ctx, "session_create", "prompt_validated", workspaceID, session.ID, session.Provider, startedAt, err)
return createSessionFailureResult(input, cleanup(err, runtimeCreated, canonicalCreated))
}
h.observeStep(ctx, "session_create", "prompt_validated", workspaceID, session.ID, session.Provider, startedAt, nil)
startedAt = h.now()
preparedContent, err := h.prepareContent(workspaceID, session.ID, normalized)
if err != nil {
h.observeStep(ctx, "session_create", "prompt_prepared", workspaceID, session.ID, session.Provider, startedAt, err)
return createSessionFailureResult(input, cleanup(err, runtimeCreated, canonicalCreated))
}
h.observeStep(ctx, "session_create", "prompt_prepared", workspaceID, session.ID, session.Provider, startedAt, nil)
displayPrompt := strings.TrimSpace(input.InitialDisplayPrompt)
initialTitle := ""
if !session.InitialTitleEstablished {
initialTitle = DeriveInitialTitle(session.Title, firstNonEmpty(displayPrompt, promptText, preparedContent.DisplayText))
}
startedAt = h.now()
turnID := strings.TrimSpace(input.TurnID)
if turnID == "" {
turnID = uuid.NewString()
}
execResult, err := h.runtime.Exec(ctx, RuntimeExecInput{
WorkspaceID: workspaceID, AgentSessionID: session.ID, TurnID: turnID,
ClientSubmitID: claim.ClientSubmitID, CanonicalSubmitOccurredAtUnixMS: claim.CreatedAtUnixMS,
CapabilityRefs: append([]CapabilityReference(nil), input.CapabilityRefs...), Content: preparedContent.Hydrated,
DisplayPrompt: displayPrompt, InitialTitle: initialTitle, InitialTitleBase: session.Title,
Metadata: cloneMap(metadata), TuttiModeSnapshot: input.TuttiModeSnapshot,
RequireProviderAcceptance: true,
})
recordProviderAcceptanceDiagnostics(ctx, execResult.ProviderDispatch)
if err != nil {
h.observeStep(ctx, "session_create", "runtime_exec", workspaceID, session.ID, session.Provider, startedAt, err)
disposition := execResult.ProviderDispatch.Disposition
if disposition == RuntimeDispatchDispositionRejected ||
disposition == RuntimeDispatchDispositionApplied ||
disposition == RuntimeDispatchDispositionOutcomeUnknown {
if persistErr := h.persistRuntimeSubmitOutcome(
ctx, SessionRef{WorkspaceID: workspaceID, AgentSessionID: session.ID}, execResult,
firstNonEmpty(claim.ClientSubmitID, input.ClientSubmitID, legacyClientSubmitID(metadata)),
claim.CreatedAtUnixMS, preparedContent, displayPrompt, input.CapabilityRefs,
metadata, input.TuttiModeSnapshot,
); persistErr != nil {
claimPending = false
return createSessionCreatedErrorResult(input, session, canonicalSession, errors.Join(ErrSubmitDeliveryUnknown, err, persistErr))
}
if disposition == RuntimeDispatchDispositionRejected {
// A definitive rejection keeps the visible Session/failed Turn. The
// claim is a terminal idempotency fence, so replay reads the same
// failed Turn without invoking the provider again. Once that terminal
// report is durable, discard the startup runtime without publishing a
// canonical completion over the failure.
if strings.TrimSpace(execResult.TurnID) != "" {
claimPending = false
if rejectErr := h.finalizeRejectedSubmitClaim(ref, firstNonEmpty(claim.ClientSubmitID, input.ClientSubmitID, legacyClientSubmitID(metadata)), execResult.TurnID); rejectErr != nil {
return createSessionCreatedErrorResult(input, session, canonicalSession, errors.Join(ErrSubmitDeliveryUnknown, err, rejectErr))
}
}
return createSessionCreatedErrorResult(input, session, canonicalSession, h.discardRejectedPreparedRuntime(ctx, err, workspaceID, session.ID, session.Provider))
}
claimPending = false
return createSessionCreatedErrorResult(input, session, canonicalSession, errors.Join(ErrSubmitDeliveryUnknown, err))
}
return createSessionFailureResult(input, cleanup(err, runtimeCreated, canonicalCreated))
}
turnID = strings.TrimSpace(execResult.TurnID)
if turnID == "" {
h.observeStep(ctx, "session_create", "runtime_exec", workspaceID, session.ID, session.Provider, startedAt, ErrSubmitDeliveryUnknown)
return createSessionFailureResult(input, cleanup(ErrSubmitDeliveryUnknown, runtimeCreated, canonicalCreated))
}
if expectedTurnID := strings.TrimSpace(input.TurnID); expectedTurnID != "" && turnID != expectedTurnID {
claimPending = false
return createSessionCreatedErrorResult(input, session, canonicalSession, ErrSubmitDeliveryUnknown)
}
if reporter, ok := h.runtime.(RuntimeSubmitProvenanceReporter); ok {
if err := reporter.DurablyReportSubmitProvenance(ctx, RuntimeSubmitProvenanceInput{
WorkspaceID: workspaceID, AgentSessionID: session.ID, TurnID: turnID,
ClientSubmitID: claim.ClientSubmitID, CanonicalSubmitOccurredAtUnixMS: claim.CreatedAtUnixMS,
Content: preparedContent.Hydrated, DisplayPrompt: displayPrompt,
}); err != nil {
// Provider acceptance is already possible. Keep the runtime, canonical
// session, and prepared claim intact so a retry cannot dispatch twice.
claimPending = false
return createSessionCreatedErrorResult(input, session, canonicalSession, errors.Join(ErrSubmitDeliveryUnknown, err))
}
}
if err := h.recordTurnSubmission(
ctx, ref, turnID, input.ClientSubmitID, preparedContent.Persisted,
displayPrompt, input.CapabilityRefs, metadata, input.TuttiModeSnapshot,
); err != nil {
claimPending = false
return createSessionCreatedErrorResult(input, session, canonicalSession, errors.Join(ErrSubmitDeliveryUnknown, err))
}
if claim.ClientSubmitID != "" {
claimPending = false
if err := h.acceptSubmitClaim(ref, claim.ClientSubmitID, turnID); err != nil {
return createSessionCreatedErrorResult(input, session, canonicalSession, errors.Join(ErrSubmitDeliveryUnknown, err))
}
}
if refreshed, ok := h.runtime.Session(workspaceID, session.ID); ok {
session = refreshed
}
if refreshed, ok, readErr := h.store.GetSession(ctx, workspaceID, session.ID); readErr == nil && ok {
canonicalSession = refreshed
}
h.observeStep(ctx, "session_create", "runtime_exec", workspaceID, session.ID, session.Provider, startedAt, nil)
return CreateSessionResult{Session: session, Canonical: canonicalSession, TurnID: turnID, SessionStatus: CreateSessionStatusCreated, InitialGoalStatus: CreateSessionInitialGoalStatusNotRequested}, nil
}
func (h *Host) EnsureRuntimeSession(ctx context.Context, ref SessionRef) (ProviderRuntimeSession, error) {
ref.WorkspaceID, ref.AgentSessionID = strings.TrimSpace(ref.WorkspaceID), strings.TrimSpace(ref.AgentSessionID)
if h == nil || h.runtime == nil || h.store == nil || ref.WorkspaceID == "" || ref.AgentSessionID == "" {
return ProviderRuntimeSession{}, ErrSessionNotFound
}
var result ProviderRuntimeSession
err := h.withWorkspaceRuntimeOperationInfo(ctx, WorkspaceRuntimeOperationInfo{
WorkspaceID: ref.WorkspaceID, Kind: "ensure_runtime_session",
AgentSessionID: ref.AgentSessionID, Source: "host.EnsureRuntimeSession",
}, func(operationCtx context.Context) error {
var ensureErr error
result, ensureErr = h.ensureRuntimeSession(operationCtx, ref)
return ensureErr
})
return result, err
}
func (h *Host) ensureRuntimeSession(ctx context.Context, ref SessionRef) (ProviderRuntimeSession, error) {
release, err := h.acquireSession(ctx, ref)
if err != nil {
return ProviderRuntimeSession{}, err
}
defer release()
return h.ensureRuntimeSessionLocked(ctx, ref)
}
func (h *Host) ensureRuntimeSessionLocked(ctx context.Context, ref SessionRef) (ProviderRuntimeSession, error) {
deleted, err := h.store.SessionDeleted(ctx, ref.WorkspaceID, ref.AgentSessionID)
if err != nil {
return ProviderRuntimeSession{}, err
}
if deleted {
return ProviderRuntimeSession{}, ErrSessionNotFound
}
canonicalSession, found, err := h.store.GetSession(ctx, ref.WorkspaceID, ref.AgentSessionID)
if err != nil {
return ProviderRuntimeSession{}, err
}
if found && ResolveResumePolicy(canonicalSession).Mode == ResumeModeReject {
return ProviderRuntimeSession{}, ErrSessionNotFound
}
policy := ResolveResumePolicy(canonicalSession)
evidence := storesqlite.ProviderSessionResumeEvidence{}
if found && policy.Mode != ResumeModeRecreate {
evidence, err = h.store.GetProviderSessionResumeEvidence(ctx, ref.WorkspaceID, ref.AgentSessionID)
if err != nil {
return ProviderRuntimeSession{}, err
}
if !evidence.Established {
evidence.Established, err = h.goalStateProvesProviderSessionEstablished(ctx, ref)
if err != nil {
return ProviderRuntimeSession{}, err
}
}
}
if live, ok := h.runtime.Session(ref.WorkspaceID, ref.AgentSessionID); ok {
if !ExternalImportResumeSupported(live.RuntimeContext) {
return ProviderRuntimeSession{}, ErrSessionNotFound
}
if policy.Mode != ResumeModeRecreate &&
!runtimeSessionHasActiveTurn(live) &&
strings.TrimSpace(canonicalSession.ActiveTurnID) == "" &&
evidence.HasSettledTurn && !evidence.Established {
return ProviderRuntimeSession{}, ErrProviderSessionNotEstablished
}
live.Resumable = live.Resumable || evidence.Established
// Controller may retain the Session record after releasing an idle
// provider connection. Controller's registry handles connection
// replacement; clearing this Host marker additionally refreshes its
// retained set from the durable store before Ensure returns.
if !h.runtimeSessionLive(ref.WorkspaceID, ref.AgentSessionID) {
h.goalFencesRestored.Delete(ref.WorkspaceID + "\x00" + ref.AgentSessionID)
}
if err := h.restoreGoalGenerationFencesOnce(ctx, ref); err != nil {
return ProviderRuntimeSession{}, err
}
return live, nil
}
if !found || strings.TrimSpace(canonicalSession.Provider) == "" {
return ProviderRuntimeSession{}, ErrSessionNotFound
}
if policy.Mode != ResumeModeRecreate &&
!evidence.Established &&
(!evidence.HasTurns || evidence.HasSettledTurn) {
return ProviderRuntimeSession{}, ErrProviderSessionNotEstablished
}
prepared := PreparedRuntime{Cwd: strings.TrimSpace(canonicalSession.Cwd)}
settings := composerSettingsFromMap(canonicalSession.Settings)
if h.preparation != nil {
prepared, err = h.preparation.Prepare(ctx, resumePreparationInput(canonicalSession, settings))
if err != nil {
return ProviderRuntimeSession{}, err
}
}
if prepared.Settings != nil {
settings = *prepared.Settings
}
if prepared.Env, err = runtimeEnvironmentForCanonicalSession(prepared.Env, prepared.Cwd, canonicalSession); err != nil {
return ProviderRuntimeSession{}, err
}
release, err := h.acquireStartup(ctx, canonicalSession.Provider)
if err != nil {
return ProviderRuntimeSession{}, h.cleanupFailedRuntimeResume(ctx, ref, canonicalSession.Provider, err)
}
defer release()
goalGenerationFences, err := h.listRuntimeGoalGenerationFences(ctx, ref)
if err != nil {
return ProviderRuntimeSession{}, h.cleanupFailedRuntimeResume(ctx, ref, canonicalSession.Provider, err)
}
result, err := h.runtime.Resume(ctx, RuntimeResumeInput{
WorkspaceID: ref.WorkspaceID, AgentSessionID: ref.AgentSessionID,
AgentTargetID: strings.TrimSpace(canonicalSession.AgentTargetID), Provider: strings.TrimSpace(canonicalSession.Provider),
ProviderSessionID: strings.TrimSpace(canonicalSession.ProviderSessionID), Resumable: evidence.Established, Cwd: prepared.Cwd,
Env: append([]string(nil), prepared.Env...), MCPServers: cloneHostMCPServerBindings(prepared.MCPServers), Title: strings.TrimSpace(canonicalSession.Title),
Status: persistedRuntimeStatus(canonicalSession.ActiveTurnID), Settings: settings,
CreatedAtUnixMS: canonicalSession.CreatedAtUnixMS, UpdatedAtUnixMS: canonicalSession.UpdatedAtUnixMS,
Visible: boolPointer(canonicalSession.Metadata.Visible), RuntimeContext: cloneMap(firstMap(prepared.RuntimeContext, canonicalSession.InternalRuntimeContext)),
ProviderTargetRef: cloneMap(prepared.ProviderTargetRef), Metadata: canonicalSession.Metadata,
InternalRuntimeContext: cloneMap(canonicalSession.InternalRuntimeContext),
GoalGenerationFences: append([]RuntimeGoalGenerationFenceInput(nil), goalGenerationFences...),
RecreateIfMissing: policy.Mode == ResumeModeRecreate,
})
if err != nil {
return ProviderRuntimeSession{}, h.cleanupFailedRuntimeResume(ctx, ref, canonicalSession.Provider, err)
}
if err := h.restoreGoalGenerationFences(ctx, ref); err != nil {
return ProviderRuntimeSession{}, h.cleanupFailedRuntimeResume(ctx, ref, canonicalSession.Provider, err)
}
h.goalFencesRestored.Store(ref.WorkspaceID+"\x00"+ref.AgentSessionID, struct{}{})
return result, nil
}
// SendInput reports at most one aggregated TerminalFailure for a failed
// command. Guidance target binding and goal control own their own emissions.
func (h *Host) SendInput(ctx context.Context, ref SessionRef, input SendInput) (SendInputResult, error) {
clientSubmitID := firstNonEmptyTrimmed(input.ClientSubmitID, legacyClientSubmitID(input.Metadata))
ctx, command := h.beginCommand(ctx, commandTerminalFailureInput{
flow: "message_send", workspaceID: ref.WorkspaceID, agentSessionID: ref.AgentSessionID,
operationID: firstNonEmptyTrimmed(clientSubmitID, input.TurnID),
clientSubmitID: clientSubmitID, turnID: input.TurnID,
})
result, err := h.sendInput(ctx, ref, input)
command.finish(ctx, h, err)
return result, err
}
func (h *Host) sendInput(ctx context.Context, ref SessionRef, input SendInput) (SendInputResult, error) {
ref.WorkspaceID, ref.AgentSessionID = strings.TrimSpace(ref.WorkspaceID), strings.TrimSpace(ref.AgentSessionID)
if h == nil || h.runtime == nil || h.store == nil || ref.WorkspaceID == "" || ref.AgentSessionID == "" {
return SendInputResult{}, ErrInvalidArgument
}
// Guidance is a mutation of an already-running canonical Turn. Host
// consumers must bind that mutation to the exact Turn observed at the
// interaction boundary; allowing the runtime to infer "current" would make
// an A->B transition during transport silently steer B.
if input.Guidance && strings.TrimSpace(input.TurnID) == "" {
err := ErrActiveTurnTargetRequired
h.observeTerminalFailure(ctx, TerminalFailure{
Flow: "guidance",
FailureStage: "guidance_target",
WorkspaceID: ref.WorkspaceID,
AgentSessionID: ref.AgentSessionID,
ClientSubmitID: strings.TrimSpace(input.ClientSubmitID),
ErrorCode: guidanceTargetFailureCode(err),
ErrorMessage: err.Error(),
Retryable: false,
})
return SendInputResult{}, err
}
normalized, promptText, err := normalizePromptContent(input.Content)
if err != nil {
return SendInputResult{}, err
}
metadata := submissionMetadata(input.Metadata, input.ClientSubmitID)
if typedGoal, ok := ParseTypedGoalControl(normalized, input.Guidance); ok {
goalResult, goalErr := h.goalControl(ctx, GoalControlInput{
WorkspaceID: ref.WorkspaceID, AgentSessionID: ref.AgentSessionID,
Action: typedGoal.Action, Objective: typedGoal.Objective,
ClientSubmitID: input.ClientSubmitID,
SubmissionMetadata: metadata,
})
if goalErr != nil {
return SendInputResult{}, goalErr
}
session, _ := h.runtime.Session(ref.WorkspaceID, ref.AgentSessionID)
return SendInputResult{
Session: session, Canonical: goalResult.Canonical,
Kind: "goalControl", GoalControl: &goalResult,
}, nil
}
var result SendInputResult
err = h.withSessionMutationActor(ctx, ref.WorkspaceID, ref.AgentSessionID, func(actorCtx context.Context) error {
var sendErr error
result, sendErr = h.sendInputSerialized(actorCtx, ref, input, normalized, promptText, metadata)
return sendErr
})
return result, err
}
func (h *Host) sendInputSerialized(
ctx context.Context,
ref SessionRef,
input SendInput,
normalized []PromptContentBlock,
promptText string,
metadata map[string]any,
) (SendInputResult, error) {
var err error
if err := h.requireSendAllowedByEffectiveHistory(ctx, ref); err != nil {
return SendInputResult{}, err
}
if !input.Guidance && strings.TrimSpace(input.TurnID) == "" {
input.TurnID = uuid.NewString()
}
claim, claimPending, err := h.prepareSubmitClaim(ctx, ref, metadata, input.TurnID)
if err != nil {
if errors.Is(err, storesqlite.ErrSubmitClaimTurnConflict) {
return SendInputResult{}, errors.Join(ErrSubmitDeliveryUnknown, err)
}
return SendInputResult{}, err
}
if claim.ClientSubmitID != "" && !claimPending {
if claim.Status != "accepted" && claim.Status != "rejected" {
return SendInputResult{}, ErrSubmitDeliveryUnknown
}
return h.replayedSubmitResult(ctx, ref, claim)
}
defer func() {
if claimPending {
h.abandonSubmitClaim(ref, claim.ClientSubmitID)
}
}()
release, err := h.acquireSession(ctx, ref)
if err != nil {
return SendInputResult{}, err
}
defer release()
startedAt := h.now()
session, err := h.ensureRuntimeSessionLocked(ctx, ref)
if err != nil {
h.observeStep(ctx, "message_send", "runtime_session_ready", ref.WorkspaceID, ref.AgentSessionID, "", startedAt, err)
return SendInputResult{}, err
}
h.observeStep(ctx, "message_send", "runtime_session_ready", ref.WorkspaceID, ref.AgentSessionID, session.Provider, startedAt, nil)
startedAt = h.now()
if err := h.runtime.ValidatePromptContent(ctx, RuntimeExecInput{WorkspaceID: ref.WorkspaceID, AgentSessionID: ref.AgentSessionID, Content: normalized}); err != nil {
h.observeStep(ctx, "message_send", "prompt_validated", ref.WorkspaceID, ref.AgentSessionID, session.Provider, startedAt, err)
return SendInputResult{}, err
}
h.observeStep(ctx, "message_send", "prompt_validated", ref.WorkspaceID, ref.AgentSessionID, session.Provider, startedAt, nil)
startedAt = h.now()
preparedContent, err := h.prepareContent(ref.WorkspaceID, ref.AgentSessionID, normalized)
if err != nil {
h.observeStep(ctx, "message_send", "prompt_prepared", ref.WorkspaceID, ref.AgentSessionID, session.Provider, startedAt, err)
return SendInputResult{}, err
}
h.observeStep(ctx, "message_send", "prompt_prepared", ref.WorkspaceID, ref.AgentSessionID, session.Provider, startedAt, nil)
displayPrompt, initialTitle := strings.TrimSpace(input.DisplayPrompt), ""
if !input.Guidance && !session.InitialTitleEstablished {
initialTitle = DeriveInitialTitle(session.Title, firstNonEmpty(displayPrompt, promptText, preparedContent.DisplayText))
}
startedAt = h.now()
releaseStartup, err := h.acquireStartup(ctx, session.Provider)
if err != nil {
h.observeStep(ctx, "message_send", "runtime_exec", ref.WorkspaceID, ref.AgentSessionID, session.Provider, startedAt, err)
return SendInputResult{}, err
}
execResult, err := func() (RuntimeExecResult, error) {
defer releaseStartup()
turnID := strings.TrimSpace(input.TurnID)
if turnID == "" && !input.Guidance {
turnID = uuid.NewString()
}
return h.runtime.Exec(ctx, RuntimeExecInput{
WorkspaceID: ref.WorkspaceID, AgentSessionID: ref.AgentSessionID,
TurnID: turnID, ClientSubmitID: claim.ClientSubmitID,
CanonicalSubmitOccurredAtUnixMS: claim.CreatedAtUnixMS,
CapabilityRefs: append([]CapabilityReference(nil), input.CapabilityRefs...), Content: preparedContent.Hydrated,
DisplayPrompt: displayPrompt, InitialTitle: initialTitle, InitialTitleBase: session.Title,
Guidance: input.Guidance, Metadata: cloneMap(metadata), TuttiModeSnapshot: input.TuttiModeSnapshot,
RequireProviderAcceptance: !input.Guidance,
ConnectorRoutingUpdate: cloneStringPointer(input.ConnectorRoutingUpdate),
})
}()
recordProviderAcceptanceDiagnostics(ctx, execResult.ProviderDispatch)
if err != nil {
// Only an explicit target verdict is a guidance-target failure. Any
// other undispatched guidance is an ordinary runtime_exec failure that
// the command boundary aggregates.
if input.Guidance &&
(errors.Is(err, ErrActiveTurnTargetMismatch) || errors.Is(err, ErrActiveTurnTargetRequired)) {
h.observeGuidanceTargetFailure(ctx, ref, session.Provider, input.TurnID, claim.ClientSubmitID, startedAt, err)
} else {
h.observeStep(ctx, "message_send", "runtime_exec", ref.WorkspaceID, ref.AgentSessionID, session.Provider, startedAt, err)
}
if !input.Guidance && strings.TrimSpace(execResult.TurnID) != "" {
if persistErr := h.persistRuntimeSubmitOutcome(
ctx, ref, execResult,
firstNonEmpty(claim.ClientSubmitID, input.ClientSubmitID, legacyClientSubmitID(metadata)),
claim.CreatedAtUnixMS, preparedContent, displayPrompt, input.CapabilityRefs,
metadata, input.TuttiModeSnapshot,
); persistErr != nil {
claimPending = false
return SendInputResult{}, errors.Join(ErrSubmitDeliveryUnknown, err, persistErr)
}
}
if !input.Guidance &&
execResult.ProviderDispatch.Disposition == RuntimeDispatchDispositionRejected {
// The failed Turn and prompt are already durable. Resolve the claim to
// a terminal rejected state before the deferred cleanup can run.
if strings.TrimSpace(execResult.TurnID) != "" {
claimPending = false
if rejectErr := h.finalizeRejectedSubmitClaim(ref, firstNonEmpty(claim.ClientSubmitID, input.ClientSubmitID, legacyClientSubmitID(metadata)), execResult.TurnID); rejectErr != nil {
return SendInputResult{}, errors.Join(ErrSubmitDeliveryUnknown, err, rejectErr)
}
return SendInputResult{}, err
}
}
if input.Guidance && execResult.ProviderDispatch.Disposition == RuntimeDispatchDispositionNotDispatched {
// The runtime rejected the exact target before provider admission. Keep
// claimPending true so the deferred cleanup removes the prepared claim;
// this is a known rejection, not an outcome-unknown delivery.
return SendInputResult{}, err
}
if input.Guidance ||
execResult.ProviderDispatch.Disposition == RuntimeDispatchDispositionApplied ||
execResult.ProviderDispatch.Disposition == RuntimeDispatchDispositionOutcomeUnknown {
// Guidance targets an already-live turn and transport failure cannot
// prove rejection. A positive/unknown provider dispatch likewise
// preserves the claim as a recovery fence.
claimPending = false
return SendInputResult{}, errors.Join(ErrSubmitDeliveryUnknown, err)
}
return SendInputResult{}, err
}
turnID := strings.TrimSpace(execResult.TurnID)
if turnID == "" {
h.observeStep(ctx, "message_send", "runtime_exec", ref.WorkspaceID, ref.AgentSessionID, session.Provider, startedAt, ErrSubmitDeliveryUnknown)
return SendInputResult{}, ErrSubmitDeliveryUnknown
}
if expectedTurnID := strings.TrimSpace(input.TurnID); !input.Guidance && expectedTurnID != "" && turnID != expectedTurnID {
claimPending = false
return SendInputResult{}, ErrSubmitDeliveryUnknown
}
if reporter, ok := h.runtime.(RuntimeSubmitProvenanceReporter); ok {
if err := reporter.DurablyReportSubmitProvenance(ctx, RuntimeSubmitProvenanceInput{
WorkspaceID: ref.WorkspaceID, AgentSessionID: ref.AgentSessionID, TurnID: turnID,
ClientSubmitID: claim.ClientSubmitID, CanonicalSubmitOccurredAtUnixMS: claim.CreatedAtUnixMS,
Content: preparedContent.Hydrated, DisplayPrompt: displayPrompt, Guidance: input.Guidance,
}); err != nil {
claimPending = false
return SendInputResult{}, errors.Join(ErrSubmitDeliveryUnknown, err)
}
}
if !input.Guidance {
if err := h.recordTurnSubmission(
ctx, ref, turnID, input.ClientSubmitID, preparedContent.Persisted,
displayPrompt, input.CapabilityRefs, metadata, input.TuttiModeSnapshot,
); err != nil {
claimPending = false
return SendInputResult{}, errors.Join(ErrSubmitDeliveryUnknown, err)
}
}
if claim.ClientSubmitID != "" {
claimPending = false
if err := h.acceptSubmitClaim(ref, claim.ClientSubmitID, turnID); err != nil {
return SendInputResult{}, errors.Join(ErrSubmitDeliveryUnknown, err)
}
}
h.observeStep(ctx, "message_send", "runtime_exec", ref.WorkspaceID, ref.AgentSessionID, session.Provider, startedAt, nil)
canonicalSession, ok, err := h.store.GetSession(ctx, ref.WorkspaceID, ref.AgentSessionID)
if err != nil {
return SendInputResult{}, err
}
_ = ok
turn, ok, err := h.store.GetTurn(ctx, ref.WorkspaceID, ref.AgentSessionID, turnID)
if err != nil {
return SendInputResult{}, err
}
var turnPtr *storesqlite.Turn
if ok {
turnPtr = &turn
}
return SendInputResult{
Session: session, Canonical: canonicalSession, Turn: turnPtr, TurnID: turnID,
TurnLifecycle: execResult.TurnLifecycle, SubmitAvailability: execResult.SubmitAvailability,
}, nil
}
func (h *Host) UpdateTitle(ctx context.Context, input UpdateTitleInput) (UpdateTitleResult, error) {
input.WorkspaceID, input.AgentSessionID = strings.TrimSpace(input.WorkspaceID), strings.TrimSpace(input.AgentSessionID)
input.Title = strings.TrimSpace(input.Title)
if h == nil || h.store == nil || h.runtime == nil || input.WorkspaceID == "" || input.AgentSessionID == "" {
return UpdateTitleResult{}, ErrInvalidArgument
}
if utf8.RuneCountInString(input.Title) > MaxSessionTitleRunes {
return UpdateTitleResult{}, ErrSessionTitleTooLong
}
canonicalSession, updated, err := h.store.UpdateSessionTitle(ctx, input.WorkspaceID, input.AgentSessionID, input.Title)
if err != nil {
return UpdateTitleResult{}, err
}
if !updated {
return UpdateTitleResult{}, ErrSessionNotFound
}
result := UpdateTitleResult{Canonical: canonicalSession}
if _, ok := h.runtime.Session(input.WorkspaceID, input.AgentSessionID); !ok {
return result, nil
}
runtimeSession, err := h.runtime.SetTitle(ctx, RuntimeSetTitleInput{
WorkspaceID: input.WorkspaceID, AgentSessionID: input.AgentSessionID, Title: canonicalSession.Title,
})
if err != nil {
return UpdateTitleResult{}, err
}
result.Session = runtimeSession
return result, nil
}
func (h *Host) acquireSession(ctx context.Context, ref SessionRef) (func(), error) {
if h.locker == nil {
return func() {}, nil
}
return h.locker.Acquire(ctx, ref)
}
func (h *Host) acquireStartup(ctx context.Context, provider string) (func(), error) {
if h.startupGate == nil {
return func() {}, nil
}
return h.startupGate.Acquire(ctx, provider)
}