Version: 0.8.8.dev.260403

后端:
1.新建Deliver节点:LLM生成任务总结,失败降级到机械格式化,伪流式输出
2.新建Confirm节点:确认卡片推送与状态持久化
3.新建Interrupt节点:追问/确认/默认中断三种处理路径
4.实现状态持久化体系:model层定义AgentStateStore接口+AgentStateSnapshot快照,dao/cache.go新增Redis CRUD,agent_nodes层每节点自动存快照、Deliver完成后清理
5.所有model struct补充JSON tags,支持Redis序列化/反序列化
前端:无
仓库:无
This commit is contained in:
LoveLosita
2026-04-03 20:36:31 +08:00
parent 64b946816f
commit 17e3615f74
14 changed files with 1600 additions and 112 deletions

View File

@@ -15,27 +15,27 @@ const DefaultMaxRounds = 30
// CommonState 承载可持久化的主流程状态。
//
// 职责边界:
// 1. 负责记录当前处于哪个阶段、当前计划是什么、执行到了第几步、已经消耗了多少轮
// 1. 负责记录"当前处于哪个阶段、当前计划是什么、执行到了第几步、已经消耗了多少轮"
// 2. 负责提供最小必要的安全访问方法,避免 graph/node/prompt 层到处手写切片越界判断;
// 3. 不负责承载对话历史、tool schema、pinned context 这类模型输入材料,它们仍然属于 ConversationContext。
type CommonState struct {
// 身份信息
TraceID string
UserID int
ConversationID string
TraceID string `json:"trace_id"`
UserID int `json:"user_id"`
ConversationID string `json:"conversation_id"`
// 流程阶段
Phase Phase
Phase Phase `json:"phase"`
// 计划状态
// 1. 这里直接使用结构化的 PlanStep避免 planning -> execute 之间丢失 done_when。
// 2. CurrentStep 表示当前 plan 步骤下标,不是 execute 内部 ReAct 的思考轮次。
PlanSteps []PlanStep
CurrentStep int
// 2. CurrentStep 表示"当前 plan 步骤下标",不是 execute 内部 ReAct 的思考轮次。
PlanSteps []PlanStep `json:"plan_steps"`
CurrentStep int `json:"current_step"`
// 安全边界
MaxRounds int
RoundUsed int
MaxRounds int `json:"max_rounds"`
RoundUsed int `json:"round_used"`
}
func NewCommonState(traceID string, userID int, conversationID string) *CommonState {
@@ -97,7 +97,7 @@ func (s *CommonState) Done() {
// HasPlan 判断当前 state 是否已经持有一份完整计划。
//
// 职责边界:
// 1. 负责收口是否存在 plan这一层判断,避免外层到处写 len(PlanSteps) > 0
// 1. 负责收口"是否存在 plan"这一层判断,避免外层到处写 len(PlanSteps) > 0
// 2. 不判断 CurrentStep 当前是否有效,当前步骤是否合法由 HasCurrentPlanStep 回答;
// 3. state 为空时统一返回 false调用方可据此决定是否回退到 planning。
func (s *CommonState) HasPlan() bool {
@@ -123,7 +123,7 @@ func (s *CommonState) CurrentPlanStep() (PlanStep, bool) {
return s.PlanSteps[s.CurrentStep], true
}
// HasCurrentPlanStep 判断当前步骤是否存在且可安全读取。
// HasCurrentPlanStep 判断"当前步骤"是否存在且可安全读取。
func (s *CommonState) HasCurrentPlanStep() bool {
_, ok := s.CurrentPlanStep()
return ok

View File

@@ -6,45 +6,42 @@ import (
"github.com/cloudwego/eino/schema"
)
// ConversationContext 承载本轮要喂给模型的输入材料
// 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
SystemPrompt string `json:"system_prompt"`
History []*schema.Message `json:"history"`
PinnedBlocks []ContextBlock `json:"pinned_blocks"`
ToolSchemas []ToolSchemaContext `json:"-"` // 每次请求由 Service 层重新注入,不持久化
}
// ContextBlock 表示一段可被置顶注入的自然语言上下文。
// ContextBlock 表示一段可被"置顶注入"的自然语言上下文。
//
// 设计目的:
// 1. Key 用于让调用方按语义覆盖,例如 current_plan / current_step / execution_rule
// 2. Title 用于 prompt 层后续决定是否渲染成小标题;
// 3. Content 存真正的自然语言内容,保持你当前plan 用自然语言表达的思路。
// 3. Content 存真正的自然语言内容,保持你当前"plan 用自然语言表达"的思路。
type ContextBlock struct {
Key string
Title string
Content string
Key string `json:"key"`
Title string `json:"title"`
Content string `json:"content"`
}
// ToolSchemaContext 是工具描述的轻量快照。
//
// 职责边界:
// 1. 这里只保留 prompt 注入真正需要的摘要信息;
// 2. SchemaText 约定存已经整理好的自然语言 / JSON schema 摘要
// 2. SchemaText 约定存"已经整理好的自然语言 / JSON schema 摘要"
// 3. 不直接耦合具体 tool registry 里的复杂结构,避免 model 层反向依赖工具实现。
type ToolSchemaContext struct {
Name string
Desc string
SchemaText string
Name string `json:"name"`
Desc string `json:"desc"`
SchemaText string `json:"schema_text"`
}
// NewConversationContext 创建最小上下文容器。
@@ -65,7 +62,7 @@ func (c *ConversationContext) SetSystemPrompt(systemPrompt string) {
// ReplaceHistory 整体替换对话历史。
//
// 职责边界:
// 1. 负责把会话快照恢复这类场景需要的一次性覆盖入口收口到这里;
// 1. 负责把"会话快照恢复"这类场景需要的一次性覆盖入口收口到这里;
// 2. 只复制消息切片本身,避免调用方后续 append 污染同一底层数组;
// 3. 不深拷贝每个 message 指针,消息对象本身仍默认由上游按只读方式使用。
func (c *ConversationContext) ReplaceHistory(history []*schema.Message) {
@@ -105,7 +102,7 @@ func (c *ConversationContext) HistorySnapshot() []*schema.Message {
//
// 步骤说明:
// 1. Key 为空时直接忽略,因为后续无法做稳定覆盖;
// 2. 若已存在同 Key block则原位覆盖保证当前 plan / 当前步骤这类上下文始终只有一份;
// 2. 若已存在同 Key block则原位覆盖保证"当前 plan / 当前步骤"这类上下文始终只有一份;
// 3. 若不存在,则追加到末尾,至于渲染顺序由 prompt 层统一决定;
// 4. 此处不自动裁剪旧内容,避免 model 层擅自丢信息。
func (c *ConversationContext) UpsertPinnedBlock(block ContextBlock) {

View File

@@ -39,6 +39,7 @@ type AgentGraphDeps struct {
ExecuteClient *newagentllm.Client
DeliverClient *newagentllm.Client
ChunkEmitter *newagentstream.ChunkEmitter
StateStore AgentStateStore
}
// EnsureChunkEmitter 保证 graph 运行时始终有一个可用的 chunk 发射器。

View File

@@ -6,7 +6,7 @@ const (
// PhaseChatting 表示当前请求只需正常聊天,不进入 plan / execute 主链路。
PhaseChatting Phase = "chatting"
// PhaseInterrupted 表示本轮执行被待用户交互显式打断,当前连接应结束并等待恢复。
// PhaseInterrupted 表示本轮执行被"待用户交互"显式打断,当前连接应结束并等待恢复。
PhaseInterrupted Phase = "interrupted"
)
@@ -30,49 +30,52 @@ const (
PendingInteractionStatusCanceled PendingInteractionStatus = "canceled"
)
// PendingToolCallSnapshot 保存待确认工具调用的最小快照。
// PendingToolCallSnapshot 保存"待确认工具调用"的最小快照。
//
// 职责边界:
// 1. 负责保存真正落库 / 落缓存恢复执行所需的最小信息;
// 2. ArgsJSON 约定存已经序列化好的参数快照,避免此处反向依赖具体 tool 参数结构;
// 3. 不负责工具执行,不负责幂等校验,不负责回滚。
type PendingToolCallSnapshot struct {
ToolName string
ArgsJSON string
Summary string
ToolName string `json:"tool_name"`
ArgsJSON string `json:"args_json"`
Summary string `json:"summary"`
}
// PendingInteraction 保存本轮需要中断并等待用户后续动作的交互快照。
// PendingInteraction 保存"本轮需要中断并等待用户后续动作"的交互快照。
//
// 设计目的:
// 1. ask_user 与 confirm 都不是业务 tool而是流程级中断所以单独建模
// 2. ResumeNode / ResumePhase / ResumeStep 用来记录恢复点,避免用户回答后整条链路从头乱跑;
// 3. 该结构设计成可被 Redis + MySQL 直接存储的快照骨架,后续只需要补序列化与持久化接线。
//
// TODO(newagent/store): 后续把该结构整体快照到 Redis + MySQL形成双保险恢复点
// TODO(newagent/api): 后续由“用户追问回复接口 / 确认回调接口”读取这份快照并恢复运行。
// 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
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 负责承载需要中断后恢复的交互快照;
// 2. PendingInteraction 负责承载"需要中断后恢复"的交互快照;
// 3. 这样既不污染 CommonState 的职责,又能让 graph 在一次入参里拿到完整运行态。
type AgentRuntimeState struct {
*CommonState
PendingInteraction *PendingInteraction
*CommonState `json:"common_state"`
// PendingInteraction 承载挂起交互的持久化快照。
PendingInteraction *PendingInteraction `json:"pending_interaction,omitempty"`
// PendingConfirmTool 是 Execute → Confirm 之间传递待确认工具信息的临时邮箱。
// Execute 节点写入Confirm 节点读出并清空,不参与持久化。
PendingConfirmTool *PendingToolCallSnapshot `json:"-"`
}
// NewAgentRuntimeState 创建 graph 运行态。
@@ -120,7 +123,7 @@ func (s *AgentRuntimeState) PendingInteractionType() PendingInteractionType {
return s.PendingInteraction.Type
}
// OpenAskUserInteraction 打开一个向用户追问的中断快照。
// OpenAskUserInteraction 打开一个"向用户追问"的中断快照。
func (s *AgentRuntimeState) OpenAskUserInteraction(interactionID, question, resumeNode string) {
s.openPendingInteraction(
PendingInteractionTypeAskUser,
@@ -131,7 +134,7 @@ func (s *AgentRuntimeState) OpenAskUserInteraction(interactionID, question, resu
)
}
// OpenConfirmInteraction 打开一个写操作待确认的中断快照。
// OpenConfirmInteraction 打开一个"写操作待确认"的中断快照。
func (s *AgentRuntimeState) OpenConfirmInteraction(interactionID, confirmText, resumeNode string, pendingTool *PendingToolCallSnapshot) {
s.openPendingInteraction(
PendingInteractionTypeConfirm,
@@ -166,7 +169,7 @@ func (s *AgentRuntimeState) ResumeFromPending() bool {
//
// 职责边界:
// 1. 仅负责粗暴清空快照;
// 2. 不自动恢复 phase / step避免误把取消交互”与“恢复执行混为一谈;
// 2. 不自动恢复 phase / step避免误把"取消交互"与"恢复执行"混为一谈;
// 3. 若需要恢复流程,应优先使用 ResumeFromPending。
func (s *AgentRuntimeState) ClearPendingInteraction() {
if s == nil || s.PendingInteraction == nil {
@@ -207,7 +210,7 @@ func (s *AgentRuntimeState) openPendingInteraction(
// 1. 一旦进入 pending 状态,当前连接上的 graph 应立即停止向后执行。
// 2. 这里先统一把 Phase 置为 interrupted后续恢复时再按快照写回原阶段。
// 3. 这样分支函数只需要判断 HasPendingInteraction(),无需猜测当前 phase 是否仍可信
// 3. 这样分支函数只需要判断 HasPendingInteraction(),无需猜测"当前 phase 是否仍可信"
flowState.Phase = PhaseInterrupted
}

View File

@@ -0,0 +1,49 @@
package model
import "context"
// AgentStateSnapshot 是需要持久化的 agent 运行态最小快照。
//
// 设计说明:
// 1. 只保存恢复执行所需的 RuntimeState 和 ConversationContext
// 2. 不保存 Request每轮请求级天然不跨连接
// 3. 不保存 Deps依赖注入每次由 Service 层重建);
// 4. 不保存 ToolSchemas每次请求由 Service 层重新注入)。
type AgentStateSnapshot struct {
RuntimeState *AgentRuntimeState `json:"runtime_state"`
ConversationContext *ConversationContext `json:"conversation_context"`
}
// AgentStateStore 定义 agent 状态持久化的最小接口。
//
// 职责边界:
// 1. 只负责"存 / 取 / 删"三个原子操作;
// 2. 不负责序列化细节(由实现层决定 JSON / protobuf
// 3. 不负责业务级状态校验,校验仍在 node / graph 层完成。
//
// 实现层:
// 1. dao/cache.go 上的 CacheDAO 隐式实现该接口Go duck typing
// 2. newAgent 包不直接 import dao由 Service 层在组装 Deps 时注入。
type AgentStateStore interface {
// Save 序列化并保存一份 agent 状态快照。
//
// 语义:
// 1. 同一 conversationID 被覆盖写入,保证 Redis 里始终只有最新快照;
// 2. 实现层应设 TTL避免已完成的任务快照永不清理。
Save(ctx context.Context, conversationID string, snapshot *AgentStateSnapshot) error
// Load 读取并反序列化 agent 状态快照。
//
// 返回值语义:
// 1. (snapshot, true, nil):命中快照,正常返回;
// 2. (nil, false, nil):未命中,不是错误,调用方应走新建对话路径;
// 3. (nil, false, error):真正的存储层错误。
Load(ctx context.Context, conversationID string) (*AgentStateSnapshot, bool, error)
// Delete 删除指定会话的 agent 状态快照。
//
// 语义:
// 1. 删除是幂等的key 不存在也视为成功;
// 2. 典型调用时机Deliver 节点任务完成后清理。
Delete(ctx context.Context, conversationID string) error
}