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

180 lines
5.4 KiB
Go

package agenthost
import (
"context"
"errors"
"fmt"
"strings"
storesqlite "github.com/tutti-os/tutti/packages/agent/store-sqlite"
)
var (
ErrHistoricalStateUnavailable = errors.New("historical Agent state is unavailable")
ErrHistoricalStateConflict = storesqlite.ErrHistoricalStateConflict
)
type HistoricalSessionGraph = storesqlite.HistoricalSessionGraph
type HistoricalSessionGraphRestoreInput = storesqlite.HistoricalSessionGraphRestoreInput
type HistoricalSession = storesqlite.HistoricalSession
type HistoricalTurn = storesqlite.HistoricalTurn
type HistoricalMessage = storesqlite.HistoricalMessage
type HistoricalInteraction = storesqlite.HistoricalInteraction
type HistoricalGoal = storesqlite.HistoricalGoal
func (h *Host) CaptureHistoricalSessionGraph(
ctx context.Context,
ref SessionRef,
) (HistoricalSessionGraph, error) {
if h == nil || h.historicalState == nil {
return HistoricalSessionGraph{}, ErrHistoricalStateUnavailable
}
if strings.TrimSpace(ref.WorkspaceID) == "" ||
strings.TrimSpace(ref.AgentSessionID) == "" {
return HistoricalSessionGraph{}, ErrInvalidArgument
}
graph, err := h.historicalState.CaptureHistoricalSessionGraph(
ctx,
strings.TrimSpace(ref.WorkspaceID),
strings.TrimSpace(ref.AgentSessionID),
)
if err != nil {
return HistoricalSessionGraph{}, err
}
if err := ValidateHistoricalSessionGraph(graph); err != nil {
return HistoricalSessionGraph{}, err
}
return graph, nil
}
// RestoreHistoricalSessionGraph imports settled canonical history without
// starting or resuming a Provider. Callers invoke it for a fresh isolated
// Replay Workspace before Host recovery.
func (h *Host) RestoreHistoricalSessionGraph(
ctx context.Context,
input HistoricalSessionGraphRestoreInput,
) error {
if h == nil || h.historicalState == nil {
return ErrHistoricalStateUnavailable
}
input.WorkspaceID = strings.TrimSpace(input.WorkspaceID)
input.UserID = strings.TrimSpace(input.UserID)
if input.WorkspaceID == "" || input.UserID == "" {
return ErrInvalidArgument
}
if err := ValidateHistoricalSessionGraph(input.Graph); err != nil {
return err
}
return h.historicalState.RestoreHistoricalSessionGraph(ctx, input)
}
func ValidateHistoricalSessionGraph(graph HistoricalSessionGraph) error {
rootID := strings.TrimSpace(graph.RootSessionID)
if rootID == "" || len(graph.Sessions) == 0 {
return ErrInvalidArgument
}
sessions := make(map[string]HistoricalSession, len(graph.Sessions))
for _, session := range graph.Sessions {
sessionID := strings.TrimSpace(session.ID)
if sessionID == "" ||
(session.Kind != "root" && session.Kind != "child") ||
strings.TrimSpace(session.AgentTargetID) == "" ||
strings.TrimSpace(session.Provider) == "" ||
strings.TrimSpace(session.ProviderSessionID) == "" {
return ErrInvalidArgument
}
if session.ActiveTurnID != "" ||
(session.Goal != nil && session.Goal.PendingOperationID != "") {
return fmt.Errorf(
"%w: Session %q history is not settled",
ErrHistoricalStateConflict,
sessionID,
)
}
if _, exists := sessions[sessionID]; exists {
return fmt.Errorf("%w: duplicate Session %q", ErrHistoricalStateConflict, sessionID)
}
turns := make(map[string]struct{}, len(session.Turns))
turnAnchors := make(map[string]string, len(session.Turns))
for _, turn := range session.Turns {
turnID := strings.TrimSpace(turn.ID)
if turnID == "" {
return ErrInvalidArgument
}
if _, exists := turns[turnID]; exists {
return fmt.Errorf("%w: duplicate Turn %q", ErrHistoricalStateConflict, turnID)
}
turns[turnID] = struct{}{}
turnAnchors[turnID] = strings.TrimSpace(turn.IdentityAnchorTurnID)
}
for _, turn := range session.Turns {
anchorTurnID := strings.TrimSpace(turn.IdentityAnchorTurnID)
if anchorTurnID == "" {
continue
}
if anchorTurnID == strings.TrimSpace(turn.ID) {
return fmt.Errorf("%w: Turn %q anchors itself", ErrHistoricalStateConflict, turn.ID)
}
if _, exists := turns[anchorTurnID]; !exists {
return fmt.Errorf(
"%w: Turn %q references missing identity anchor %q",
ErrHistoricalStateConflict,
turn.ID,
anchorTurnID,
)
}
if turnAnchors[anchorTurnID] != "" {
return fmt.Errorf(
"%w: Turn %q identity anchor %q is not ultimate",
ErrHistoricalStateConflict,
turn.ID,
anchorTurnID,
)
}
}
for _, message := range session.Messages {
if strings.TrimSpace(message.ID) == "" {
return ErrInvalidArgument
}
if message.TurnID != "" {
if _, exists := turns[message.TurnID]; !exists {
return fmt.Errorf(
"%w: Message %q references missing Turn %q",
ErrHistoricalStateConflict,
message.ID,
message.TurnID,
)
}
}
}
sessions[sessionID] = session
}
root, exists := sessions[rootID]
if !exists || (root.Kind != "" && root.Kind != "root") {
return fmt.Errorf("%w: root Session %q is missing", ErrHistoricalStateConflict, rootID)
}
for sessionID, session := range sessions {
if sessionID == rootID {
continue
}
if session.RootSessionID != rootID ||
session.ParentSessionID == "" {
return fmt.Errorf(
"%w: child Session %q is outside root %q",
ErrHistoricalStateConflict,
sessionID,
rootID,
)
}
if _, exists := sessions[session.ParentSessionID]; !exists {
return fmt.Errorf(
"%w: child Session %q references missing parent %q",
ErrHistoricalStateConflict,
sessionID,
session.ParentSessionID,
)
}
}
return nil
}