- 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 用法。
132 lines
6.9 KiB
Go
132 lines
6.9 KiB
Go
package agent
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
|
||
"github.com/Autumn-27/artex/db"
|
||
"github.com/Autumn-27/norma/agentcore"
|
||
"github.com/Autumn-27/norma/llm"
|
||
"github.com/Autumn-27/norma/permission"
|
||
actool "github.com/Autumn-27/norma/tool"
|
||
"github.com/Autumn-27/norma/transcript"
|
||
)
|
||
|
||
// MainAgent is the thin human-interface orchestrator (docs §4.2 / §7). The human
|
||
// chats with it; it observes (read tools), and steers by injecting hints
|
||
// (→planner) or direct high-priority intents (→frontier). It does NOT run the
|
||
// autonomous intent-generation loop (that is the planner's job).
|
||
type MainAgent struct {
|
||
prov llm.Provider
|
||
model string
|
||
tx *transcript.Store // raw LLM conversation persistence (nil = off)
|
||
window int // context window in tokens (for compaction)
|
||
maxTurns int // max agent turns per run (0 = unlimited)
|
||
proxyAddr string // recording proxy for WebFetch (empty = direct)
|
||
proxyCACert string // recording proxy's CA cert path (HTTPS verify)
|
||
webSearch WebSearchOpts // web_search tool backend selection (off by default)
|
||
workDir string // shared work dir (surfaced in prompt as artifact-output target)
|
||
}
|
||
|
||
func NewMainAgent(prov llm.Provider, model, workDir string, tx *transcript.Store, window, maxTurns int) *MainAgent {
|
||
return &MainAgent{prov: prov, model: model, workDir: workDir, tx: tx, window: window, maxTurns: maxTurns}
|
||
}
|
||
|
||
// SetProxy points the main agent's WebFetch at the recording proxy plus the CA
|
||
// cert it trusts to verify HTTPS through it (empty addr = direct).
|
||
func (m *MainAgent) SetProxy(addr, caCert string) { m.proxyAddr, m.proxyCACert = addr, caCert }
|
||
|
||
// SetWebSearch selects the web_search backend for the main agent (off by default).
|
||
func (m *MainAgent) SetWebSearch(o WebSearchOpts) { m.webSearch = o }
|
||
|
||
// mainAgentDefaultTmpl is the built-in EDITABLE body (段 [A]) of the main agent
|
||
// prompt, seeded into agent_prompts. Goal is a {{.Goal}} template var; the 中间
|
||
// 产物输出规约 tail is code-owned (artifactSpec), appended after rendering.
|
||
const mainAgentDefaultTmpl = `你是一个授权渗透测试系统的"主 agent",是人类操作员的接口。你不亲自探索、也不自主连续生成意图(那是规划者的工作)。你的职责:
|
||
|
||
1. 观察:用 graph_overview / list_findings / list_facts / list_assets / get_worker_output 回答人关于当前进展的问题。
|
||
2. 操舵(把人的意图落到系统):
|
||
- 人想"改方向/强调某类漏洞/重点某区域" → 用 add_hint 写提示(规划者下次会读到)。
|
||
- 人想"立刻测某个具体目标" → 用 add_intent 直接注入一条高优先级意图(priority 8-10)。
|
||
- 人想"新增一个要达成的最终目标" → 用 set_goals 增补目标。系统会把该目标写入任务图并**自动把已完成/暂停的任务拉回运行态继续跑**(规划者随后会据此重新判断是否达成),无需人工再点恢复。
|
||
3. 用人话简洁回复,说明你做了什么。
|
||
|
||
当前任务目标:{{.Goal}}
|
||
|
||
不要编造发现;只根据工具返回的真实数据回答。`
|
||
|
||
func mainAgentSystem(goal, dataDir, workDir string) string {
|
||
body := renderSystem("mainagent", mainAgentDefaultTmpl, MainVars{Goal: goal, DataDir: dataDir, Now: nowStr()})
|
||
return body + artifactSpec(workDir)
|
||
}
|
||
|
||
// Chat handles one human message and returns the assistant reply. emit, if
|
||
// 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) {
|
||
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
|
||
// 领域工具 + 基础默认工具集(Read/Write/Edit/MultiEdit/LS/Glob/Grep/Bash)
|
||
base := append(tsx.MainAgentTools(), actool.DefaultTools()...)
|
||
tools, def, cleanup := AugmentTools(ctx, "mainagent", base)
|
||
defer cleanup()
|
||
// 本任务的工作目录 <workDir>/<taskID>,先建好。
|
||
mainDir := ensureRunDir(m.workDir, taskID, 0)
|
||
system, boundary := deferredSystem(mainAgentSystem(goal, m.workDir, mainDir), def)
|
||
opts := agentcore.Options{
|
||
Provider: m.prov,
|
||
SystemPrompt: system,
|
||
DynamicBoundary: boundary,
|
||
Tools: tools,
|
||
DeferredTools: def.Deferred,
|
||
UnlockSet: def.Unlock,
|
||
PermissionMode: permission.ModeBypass,
|
||
EnableWebFetch: true, // 走记录代理留痕;载入代理 CA 验证 MITM 重签的 HTTPS 证书
|
||
WebFetchProxy: m.proxyAddr,
|
||
WebFetchCACert: m.proxyCACert,
|
||
// 联网搜索(可选)。ddgs 无需 key;brave-free 需 BraveKey;tavily 需 TavilyKey。
|
||
// WebSearchProxy 是独立出口代理(http/https/socks5),与记录流量的 MITM 代理无关;空则直连。
|
||
EnableWebSearch: m.webSearch.Enabled,
|
||
WebSearchBackend: m.webSearch.Backend,
|
||
BraveSearchAPIKey: m.webSearch.BraveKey,
|
||
TavilySearchAPIKey: m.webSearch.TavilyKey,
|
||
WebSearchProxy: m.webSearch.Proxy,
|
||
BashEnv: proxyEnv(m.proxyAddr, m.proxyCACert), // Bash 子命令默认走代理+信任 CA
|
||
WorkingDir: mainDir, // 本任务工作目录 <workDir>/<taskID>
|
||
ToolOutputDir: cmdOutDir(mainDir),
|
||
MaxTurns: m.maxTurns, // 0 = unlimited (configurable in agent management)
|
||
Compaction: compactionConfig(m.window), // long chats stay within the window
|
||
Todos: actool.NewTodoStore(), // 会话级临时待办(TodoWrite),纯规划用,退出即丢
|
||
// 命中预算(步数)→ SDK 跑收尾:向用户输出一句进展总结。Prompt 与收尾轮数可后台编辑(默认 5 轮)。
|
||
Settlement: wrapupSettlement("mainagent", nil),
|
||
}
|
||
if m.tx != nil { // persist raw human↔AI conversation; one accumulating file per task
|
||
opts.Transcript = m.tx
|
||
opts.SessionID = fmt.Sprintf("exp%d-main", ts.ID())
|
||
}
|
||
s := agentcore.NewSession(opts)
|
||
defer s.Close()
|
||
// reload the prior conversation from the transcript so the agent has context
|
||
// across turns (each Chat is a fresh session; without this it can't see earlier
|
||
// messages). First turn: no file yet → Resume loads nothing and proceeds.
|
||
if m.tx != nil {
|
||
_ = s.Resume(opts.SessionID)
|
||
}
|
||
// C2: this session is fresh each turn; re-unlock skill-gated MCPs from prior
|
||
// Skill() calls in the reloaded history so revealed tools stay callable.
|
||
seedUnlockFromHistory(s.Messages(), def.UnlockSkill)
|
||
text, _, err := captureRunSession(ctx, s, message, func(r db.Activity) {
|
||
if emit != nil {
|
||
r.Worker = "mainagent"
|
||
emit(r)
|
||
}
|
||
})
|
||
return text, err
|
||
}
|