From 8e030435486f2225cde5df75ca23b6a0b8dd0324 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:12:25 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BB=20agent=20set=5Fgoals=20?= =?UTF-8?q?=E5=8A=A0=E7=9B=AE=E6=A0=87=E6=97=B6=E7=BB=99=20planner=20?= =?UTF-8?q?=E8=AE=B0=E3=80=8C=E4=BA=BA=E6=96=B0=E5=A2=9E=E4=BA=86=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E3=80=8D=E8=A7=A6=E5=8F=91(=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=84=9F=E7=9F=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 TriggerEvent{Kind:"goal", Goals:[]string} + Task.NotifyGoal([]string): 一次 set_goals(不论单条/数组)合并成一条触发,planner 下轮开头据 len 渲染 「人新增了一个目标:X」或「人新增了 N 个目标:A;B;C」,不再靠翻 goals 列表发现。 - set_goals 工具:addOneGoal 退化为纯落库;唤醒/触发/复活挪到整批写完后统一做 (notifyGoal(addedTexts) 一次 + resumeTask 一次)。仅 mainagent 接 notifyGoal; 拆解器/worker 退回纯 notify,round-0 不受影响。 - 复活场景顺序仍成立:goal 触发先入队(终态门在 drain 之前不会吞),resumeTask 置 running 后那一轮才 drain 渲染。mainagent.Chat 加 notifyGoal 入参。 --- agent/mainagent.go | 7 ++++--- agent/planner.go | 14 +++++++++++--- agent/tools.go | 43 +++++++++++++++++++++++++++++++------------ server/manager.go | 16 ++++++++++++++++ server/server.go | 2 +- 5 files changed, 63 insertions(+), 19 deletions(-) diff --git a/agent/mainagent.go b/agent/mainagent.go index 0d440b7..9406569 100644 --- a/agent/mainagent.go +++ b/agent/mainagent.go @@ -64,14 +64,15 @@ func mainAgentSystem(goal, dataDir, workDir string) string { // non-nil, receives each execution step (thinking / tool_use / tool_result / // text / result) so the main-agent session shows its work — exactly like the // worker/planner sessions — not just the final answer. -func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, ts *db.ExplorationStore, goal, message string, emit func(db.Activity), notify, resume func()) (string, error) { +func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, ts *db.ExplorationStore, goal, message string, emit func(db.Activity), notify, resume func(), notifyGoal func([]string)) (string, error) { tsx := NewToolSet(ts, "human") if as != nil { tsx.SetAssetStore(as, as.Companies()) } tsx.SetTaskID(taskID) - tsx.SetNotify(notify) // add_hint wakes this task's planner (debounced) - tsx.SetResumeTask(resume) // set_goals 新增目标 → 把已完成/暂停的任务拉回 running + tsx.SetNotify(notify) // add_hint wakes this task's planner (debounced) + tsx.SetResumeTask(resume) // set_goals 新增目标 → 把已完成/暂停的任务拉回 running + tsx.SetNotifyGoal(notifyGoal) // set_goals 新增目标 → 给 planner 记一条「人新增了目标:…」触发 // 领域工具 + 基础默认工具集(Read/Write/Edit/MultiEdit/LS/Glob/Grep/Bash) base := append(tsx.MainAgentTools(), actool.DefaultTools()...) tools, def, cleanup := AugmentTools(ctx, "mainagent", base) diff --git a/agent/planner.go b/agent/planner.go index ba297c6..6ecd582 100644 --- a/agent/planner.go +++ b/agent/planner.go @@ -96,10 +96,13 @@ func renderPlannerTodos(items []actool.Todo) string { // overview. Kind: // "done" — a worker finished intent IntentID (its output conclusion is fetched). // "finding" — a worker reported a finding on intent IntentID (Detail = 摘要). +// "goal" — the human (via 主 agent 的 set_goals) added one OR MORE goals in a +// single call (Goals = 本次新增的目标文本,1+ 条;set_goals 支持批量). type TriggerEvent struct { Kind string IntentID int64 Detail string + Goals []string // Kind=="goal" 专用:本次 set_goals 新增的目标文本(1 条或多条) } // renderTriggers spells out the change(s) that fired this round: for a finished @@ -113,12 +116,17 @@ func renderTriggers(ts *db.ExplorationStore, evs []TriggerEvent) string { var b strings.Builder b.WriteString("\n\n【本次触发本轮的实际变动(先看这里,再决定是否补方向)】:") for _, ev := range evs { - sum := intentSummary(ts, ev.IntentID) switch ev.Kind { + case "goal": + if len(ev.Goals) == 1 { + b.WriteString(fmt.Sprintf("\n- 人(主 agent)新增了一个目标:%s —— 新的待达成目标,请据此补充探索方向(若尚无对应意图)。", ev.Goals[0])) + } else { + b.WriteString(fmt.Sprintf("\n- 人(主 agent)新增了 %d 个目标:%s —— 均为新的待达成目标,请逐一为尚无对应意图的目标补充探索方向。", len(ev.Goals), strings.Join(ev.Goals, ";"))) + } case "finding": - b.WriteString(fmt.Sprintf("\n- 意图 #%d(%s)的 worker 报告了一个 finding:%s", ev.IntentID, sum, ev.Detail)) + b.WriteString(fmt.Sprintf("\n- 意图 #%d(%s)的 worker 报告了一个 finding:%s", ev.IntentID, intentSummary(ts, ev.IntentID), ev.Detail)) default: // "done" - b.WriteString(fmt.Sprintf("\n- 意图 #%d(%s)的 worker 结束,输出结论:%s", ev.IntentID, sum, workerOutput(ts, ev.IntentID))) + b.WriteString(fmt.Sprintf("\n- 意图 #%d(%s)的 worker 结束,输出结论:%s", ev.IntentID, intentSummary(ts, ev.IntentID), workerOutput(ts, ev.IntentID))) } } b.WriteString("\n(完整细节可 node_detail / get_worker_output / list_findings 再查。)") diff --git a/agent/tools.go b/agent/tools.go index 3430ef4..d45d8bc 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -83,8 +83,19 @@ type ToolSet struct { // 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() + // notifyGoal, if set, wakes the planner AND records ONE "人新增了 N 个目标:…" trigger + // for a whole set_goals call (batch-aware — one call, one trigger, not one per goal) + // so the next round spells out the added goals (instead of the planner having to + // spot new open goals in the overview). Wired ONLY for the main agent; nil for the + // goals decomposer (round-0 has no running planner to inform) and workers → those + // fall back to the bare notify. + notifyGoal func(texts []string) } +// SetNotifyGoal wires the goal-add trigger callback (see ToolSet.notifyGoal). Set only +// by the main-agent chat, so runtime-added goals are announced to the planner by name. +func (t *ToolSet) SetNotifyGoal(fn func([]string)) { t.notifyGoal = fn } + // 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 } @@ -814,9 +825,9 @@ type goalItem struct { VulnClass string `json:"vulnclass"` } -// addOneGoal 挂一条 goal 节点(open)到探索图:连到任务根(origin fact,rel spawns), -// 并唤醒 planner(运行时新增的目标要尽快被判定)。origin 取 t.worker(缺省 system): -// goals 拆解器写入的记 "goals"、主 agent 运行时新增的记 "human"。 +// addOneGoal 挂一条 goal 节点(open)到探索图:连到任务根(origin fact,rel spawns)。 +// origin 取 t.worker(缺省 system):goals 拆解器写入的记 "goals"、主 agent 运行时记 +// "human"。唤醒 planner 由 setGoals 在整批写完后统一做(见下),这里只负责落库。 func (t *ToolSet) addOneGoal(it goalItem) (int64, error) { text := strings.TrimSpace(it.Text) if text == "" { @@ -837,9 +848,6 @@ func (t *ToolSet) addOneGoal(it goalItem) (int64, error) { 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 } @@ -872,7 +880,7 @@ func (t *ToolSet) setGoals() actool.CoreTool { ids := make([]int64, len(items)) errs := map[string]string{} - added := 0 + var addedTexts []string for i, it := range items { id, err := t.addOneGoal(it) if err != nil { @@ -880,12 +888,23 @@ func (t *ToolSet) setGoals() actool.CoreTool { continue } ids[i] = id - added++ + addedTexts = append(addedTexts, strings.TrimSpace(it.Text)) } - // 主 agent 运行时新增目标 → 把已完成/暂停的任务拉回 running 继续跑(终态门会吞 - // 掉普通 notify,必须显式复活)。仅 mainagent 接了此回调;拆解器/worker 为 nil。 - if added > 0 && t.resumeTask != nil { - t.resumeTask() + if len(addedTexts) > 0 { + // 唤醒 planner(整批一次)。优先 notifyGoal:一次 set_goals 记一条「人新增了 + // N 个目标:…」触发,不逐条刷屏;拆解器/worker 无此回调 → 退回纯 notify(拆解器 + // round-0 连 notify 也没接,即无操作,因为此时 planner 尚未启动)。 + switch { + case t.notifyGoal != nil: + t.notifyGoal(addedTexts) + case t.notify != nil: + t.notify() + } + // 主 agent 运行时新增目标 → 把已完成/暂停的任务拉回 running 继续跑(终态门会 + // 吞掉普通 notify,必须显式复活)。仅 mainagent 接了此回调;拆解器/worker 为 nil。 + if t.resumeTask != nil { + t.resumeTask() + } } if !batch { // 单条:保持原返回 diff --git a/server/manager.go b/server/manager.go index e37b73b..63b4091 100644 --- a/server/manager.go +++ b/server/manager.go @@ -718,6 +718,22 @@ func (t *Task) NotifyFinding(intentID int64, summary string) { t.Notify() } +// NotifyGoal records that one OR MORE goals were added in a single set_goals call — +// by the human via the main agent — then wakes the planner, so the next round spells +// out "人新增了 N 个目标:…" instead of the planner having to spot new open goals in +// the overview. One call → one trigger event (set_goals 的一次批量算一条,不逐条刷屏). +// The event survives an early-returning terminal round (drain happens after the gate), +// so a set_goals that revives a done task still surfaces it once the task is running. +func (t *Task) NotifyGoal(texts []string) { + if len(texts) == 0 { + return + } + t.trigMu.Lock() + t.pendingTriggers = append(t.pendingTriggers, agent.TriggerEvent{Kind: "goal", Goals: texts}) + t.trigMu.Unlock() + t.Notify() +} + // drainTriggers returns and clears the trigger events accumulated since the last round. func (t *Task) drainTriggers() []agent.TriggerEvent { t.trigMu.Lock() diff --git a/server/server.go b/server/server.go index 0a13c12..f12185c 100644 --- a/server/server.go +++ b/server/server.go @@ -2053,7 +2053,7 @@ func (s *Server) chat(w http.ResponseWriter, r *http.Request) { emit := func(rec db.Activity) { s.engine.emitActivity(t, rec) } maTaskID, _ := strconv.ParseInt(t.ID, 10, 64) resume := func() { s.resumeTaskForGoal(t) } // set_goals 新增目标 → 把任务拉回 running - if _, err := ma.Chat(ctx, maTaskID, s.m.Assets(), t.Store, t.Goal, req.Message, emit, t.Notify, resume); err != nil && ctx.Err() == nil { + if _, err := ma.Chat(ctx, maTaskID, s.m.Assets(), t.Store, t.Goal, req.Message, emit, t.Notify, resume, t.NotifyGoal); err != nil && ctx.Err() == nil { s.engine.emitActivity(t, db.Activity{Worker: "mainagent", Kind: "text", IsError: true, Summary: "(主 Agent 出错:" + err.Error() + ")"}) } }()