Files
smartmate/backend/newAgent/model/common_state.go
Losita 310cdfbcb7 Version: 0.9.6.dev.260407
后端:
1.execute 正式终止协议补齐(abort / exhausted / completed 统一建模)
  - 更新model/common_state.go:新增 FlowTerminalStatus / FlowTerminalOutcome;补齐 Abort/Exhaust/ClearTerminalOutcome/IsCompleted 等统一终止语义
  - 更新model/execute_contract.go:新增 ExecuteActionAbort 与 AbortIntent;补齐 action 校验互斥规则
  - 更新prompt/execute.go:Plan/ReAct 两套 execute contract 升级到 V2,补充 abort 协议与 JSON 示例
2.graph 路由与 deliver 收口统一围绕 terminal outcome
  - 更新graph/common_graph.go:RoughBuild 改 branch;粗排异常可直接 Deliver;Execute 路由不再按“最后一轮”提前误收口
  - 更新node/execute.go:轮次耗尽改写为 Exhaust;接入 handleExecuteActionAbort;abort 不在 execute 直接对用户收口
  - 更新node/deliver.go:deliver summary 优先按 abort/exhausted 收口;不再无脑 Done;最终状态文案改为“本轮流程已结束”
  - 更新node/agent_nodes.go:仅 completed 路径写 schedule preview,aborted/exhausted 跳过
3.提示与状态摘要同步终止语义
  - 更新prompt/base.go:state summary 增加 terminal outcome 展示
前端:无 仓库:无
2026-04-07 23:59:50 +08:00

