Version: 0.8.4.dev.260329

后端:
1.新建newAgent文件夹,是的你没听错,刚刚搬迁完的旧结构又准备推翻了:因为通用性太差,用户需求复杂一点就招架不了。最新的架构已经在路上,这应该是这个项目的正确路线了,目前正在搭骨架。

前端:
无改动

全仓库:
无改动
This commit is contained in:
Losita
2026-03-29 22:12:23 +08:00
parent 468367d617
commit 6d22acb270
17 changed files with 2474 additions and 51 deletions

View File

@@ -0,0 +1,147 @@
package model
// Phase 表示 agent 循环图当前所处的阶段。
type Phase string
const (
PhasePlanning Phase = "planning"
PhaseWaitingConfirm Phase = "waiting_confirm"
PhaseExecuting Phase = "executing"
PhaseDone Phase = "done"
)
const DefaultMaxRounds = 30
type CommonState struct {
// 身份
TraceID string
UserID int
ConversationID string
// 流程阶段
Phase Phase
// Plan
PlanSteps []string
CurrentStep int
// 安全边界
MaxRounds int
RoundUsed int
}
func NewCommonState(traceID string, userID int, conversationID string) *CommonState {
return &CommonState{
TraceID: traceID,
UserID: userID,
ConversationID: conversationID,
Phase: PhasePlanning,
MaxRounds: DefaultMaxRounds,
}
}
// NextRound 消耗一轮并返回是否还有余量。
func (s *CommonState) NextRound() bool {
s.RoundUsed++
return s.RoundUsed <= s.MaxRounds
}
// Exhausted 判断是否已耗尽轮次。
func (s *CommonState) Exhausted() bool {
return s.RoundUsed >= s.MaxRounds
}
// FinishPlan 标记 plan 完成,进入等待确认阶段。
func (s *CommonState) FinishPlan(steps []string) {
s.PlanSteps = steps
s.CurrentStep = 0
s.Phase = PhaseWaitingConfirm
}
// ConfirmPlan 用户确认后进入执行阶段。
func (s *CommonState) ConfirmPlan() {
s.Phase = PhaseExecuting
}
// RejectPlan 用户拒绝,回到规划阶段。
func (s *CommonState) RejectPlan() {
s.PlanSteps = nil
s.CurrentStep = 0
s.Phase = PhasePlanning
}
// AdvanceStep 推进到下一个 plan 步骤,返回是否还有剩余步骤。
func (s *CommonState) AdvanceStep() bool {
s.CurrentStep++
return s.CurrentStep < len(s.PlanSteps)
}
// Done 标记整个流程结束。
func (s *CommonState) Done() {
s.Phase = PhaseDone
}
// HasPlan 判断当前 state 是否已经持有一份可执行的 plan。
//
// 职责边界:
// 1. 负责把“外部直接判断 len(PlanSteps) > 0”的零散逻辑收口到 state 内部;
// 2. 只回答“是否存在 plan”不判断当前索引是否有效
// 3. 当 state 为空时返回 false调用方可据此决定是否回退到重新规划。
func (s *CommonState) HasPlan() bool {
if s == nil {
return false
}
return len(s.PlanSteps) > 0
}
// CurrentPlanStep 返回当前正在执行的 plan 步骤文本。
//
// 职责边界:
// 1. 负责根据 CurrentStep 安全读取 PlanSteps避免调用方重复写切片越界判断
// 2. 当 state 为空、plan 为空、或当前索引越界时,统一返回 ("", false)
// 3. 不负责推进步骤,也不负责修正 CurrentStep 的取值。
func (s *CommonState) CurrentPlanStep() (string, bool) {
if s == nil {
return "", false
}
if s.CurrentStep < 0 || s.CurrentStep >= len(s.PlanSteps) {
return "", false
}
return s.PlanSteps[s.CurrentStep], true
}
// HasCurrentPlanStep 判断“当前步骤”是否存在且可安全读取。
//
// 职责边界:
// 1. 负责给 graph / node 层提供一个更直白的布尔判断入口;
// 2. 内部复用 CurrentPlanStep避免两处维护相同的索引边界逻辑
// 3. 不返回步骤内容,只回答“当前是否还有可注入的步骤”。
func (s *CommonState) HasCurrentPlanStep() bool {
_, ok := s.CurrentPlanStep()
return ok
}
// PlanProgress 返回当前 plan 的执行进度。
//
// 输出语义:
// 1. current 使用对人类更友好的 1-based 序号,适合直接写入 prompt 或日志;
// 2. total 表示当前 plan 总步数;
// 3. 若尚未生成 plan则返回 (0, 0)
// 4. 若 CurrentStep 已越过末尾,则 current 会被收敛到 total避免上层出现 total+1 这类噪音值。
func (s *CommonState) PlanProgress() (current int, total int) {
if s == nil {
return 0, 0
}
total = len(s.PlanSteps)
if total == 0 {
return 0, 0
}
if s.CurrentStep < 0 {
return 0, total
}
if s.CurrentStep >= total {
return total, total
}
return s.CurrentStep + 1, total
}

