Files
Agent-System-Backend/internal/squadadapter/adapter.go
T
weichen c7736a1917 Integrate the Agent-Squad engine
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.
2026-09-29 18:27:49 +00:00

259 lines
9.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 ""
}