Version: 0.9.45.dev.260427
后端: 1. execute 主链路重构为“上下文工具域 + 主动优化候选闭环”——移除 order_guard,粗排后默认进入主动微调,先诊断再从后端候选中选择 move/swap,避免 LLM 自由全局乱搜 2. 工具体系升级为动态注入协议——新增 context_tools_add / remove、工具域与二级包映射、主动优化白名单;schedule / taskclass / web 工具按域按包暴露,msg0 规则包与 execute 上下文同步重写 3. analyze_health 升级为主动优化唯一裁判入口——补齐 rhythm / tightness / profile / feasibility 指标、候选扫描与复诊打分、停滞信号、forced imperfection 判定,并把连续优化状态写回运行态 4. 任务类能力并入新 Agent 执行链——新增 upsert_task_class 写工具与启动注入事务写入;任务类模型补充学科画像与整天屏蔽配置,粗排支持 excluded_days_of_week,steady 策略改为基于目标位置/单日负载/分散度/缓冲的候选打分 5. 运行态与路由补齐优化模式语义——新增 active tool domain/packs、pending context hook、active optimize only、taskclass 写入回盘快照;区分 first_full / global_reopt / local_adjust,并完善首次粗排后默认 refine 的判定 前端: 6. 助手时间线渲染细化——推理内容改为独立 reasoning block,支持与工具/状态/正文按时序交错展示,自动收口折叠,修正 confirm reject 恢复动作 仓库: 7. newAgent 文档整体迁入 docs/backend,补充主动优化执行规划与顺序约束拆解文档,删除旧调试日志文件 PS:这次科研了2天,总算是有些进展了——LLM永远只适合做选择题、判断题,不适合做开放创新题。
This commit is contained in:
@@ -71,6 +71,24 @@ type CommonState struct {
|
||||
TraceID string `json:"trace_id"`
|
||||
UserID int `json:"user_id"`
|
||||
ConversationID string `json:"conversation_id"`
|
||||
// ActiveToolDomain 记录当前 msg0 动态区激活的业务工具域。
|
||||
// 说明:
|
||||
// 1. 空字符串表示仅保留 context 管理工具,不注入业务工具定义;
|
||||
// 2. 非空时仅允许注入对应域的工具(如 schedule/taskclass);
|
||||
// 3. 该字段由 context_tools_add/remove 工具结果驱动更新。
|
||||
ActiveToolDomain string `json:"active_tool_domain,omitempty"`
|
||||
// ActiveToolPacks 记录当前激活域下的可选二级包(不含 core 固定包)。
|
||||
// 说明:
|
||||
// 1. 仅对 schedule 域生效(queue/mutation/analyze/web);
|
||||
// 2. 为空时按域默认策略解释(schedule 兼容为“全可选包”);
|
||||
// 3. 该字段与 ActiveToolDomain 一起由 context_tools_add/remove 结果更新。
|
||||
ActiveToolPacks []string `json:"active_tool_packs,omitempty"`
|
||||
// PendingContextHook 保存 plan 阶段给 execute 阶段的一次性注入建议。
|
||||
// 说明:
|
||||
// 1. 可由 plan_done 或 rough_build->execute 分支写入;
|
||||
// 2. execute 首轮消费一次后清空;
|
||||
// 3. 该字段只表达建议,不直接触发工具调用。
|
||||
PendingContextHook *ContextHook `json:"pending_context_hook,omitempty"`
|
||||
|
||||
// 流程阶段
|
||||
Phase Phase `json:"phase"`
|
||||
@@ -106,12 +124,70 @@ type CommonState struct {
|
||||
NeedsRefineAfterRoughBuild bool `json:"needs_refine_after_rough_build,omitempty"`
|
||||
// AllowReorder 表示本轮是否允许打乱 suggested 任务的相对顺序。
|
||||
// 默认 false,只有用户明确说明"可以打乱顺序/顺序不重要"才会为 true。
|
||||
AllowReorder bool `json:"allow_reorder,omitempty"`
|
||||
// SuggestedOrderBaseline 保存"本轮 execute 启动前"的 suggested 任务相对顺序基线。
|
||||
// OrderGuard 节点会基于该基线判断微调是否破坏顺序约束。
|
||||
SuggestedOrderBaseline []int `json:"suggested_order_baseline,omitempty"`
|
||||
AllowReorder bool `json:"allow_reorder,omitempty"`
|
||||
OptimizationMode string `json:"optimization_mode,omitempty"`
|
||||
// ActiveOptimizeOnly 标记“当前是否处于粗排后主动优化专用模式”。
|
||||
// 1. true 时,execute 只向 LLM 暴露 analyze_health + move + swap 这组最小闭环工具;
|
||||
// 2. 该开关只用于首次粗排后的自动微调,不影响用户后续明确提出的日程调整请求;
|
||||
// 3. 流程收口、重开新请求或切换业务域后,必须重置为 false。
|
||||
ActiveOptimizeOnly bool `json:"active_optimize_only,omitempty"`
|
||||
HealthCheckDone bool `json:"health_check_done,omitempty"`
|
||||
HealthIsFeasible bool `json:"health_is_feasible,omitempty"`
|
||||
HealthCapacityGap int `json:"health_capacity_gap,omitempty"`
|
||||
HealthReasonCode string `json:"health_reason_code,omitempty"`
|
||||
// HealthShouldContinueOptimize 记录最近一次 analyze_health 是否认为“还值得继续优化”。
|
||||
// 调用目的:
|
||||
// 1. 让 execute prompt 直接读取后端诊断结论,而不是只根据 issues 猜下一步;
|
||||
// 2. 该字段只表达“是否值得继续动”,不替 LLM 决定具体写参数;
|
||||
// 3. 默认 false,只有 analyze_health 明确判定后才会更新。
|
||||
HealthShouldContinueOptimize bool `json:"health_should_continue_optimize,omitempty"`
|
||||
// HealthTightnessLevel 记录最近一次诊断得到的优化空间等级:loose / tight / locked。
|
||||
// 调用目的:
|
||||
// 1. 用于提示 LLM 区分“还能优化”和“已经是被迫不完美”;
|
||||
// 2. 该字段只服务主动优化链路,不参与粗排可行性判断;
|
||||
// 3. 空字符串表示尚未拿到有效诊断。
|
||||
HealthTightnessLevel string `json:"health_tightness_level,omitempty"`
|
||||
// HealthPrimaryProblem 保存最近一次诊断的主要局部问题摘要。
|
||||
// 调用目的:
|
||||
// 1. 帮助 execute 聚焦当前最值得处理的那个点,避免全局乱搜;
|
||||
// 2. 只保存短摘要,不保存完整工具原文,避免状态膨胀;
|
||||
// 3. 为空表示当前没有明确主问题或诊断失败。
|
||||
HealthPrimaryProblem string `json:"health_primary_problem,omitempty"`
|
||||
// HealthRecommendedOperation 保存最近一次诊断建议优先考虑的动作类型。
|
||||
// 允许值由 analyze_health 控制,当前主要为 swap / move / close / ask_user。
|
||||
HealthRecommendedOperation string `json:"health_recommended_operation,omitempty"`
|
||||
// HealthIsForcedImperfection 标记当前剩余问题是否更像“约束代价”而非“仍值得修”的问题。
|
||||
// 调用目的:
|
||||
// 1. 给 LLM 一个明确的收口信号;
|
||||
// 2. 仅在 analyze_health 返回结构化 decision 时更新;
|
||||
// 3. false 不代表一定要继续优化,只代表“不是明确的被迫不完美”。
|
||||
HealthIsForcedImperfection bool `json:"health_is_forced_imperfection,omitempty"`
|
||||
// HealthImprovementSignal 保存最近一次诊断的紧凑对比信号,用于判断是否连续停滞。
|
||||
// 调用目的:
|
||||
// 1. execute 可基于该字段识别“连续两轮几乎没改善”;
|
||||
// 2. 信号由 analyze_health 生成,格式稳定但不面向用户展示;
|
||||
// 3. 若诊断失败则保持空字符串。
|
||||
HealthImprovementSignal string `json:"health_improvement_signal,omitempty"`
|
||||
// HealthStagnationCount 记录连续多少次 analyze_health 给出了相同的 improvement_signal。
|
||||
// 调用目的:
|
||||
// 1. 让 prompt 可以在“继续磨也没明显改善”时提醒 LLM 主动收口;
|
||||
// 2. 仅在两次连续有效诊断的信号完全相同时递增;
|
||||
// 3. 只做软提醒,不做后端硬拦截。
|
||||
HealthStagnationCount int `json:"health_stagnation_count,omitempty"`
|
||||
// TaskClassUpsertLastTried 标记本轮是否至少调用过一次 upsert_task_class。
|
||||
// 调用目的:execute_context 仅在该标记为 true 时注入“最近一次任务类写入结果”,避免噪音。
|
||||
TaskClassUpsertLastTried bool `json:"task_class_upsert_last_tried,omitempty"`
|
||||
// TaskClassUpsertLastSuccess 记录最近一次 upsert_task_class 是否成功。
|
||||
// 调用目的:为 prompt 提供“是否需要继续追问补字段”的明确信号。
|
||||
TaskClassUpsertLastSuccess bool `json:"task_class_upsert_last_success,omitempty"`
|
||||
// TaskClassUpsertLastIssues 记录最近一次写入返回的校验问题(validation.issues)。
|
||||
// 调用目的:让 LLM 直接按缺失字段追问,减少泛化提问。
|
||||
TaskClassUpsertLastIssues []string `json:"task_class_upsert_last_issues,omitempty"`
|
||||
// TaskClassUpsertConsecutiveFailures 记录连续写入失败次数。
|
||||
// 调用目的:给 prompt 注入“避免空转”的软提示,不做硬拦截。
|
||||
TaskClassUpsertConsecutiveFailures int `json:"task_class_upsert_consecutive_failures,omitempty"`
|
||||
// HasScheduleWriteOps 标记本轮 execute 循环是否执行过日程写工具。
|
||||
// 调用目的:graph 分支函数据此判断是否需要走 order_guard,非日程操作跳过守卫。
|
||||
// 调用目的:为 prompt/收口层提供“本轮是否真的动过日程写工具”的运行态信号。
|
||||
HasScheduleWriteOps bool `json:"has_schedule_write_ops,omitempty"`
|
||||
// UsedQuickNote 标记本轮是否调用过 quick_note_create 工具。
|
||||
// 调用目的:graph 完成后据此决定是否跳过记忆抽取,避免随口记内容被错误归类。
|
||||
@@ -164,8 +240,12 @@ func (s *CommonState) FinishPlan(steps []PlanStep) {
|
||||
s.PlanSteps = steps
|
||||
s.CurrentStep = 0
|
||||
s.Phase = PhaseWaitingConfirm
|
||||
s.ActiveToolDomain = ""
|
||||
s.ActiveToolPacks = nil
|
||||
s.PendingContextHook = nil
|
||||
s.NeedsRefineAfterRoughBuild = false
|
||||
s.SuggestedOrderBaseline = nil
|
||||
s.ActiveOptimizeOnly = false
|
||||
s.resetTaskClassUpsertSnapshot()
|
||||
s.ClearTerminalOutcome()
|
||||
}
|
||||
|
||||
@@ -173,7 +253,8 @@ func (s *CommonState) FinishPlan(steps []PlanStep) {
|
||||
func (s *CommonState) ConfirmPlan() {
|
||||
s.Phase = PhaseExecuting
|
||||
s.NeedsRefineAfterRoughBuild = false
|
||||
s.SuggestedOrderBaseline = nil
|
||||
s.ActiveOptimizeOnly = false
|
||||
s.resetTaskClassUpsertSnapshot()
|
||||
s.ClearTerminalOutcome()
|
||||
}
|
||||
|
||||
@@ -185,9 +266,13 @@ func (s *CommonState) StartDirectExecute() {
|
||||
s.PlanSteps = nil
|
||||
s.CurrentStep = 0
|
||||
s.Phase = PhaseExecuting
|
||||
s.ActiveToolDomain = ""
|
||||
s.ActiveToolPacks = nil
|
||||
s.PendingContextHook = nil
|
||||
s.NeedsRoughBuild = false
|
||||
s.NeedsRefineAfterRoughBuild = false
|
||||
s.SuggestedOrderBaseline = nil
|
||||
s.ActiveOptimizeOnly = false
|
||||
s.resetTaskClassUpsertSnapshot()
|
||||
s.ClearTerminalOutcome()
|
||||
}
|
||||
|
||||
@@ -196,8 +281,12 @@ func (s *CommonState) RejectPlan() {
|
||||
s.PlanSteps = nil
|
||||
s.CurrentStep = 0
|
||||
s.Phase = PhasePlanning
|
||||
s.ActiveToolDomain = ""
|
||||
s.ActiveToolPacks = nil
|
||||
s.PendingContextHook = nil
|
||||
s.NeedsRefineAfterRoughBuild = false
|
||||
s.SuggestedOrderBaseline = nil
|
||||
s.ActiveOptimizeOnly = false
|
||||
s.resetTaskClassUpsertSnapshot()
|
||||
s.ClearTerminalOutcome()
|
||||
}
|
||||
|
||||
@@ -223,18 +312,50 @@ func (s *CommonState) ResetForNextRun() {
|
||||
// 4. 清理计划执行游标与粗排相关临时标记,确保新请求不会误沿用旧计划。
|
||||
s.PlanSteps = nil
|
||||
s.CurrentStep = 0
|
||||
s.ActiveToolDomain = ""
|
||||
s.ActiveToolPacks = nil
|
||||
s.PendingContextHook = nil
|
||||
s.NeedsRoughBuild = false
|
||||
s.NeedsRefineAfterRoughBuild = false
|
||||
s.ActiveOptimizeOnly = false
|
||||
|
||||
// 5. 重置顺序约束临时态与终止结果,避免上一轮 completed/aborted/exhausted 语义串到下一轮。
|
||||
s.AllowReorder = false
|
||||
s.OptimizationMode = ""
|
||||
s.HealthCheckDone = false
|
||||
s.HealthIsFeasible = true
|
||||
s.HealthCapacityGap = 0
|
||||
s.HealthReasonCode = ""
|
||||
s.HealthShouldContinueOptimize = false
|
||||
s.HealthTightnessLevel = ""
|
||||
s.HealthPrimaryProblem = ""
|
||||
s.HealthRecommendedOperation = ""
|
||||
s.HealthIsForcedImperfection = false
|
||||
s.HealthImprovementSignal = ""
|
||||
s.HealthStagnationCount = 0
|
||||
s.HasScheduleWriteOps = false
|
||||
s.HasScheduleChanges = false
|
||||
s.UsedQuickNote = false
|
||||
s.SuggestedOrderBaseline = nil
|
||||
s.resetTaskClassUpsertSnapshot()
|
||||
s.ClearTerminalOutcome()
|
||||
}
|
||||
|
||||
// resetTaskClassUpsertSnapshot 清理“任务类写入回盘”运行态。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 仅清理 upsert_task_class 相关的临时回盘字段;
|
||||
// 2. 不影响 Health/Plan/Phase 等其他执行状态;
|
||||
// 3. 作为新一轮入口统一调用,避免旧失败信息污染本轮追问。
|
||||
func (s *CommonState) resetTaskClassUpsertSnapshot() {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
s.TaskClassUpsertLastTried = false
|
||||
s.TaskClassUpsertLastSuccess = false
|
||||
s.TaskClassUpsertLastIssues = nil
|
||||
s.TaskClassUpsertConsecutiveFailures = 0
|
||||
}
|
||||
|
||||
// AdvanceStep 推进到下一个计划步骤,并返回是否仍有剩余步骤。
|
||||
func (s *CommonState) AdvanceStep() bool {
|
||||
s.CurrentStep++
|
||||
@@ -248,6 +369,12 @@ func (s *CommonState) AdvanceStep() bool {
|
||||
// 2. 只有在尚未写入任何终止结果时,才默认补成 completed。
|
||||
func (s *CommonState) Done() {
|
||||
s.Phase = PhaseDone
|
||||
// 收口时自动清空工具域,确保下一轮 msg0 动态区回到最小集合(仅 context 管理工具)。
|
||||
// 调用目的:把“收尾清理”从 LLM 决策中剥离,减少 done 阶段无关 tool_call 噪音。
|
||||
s.ActiveToolDomain = ""
|
||||
s.ActiveToolPacks = nil
|
||||
s.PendingContextHook = nil
|
||||
s.ActiveOptimizeOnly = false
|
||||
if s.TerminalOutcome != nil {
|
||||
s.TerminalOutcome.Normalize()
|
||||
return
|
||||
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -61,7 +62,7 @@ func (d *ExecuteDecision) UnmarshalJSON(data []byte) error {
|
||||
Speak string `json:"speak,omitempty"`
|
||||
Action ExecuteAction `json:"action"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
GoalCheck string `json:"goal_check,omitempty"`
|
||||
GoalCheck json.RawMessage `json:"goal_check,omitempty"`
|
||||
ToolCall json.RawMessage `json:"tool_call,omitempty"`
|
||||
Abort json.RawMessage `json:"abort,omitempty"`
|
||||
}
|
||||
@@ -74,7 +75,11 @@ func (d *ExecuteDecision) UnmarshalJSON(data []byte) error {
|
||||
d.Speak = raw.Speak
|
||||
d.Action = raw.Action
|
||||
d.Reason = raw.Reason
|
||||
d.GoalCheck = raw.GoalCheck
|
||||
goalCheck, err := decodeGoalCheckText(raw.GoalCheck)
|
||||
if err != nil {
|
||||
return fmt.Errorf("goal_check 解析失败: %w", err)
|
||||
}
|
||||
d.GoalCheck = goalCheck
|
||||
|
||||
toolCall, err := decodeOptionalJSONObject[ToolCallIntent](raw.ToolCall)
|
||||
if err != nil {
|
||||
@@ -91,6 +96,124 @@ func (d *ExecuteDecision) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodeGoalCheckText 兼容 goal_check 的字符串/对象写法,统一降级为字符串。
|
||||
//
|
||||
// 步骤化说明:
|
||||
// 1. 字符串:直接使用,保持主协议不变;
|
||||
// 2. 对象:按 done_when/evidence 提取并拼接为单行证据文本;
|
||||
// 3. 数组或其他标量:尽量转成可读字符串,避免仅因格式漂移导致整轮失败。
|
||||
func decodeGoalCheckText(raw json.RawMessage) (string, error) {
|
||||
trimmed := strings.TrimSpace(string(raw))
|
||||
if trimmed == "" || trimmed == "null" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// 1. 标准写法:goal_check 为字符串。
|
||||
if strings.HasPrefix(trimmed, "\"") {
|
||||
var text string
|
||||
if err := json.Unmarshal(raw, &text); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(text), nil
|
||||
}
|
||||
|
||||
// 2. 兼容写法:goal_check 被模型写成对象。
|
||||
if strings.HasPrefix(trimmed, "{") {
|
||||
var obj map[string]any
|
||||
if err := json.Unmarshal(raw, &obj); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return compactGoalCheckObject(obj), nil
|
||||
}
|
||||
|
||||
// 3. 兜底:数组/标量场景,尽量保留可读信息。
|
||||
var generic any
|
||||
if err := json.Unmarshal(raw, &generic); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(formatGoalCheckValue(generic)), nil
|
||||
}
|
||||
|
||||
// compactGoalCheckObject 将对象型 goal_check 压缩为可读单行文本,优先提取 done_when/evidence。
|
||||
func compactGoalCheckObject(obj map[string]any) string {
|
||||
if len(obj) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
doneWhen := strings.TrimSpace(formatGoalCheckValue(obj["done_when"]))
|
||||
evidence := strings.TrimSpace(formatGoalCheckValue(obj["evidence"]))
|
||||
|
||||
parts := make([]string, 0, 2)
|
||||
if doneWhen != "" {
|
||||
parts = append(parts, "已满足 done_when:"+doneWhen)
|
||||
}
|
||||
if evidence != "" {
|
||||
parts = append(parts, "证据:"+evidence)
|
||||
}
|
||||
if len(parts) > 0 {
|
||||
return strings.Join(parts, ";")
|
||||
}
|
||||
|
||||
// done_when/evidence 缺失时,按 key 排序拼接,保证日志稳定可读。
|
||||
keys := make([]string, 0, len(obj))
|
||||
for key := range obj {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
fallback := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
text := strings.TrimSpace(formatGoalCheckValue(obj[key]))
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
fallback = append(fallback, key+"="+text)
|
||||
}
|
||||
return strings.Join(fallback, ";")
|
||||
}
|
||||
|
||||
// formatGoalCheckValue 将任意值转成单行可读文本,用于 goal_check 压缩拼接。
|
||||
func formatGoalCheckValue(value any) string {
|
||||
switch typed := value.(type) {
|
||||
case nil:
|
||||
return ""
|
||||
case string:
|
||||
return strings.TrimSpace(typed)
|
||||
case bool:
|
||||
if typed {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
case []any:
|
||||
parts := make([]string, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
text := strings.TrimSpace(formatGoalCheckValue(item))
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, text)
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
case map[string]any:
|
||||
keys := make([]string, 0, len(typed))
|
||||
for key := range typed {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
parts := make([]string, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
text := strings.TrimSpace(formatGoalCheckValue(typed[key]))
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, key+"="+text)
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprintf("%v", typed))
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize 统一清洗 execute 决策中的字符串字段。
|
||||
func (d *ExecuteDecision) Normalize() {
|
||||
if d == nil {
|
||||
|
||||
@@ -12,6 +12,15 @@ const (
|
||||
|
||||
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
|
||||
|
||||
@@ -179,6 +188,26 @@ func (s *AgentRuntimeState) ClearPendingInteraction() {
|
||||
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,
|
||||
|
||||
@@ -55,6 +55,19 @@ type PlanDecision struct {
|
||||
PlanSteps []PlanStep `json:"plan_steps,omitempty"`
|
||||
NeedsRoughBuild bool `json:"needs_rough_build,omitempty"`
|
||||
TaskClassIDs []int `json:"task_class_ids,omitempty"`
|
||||
ContextHook *ContextHook `json:"context_hook,omitempty"`
|
||||
}
|
||||
|
||||
// ContextHook 表示 plan 阶段给 execute 阶段的上下文注入建议。
|
||||
//
|
||||
// 职责边界:
|
||||
// 1. 仅承载“建议激活哪个 domain/packs”,不负责真正执行 context_tools_add/remove;
|
||||
// 2. domain 仅允许 schedule/taskclass,packs 仅允许 schedule 的可选包;
|
||||
// 3. 该结构会在 execute 首轮被消费一次,消费后由后端清空。
|
||||
type ContextHook struct {
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Packs []string `json:"packs,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// Normalize 统一清洗规划决策中的字符串字段。
|
||||
@@ -69,6 +82,9 @@ func (d *PlanDecision) Normalize() {
|
||||
for i := range d.PlanSteps {
|
||||
d.PlanSteps[i].Normalize()
|
||||
}
|
||||
if d.ContextHook != nil {
|
||||
d.ContextHook.Normalize()
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 校验规划决策的最小合法性。
|
||||
@@ -102,6 +118,9 @@ func (d *PlanDecision) Validate() error {
|
||||
if len(d.PlanSteps) > 0 {
|
||||
return fmt.Errorf("%s 动作不应携带 plan_steps", d.Action)
|
||||
}
|
||||
if d.ContextHook != nil {
|
||||
return fmt.Errorf("%s 动作不应携带 context_hook", d.Action)
|
||||
}
|
||||
return nil
|
||||
case PlanActionDone:
|
||||
if len(d.PlanSteps) == 0 {
|
||||
@@ -112,6 +131,11 @@ func (d *PlanDecision) Validate() error {
|
||||
return fmt.Errorf("plan_steps[%d] 非法: %w", i, err)
|
||||
}
|
||||
}
|
||||
if d.ContextHook != nil {
|
||||
if err := d.ContextHook.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("未知 plan action: %s", d.Action)
|
||||
@@ -149,3 +173,73 @@ func (s *PlanStep) Validate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Normalize 统一清洗 context hook 字段。
|
||||
func (h *ContextHook) Normalize() {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
h.Domain = normalizeContextHookDomain(h.Domain)
|
||||
h.Reason = strings.TrimSpace(h.Reason)
|
||||
h.Packs = normalizeContextHookPacks(h.Domain, h.Packs)
|
||||
}
|
||||
|
||||
// Validate 校验 context hook 最小合法性。
|
||||
func (h *ContextHook) Validate() error {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
h.Normalize()
|
||||
if h.Domain == "" {
|
||||
return fmt.Errorf("context_hook.domain 非法,仅支持 schedule/taskclass")
|
||||
}
|
||||
if h.Domain == "taskclass" && len(h.Packs) > 0 {
|
||||
return fmt.Errorf("context_hook.taskclass 暂不支持 packs")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeContextHookDomain(domain string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(domain)) {
|
||||
case "schedule":
|
||||
return "schedule"
|
||||
case "taskclass":
|
||||
return "taskclass"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeContextHookPacks(domain string, packs []string) []string {
|
||||
if domain != "schedule" || len(packs) == 0 {
|
||||
return nil
|
||||
}
|
||||
allowed := map[string]struct{}{
|
||||
"queue": {},
|
||||
"mutation": {},
|
||||
"analyze": {},
|
||||
"detail_read": {},
|
||||
"deep_analyze": {},
|
||||
"web": {},
|
||||
}
|
||||
seen := make(map[string]struct{}, len(packs))
|
||||
result := make([]string, 0, len(packs))
|
||||
for _, raw := range packs {
|
||||
pack := strings.ToLower(strings.TrimSpace(raw))
|
||||
if pack == "" || pack == "core" {
|
||||
continue
|
||||
}
|
||||
if _, ok := allowed[pack]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[pack]; exists {
|
||||
continue
|
||||
}
|
||||
seen[pack] = struct{}{}
|
||||
result = append(result, pack)
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user