Details (original message): Integrate the Agent-Squad engine: squadadapter maps RoleRegistry/RulePlanner/BackendResolver/ProgressSink/HITL onto the unified Run and event model (step subagents, plan updates, deliverable artifacts, confirmation cards); step-wise engine driving loop; model routing respects profile routes with layered resolver + integration tests; context assembly observation flows to run.completed; mock outputs and e2e coverage.
259 lines
9.5 KiB
Go
259 lines
9.5 KiB
Go
// Package squadadapter 把 Agent-Squad 引擎接入统一 Run/事件模型(设计 P3:
|
||
// runtime 接 Agent-Squad——spawn/黑板/HITL 由 runmgr 映射为统一 Run + 事件)。
|
||
//
|
||
// 装配映射:
|
||
// - RoleRegistry → 内置小队角色(规则规划器自主编排四阶段骨架)
|
||
// - Planner → squad RulePlanner(确定性;LLM 规划器为后续档)
|
||
// - BackendResolver→ 自定义 "agent-system" 后端工厂:每步执行 = rc.SpawnSubagent
|
||
// (子 Run 进任务树,走既有 mock/LLM 子代理执行器与事件)
|
||
// - ProgressSink → 步骤事件映射 plan.update / agent.progress
|
||
// - HITL(挂起) → 引擎 Pending HumanRequest ↔ rc.AskHITL 卡片;RespondDecision 回填
|
||
package squadadapter
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"agent-native/exec"
|
||
"agent-squad/squad"
|
||
core "agent-system-core"
|
||
)
|
||
|
||
// Ctx 是适配器所需的窄端口(结构性接口;runmgr 的 runCtx 天然满足,
|
||
// 避免适配器反向依赖 runtime 包形成环)。
|
||
type Ctx interface {
|
||
Context() context.Context
|
||
Run() core.Run
|
||
Settings() core.EffectiveSettings
|
||
Emit(typ core.StreamEventType, payload any) error
|
||
AskHITL(card core.HITLCard) (core.HITLResponse, error)
|
||
SpawnSubagent(name, task string, hint string) (core.AgentResultPayload, error)
|
||
UpdatePlan(p core.PlanSnapshot) error
|
||
WriteArtifact(name, kind, mime string, content []byte) (core.Artifact, error)
|
||
}
|
||
|
||
// Orchestrate 在一次 conversation run 内驱动 squad 引擎完成目标。
|
||
// 返回摘要文本(由 runner 落为消息部件);error = 编排失败。
|
||
func Orchestrate(rc Ctx, dataDir, goalText string) (string, error) {
|
||
if strings.TrimSpace(goalText) == "" {
|
||
return "", fmt.Errorf("squad: goal text is empty")
|
||
}
|
||
runDir := filepath.Join(dataDir, "squad", rc.Run().ID)
|
||
|
||
registry := squad.NewRoleRegistry()
|
||
for _, role := range builtinRoles() {
|
||
if err := registry.Register(role); err != nil {
|
||
return "", fmt.Errorf("squad: register role %s: %w", role.Role, err)
|
||
}
|
||
}
|
||
|
||
backends := squad.NewBackendResolver()
|
||
backends.Register("agent-system", func(role squad.RoleSpec) (exec.Backend, error) {
|
||
return &stepBackend{rc: rc, role: role}, nil
|
||
})
|
||
|
||
engine, err := squad.NewEngine(squad.Options{
|
||
DataDir: runDir,
|
||
Registry: registry,
|
||
Planner: squad.NewRulePlanner(),
|
||
Backends: backends,
|
||
DefaultBackend: "agent-system",
|
||
StepTimeout: 5 * time.Minute,
|
||
HitlPolicy: squad.HitlPolicy{Mode: squad.HitlInteractive},
|
||
Sink: squad.ProgressFunc(func(ev squad.Event) { onSquadEvent(rc, ev) }),
|
||
})
|
||
if err != nil {
|
||
return "", fmt.Errorf("squad: engine: %w", err)
|
||
}
|
||
|
||
// 驱动循环:引擎为步进语义(每次调用推进一个步骤波次)——
|
||
// 挂起态 → HITL 卡片 → Respond;执行态 → Resume 推进;直至终态。
|
||
res, err := engine.Run(rc.Context(), squad.Goal{Text: goalText}, squad.RunOptions{})
|
||
for err == nil && !squad.RunStatus(res.Status).Terminal() {
|
||
switch {
|
||
case isHoldStatus(string(res.Status)):
|
||
req := res.Pending
|
||
if req == nil {
|
||
if req, err = engine.Pending(res.RunID); err != nil || req == nil {
|
||
break
|
||
}
|
||
}
|
||
decision, aerr := askHuman(rc, req)
|
||
if aerr != nil {
|
||
return "", aerr
|
||
}
|
||
res, err = engine.Respond(rc.Context(), res.RunID, decision)
|
||
default:
|
||
res, err = engine.Resume(rc.Context(), res.RunID)
|
||
}
|
||
if err != nil {
|
||
break
|
||
}
|
||
// 步进间映射最新计划到右栏。
|
||
if res.Plan != nil {
|
||
_ = rc.UpdatePlan(mapPlan(res.Plan))
|
||
}
|
||
}
|
||
if err != nil {
|
||
return "", fmt.Errorf("squad: run: %w", err)
|
||
}
|
||
|
||
// 终态计划映射 + 交付物落工件。
|
||
if res.Plan != nil {
|
||
_ = rc.UpdatePlan(mapPlan(res.Plan))
|
||
}
|
||
summary := firstNonEmpty(res.Summary, res.Deliverable, "编排完成")
|
||
if res.Deliverable != "" {
|
||
if _, werr := rc.WriteArtifact("squad-"+res.RunID+".md", "document", "text/markdown", []byte(res.Deliverable)); werr != nil {
|
||
summary += "(交付物写入失败:" + werr.Error() + ")"
|
||
}
|
||
}
|
||
switch res.Status {
|
||
case squad.RunCompleted:
|
||
return summary, nil
|
||
default:
|
||
return summary, fmt.Errorf("squad: run %s status %s: %s", res.RunID, res.Status, res.Error)
|
||
}
|
||
}
|
||
|
||
// stepBackend:小队步骤 → 统一子 Run(任务树可见;执行走 runmgr 的子代理执行器)。
|
||
type stepBackend struct {
|
||
rc Ctx
|
||
role squad.RoleSpec
|
||
}
|
||
|
||
func (b *stepBackend) Name() string { return "agent-system" }
|
||
|
||
func (b *stepBackend) Execute(ctx context.Context, req exec.Request) (*exec.Result, error) {
|
||
task := strings.TrimSpace(req.Prompt)
|
||
if task == "" {
|
||
task = req.Instructions
|
||
}
|
||
hint := strings.TrimSpace(req.Instructions)
|
||
out, err := b.rc.SpawnSubagent(firstNonEmpty(b.role.Name, b.role.Role), task, hint)
|
||
res := &exec.Result{Success: err == nil, Logs: firstNonEmpty(out.Summary, "子代理完成")}
|
||
if err != nil {
|
||
res.ErrorReason = err.Error()
|
||
return res, nil
|
||
}
|
||
// 交付物:子代理全文写入步骤子工作区的目标文件(引擎契约:后端自行落盘,
|
||
// 检查器按 allTargetsPresent 校验;Patches 同时回传供引擎重试路径复用)。
|
||
content := firstNonEmpty(out.Text, out.Summary, "(子代理无文本产出)")
|
||
targets := req.Targets
|
||
if len(targets) == 0 {
|
||
targets = []string{"output.md"}
|
||
}
|
||
res.Patches = make(map[string][]byte, len(targets))
|
||
for _, t := range targets {
|
||
body := []byte(content + "\n")
|
||
res.Patches[t] = body
|
||
dest := filepath.Join(req.WorkDir, filepath.FromSlash(t))
|
||
if merr := os.MkdirAll(filepath.Dir(dest), 0o755); merr == nil {
|
||
_ = os.WriteFile(dest, body, 0o644)
|
||
}
|
||
}
|
||
return res, nil
|
||
}
|
||
|
||
// askHuman:HumanRequest → HITL 卡片 → RespondDecision(选项直达 / 自由文本作答)。
|
||
func askHuman(rc Ctx, req *squad.HumanRequest) (squad.RespondDecision, error) {
|
||
choices := make([]core.HITLChoice, 0, len(req.Options))
|
||
for _, o := range req.Options {
|
||
choices = append(choices, core.HITLChoice{
|
||
ID: o.Value, Label: o.Label,
|
||
Description: firstNonEmpty(req.Context, "小队编排挂起"),
|
||
Impact: fmt.Sprintf("决策类型 %s:%s 将继续/终止本次编排", req.Kind, o.Value),
|
||
})
|
||
}
|
||
if len(choices) == 0 {
|
||
choices = append(choices,
|
||
core.HITLChoice{ID: "approve", Label: "继续", Description: "按当前计划继续执行", Impact: "小队继续执行后续步骤", Recommended: true},
|
||
core.HITLChoice{ID: "abort", Label: "中止", Description: "终止本次编排", Impact: "编排取消,已产出保留"},
|
||
)
|
||
}
|
||
resp, err := rc.AskHITL(core.HITLCard{
|
||
Question: req.Question,
|
||
Kind: core.HITLKindChoice,
|
||
Choices: choices,
|
||
AllowFreeInput: true,
|
||
FreeInputHint: "可直接输入回答或修改指令",
|
||
})
|
||
if err != nil {
|
||
return squad.RespondDecision{RequestID: req.ID, Choice: "abort"}, err
|
||
}
|
||
d := squad.RespondDecision{RequestID: req.ID}
|
||
if resp.ChoiceID != "" {
|
||
d.Choice = resp.ChoiceID
|
||
} else {
|
||
d.Answer = resp.FreeText
|
||
}
|
||
return d, nil
|
||
}
|
||
|
||
// onSquadEvent:引擎事件 → 统一事件(计划步态 + 过程注记)。
|
||
func onSquadEvent(rc Ctx, ev squad.Event) {
|
||
switch ev.Type {
|
||
case squad.EvStepStarted, squad.EvStepCompleted, squad.EvStepFailed, squad.EvStepSkipped:
|
||
note := fmt.Sprintf("小队步骤 %s(%s)%s", ev.StepID, ev.Role, strings.TrimPrefix(ev.Type, "step_"))
|
||
if ev.Text != "" {
|
||
note += ":" + ev.Text
|
||
}
|
||
_ = rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: note, Phase: core.PhaseActing})
|
||
case squad.EvHumanInputRequired, squad.EvHumanResponded:
|
||
// 挂起/回填经 AskHITL 卡片主路径呈现;此处仅过程注记。
|
||
_ = rc.Emit(core.EvAgentProgress, core.AgentProgressPayload{AgentRun: rc.Run().ID, Note: ev.Type + ": " + ev.Text})
|
||
}
|
||
}
|
||
|
||
// mapPlan:WorkflowPlan → 右栏计划快照。
|
||
func mapPlan(p *squad.WorkflowPlan) core.PlanSnapshot {
|
||
snap := core.PlanSnapshot{Title: firstNonEmpty(p.Title, "小队编排计划"), Steps: make([]core.PlanStep, 0, len(p.Steps))}
|
||
for _, s := range p.Steps {
|
||
snap.Steps = append(snap.Steps, core.PlanStep{ID: s.ID, Text: firstNonEmpty(s.Title, s.ID), Status: mapStepStatus(s.Status)})
|
||
}
|
||
return snap
|
||
}
|
||
|
||
func mapStepStatus(s string) string {
|
||
switch s {
|
||
case "running":
|
||
return "in_progress"
|
||
case "completed":
|
||
return "done"
|
||
case "skipped":
|
||
return "skipped"
|
||
default:
|
||
return "pending"
|
||
}
|
||
}
|
||
|
||
func isHoldStatus(s string) bool {
|
||
return s == "waiting_human" || s == "awaiting_confirmation" || s == "awaiting_clarification" || s == "quality_review"
|
||
}
|
||
|
||
func builtinRoles() []squad.RoleSpec {
|
||
return []squad.RoleSpec{
|
||
{Role: "planner", Name: "规划员", Backend: "agent-system", BashPermission: "deny",
|
||
Keywords: []string{"梳理", "规划", "分析"}, Instructions: "梳理目标与材料,产出结构化要点与执行建议。"},
|
||
{Role: "researcher", Name: "调研员", Backend: "agent-system", BashPermission: "deny",
|
||
Keywords: []string{"调研", "检索", "资料"}, Instructions: "围绕目标调研关键问题,输出简明结论与依据。"},
|
||
{Role: "writer", Name: "撰稿员", Backend: "agent-system", BashPermission: "deny",
|
||
Keywords: []string{"撰写", "成文", "报告"}, Instructions: "把前序要点与调研结论整理为清晰成文的交付文档。"},
|
||
{Role: "reviewer", Name: "审校员", Backend: "agent-system", BashPermission: "deny",
|
||
Keywords: []string{"审校", "把关", "终审"}, Instructions: "终审交付文档:一致性、完整性与风险提示,给出通过或修改意见。"},
|
||
}
|
||
}
|
||
|
||
func firstNonEmpty(vals ...string) string {
|
||
for _, v := range vals {
|
||
if strings.TrimSpace(v) != "" {
|
||
return v
|
||
}
|
||
}
|
||
return ""
|
||
}
|