View File

@@ -0,0 +1,215 @@
package model
import (
"strings"
"github.com/cloudwego/eino/schema"
)
// ConversationContext 承载“本轮要喂给模型的输入材料”。
//
// 职责边界:
// 1. 负责保存 system prompt、对话历史、置顶注入块、工具 schema 摘要;
// 2. 负责提供最小必要的安全访问方法,避免 node / prompt 层直接散落切片操作;
// 3. 不负责流程推进phase / round / current step 仍归 CommonState 管;
// 4. 不负责真正的 prompt 组装,消息如何拼接仍应放在 prompt 层处理。
//
// TODO(newagent/prompt): 后续由 plan / execute 的 prompt builder 读取这里的数据,组装真正发给 LLM 的 messages。
// TODO(newagent/node): 后续 planNode / executeNode 只通过这里的访问方法读写上下文,避免多处直接改切片。
type ConversationContext struct {
SystemPrompt string
History []*schema.Message
PinnedBlocks []ContextBlock
ToolSchemas []ToolSchemaContext
}
// ContextBlock 表示一段可被“置顶注入”的自然语言上下文。
//
// 设计目的:
// 1. Key 用于让调用方按语义覆盖,例如 current_plan / current_step / execution_rule
// 2. Title 用于 prompt 层后续决定是否渲染成小标题;
// 3. Content 存真正的自然语言内容保持你当前“plan 用自然语言表达”的思路。
type ContextBlock struct {
Key string
Title string
Content string
}
// ToolSchemaContext 是工具描述的轻量快照。
//
// 职责边界:
// 1. 这里只保留 prompt 注入真正需要的摘要信息;
// 2. SchemaText 约定存“已经整理好的自然语言 / JSON schema 摘要”;
// 3. 不直接耦合具体 tool registry 里的复杂结构,避免 model 层反向依赖工具实现。
type ToolSchemaContext struct {
Name string
Desc string
SchemaText string
}
// NewConversationContext 创建最小上下文容器。
func NewConversationContext(systemPrompt string) *ConversationContext {
return &ConversationContext{
SystemPrompt: strings.TrimSpace(systemPrompt),
}
}
// SetSystemPrompt 更新系统提示词。
func (c *ConversationContext) SetSystemPrompt(systemPrompt string) {
if c == nil {
return
}
c.SystemPrompt = strings.TrimSpace(systemPrompt)
}
// ReplaceHistory 整体替换对话历史。
//
// 职责边界:
// 1. 负责把“会话快照恢复”这类场景需要的一次性覆盖入口收口到这里;
// 2. 只复制消息切片本身,避免调用方后续 append 污染同一底层数组;
// 3. 不深拷贝每个 message 指针,消息对象本身仍默认由上游按只读方式使用。
func (c *ConversationContext) ReplaceHistory(history []*schema.Message) {
if c == nil {
return
}
c.History = cloneMessageSlice(history)
}
// AppendHistory 追加对话历史。
//
// 处理策略:
// 1. 跳过 nil message避免后续 prompt 拼装时出现空指针;
// 2. 仅负责顺序追加,不做去重,不做裁剪;
// 3. 历史裁剪策略属于后续 prompt / memory 层能力,此处先不下沉。
func (c *ConversationContext) AppendHistory(messages ...*schema.Message) {
if c == nil || len(messages) == 0 {
return
}
for _, msg := range messages {
if msg == nil {
continue
}
c.History = append(c.History, msg)
}
}
// HistorySnapshot 返回历史消息的浅拷贝切片。
func (c *ConversationContext) HistorySnapshot() []*schema.Message {
if c == nil {
return nil
}
return cloneMessageSlice(c.History)
}
// UpsertPinnedBlock 按 Key 写入或覆盖一段置顶上下文。
//
// 步骤说明:
// 1. Key 为空时直接忽略,因为后续无法做稳定覆盖;
// 2. 若已存在同 Key block则原位覆盖保证“当前 plan / 当前步骤”这类上下文始终只有一份;
// 3. 若不存在,则追加到末尾,至于渲染顺序由 prompt 层统一决定;
// 4. 此处不自动裁剪旧内容,避免 model 层擅自丢信息。
func (c *ConversationContext) UpsertPinnedBlock(block ContextBlock) {
if c == nil {
return
}
key := strings.TrimSpace(block.Key)
if key == "" {
return
}
block.Key = key
block.Title = strings.TrimSpace(block.Title)
block.Content = strings.TrimSpace(block.Content)
for i := range c.PinnedBlocks {
if c.PinnedBlocks[i].Key == key {
c.PinnedBlocks[i] = block
return
}
}
c.PinnedBlocks = append(c.PinnedBlocks, block)
}
// RemovePinnedBlock 删除指定 Key 的置顶上下文。
func (c *ConversationContext) RemovePinnedBlock(key string) bool {
if c == nil {
return false
}
key = strings.TrimSpace(key)
if key == "" {
return false
}
for i := range c.PinnedBlocks {
if c.PinnedBlocks[i].Key != key {
continue
}
c.PinnedBlocks = append(c.PinnedBlocks[:i], c.PinnedBlocks[i+1:]...)
return true
}
return false
}
// PinnedBlockByKey 按 Key 读取指定的置顶上下文。
func (c *ConversationContext) PinnedBlockByKey(key string) (ContextBlock, bool) {
if c == nil {
return ContextBlock{}, false
}
key = strings.TrimSpace(key)
if key == "" {
return ContextBlock{}, false
}
for i := range c.PinnedBlocks {
if c.PinnedBlocks[i].Key == key {
return c.PinnedBlocks[i], true
}
}
return ContextBlock{}, false
}
// PinnedBlocksSnapshot 返回置顶上下文块的浅拷贝切片。
func (c *ConversationContext) PinnedBlocksSnapshot() []ContextBlock {
if c == nil {
return nil
}
result := make([]ContextBlock, len(c.PinnedBlocks))
copy(result, c.PinnedBlocks)
return result
}
// SetToolSchemas 整体替换工具 schema 摘要。
func (c *ConversationContext) SetToolSchemas(schemas []ToolSchemaContext) {
if c == nil {
return
}
c.ToolSchemas = cloneToolSchemaSlice(schemas)
}
// ToolSchemasSnapshot 返回工具 schema 摘要的浅拷贝切片。
func (c *ConversationContext) ToolSchemasSnapshot() []ToolSchemaContext {
if c == nil {
return nil
}
return cloneToolSchemaSlice(c.ToolSchemas)
}
func cloneMessageSlice(messages []*schema.Message) []*schema.Message {
if len(messages) == 0 {
return nil
}
result := make([]*schema.Message, len(messages))
copy(result, messages)
return result
}
func cloneToolSchemaSlice(schemas []ToolSchemaContext) []ToolSchemaContext {
if len(schemas) == 0 {
return nil
}
result := make([]ToolSchemaContext, len(schemas))
copy(result, schemas)
return result
}

