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

555 lines
20 KiB
Go

package agenthost
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"unicode"
"github.com/google/uuid"
storesqlite "github.com/tutti-os/tutti/packages/agent/store-sqlite"
)
type TypedGoalControl struct {
Action string
Objective string
}
func normalizeTypedGoalControl(input TypedGoalControl) (TypedGoalControl, error) {
action := strings.ToLower(strings.TrimSpace(input.Action))
objective := strings.TrimSpace(input.Objective)
switch action {
case "set":
if objective == "" {
return TypedGoalControl{}, ErrInvalidArgument
}
return TypedGoalControl{Action: action, Objective: objective}, nil
case "pause", "resume", "clear":
if objective != "" {
return TypedGoalControl{}, ErrInvalidArgument
}
return TypedGoalControl{Action: action}, nil
default:
return TypedGoalControl{}, ErrInvalidArgument
}
}
func (h *Host) goalOperationOwner() string {
owner := strings.TrimSpace(h.goalOwner)
if owner == "" {
owner = strings.TrimSpace(h.owner)
}
if owner == "" {
owner = "goal-worker-local"
}
return owner
}
func staleGoalResultEvidence(evidence map[string]any, resultRevision, currentRevision int64) map[string]any {
result := clonePayload(evidence)
if result == nil {
result = map[string]any{}
}
result["staleResult"] = true
result["resultRevision"] = resultRevision
result["currentRevision"] = currentRevision
return result
}
func durableGoalForResponse(state storesqlite.SessionGoalState) map[string]any {
if state.Tombstoned {
return nil
}
return clonePayload(state.Desired)
}
func acceptedGoalControlResult(operationID string, state *storesqlite.SessionGoalState) GoalControlResult {
result := GoalControlResult{OperationID: strings.TrimSpace(operationID)}
if result.OperationID == "" || state == nil {
return result
}
result.Goal = durableGoalForResponse(*state)
result.GoalState = state
result.IntentAccepted = true
return result
}
func goalControlResultPending(result GoalControlResult) bool {
if !result.IntentAccepted || result.GoalState == nil ||
strings.TrimSpace(result.OperationID) == "" ||
strings.TrimSpace(result.GoalState.PendingOperationID) != strings.TrimSpace(result.OperationID) {
return false
}
switch strings.TrimSpace(result.GoalState.SyncStatus) {
case storesqlite.GoalSyncStatusPending, storesqlite.GoalSyncStatusApplying:
return true
default:
return false
}
}
func goalControlOperationID(workspaceID, agentSessionID, clientSubmitID string) string {
clientSubmitID = strings.TrimSpace(clientSubmitID)
if clientSubmitID == "" {
return uuid.NewString()
}
name := strings.Join([]string{
strings.TrimSpace(workspaceID), strings.TrimSpace(agentSessionID), clientSubmitID,
}, "\x00")
return uuid.NewSHA1(uuid.NameSpaceURL, []byte(name)).String()
}
func goalControlClientSubmitID(input GoalControlInput, submissionMetadata map[string]any) string {
if clientSubmitID := strings.TrimSpace(input.ClientSubmitID); clientSubmitID != "" {
return clientSubmitID
}
return metadataString(submissionMetadata, "clientSubmitId")
}
// existingGoalControlResult resolves a durable replay before callers perform
// any provider-side setup. CreateSession uses it to keep a response-loss retry
// from starting a second provider Session after the Host process restarts.
func (h *Host) existingGoalControlResult(
ctx context.Context,
input GoalControlInput,
) (GoalControlResult, bool, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
agentSessionID := strings.TrimSpace(input.AgentSessionID)
clientSubmitID := goalControlClientSubmitID(input, input.SubmissionMetadata)
action := strings.TrimSpace(input.Action)
objective := strings.TrimSpace(input.Objective)
if h == nil || h.goals == nil || workspaceID == "" || agentSessionID == "" || clientSubmitID == "" {
return GoalControlResult{}, false, nil
}
operationID := goalControlOperationID(workspaceID, agentSessionID, clientSubmitID)
operation, found, err := h.goals.GetGoalControlOperation(ctx, workspaceID, operationID)
if err != nil || !found {
return GoalControlResult{}, false, err
}
if operation.AgentSessionID != agentSessionID ||
operation.ClientSubmitID != clientSubmitID ||
operation.Action != action ||
operation.Objective != objective {
return GoalControlResult{}, true, storesqlite.ErrGoalOperationConflict
}
state, stateFound, stateErr := h.goals.GetSessionGoalState(ctx, workspaceID, agentSessionID)
if stateErr != nil {
return GoalControlResult{}, true, stateErr
}
if !stateFound {
return GoalControlResult{}, true, storesqlite.ErrGoalStateAbsent
}
accepted := acceptedGoalControlResult(operation.OperationID, &state)
switch operation.Status {
case storesqlite.GoalOperationStatusCompleted, storesqlite.GoalOperationStatusSuperseded:
canonical, sessionFound, sessionErr := h.store.GetSession(ctx, workspaceID, agentSessionID)
if sessionErr != nil {
return accepted, true, sessionErr
}
if !sessionFound {
return accepted, true, ErrSessionNotFound
}
accepted.Canonical = canonical
return accepted, true, nil
case storesqlite.GoalOperationStatusFailed:
return accepted, true, fmt.Errorf(
"%w: %s",
ErrRuntimeOperationFailed,
strings.TrimSpace(operation.LastError),
)
default:
return accepted, true, ErrRuntimeOperationInProgress
}
}
// ParseTypedGoalControl recognizes the text-only slash surface at the Host
// command boundary. It intentionally runs before submit-claim allocation so typed and
// dedicated controls share one durable saga and no Turn contract is opened.
func ParseTypedGoalControl(content []PromptContentBlock, guidance bool) (TypedGoalControl, bool) {
if guidance || len(content) != 1 || strings.TrimSpace(content[0].Type) != "text" {
return TypedGoalControl{}, false
}
// Content is the semantic command carrier. DisplayPrompt is presentation
// only and must not be able to turn ordinary content into control, or hide
// a real control command from the durable saga.
prompt := strings.TrimSpace(content[0].Text)
separator := strings.IndexFunc(prompt, unicode.IsSpace)
if separator < 0 {
return TypedGoalControl{}, false
}
command, args := prompt[:separator], strings.TrimSpace(prompt[separator:])
if !strings.EqualFold(strings.TrimSpace(command), "/goal") {
return TypedGoalControl{}, false
}
args = strings.TrimSpace(args)
if args == "" {
return TypedGoalControl{}, false
}
switch strings.ToLower(args) {
case "clear", "reset":
return TypedGoalControl{Action: "clear"}, true
case "pause":
return TypedGoalControl{Action: "pause"}, true
case "resume", "active":
return TypedGoalControl{Action: "resume"}, true
default:
return TypedGoalControl{Action: "set", Objective: args}, true
}
}
func typedGoalDisplayPrompt(goal TypedGoalControl) string {
if goal.Action == "set" {
return firstNonEmpty("/goal "+strings.TrimSpace(goal.Objective), "")
}
return firstNonEmpty("/goal "+strings.TrimSpace(goal.Action), "")
}
func initialGoalRuntimeTitle(
explicitTitle string,
displayPrompt string,
goal TypedGoalControl,
isTypedGoal bool,
) (string, bool) {
title := strings.TrimSpace(explicitTitle)
if isTypedGoal && NormalizeTitle(title) == "" {
title = DeriveInitialTitle(
"",
firstNonEmpty(strings.TrimSpace(displayPrompt), typedGoalDisplayPrompt(goal)),
)
}
return title, NormalizeTitle(title) != ""
}
// GoalControl performs a direct goal action (pause/resume/clear/set) on the
// session's thread. Like Cancel it is a control operation: it never opens a
// turn, so it works while a turn is running.
func (h *Host) GoalControl(ctx context.Context, input GoalControlInput) (GoalControlResult, error) {
return h.goalControl(ctx, input)
}
func (h *Host) goalControl(
ctx context.Context,
input GoalControlInput,
) (GoalControlResult, error) {
if h == nil {
return GoalControlResult{}, ErrInvalidArgument
}
workspaceID := strings.TrimSpace(input.WorkspaceID)
agentSessionID := strings.TrimSpace(input.AgentSessionID)
if workspaceID == "" || agentSessionID == "" {
return GoalControlResult{}, ErrInvalidArgument
}
var result GoalControlResult
err := h.withSessionMutationActor(ctx, workspaceID, agentSessionID, func(actorCtx context.Context) error {
var commandErr error
result, commandErr = h.goalControlSerialized(actorCtx, input)
return commandErr
})
return result, err
}
// goalControlSerialized keeps the provider mutation and its durable revision
// transition in one per-session command lane. A clear submitted immediately
// after set must reach the provider after set; revision repair alone cannot
// prevent the older provider call from temporarily resurrecting a goal.
func (h *Host) goalControlSerialized(
ctx context.Context,
input GoalControlInput,
) (GoalControlResult, error) {
workspaceID := strings.TrimSpace(input.WorkspaceID)
agentSessionID := strings.TrimSpace(input.AgentSessionID)
action := strings.TrimSpace(input.Action)
objective := strings.TrimSpace(input.Objective)
submissionMetadata := clonePayload(input.SubmissionMetadata)
if h == nil || h.store == nil || h.runtime == nil || h.goalRuntime == nil || workspaceID == "" || agentSessionID == "" || action == "" {
return GoalControlResult{}, ErrInvalidArgument
}
slog.Info("workspace agent session goal control requested",
"event", "workspace_agent_session.goal_control.requested",
"workspaceId", workspaceID,
"agentSessionId", agentSessionID,
"action", action,
)
operationID := ""
goalRevision := int64(0)
clientSubmitID := goalControlClientSubmitID(input, submissionMetadata)
var persistedState *storesqlite.SessionGoalState
replayed := false
if h.goals != nil {
operationID = goalControlOperationID(workspaceID, agentSessionID, clientSubmitID)
err := h.withGoalActor(ctx, workspaceID, agentSessionID, func(actorCtx context.Context) error {
now := h.goalOperationNow()
op, state, created, err := h.goals.PrepareGoalControlOperation(actorCtx, storesqlite.GoalControlOperationPrepare{
OperationID: operationID, WorkspaceID: workspaceID, AgentSessionID: agentSessionID,
Action: strings.TrimSpace(action), Objective: strings.TrimSpace(objective), ClientSubmitID: clientSubmitID,
ExpectedRevision: input.ExpectedRevision,
OccurredAtUnixMS: now.UnixMilli(),
})
if err != nil {
return err
}
goalRevision = op.GoalRevision
persistedState = &state
if !created {
switch op.Status {
case storesqlite.GoalOperationStatusCompleted, storesqlite.GoalOperationStatusSuperseded:
replayed = true
return nil
case storesqlite.GoalOperationStatusFailed:
return fmt.Errorf("%w: %s", ErrRuntimeOperationFailed, strings.TrimSpace(op.LastError))
default:
return ErrRuntimeOperationInProgress
}
}
return nil
})
if err != nil {
return acceptedGoalControlResult(operationID, persistedState), err
}
}
if replayed {
canonical, found, err := h.store.GetSession(ctx, workspaceID, agentSessionID)
if err != nil {
return GoalControlResult{}, err
}
if !found {
return GoalControlResult{}, ErrSessionNotFound
}
return GoalControlResult{
Canonical: canonical, Goal: durableGoalForResponse(*persistedState),
OperationID: operationID, GoalState: persistedState,
IntentAccepted: true,
}, nil
}
if _, err := h.EnsureRuntimeSession(ctx, SessionRef{WorkspaceID: workspaceID, AgentSessionID: agentSessionID}); err != nil {
slog.Warn("workspace agent session goal control prepare failed",
"event", "workspace_agent_session.goal_control.prepare_failed",
"workspaceId", workspaceID,
"agentSessionId", agentSessionID,
"error", err.Error(),
)
h.observeTerminalFailure(ctx, TerminalFailure{
Flow: "goal_control",
FailureStage: "prepare",
WorkspaceID: workspaceID,
AgentSessionID: agentSessionID,
OperationID: operationID,
ClientSubmitID: clientSubmitID,
ErrorCode: terminalFailureCode(err),
ErrorMessage: err.Error(),
Retryable: isRetryableRuntimeOperationError(err),
})
return acceptedGoalControlResult(operationID, persistedState), err
}
if h.goals != nil {
err := h.withGoalActor(ctx, workspaceID, agentSessionID, func(actorCtx context.Context) error {
now := h.goalOperationNow()
owner := h.goalOperationOwner()
if _, claimed, err := h.goals.ClaimGoalControlOperation(actorCtx, storesqlite.ClaimGoalControlOperationInput{
WorkspaceID: workspaceID, OperationID: operationID, LeaseOwner: owner,
NowUnixMS: now.UnixMilli(), LeaseExpiresAtMS: now.Add(goalOperationLeaseDuration).UnixMilli(),
}); err != nil || !claimed {
if err != nil {
return err
}
return ErrRuntimeOperationInProgress
}
current, found, err := h.goals.GetSessionGoalState(actorCtx, workspaceID, agentSessionID)
if err != nil || !found || current.Revision != goalRevision || current.PendingOperationID != operationID {
if err != nil {
return err
}
return ErrRuntimeOperationInProgress
}
_, _, err = h.goals.MarkGoalControlOperationDispatched(actorCtx, workspaceID, operationID, h.goalOperationNow().UnixMilli())
return err
})
if err != nil {
return acceptedGoalControlResult(operationID, persistedState), err
}
}
controlResult, err := h.goalRuntime.GoalControl(ctx, RuntimeGoalControlInput{
WorkspaceID: workspaceID,
AgentSessionID: agentSessionID,
Action: action,
Objective: objective,
OperationID: operationID,
GoalRevision: goalRevision,
RepairEpoch: 0,
SubmissionMetadata: goalControlSubmissionMetadata(clientSubmitID),
})
if err != nil {
normalizedErr := err
if h.goals != nil && operationID != "" {
persistCtx, cancel := goalPersistenceContext()
persistErr := h.withGoalActor(persistCtx, workspaceID, agentSessionID, func(actorCtx context.Context) error {
now := h.goalOperationNow()
current, found, currentErr := h.goals.GetSessionGoalState(actorCtx, workspaceID, agentSessionID)
if currentErr != nil {
return currentErr
}
if found && current.Revision > goalRevision {
_, repairErr := h.ensureStaleGoalRepair(actorCtx, current, operationID, goalRevision,
staleGoalResultEvidence(map[string]any{"error": normalizedErr.Error(), "ambiguous": true}, goalRevision, current.Revision),
storesqlite.GoalProviderPhaseUnknown)
return repairErr
}
fail := !isRetryableRuntimeOperationError(normalizedErr)
_, _, releaseErr := h.goals.ReleaseGoalControlOperation(actorCtx, storesqlite.ReleaseGoalControlOperationInput{
WorkspaceID: workspaceID, OperationID: operationID, LeaseOwner: h.goalOperationOwner(),
ProviderPhase: storesqlite.GoalProviderPhaseDispatched, LastError: normalizedErr.Error(),
NowUnixMS: now.UnixMilli(), NextAttemptAtMS: runtimeOperationNextAttemptAt(now, 1, fail), Fail: fail,
})
return releaseErr
})
if latest, found, stateErr := h.goals.GetSessionGoalState(persistCtx, workspaceID, agentSessionID); stateErr == nil && found {
persistedState = &latest
}
cancel()
if persistErr != nil {
return acceptedGoalControlResult(operationID, persistedState), errors.Join(normalizedErr, persistErr)
}
}
slog.Warn("workspace agent session goal control runtime request failed",
"event", "workspace_agent_session.goal_control.runtime_failed",
"workspaceId", workspaceID,
"agentSessionId", agentSessionID,
"action", action,
"error", normalizedErr.Error(),
)
return acceptedGoalControlResult(operationID, persistedState), normalizedErr
}
responseGoal := clonePayload(controlResult.Goal)
if h.goals != nil && operationID != "" {
persistCtx, cancel := goalPersistenceContext()
persistErr := h.withGoalActor(persistCtx, workspaceID, agentSessionID, func(actorCtx context.Context) error {
current, found, err := h.goals.GetSessionGoalState(actorCtx, workspaceID, agentSessionID)
if err != nil {
return err
}
if !found {
return errors.New("durable goal state disappeared after provider result")
}
if current.Revision > goalRevision {
latest, err := h.ensureStaleGoalRepair(actorCtx, current, operationID, goalRevision,
staleGoalResultEvidence(controlResult.Evidence, goalRevision, current.Revision), controlResult.ProviderPhase)
if err != nil {
return err
}
persistedState = &latest
responseGoal = durableGoalForResponse(latest)
return nil
}
if current.Revision < goalRevision {
return errors.New("durable goal revision regressed behind provider result")
}
if controlResult.ProviderPhase == "accepted" {
_, state, _, err := h.goals.AcknowledgeGoalControlOperation(actorCtx, storesqlite.GoalControlOperationAcknowledge{
WorkspaceID: workspaceID, OperationID: operationID,
Evidence: clonePayload(controlResult.Evidence), OccurredAtUnixMS: h.goalOperationNow().UnixMilli(),
ExecutionPending: controlResult.ExecutionPending,
})
persistedState = &state
return err
}
_, state, _, err := h.goals.CompleteGoalControlOperation(actorCtx, storesqlite.GoalControlOperationComplete{
WorkspaceID: workspaceID, OperationID: operationID, Succeeded: true,
Observed: clonePayload(controlResult.Goal), Evidence: clonePayload(controlResult.Evidence),
OccurredAtUnixMS: h.goalOperationNow().UnixMilli(),
ExecutionPending: controlResult.ExecutionPending,
})
persistedState = &state
return err
})
cancel()
if persistErr != nil {
return acceptedGoalControlResult(operationID, persistedState), persistErr
}
}
if persistedState != nil {
// GoalControlResult.Goal is the Host-owned durable projection used by
// every consumer. Provider output remains available independently as
// GoalState.Observed and may legitimately be empty while a pause or
// resume is applied. Only a durable tombstone projects an explicit nil.
responseGoal = durableGoalForResponse(*persistedState)
}
canonical, found, err := h.store.GetSession(ctx, workspaceID, agentSessionID)
if err != nil {
slog.Warn("workspace agent session goal control refresh failed",
"event", "workspace_agent_session.goal_control.refresh_failed",
"workspaceId", workspaceID,
"agentSessionId", agentSessionID,
"error", err.Error(),
)
h.observeTerminalFailure(ctx, TerminalFailure{
Flow: "goal_control",
FailureStage: "refresh",
WorkspaceID: workspaceID,
AgentSessionID: agentSessionID,
OperationID: operationID,
ClientSubmitID: clientSubmitID,
ErrorCode: terminalFailureCode(err),
ErrorMessage: err.Error(),
Retryable: isRetryableRuntimeOperationError(err),
})
return acceptedGoalControlResult(operationID, persistedState), err
}
if !found {
return acceptedGoalControlResult(operationID, persistedState), ErrSessionNotFound
}
slog.Info("workspace agent session goal control completed",
"event", "workspace_agent_session.goal_control.completed",
"workspaceId", workspaceID,
"agentSessionId", agentSessionID,
"action", action,
)
return GoalControlResult{
Canonical: canonical, Goal: responseGoal, OperationID: operationID,
GoalState: persistedState, IntentAccepted: operationID != "" && persistedState != nil,
}, nil
}
func goalControlSubmissionMetadata(clientSubmitID string) map[string]any {
clientSubmitID = strings.TrimSpace(clientSubmitID)
if clientSubmitID == "" {
return nil
}
return map[string]any{"clientSubmitId": clientSubmitID}
}
func (h *Host) ensureStaleGoalRepair(ctx context.Context, current storesqlite.SessionGoalState,
sourceOperationID string, sourceRevision int64, evidence map[string]any, providerPhase string,
) (storesqlite.SessionGoalState, error) {
now := h.goalOperationNow().UnixMilli()
if _, _, err := h.goals.RecordGoalControlOperationEvidence(ctx, storesqlite.GoalControlOperationEvidence{
WorkspaceID: current.WorkspaceID, OperationID: sourceOperationID, ProviderPhase: providerPhase,
Evidence: evidence, OccurredAtUnixMS: now,
}); err != nil {
return storesqlite.SessionGoalState{}, err
}
for attempt := 0; attempt < 4; attempt++ {
_, attached, _, err := h.goals.EnsureOrWakeGoalRepairOperation(ctx, storesqlite.EnsureGoalRepairOperationInput{
WorkspaceID: current.WorkspaceID, AgentSessionID: current.AgentSessionID,
SourceOperationID: sourceOperationID, SourceRevision: sourceRevision,
CurrentRevision: current.Revision, OccurredAtUnixMS: now,
})
if err == nil {
return attached, nil
}
if !errors.Is(err, storesqlite.ErrGoalReconcileConflict) {
return storesqlite.SessionGoalState{}, err
}
latest, found, readErr := h.goals.GetSessionGoalState(ctx, current.WorkspaceID, current.AgentSessionID)
if readErr != nil || !found {
return storesqlite.SessionGoalState{}, readErr
}
if latest.Revision <= sourceRevision {
return latest, nil
}
current = latest
}
return storesqlite.SessionGoalState{}, storesqlite.ErrGoalReconcileConflict
}