feat(agent): 按任务/意图预建 agent 工作目录,worker 免手动建目录

- 新增 ensureRunDir(base,taskID,intentID):os.MkdirAll 建
  <workDir>/<taskID>[/i<intentID>],返回路径;cmdOutDir 助手
- worker:CWD/ToolOutputDir/提示词指到 <workDir>/<taskID>/i<意图id>/
  (引擎预建),删 workerArtifactSubdir,新 workerArtifactSpec 文案
- planner/mainagent:补 WorkingDir=<workDir>/<taskID>/ + ToolOutputDir
  (此前根本没设 WorkingDir);artifactSpec 文案改为「本任务工作目录」

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Autumn-27andClaude Opus 4.8 committed 2026-08-09 00:40:31 -04:00
1 parent f7197da988
commit 5f0fac33aa
3 files changed
+53 -22

No files matched your search

+10 -6
View File
@@ -4,12 +4,12 @@ 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"
"github.com/Autumn-27/artex/db"
)
// MainAgent is the thin human-interface orchestrator (docs §4.2 / §7). The human
@@ -74,7 +74,9 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t
base := append(tsx.MainAgentTools(), actool.DefaultTools()...)
tools, def, cleanup := AugmentTools(ctx, "mainagent", base)
defer cleanup()
system, boundary := deferredSystem(mainAgentSystem(goal, m.workDir), def)
// 本任务的工作目录 <workDir>/<taskID>,先建好。
mainDir := ensureRunDir(m.workDir, taskID, 0)
system, boundary := deferredSystem(mainAgentSystem(goal, mainDir), def)
opts := agentcore.Options{
Provider: m.prov,
SystemPrompt: system,
@@ -93,10 +95,12 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t
BraveSearchAPIKey: m.webSearch.BraveKey,
TavilySearchAPIKey: m.webSearch.TavilyKey,
WebSearchProxy: m.webSearch.Proxy,
BashEnv: proxyEnv(m.proxyAddr, m.proxyCACert), // Bash 子命令默认走代理+信任 CA
MaxTurns: m.maxTurns, // 0 = unlimited (configurable in agent management)
Compaction: compactionConfig(m.window), // long chats stay within the window
Todos: actool.NewTodoStore(), // 会话级临时待办(TodoWrite),纯规划用,退出即丢
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),
}
+10 -6
View File
@@ -8,12 +8,12 @@ import (
"sync"
"time"
"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"
"github.com/Autumn-27/artex/db"
)
// Planner is the event-driven LLM planner (docs §4.3): each time the asset or
@@ -197,7 +197,9 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
if tc.Final {
situational += "\n\n【任务终局收尾(本轮特殊指令,覆盖上面的常规规划流程)】:" + resolveTaskTimeoutWrapup("planner")
}
system, boundary := deferredSystem(plannerSystem(goal, p.workDir)+situational, def)
// 本任务的工作目录 <workDir>/<taskID>,先建好。
taskDir := ensureRunDir(p.workDir, taskID, 0)
system, boundary := deferredSystem(plannerSystem(goal, taskDir)+situational, def)
// planner 无自身墙钟预算;有 deadline 时把 MaxDuration 夹逼到剩余,让在跑的规划轮在
// 任务到点时进收尾(因超时→任务超时词,因步数→per-run 词)。
maxDur, clamped := clampMaxDuration(tc.DeadlineUnix, 0)
@@ -223,10 +225,12 @@ func (p *Planner) Plan(ctx context.Context, taskID int64, as *db.AssetStore, ts
BraveSearchAPIKey: p.webSearch.BraveKey,
TavilySearchAPIKey: p.webSearch.TavilyKey,
WebSearchProxy: p.webSearch.Proxy,
BashEnv: proxyEnv(p.proxyAddr, p.proxyCACert), // Bash 子命令默认走代理+信任 CA
MaxTurns: p.maxTurns, // 0 = unlimited (configurable in agent management)
MaxDuration: maxDur, // 0=不限;有 deadline 时=距 deadline 剩余
Compaction: compactionConfig(p.window),
BashEnv: proxyEnv(p.proxyAddr, p.proxyCACert), // Bash 子命令默认走代理+信任 CA
WorkingDir: taskDir, // 本任务工作目录 <workDir>/<taskID>
ToolOutputDir: cmdOutDir(taskDir),
MaxTurns: p.maxTurns, // 0 = unlimited (configurable in agent management)
MaxDuration: maxDur, // 0=不限;有 deadline 时=距 deadline 剩余
Compaction: compactionConfig(p.window),
// 跨唤醒共享的规划待办:让串行链在多轮之间保留(session 是新的,store 不是)。
Todos: p.todoFor(ts.ID()),
// 命中【本轮】步数预算→ SDK 跑收尾:把本轮已想清楚的结论落地(该派的 add_intent、
+33 -10
View File
@@ -4,7 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"time"
"github.com/Autumn-27/artex/db"
@@ -154,17 +156,36 @@ func workerTrafficBlock(proxyAddr string) string {
// artifactSpec is 段 [C]: the code-owned, non-editable tail appended to every
// pentest agent's prompt — intermediate artifacts must land in the shared work
// dir, never /tmp. Guaranteed present regardless of how the DB body is edited.
func artifactSpec(workDir string) string {
return "\n\n**中间产物输出规约**:脚本、payload、抓到的响应体、临时数据等一切中间产物,**一律写到共享工作目录 " + workDir + "**(这是所有 agent 共用的 CWD,相对路径即写在这里,也可用该绝对路径)——**不要写 /tmp、不要用其它绝对路径**。"
func artifactSpec(dir string) string {
return "\n\n**中间产物输出规约**:脚本、payload、抓到的响应体、临时数据等一切中间产物,**一律写到本任务工作目录 " + dir + "**(相对路径即写在这里,也可用该绝对路径)——**不要写 /tmp、不要用其它绝对路径**。"
}
// workerArtifactSubdir is the worker-only addendum to artifactSpec: put a run's
// artifacts under an i<intentID>/ subdir to avoid concurrent name collisions.
const workerArtifactSubdir = "为避免与其他 work 撞名,把本次产物放到子目录 i<意图id>/ 下(如 i123/exploit.py)。"
// workerArtifactSpec is the worker's 段 [C]: its per-intent run dir is pre-created
// by the engine (ensureRunDir), so it just writes relative paths there — no manual
// mkdir, no cross-worker name collisions.
func workerArtifactSpec(runDir string) string {
return "\n\n**中间产物输出规约**:脚本、payload、抓到的响应体、临时数据等一切中间产物,**一律写到本次意图的专属工作目录 " + runDir + "**(已自动建好,直接用相对路径写在这里即可,无需再手动建目录)——**不要写 /tmp、不要用其它绝对路径**。"
}
func workerSystem(proxyAddr, workDir string) string {
// ensureRunDir builds and creates an agent's working directory under base:
// <base>/<taskID> for planner/main; <base>/<taskID>/i<intentID> for a worker
// (intentID<=0 → task dir only). Best-effort mkdir — on failure, writes fail the
// same way an unwritable CWD would.
func ensureRunDir(base string, taskID, intentID int64) string {
dir := filepath.Join(base, strconv.FormatInt(taskID, 10))
if intentID > 0 {
dir = filepath.Join(dir, "i"+strconv.FormatInt(intentID, 10))
}
_ = os.MkdirAll(dir, 0o755)
return dir
}
// 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})
return body + workerTrafficBlock(proxyAddr) + artifactSpec(workDir) + workerArtifactSubdir
return body + workerTrafficBlock(proxyAddr) + workerArtifactSpec(runDir)
}
// renderIntentTask formats the claimed intent for the worker's SYSTEM prompt: the
@@ -235,8 +256,10 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.
// 意图是 worker 的全部任务,放进 system prompt(抗 compaction、整轮常驻),而不是
// user 输入——与 planner 把关键态势放 system 一致。落在指令之后、deferred 块之前。
// 全局态势(graphOverviewData)也带上,但仅供了解大局;意图放在最后、最醒目的位置。
// 本次意图的专属工作目录 <workDir>/<taskID>/i<intentID>,引擎侧先建好。
runDir := ensureRunDir(w.workDir, taskID, intent.ID)
overview := renderWorkerGraphOverview(tsx.graphOverviewData())
system, boundary := deferredSystem(workerSystem(w.proxyAddr, w.workDir)+overview+renderIntentTask(intent), def)
system, boundary := deferredSystem(workerSystem(w.proxyAddr, runDir)+overview+renderIntentTask(intent), def)
// 任务级 deadline(经 ctx 注入)夹逼本 run 的墙钟预算 + 决定收尾词(见 taskclock.go)。
tc := taskClockFrom(ctx)
maxDur, clamped := clampMaxDuration(tc.DeadlineUnix, w.runTimeout)
@@ -266,7 +289,7 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.
WebSearchProxy: w.webSearch.Proxy,
// Bash 子命令的 HTTP 默认走记录代理 + 信任其 CA(工具无需 -x/-k)。
BashEnv: proxyEnv(w.proxyAddr, w.proxyCACert),
WorkingDir: w.workDir,
WorkingDir: runDir,
MaxTurns: w.maxTurns, // 0 = unlimited (configurable in agent management)
// 墙钟预算,轮边界判,不打断半路;0 = 不限。有任务级 deadline 时夹逼到 min(自身预算,
// 距 deadline 剩余),让本 run 在任务到点时自然进收尾(见 taskclock.go)。
@@ -277,7 +300,7 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.
Settlement: settle,
// large tool output spills to cmd-output/ with a head + pointer (SDK tool.Capture);
// full output preserved on disk. 截断上限用 SDK 默认(30000 字符)。
ToolOutputDir: filepath.Join(w.workDir, "cmd-output"),
ToolOutputDir: cmdOutDir(runDir),
Compaction: compactionConfig(w.window), // long tool-heavy runs stay within the window
Todos: actool.NewTodoStore(), // 会话级临时待办(TodoWrite),纯规划用,退出即丢
}