From f86885ec3d137679365cb53ecf9a3474dca3b1f8 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Sun, 9 Aug 2026 02:19:40 -0400 Subject: [PATCH] =?UTF-8?q?feat(task):=20=E5=88=9B=E5=BB=BA=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=8F=AF=E9=80=89=E4=B8=8B=E5=8F=91=E7=A7=8D=E5=AD=90?= =?UTF-8?q?=E6=84=8F=E5=9B=BE,worker=20=E5=85=8D=E7=AD=89=E9=A6=96?= =?UTF-8?q?=E8=BD=AE=E8=A7=84=E5=88=92=E7=9B=B4=E6=8E=A5=E5=BC=80=E8=B7=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 seed_first_intent(默认开启):创建即把"描述+目标"作为一条意图 写入 frontier,worker 轮询领取立即执行,省掉开跑前的首轮 planner LLM; 跑完由 NotifyDone 正常唤醒 planner 判定/补充。CTF 场景常一 work 解决。 - Engine.Run 首轮 kick 改为"frontier 为空才触发",一处覆盖普通/种子/恢复三种情况 - HTTP createTask 与 spawn_task 工具同步支持;bump 工具 schema 刷新 flag 使旧库生效 Co-Authored-By: Claude Opus 4.8 --- server/engine.go | 11 ++++++-- server/orchestration.go | 24 ++++++++++------ server/server.go | 33 +++++++++++++++++++--- web/src/app/(main)/function/tasks/page.tsx | 14 ++++++++- web/src/lib/api.ts | 10 +++++-- 5 files changed, 73 insertions(+), 19 deletions(-) diff --git a/server/engine.go b/server/engine.go index 9694911..c235d48 100644 --- a/server/engine.go +++ b/server/engine.go @@ -9,11 +9,11 @@ import ( "sync" "time" - "github.com/Autumn-27/norma/harness" - "github.com/Autumn-27/norma/llm" "github.com/Autumn-27/artex/agent" "github.com/Autumn-27/artex/db" "github.com/Autumn-27/artex/intercept" + "github.com/Autumn-27/norma/harness" + "github.com/Autumn-27/norma/llm" ) // model_error(provider/API 故障:LLM 层瞬时重试耗尽,或流已开始后中途断流) @@ -367,7 +367,12 @@ func (e *Engine) Run(ctx context.Context, t *Task) { go e.workerLoop(ctx, t, fmt.Sprintf("work#%d", i+1)) } e.startDeadlineCoordinator(ctx, t) // 任务级超时定时器(仅 timeout>0;去重) - t.Notify() // kick the first planning round (acted on once LLM is ready) + // 仅在 frontier 为空时才 kick 首轮规划:普通任务(空 frontier)照常触发;带种子意图的 + // 任务 frontier 已非空 → 跳过首轮 planner,worker 直接领种子意图开跑,跑完由 NotifyDone + // 唤醒 planner。重启自动恢复时 frontier 通常也非空 → 顺带避免白烧一轮。 + if fr, _ := t.Store.Frontier(1); len(fr) == 0 { + t.Notify() // kick the first planning round (acted on once LLM is ready) + } } func (e *Engine) plannerLoop(ctx context.Context, t *Task) { diff --git a/server/orchestration.go b/server/orchestration.go index 49f59c5..57eac71 100644 --- a/server/orchestration.go +++ b/server/orchestration.go @@ -9,9 +9,9 @@ import ( "strings" "time" + "github.com/Autumn-27/artex/agent" "github.com/Autumn-27/norma/permission" actool "github.com/Autumn-27/norma/tool" - "github.com/Autumn-27/artex/agent" ) // jsonResult marshals v to a JSON tool result. @@ -234,17 +234,19 @@ func (s *Server) toolSpawnTask() actool.CoreTool { return wrTool("spawn_task", "新建一个子任务并启动探索引擎,返回 task_id。用于把一件事(如一道题/一个目标)派成独立任务。parent_ref 可选:填当前编排关联的父任务 id 做父子关联。", objSchema(map[string]any{ - "description": strParam("任务描述(简短标题)"), - "goal": strParam("任务目标(要达成什么)"), - "parent_ref": strParam("可选:父任务 id(做父子关联)"), - "llm_profile_id": map[string]any{"type": "integer", "description": "可选:指定本子任务 planner/worker 用的 LLM 配置 id(见 list_llm_profiles);留空则继承父任务、再回退全局激活配置"}, - "timeout_seconds": map[string]any{"type": "integer", "description": "可选:任务级超时(秒)。到点后触发优雅收尾并进入 timeout 终态;留空或 0 = 不限时"}, + "description": strParam("任务描述(简短标题)"), + "goal": strParam("任务目标(要达成什么)"), + "parent_ref": strParam("可选:父任务 id(做父子关联)"), + "llm_profile_id": map[string]any{"type": "integer", "description": "可选:指定本子任务 planner/worker 用的 LLM 配置 id(见 list_llm_profiles);留空则继承父任务、再回退全局激活配置"}, + "timeout_seconds": map[string]any{"type": "integer", "description": "可选:任务级超时(秒)。到点后触发优雅收尾并进入 timeout 终态;留空或 0 = 不限时"}, + "seed_first_intent": map[string]any{"type": "boolean", "description": "可选:默认 true。true=创建即把「描述+目标」作为一条种子意图下发,worker 免等首轮 planner 直接开跑(CTF 等常一个 work 解决的场景推荐);false=走标准的先规划再执行"}, }, "description", "goal"), func(_ context.Context, in json.RawMessage) (actool.Result, error) { var a struct { Description, Goal, ParentRef string LLMProfileID json.RawMessage `json:"llm_profile_id"` TimeoutSeconds int `json:"timeout_seconds"` + SeedFirstIntent *bool `json:"seed_first_intent"` } _ = json.Unmarshal(in, &a) if strings.TrimSpace(a.Description) == "" { @@ -279,8 +281,12 @@ func (s *Server) toolSpawnTask() actool.CoreTool { } } s.seed(t, a.Description+" "+a.Goal) // seed 初始资产,喂给事件驱动 loop - s.createGoals(s.ctx, t, nil) // 目标分解(LLM;规则兜底) - s.engine.Run(s.ctx, t) // 启动该任务的探索引擎 + // 种子意图(默认开启,仅显式 false 关闭):worker 免等首轮 planner 直接开跑。 + if a.SeedFirstIntent == nil || *a.SeedFirstIntent { + s.seedFirstIntent(t) + } + s.createGoals(s.ctx, t, nil) // 目标分解(LLM;规则兜底) + s.engine.Run(s.ctx, t) // 启动该任务的探索引擎 return actool.Text(fmt.Sprintf("task created: %s", t.ID)), nil }) } @@ -403,7 +409,7 @@ func (s *Server) seedOrchestrationTools() { // reaches an old DB otherwise. Preserves each tool's agent binding + enabled flag. // Bump the flag whenever these tools' schemas/descriptions change in code. func (s *Server) refreshBuiltinToolSchemas() { - const flag = "tool_schema_refresh_v4_goal_met_desc" + const flag = "tool_schema_refresh_v5_spawn_seed_intent" if v, _, _ := s.m.pg.GetSetting(flag); v == "true" { return } diff --git a/server/server.go b/server/server.go index 514d3d0..d7e68a8 100644 --- a/server/server.go +++ b/server/server.go @@ -926,10 +926,11 @@ func (s *Server) testLLM(w http.ResponseWriter, r *http.Request) { } type createTaskReq struct { - Description string `json:"description"` - Goal string `json:"goal"` - LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务的 LLM 配置;省略/null=用激活配置 - TimeoutSeconds int `json:"timeout_seconds"` // 任务级超时(秒);0/省略=不限时 + Description string `json:"description"` + Goal string `json:"goal"` + LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务的 LLM 配置;省略/null=用激活配置 + TimeoutSeconds int `json:"timeout_seconds"` // 任务级超时(秒);0/省略=不限时 + SeedFirstIntent *bool `json:"seed_first_intent,omitempty"` // 创建时直接下发一条种子意图(内容=描述+目标),让 worker 免等首轮 planner 直接开跑;省略/null=默认开启。CTF 常一 work 解决,省掉开跑前的 planner 轮。 } func (s *Server) createTask(w http.ResponseWriter, r *http.Request) { @@ -960,6 +961,12 @@ func (s *Server) createTask(w http.ResponseWriter, r *http.Request) { log.Printf("[task] 新建任务 #%s «%s» 目标: %s", t.ID, req.Description, req.Goal) // seed initial asset(s) from goal/description so the event-driven loop has a root. s.seed(t, req.Description+" "+req.Goal) + // 种子意图(默认开启):直接把"描述+目标"作为一条待办意图写入 frontier,worker 轮询即可 + // 领取开跑,省掉开跑前的首轮 planner LLM;跑完由 NotifyDone 正常唤醒 planner 判定/补充。 + // CTF 场景常一个 work 就解决。仅显式传 false 才关闭。 + if req.SeedFirstIntent == nil || *req.SeedFirstIntent { + s.seedFirstIntent(t) + } // Return immediately — goal decomposition is async so the UI isn't blocked. writeJSON(w, 201, t) // Background: emit round-0 marker, decompose goals via LLM, then start engine. @@ -1088,6 +1095,24 @@ func (s *Server) seed(t *Task, text string) { t.Notify() } +// seedFirstIntent writes ONE open intent (summary = 描述+目标) into the task's +// frontier at creation, so a worker can claim and run it immediately without first +// waiting a planner round. Mirrors a planner top-level intent: it links from the +// origin fact (RelDerivedFrom) so it still traces back to a fact node. Best-effort — +// a failure just falls back to the normal planner-driven flow. +func (s *Server) seedFirstIntent(t *Task) { + summary := fmt.Sprintf("完成任务目标:%s(任务:%s)", t.Goal, t.Description) + id, err := t.Store.AddIntent(map[string]any{"summary": summary}, 8, nil, "seed") + if err != nil { + log.Printf("[seed] task %s: 下发种子意图失败: %v", t.ID, err) + return + } + if origin, _ := t.Store.OriginFactID(); origin > 0 { + _ = t.Store.Link(origin, db.RelDerivedFrom, id) + } + log.Printf("[seed] task %s: 已下发种子意图 #%d", t.ID, id) +} + func (s *Server) getTask(w http.ResponseWriter, r *http.Request) { t, ok := s.m.Task(r.PathValue("id")) if !ok { diff --git a/web/src/app/(main)/function/tasks/page.tsx b/web/src/app/(main)/function/tasks/page.tsx index 6858e0d..858ff50 100644 --- a/web/src/app/(main)/function/tasks/page.tsx +++ b/web/src/app/(main)/function/tasks/page.tsx @@ -15,6 +15,7 @@ import { import { StatusBadge } from "@/components/status-badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; @@ -110,6 +111,7 @@ export default function TasksPage() { const [profiles, setProfiles] = React.useState([]); const [llmProfile, setLlmProfile] = React.useState(ACTIVE_PROFILE); // sentinel = active const [timeoutMin, setTimeoutMin] = React.useState(""); // 任务级超时(分钟);空/0 = 不限时 + const [seedFirstIntent, setSeedFirstIntent] = React.useState(true); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑 const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(""); const [statusFilter, setStatusFilter] = React.useState("all"); @@ -171,12 +173,13 @@ export default function TasksPage() { try { const pid = llmProfile === ACTIVE_PROFILE ? undefined : Number(llmProfile); const timeoutSec = Math.max(0, Math.floor(Number(timeoutMin) || 0)) * 60; - await api.createTask(description.trim(), goal.trim(), pid, timeoutSec); + await api.createTask(description.trim(), goal.trim(), pid, timeoutSec, seedFirstIntent); toast.success("任务已创建"); setDescription(""); setGoal(""); setLlmProfile(ACTIVE_PROFILE); setTimeoutMin(""); + setSeedFirstIntent(true); setOpen(false); load(); } catch (e) { @@ -300,6 +303,15 @@ export default function TasksPage() { 到点后触发优雅收尾(各 agent 写回 + planner 终局判定),任务进入 timeout 终态。

