From edbc296e291e4ff9098c434a6e9731af075b1cc6 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Sun, 13 Sep 2026 11:36:31 -0400 Subject: [PATCH] =?UTF-8?q?feat(agent):=20add=5Fhint=20=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E7=9A=84=E8=A7=84=E5=88=92=E8=BD=AE=E6=98=BE=E5=BC=8F=E6=92=AD?= =?UTF-8?q?=E6=8A=A5=E3=80=8C=E6=9C=AC=E8=BD=AE=E7=94=B1=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=20hint=20=E8=A7=A6=E5=8F=91=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前 add_hint 走裸 notify,触发的规划轮 triggers 为空,开场白说"无具体变动", hint 只折在 graph_overview 里让 planner 自己发现。仿照 set_goals/NotifyGoal, 让 add_hint 记一条 hint 触发事件并带上提示文本。 - TriggerEvent 增加 Hints 字段;renderTriggers 增加 hint 分支,播报「人新增了 N 条战略提示:…」 - ToolSet 增加 notifyHint 回调 + SetNotifyHint;add_hint 整批写完统一唤醒一次 (优先 notifyHint,未接则退回裸 notify),notify 从 addOneHint 上移到 handler - 主 agent(server.go) 与跨任务编排(orchestration.go) 均接上 Task.NotifyHint --- agent/mainagent.go | 5 +++-- agent/planner.go | 7 +++++++ agent/tools.go | 32 +++++++++++++++++++++++++++----- server/manager.go | 15 +++++++++++++++ server/orchestration.go | 3 ++- server/server.go | 2 +- 6 files changed, 55 insertions(+), 9 deletions(-) diff --git a/agent/mainagent.go b/agent/mainagent.go index 513706d..05211f2 100644 --- a/agent/mainagent.go +++ b/agent/mainagent.go @@ -102,7 +102,7 @@ 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, mainSeg int, as *db.AssetStore, ts *db.ExplorationStore, goal, message string, emit func(db.Activity), notify, resume func(), notifyGoal func([]string)) (string, error) { +func (m *MainAgent) Chat(ctx context.Context, taskID int64, mainSeg int, as *db.AssetStore, ts *db.ExplorationStore, goal, message string, emit func(db.Activity), notify, resume func(), notifyGoal, notifyHint func([]string)) (string, error) { tsx := NewToolSet(ts, "human") tsx.SetFindingRecorder(m.findingRecorder) if as != nil { @@ -110,9 +110,10 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, mainSeg int, as *db. } tsx.SetTaskID(taskID) tsx.SetCoverageEnabled(as == nil || as.CoverageEnabled(taskID)) - tsx.SetNotify(notify) // add_hint wakes this task's planner (debounced) + tsx.SetNotify(notify) // 通用唤醒(无专用回调的写操作走它,debounced) tsx.SetResumeTask(resume) // set_goals 新增目标 → 把已完成/暂停的任务拉回 running tsx.SetNotifyGoal(notifyGoal) // set_goals 新增目标 → 给 planner 记一条「人新增了目标:…」触发 + tsx.SetNotifyHint(notifyHint) // add_hint 新增提示 → 给 planner 记一条「人新增了 N 条战略提示:…」触发 tsx.steerWork = m.steerWork // enable steer_work tool (nil = unavailable) // 领域工具 + 基础默认工具集(Read/Write/Edit/MultiEdit/LS/Glob/Grep/Bash) // 资产覆盖度功能关闭时剔除 add_task_scope/list_untested_assets(不入 prompt)。 diff --git a/agent/planner.go b/agent/planner.go index 7e2651b..2402728 100644 --- a/agent/planner.go +++ b/agent/planner.go @@ -155,6 +155,7 @@ type TriggerEvent struct { Goals []string // Kind=="goal" 专用:本次 set_goals 新增的目标文本(1 条或多条) OldGoal string // Kind=="goal_edited" 专用:修改前的目标文本 NewGoal string // Kind=="goal_edited" 专用:修改后的目标文本 + Hints []string // Kind=="hint" 专用:本次 add_hint 新增的提示文本(1 条或多条) } // renderTriggers spells out the change(s) that fired this round: for a finished @@ -175,6 +176,12 @@ func renderTriggers(ts *db.ExplorationStore, evs []TriggerEvent) string { } else { b.WriteString(fmt.Sprintf("\n- 人(主 agent)新增了 %d 个目标:%s —— 均为新的待达成目标,请逐一为尚无对应意图的目标补充探索方向。", len(ev.Goals), strings.Join(ev.Goals, ";"))) } + case "hint": + if len(ev.Hints) == 1 { + b.WriteString(fmt.Sprintf("\n- 人(主 agent)新增了一条战略提示:%s —— 已挂到探索图上,请据此调整/补充探索方向(若尚无对应意图)。", ev.Hints[0])) + } else { + b.WriteString(fmt.Sprintf("\n- 人(主 agent)新增了 %d 条战略提示:%s —— 均已挂到探索图上,请逐一据此调整/补充探索方向。", len(ev.Hints), strings.Join(ev.Hints, ";"))) + } case "goal_deleted": b.WriteString(fmt.Sprintf("\n- 人删除了该目标:%s —— 该目标已移除,请据此重判剩余目标/方向(不必再为它派意图)。", ev.Detail)) case "goal_edited": diff --git a/agent/tools.go b/agent/tools.go index 2974ab9..60957c7 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -123,12 +123,23 @@ type ToolSet struct { // goals decomposer (round-0 has no running planner to inform) and workers → those // fall back to the bare notify. notifyGoal func(texts []string) + // notifyHint, if set, wakes the planner AND records ONE "人新增了 N 条战略提示:…" + // trigger for a whole add_hint call (batch-aware — one call, one trigger) so the next + // round is told the round was fired by a new hint and spells the hint out, instead of + // the planner having to spot it folded into the graph overview. Wired for the main + // agent + cross-task orchestration; nil elsewhere → falls back to the bare notify. + notifyHint 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 } +// SetNotifyHint wires the hint-add trigger callback (see ToolSet.notifyHint). Set by +// the main-agent chat and cross-task orchestration, so a runtime-added hint fires a +// planner round announced by name instead of a bare wake. +func (t *ToolSet) SetNotifyHint(fn func([]string)) { t.notifyHint = 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 } @@ -1540,11 +1551,9 @@ func (t *ToolSet) addOneHint(it hintItem) (int64, error) { if len(refs) > 0 { payload["traffic_refs"] = refs } - id, err := t.ts.AddNode(db.KindHint, payload, 0, "active", "human", anchors) - if err == nil && t.notify != nil { - t.notify() // wake the planner so the new hint is read promptly (debounced) - } - return id, err + // 唤醒 planner 不在此处逐条做——由 addHint 在整批写完后统一触发一次(带上提示文本), + // 避免一次 add_hint 多条提示逐条刷屏 planner 的触发行。 + return t.ts.AddNode(db.KindHint, payload, 0, "active", "human", anchors) } type goalItem struct { @@ -1745,6 +1754,7 @@ func (t *ToolSet) addHint() actool.CoreTool { ids := make([]int64, len(items)) errs := map[string]string{} + var addedTexts []string for i, it := range items { id, err := t.addOneHint(it) if err != nil { @@ -1752,6 +1762,18 @@ func (t *ToolSet) addHint() actool.CoreTool { continue } ids[i] = id + addedTexts = append(addedTexts, strings.TrimSpace(it.Text)) + } + if len(addedTexts) > 0 { + // 唤醒 planner(整批一次)。优先 notifyHint:一次 add_hint 记一条「人新增了 + // N 条战略提示:…」触发,让 planner 明确"本轮由新增 hint 触发"并看到提示内容; + // 未接该回调时退回纯 notify(bare wake,hint 仍折在图里供其自行读取)。 + switch { + case t.notifyHint != nil: + t.notifyHint(addedTexts) + case t.notify != nil: + t.notify() + } } if !batch { // 单条:保持原返回 diff --git a/server/manager.go b/server/manager.go index 5101e6f..fc3542e 100644 --- a/server/manager.go +++ b/server/manager.go @@ -1776,6 +1776,21 @@ func (t *Task) NotifyGoal(texts []string) { t.Notify() } +// NotifyHint records that one OR MORE hints were added in a single add_hint call — +// by the human via the main agent, or by cross-task orchestration — then wakes the +// planner, so the next round is told "人新增了 N 条战略提示:…" and looks at them +// directly instead of having to spot the new hint folded into the graph overview. +// One call → one trigger event (a batched add_hint counts as one, not one per hint). +func (t *Task) NotifyHint(texts []string) { + if len(texts) == 0 { + return + } + t.trigMu.Lock() + t.pendingTriggers = append(t.pendingTriggers, agent.TriggerEvent{Kind: "hint", Hints: texts}) + t.trigMu.Unlock() + t.Notify() +} + // NotifyGoalDeleted records that the human deleted a goal (via 总览的目标管理), then // wakes the planner so the next round spells out which goal was removed. The event // survives an early-returning terminal round (drain happens after the gate). diff --git a/server/orchestration.go b/server/orchestration.go index 2768285..c5baa9b 100644 --- a/server/orchestration.go +++ b/server/orchestration.go @@ -166,7 +166,8 @@ func (s *Server) delegateToTask(ctx context.Context, in json.RawMessage, pick fu if s.m.Assets() != nil { tsx.SetAssetStore(s.m.Assets(), s.m.Assets().Companies()) } - tsx.SetNotify(t.Notify) // hint writes wake this task's planner (no-op for read tools) + tsx.SetNotify(t.Notify) // 通用唤醒(无专用回调的写操作走它;读工具为 no-op) + tsx.SetNotifyHint(t.NotifyHint) // add_hint → 记一条「人新增了 N 条战略提示:…」触发并唤醒 planner return pick(tsx).Call(ctx, inner, nil) } diff --git a/server/server.go b/server/server.go index 9a867d5..16a978f 100644 --- a/server/server.go +++ b/server/server.go @@ -3571,7 +3571,7 @@ func (s *Server) chat(w http.ResponseWriter, r *http.Request) { taskDir := filepath.Join(s.m.dir, "tasks", t.ID) agentMsg := composeAgentMessage(req.Message, req.Attachments, taskDir) s.engine.BeginLLMCall(t.ID) - _, err := ma.Chat(ctx, maTaskID, mainSeg, s.m.Assets(), t.Store, t.Goal, agentMsg, emit, t.Notify, resume, t.NotifyGoal) + _, err := ma.Chat(ctx, maTaskID, mainSeg, s.m.Assets(), t.Store, t.Goal, agentMsg, emit, t.Notify, resume, t.NotifyGoal, t.NotifyHint) s.engine.EndLLMCall(t.ID) if err != nil && ctx.Err() == nil { s.engine.emitActivity(t, db.Activity{Worker: "mainagent", Kind: "text", IsError: true, Summary: "(主 Agent 出错:" + err.Error() + ")", MainSeg: segPtr})