Add catalog-driven orchestration: intent catalog injection
Details (original message): Add catalog-driven orchestration: intent catalog injection, deterministic plan validation, and system-function steps.
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
|
||||
- 本库只做编排与任务拆解,不实现 Agent。智能体执行只来自 Agent-Native 或调用方注册的后端。
|
||||
- 未知 backend fail-closed,禁止静默回落 stub。
|
||||
- 意图闸门为可选能力:唯 `squadintent` 与组合根(cli 接线)可 import agent-intent;`squad`/`server`/`store` 核心不感知意图。未配置意图(无闸门或零意图)时引擎行为与原实现一致;闸门错误与守卫拒绝 fail-closed(`squad.intent_refused`)。
|
||||
- 意图闸门与能力目录为可选能力:唯 `squadintent` 与组合根(cli 接线)可 import agent-intent;`squad`/`server`/`store` 核心不感知意图(目录以 `squad.IntentCatalog` 数据视图注入)。未配置意图(无闸门或零意图)时引擎行为与原实现一致;闸门错误、守卫拒绝与系统功能步的目录/校验/handler 缺失均 fail-closed(`squad.intent_refused` / 明确步骤错误)。
|
||||
- 磁盘上的运行状态是恢复权威;HTTP 错误使用稳定规范码和 `X-Trace-Id`,不以自由文本作为唯一错误合同。
|
||||
- 依赖仓版本以本仓 gitlink 为准。构建时只认本仓 `go.mod` 的 `replace`;嵌套子模块里的 `replace` 不参与本仓编译。
|
||||
|
||||
|
||||
+19
@@ -129,6 +129,8 @@ func assemble(dir, plannerName, modelOverride, intentRoot string) (*app, error)
|
||||
fanout := squad.NewFanoutSink(8192)
|
||||
fileSink := store.NewFileEventSink(dir, fanout)
|
||||
var gate squad.IntentGate
|
||||
var catalog *squad.IntentCatalog
|
||||
var runner squad.IntentRunner
|
||||
if intentRoot != "" {
|
||||
g, err := squadintent.New(context.Background(), squadintent.Options{
|
||||
IntentOptions: intent.Options{
|
||||
@@ -142,10 +144,27 @@ func assemble(dir, plannerName, modelOverride, intentRoot string) (*app, error)
|
||||
return nil, fmt.Errorf("意图配置加载失败(fail-closed):%w", err)
|
||||
}
|
||||
gate = g // g 可为 nil(来源为空),等价未启用
|
||||
if g != nil {
|
||||
catalog = g.Catalog()
|
||||
// 系统功能步执行通道:默认 fail-closed(无 handler 拒绝);
|
||||
// AGENT_SQUAD_INTENT_ECHO=1 时用演示执行器物化调用(开发/演示)。
|
||||
if os.Getenv("AGENT_SQUAD_INTENT_ECHO") == "1" {
|
||||
runner = g.EchoRunner()
|
||||
} else {
|
||||
runner = g.Runner(nil)
|
||||
}
|
||||
// 目录驱动的编排需要系统功能步角色存在(编制覆盖用,无后端语义)。
|
||||
if _, ok := reg.Get(squad.DefaultIntentRole); !ok {
|
||||
if err := reg.Register(squad.DefaultIntentRoleSpec()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
engine, err := squad.NewEngine(squad.Options{
|
||||
DataDir: dir, Registry: reg, Blueprints: bps, Planner: planner,
|
||||
Backends: backends, Sink: fileSink, IntentGate: gate,
|
||||
IntentCatalog: catalog, IntentRunner: runner,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+2
-2
@@ -79,7 +79,7 @@ func cmdDemo(args []string) int {
|
||||
backends := squad.NewBackendResolver()
|
||||
demoScript := map[string]map[string]string{
|
||||
"01_researcher": {
|
||||
"notes.md": "# 调研笔记\n\n- 目标产品:便携式超声诊断仪\n- 竞品:A/B/C 三款\n- 法规路径:二类器械,注册检验 + 临床评价\n",
|
||||
"notes.md": "# 调研笔记\n\n- 目标产品:便携式超声诊断仪\n- 竞品:A/B/C 三款\n- 法规路径:二类器械,注册检验 + 临床评价\n",
|
||||
"fields.json": `{"target_market":"中国","device_class":"二类","budget":"15万"}`,
|
||||
"gaps.json": `[{"item":"目标年产量未提供,影响成本测算","blocking":true}]`,
|
||||
},
|
||||
@@ -87,7 +87,7 @@ func cmdDemo(args []string) int {
|
||||
"output.md": "# 便携式超声诊断仪调研报告\n\n## 概述与背景\n\n本报告基于输入材料与调研产出,梳理便携式超声诊断仪的市场与技术背景。目标市场为中国(二类器械),预算 15 万元。\n\n## 核心内容\n\n竞品 A/B/C 在成像质量与便携性上各有取舍;技术路线建议探头一体化设计与电池供电优化。注册路径按二类器械执行注册检验与临床评价。\n\n## 风险与注意事项\n\n- 供应链对探头晶片依赖较高\n- 注册周期受检验排队影响\n\n## 基准假设与待确认清单\n\n- 年产量按人工澄清结论执行\n",
|
||||
},
|
||||
"03_reviewer": {
|
||||
"output.md": "# 便携式超声诊断仪调研报告(终审版)\n\n## 概述与背景\n\n本报告由小队协作产出:调研专员梳理事实与缺口,撰写专员成文,审核专员终审。目标市场为中国(二类器械),预算 15 万元,年产量 10 万套。\n\n## 核心内容\n\n竞品 A/B/C 在成像质量与便携性上各有取舍;技术路线建议探头一体化设计与电池供电优化。注册路径按二类器械执行注册检验与临床评价。成本测算基于年产量 10 万套摊薄产线投入。\n\n## 风险与注意事项\n\n- 供应链对探头晶片依赖较高\n- 注册周期受检验排队影响\n- 年产量波动超过 ±30% 时需重估成本模型\n\n## 基准假设与待确认清单\n\n- 灭菌方式采用环氧乙烷(EO)灭菌(行业常规)\n",
|
||||
"output.md": "# 便携式超声诊断仪调研报告(终审版)\n\n## 概述与背景\n\n本报告由小队协作产出:调研专员梳理事实与缺口,撰写专员成文,审核专员终审。目标市场为中国(二类器械),预算 15 万元,年产量 10 万套。\n\n## 核心内容\n\n竞品 A/B/C 在成像质量与便携性上各有取舍;技术路线建议探头一体化设计与电池供电优化。注册路径按二类器械执行注册检验与临床评价。成本测算基于年产量 10 万套摊薄产线投入。\n\n## 风险与注意事项\n\n- 供应链对探头晶片依赖较高\n- 注册周期受检验排队影响\n- 年产量波动超过 ±30% 时需重估成本模型\n\n## 基准假设与待确认清单\n\n- 灭菌方式采用环氧乙烷(EO)灭菌(行业常规)\n",
|
||||
"fields.json": `{"annual_volume":"100000","review":"passed"}`,
|
||||
},
|
||||
}
|
||||
|
||||
Vendored
+1
-1
Submodule deps/agent-intent updated: 55780ae2cc...3674dc4bfd
Vendored
+1
-1
Submodule deps/agent-native updated: c659dc17d5...292142ac1a
@@ -0,0 +1,36 @@
|
||||
# Agent-Squad 目录驱动的编排(能力目录 + 系统功能步)
|
||||
|
||||
对应设计:Agent-Intent 仓 `docs/records/2026-09-29-orchestration-capability-catalog.md`(G3–G5);依赖 agent-intent `3674dc4`(E2 槽位约束 + E4 目录)。
|
||||
上一阶段(入口闸门)见 [2026-09-29-intent-gate-adoption.md](2026-09-29-intent-gate-adoption.md)。
|
||||
|
||||
## 形态
|
||||
|
||||
- `squad.IntentCatalog`(数据视图,squadintent 从识别快照渲染):`Markdown` 注入 LLM 规划器与 leader 提示词;`Specs` 用于计划确定性校验。核心包仍零 import agent-intent(`go list -deps ./squad ./server ./store` 0 命中)。
|
||||
- `StepSpec.IntentID / IntentSlots`:系统功能步声明。LLM 规划器 schema 扩展 `intent_id/intent_slots` 并新增规则 8(命中目录输出意图步、必填槽位不足改 clarify 且用目录提问建议)。
|
||||
- 计划校验 `validateIntentSteps`(fail-closed,同未知 backend 先例):无目录、意图不在目录、缺必填槽位(错误带澄清建议)、枚举不合法 → 整计划拒绝。
|
||||
- 执行路由:`runOneStep` 中 IntentID 步不经 BackendResolver/智能体,由 `IntentRunner` 执行;Runner 只产出内容(patches),引擎统一物化到子工作区;Runner 缺席或 handler 未注册 fail-closed。
|
||||
- `squadintent.Runner`:按 `spec.Handler` 分发宿主处理函数;`EchoRunner` 为演示执行器(物化调用文本,明确声明未真实执行)。
|
||||
- cli:`--intent-root` 启用时自动注册 `intent` 角色与目录/Runner;`AGENT_SQUAD_INTENT_ECHO=1` 切换演示执行器,默认 fail-closed。
|
||||
|
||||
## 验证证据(2026-09-29,go vet/test/-race 全绿;既有 conformance 未改动即过)
|
||||
|
||||
| 层 | 用例 | 期望与结果 |
|
||||
|---|---|---|
|
||||
| 校验 | TestValidateIntentSteps(ok/无目录/未知意图/缺必填槽位/枚举违规) | 全部按预期拒绝或通过 ✅ |
|
||||
| 执行 | TestIntentStepExecutedByRunnerNotBackend | 混合蓝图完成;Runner 收到 IntentID+Slots;产物落盘 ✅ |
|
||||
| 执行 | TestIntentStepWithoutRunnerFailsClosed | 无 Runner → 步骤失败 ✅ |
|
||||
| 校验 | TestIntentStepPlanValidationFailsClosed | 无目录 → 计划即拒(不到执行层) ✅ |
|
||||
| 提示词 | TestLLMPlannerPromptIncludesCatalog / TestLeaderPromptIncludesCatalog | 目录与提问建议进入提示词;未配置时不出现 ✅ |
|
||||
| 适配器 | TestGateCatalogMapsSpecsAndSlots / Runner 分发与 fail-closed / EchoRunner 物化 / nil Gate | 全通过 ✅ |
|
||||
|
||||
## E2E(真实 CLI,外挂意图含 `demo.run.status_query`:必填槽 run_id + 提问建议;蓝图 mixed_demo = 系统功能步 sys1 + agent 步 write1)
|
||||
|
||||
| 用例 | 配置 | 输入 | 实际结果 | 期望 | 判定 |
|
||||
|---|---|---|---|---|---|
|
||||
| F 混合步 | `--intent-root` + `AGENT_SQUAD_INTENT_ECHO=1` | `做一次混合演示` | `completed`,2 步;sys1 产物为系统功能调用物化(intent_id/demo.run.status_query、handler、slot run_id=run-42),write1 为 agent 后端产物 | 系统功能步不经智能体、agent 步照常 ✅ |
|
||||
| G 无执行器 | `--intent-root`,无 ECHO | 同上 | `failed`:`intent runner: handler "taskrun.query_status" 未注册(fail-closed,不静默降级)` | fail-closed ✅ |
|
||||
| 目录输出 | intentctl catalog | 同一意图目录 | 目录 Markdown 含能力、必填槽位、提问建议(守卫 spec 不出现) | 供规划者/领导提示词注入 ✅ |
|
||||
|
||||
## 回退程序
|
||||
|
||||
在上一阶段清单之上追加:删 `squadintent/catalog.go`、`squad/intent.go` 的目录/Runner 段、models 的 IntentID/IntentSlots、planner_llm 的 schema 规则与提示词段、leader 的目录参数、workflow 的意图路由块、cli 的目录/Runner 接线。核心流程恢复为纯 agent 步。
|
||||
@@ -46,3 +46,9 @@
|
||||
- 依赖边界新增:唯 `squadintent` 与 cli 组合根可 import agent-intent;`squad`/`server`/`store` 不感知意图;未配置意图时引擎行为与原实现一致。
|
||||
- 接入与验证记录见 [docs/records/2026-09-29-intent-gate-adoption.md](../docs/records/2026-09-29-intent-gate-adoption.md);跨仓测试报告见 Agent-Intent 仓 docs/records。
|
||||
- 回退:删 `squadintent/` 与 cli 接线、go.mod 两行、submodule 与 `.gitmodules` 条目、一致性脚本对应段;可将 agent-native 回退到 `cc8a6a7`(其 intentgate 为可选包,不回退亦可编译)。
|
||||
|
||||
### 2026-09-29 追加(二):目录驱动的编排
|
||||
|
||||
- `deps/agent-intent` 前进到 `3674dc4bfd0b49191d21f0afab4002381e41a429`(槽位约束与能力目录,E2/E4);`deps/agent-native` 前进到 `292142ac1a8b125488794e9374faee2cb1c8c579`(pin 同步)。
|
||||
- 新增能力目录(`squad.IntentCatalog` 数据视图)与系统功能步(`StepSpec.IntentID` + `IntentRunner`);核心包仍零 import agent-intent。
|
||||
- 接入与验证记录见 [docs/records/2026-09-29-catalog-driven-orchestration.md](../docs/records/2026-09-29-catalog-driven-orchestration.md)。
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ import (
|
||||
// BlueprintRegistry 任务模板注册中心:空表起步。
|
||||
// 命中模板的 Goal 走基线工作流,未命中走规划器自主拆解。
|
||||
type BlueprintRegistry struct {
|
||||
mu sync.RWMutex
|
||||
bps map[string]Blueprint
|
||||
mu sync.RWMutex
|
||||
bps map[string]Blueprint
|
||||
// OnChange 可选钩子:注册/删除后回调(store 层用于落盘)。
|
||||
OnChange func(bp Blueprint)
|
||||
}
|
||||
|
||||
+22
-8
@@ -37,18 +37,25 @@ type Options struct {
|
||||
// IntentGate 可选意图闸门:Run 入口一次确定性裁决(nil = 不启用,
|
||||
// 行为与原实现一致)。见 intent.go;实现方为 squadintent 适配器。
|
||||
IntentGate IntentGate
|
||||
// IntentCatalog 可选能力目录(squadintent 渲染):注入 PlanInput 与 leader 提示词,
|
||||
// 并用于 intent 步骤的确定性校验。nil = 未启用目录驱动编排。
|
||||
IntentCatalog *IntentCatalog
|
||||
// IntentRunner 系统功能步执行器(squadintent 提供)。目录启用而 Runner 缺席时,
|
||||
// intent 步骤 fail-closed。
|
||||
IntentRunner IntentRunner
|
||||
}
|
||||
|
||||
// engineDeps 驱动器依赖(由引擎从 Options 派生)。
|
||||
type engineDeps struct {
|
||||
Registry *RoleRegistry
|
||||
Backends *BackendResolver
|
||||
Evaluator StepEvaluator
|
||||
Checker Checker
|
||||
Sink ProgressSink
|
||||
Workspaces *WorkspaceEngine
|
||||
StepTimeout time.Duration
|
||||
HitlPolicy HitlPolicy
|
||||
Registry *RoleRegistry
|
||||
Backends *BackendResolver
|
||||
Evaluator StepEvaluator
|
||||
Checker Checker
|
||||
Sink ProgressSink
|
||||
Workspaces *WorkspaceEngine
|
||||
StepTimeout time.Duration
|
||||
HitlPolicy HitlPolicy
|
||||
IntentRunner IntentRunner
|
||||
}
|
||||
|
||||
// RunOptions 单次运行的可选项。
|
||||
@@ -120,6 +127,7 @@ func NewEngine(opts Options) (*SquadEngine, error) {
|
||||
Registry: opts.Registry, Backends: opts.Backends, Evaluator: opts.Evaluator,
|
||||
Checker: opts.Checker, Sink: opts.Sink, Workspaces: ws,
|
||||
StepTimeout: opts.StepTimeout, HitlPolicy: opts.HitlPolicy,
|
||||
IntentRunner: opts.IntentRunner,
|
||||
}
|
||||
return &SquadEngine{opts: opts, deps: deps, ws: ws, runLocks: map[string]*sync.Mutex{}}, nil
|
||||
}
|
||||
@@ -212,10 +220,16 @@ func (e *SquadEngine) planAndExecute(ctx context.Context, st *runState, runOpts
|
||||
bpMatch, _ := e.opts.Blueprints.Match(meta.Goal.Text, runOpts.TaskKey)
|
||||
planOut, err := e.opts.Planner.Plan(ctx, PlanInput{
|
||||
Goal: meta.Goal, Blueprint: bpPtr(bpMatch), Registry: e.opts.Registry, RunID: meta.ID,
|
||||
IntentCatalog: e.opts.IntentCatalog,
|
||||
})
|
||||
if err != nil {
|
||||
return e.failNew(meta, err)
|
||||
}
|
||||
// 目录驱动编排的确定性校验:intent 步骤命中目录、必填槽位齐、枚举合法(fail-closed)。
|
||||
if err := validateIntentSteps(planOut.Plan, e.opts.IntentCatalog); err != nil {
|
||||
e.deps.Sink.Emit(Event{Type: EvRunFailed, RunID: meta.ID, Text: err.Error(), At: time.Now()})
|
||||
return e.failNew(meta, err)
|
||||
}
|
||||
// 规划歧义:挂起澄清(auto/disabled 模式直接失败,不猜计划)。
|
||||
if planOut.Clarification != nil {
|
||||
if drv.policy(st).mode() != HitlInteractive {
|
||||
|
||||
@@ -125,9 +125,9 @@ func TestEngineHappyPath(t *testing.T) {
|
||||
func TestEngineGapHitlSuspendRespondAndRestartRecovery(t *testing.T) {
|
||||
script := map[string]map[string]string{
|
||||
"01_researcher": {
|
||||
"notes.md": "# 笔记\n内容",
|
||||
"notes.md": "# 笔记\n内容",
|
||||
"fields.json": `{"k":"v"}`,
|
||||
"gaps.json": `[{"item":"缺少产量","blocking":true}]`,
|
||||
"gaps.json": `[{"item":"缺少产量","blocking":true}]`,
|
||||
},
|
||||
"02_writer": {"output.md": goodReport},
|
||||
"03_reviewer": {"output.md": goodReport},
|
||||
@@ -297,9 +297,9 @@ func TestEngineQualityReviewManualPass(t *testing.T) {
|
||||
func TestEngineAutoModeGapFallbackAssumption(t *testing.T) {
|
||||
script := map[string]map[string]string{
|
||||
"01_researcher": {
|
||||
"notes.md": "# 笔记\n内容",
|
||||
"notes.md": "# 笔记\n内容",
|
||||
"fields.json": `{"k":"v"}`,
|
||||
"gaps.json": `[{"item":"缺少产量","blocking":true}]`,
|
||||
"gaps.json": `[{"item":"缺少产量","blocking":true}]`,
|
||||
},
|
||||
"02_writer": {"output.md": goodReport},
|
||||
"03_reviewer": {"output.md": goodReport},
|
||||
|
||||
+11
-11
@@ -12,22 +12,22 @@ type StepEvaluator interface {
|
||||
|
||||
// EvaluateRequest 评估输入。
|
||||
type EvaluateRequest struct {
|
||||
RunID string
|
||||
Step StepSpec
|
||||
Workspace string // 运行工作区根目录
|
||||
Output string // 步骤主产出文本
|
||||
RunID string
|
||||
Step StepSpec
|
||||
Workspace string // 运行工作区根目录
|
||||
Output string // 步骤主产出文本
|
||||
Blackboard *BlackboardState
|
||||
}
|
||||
|
||||
// EvaluationResult 评估结论与动态调整建议(与产品 squad 语义对齐)。
|
||||
type EvaluationResult struct {
|
||||
StepID string `json:"step_id"`
|
||||
Action StepAction `json:"action"`
|
||||
Reason string `json:"reason"`
|
||||
NewAssumptions []AssumptionItem `json:"new_assumptions,omitempty"`
|
||||
ResolvedGaps []string `json:"resolved_gaps,omitempty"`
|
||||
DynamicStepsToAdd []StepSpec `json:"dynamic_steps_to_add,omitempty"`
|
||||
StepsToSkip []string `json:"steps_to_skip,omitempty"`
|
||||
StepID string `json:"step_id"`
|
||||
Action StepAction `json:"action"`
|
||||
Reason string `json:"reason"`
|
||||
NewAssumptions []AssumptionItem `json:"new_assumptions,omitempty"`
|
||||
ResolvedGaps []string `json:"resolved_gaps,omitempty"`
|
||||
DynamicStepsToAdd []StepSpec `json:"dynamic_steps_to_add,omitempty"`
|
||||
StepsToSkip []string `json:"steps_to_skip,omitempty"`
|
||||
// NeedsApproval 为 true 时,本批调整先经 HITL 审批再应用。
|
||||
NeedsApproval bool `json:"needs_approval,omitempty"`
|
||||
ApprovalNote string `json:"approval_note,omitempty"`
|
||||
|
||||
@@ -68,7 +68,7 @@ func TestDynamicAdjustmentWithApproval(t *testing.T) {
|
||||
"01_researcher": {"notes.md": "# 笔记\n内容"},
|
||||
"02_writer": {"output.md": goodReport},
|
||||
// 动态加步发生在子工作区构建之后,runOneStep 按索引兜底目录名。
|
||||
"03_writer": {"price.md": priceNote},
|
||||
"03_writer": {"price.md": priceNote},
|
||||
"03_reviewer": {"review.md": goodReport},
|
||||
}
|
||||
engine := newDynamicEngine(t, script, &scriptedEvaluator{})
|
||||
|
||||
+21
-21
@@ -40,11 +40,11 @@ func (p HitlPolicy) mode() HitlMode {
|
||||
type HumanRequestKind string
|
||||
|
||||
const (
|
||||
HitlClarification HumanRequestKind = "clarification" // 规划歧义澄清
|
||||
HitlConfirmation HumanRequestKind = "confirmation" // 计划/高风险步骤确认
|
||||
HitlGap HumanRequestKind = "gap" // 阻塞缺口
|
||||
HitlAdjustment HumanRequestKind = "adjustment" // 动态调整审批
|
||||
HitlPermission HumanRequestKind = "permission" // 权限升级审批
|
||||
HitlClarification HumanRequestKind = "clarification" // 规划歧义澄清
|
||||
HitlConfirmation HumanRequestKind = "confirmation" // 计划/高风险步骤确认
|
||||
HitlGap HumanRequestKind = "gap" // 阻塞缺口
|
||||
HitlAdjustment HumanRequestKind = "adjustment" // 动态调整审批
|
||||
HitlPermission HumanRequestKind = "permission" // 权限升级审批
|
||||
HitlQualityReview HumanRequestKind = "quality_review" // 质检超限人工裁决
|
||||
HitlEscalation HumanRequestKind = "escalation" // leader/成员升级问人(委派不了/需要拍板)
|
||||
)
|
||||
@@ -59,9 +59,9 @@ const (
|
||||
// WaitCondition 挂起请求的自动唤醒条件(等事件而非只等人)。
|
||||
// 条件满足时引擎合成一条自动决策续跑——唤醒报告的是事实,不是业务完成。
|
||||
type WaitCondition struct {
|
||||
Type string `json:"type"` // run_done | field_set
|
||||
RunID string `json:"run_id,omitempty"` // run_done:等待该运行到达终态
|
||||
Field string `json:"field,omitempty"` // field_set:等待黑板出现该字段
|
||||
Type string `json:"type"` // run_done | field_set
|
||||
RunID string `json:"run_id,omitempty"` // run_done:等待该运行到达终态
|
||||
Field string `json:"field,omitempty"` // field_set:等待黑板出现该字段
|
||||
}
|
||||
|
||||
// MaxWakeupFires 单个挂起请求自动唤醒次数上限(防失控护栏)。
|
||||
@@ -69,14 +69,14 @@ const MaxWakeupFires = 5
|
||||
|
||||
// HumanRequest 一次结构化人工请求(可持久化、可恢复、按 ID 幂等回应)。
|
||||
type HumanRequest struct {
|
||||
ID string `json:"id"`
|
||||
RunID string `json:"run_id"`
|
||||
StepID string `json:"step_id,omitempty"`
|
||||
Kind HumanRequestKind `json:"kind"`
|
||||
Severity string `json:"severity,omitempty"` // action_required(默认)/ attention
|
||||
Question string `json:"question"`
|
||||
Options []HumanOption `json:"options,omitempty"`
|
||||
Context string `json:"context,omitempty"` // 脱敏摘要(计划/缺口/质检报告)
|
||||
ID string `json:"id"`
|
||||
RunID string `json:"run_id"`
|
||||
StepID string `json:"step_id,omitempty"`
|
||||
Kind HumanRequestKind `json:"kind"`
|
||||
Severity string `json:"severity,omitempty"` // action_required(默认)/ attention
|
||||
Question string `json:"question"`
|
||||
Options []HumanOption `json:"options,omitempty"`
|
||||
Context string `json:"context,omitempty"` // 脱敏摘要(计划/缺口/质检报告)
|
||||
// WaitCondition 非空时,条件满足可自动唤醒(人工回应仍然优先)。
|
||||
WaitCondition *WaitCondition `json:"wait_condition,omitempty"`
|
||||
// WakeupFires 已自动唤醒次数(护栏:达到 MaxWakeupFires 后过期)。
|
||||
@@ -114,11 +114,11 @@ func newRequestID() string {
|
||||
|
||||
// hitlGate 引擎内部:一次挂起的完整上下文(落盘 pending.json)。
|
||||
type hitlGate struct {
|
||||
Request HumanRequest `json:"request"`
|
||||
Plan *SquadPlan `json:"plan,omitempty"` // confirmation 时携带候选计划
|
||||
Steps []StepSpec `json:"steps,omitempty"` // adjustment:建议追加的步骤
|
||||
SkipIDs []string `json:"skip_ids,omitempty"` // adjustment:建议跳过的步骤
|
||||
PrevStatus RunStatus `json:"prev_status"` // 回应后恢复到的状态
|
||||
Request HumanRequest `json:"request"`
|
||||
Plan *SquadPlan `json:"plan,omitempty"` // confirmation 时携带候选计划
|
||||
Steps []StepSpec `json:"steps,omitempty"` // adjustment:建议追加的步骤
|
||||
SkipIDs []string `json:"skip_ids,omitempty"` // adjustment:建议跳过的步骤
|
||||
PrevStatus RunStatus `json:"prev_status"` // 回应后恢复到的状态
|
||||
}
|
||||
|
||||
// mergeHitlPolicy 按全局 → 角色 → Goal 顺序取窄者(后设置者覆盖)。
|
||||
|
||||
+121
-1
@@ -1,6 +1,11 @@
|
||||
package squad
|
||||
|
||||
import "context"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"agent-native/exec"
|
||||
)
|
||||
|
||||
// IntentRejection 守卫拦截:拒绝该次运行并给出引导("识别即拒绝"语义)。
|
||||
type IntentRejection struct {
|
||||
@@ -29,3 +34,118 @@ type IntentDecision struct {
|
||||
type IntentGate interface {
|
||||
Decide(ctx context.Context, goal Goal, opts RunOptions) (*IntentDecision, error)
|
||||
}
|
||||
|
||||
// ---- 能力目录(目录驱动的编排)----
|
||||
|
||||
// IntentSlotView 目录中的槽位视图:规划者/领导据此知道缺什么信息、建议怎么问。
|
||||
type IntentSlotView struct {
|
||||
Key string `json:"key"`
|
||||
Required bool `json:"required,omitempty"`
|
||||
Label string `json:"label,omitempty"`
|
||||
Question string `json:"question,omitempty"`
|
||||
Enum []string `json:"enum,omitempty"`
|
||||
Format string `json:"format,omitempty"`
|
||||
}
|
||||
|
||||
// IntentSpecView 目录中的单条可调用能力。
|
||||
type IntentSpecView struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Risk string `json:"risk,omitempty"`
|
||||
RequiresConfirm bool `json:"requires_confirm,omitempty"`
|
||||
Handler string `json:"handler,omitempty"`
|
||||
Slots []IntentSlotView `json:"slots,omitempty"`
|
||||
RequiredSlots []string `json:"required_slots,omitempty"`
|
||||
Metadata map[string]string `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// IntentCatalog 意图能力目录:squadintent 从识别快照渲染的数据视图,
|
||||
// squad 只消费不构造(不 import agent-intent)。Markdown 注入规划者/领导提示词;
|
||||
// Specs 用于计划的确定性校验(意图存在、必填槽位齐、枚举合法)。
|
||||
type IntentCatalog struct {
|
||||
Revision int64
|
||||
Markdown string
|
||||
Specs map[string]IntentSpecView
|
||||
}
|
||||
|
||||
// intentCatalogMd nil 安全的目录文本(未启用时为空串)。
|
||||
func (c *IntentCatalog) intentCatalogMd() string {
|
||||
if c == nil {
|
||||
return ""
|
||||
}
|
||||
return c.Markdown
|
||||
}
|
||||
|
||||
// Get 按 id 查目录能力。
|
||||
func (c *IntentCatalog) Get(id string) (IntentSpecView, bool) {
|
||||
if c == nil {
|
||||
return IntentSpecView{}, false
|
||||
}
|
||||
v, ok := c.Specs[id]
|
||||
return v, ok
|
||||
}
|
||||
|
||||
// IntentStep 系统功能步:不经 BackendResolver/智能体,由宿主 IntentRunner 直接执行。
|
||||
type IntentStep struct {
|
||||
IntentID string
|
||||
Slots map[string]string
|
||||
Title string
|
||||
WorkDir string
|
||||
Targets []string
|
||||
}
|
||||
|
||||
// IntentRunner 系统功能执行器(squadintent 提供实现:查 spec.Handler 并调宿主处理函数)。
|
||||
type IntentRunner interface {
|
||||
Run(ctx context.Context, step IntentStep) (*exec.Result, error)
|
||||
}
|
||||
|
||||
// DefaultIntentRole 系统功能步的默认角色 key(无后端语义,仅满足编制覆盖)。
|
||||
const DefaultIntentRole = "intent"
|
||||
|
||||
// DefaultIntentRoleSpec 返回系统功能步的默认角色(组合根在启用意图目录时注册)。
|
||||
func DefaultIntentRoleSpec() RoleSpec {
|
||||
return RoleSpec{Role: DefaultIntentRole, Name: "系统能力", Backend: "stub", Keywords: []string{"系统功能"}}
|
||||
}
|
||||
|
||||
// validateIntentSteps 计划落盘前的确定性校验:意图步必须命中目录、必填槽位齐、枚举合法。
|
||||
// fail-closed:任一不通过即整计划拒绝(与未知 backend 拒绝同构)。
|
||||
func validateIntentSteps(plan *SquadPlan, catalog *IntentCatalog) error {
|
||||
if plan == nil {
|
||||
return nil
|
||||
}
|
||||
for _, step := range plan.Workflow.Steps {
|
||||
if step.IntentID == "" {
|
||||
continue
|
||||
}
|
||||
if catalog == nil {
|
||||
return fmt.Errorf("intent 步骤 %s:引擎未装配意图目录(IntentCatalog),无法执行系统功能步", step.ID)
|
||||
}
|
||||
view, ok := catalog.Get(step.IntentID)
|
||||
if !ok {
|
||||
return fmt.Errorf("intent 步骤 %s:目录中不存在意图 %s", step.ID, step.IntentID)
|
||||
}
|
||||
for _, sl := range view.Slots {
|
||||
v, given := step.IntentSlots[sl.Key]
|
||||
if sl.Required && (!given || v == "") {
|
||||
q := sl.Question
|
||||
if q == "" {
|
||||
q = "请提供 " + sl.Key
|
||||
}
|
||||
return fmt.Errorf("intent 步骤 %s:意图 %s 缺必填槽位 %s(澄清建议:%s)", step.ID, step.IntentID, sl.Key, q)
|
||||
}
|
||||
if given && len(sl.Enum) > 0 && !containsString(sl.Enum, v) {
|
||||
return fmt.Errorf("intent 步骤 %s:槽位 %s 的值 %q 不在枚举 %v 内", step.ID, sl.Key, v, sl.Enum)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsString(list []string, s string) bool {
|
||||
for _, v := range list {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package squad
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"agent-native/exec"
|
||||
)
|
||||
|
||||
type stubRunner struct {
|
||||
calls []IntentStep
|
||||
text string
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *stubRunner) Run(ctx context.Context, step IntentStep) (*exec.Result, error) {
|
||||
r.calls = append(r.calls, step)
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
patches := map[string][]byte{}
|
||||
for _, t := range step.Targets {
|
||||
patches[t] = []byte(r.text)
|
||||
}
|
||||
return &exec.Result{Success: true, Patches: patches, Logs: "stub-intent"}, nil
|
||||
}
|
||||
|
||||
func testCatalog() *IntentCatalog {
|
||||
return &IntentCatalog{
|
||||
Revision: 1,
|
||||
Markdown: "## demo.run.status_query — 查询运行进度\n- 槽位 run_id(必填)\n - 提问建议:要查询哪个运行?\n",
|
||||
Specs: map[string]IntentSpecView{
|
||||
"demo.run.status_query": {
|
||||
ID: "demo.run.status_query", Description: "查询运行进度", Handler: "taskrun.query_status",
|
||||
Slots: []IntentSlotView{
|
||||
{Key: "run_id", Required: true, Label: "运行编号", Question: "要查询哪个运行?"},
|
||||
{Key: "env", Enum: []string{"prod", "staging"}},
|
||||
},
|
||||
RequiredSlots: []string{"run_id"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIntentSteps(t *testing.T) {
|
||||
step := func(id, intent string, slots map[string]string) SquadPlan {
|
||||
return SquadPlan{Workflow: WorkflowPlan{Steps: []StepSpec{{ID: id, IntentID: intent, IntentSlots: slots}}}}
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
plan SquadPlan
|
||||
catalog *IntentCatalog
|
||||
wantErr string
|
||||
}{
|
||||
{"ok", step("s1", "demo.run.status_query", map[string]string{"run_id": "run-1"}), testCatalog(), ""},
|
||||
{"no_catalog", step("s1", "demo.run.status_query", nil), nil, "未装配意图目录"},
|
||||
{"unknown_intent", step("s1", "demo.nope.x", nil), testCatalog(), "不存在意图"},
|
||||
{"missing_required", step("s1", "demo.run.status_query", nil), testCatalog(), "缺必填槽位 run_id(澄清建议:要查询哪个运行?)"},
|
||||
{"enum_violation", step("s1", "demo.run.status_query", map[string]string{"run_id": "run-1", "env": "dev"}), testCatalog(), "不在枚举"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := validateIntentSteps(&tc.plan, tc.catalog)
|
||||
if tc.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("expected error containing %q, got %v", tc.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newCatalogTestEngine(t *testing.T, runner IntentRunner, catalog *IntentCatalog) *SquadEngine {
|
||||
t.Helper()
|
||||
engine, _ := newTestEngine(t, NewRulePlanner(),
|
||||
map[string]map[string]string{"02_writer": {"output.md": goodReport}}, nil)
|
||||
if err := engine.Blueprints().Register(Blueprint{
|
||||
Key: "mixed_demo", Title: "混合演示", Match: []string{"混合演示"},
|
||||
Steps: []StepSpec{
|
||||
{ID: "sys1", Role: "researcher", Title: "查询运行进度",
|
||||
IntentID: "demo.run.status_query", IntentSlots: map[string]string{"run_id": "run-42"}},
|
||||
{ID: "write1", Role: "writer", Title: "汇总成文", Targets: []string{"output.md"}},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 产品侧经 Options 在 NewEngine 传入;测试在构造后注入,需同时更新
|
||||
// engineDeps(步骤执行走 deps 副本,构建期已定型)。
|
||||
engine.opts.IntentCatalog = catalog
|
||||
engine.opts.IntentRunner = runner
|
||||
engine.deps.IntentRunner = runner
|
||||
return engine
|
||||
}
|
||||
|
||||
func TestIntentStepExecutedByRunnerNotBackend(t *testing.T) {
|
||||
runner := &stubRunner{text: "# 查询结果\n\nrun-42 进度 100%,内容满足最小质检长度要求,作为系统功能步产物。\n"}
|
||||
engine := newCatalogTestEngine(t, runner, testCatalog())
|
||||
res, err := engine.Run(context.Background(), Goal{Text: "做一次混合演示", HitlMode: HitlAuto}, RunOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Status != RunCompleted {
|
||||
t.Fatalf("expected completed, got %s (err=%s)", res.Status, res.Error)
|
||||
}
|
||||
if len(runner.calls) != 1 || runner.calls[0].IntentID != "demo.run.status_query" || runner.calls[0].Slots["run_id"] != "run-42" {
|
||||
t.Fatalf("runner calls: %+v", runner.calls)
|
||||
}
|
||||
// 系统功能步产物落盘。
|
||||
sysOut := filepath.Join(res.WorkDir, "tasks", "01_researcher", "output.md")
|
||||
if data, err := os.ReadFile(sysOut); err != nil || !strings.Contains(string(data), "查询结果") {
|
||||
t.Fatalf("intent step deliverable missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentStepWithoutRunnerFailsClosed(t *testing.T) {
|
||||
engine := newCatalogTestEngine(t, nil, testCatalog())
|
||||
res, err := engine.Run(context.Background(), Goal{Text: "做一次混合演示", HitlMode: HitlAuto}, RunOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Status != RunFailed || !strings.Contains(res.Error, "IntentRunner") {
|
||||
t.Fatalf("expected fail-closed runner error, got %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntentStepPlanValidationFailsClosed(t *testing.T) {
|
||||
// 目录缺席时,含 intent 步的蓝图在计划校验即被拒(不到执行层)。
|
||||
engine := newCatalogTestEngine(t, &stubRunner{text: "x"}, nil)
|
||||
res, err := engine.Run(context.Background(), Goal{Text: "做一次混合演示", HitlMode: HitlAuto}, RunOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if res.Status != RunFailed || !strings.Contains(res.Error, "未装配意图目录") {
|
||||
t.Fatalf("expected validation failure, got %+v", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMPlannerPromptIncludesCatalog(t *testing.T) {
|
||||
p := NewLLMPlanner(nil, "m")
|
||||
input := PlanInput{
|
||||
Goal: Goal{Text: "目标"},
|
||||
Registry: NewRoleRegistry(),
|
||||
IntentCatalog: testCatalog(),
|
||||
}
|
||||
got := p.buildPrompt(input)
|
||||
for _, want := range []string{"系统能力目录", "demo.run.status_query", "提问建议:要查询哪个运行"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("prompt missing %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(NewLLMPlanner(nil, "m").buildPrompt(PlanInput{Goal: Goal{Text: "g"}, Registry: NewRoleRegistry()}), "系统能力目录") {
|
||||
t.Fatal("catalog must be absent when not configured")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLeaderPromptIncludesCatalog(t *testing.T) {
|
||||
md := leaderPrompt(nil, testCatalog().Markdown)
|
||||
if !strings.Contains(md, "系统能力目录") || !strings.Contains(md, "demo.run.status_query") {
|
||||
t.Fatalf("leader prompt missing catalog:\n%s", md)
|
||||
}
|
||||
if strings.Contains(leaderPrompt(nil, ""), "系统能力目录") {
|
||||
t.Fatal("empty catalog must not add section")
|
||||
}
|
||||
}
|
||||
+31
-27
@@ -20,7 +20,7 @@ func execRequest(workDir, prompt, instructions string, targets []string, timeout
|
||||
Instructions: instructions,
|
||||
Targets: targets,
|
||||
Timeout: timeout,
|
||||
OnEvent: func(ev exec.ProcessEvent) {},
|
||||
OnEvent: func(ev exec.ProcessEvent) {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,31 +35,31 @@ const DefaultMaxLeaderTurns = 8
|
||||
|
||||
// LeaderState leader 循环状态(workspace leader.json 持久化,重启可恢复)。
|
||||
type LeaderState struct {
|
||||
Turns int `json:"turns"`
|
||||
MaxTurns int `json:"max_turns"`
|
||||
LeaderRole string `json:"leader_role"`
|
||||
Delegations []DelegationRecord `json:"delegations"`
|
||||
Turns int `json:"turns"`
|
||||
MaxTurns int `json:"max_turns"`
|
||||
LeaderRole string `json:"leader_role"`
|
||||
Delegations []DelegationRecord `json:"delegations"`
|
||||
}
|
||||
|
||||
// DelegationRecord 一次已执行的委派(历史注入下一轮 leader 上下文)。
|
||||
type DelegationRecord struct {
|
||||
Turn int `json:"turn"`
|
||||
Member string `json:"member"`
|
||||
Title string `json:"title"`
|
||||
Targets []string `json:"targets,omitempty"`
|
||||
Status string `json:"status"` // completed / failed / degraded
|
||||
Error string `json:"error,omitempty"`
|
||||
Turn int `json:"turn"`
|
||||
Member string `json:"member"`
|
||||
Title string `json:"title"`
|
||||
Targets []string `json:"targets,omitempty"`
|
||||
Status string `json:"status"` // completed / failed / degraded
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// delegation leader 单轮产出的结构化决策。
|
||||
type delegation struct {
|
||||
Action string `json:"action"` // delegate | escalate | close | failed
|
||||
Member string `json:"member,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Targets []string `json:"targets,omitempty"`
|
||||
Briefing string `json:"briefing,omitempty"` // 给成员的补充指令
|
||||
Question string `json:"question,omitempty"` // escalate 时的问题
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Action string `json:"action"` // delegate | escalate | close | failed
|
||||
Member string `json:"member,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Targets []string `json:"targets,omitempty"`
|
||||
Briefing string `json:"briefing,omitempty"` // 给成员的补充指令
|
||||
Question string `json:"question,omitempty"` // escalate 时的问题
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
const leaderSubWorkspace = "tasks/00_leader"
|
||||
@@ -83,7 +83,7 @@ func (st *runState) saveLeader() {
|
||||
}
|
||||
|
||||
// leaderPrompt 组装 leader 的系统指令(协调契约 + 通用守则)。
|
||||
func leaderPrompt(members []RoleSpec) string {
|
||||
func leaderPrompt(members []RoleSpec, catalogMd string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(`你是本小队的 LEADER。你被激活的唯一职责是协调,不是自己实现。
|
||||
|
||||
@@ -108,6 +108,10 @@ func leaderPrompt(members []RoleSpec) string {
|
||||
for _, m := range members {
|
||||
fmt.Fprintf(&b, "- %s — %s — skills=%v — %v\n", m.Role, m.Name, m.Skills, m.Responsibilities)
|
||||
}
|
||||
if catalogMd != "" {
|
||||
b.WriteString("\n系统能力目录(供你判断哪些子任务不必派成员、以及该向用户收集什么信息;缺失必填信息时 escalate 并使用提问建议):\n")
|
||||
b.WriteString(catalogMd)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -189,7 +193,7 @@ func (e *SquadEngine) runLeaderLoop(ctx context.Context, st *runState) (*RunResu
|
||||
if leader.BashPermission == "ask" && !st.Approved["role:"+leader.Role] {
|
||||
if drv.policy(st).mode() != HitlInteractive {
|
||||
return &RunResult{RunID: st.Meta.ID, Status: RunFailed, Plan: planPtr(st),
|
||||
Error: "leader role requires bash approval but hitl mode is " + string(drv.policy(st).mode()),
|
||||
Error: "leader role requires bash approval but hitl mode is " + string(drv.policy(st).mode()),
|
||||
WorkDir: st.Meta.WorkDir}, nil, nil
|
||||
}
|
||||
req := &HumanRequest{
|
||||
@@ -213,7 +217,7 @@ func (e *SquadEngine) runLeaderLoop(ctx context.Context, st *runState) (*RunResu
|
||||
if err := os.WriteFile(filepath.Join(subDir, "TASK.md"), []byte(leaderTaskBrief(st)), 0o644); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(subDir, "AGENTS.md"), []byte(leaderPrompt(members)), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(subDir, "AGENTS.md"), []byte(leaderPrompt(members, e.opts.IntentCatalog.intentCatalogMd())), 0o644); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
@@ -228,7 +232,7 @@ func (e *SquadEngine) runLeaderLoop(ctx context.Context, st *runState) (*RunResu
|
||||
}
|
||||
prompt := readFileOr(filepath.Join(subDir, "TASK.md"), leaderTaskBrief(st))
|
||||
prompt = st.injectSteering(prompt)
|
||||
instructions := readFileOr(filepath.Join(subDir, "AGENTS.md"), leaderPrompt(members))
|
||||
instructions := readFileOr(filepath.Join(subDir, "AGENTS.md"), leaderPrompt(members, e.opts.IntentCatalog.intentCatalogMd()))
|
||||
result, execErr := backend.Execute(ctx, execRequest(subDir, prompt, instructions, []string{"delegation.json"}, e.deps.StepTimeout))
|
||||
if execErr != nil || result == nil || !result.Success {
|
||||
reason := "leader execution failed"
|
||||
@@ -264,7 +268,7 @@ func (e *SquadEngine) runLeaderLoop(ctx context.Context, st *runState) (*RunResu
|
||||
}
|
||||
req := &HumanRequest{
|
||||
ID: newRequestID(), RunID: st.Meta.ID, Kind: HitlEscalation, Severity: SeverityActionRequired,
|
||||
StepID: fmt.Sprintf("leader_turn_%d", st.Leader.Turns),
|
||||
StepID: fmt.Sprintf("leader_turn_%d", st.Leader.Turns),
|
||||
Question: firstNonEmpty(dec.Question, "leader 需要人工拍板:") + "(原因:" + dec.Reason + ")",
|
||||
Options: []HumanOption{
|
||||
{Value: "continue", Label: "已在回答中给出决定,leader 继续"},
|
||||
@@ -285,11 +289,11 @@ func (e *SquadEngine) runLeaderLoop(ctx context.Context, st *runState) (*RunResu
|
||||
return e.leaderFail(st, fmt.Sprintf("leader 委派了编制外成员 %q", dec.Member)), nil, nil
|
||||
}
|
||||
step := StepSpec{
|
||||
ID: fmt.Sprintf("m_%02d_%s", st.Leader.Turns, member.Role),
|
||||
Role: member.Role,
|
||||
Title: firstNonEmpty(dec.Title, "子任务"),
|
||||
ID: fmt.Sprintf("m_%02d_%s", st.Leader.Turns, member.Role),
|
||||
Role: member.Role,
|
||||
Title: firstNonEmpty(dec.Title, "子任务"),
|
||||
SubWorkspace: fmt.Sprintf("tasks/%02d_%s", st.Leader.Turns, member.Role),
|
||||
Status: "pending",
|
||||
Status: "pending",
|
||||
}
|
||||
if len(dec.Targets) > 0 {
|
||||
step.Targets = dec.Targets
|
||||
|
||||
+52
-47
@@ -16,22 +16,22 @@ const (
|
||||
|
||||
// RoleSpec 声明一个小队角色的完整设定。空表起步:库不预置任何角色。
|
||||
type RoleSpec struct {
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Source RoleSource `json:"source,omitempty"`
|
||||
BaseRole string `json:"base_role,omitempty"`
|
||||
Backend string `json:"backend,omitempty"` // 绑定的智能体名:stub/llm/agent-native/pi-agent/自定义注册名
|
||||
Model string `json:"model,omitempty"` // 角色级模型(backend 支持时生效)
|
||||
ToolProfile string `json:"tool_profile,omitempty"` // agent-native ToolProfile:workspace/reasoning
|
||||
Tools []string `json:"tools,omitempty"`
|
||||
Skills []string `json:"skills,omitempty"` // 技能标识(backend 支持时注入)
|
||||
BashPermission string `json:"bash_permission"` // allow / ask / deny
|
||||
Responsibilities []string `json:"responsibilities,omitempty"`
|
||||
ForbiddenActions []string `json:"forbidden_actions,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"` // 规则规划器推荐与匹配用
|
||||
Instructions string `json:"instructions,omitempty"` // 内联指令(优先)
|
||||
InstructionsFile string `json:"instructions_file,omitempty"` // 指令文件(数据目录相对或绝对路径)
|
||||
HitlPolicy *HitlPolicy `json:"hitl_policy,omitempty"` // 角色级 HITL 策略(覆盖全局)
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name"`
|
||||
Source RoleSource `json:"source,omitempty"`
|
||||
BaseRole string `json:"base_role,omitempty"`
|
||||
Backend string `json:"backend,omitempty"` // 绑定的智能体名:stub/llm/agent-native/pi-agent/自定义注册名
|
||||
Model string `json:"model,omitempty"` // 角色级模型(backend 支持时生效)
|
||||
ToolProfile string `json:"tool_profile,omitempty"` // agent-native ToolProfile:workspace/reasoning
|
||||
Tools []string `json:"tools,omitempty"`
|
||||
Skills []string `json:"skills,omitempty"` // 技能标识(backend 支持时注入)
|
||||
BashPermission string `json:"bash_permission"` // allow / ask / deny
|
||||
Responsibilities []string `json:"responsibilities,omitempty"`
|
||||
ForbiddenActions []string `json:"forbidden_actions,omitempty"`
|
||||
Keywords []string `json:"keywords,omitempty"` // 规则规划器推荐与匹配用
|
||||
Instructions string `json:"instructions,omitempty"` // 内联指令(优先)
|
||||
InstructionsFile string `json:"instructions_file,omitempty"` // 指令文件(数据目录相对或绝对路径)
|
||||
HitlPolicy *HitlPolicy `json:"hitl_policy,omitempty"` // 角色级 HITL 策略(覆盖全局)
|
||||
// MaxConcurrentTasks 该角色同时执行步骤的上限(<=1 视为 1);
|
||||
// 引擎在解析后端时以并发限流包装实现(对象级串行语义)。
|
||||
MaxConcurrentTasks int `json:"max_concurrent_tasks,omitempty"`
|
||||
@@ -55,20 +55,20 @@ type MaterialFile struct {
|
||||
|
||||
// Blueprint 已注册的任务模板:命中的 Goal 走基线工作流,未注册走自主规划。
|
||||
type Blueprint struct {
|
||||
Key string `json:"key"` // 模板键(可由 BlueprintMatcher 匹配 Goal.Text)
|
||||
Title string `json:"title"`
|
||||
Match []string `json:"match,omitempty"` // 关键词命中即视为命中模板
|
||||
Description string `json:"description,omitempty"`
|
||||
Agents []RoleSpec `json:"agents,omitempty"` // 默认编制(引用已注册角色或内联角色)
|
||||
Steps []StepSpec `json:"steps"`
|
||||
Template string `json:"template,omitempty"` // 交付物骨架模板(Markdown)
|
||||
Key string `json:"key"` // 模板键(可由 BlueprintMatcher 匹配 Goal.Text)
|
||||
Title string `json:"title"`
|
||||
Match []string `json:"match,omitempty"` // 关键词命中即视为命中模板
|
||||
Description string `json:"description,omitempty"`
|
||||
Agents []RoleSpec `json:"agents,omitempty"` // 默认编制(引用已注册角色或内联角色)
|
||||
Steps []StepSpec `json:"steps"`
|
||||
Template string `json:"template,omitempty"` // 交付物骨架模板(Markdown)
|
||||
}
|
||||
|
||||
// PlanningMode 规划模式。
|
||||
type PlanningMode string
|
||||
|
||||
const (
|
||||
PlanModeBlueprint PlanningMode = "blueprint" // 命中模板:基线步骤 + 动态微调
|
||||
PlanModeBlueprint PlanningMode = "blueprint" // 命中模板:基线步骤 + 动态微调
|
||||
PlanModeAutonomous PlanningMode = "autonomous" // 未命中模板:规划器自主拆解
|
||||
)
|
||||
|
||||
@@ -87,6 +87,11 @@ type StepSpec struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
// RequiresApproval 为 true 时,该步骤启动前进入 HITL 计划确认。
|
||||
RequiresApproval bool `json:"requires_approval,omitempty"`
|
||||
// IntentID 非空 = 系统功能步:不经 BackendResolver/智能体,由 IntentRunner 直接执行
|
||||
//(计划确定性校验:意图须在目录内且必填槽位齐)。
|
||||
IntentID string `json:"intent_id,omitempty"`
|
||||
// IntentSlots 系统功能步的槽位取值(规划者按目录填写)。
|
||||
IntentSlots map[string]string `json:"intent_slots,omitempty"`
|
||||
// AllowDynamicAdjustment 允许评估器在步骤后做动态调整(加步/跳步/改步)。
|
||||
AllowDynamicAdjustment bool `json:"allow_dynamic_adjustment,omitempty"`
|
||||
// Critical: nil(默认)= 关键步骤失败即终止;false = 非关键,失败降级继续。
|
||||
@@ -109,9 +114,9 @@ type WorkflowPlan struct {
|
||||
|
||||
// SquadConfig 激活的小队编制(与产品 squad.json 字段兼容)。
|
||||
type SquadConfig struct {
|
||||
SquadName string `json:"squad_name"`
|
||||
Version string `json:"version"`
|
||||
Agents []RoleSpec `json:"agents"`
|
||||
SquadName string `json:"squad_name"`
|
||||
Version string `json:"version"`
|
||||
Agents []RoleSpec `json:"agents"`
|
||||
}
|
||||
|
||||
// ---- 黑板 ----
|
||||
@@ -178,11 +183,11 @@ type ContextIndex struct {
|
||||
|
||||
// QualityReport 交付检查报告。
|
||||
type QualityReport struct {
|
||||
Passed bool `json:"passed"`
|
||||
Score float64 `json:"score"`
|
||||
Violations []string `json:"violations,omitempty"`
|
||||
MissingSections []string `json:"missing_sections,omitempty"`
|
||||
Placeholders []string `json:"placeholders,omitempty"`
|
||||
Passed bool `json:"passed"`
|
||||
Score float64 `json:"score"`
|
||||
Violations []string `json:"violations,omitempty"`
|
||||
MissingSections []string `json:"missing_sections,omitempty"`
|
||||
Placeholders []string `json:"placeholders,omitempty"`
|
||||
// ManualPass 为 true 时表示质检未过但经 HITL 人工放行。
|
||||
ManualPass bool `json:"manual_pass,omitempty"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
@@ -201,11 +206,11 @@ type Manifest struct {
|
||||
|
||||
// RunMetadata 工作区元信息(workspace.json)。
|
||||
type RunMetadata struct {
|
||||
ID string `json:"id"`
|
||||
Goal Goal `json:"goal"`
|
||||
Status string `json:"status"`
|
||||
Backend string `json:"backend,omitempty"` // 默认后端名(角色级 Backend 优先)
|
||||
Planner string `json:"planner,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Goal Goal `json:"goal"`
|
||||
Status string `json:"status"`
|
||||
Backend string `json:"backend,omitempty"` // 默认后端名(角色级 Backend 优先)
|
||||
Planner string `json:"planner,omitempty"`
|
||||
WorkDir string `json:"work_dir"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -217,17 +222,17 @@ type RunMetadata struct {
|
||||
type RunStatus string
|
||||
|
||||
const (
|
||||
RunCreated RunStatus = "created"
|
||||
RunPlanning RunStatus = "planning"
|
||||
RunCreated RunStatus = "created"
|
||||
RunPlanning RunStatus = "planning"
|
||||
RunAwaitingClarification RunStatus = "awaiting_clarification" // HITL:规划歧义,等待澄清
|
||||
RunAwaitingConfirmation RunStatus = "awaiting_confirmation" // HITL:计划待确认
|
||||
RunExecuting RunStatus = "executing"
|
||||
RunWaitingHuman RunStatus = "waiting_human" // HITL:执行中挂起(缺口/审批/权限/质检)
|
||||
RunQualityReview RunStatus = "quality_review" // HITL:质检超限转人工
|
||||
RunCompleted RunStatus = "completed"
|
||||
RunDegraded RunStatus = "degraded"
|
||||
RunFailed RunStatus = "failed"
|
||||
RunCancelled RunStatus = "cancelled"
|
||||
RunAwaitingConfirmation RunStatus = "awaiting_confirmation" // HITL:计划待确认
|
||||
RunExecuting RunStatus = "executing"
|
||||
RunWaitingHuman RunStatus = "waiting_human" // HITL:执行中挂起(缺口/审批/权限/质检)
|
||||
RunQualityReview RunStatus = "quality_review" // HITL:质检超限转人工
|
||||
RunCompleted RunStatus = "completed"
|
||||
RunDegraded RunStatus = "degraded"
|
||||
RunFailed RunStatus = "failed"
|
||||
RunCancelled RunStatus = "cancelled"
|
||||
)
|
||||
|
||||
// IsHeld 返回该状态是否为 HITL 挂起态(等待人工回应)。
|
||||
|
||||
+11
-8
@@ -19,6 +19,9 @@ type PlanInput struct {
|
||||
Blueprint *Blueprint // 命中模板时非 nil(基线步骤,规划器可微调)
|
||||
Registry *RoleRegistry
|
||||
RunID string
|
||||
// IntentCatalog 可选能力目录:启用意图编排时非 nil。规划者应把命中目录的
|
||||
// 子任务输出为 intent_id 步骤(必填槽位不足时改输出澄清,用目录的提问建议)。
|
||||
IntentCatalog *IntentCatalog
|
||||
}
|
||||
|
||||
// PlanOutput 规划输出:计划或澄清请求二选一。
|
||||
@@ -35,22 +38,22 @@ type PlanOutput struct {
|
||||
|
||||
// SquadPlan 一次规划产物。
|
||||
type SquadPlan struct {
|
||||
Squad SquadConfig `json:"squad"`
|
||||
Workflow WorkflowPlan `json:"workflow"`
|
||||
Template string `json:"template,omitempty"` // 交付物骨架(蓝图携带或规划器生成)
|
||||
Squad SquadConfig `json:"squad"`
|
||||
Workflow WorkflowPlan `json:"workflow"`
|
||||
Template string `json:"template,omitempty"` // 交付物骨架(蓝图携带或规划器生成)
|
||||
}
|
||||
|
||||
// Clarification 澄清请求:问题 + 可选建议选项。
|
||||
type Clarification struct {
|
||||
Reason string `json:"reason"`
|
||||
Questions []Question `json:"questions"`
|
||||
Reason string `json:"reason"`
|
||||
Questions []Question `json:"questions"`
|
||||
}
|
||||
|
||||
// Question 单个澄清问题。
|
||||
type Question struct {
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Options []HumanOption `json:"options,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Text string `json:"text"`
|
||||
Options []HumanOption `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// NewPlanID 生成工作流 ID。
|
||||
|
||||
+37
-28
@@ -13,11 +13,11 @@ import (
|
||||
// 模型客户端与默认模型在构造时注入(角色级模型在派生角色时由模型自行指定)。
|
||||
// 输出为严格 JSON;解析失败时重试一次,仍失败则 fail-closed 返回错误。
|
||||
type LLMPlanner struct {
|
||||
Client *llm.Client
|
||||
Model string
|
||||
APIBase string
|
||||
APIKey string
|
||||
MaxTokens int
|
||||
Client *llm.Client
|
||||
Model string
|
||||
APIBase string
|
||||
APIKey string
|
||||
MaxTokens int
|
||||
}
|
||||
|
||||
// NewLLMPlanner 创建 LLM 规划器。
|
||||
@@ -29,38 +29,40 @@ func (p *LLMPlanner) Name() string { return "llm" }
|
||||
|
||||
// llmPlanSchema 期望模型输出的 JSON 结构(宽松解析:仅取必要字段)。
|
||||
type llmPlanSchema struct {
|
||||
Clarify []llmQuestion `json:"clarify,omitempty"`
|
||||
Confirm bool `json:"confirm,omitempty"`
|
||||
Reason string `json:"confirm_reason,omitempty"`
|
||||
Agents []llmAgent `json:"agents"`
|
||||
Steps []llmStep `json:"steps"`
|
||||
Template string `json:"template,omitempty"`
|
||||
Clarify []llmQuestion `json:"clarify,omitempty"`
|
||||
Confirm bool `json:"confirm,omitempty"`
|
||||
Reason string `json:"confirm_reason,omitempty"`
|
||||
Agents []llmAgent `json:"agents"`
|
||||
Steps []llmStep `json:"steps"`
|
||||
Template string `json:"template,omitempty"`
|
||||
}
|
||||
|
||||
type llmQuestion struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Text string `json:"text"`
|
||||
Options []HumanOption `json:"options,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Text string `json:"text"`
|
||||
Options []HumanOption `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
type llmAgent struct {
|
||||
Action string `json:"action"` // use | derive
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name,omitempty"`
|
||||
BaseRole string `json:"base_role,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
DomainTopic string `json:"domain_topic,omitempty"`
|
||||
Action string `json:"action"` // use | derive
|
||||
Role string `json:"role"`
|
||||
Name string `json:"name,omitempty"`
|
||||
BaseRole string `json:"base_role,omitempty"`
|
||||
Model string `json:"model,omitempty"`
|
||||
Backend string `json:"backend,omitempty"`
|
||||
DomainTopic string `json:"domain_topic,omitempty"`
|
||||
Responsibilities []string `json:"responsibilities,omitempty"`
|
||||
}
|
||||
|
||||
type llmStep struct {
|
||||
ID string `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Title string `json:"title"`
|
||||
Parallel bool `json:"parallel,omitempty"`
|
||||
Targets []string `json:"targets,omitempty"`
|
||||
RequiresApproval bool `json:"requires_approval,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Role string `json:"role"`
|
||||
Title string `json:"title"`
|
||||
Parallel bool `json:"parallel,omitempty"`
|
||||
Targets []string `json:"targets,omitempty"`
|
||||
RequiresApproval bool `json:"requires_approval,omitempty"`
|
||||
IntentID string `json:"intent_id,omitempty"`
|
||||
IntentSlots map[string]string `json:"intent_slots,omitempty"`
|
||||
}
|
||||
|
||||
const llmPlannerSystem = `你是智能体小队的规划器。给定用户目标、已注册角色清单和可选的任务模板,输出一个可执行的小队计划。
|
||||
@@ -71,7 +73,8 @@ const llmPlannerSystem = `你是智能体小队的规划器。给定用户目标
|
||||
4. agents: 优先复用已注册角色({"action":"use","role":"已注册key"});确无合适角色才派生({"action":"derive","role":"新key","base_role":"基线key","name":"中文名","domain_topic":"领域","responsibilities":[...]})。派生数量尽量少。
|
||||
5. steps: 每步 {"id":"step_01_x","role":"角色key","title":"步骤职责"};可并行的相邻步骤 parallel=true;高风险/破坏性/对外发布类步骤 requires_approval=true;步骤通常 2~6 个,含梳理、专业执行、汇总成文、终审把关。
|
||||
6. 涉及写入、删除、对外提交或超出常规预算的计划 confirm=true。
|
||||
7. 通用守则:缺失的信息若会实质改变结果、执行路径、权限或安全,必须 clarify 问人;否则自行决定并在计划中说明你决定了什么。严禁假设交付完成:无法覆盖的目标范围要如实体现在步骤或假设中,不许静默缩水。`
|
||||
7. 通用守则:缺失的信息若会实质改变结果、执行路径、权限或安全,必须 clarify 问人;否则自行决定并在计划中说明你决定了什么。严禁假设交付完成:无法覆盖的目标范围要如实体现在步骤或假设中,不许静默缩水。
|
||||
8. 目标下方若给出"系统能力目录":子任务命中目录能力时,该步输出 {"role":"intent","intent_id":"目录id","intent_slots":{...},"title":"..."},槽位必须按目录填齐;必填槽位信息不足时禁止编造,改为 clarify 并使用目录的提问建议。未命中目录的子任务照常派给角色。没有目录时不得输出 intent_id 步骤。`
|
||||
|
||||
func (p *LLMPlanner) Plan(ctx context.Context, input PlanInput) (*PlanOutput, error) {
|
||||
if p.Client == nil || p.Model == "" {
|
||||
@@ -175,6 +178,7 @@ func (p *LLMPlanner) materialize(input PlanInput, parsed *llmPlanSchema) (*PlanO
|
||||
steps = append(steps, StepSpec{
|
||||
ID: id, Role: ls.Role, Title: ls.Title, Parallel: ls.Parallel,
|
||||
Targets: targets, RequiresApproval: ls.RequiresApproval, Status: "pending",
|
||||
IntentID: ls.IntentID, IntentSlots: ls.IntentSlots,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -219,6 +223,11 @@ func (p *LLMPlanner) buildPrompt(input PlanInput) string {
|
||||
b.Write(enc)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if input.IntentCatalog != nil && input.IntentCatalog.Markdown != "" {
|
||||
b.WriteString("\n## 系统能力目录(可直接调用,不必派智能体)\n")
|
||||
b.WriteString(input.IntentCatalog.Markdown)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
|
||||
@@ -97,10 +97,10 @@ func (p *RulePlanner) autonomous(input PlanInput) (*PlanOutput, error) {
|
||||
// 以推荐角色/首个角色为基线派生通用角色。
|
||||
base := p.baseRole(input.Registry)
|
||||
derived, err := input.Registry.Derive(DeriveRequest{
|
||||
RoleKey: st.role,
|
||||
Name: fmt.Sprintf("%s专员", st.role),
|
||||
BaseRole: base.Role,
|
||||
DomainTopic: goalText,
|
||||
RoleKey: st.role,
|
||||
Name: fmt.Sprintf("%s专员", st.role),
|
||||
BaseRole: base.Role,
|
||||
DomainTopic: goalText,
|
||||
Responsibilities: []string{st.title},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
+4
-4
@@ -13,10 +13,10 @@ import (
|
||||
// runState 引擎内部运行状态(磁盘即权威:workspace.json / workflow.json /
|
||||
// squad.json / blackboard.json / pending.json 全部落盘,进程重启可恢复)。
|
||||
type runState struct {
|
||||
Meta RunMetadata `json:"meta"`
|
||||
Plan SquadPlan `json:"plan"`
|
||||
BB BlackboardState `json:"bb"`
|
||||
Template string `json:"template,omitempty"`
|
||||
Meta RunMetadata `json:"meta"`
|
||||
Plan SquadPlan `json:"plan"`
|
||||
BB BlackboardState `json:"bb"`
|
||||
Template string `json:"template,omitempty"`
|
||||
// Approved 运行内一次性授权(permission/confirmation),键为 role:/step: 前缀。
|
||||
Approved map[string]bool `json:"approved,omitempty"`
|
||||
// RepairBudgetRemaining 质检失败后的自动重跑剩余次数。
|
||||
|
||||
@@ -129,10 +129,10 @@ func TestUsageAggregatedPerRole(t *testing.T) {
|
||||
|
||||
// slowCountBackend 统计并发峰值(验证角色级并发上限)。
|
||||
type slowCountBackend struct {
|
||||
mu sync.Mutex
|
||||
cur int
|
||||
peak int
|
||||
delay time.Duration
|
||||
mu sync.Mutex
|
||||
cur int
|
||||
peak int
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (b *slowCountBackend) Name() string { return "slow" }
|
||||
|
||||
+31
-6
@@ -187,11 +187,18 @@ func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*Ru
|
||||
prompt = st.injectSteering(prompt)
|
||||
instructions := readFileOr(filepath.Join(subDir, "AGENTS.md"), d.opts.Workspaces.ResolveInstructions(role))
|
||||
|
||||
backend, backendName, err := d.opts.Backends.Resolve(role, st.Meta.Backend)
|
||||
if err != nil {
|
||||
step.Status, step.Error = "failed", err.Error()
|
||||
st.setStep(idx, step)
|
||||
return d.failStep(ctx, st, idx)
|
||||
var backend exec.Backend
|
||||
backendName := ""
|
||||
if step.IntentID != "" {
|
||||
backendName = "intent:" + step.IntentID
|
||||
} else {
|
||||
var rerr error
|
||||
backend, backendName, rerr = d.opts.Backends.Resolve(role, st.Meta.Backend)
|
||||
if rerr != nil {
|
||||
step.Status, step.Error = "failed", rerr.Error()
|
||||
st.setStep(idx, step)
|
||||
return d.failStep(ctx, st, idx)
|
||||
}
|
||||
}
|
||||
|
||||
targets := step.Targets
|
||||
@@ -199,6 +206,7 @@ func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*Ru
|
||||
targets = []string{"output.md"}
|
||||
}
|
||||
var result *exec.Result
|
||||
var err error
|
||||
for attempt := 1; attempt <= exec.MaxTransientStepInvocations; attempt++ {
|
||||
reqPrompt := prompt
|
||||
if attempt > 1 {
|
||||
@@ -222,7 +230,24 @@ func (d *flowDriver) runOneStep(ctx context.Context, st *runState, idx int) (*Ru
|
||||
Data: marshalJSON(map[string]string{"process_type": ev.Type, "tool": ev.Tool, "detail": ev.Detail})})
|
||||
},
|
||||
}
|
||||
result, err = backend.Execute(ctx, req)
|
||||
if step.IntentID != "" {
|
||||
// 系统功能步:不经智能体,由 IntentRunner 直接执行;缺席即 fail-closed。
|
||||
// Runner 只产出内容(patches),由引擎统一物化到子工作区。
|
||||
if d.opts.IntentRunner == nil {
|
||||
step.Status, step.Error = "failed", fmt.Sprintf("intent step %s: no IntentRunner configured", step.ID)
|
||||
st.setStep(idx, step)
|
||||
return d.failStep(ctx, st, idx)
|
||||
}
|
||||
result, err = d.opts.IntentRunner.Run(ctx, IntentStep{
|
||||
IntentID: step.IntentID, Slots: step.IntentSlots,
|
||||
Title: step.Title, WorkDir: subDir, Targets: targets,
|
||||
})
|
||||
if err == nil && result != nil && len(result.Patches) > 0 {
|
||||
_, _ = exec.MaterializeTargets(subDir, targets, result.Patches)
|
||||
}
|
||||
} else {
|
||||
result, err = backend.Execute(ctx, req)
|
||||
}
|
||||
if err == nil && result.Success {
|
||||
break
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package squadintent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"agent-native/exec"
|
||||
"agent-squad/squad"
|
||||
)
|
||||
|
||||
// Catalog 把识别快照渲染为 squad 的能力目录数据视图(规划者提示词 + 计划校验用)。
|
||||
// 闸门未启用(nil Gate)时返回 nil。
|
||||
func (g *Gate) Catalog() *squad.IntentCatalog {
|
||||
if g == nil || g.snap == nil {
|
||||
return nil
|
||||
}
|
||||
catalog := &squad.IntentCatalog{
|
||||
Revision: g.snap.Revision(),
|
||||
Markdown: g.snap.CatalogMarkdown(),
|
||||
Specs: make(map[string]squad.IntentSpecView, len(g.snap.SpecIDs())),
|
||||
}
|
||||
for _, cs := range g.snap.Catalog() {
|
||||
view := squad.IntentSpecView{
|
||||
ID: cs.ID,
|
||||
Description: cs.Description,
|
||||
Risk: cs.Risk,
|
||||
RequiresConfirm: cs.RequiresConfirm,
|
||||
Handler: cs.Handler,
|
||||
Metadata: cs.Metadata,
|
||||
}
|
||||
for _, sl := range cs.Slots {
|
||||
view.Slots = append(view.Slots, squad.IntentSlotView{
|
||||
Key: sl.Key, Required: sl.Required, Label: sl.Label,
|
||||
Question: sl.Question, Enum: sl.Enum, Format: sl.Format,
|
||||
})
|
||||
if sl.Required {
|
||||
view.RequiredSlots = append(view.RequiredSlots, sl.Key)
|
||||
}
|
||||
}
|
||||
catalog.Specs[cs.ID] = view
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
// IntentHandler 宿主注册的系统功能处理函数:返回写入步骤 targets 的正文。
|
||||
type IntentHandler func(ctx context.Context, step squad.IntentStep) (string, error)
|
||||
|
||||
// Runner 系统功能执行器:按 spec.Handler 查宿主处理函数并调用;
|
||||
// echo=true 时未注册的 handler 以物化文本回退(开发/演示用,正文明确声明未真实执行)。
|
||||
type Runner struct {
|
||||
gate *Gate
|
||||
handlers map[string]IntentHandler
|
||||
echo bool
|
||||
}
|
||||
|
||||
// Runner 基于闸门快照构建执行器。handlers 以 spec.Handler 为键。
|
||||
func (g *Gate) Runner(handlers map[string]IntentHandler) *Runner {
|
||||
return &Runner{gate: g, handlers: handlers}
|
||||
}
|
||||
|
||||
// EchoRunner 演示执行器:所有调用物化为文本(AGENT_SQUAD_INTENT_ECHO 场景)。
|
||||
func (g *Gate) EchoRunner() *Runner {
|
||||
return &Runner{gate: g, echo: true}
|
||||
}
|
||||
|
||||
// Run 实现 squad.IntentRunner。未知意图/未知 handler fail-closed(不静默降级)。
|
||||
func (r *Runner) Run(ctx context.Context, step squad.IntentStep) (*exec.Result, error) {
|
||||
if r == nil || r.gate == nil || r.gate.snap == nil {
|
||||
return nil, fmt.Errorf("intent runner: gate not configured")
|
||||
}
|
||||
spec, ok := r.gate.snap.SpecByID(step.IntentID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("intent runner: snapshot 中不存在意图 %s", step.IntentID)
|
||||
}
|
||||
handlerKey := spec.Handler
|
||||
h, registered := r.handlers[handlerKey]
|
||||
if !registered {
|
||||
if !r.echo {
|
||||
return nil, fmt.Errorf("intent runner: handler %q 未注册(fail-closed,不静默降级)", handlerKey)
|
||||
}
|
||||
text := echoText(step, handlerKey, false)
|
||||
return textResult(step, text), nil
|
||||
}
|
||||
text, err := h(ctx, step)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return textResult(step, text), nil
|
||||
}
|
||||
|
||||
func textResult(step squad.IntentStep, text string) *exec.Result {
|
||||
patches := make(map[string][]byte, len(step.Targets))
|
||||
for _, t := range step.Targets {
|
||||
patches[t] = []byte(text)
|
||||
}
|
||||
return &exec.Result{Success: true, Patches: patches, Logs: "intent:" + step.IntentID}
|
||||
}
|
||||
|
||||
func echoText(step squad.IntentStep, handlerKey string, _ bool) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("# 系统功能调用(EchoRunner 物化,未真实执行)\n\n")
|
||||
fmt.Fprintf(&b, "- intent_id: %s\n- handler: %s\n- title: %s\n", step.IntentID, handlerKey, step.Title)
|
||||
keys := make([]string, 0, len(step.Slots))
|
||||
for k := range step.Slots {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(&b, "- slot %s = %s\n", k, step.Slots[k])
|
||||
}
|
||||
b.WriteString("\n宿主应用注册真实 handler 后,此文件将是系统功能的真实输出。\n")
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package squadintent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"agent-intent/intent"
|
||||
"agent-squad/squad"
|
||||
)
|
||||
|
||||
// catalogFS:带槽位(label/question/enum)的可调用能力 + 守卫。
|
||||
func catalogFS() fstest.MapFS {
|
||||
return fstest.MapFS{"intents.json": &fstest.MapFile{Data: []byte(`{
|
||||
"intents": [
|
||||
{"id": "guard.config_change", "domain": "guard", "version": 1,
|
||||
"matcher": {"includes_any": ["系统配置"]}},
|
||||
{"id": "demo.run.status_query", "domain": "demo", "version": 1,
|
||||
"description": "查询运行进度",
|
||||
"matcher": {"includes_any": ["进度"]},
|
||||
"handler": "taskrun.query_status",
|
||||
"slots": [
|
||||
{"key": "run_id", "required": true, "pattern": "(run-\\d+)",
|
||||
"label": "运行编号", "question": "要查询哪个运行?请提供 run 编号"}
|
||||
]}
|
||||
]
|
||||
}`)}}
|
||||
}
|
||||
|
||||
func newCatalogGate(t *testing.T) *Gate {
|
||||
t.Helper()
|
||||
g, err := New(context.Background(), Options{
|
||||
IntentOptions: intent.Options{GuardDomains: []string{"guard"}, ProtectedDomains: []string{"guard"}},
|
||||
FS: catalogFS(),
|
||||
Roots: []string{"."},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func TestGateCatalogMapsSpecsAndSlots(t *testing.T) {
|
||||
g := newCatalogGate(t)
|
||||
c := g.Catalog()
|
||||
if c == nil || len(c.Specs) != 1 {
|
||||
t.Fatalf("catalog specs: %+v", c)
|
||||
}
|
||||
view, ok := c.Get("demo.run.status_query")
|
||||
if !ok || view.Handler != "taskrun.query_status" || len(view.RequiredSlots) != 1 || view.RequiredSlots[0] != "run_id" {
|
||||
t.Fatalf("view: %+v", view)
|
||||
}
|
||||
if len(view.Slots) != 1 || view.Slots[0].Question == "" || view.Slots[0].Label != "运行编号" {
|
||||
t.Fatalf("slot view: %+v", view.Slots)
|
||||
}
|
||||
if _, ok := c.Get("guard.config_change"); ok {
|
||||
t.Fatal("guard spec must not appear in catalog")
|
||||
}
|
||||
if !strings.Contains(c.Markdown, "提问建议:要查询哪个运行") {
|
||||
t.Fatalf("markdown missing question: %s", c.Markdown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerDispatchesToHandlerAndFailsClosed(t *testing.T) {
|
||||
g := newCatalogGate(t)
|
||||
var got squad.IntentStep
|
||||
runner := g.Runner(map[string]IntentHandler{
|
||||
"taskrun.query_status": func(ctx context.Context, step squad.IntentStep) (string, error) {
|
||||
got = step
|
||||
return "# 进度\n\nrun-42 已完成,正文满足最小长度要求用于质检演示。\n", nil
|
||||
},
|
||||
})
|
||||
res, err := runner.Run(context.Background(), squad.IntentStep{
|
||||
IntentID: "demo.run.status_query", Slots: map[string]string{"run_id": "run-42"},
|
||||
Targets: []string{"output.md"},
|
||||
})
|
||||
if err != nil || res == nil || !res.Success {
|
||||
t.Fatalf("run: %+v %v", res, err)
|
||||
}
|
||||
if got.Slots["run_id"] != "run-42" || string(res.Patches["output.md"]) == "" {
|
||||
t.Fatalf("handler dispatch: %+v", got)
|
||||
}
|
||||
|
||||
// 未知意图与未注册 handler:fail-closed(无 handler 的 runner 拒绝执行)。
|
||||
if _, err := runner.Run(context.Background(), squad.IntentStep{IntentID: "demo.nope"}); err == nil {
|
||||
t.Fatal("expected unknown intent error")
|
||||
}
|
||||
bare := g.Runner(nil)
|
||||
if _, err := bare.Run(context.Background(), squad.IntentStep{IntentID: "demo.run.status_query", Slots: map[string]string{"run_id": "run-1"}}); err == nil {
|
||||
t.Fatal("expected unregistered handler error")
|
||||
} else if !strings.Contains(err.Error(), "fail-closed") {
|
||||
t.Fatalf("error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEchoRunnerMaterializesCall(t *testing.T) {
|
||||
g := newCatalogGate(t)
|
||||
res, err := g.EchoRunner().Run(context.Background(), squad.IntentStep{
|
||||
IntentID: "demo.run.status_query", Slots: map[string]string{"run_id": "run-7"},
|
||||
Targets: []string{"output.md"}, Title: "查询进度",
|
||||
})
|
||||
if err != nil || !res.Success {
|
||||
t.Fatalf("echo run: %+v %v", res, err)
|
||||
}
|
||||
text := string(res.Patches["output.md"])
|
||||
for _, want := range []string{"EchoRunner", "demo.run.status_query", "run-7", "未真实执行"} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("echo text missing %q: %s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilGateCatalogAndRunner(t *testing.T) {
|
||||
var g *Gate
|
||||
if g.Catalog() != nil {
|
||||
t.Fatal("nil gate catalog must be nil")
|
||||
}
|
||||
if _, err := g.EchoRunner().Run(context.Background(), squad.IntentStep{IntentID: "x"}); err == nil {
|
||||
t.Fatal("nil gate runner must fail-closed")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user