Version: 0.8.4.dev.260329
后端: 1.新建newAgent文件夹,是的你没听错,刚刚搬迁完的旧结构又准备推翻了:因为通用性太差,用户需求复杂一点就招架不了。最新的架构已经在路上,这应该是这个项目的正确路线了,目前正在搭骨架。 前端: 无改动 全仓库: 无改动
This commit is contained in:
147
backend/newAgent/model/common_state.go
Normal file
147
backend/newAgent/model/common_state.go
Normal 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
|
||||
}
|
||||
215
backend/newAgent/model/conversation_context.go
Normal file
215
backend/newAgent/model/conversation_context.go
Normal 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
|
||||
}
|
||||
205
backend/newAgent/model/execute_contract.go
Normal file
205
backend/newAgent/model/execute_contract.go
Normal 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"`
|
||||
}
|
||||
220
backend/newAgent/model/pending_interaction.go
Normal file
220
backend/newAgent/model/pending_interaction.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user