View File

@@ -0,0 +1,205 @@
package model
import (
"fmt"
"strings"
)
// ExecuteAction 表示 execute 阶段单轮决策的动作类型。
//
// 设计原则:
// 1. LLM 只负责“申报本轮想做什么”,不直接推进状态;
// 2. 后端只围绕这些有限动作做流程校验、证据校验、安全校验;
// 3. 动作枚举保持收敛,避免 execute 节点后续再次长成“自由文本协议”。
type ExecuteAction string
const (
// ExecuteActionContinue 表示当前步骤尚未完成,需要继续本步骤的 ReAct 循环。
ExecuteActionContinue ExecuteAction = "continue"
// ExecuteActionAskUser 表示当前步骤缺少外部信息,需要中断并追问用户。
ExecuteActionAskUser ExecuteAction = "ask_user"
// ExecuteActionConfirm 表示当前步骤准备执行写操作,但必须先进入确认闸门。
ExecuteActionConfirm ExecuteAction = "confirm"
// ExecuteActionNextPlan 表示当前步骤已完成,可以推进到下一个 plan 步骤。
ExecuteActionNextPlan ExecuteAction = "next_plan"
// ExecuteActionDone 表示整个任务已完成,可以进入最终交付。
ExecuteActionDone ExecuteAction = "done"
)
// ExecuteDecision 是 execute prompt 单轮产出的统一决策结构。
//
// 职责边界:
// 1. Speak 是这轮先对用户说的话,适合在真正调工具前流式吐给前端;
// 2. Action 是模型申报的“下一步动作类型”;
// 3. Reason 是给后端和日志看的简短解释,不直接等价于完成证明;
// 4. ToolCall 只是“意图”,不代表工具已经真正执行成功。
type ExecuteDecision struct {
Speak string `json:"speak,omitempty"`
Action ExecuteAction `json:"action"`
Reason string `json:"reason,omitempty"`
ToolCall *ToolCallIntent `json:"tool_call,omitempty"`
}
// Normalize 统一清洗 execute 决策中的字符串字段。
func (d *ExecuteDecision) Normalize() {
if d == nil {
return
}
d.Speak = strings.TrimSpace(d.Speak)
d.Action = ExecuteAction(strings.TrimSpace(string(d.Action)))
d.Reason = strings.TrimSpace(d.Reason)
if d.ToolCall != nil {
d.ToolCall.Normalize()
}
}
// Validate 校验 execute 决策的最小合法性。
//
// 校验原则:
// 1. 这里只校验“协议是否自洽”,不校验工具是否真实存在,也不校验当前步骤是否真的完成;
// 2. 只允许少量动作与 tool_call 共存,避免后续 node 层收到含糊决策;
// 3. 真正的三类最小校验应放在执行层,这里只做第一道轻量门禁。
func (d *ExecuteDecision) Validate() error {
if d == nil {
return fmt.Errorf("execute decision 不能为空")
}
d.Normalize()
if d.Action == "" {
return fmt.Errorf("execute decision.action 不能为空")
}
switch d.Action {
case ExecuteActionContinue:
if d.ToolCall != nil {
return d.ToolCall.Validate()
}
return nil
case ExecuteActionAskUser:
if d.ToolCall != nil {
return fmt.Errorf("ask_user 动作不应携带 tool_call")
}
return nil
case ExecuteActionConfirm:
if d.ToolCall == nil {
return fmt.Errorf("confirm 动作必须携带待确认的 tool_call")
}
return d.ToolCall.Validate()
case ExecuteActionNextPlan, ExecuteActionDone:
if d.ToolCall != nil {
return fmt.Errorf("%s 动作不应携带 tool_call", d.Action)
}
return nil
default:
return fmt.Errorf("未知 execute action: %s", d.Action)
}
}
// ToolCallIntent 表示 execute 阶段申报的工具调用意图。
//
// 设计目的:
// 1. 这里只描述“模型想调用什么工具、传什么参数”,不代表调用已经发生;
// 2. Arguments 暂时保留 map 结构,方便 prompt 输出原生 JSON 对象;
// 3. 是否需要 confirm 不应由模型决定,后续应由工具注册表或后端策略判定。
type ToolCallIntent struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
}
// Normalize 清洗工具调用意图中的稳定字段。
func (t *ToolCallIntent) Normalize() {
if t == nil {
return
}
t.Name = strings.TrimSpace(t.Name)
}
// Validate 校验工具调用意图的最小合法性。
func (t *ToolCallIntent) Validate() error {
if t == nil {
return fmt.Errorf("tool_call 不能为空")
}
t.Normalize()
if t.Name == "" {
return fmt.Errorf("tool_call.name 不能为空")
}
return nil
}
// ExecuteEvidenceSource 表示“当前步骤完成证明”来自哪里。
type ExecuteEvidenceSource string
const (
// ExecuteEvidenceSourceToolObservation 表示来自读工具或分析工具的真实 observation。
ExecuteEvidenceSourceToolObservation ExecuteEvidenceSource = "tool_observation"
// ExecuteEvidenceSourceWriteReceipt 表示来自写工具成功执行后的回执。
ExecuteEvidenceSourceWriteReceipt ExecuteEvidenceSource = "write_receipt"
// ExecuteEvidenceSourceUserReply 表示来自用户补充回答的外部事实。
ExecuteEvidenceSourceUserReply ExecuteEvidenceSource = "user_reply"
)
// ExecuteEvidenceReceipt 表示“一条可被后端认可的最小事实证据”。
//
// 职责边界:
// 1. StepIndex 用来绑定这条证据属于哪个 plan 步骤,避免旧 observation 污染新步骤;
// 2. Source / Name / Success 描述“这条证据是怎么来的、是否真的发生了”;
// 3. Summary 只用于日志、调试和交付串联,不替代原始 observation 本身;
// 4. 这里不做语义推理,只负责记录事实。
type ExecuteEvidenceReceipt struct {
StepIndex int `json:"step_index"`
Source ExecuteEvidenceSource `json:"source"`
Name string `json:"name,omitempty"`
ArgumentsDigest string `json:"arguments_digest,omitempty"`
Success bool `json:"success"`
Summary string `json:"summary,omitempty"`
}
// Normalize 清洗证据回执中的稳定字段。
func (r *ExecuteEvidenceReceipt) Normalize() {
if r == nil {
return
}
r.Source = ExecuteEvidenceSource(strings.TrimSpace(string(r.Source)))
r.Name = strings.TrimSpace(r.Name)
r.ArgumentsDigest = strings.TrimSpace(r.ArgumentsDigest)
r.Summary = strings.TrimSpace(r.Summary)
}
// Validate 校验证据回执是否具备最小可用信息。
func (r *ExecuteEvidenceReceipt) Validate() error {
if r == nil {
return fmt.Errorf("evidence receipt 不能为空")
}
r.Normalize()
if r.StepIndex < 0 {
return fmt.Errorf("evidence receipt.step_index 不能小于 0")
}
switch r.Source {
case ExecuteEvidenceSourceToolObservation, ExecuteEvidenceSourceWriteReceipt, ExecuteEvidenceSourceUserReply:
default:
return fmt.Errorf("未知 evidence source: %s", r.Source)
}
return nil
}
// ExecuteValidationResult 保存 execute 单轮的三类最小校验结果。
//
// 三类校验语义:
// 1. FlowPassed当前动作在流程上是否合法例如 done 是否允许直接发生;
// 2. EvidencePassed当前动作是否有最小事实证据支撑
// 3. SafetyPassed当前动作是否触发了安全兜底例如超轮次、重复空转、待确认未完成。
type ExecuteValidationResult struct {
FlowPassed bool `json:"flow_passed"`
FlowReason string `json:"flow_reason,omitempty"`
EvidencePassed bool `json:"evidence_passed"`
EvidenceReason string `json:"evidence_reason,omitempty"`
SafetyPassed bool `json:"safety_passed"`
SafetyReason string `json:"safety_reason,omitempty"`
}

