feat: set_goals 升级为受管领域工具(可 web 管理,默认绑 goals+mainagent)+ 主 agent 加目标复活任务

- set_goals 从目标拆解器内联提交工具,改为入 tools 目录的领域工具:直接写 goal 节点、
  连任务根(RelSpawns)、notify 唤醒 planner;支持单条/批量;origin 按来源区分
  (拆解器=goals、主 agent 运行时=human)。新 key 走 SeedTool 首插入,旧库无需迁移。
- 默认绑定 goals + mainagent:目录 seed 新增 goals 项并入 order,MainAgentTools 纳入;
  DecomposeGoals 统一改用同一受管工具直接落库,createGoals 只保留无产出兜底。
- 主 agent 通过 set_goals 新增目标时,把终态(done/failed/timeout)/暂停的任务拉回
  running 继续跑:新增 ToolSet.resumeTask 回调(仅 mainagent 接线),经 resumeTaskForGoal
  置 running + engine.Run(幂等/重启后重起循环)+ 解除暂停。拆解器/worker 不触发。
- mainagent 提示词补充 set_goals 用法。
This commit is contained in:
Autumn-27 committed 2026-08-17 09:01:19 -04:00
1 parent 1029c4a84c
commit 2ac176b96a
7 files changed
+184 -45

No files matched your search

+103
View File
@@ -77,8 +77,18 @@ type ToolSet struct {
// carrying (intentID, summary) so the round can spell out which intent found what.
// Wired for workers; nil elsewhere → falls back to notify (bare wake).
notifyFinding func(intentID int64, summary string)
// resumeTask, if set, revives the task after a graph change that should make a
// stopped task run again (currently: set_goals adds a goal). It flips a terminal/
// paused task back to running and (re)starts the engine loops — a plain notify()
// can't, because the planner's terminal gate swallows wakes. Wired ONLY for the
// main agent (human steering); nil for the goals decomposer and workers.
resumeTask func()
}
// SetResumeTask wires the task-revive callback (see ToolSet.resumeTask). Set only by
// the main-agent chat, so runtime-added goals can pull a finished task back to running.
func (t *ToolSet) SetResumeTask(fn func()) { t.resumeTask = fn }
// SetNotify wires the planner-wake callback (see ToolSet.notify). Set by callers
// that hold the task handle (main-agent chat, cross-task orchestration).
func (t *ToolSet) SetNotify(fn func()) { t.notify = fn }
@@ -799,6 +809,99 @@ func (t *ToolSet) addOneHint(it hintItem) (int64, error) {
return id, err
}
type goalItem struct {
Text string `json:"text"`
VulnClass string `json:"vulnclass"`
}
// addOneGoal 挂一条 goal 节点(open)到探索图:连到任务根(origin fact,rel spawns),
// 并唤醒 planner(运行时新增的目标要尽快被判定)。origin 取 t.worker(缺省 system):
// goals 拆解器写入的记 "goals"、主 agent 运行时新增的记 "human"。
func (t *ToolSet) addOneGoal(it goalItem) (int64, error) {
text := strings.TrimSpace(it.Text)
if text == "" {
return 0, fmt.Errorf("text 不能为空")
}
payload := map[string]any{"text": text}
if vc := strings.TrimSpace(it.VulnClass); vc != "" {
payload["vulnclass"] = vc
}
origin := t.worker
if origin == "" {
origin = "system"
}
id, err := t.ts.AddNode(db.KindGoal, payload, 0, "open", origin, nil)
if err != nil {
return 0, err
}
if of, _ := t.ts.OriginFactID(); of > 0 && id > 0 {
_ = t.ts.Link(of, db.RelSpawns, id) // goals descend from the task root (origin fact)
}
if t.notify != nil {
t.notify() // wake the planner so a runtime-added goal is judged promptly (debounced)
}
return id, nil
}
// setGoals 给【本任务】新增探索目标(goal 节点)。既是目标拆解器的提交工具,也是主
// agent 运行时补目标的工具——同一个受管工具,可在 web 端改描述/schema、按 agent 绑定。
func (t *ToolSet) setGoals() actool.CoreTool {
return writeTool("set_goals",
"给【本任务】新增探索目标(goal)。目标=最终可交付/可核验的结果,不是攻击步骤或侦察动作。\n"+
"★优先批量:多个目标放进 goals 数组一次提交,返回 ids 与之等长同序(失败项 id=0,详情见 errors)。单条则省略 goals 直接给顶层 text。\n"+
"vulnclass 可选:对应漏洞类(如 SQLi/IDOR),业务逻辑类目标留空。目标是否达成由系统判定标记 met,本工具只负责新增。",
obj(map[string]any{
"goals": map[string]any{"type": "array", "description": "【优先用这个】要新增的目标数组,按顺序处理。每个元素:text(必填,一个独立可验证的最终目标)+ vulnclass(可选)。返回 ids 与本数组等长、同序。", "items": map[string]any{"type": "object"}},
"text": str("[单条] 一个独立可验证的最终目标"),
"vulnclass": str("[单条] 对应漏洞类(若明确),如 SQLi/IDOR;业务逻辑目标可留空"),
}),
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
if t.ts == nil {
return actool.Errorf("set_goals 未启用: ExplorationStore 未初始化"), nil
}
var a struct {
Goals []goalItem `json:"goals"`
goalItem // 单条模式:顶层 text/vulnclass
}
_ = json.Unmarshal(in, &a)
batch := len(a.Goals) > 0
items := a.Goals
if !batch {
items = []goalItem{a.goalItem}
}
ids := make([]int64, len(items))
errs := map[string]string{}
added := 0
for i, it := range items {
id, err := t.addOneGoal(it)
if err != nil {
errs[strconv.Itoa(i)] = err.Error()
continue
}
ids[i] = id
added++
}
// 主 agent 运行时新增目标 → 把已完成/暂停的任务拉回 running 继续跑(终态门会吞
// 掉普通 notify,必须显式复活)。仅 mainagent 接了此回调;拆解器/worker 为 nil。
if added > 0 && t.resumeTask != nil {
t.resumeTask()
}
if !batch { // 单条:保持原返回
if e, bad := errs["0"]; bad {
return actool.Errorf(e), nil
}
return actool.Text(fmt.Sprintf("goal added: %d", ids[0])), nil
}
out := map[string]any{"ids": ids}
if len(errs) > 0 {
out["errors"] = errs
}
return jsonResult(out)
})
}
func (t *ToolSet) addHint() actool.CoreTool {
return writeTool("add_hint", "把人类/主 agent 的战略提示挂到探索图,规划者下次生成意图时会读到它。\n"+
"★优先批量:多条提示放进 hints 数组一次提交(比逐条调用省往返)。返回 ids 数组,与 hints 等长同序(失败项 id=0,详情见 errors)。单条则省略 hints 直接给顶层 text。",