Files
ARTEX/agent/wrapup.go
T

174 lines
8.7 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 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 覆盖)。
// 均给 10 轮,保证收尾阶段有足够步数落库。未命中走 genericWrapupTurns。
var wrapupTurnDefaults = map[string]int{
"worker": 10,
"planner": 10,
"mainagent": 10,
}
const genericWrapupTurns = 10
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
}