313 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package model
import (
"strings"
newagenttools "github.com/LoveLosita/smartflow/backend/newAgent/tools"
)
// Phase 表示 agent 主循环当前所处的大阶段。
type Phase string
const (
PhasePlanning Phase = "planning"
PhaseWaitingConfirm Phase = "waiting_confirm"
PhaseExecuting Phase = "executing"
PhaseDone Phase = "done"
)
// FlowTerminalStatus 表示本轮流程最终是如何结束的。
//
// 说明:
// 1. completed 表示任务按预期完成,允许走正常交付与预览落盘;
// 2. aborted 表示业务语义上的主动终止,例如粗排异常、执行期明确中止;
// 3. exhausted 表示安全边界触发的被动停止,例如执行轮次耗尽。
type FlowTerminalStatus string
const (
FlowTerminalStatusCompleted FlowTerminalStatus = "completed"
FlowTerminalStatusAborted FlowTerminalStatus = "aborted"
FlowTerminalStatusExhausted FlowTerminalStatus = "exhausted"
)
// FlowTerminalOutcome 保存“流程为什么结束”的最终结果快照。
//
// 职责边界:
// 1. Stage 说明终止发生在哪个阶段,便于 graph/deliver/debug 统一收口;
// 2. Code 作为稳定机器码,便于后续前端或埋点按类型识别;
// 3. UserMessage 是最终给用户看的收口文案;
// 4. InternalReason 只用于日志与排查,不直接暴露给用户。
type FlowTerminalOutcome struct {
Status FlowTerminalStatus `json:"status"`
Stage string `json:"stage,omitempty"`
Code string `json:"code,omitempty"`
UserMessage string `json:"user_message,omitempty"`
InternalReason string `json:"internal_reason,omitempty"`
}
// Normalize 统一清洗终止结果里的字符串字段。
func (o *FlowTerminalOutcome) Normalize() {
if o == nil {
return
}
o.Status = FlowTerminalStatus(strings.TrimSpace(string(o.Status)))
o.Stage = strings.TrimSpace(o.Stage)
o.Code = strings.TrimSpace(o.Code)
o.UserMessage = strings.TrimSpace(o.UserMessage)
o.InternalReason = strings.TrimSpace(o.InternalReason)
}
const DefaultMaxRounds = 30
// CommonState 承载可持久化的主流程状态。
//
// 职责边界:
// 1. 负责记录"当前处于哪个阶段、当前计划是什么、执行到了第几步、已经消耗了多少轮"
// 2. 负责提供最小必要的安全访问方法,避免 graph/node/prompt 层到处手写切片越界判断;
// 3. 不负责承载对话历史、tool schema、pinned context 这类模型输入材料,它们仍然属于 ConversationContext。
type CommonState struct {
// 身份信息
TraceID string `json:"trace_id"`
UserID int `json:"user_id"`
ConversationID string `json:"conversation_id"`
// 流程阶段
Phase Phase `json:"phase"`
// 计划状态
// 1. 这里直接使用结构化的 PlanStep避免 planning -> execute 之间丢失 done_when。
// 2. CurrentStep 表示"当前 plan 步骤下标",不是 execute 内部 ReAct 的思考轮次。
PlanSteps []PlanStep `json:"plan_steps"`
CurrentStep int `json:"current_step"`
// 安全边界
MaxRounds int `json:"max_rounds"`
RoundUsed int `json:"round_used"`
// 连续修正计数LLM 连续输出不合法决策的次数,超过阈值后强制终止避免死循环。
ConsecutiveCorrections int `json:"consecutive_corrections"`
// TaskClassIDs 本次排课请求涉及的任务类 ID 列表,由前端 extra.task_class_ids 传入。
// Plan 节点据此判断是否需要粗排;跨轮次持久化,不会因会话恢复而丢失。
TaskClassIDs []int `json:"task_class_ids,omitempty"`
// TaskClasses 本次排课涉及的任务类约束元数据(含日期、策略、时段预算等),
// 在 Service 层从 DB 加载并注入,供 Plan prompt 直接消费,避免 LLM 因信息不足而追问用户。
TaskClasses []newagenttools.TaskClassMeta `json:"task_classes,omitempty"`
// NeedsRoughBuild 由 Plan 节点在 plan_done 时写入,标记 Confirm 后是否需要走粗排节点。
// 粗排节点执行完毕后会将此字段重置为 false。
NeedsRoughBuild bool `json:"needs_rough_build,omitempty"`
// TerminalOutcome 保存“本轮流程最终如何结束”的统一收口结果。
// 第二轮开始rough_build / execute / deliver 都应围绕这份快照判断收口语义。
TerminalOutcome *FlowTerminalOutcome `json:"terminal_outcome,omitempty"`
}
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 在 planning 完成后固化完整计划,并推进到待确认阶段。
//
// 步骤说明:
// 1. 直接保存完整的 []PlanStep避免 execute 阶段再去依赖 pinned context 回捞完成判定;
// 2. 统一把 CurrentStep 重置到第 0 步,保证后续 confirm/execute 都从计划开头进入;
// 3. 这里只负责状态切换,不负责刷新 ConversationContext 中的置顶 plan 文本。
func (s *CommonState) FinishPlan(steps []PlanStep) {
s.PlanSteps = steps
s.CurrentStep = 0
s.Phase = PhaseWaitingConfirm
s.ClearTerminalOutcome()
}
// ConfirmPlan 表示用户已确认计划,流程进入执行阶段。
func (s *CommonState) ConfirmPlan() {
s.Phase = PhaseExecuting
s.ClearTerminalOutcome()
}
// StartDirectExecute 进入无 plan 的直接执行ReAct模式。
// Chat 节点路由到 execute 时必须调用此方法,而非直接赋值 Phase
// 否则上一次任务残留的 PlanSteps 会被 HasPlan() 误判为仍有计划,
// 导致 Execute 节点用旧步骤跑 plan 模式而非 ReAct 模式。
func (s *CommonState) StartDirectExecute() {
s.PlanSteps = nil
s.CurrentStep = 0
s.Phase = PhaseExecuting
s.ClearTerminalOutcome()
}
// RejectPlan 表示用户拒绝当前计划,清空计划并回退到 planning。
func (s *CommonState) RejectPlan() {
s.PlanSteps = nil
s.CurrentStep = 0
s.Phase = PhasePlanning
s.ClearTerminalOutcome()
}
// AdvanceStep 推进到下一个计划步骤,并返回是否仍有剩余步骤。
func (s *CommonState) AdvanceStep() bool {
s.CurrentStep++
return s.CurrentStep < len(s.PlanSteps)
}
// Done 标记整个任务流程已经结束。
//
// 说明:
// 1. 若此前已经写入 aborted / exhausted 等终止结果,这里只负责兜底维持 PhaseDone不覆盖已有语义
// 2. 只有在尚未写入任何终止结果时,才默认补成 completed。
func (s *CommonState) Done() {
s.Phase = PhaseDone
if s.TerminalOutcome != nil {
s.TerminalOutcome.Normalize()
return
}
s.TerminalOutcome = &FlowTerminalOutcome{
Status: FlowTerminalStatusCompleted,
}
}
// Abort 将当前流程标记为“业务语义上的主动终止”。
//
// 步骤说明:
// 1. 统一写入 PhaseDone保证 graph 后续直接进入 deliver 收口;
// 2. UserMessage 作为最终可见文案,必须尽量完整,避免 deliver 再二次猜测;
// 3. InternalReason 只用于排查,允许比用户文案更技术化。
func (s *CommonState) Abort(stage, code, userMessage, internalReason string) {
s.Phase = PhaseDone
s.TerminalOutcome = &FlowTerminalOutcome{
Status: FlowTerminalStatusAborted,
Stage: stage,
Code: code,
UserMessage: userMessage,
InternalReason: internalReason,
}
s.TerminalOutcome.Normalize()
}
// Exhaust 将当前流程标记为“安全边界触发的被动停止”。
func (s *CommonState) Exhaust(stage, userMessage, internalReason string) {
s.Phase = PhaseDone
s.TerminalOutcome = &FlowTerminalOutcome{
Status: FlowTerminalStatusExhausted,
Stage: stage,
Code: "round_exhausted",
UserMessage: userMessage,
InternalReason: internalReason,
}
s.TerminalOutcome.Normalize()
}
// ClearTerminalOutcome 清空上一轮遗留的终止结果。
func (s *CommonState) ClearTerminalOutcome() {
if s == nil {
return
}
s.TerminalOutcome = nil
}
// HasTerminalOutcome 判断当前是否已经写入正式终止结果。
func (s *CommonState) HasTerminalOutcome() bool {
return s != nil && s.TerminalOutcome != nil
}
// TerminalStatus 返回当前终止结果的状态枚举。
func (s *CommonState) TerminalStatus() FlowTerminalStatus {
if s == nil || s.TerminalOutcome == nil {
return ""
}
return s.TerminalOutcome.Status
}
// IsCompleted 判断当前是否属于“正常完成”。
func (s *CommonState) IsCompleted() bool {
return s.TerminalStatus() == FlowTerminalStatusCompleted
}
// IsAborted 判断当前是否属于“主动中止”。
func (s *CommonState) IsAborted() bool {
return s.TerminalStatus() == FlowTerminalStatusAborted
}
// IsExhaustedTerminal 判断当前是否属于“轮次耗尽收口”。
func (s *CommonState) IsExhaustedTerminal() bool {
return s.TerminalStatus() == FlowTerminalStatusExhausted
}
// HasPlan 判断当前 state 是否已经持有一份完整计划。
//
// 职责边界:
// 1. 负责收口"是否存在 plan"这一层判断,避免外层到处写 len(PlanSteps) > 0
// 2. 不判断 CurrentStep 当前是否有效,当前步骤是否合法由 HasCurrentPlanStep 回答;
// 3. state 为空时统一返回 false调用方可据此决定是否回退到 planning。
func (s *CommonState) HasPlan() bool {
if s == nil {
return false
}
return len(s.PlanSteps) > 0
}
// CurrentPlanStep 返回当前正在执行的结构化计划步骤。
//
// 职责边界:
// 1. 负责根据 CurrentStep 安全读取 PlanSteps避免 graph/node/prompt 层重复写越界判断;
// 2. 若 state 为空、plan 为空、或当前索引越界,则统一返回 (PlanStep{}, false)
// 3. 不负责推进步骤,也不负责修正 CurrentStep 的取值。
func (s *CommonState) CurrentPlanStep() (PlanStep, bool) {
if s == nil {
return PlanStep{}, false
}
if s.CurrentStep < 0 || s.CurrentStep >= len(s.PlanSteps) {
return PlanStep{}, false
}
return s.PlanSteps[s.CurrentStep], true
}
// HasCurrentPlanStep 判断"当前步骤"是否存在且可安全读取。
func (s *CommonState) HasCurrentPlanStep() bool {
_, ok := s.CurrentPlanStep()
return ok
}
// PlanProgress 返回当前计划的执行进度。
//
// 输出语义:
// 1. current 使用更适合给用户看的 1-based 序号;
// 2. total 表示当前计划的总步数;
// 3. 若当前还没有计划,则返回 (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
}