package agent import ( "strings" "github.com/Autumn-27/norma/harness" ) // 收尾提示词(wrap-up / settlement prompt):当 agent 因【步数耗尽(MaxTurns)】或 // 【超时(run_seconds/MaxDuration)】被终止时,SDK 的 settlement 阶段会注入这段提示, // 让 agent 先把已识别但未写回的内容落库、再输出一句总结,避免烂尾。 // // 每个 agent 的收尾提示词可在后台按需覆盖(存 agents.wrapup_prompt),留空则用这里的 // 内置默认。仅【提示词正文】可编辑;禁用哪些工具、收尾自身给几轮预算属代码固定策略。 // WrapupOverride, if set, returns the stored wrap-up prompt for an agent key and // whether a non-empty one exists. Wired by the server to the agents table (like // PromptOverride for system prompts). nil / empty → the built-in default is used. var WrapupOverride func(agentKey string) (string, bool) // WrapupMaxTurnsOverride, if set, returns the admin-configured turn budget for the // wrap-up phase of an agent and whether a positive one exists. Wired to the agents // table. nil / ≤0 → the built-in per-agent default (wrapupTurnDefaults) is used. var WrapupMaxTurnsOverride func(agentKey string) (int, bool) // 内置默认收尾提示词,按 agent key 索引。worker 复用历史上硬编码的 settleWrapUpPrompt // (定义在 worker.go),planner/mainagent 各有一版;未命中的(自定义 agent)走通用兜底。 var wrapupDefaults = map[string]string{ "worker": settleWrapUpPrompt, "planner": plannerWrapUpDefault, "mainagent": mainAgentWrapUpDefault, } // wrapupTurnDefaults: 各 agent 收尾阶段【自身】的轮数预算内置默认(可被后台 >0 覆盖)。 // worker 写回可能占几轮(record_fact/insert_assets)故给 4;其余给 5。未命中走 genericWrapupTurns。 var wrapupTurnDefaults = map[string]int{ "worker": 4, "planner": 5, "mainagent": 5, } const genericWrapupTurns = 5 const plannerWrapUpDefault = "你本轮规划的步数即将用尽——注意只是【这一轮】结束,系统之后仍会随态势变化再次唤醒你继续规划,并非任务终止,你无需在此收束整个规划。请把本轮已经想清楚的结论落地、别让这一轮白跑,但也【不要为了收尾硬凑意图】(本轮 0 个意图仍是完全正常的结果):(1) 若已判断出【当前就该派发】的探索方向,用一次 add_intent 批量提交(想好的别憋着不发);(2) 对已被某发现/事实证明达成的目标,调 prove_goal 标记 met(别漏判);(3) 若识别出需要分步的串行利用链,用 TodoWrite 记下,便于下次唤醒接着派。做完直接结束本轮,无需输出总结文本。" const mainAgentWrapUpDefault = "你的步数即将用尽,本次交互就要结束。不要再发起新的探索/操作。请**单独用一句话纯文本**向用户总结当前进展、关键结论,以及建议的下一步。" const genericWrapUpDefault = "你即将因预算耗尽被终止。请先把已完成但未落库的结果写回,再**单独用一句话纯文本**总结你做了什么、得到哪些关键结论(这句会作为本次运行的结果展示)。" // WrapupDefault returns the built-in default wrap-up prompt for an agent key — // used by the admin UI as the "restore default" value and empty-field placeholder. func WrapupDefault(agentKey string) string { if d, ok := wrapupDefaults[agentKey]; ok { return d } return genericWrapUpDefault } // WrapupTurnsDefault returns the built-in wrap-up turn budget for an agent key — // used by the admin UI as the "0 = default N" hint. func WrapupTurnsDefault(agentKey string) int { if n, ok := wrapupTurnDefaults[agentKey]; ok { return n } return genericWrapupTurns } // resolveWrapup returns the effective wrap-up prompt: the DB override (if set and // non-empty) over the built-in default. func resolveWrapup(agentKey string) string { if WrapupOverride != nil { if t, ok := WrapupOverride(agentKey); ok && strings.TrimSpace(t) != "" { return t } } return WrapupDefault(agentKey) } // resolveWrapupTurns returns the effective wrap-up turn budget: a positive DB // override over the built-in per-agent default. func resolveWrapupTurns(agentKey string) int { if WrapupMaxTurnsOverride != nil { if v, ok := WrapupMaxTurnsOverride(agentKey); ok && v > 0 { return v } } return WrapupTurnsDefault(agentKey) } // wrapupSettlement builds the settlement config for an agent's run. Prompt and the // turn budget are admin-editable per agent; disabled tools are code-owned policy so // a user can't edit away the "stop probing" guardrail. Resolved fresh each run // (reads DB live), so edits apply on the next run without a restart. func wrapupSettlement(agentKey string, disabledTools []string) *harness.Settlement { return &harness.Settlement{ Prompt: resolveWrapup(agentKey), DisabledTools: disabledTools, MaxTurns: resolveWrapupTurns(agentKey), } } // ---------- 任务级超时收尾词(见 docs/任务级超时与收尾设计.md)---------- // // 与 per-run 收尾词是【两套】:per-run 是"你这一次 run 的预算用完了";任务超时是 // "整个任务到点、即将结束"。语义常相反(尤其 planner:per-run 说"别停继续规划", // 任务超时说"到点停止规划、做最后判定")。只给 worker/planner 配置。 // WrapupTaskTimeoutOverride / …TurnsOverride:任务超时收尾词与轮数的 DB 覆盖 // (wire 到 agents.task_timeout_wrapup_prompt / _max_turns,仅 worker/planner)。 var ( WrapupTaskTimeoutOverride func(agentKey string) (string, bool) WrapupTaskTimeoutTurnsOverride func(agentKey string) (int, bool) ) var taskTimeoutWrapupDefaults = map[string]string{ "worker": workerTaskTimeoutDefault, "planner": plannerTaskTimeoutDefault, } const workerTaskTimeoutDefault = "**整个任务已到达超时上限,即将结束**(不是你这次 run 的预算,是整场探索到点了)。这是最后机会:(1) 把你已识别但还没写回的内容【全部】落库——新资产 insert_assets、探索结论/事实 record_fact、确认漏洞 report_finding;(2) 不要再启动任何新命令/探测;(3) **最后单独用一句话纯文本**总结你在本意图上的关键结论。" const plannerTaskTimeoutDefault = "**整个任务已到达超时上限,即将结束**(不是本轮,是整个任务终止)。请基于当前【全部】事实与发现,做最后一次目标判定:对已被证据证明达成的目标调 prove_goal 标记 met(别漏判)。**不要再生成任何新意图**(此时派意图也不会再被执行)。判定完即收束,无需输出总结文本。" // TaskTimeoutWrapupDefault 返回某 agent 的任务超时内置默认收尾词(供后台占位/恢复默认)。 func TaskTimeoutWrapupDefault(agentKey string) string { return taskTimeoutWrapupDefaults[agentKey] // 未配置(mainagent/chat)返回空串 } // resolveTaskTimeoutWrapup:DB 覆盖(非空) > 内置默认。空串表示该 agent 无任务超时词 // (非 worker/planner),此时调用方应回退 per-run 词。 func resolveTaskTimeoutWrapup(agentKey string) string { if WrapupTaskTimeoutOverride != nil { if t, ok := WrapupTaskTimeoutOverride(agentKey); ok && strings.TrimSpace(t) != "" { return t } } return TaskTimeoutWrapupDefault(agentKey) } func resolveTaskTimeoutTurns(agentKey string) int { if WrapupTaskTimeoutTurnsOverride != nil { if v, ok := WrapupTaskTimeoutTurnsOverride(agentKey); ok && v > 0 { return v } } return resolveWrapupTurns(agentKey) // 默认沿用 per-run 轮数 } // wrapupSettlementForTask builds settlement for a worker/planner run that is aware // of the task deadline. See §5 of the design doc: // - clamped=true → 本次 run 被任务 deadline 夹逼:因 Timeout 收尾=任务到点→任务超时词; // 因 MaxTurns 收尾=夹逼窗口内步数先耗尽、任务还剩几分钟→回落 per-run 词。 // - clamped=false → 任务还早:两种 reason 都用 per-run 词(即退化为 wrapupSettlement)。 // // 交给 harness 的 PromptByReason 在收尾时按【实际】reason 现场挑,无 build 时错配。 func wrapupSettlementForTask(agentKey string, disabledTools []string, clamped bool) *harness.Settlement { perRun := resolveWrapup(agentKey) st := &harness.Settlement{ Prompt: perRun, // 兜底(也是非 clamped 时两种 reason 的取值) DisabledTools: disabledTools, MaxTurns: resolveWrapupTurns(agentKey), } if clamped { if tt := resolveTaskTimeoutWrapup(agentKey); tt != "" { st.PromptByReason = map[harness.TerminalReason]string{ harness.ReasonTimeout: tt, // 任务到点 harness.ReasonMaxTurns: perRun, // 步数先耗尽、任务还剩时间 } st.MaxTurns = resolveTaskTimeoutTurns(agentKey) } } return st }