package model import "strings" const ( // PhaseChatting 表示当前请求只需正常聊天,不进入 plan / execute 主链路。 PhaseChatting Phase = "chatting" // PhaseInterrupted 表示本轮执行被"待用户交互"显式打断,当前连接应结束并等待恢复。 PhaseInterrupted Phase = "interrupted" ) const PendingInteractionSnapshotVersion = 1 const ( // PendingMetaAskUserSpeakStreamed 表示 ask_user 文本已在上游节点流式推送过。 // interrupt 节点据此决定是否跳过二次正文推送,避免前端出现重复气泡。 PendingMetaAskUserSpeakStreamed = "ask_user_speak_streamed" // PendingMetaAskUserHistoryAppended 表示 ask_user 文本已在上游写入过 history。 // interrupt 节点据此避免二次追加历史,防止上下文重复。 PendingMetaAskUserHistoryAppended = "ask_user_history_appended" ) // PendingInteractionType 表示当前挂起交互的类型。 type PendingInteractionType string const ( PendingInteractionTypeAskUser PendingInteractionType = "ask_user" PendingInteractionTypeConfirm PendingInteractionType = "confirm" PendingInteractionTypeConnectionLost PendingInteractionType = "connection_lost" ) // PendingInteractionStatus 表示挂起交互的生命周期状态。 type PendingInteractionStatus string const ( PendingInteractionStatusOpen PendingInteractionStatus = "open" PendingInteractionStatusResolved PendingInteractionStatus = "resolved" PendingInteractionStatusCanceled PendingInteractionStatus = "canceled" ) // PendingToolCallSnapshot 保存"待确认工具调用"的最小快照。 // // 职责边界: // 1. 负责保存真正落库 / 落缓存恢复执行所需的最小信息; // 2. ArgsJSON 约定存已经序列化好的参数快照,避免此处反向依赖具体 tool 参数结构; // 3. 不负责工具执行,不负责幂等校验,不负责回滚。 type PendingToolCallSnapshot struct { ToolName string `json:"tool_name"` ArgsJSON string `json:"args_json"` Summary string `json:"summary"` } // PendingInteraction 保存"本轮需要中断并等待用户后续动作"的交互快照。 // // 设计目的: // 1. ask_user 与 confirm 都不是业务 tool,而是流程级中断,所以单独建模; // 2. ResumeNode / ResumePhase / ResumeStep 用来记录恢复点,避免用户回答后整条链路从头乱跑; // 3. 该结构设计成可被 Redis + MySQL 直接存储的快照骨架,后续只需要补序列化与持久化接线。 // // TODO(agent/api): 后续由"用户追问回复接口 / 确认回调接口"读取这份快照并恢复运行。 type PendingInteraction struct { Version int `json:"version"` InteractionID string `json:"interaction_id"` Type PendingInteractionType `json:"type"` Status PendingInteractionStatus `json:"status"` DisplayText string `json:"display_text"` ResumeNode string `json:"resume_node"` ResumePhase Phase `json:"resume_phase"` ResumeStep int `json:"resume_step"` PendingTool *PendingToolCallSnapshot `json:"pending_tool,omitempty"` Metadata map[string]any `json:"metadata,omitempty"` } // AgentRuntimeState 是 graph 运行时真正流转的状态容器。 // // 职责边界: // 1. CommonState 继续只负责主流程控制; // 2. PendingInteraction 负责承载"需要中断后恢复"的交互快照; // 3. 这样既不污染 CommonState 的职责,又能让 graph 在一次入参里拿到完整运行态。 type AgentRuntimeState struct { *CommonState `json:"common_state"` // PendingInteraction 承载挂起交互的持久化快照。 PendingInteraction *PendingInteraction `json:"pending_interaction,omitempty"` // PendingConfirmTool 是 Execute → Confirm 之间传递待确认工具信息的临时邮箱。 // Execute 节点写入,Confirm 节点读出并清空,不参与持久化。 PendingConfirmTool *PendingToolCallSnapshot `json:"-"` } // NewAgentRuntimeState 创建 graph 运行态。 func NewAgentRuntimeState(state *CommonState) *AgentRuntimeState { rt := &AgentRuntimeState{CommonState: state} rt.EnsureCommonState() return rt } // EnsureCommonState 保证运行态里始终有一份可用的流程状态。 // // 步骤说明: // 1. 若 CommonState 为空,则补一份最小默认值,避免 graph / node 层空指针; // 2. 若 Phase 尚未设置,则默认回到 planning,保持当前主链路的保守起点; // 3. 若 MaxRounds 未设置,则回填默认值,避免编译后运行时无上限循环。 func (s *AgentRuntimeState) EnsureCommonState() *CommonState { if s == nil { return nil } if s.CommonState == nil { s.CommonState = &CommonState{} } if s.CommonState.Phase == "" { s.CommonState.Phase = PhasePlanning } if s.CommonState.MaxRounds <= 0 { s.CommonState.MaxRounds = DefaultMaxRounds } return s.CommonState } // HasPendingInteraction 判断当前是否存在待恢复交互。 func (s *AgentRuntimeState) HasPendingInteraction() bool { if s == nil || s.PendingInteraction == nil { return false } return s.PendingInteraction.Status == PendingInteractionStatusOpen } // PendingInteractionType 返回当前挂起交互类型。 func (s *AgentRuntimeState) PendingInteractionType() PendingInteractionType { if !s.HasPendingInteraction() { return "" } return s.PendingInteraction.Type } // OpenAskUserInteraction 打开一个"向用户追问"的中断快照。 func (s *AgentRuntimeState) OpenAskUserInteraction(interactionID, question, resumeNode string) { s.openPendingInteraction( PendingInteractionTypeAskUser, interactionID, question, resumeNode, nil, ) } // OpenConfirmInteraction 打开一个"写操作待确认"的中断快照。 func (s *AgentRuntimeState) OpenConfirmInteraction(interactionID, confirmText, resumeNode string, pendingTool *PendingToolCallSnapshot) { s.openPendingInteraction( PendingInteractionTypeConfirm, interactionID, confirmText, resumeNode, pendingTool, ) } // ResumeFromPending 从挂起交互恢复主流程。 // // 步骤说明: // 1. 仅当存在 open 状态的 pending interaction 时才执行恢复; // 2. 恢复时回写之前快照下来的 phase / step,确保继续跑的是原任务位置而不是新分支; // 3. 恢复成功后清空挂起快照,避免同一份 pending 被重复消费。 func (s *AgentRuntimeState) ResumeFromPending() bool { if !s.HasPendingInteraction() { return false } flowState := s.EnsureCommonState() pending := s.PendingInteraction flowState.Phase = pending.ResumePhase flowState.CurrentStep = pending.ResumeStep pending.Status = PendingInteractionStatusResolved s.PendingInteraction = nil return true } // ClearPendingInteraction 直接清空挂起交互。 // // 职责边界: // 1. 仅负责粗暴清空快照; // 2. 不自动恢复 phase / step,避免误把"取消交互"与"恢复执行"混为一谈; // 3. 若需要恢复流程,应优先使用 ResumeFromPending。 func (s *AgentRuntimeState) ClearPendingInteraction() { if s == nil || s.PendingInteraction == nil { return } s.PendingInteraction.Status = PendingInteractionStatusCanceled s.PendingInteraction = nil } // SetPendingInteractionMetadata 为当前 open 状态的 pending interaction 写入元信息。 // // 职责边界: // 1. 仅对当前挂起交互打运行态标记,不参与业务语义判断; // 2. 若当前没有 pending interaction,则静默跳过; // 3. metadata 仅用于节点间协作(如避免 ask_user 重复推送)。 func (s *AgentRuntimeState) SetPendingInteractionMetadata(key string, value any) { if s == nil || s.PendingInteraction == nil || s.PendingInteraction.Status != PendingInteractionStatusOpen { return } trimmedKey := strings.TrimSpace(key) if trimmedKey == "" { return } if s.PendingInteraction.Metadata == nil { s.PendingInteraction.Metadata = make(map[string]any) } s.PendingInteraction.Metadata[trimmedKey] = value } func (s *AgentRuntimeState) openPendingInteraction( interactionType PendingInteractionType, interactionID string, displayText string, resumeNode string, pendingTool *PendingToolCallSnapshot, ) { if s == nil { return } flowState := s.EnsureCommonState() resumePhase := flowState.Phase if resumePhase == "" { resumePhase = PhasePlanning } s.PendingInteraction = &PendingInteraction{ Version: PendingInteractionSnapshotVersion, InteractionID: strings.TrimSpace(interactionID), Type: interactionType, Status: PendingInteractionStatusOpen, DisplayText: strings.TrimSpace(displayText), ResumeNode: strings.TrimSpace(resumeNode), ResumePhase: resumePhase, ResumeStep: flowState.CurrentStep, PendingTool: clonePendingToolCallSnapshot(pendingTool), } // 1. 一旦进入 pending 状态,当前连接上的 graph 应立即停止向后执行。 // 2. 这里先统一把 Phase 置为 interrupted,后续恢复时再按快照写回原阶段。 // 3. 这样分支函数只需要判断 HasPendingInteraction(),无需猜测"当前 phase 是否仍可信"。 flowState.Phase = PhaseInterrupted } func clonePendingToolCallSnapshot(snapshot *PendingToolCallSnapshot) *PendingToolCallSnapshot { if snapshot == nil { return nil } copied := *snapshot return &copied }