feat: 主 agent set_goals 加目标时给 planner 记「人新增了目标」触发(批量感知)
- 新增 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 入参。
This commit is contained in:
1 parent
3c81d3d248
commit
8e03043548
5 files changed
+63
-19
No files matched your search
+4
-3
@@ -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)
|
||||
|
||||
+11
-3
@@ -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 再查。)")
|
||||
|
||||
+31
-12
@@ -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 { // 单条:保持原返回
|
||||
|
||||
@@ -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()
|
||||
|
||||
+1
-1
@@ -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() + ")"})
|
||||
}
|
||||
}()
|
||||
|
||||
Reference in new issue
Block a user