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:
1 parent
4c33317c6a
commit
80fcf528dc
18 files changed
+61
-43
No files matched your search
+7
-6
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user