// 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 "" }