feat(task): 创建任务可选下发种子意图,worker 免等首轮规划直接开跑
新增 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 <[email protected]>
This commit is contained in:
1 parent
4cf622593a
commit
f86885ec3d
5 files changed
+73
-19
No files matched your search
+8
-3
@@ -9,11 +9,11 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Autumn-27/norma/harness"
|
|
||||||
"github.com/Autumn-27/norma/llm"
|
|
||||||
"github.com/Autumn-27/artex/agent"
|
"github.com/Autumn-27/artex/agent"
|
||||||
"github.com/Autumn-27/artex/db"
|
"github.com/Autumn-27/artex/db"
|
||||||
"github.com/Autumn-27/artex/intercept"
|
"github.com/Autumn-27/artex/intercept"
|
||||||
|
"github.com/Autumn-27/norma/harness"
|
||||||
|
"github.com/Autumn-27/norma/llm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// model_error(provider/API 故障: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))
|
go e.workerLoop(ctx, t, fmt.Sprintf("work#%d", i+1))
|
||||||
}
|
}
|
||||||
e.startDeadlineCoordinator(ctx, t) // 任务级超时定时器(仅 timeout>0;去重)
|
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) {
|
func (e *Engine) plannerLoop(ctx context.Context, t *Task) {
|
||||||
|
|||||||
+15
-9
@@ -9,9 +9,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Autumn-27/artex/agent"
|
||||||
"github.com/Autumn-27/norma/permission"
|
"github.com/Autumn-27/norma/permission"
|
||||||
actool "github.com/Autumn-27/norma/tool"
|
actool "github.com/Autumn-27/norma/tool"
|
||||||
"github.com/Autumn-27/artex/agent"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// jsonResult marshals v to a JSON tool result.
|
// jsonResult marshals v to a JSON tool result.
|
||||||
@@ -234,17 +234,19 @@ func (s *Server) toolSpawnTask() actool.CoreTool {
|
|||||||
return wrTool("spawn_task",
|
return wrTool("spawn_task",
|
||||||
"新建一个子任务并启动探索引擎,返回 task_id。用于把一件事(如一道题/一个目标)派成独立任务。parent_ref 可选:填当前编排关联的父任务 id 做父子关联。",
|
"新建一个子任务并启动探索引擎,返回 task_id。用于把一件事(如一道题/一个目标)派成独立任务。parent_ref 可选:填当前编排关联的父任务 id 做父子关联。",
|
||||||
objSchema(map[string]any{
|
objSchema(map[string]any{
|
||||||
"description": strParam("任务描述(简短标题)"),
|
"description": strParam("任务描述(简短标题)"),
|
||||||
"goal": strParam("任务目标(要达成什么)"),
|
"goal": strParam("任务目标(要达成什么)"),
|
||||||
"parent_ref": strParam("可选:父任务 id(做父子关联)"),
|
"parent_ref": strParam("可选:父任务 id(做父子关联)"),
|
||||||
"llm_profile_id": map[string]any{"type": "integer", "description": "可选:指定本子任务 planner/worker 用的 LLM 配置 id(见 list_llm_profiles);留空则继承父任务、再回退全局激活配置"},
|
"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 = 不限时"},
|
"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"),
|
}, "description", "goal"),
|
||||||
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
|
func(_ context.Context, in json.RawMessage) (actool.Result, error) {
|
||||||
var a struct {
|
var a struct {
|
||||||
Description, Goal, ParentRef string
|
Description, Goal, ParentRef string
|
||||||
LLMProfileID json.RawMessage `json:"llm_profile_id"`
|
LLMProfileID json.RawMessage `json:"llm_profile_id"`
|
||||||
TimeoutSeconds int `json:"timeout_seconds"`
|
TimeoutSeconds int `json:"timeout_seconds"`
|
||||||
|
SeedFirstIntent *bool `json:"seed_first_intent"`
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(in, &a)
|
_ = json.Unmarshal(in, &a)
|
||||||
if strings.TrimSpace(a.Description) == "" {
|
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.seed(t, a.Description+" "+a.Goal) // seed 初始资产,喂给事件驱动 loop
|
||||||
s.createGoals(s.ctx, t, nil) // 目标分解(LLM;规则兜底)
|
// 种子意图(默认开启,仅显式 false 关闭):worker 免等首轮 planner 直接开跑。
|
||||||
s.engine.Run(s.ctx, t) // 启动该任务的探索引擎
|
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
|
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.
|
// reaches an old DB otherwise. Preserves each tool's agent binding + enabled flag.
|
||||||
// Bump the flag whenever these tools' schemas/descriptions change in code.
|
// Bump the flag whenever these tools' schemas/descriptions change in code.
|
||||||
func (s *Server) refreshBuiltinToolSchemas() {
|
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" {
|
if v, _, _ := s.m.pg.GetSetting(flag); v == "true" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-4
@@ -926,10 +926,11 @@ func (s *Server) testLLM(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type createTaskReq struct {
|
type createTaskReq struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Goal string `json:"goal"`
|
Goal string `json:"goal"`
|
||||||
LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务的 LLM 配置;省略/null=用激活配置
|
LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务的 LLM 配置;省略/null=用激活配置
|
||||||
TimeoutSeconds int `json:"timeout_seconds"` // 任务级超时(秒);0/省略=不限时
|
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) {
|
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)
|
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.
|
// seed initial asset(s) from goal/description so the event-driven loop has a root.
|
||||||
s.seed(t, req.Description+" "+req.Goal)
|
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.
|
// Return immediately — goal decomposition is async so the UI isn't blocked.
|
||||||
writeJSON(w, 201, t)
|
writeJSON(w, 201, t)
|
||||||
// Background: emit round-0 marker, decompose goals via LLM, then start engine.
|
// 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()
|
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) {
|
func (s *Server) getTask(w http.ResponseWriter, r *http.Request) {
|
||||||
t, ok := s.m.Task(r.PathValue("id"))
|
t, ok := s.m.Task(r.PathValue("id"))
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { StatusBadge } from "@/components/status-badge";
|
import { StatusBadge } from "@/components/status-badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
@@ -110,6 +111,7 @@ export default function TasksPage() {
|
|||||||
const [profiles, setProfiles] = React.useState<LLMProfile[]>([]);
|
const [profiles, setProfiles] = React.useState<LLMProfile[]>([]);
|
||||||
const [llmProfile, setLlmProfile] = React.useState<string>(ACTIVE_PROFILE); // sentinel = active
|
const [llmProfile, setLlmProfile] = React.useState<string>(ACTIVE_PROFILE); // sentinel = active
|
||||||
const [timeoutMin, setTimeoutMin] = React.useState(""); // 任务级超时(分钟);空/0 = 不限时
|
const [timeoutMin, setTimeoutMin] = React.useState(""); // 任务级超时(分钟);空/0 = 不限时
|
||||||
|
const [seedFirstIntent, setSeedFirstIntent] = React.useState(true); // 创建时下发种子意图,worker 免等首轮 planner 直接开跑
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [query, setQuery] = React.useState("");
|
const [query, setQuery] = React.useState("");
|
||||||
const [statusFilter, setStatusFilter] = React.useState<TaskStatus | "all">("all");
|
const [statusFilter, setStatusFilter] = React.useState<TaskStatus | "all">("all");
|
||||||
@@ -171,12 +173,13 @@ export default function TasksPage() {
|
|||||||
try {
|
try {
|
||||||
const pid = llmProfile === ACTIVE_PROFILE ? undefined : Number(llmProfile);
|
const pid = llmProfile === ACTIVE_PROFILE ? undefined : Number(llmProfile);
|
||||||
const timeoutSec = Math.max(0, Math.floor(Number(timeoutMin) || 0)) * 60;
|
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("任务已创建");
|
toast.success("任务已创建");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setGoal("");
|
setGoal("");
|
||||||
setLlmProfile(ACTIVE_PROFILE);
|
setLlmProfile(ACTIVE_PROFILE);
|
||||||
setTimeoutMin("");
|
setTimeoutMin("");
|
||||||
|
setSeedFirstIntent(true);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
load();
|
load();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -300,6 +303,15 @@ export default function TasksPage() {
|
|||||||
到点后触发优雅收尾(各 agent 写回 + planner 终局判定),任务进入 timeout 终态。
|
到点后触发优雅收尾(各 agent 写回 + planner 终局判定),任务进入 timeout 终态。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<Checkbox checked={seedFirstIntent} onCheckedChange={(v) => setSeedFirstIntent(!!v)} />
|
||||||
|
直接下发首个意图(描述+目标)
|
||||||
|
</label>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
开启后创建即把「描述+目标」作为一条意图下发,worker 免等首轮规划直接开跑,跑完再由 planner 接手判定/补充。CTF 等常一个 work 直接解决的场景推荐开启;关闭则走标准的先规划再执行。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
|
|||||||
+8
-2
@@ -109,8 +109,14 @@ export const api = {
|
|||||||
|
|
||||||
// ---- tasks ----
|
// ---- tasks ----
|
||||||
tasks: () => get<{ tasks: Task[]; active: string }>("/tasks").then((r) => ({ tasks: arr(r.tasks), active: r.active ?? "" })),
|
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) =>
|
createTask: (description: string, goal: string, llmProfileId?: number, timeoutSeconds?: number, seedFirstIntent?: boolean) =>
|
||||||
post<Task>("/tasks", { description, goal, llm_profile_id: llmProfileId ?? null, timeout_seconds: timeoutSeconds ?? 0 }),
|
post<Task>("/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}`),
|
deleteTask: (id: string) => del<{ deleted: number }>(`/tasks/${id}`),
|
||||||
controlTask: (id: string, action: "pause" | "resume") => post<{ id: string; paused: boolean }>(`/tasks/${id}/control`, { action }),
|
controlTask: (id: string, action: "pause" | "resume") => post<{ id: string; paused: boolean }>(`/tasks/${id}/control`, { action }),
|
||||||
setActive: (id: string) => post<{ active: string }>("/active", { id }),
|
setActive: (id: string) => post<{ active: string }>("/active", { id }),
|
||||||
|
|||||||
Reference in new issue
Block a user