+
+ +

+ 开启后创建即把「描述+目标」作为一条意图下发,worker 免等首轮规划直接开跑,跑完再由 planner 接手判定/补充。CTF 等常一个 work 直接解决的场景推荐开启;关闭则走标准的先规划再执行。 +

+
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 0e08c79..d745726 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -109,8 +109,14 @@ export const api = { // ---- tasks ---- tasks: () => get<{ tasks: Task[]; active: string }>("/tasks").then((r) => ({ tasks: arr(r.tasks), active: r.active ?? "" })), - createTask: (description: string, goal: string, llmProfileId?: number, timeoutSeconds?: number) => - post("/tasks", { description, goal, llm_profile_id: llmProfileId ?? null, timeout_seconds: timeoutSeconds ?? 0 }), + createTask: (description: string, goal: string, llmProfileId?: number, timeoutSeconds?: number, seedFirstIntent?: boolean) => + post("/tasks", { + description, + goal, + llm_profile_id: llmProfileId ?? null, + timeout_seconds: timeoutSeconds ?? 0, + seed_first_intent: seedFirstIntent ?? true, + }), deleteTask: (id: string) => del<{ deleted: number }>(`/tasks/${id}`), controlTask: (id: string, action: "pause" | "resume") => post<{ id: string; paused: boolean }>(`/tasks/${id}/control`, { action }), setActive: (id: string) => post<{ active: string }>("/active", { id }),