- 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。
111 lines
4.1 KiB
Go
111 lines
4.1 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/Autumn-27/artex/agent"
|
|
"github.com/Autumn-27/artex/db"
|
|
)
|
|
|
|
type goalSpec struct {
|
|
Text string
|
|
VulnClass string
|
|
}
|
|
|
|
// launchTask runs the shared post-creation sequence for a task created via ANY
|
|
// path (HTTP createTask 或 orchestration spawn_task),避免两处复制粘贴:
|
|
// 1. seed 根资产,喂给事件驱动 loop;
|
|
// 2. 可选种子意图,worker 免等首轮 planner 直接开跑;
|
|
// 3. 后台异步做目标分解(发「第 0 轮目标拆解」round + LLM 分解步骤 + 逐条 goal,页面可见),
|
|
// 分解完再 engine.Run —— 引擎在 goal 节点就绪后才启动,避免 planner 抢在 goal 之前跑的竞态。
|
|
//
|
|
// 异步(goroutine)所以调用方立即返回,两条路径行为一致:秒建任务、后台拆目标。
|
|
func (s *Server) launchTask(t *Task, seedText string, seedFirstIntent bool) {
|
|
s.seed(t, seedText)
|
|
if seedFirstIntent {
|
|
s.seedFirstIntent(t)
|
|
}
|
|
go func() {
|
|
s.engine.emitActivity(t, db.Activity{Worker: "planner", Kind: "round",
|
|
Summary: "第 0 轮目标拆解"})
|
|
goals := s.createGoals(s.ctx, t, func(r db.Activity) {
|
|
s.engine.emitActivity(t, r)
|
|
})
|
|
for _, g := range goals {
|
|
summary := g.Text
|
|
if g.VulnClass != "" {
|
|
summary = fmt.Sprintf("[%s] %s", g.VulnClass, g.Text)
|
|
}
|
|
s.engine.emitActivity(t, db.Activity{Worker: "planner", Kind: "text", Summary: summary})
|
|
}
|
|
s.engine.Run(s.ctx, t)
|
|
}()
|
|
}
|
|
|
|
// createGoals materializes the goal node(s) under the task root (rel objective).
|
|
// Decomposition is done ENTIRELY by the LLM (the project requires an LLM). There is
|
|
// no rule-based fallback splitter — it only ever produced garbage (shredded URLs,
|
|
// meaningless 2-way splits). If the LLM yields nothing (an error), the raw task goal
|
|
// is used verbatim as a single goal so the task still has something to judge against.
|
|
// Returns the seeded specs so callers can emit activity records for them.
|
|
// emit, when non-nil, is forwarded to DecomposeGoals so LLM steps are visible in the UI.
|
|
func (s *Server) createGoals(ctx context.Context, t *Task, emit func(db.Activity)) []goalSpec {
|
|
// Use the SAME LLM the task runs on (its pinned profile, else the active profile),
|
|
// NOT agent.FromEnv() — the LLM is configured via the UI (DB profile), not env vars,
|
|
// so FromEnv returned empty and every task silently fell back to the crude rule
|
|
// splitter (which shredded URLs / made meaningless 2-way splits).
|
|
var specs []goalSpec
|
|
if cfg, ok := s.goalsLLMConfig(t); ok {
|
|
var as *db.AssetStore
|
|
if s.m != nil {
|
|
as = s.m.Assets()
|
|
}
|
|
taskID, _ := strconv.ParseInt(t.ID, 10, 64)
|
|
for _, g := range agent.DecomposeGoals(ctx, cfg, s.m.dir, t.Goal, t.Description, as, taskID, emit) {
|
|
if strings.TrimSpace(g.Text) != "" {
|
|
specs = append(specs, goalSpec{Text: g.Text, VulnClass: g.VulnClass})
|
|
}
|
|
}
|
|
}
|
|
if len(specs) == 0 {
|
|
// No rule-based splitting: use the raw task goal verbatim as a single goal
|
|
// (only reached on an LLM error — normal runs get real decomposed goals).
|
|
if g := strings.TrimSpace(t.Goal); g != "" {
|
|
log.Printf("[goals] task %s: LLM 目标拆解无产出,回退为「原始目标作为单目标」", t.ID)
|
|
specs = []goalSpec{{Text: g}}
|
|
}
|
|
}
|
|
origin, _ := t.Store.OriginFactID()
|
|
for _, g := range specs {
|
|
payload := map[string]any{"text": g.Text}
|
|
if g.VulnClass != "" {
|
|
payload["vulnclass"] = g.VulnClass
|
|
}
|
|
id, _ := t.Store.AddNode(db.KindGoal, payload, 0, "open", "system", nil)
|
|
if origin > 0 && id > 0 {
|
|
_ = t.Store.Link(origin, db.RelSpawns, id) // goals descend from the task root (origin fact)
|
|
}
|
|
}
|
|
return specs
|
|
}
|
|
|
|
// goalsLLMConfig resolves the LLM config for goal decomposition by the standard
|
|
// precedence: the goals agent's own binding → the task's pinned profile → the global
|
|
// active profile (same source the engine runs on).
|
|
func (s *Server) goalsLLMConfig(t *Task) (agent.Config, bool) {
|
|
var pin *int64
|
|
if t != nil {
|
|
pin = t.LLMProfileID
|
|
}
|
|
if eff := s.effectiveProfileForAgent("goals", pin); eff != nil {
|
|
if cfg, ok := s.loadProfileConfig(*eff); ok {
|
|
return cfg, true
|
|
}
|
|
}
|
|
return s.loadLLMConfig()
|
|
}
|