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

429 lines
17 KiB
Go

package agenthost
import (
"context"
"log/slog"
"strings"
storesqlite "github.com/tutti-os/tutti/packages/agent/store-sqlite"
canonical "github.com/tutti-os/tutti/packages/agent/store-sqlite/canonical"
)
type ActivityStateCommitted struct {
Input canonical.ReportSessionStateInput
Reply canonical.ReportSessionStateReply
Result storesqlite.ActivityStateReportResult
}
type SessionMessagesCommitted struct {
Input canonical.ReportSessionMessagesInput
Reply canonical.ReportSessionMessagesReply
Result storesqlite.MessageReportResult
// Provider is the canonical provider identity for the reporting session.
// Message rows do not repeat session identity, so terminal tool-call
// analytics carry it on the committed wrapper.
Provider string
// IsChildSession marks a provider-native subagent session. Message reports
// carry no session state, so the reporting adapter resolves it from the
// canonical session before publishing the delta.
IsChildSession bool
}
type RootTurnSettled struct {
WorkspaceID string
AgentSessionID string
Turn storesqlite.Turn
Provider string
IsChildSession bool
// StartupReconciled marks a turn force-settled by daemon-start
// reconciliation rather than by a live provider settlement. Failure
// analytics still count it; runtime fan-out that reacts to a real
// completion must skip it.
StartupReconciled bool
}
type RuntimeOperationCommitStage string
const (
RuntimeOperationPrepared RuntimeOperationCommitStage = "prepared"
RuntimeOperationCheckpoint RuntimeOperationCommitStage = "checkpointed"
RuntimeOperationCompleted RuntimeOperationCommitStage = "completed"
RuntimeOperationReleased RuntimeOperationCommitStage = "released"
RuntimeOperationFailed RuntimeOperationCommitStage = "failed"
)
type RuntimeOperationCommitted struct {
Stage RuntimeOperationCommitStage
Operation storesqlite.RuntimeOperation
Event *storesqlite.RuntimeOperationEvent
Provider string
IsChildSession bool
}
type GoalOperationCommitStage string
const (
GoalOperationPrepared GoalOperationCommitStage = "prepared"
GoalOperationDispatched GoalOperationCommitStage = "dispatched"
GoalOperationAcknowledged GoalOperationCommitStage = "acknowledged"
GoalOperationCompleted GoalOperationCommitStage = "completed"
GoalOperationReleased GoalOperationCommitStage = "released"
GoalOperationFailed GoalOperationCommitStage = "failed"
GoalOperationEvidence GoalOperationCommitStage = "evidence"
GoalOperationReconciled GoalOperationCommitStage = "reconciled"
GoalOperationRepairPrepared GoalOperationCommitStage = "repair_prepared"
GoalOperationTerminal GoalOperationCommitStage = "terminal"
)
type GoalOperationCommitted struct {
Stage GoalOperationCommitStage
Operation storesqlite.GoalControlOperation
State storesqlite.SessionGoalState
Audit *storesqlite.Message
Provider string
IsChildSession bool
}
type CanonicalProjectionDirty struct {
WorkspaceID string
AgentSessionID string
RootAgentSessionID string
RootTurnID string
MutationID string
EntityKind string
EntityID string
Operation string
Version int64
}
type CanonicalViewInvalidated struct {
WorkspaceID string
AgentSessionID string
}
// CommittedDelta describes facts that have already committed. ProjectionDirty
// is a wake hint for a durable outbox marker written by a transaction
// participant; it is never the durable marker itself.
type CommittedDelta struct {
TransactionID string
ActivityState *ActivityStateCommitted
SessionMessages *SessionMessagesCommitted
RootTurnsSettled []RootTurnSettled
RuntimeOperation *RuntimeOperationCommitted
GoalOperation *GoalOperationCommitted
ProjectionDirty []CanonicalProjectionDirty
ViewsInvalidated []CanonicalViewInvalidated
}
func ActivityStateDelta(input canonical.ReportSessionStateInput, reply canonical.ReportSessionStateReply, result storesqlite.ActivityStateReportResult) CommittedDelta {
delta := committedDeltaFromMutations(result.TransactionID, result.CommitDelta.Mutations)
delta.ActivityState = &ActivityStateCommitted{Input: input, Reply: reply, Result: result}
if result.RootTurnAccepted && result.RootTurn.Phase == storesqlite.TurnPhaseSettled {
delta.RootTurnsSettled = append(delta.RootTurnsSettled, RootTurnSettled{
WorkspaceID: result.RootTurn.WorkspaceID, AgentSessionID: result.RootTurn.AgentSessionID, Turn: result.RootTurn,
Provider: firstNonEmptyTrimmed(result.State.Session.Provider, input.State.Provider, input.Source.Provider),
IsChildSession: rootTurnOwnerIsChild(input, result),
})
}
if goalOp := goalOperationFromActivityState(input, result); goalOp != nil {
delta.GoalOperation = goalOp
}
delta.addView(input.WorkspaceID, input.AgentSessionID)
if result.RootTurnAccepted {
delta.addView(input.WorkspaceID, result.RootTurn.AgentSessionID)
}
return delta
}
func rootTurnOwnerIsChild(
input canonical.ReportSessionStateInput,
result storesqlite.ActivityStateReportResult,
) bool {
ownerSessionID := strings.TrimSpace(result.RootTurn.AgentSessionID)
reportingSessionID := strings.TrimSpace(result.State.Session.ID)
if reportingSessionID == "" {
reportingSessionID = strings.TrimSpace(input.AgentSessionID)
}
if ownerSessionID == "" || ownerSessionID != reportingSessionID {
// A child terminal report may settle its canonical root Turn. The
// persisted Session in this result still belongs to the reporting child,
// so its kind must not be projected onto the root owner.
return false
}
if strings.TrimSpace(result.State.Session.ID) != "" {
return canonicalSessionIsChild(result.State.Session)
}
return sessionStateIsChild(input.State)
}
func sessionStateIsChild(state canonical.WorkspaceAgentSessionStateUpdate) bool {
kind := strings.TrimSpace(state.Kind)
if strings.EqualFold(kind, storesqlite.SessionKindChild) {
return true
}
return strings.TrimSpace(state.ParentToolCallID) != ""
}
// goalOperationFromActivityState promotes bottom-up Goal observed updates
// (reconcileObservedGoalFromSessionTx) into the typed GoalOperation seam so
// session-replay can mint goal.completed / goal.running checkpoints.
func goalOperationFromActivityState(
input canonical.ReportSessionStateInput,
result storesqlite.ActivityStateReportResult,
) *GoalOperationCommitted {
sessionID := strings.TrimSpace(input.AgentSessionID)
workspaceID := strings.TrimSpace(input.WorkspaceID)
hasGoalMutation := false
for _, mutation := range result.CommitDelta.Mutations {
if mutation.EntityKind != storesqlite.MutationEntityGoalState {
continue
}
hasGoalMutation = true
if id := strings.TrimSpace(mutation.AgentSessionID); id != "" {
sessionID = id
}
if id := strings.TrimSpace(mutation.WorkspaceID); id != "" {
workspaceID = id
}
}
if !hasGoalMutation || sessionID == "" || workspaceID == "" {
return nil
}
observed := clonePayload(runtimeContextGoal(input.State.RuntimeContext))
if len(observed) == 0 {
return nil
}
return &GoalOperationCommitted{
Stage: GoalOperationReconciled,
Provider: firstNonEmptyTrimmed(result.State.Session.Provider, input.State.Provider, input.Source.Provider),
IsChildSession: sessionStateIsChild(input.State),
State: storesqlite.SessionGoalState{
WorkspaceID: workspaceID,
AgentSessionID: sessionID,
Observed: observed,
CommitTransactionID: strings.TrimSpace(result.TransactionID),
},
}
}
func runtimeContextGoal(runtimeContext map[string]any) map[string]any {
if len(runtimeContext) == 0 {
return nil
}
raw, ok := runtimeContext["goal"].(map[string]any)
if !ok || len(raw) == 0 {
return nil
}
return raw
}
func SessionMessagesDelta(input canonical.ReportSessionMessagesInput, reply canonical.ReportSessionMessagesReply, result storesqlite.MessageReportResult) CommittedDelta {
delta := committedDeltaFromMutations(result.TransactionID, result.CommitDelta.Mutations)
delta.SessionMessages = &SessionMessagesCommitted{
Input: input, Reply: reply, Result: result,
Provider: strings.TrimSpace(input.Source.Provider),
}
delta.addView(input.WorkspaceID, canonicalMessageSessionID(input.AgentSessionID, result.Messages))
return delta
}
// StaleTurnSettlementDelta projects daemon-start reconciliation. Every listed
// turn was force-settled as interrupted, so it enters RootTurnsSettled with
// that outcome and the StartupReconciled marker.
func StaleTurnSettlementDelta(settlements []storesqlite.StaleTurnSettlement) CommittedDelta {
delta := CommittedDelta{}
if len(settlements) > 0 {
delta = committedDeltaFromMutations(settlements[0].TransactionID, settlements[0].CommitDelta.Mutations)
}
for _, settlement := range settlements {
delta.addView(settlement.WorkspaceID, settlement.AgentSessionID)
turn := settlement.Turn
turnID := firstNonEmptyTrimmed(turn.TurnID, settlement.TurnID)
if turnID == "" {
continue
}
workspaceID := firstNonEmptyTrimmed(turn.WorkspaceID, settlement.WorkspaceID)
agentSessionID := firstNonEmptyTrimmed(turn.AgentSessionID, settlement.AgentSessionID)
if strings.TrimSpace(turn.TurnID) == "" {
// Compatibility for callers that construct the legacy scalar-only
// settlement value. The SQLite startup path always supplies Turn.
turn = storesqlite.Turn{
WorkspaceID: workspaceID,
AgentSessionID: agentSessionID,
TurnID: turnID,
Phase: storesqlite.TurnPhaseSettled,
Outcome: storesqlite.TurnOutcomeInterrupted,
ErrorMessage: "stale turn settled on daemon startup",
StartedAtUnixMS: settlement.StartedAtUnixMS,
SettledAtUnixMS: settlement.SettledAtUnixMS,
}
}
delta.RootTurnsSettled = append(delta.RootTurnsSettled, RootTurnSettled{
WorkspaceID: workspaceID,
AgentSessionID: agentSessionID,
Turn: turn,
Provider: strings.TrimSpace(settlement.Provider),
IsChildSession: settlement.IsChildSession,
StartupReconciled: true,
})
}
return delta
}
// CanonicalDelta exposes a post-commit canonical mutation without inventing a
// command-specific Host event. Adapters use its projection-dirty identities
// and view invalidations as wake hints only.
func CanonicalDelta(commit storesqlite.TransactionDelta) CommittedDelta {
return committedDeltaFromMutations(commit.TransactionID, commit.Mutations)
}
func runtimeOperationDelta(stage RuntimeOperationCommitStage, operation storesqlite.RuntimeOperation, event *storesqlite.RuntimeOperationEvent) CommittedDelta {
delta := committedDeltaFromMutations(operation.CommitTransactionID, operation.CommitDelta.Mutations)
delta.RuntimeOperation = &RuntimeOperationCommitted{Stage: stage, Operation: operation, Event: event}
return delta
}
func goalOperationDelta(stage GoalOperationCommitStage, operation storesqlite.GoalControlOperation, state storesqlite.SessionGoalState, audit *storesqlite.Message) CommittedDelta {
transactionID := operation.CommitTransactionID
mutations := operation.CommitDelta.Mutations
if transactionID == "" {
transactionID = state.CommitTransactionID
mutations = state.CommitDelta.Mutations
}
delta := committedDeltaFromMutations(transactionID, mutations)
delta.GoalOperation = &GoalOperationCommitted{Stage: stage, Operation: operation, State: state, Audit: audit}
delta.addView(operation.WorkspaceID, operation.AgentSessionID)
if operation.AgentSessionID == "" {
delta.addView(state.WorkspaceID, state.AgentSessionID)
}
return delta
}
func committedDeltaFromMutations(transactionID string, mutations []storesqlite.TransactionMutation) CommittedDelta {
delta := CommittedDelta{TransactionID: strings.TrimSpace(transactionID)}
for _, mutation := range mutations {
delta.ProjectionDirty = append(delta.ProjectionDirty, CanonicalProjectionDirty{
WorkspaceID: mutation.WorkspaceID, AgentSessionID: mutation.AgentSessionID,
RootAgentSessionID: mutation.RootAgentSessionID, RootTurnID: mutation.RootTurnID,
MutationID: mutation.MutationID, EntityKind: mutation.EntityKind, EntityID: mutation.EntityID,
Operation: mutation.Operation, Version: mutation.Version,
})
delta.addView(mutation.WorkspaceID, mutation.AgentSessionID)
}
return delta
}
func (delta *CommittedDelta) addView(workspaceID, agentSessionID string) {
workspaceID, agentSessionID = strings.TrimSpace(workspaceID), strings.TrimSpace(agentSessionID)
if workspaceID == "" || agentSessionID == "" {
return
}
for _, existing := range delta.ViewsInvalidated {
if existing.WorkspaceID == workspaceID && existing.AgentSessionID == agentSessionID {
return
}
}
delta.ViewsInvalidated = append(delta.ViewsInvalidated, CanonicalViewInvalidated{WorkspaceID: workspaceID, AgentSessionID: agentSessionID})
}
func canonicalMessageSessionID(fallback string, messages []storesqlite.Message) string {
for _, message := range messages {
if value := strings.TrimSpace(message.AgentSessionID); value != "" {
return value
}
}
return strings.TrimSpace(fallback)
}
// NotifyCommitted deliberately swallows observer failures after logging: the
// canonical transaction is already committed, and reliable delivery must come
// from the durable participant outbox rather than rollback or command failure.
func NotifyCommitted(ctx context.Context, observer CommitObserver, delta CommittedDelta) {
if observer == nil {
return
}
if err := observer.ObserveCommitted(ctx, delta); err != nil {
slog.Warn("agent host commit observer failed",
"event", "agent_host.commit_observer.failed",
"transaction_id", delta.TransactionID,
"error", err,
)
}
}
func (h *Host) notifyCommitted(ctx context.Context, delta CommittedDelta) {
if h != nil {
h.enrichCommittedDeltaTerminalIdentity(ctx, &delta)
ObserveTerminalFailuresFromDelta(ctx, h.terminalFailure, delta)
NotifyCommitted(ctx, h.commitObserver, delta)
}
}
type terminalFailureSessionIdentity struct {
provider string
isChildSession bool
}
// enrichCommittedDeltaTerminalIdentity resolves session-owned facts from the
// canonical store after commit. Runtime and Goal operation rows deliberately
// do not duplicate provider identity, while product telemetry still needs the
// exact canonical Provider and child-session classification.
func (h *Host) enrichCommittedDeltaTerminalIdentity(ctx context.Context, delta *CommittedDelta) {
if h == nil || h.store == nil || delta == nil {
return
}
identities := map[string]terminalFailureSessionIdentity{}
resolve := func(workspaceID, agentSessionID string) terminalFailureSessionIdentity {
workspaceID = strings.TrimSpace(workspaceID)
agentSessionID = strings.TrimSpace(agentSessionID)
key := workspaceID + "\x00" + agentSessionID
if identity, ok := identities[key]; ok {
return identity
}
identity := terminalFailureSessionIdentity{}
if workspaceID != "" && agentSessionID != "" {
if session, found, err := h.store.GetSession(ctx, workspaceID, agentSessionID); err == nil && found {
identity.provider = strings.TrimSpace(session.Provider)
identity.isChildSession = canonicalSessionIsChild(session)
}
}
identities[key] = identity
return identity
}
apply := func(provider *string, isChildSession *bool, workspaceID, agentSessionID string) {
identity := resolve(workspaceID, agentSessionID)
if strings.TrimSpace(*provider) == "" {
*provider = identity.provider
}
*isChildSession = *isChildSession || identity.isChildSession
}
if committed := delta.RuntimeOperation; committed != nil && committed.Stage == RuntimeOperationFailed {
apply(&committed.Provider, &committed.IsChildSession,
committed.Operation.WorkspaceID, committed.Operation.AgentSessionID)
}
if committed := delta.GoalOperation; committed != nil &&
(committed.Stage == GoalOperationFailed || committed.Stage == GoalOperationTerminal) {
workspaceID := firstNonEmptyTrimmed(committed.Operation.WorkspaceID, committed.State.WorkspaceID)
agentSessionID := firstNonEmptyTrimmed(committed.Operation.AgentSessionID, committed.State.AgentSessionID)
apply(&committed.Provider, &committed.IsChildSession, workspaceID, agentSessionID)
}
for index := range delta.RootTurnsSettled {
settled := &delta.RootTurnsSettled[index]
if outcome := strings.TrimSpace(settled.Turn.Outcome); outcome != storesqlite.TurnOutcomeFailed &&
outcome != storesqlite.TurnOutcomeInterrupted && outcome != storesqlite.TurnOutcomeCanceled {
continue
}
apply(&settled.Provider, &settled.IsChildSession, settled.WorkspaceID, settled.AgentSessionID)
}
if committed := delta.SessionMessages; committed != nil &&
len(terminalFailuresFromSessionMessages(*committed, nil)) > 0 {
agentSessionID := canonicalMessageSessionID(committed.Input.AgentSessionID, committed.Result.Messages)
apply(&committed.Provider, &committed.IsChildSession, committed.Input.WorkspaceID, agentSessionID)
}
}
func canonicalSessionIsChild(session storesqlite.Session) bool {
return strings.EqualFold(strings.TrimSpace(session.Kind), storesqlite.SessionKindChild) ||
strings.TrimSpace(session.ParentToolCallID) != ""
}