feat(tasks): 目标完成后支持主 agent 直投意图执行(goalless)

所有目标达成后,主 agent 经 add_intent 下发的意图可被 worker 直接执行、跑完
即止;planner 在任务无 open 目标时不再运行,避免其重判目标达成而 cancelExec
杀掉直投意图;frontier 抽干后任务回到 done。

- db: 新增 HasOpenGoal 谓词(零迁移,读现有 goal 状态)
- engine: plannerLoop 加 goalless 门(无 open 目标则不进 planner,抽干则收尾)
- tools: add_intent 成功后调 resumeTask 复活任务(仅主 agent 生效,planner 为 nil)
- mainagent: 提示加入「目标达成后反问是否登记为正式目标」引导
- orchestration: reseedMainAgentPrompt 让旧库收到新提示(一次性,同构 reseedGoalsPrompt)

已知边角问题(paused/exhausted 意图的收官判据、零 goal 窗口、TOCTOU 等)
待后续讨论后再修。
This commit is contained in:
Autumn-27 committed 2026-08-24 11:58:02 -04:00
1 parent 43edb86720
commit 01018a2161
5 files changed
+71 -1

No files matched your search

+18
View File
@@ -792,6 +792,24 @@ func (e *Engine) plannerLoop(ctx context.Context, t *Task) {
if e.isSettling(t.ID) {
return
}
// goalless(人工直投)分支:任务已无 open 目标时 planner 不跑——跑了会重判
// met→cancelExec 杀掉用户经主 agent 直投的意图。是否结束改由 frontier 决定:
// 还有 open/running 意图 → 保持 running、静默等待;意图已全部跑干 → 落 done。
// 整段纯 Go、不触发任何 LLM 调用,也不打规划轮 marker。
if open, err := t.Store.HasOpenGoal(); err == nil && !open {
t.drainTriggers() // 丢弃累积的 done/finding 触发,避免 goalless 长会话里无界增长
if active, err := t.Store.HasActiveIntent(); err == nil && !active {
// frontier 抽干且无在跑意图 → 收尾。用 Guarded 版做 CAS,避免踩到并发的
// pause/delete/超时收尾的状态转换。
if won, err := e.m.SetTaskStatusGuarded(t.ID, "done"); err != nil {
log.Printf("[goalless] task %s 收尾落 done 失败: %v", t.ID, err)
} else if won {
e.emitActivity(t, db.Activity{Worker: "system", Kind: "text",
Summary: "目标已全部达成,直投意图已执行完毕,任务结束"})
}
}
return // goalless 分支永不进入 planner.Plan
}
if !e.beginTaskOperation(t.ID) {
return
}
+31
View File
@@ -451,6 +451,7 @@ func (s *Server) seedOrchestrationTools() {
s.seedAutoReportFindingBinding()
s.unbindGoalMetDefault()
s.reseedGoalsPrompt() // goals 提示词加入「抽操作约束」步 → 旧库追加一版新默认(一次性)
s.reseedMainAgentPrompt() // mainagent 提示词加入「目标达成后 add_intent 反问是否建目标」(一次性)
s.seedReporterAgent() // 预置「报告撰写」agent + 工具绑定 + finding 触发器(一次性)
// 注:pentest 的默认工具绑定无需迁移——BuiltinToolSeeds 在全新初始化时就把
// list_assets/insert_assets/report_finding/list_findings/list_companies 连同
@@ -540,6 +541,36 @@ func (s *Server) reseedGoalsPrompt() {
log.Printf("[prompts] goals 提示词已追加新默认版本(加入抽操作约束步,一次性)")
}
// reseedMainAgentPrompt 把 mainagent 提示词刷成【当前代码默认】——默认正文新增了「目标全部
// 达成后 add_intent 直投意图时,反问人是否登记为正式目标」这段引导,而 SeedPromptIfEmpty 首插入
// only,旧库已有版本收不到。用版本管理【追加一个新版本】并切过去(ResetPromptToDefault),旧版本仍
// 保留在历史里,用户若自定义过可从版本记录找回。settings flag 守卫 → 只做一次。全新库无需处理
// (SeedPromptIfEmpty 已 seed 最新默认)。与 reseedGoalsPrompt 完全同构。
func (s *Server) reseedMainAgentPrompt() {
const flag = "mainagent_prompt_goalless_intent_v1"
if v, _, _ := s.m.pg.GetSetting(flag); v == "true" {
return
}
defer func() { _ = s.m.pg.SetSetting(flag, "true") }() // 无论成功与否只尝试一次
a, err := s.m.pg.GetAgentByKey("mainagent")
if err != nil || a == nil {
return // 全新库尚未建 agent 行时,seedPrompts 会直接 seed 最新默认,无需此迁移
}
tmpl := agent.BuiltinPromptSeeds()["mainagent"]
if tmpl == "" {
return
}
// 全新库 seedPrompts 已 seed 最新默认 → 当前版本已等于代码默认,不必再追加重复版本。
if cur, err := s.m.pg.CurrentPrompt(a.ID); err == nil && cur == tmpl {
return
}
if _, err := s.m.pg.ResetPromptToDefault(a.ID, tmpl); err != nil {
log.Printf("[prompts] mainagent 提示词重刷为新默认失败: %v", err)
return
}
log.Printf("[prompts] mainagent 提示词已追加新默认版本(加入目标达成后反问建目标,一次性)")
}
// seedReporterAgent 预置一个「报告撰写」自定义 agent(builtin=false,可在 UI 编辑/删除):
// 绑定 update_finding_report + 任务查询工具,并挂一个「report_finding 被调用即触发」的
// 触发器 —— 每登记一个漏洞就唤起它写详细报告。一次性(settings flag 守卫):用户删掉后不再重建。