View File

@@ -0,0 +1,220 @@
package model
import "strings"
const (
// PhaseChatting 表示当前请求只需正常聊天,不进入 plan / execute 主链路。
PhaseChatting Phase = "chatting"
// PhaseInterrupted 表示本轮执行被“待用户交互”显式打断,当前连接应结束并等待恢复。
PhaseInterrupted Phase = "interrupted"
)
const PendingInteractionSnapshotVersion = 1
// 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
ArgsJSON string
Summary string
}
// PendingInteraction 保存“本轮需要中断并等待用户后续动作”的交互快照。
//
// 设计目的:
// 1. ask_user 与 confirm 都不是业务 tool而是流程级中断所以单独建模
// 2. ResumeNode / ResumePhase / ResumeStep 用来记录恢复点,避免用户回答后整条链路从头乱跑;
// 3. 该结构设计成可被 Redis + MySQL 直接存储的快照骨架,后续只需要补序列化与持久化接线。
//
// TODO(newagent/store): 后续把该结构整体快照到 Redis + MySQL形成双保险恢复点。
// TODO(newagent/api): 后续由“用户追问回复接口 / 确认回调接口”读取这份快照并恢复运行。
type PendingInteraction struct {
Version int
InteractionID string
Type PendingInteractionType
Status PendingInteractionStatus
DisplayText string
ResumeNode string
ResumePhase Phase
ResumeStep int
PendingTool *PendingToolCallSnapshot
Metadata map[string]any
}
// AgentRuntimeState 是 graph 运行时真正流转的状态容器。
//
// 职责边界:
// 1. CommonState 继续只负责主流程控制;
// 2. PendingInteraction 负责承载“需要中断后恢复”的交互快照;
// 3. 这样既不污染 CommonState 的职责,又能让 graph 在一次入参里拿到完整运行态。
type AgentRuntimeState struct {
*CommonState
PendingInteraction *PendingInteraction
}
// 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
}
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
}