feat: 提示词新增全局变量 DataDir + 触发合并/种子意图默认值调整

- prompt: 新增全局运行时变量 {{.DataDir}}(服务端数据根目录 s.m.dir),
  与 {{.Now}} 一样对所有 agent(planner/worker/mainagent/goals/chat 及自定义)可用;
  各 Vars 结构体加 DataDir 字段并在渲染时填入根 workDir,globalPromptVars 登记以便
  校验/UI 变量列表/预览识别(纯运行时变量,不入库,无需迁移)。
- trigger_merge_mode 默认 by_task→all(schema/config/后端兜底/前端初值同步)。
- seed_first_intent 默认 true→false(走标准先规划再执行);spawn_task 不再暴露该参数,
  工具建任务一律 false。
This commit is contained in:
Autumn-27 committed 2026-08-15 00:19:14 -04:00
1 parent 4c33317c6a
commit 80fcf528dc
18 files changed
+61 -43

No files matched your search

+7 -6
View File
@@ -57,14 +57,15 @@ func chatWorkDirSpec(workDir string) string {
// chatSystem renders the DB-managed prompt body for agentKey. Custom agents have
// no per-key in-code default, so DefaultAssistantPrompt is the render fallback.
func chatSystem(agentKey, workDir string) string {
return renderSystem(agentKey, DefaultAssistantPrompt, chatVars{Now: nowStr()}) + chatWorkDirSpec(workDir)
func chatSystem(agentKey, dataDir, workDir string) string {
return renderSystem(agentKey, DefaultAssistantPrompt, chatVars{DataDir: dataDir, Now: nowStr()}) + chatWorkDirSpec(workDir)
}
// chatVars carries the runtime variables a custom agent's prompt may reference.
// Now (server wall-clock, refreshed each turn) is the only one today; any other
// {{.X}} fails to render and falls back to DefaultAssistantPrompt.
type chatVars struct{ Now string }
// DataDir (server data root) + Now (server wall-clock, refreshed each turn) are the
// universal ones; any other {{.X}} fails to render and falls back to
// DefaultAssistantPrompt.
type chatVars struct{ DataDir, Now string }
// Chat runs ONE turn of a conversation with the agent identified by agentKey,
// resuming prior history keyed by sessionID. maxTurns is the per-turn agent step
@@ -94,7 +95,7 @@ func (c *ChatAgent) Chat(ctx context.Context, agentKey, sessionID, message strin
tools, def, cleanup := AugmentTools(ctx, agentKey, base)
defer cleanup()
system, boundary := deferredSystem(chatSystem(agentKey, sessionWorkDir), def)
system, boundary := deferredSystem(chatSystem(agentKey, c.workDir, sessionWorkDir), def)
opts := agentcore.Options{
Provider: c.prov,
SystemPrompt: system,
+2 -2
View File
@@ -72,7 +72,7 @@ type GoalSpec struct {
//
// as + taskID, when non-nil/positive, wire the add_task_scope tool so the
// decomposer can register the explicit asset scope it extracts from the goal.
func DecomposeGoals(ctx context.Context, c Config, goalText, desc string, as *db.AssetStore, taskID int64, emit func(db.Activity)) []GoalSpec {
func DecomposeGoals(ctx context.Context, c Config, dataDir, goalText, desc string, as *db.AssetStore, taskID int64, emit func(db.Activity)) []GoalSpec {
prov, err := c.NewProvider()
if err != nil {
return nil
@@ -102,7 +102,7 @@ func DecomposeGoals(ctx context.Context, c Config, goalText, desc string, as *db
// Description rides in the user message (same channel as the goal), NOT via the
// {{.EngagementDescription}} template var — else a prompt that references the var
// would inject the description twice. System prompt stays pure static instructions.
sys := renderSystem("goals", goalsDefaultTmpl, GoalsVars{Now: nowStr()})
sys := renderSystem("goals", goalsDefaultTmpl, GoalsVars{DataDir: dataDir, Now: nowStr()})
tools := []actool.CoreTool{submitTool}
// Wire add_task_scope only when we have a real asset store + task to write to.
// The scope-extraction tail is appended in lockstep so the prompt never asks for
+3 -3
View File
@@ -54,8 +54,8 @@ const mainAgentDefaultTmpl = `你是一个授权渗透测试系统的"主 agent"
不要编造发现;只根据工具返回的真实数据回答。`
func mainAgentSystem(goal, workDir string) string {
body := renderSystem("mainagent", mainAgentDefaultTmpl, MainVars{Goal: goal, Now: nowStr()})
func mainAgentSystem(goal, dataDir, workDir string) string {
body := renderSystem("mainagent", mainAgentDefaultTmpl, MainVars{Goal: goal, DataDir: dataDir, Now: nowStr()})
return body + artifactSpec(workDir)
}
@@ -76,7 +76,7 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t
defer cleanup()
// 本任务的工作目录 <workDir>/<taskID>,先建好。
mainDir := ensureRunDir(m.workDir, taskID, 0)
system, boundary := deferredSystem(mainAgentSystem(goal, mainDir), def)
system, boundary := deferredSystem(mainAgentSystem(goal, m.workDir, mainDir), def)
opts := agentcore.Options{
Provider: m.prov,
SystemPrompt: system,
+3 -3
View File
@@ -228,8 +228,8 @@ const plannerDefaultTmpl = `你是一个授权渗透测试系统的"规划者"
宁可不生成,也不要重复或硬凑。简洁、克制、高效。`
func plannerSystem(goal, workDir string) string {
body := renderSystem("planner", plannerDefaultTmpl, PlannerVars{Goal: goal, Now: nowStr()})
func plannerSystem(goal, dataDir, workDir string) string {
body := renderSystem("planner", plannerDefaultTmpl, PlannerVars{Goal: goal, DataDir: dataDir, Now: nowStr()})
return body + artifactSpec(workDir)
}
@@ -269,7 +269,7 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
}
// 本任务的工作目录 <workDir>/<taskID>,先建好。
taskDir := ensureRunDir(p.workDir, taskID, 0)
system, boundary := deferredSystem(plannerSystem(goal, taskDir), def)
system, boundary := deferredSystem(plannerSystem(goal, p.workDir, taskDir), def)
// planner 无自身墙钟预算;有 deadline 时把 MaxDuration 夹逼到剩余,让在跑的规划轮在
// 任务到点时进收尾(因超时→任务超时词,因步数→per-run 词)。
maxDur, clamped := clampMaxDuration(tc.DeadlineUnix, 0)
+4 -4
View File
@@ -15,10 +15,10 @@ var PromptOverride func(agentKey string) (string, bool)
// Prompt-variable structs — fields mirror each agent's catalog (docs §5a) so a
// user template referencing a catalog variable renders; referencing anything else
// fails template execution and falls back to the built-in default.
type PlannerVars struct{ Goal, Scope, AssetSummary, Now string }
type WorkerVars struct{ ProxyAddr, WorkerName, Now string }
type MainVars struct{ Goal, AssetSummary, FindingsSummary, Now string }
type GoalsVars struct{ EngagementDescription, Now string }
type PlannerVars struct{ Goal, Scope, AssetSummary, DataDir, Now string }
type WorkerVars struct{ ProxyAddr, WorkerName, DataDir, Now string }
type MainVars struct{ Goal, AssetSummary, FindingsSummary, DataDir, Now string }
type GoalsVars struct{ EngagementDescription, DataDir, Now string }
// nowStr is the server-local wall-clock string exposed as the universal {{.Now}}
// prompt variable. renderSystem runs on every agent turn/round, so this is fresh
+19 -2
View File
@@ -20,7 +20,7 @@ func TestChatNowVarRenders(t *testing.T) {
return "", false
}
out := chatSystem("tec_benchmark", "/tmp/x")
out := chatSystem("tec_benchmark", "/app/data", "/tmp/x")
if !strings.Contains(out, "当前时间:") {
t.Fatalf("custom prompt body missing, likely fell back to default: %q", out)
}
@@ -30,6 +30,23 @@ func TestChatNowVarRenders(t *testing.T) {
}
}
// TestChatDataDirVarRenders verifies the universal {{.DataDir}} runtime variable:
// a custom prompt referencing it renders the server data root (s.m.dir), rather
// than failing template execution and falling back to DefaultAssistantPrompt.
func TestChatDataDirVarRenders(t *testing.T) {
prev := PromptOverride
defer func() { PromptOverride = prev }()
PromptOverride = func(key string) (string, bool) {
return "数据根目录:{{.DataDir}}", true
}
out := chatSystem("tec_benchmark", "/app/data", "/tmp/x")
if !strings.Contains(out, "数据根目录:/app/data") {
t.Fatalf("{{.DataDir}} did not render the data root: %q", out)
}
}
// TestChatUnknownVarFallsBack verifies an out-of-catalog {{.X}} still degrades
// safely to the default assistant prompt (never a half-rendered prompt).
func TestChatUnknownVarFallsBack(t *testing.T) {
@@ -40,7 +57,7 @@ func TestChatUnknownVarFallsBack(t *testing.T) {
return "引用了不存在的变量:{{.Bogus}}", true
}
out := chatSystem("whatever", "/tmp/x")
out := chatSystem("whatever", "/app/data", "/tmp/x")
if strings.Contains(out, "引用了不存在的变量") {
t.Fatalf("broken template should have fallen back, got custom body: %q", out)
}
+3 -3
View File
@@ -34,7 +34,7 @@ func TestRenderSystemOverrideAndFallback(t *testing.T) {
// full plannerSystem path: DB body [A] is honored, then the code-owned tail
// [C] (中间产物输出规约) is ALWAYS appended — editing the body can't drop it.
PromptOverride = func(k string) (string, bool) { return "PLANNER {{.Goal}}", true }
got := plannerSystem("拿下X", "/data")
got := plannerSystem("拿下X", "/data", "/data")
if !strings.HasPrefix(got, "PLANNER 拿下X") {
t.Fatalf("plannerSystem body not honored: %q", got)
}
@@ -47,7 +47,7 @@ func TestRenderSystemOverrideAndFallback(t *testing.T) {
PromptOverride = func(k string) (string, bool) {
return "{{if .ProxyAddr}}走代理 {{.ProxyAddr}}{{else}}手动{{end}}", true
}
withProxy := workerSystem("127.0.0.1:8080", "/data")
withProxy := workerSystem("127.0.0.1:8080", "/data", "/data")
if !strings.HasPrefix(withProxy, "走代理 127.0.0.1:8080") {
t.Fatalf("worker proxy branch body: %q", withProxy)
}
@@ -57,7 +57,7 @@ func TestRenderSystemOverrideAndFallback(t *testing.T) {
if !strings.Contains(withProxy, "中间产物输出规约") {
t.Fatalf("worker missing artifact tail: %q", withProxy)
}
noProxy := workerSystem("", "/data")
noProxy := workerSystem("", "/data", "/data")
if !strings.HasPrefix(noProxy, "手动") {
t.Fatalf("worker no-proxy branch body: %q", noProxy)
}
+3 -3
View File
@@ -182,8 +182,8 @@ func ensureRunDir(base string, taskID, intentID int64) string {
// cmdOutDir is the SDK large-tool-output spill dir under an agent's run dir.
func cmdOutDir(dir string) string { return filepath.Join(dir, "cmd-output") }
func workerSystem(proxyAddr, runDir string) string {
body := renderSystem("worker", workerDefaultTmpl, WorkerVars{ProxyAddr: proxyAddr, Now: nowStr()})
func workerSystem(proxyAddr, dataDir, runDir string) string {
body := renderSystem("worker", workerDefaultTmpl, WorkerVars{ProxyAddr: proxyAddr, DataDir: dataDir, Now: nowStr()})
return body + workerTrafficBlock(proxyAddr) + workerArtifactSpec(runDir)
}
@@ -263,7 +263,7 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.
// 压缩。本次意图的专属工作目录 <workDir>/<taskID>/i<intentID>,引擎侧先建好。
runDir := ensureRunDir(w.workDir, taskID, intent.ID)
overview := renderWorkerGraphOverview(tsx.graphOverviewData())
system, boundary := deferredSystem(workerSystem(w.proxyAddr, runDir), def)
system, boundary := deferredSystem(workerSystem(w.proxyAddr, w.workDir, runDir), def)
// 任务级 deadline(经 ctx 注入)夹逼本 run 的墙钟预算 + 决定收尾词(见 taskclock.go)。
tc := taskClockFrom(ctx)
maxDur, clamped := clampMaxDuration(tc.DeadlineUnix, w.runTimeout)