// Package squadadapter 把 Agent-Squad 引擎接入统一 Run/事件模型(设计 P3: // runtime 接 Agent-Squad——spawn/黑板/HITL 由 runmgr 映射为统一 Run + 事件)。 // // 装配映射: // - RoleRegistry → 内置小队角色(规则规划器自主编排四阶段骨架) // - Planner → squad RulePlanner(确定性;LLM 规划器为后续档) // - BackendResolver→ "agent-native" 内核后端工厂(Q3 WP2:SpawnSubagent // 压缩路径已退役;每步执行 = agent-native 内核循环) // - ProgressSink → 步骤事件映射 plan.update / agent.progress // - HITL(挂起) → 引擎 Pending HumanRequest ↔ rc.AskHITL 卡片;RespondDecision 回填 package squadadapter import ( "context" "fmt" "path/filepath" "strings" "agent-native/runtime/subagent" nativeadp "agent-squad/adapters/native" "agent-squad/squad" "agent-system-backend/internal/quality" 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) } // CardSink 是可选的产卡能力(提案 T2 编排卡):runCtx 实现之;测试桩无需实现。 type CardSink interface { CreateCard(card core.ActivityCard) (core.ActivityCard, error) SetCardStatus(id string, status core.CardStatus, tone, note string) 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") } // 编排卡(可选能力):开始 → 执行中 → 结果/失败显式终态;无 CardSink 时静默跳过。 var cardID string if cs, ok := rc.(CardSink); ok { title := goalText if len(title) > 48 { title = title[:48] + "…" } if card, err := cs.CreateCard(core.ActivityCard{Kind: core.CardKindProgress, Title: "小队编排 · " + title, Status: core.CardActive, Tone: "info"}); err == nil { cardID = card.ID } } 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 := BuildBackends(rc) engine, err := squad.NewEngine(squad.Options{ DataDir: runDir, Registry: registry, Planner: squad.NewRulePlanner(), Backends: backends, DefaultBackend: "agent-native", StepTimeout: -1, // 负数 = 不设单步截止(引擎只在 0 时回落到 10 分钟) HitlPolicy: squad.HitlPolicy{Mode: squad.HitlInteractive}, Checker: quality.Checker{}, RepairBudget: 2, 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: if cardID != "" { if cs, ok := rc.(CardSink); ok { _ = cs.SetCardStatus(cardID, core.CardDone, "ok", summary) } } return summary, nil default: if cardID != "" { if cs, ok := rc.(CardSink); ok { _ = cs.SetCardStatus(cardID, core.CardFailed, "danger", res.Error) } } return summary, fmt.Errorf("squad: run %s status %s: %s", res.RunID, res.Status, res.Error) } } // 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" } // AgentNativeOptions 可选组合根注入:返回配置且启用时,为每次小队运行注册 // "agent-native" 内核后端工厂(模型/搜索/执行/MCP/技能根由组合根从 settings // 解析;Subagent 工厂经 rc 桥接产品 SpawnSubagent)。 // // 组合根必须按「本次运行」解析配置:模型、端点与密钥整条来自 rc.Settings() // 已合并的当前用户/项目快照(叠加 rc 的画像模型覆盖),per-route 规则与直连 // 共用同一构造器。按默认账号重新解析会让一个用户的模型名配上另一个账号的 // 端点/密钥。 var AgentNativeOptions func(rc Ctx) (nativeadp.Config, bool) // SubagentFactory 把内核委派端口(delegate_task)桥接到产品 SpawnSubagent: // 委派信息集(目标/输入/写范围/非目标/完成判据/交付物)拼装为任务书, // 子运行进入任务树(用量与事件统一记账);深度天然为 1——子运行是小队外 // 的普通代理执行,不再嵌套小队。 func SubagentFactory(rc Ctx) subagent.Factory { return func(ctx context.Context, req subagent.Request) (subagent.Result, error) { var b strings.Builder b.WriteString("委派子任务\n目标:" + req.Goal) if req.Inputs != "" { b.WriteString("\n输入:" + req.Inputs) } if req.WriteScope != "" { b.WriteString("\n可写范围(不得越界):" + req.WriteScope) } if req.NonGoals != "" { b.WriteString("\n非目标:" + req.NonGoals) } b.WriteString("\n完成判据:" + req.DoneCriteria) if len(req.Deliverables) > 0 { b.WriteString("\n交付物:" + strings.Join(req.Deliverables, ", ")) } // Q3 WP3:任务书四要素补全——目标、输出格式、工具和边界。空声明 // 不落节(沿用子代理画像默认)。 if strings.TrimSpace(req.OutputFormat) != "" { b.WriteString("\n输出格式:" + req.OutputFormat) } if len(req.Tools) > 0 { b.WriteString("\n工具面(可用工具,不得越界):" + strings.Join(req.Tools, ", ")) } // Background(N2 WP5):声明驱动后台派生,语义同产品 delegate 的 // background 参数;完成结果经通知队列回投,本次调用立即返回 run ID。 // rc 无后台能力(测试桩/旧宿主)时回落同步派生,不 fail-closed。 if req.Background { if bg, ok := rc.(interface { SpawnBackground(agentID, name, task, hint string) (core.ID, error) }); ok { id, err := bg.SpawnBackground("delegatee", "delegatee", b.String(), "内核 delegate_task 委派(后台)") if err != nil { return subagent.Result{}, err } return subagent.Result{Success: true, Text: "已在后台启动 " + id + ",完成后会自动通知"}, nil } } payload, err := rc.SpawnSubagent("delegatee", b.String(), "内核 delegate_task 委派") if err != nil { return subagent.Result{}, err } res := subagent.Result{Text: firstNonEmpty(payload.Text, payload.Summary)} switch payload.Status { case core.RunCompleted: res.Success = true default: res.Err = "子代理结束状态:" + string(payload.Status) } return res, nil } } // BuildBackends 组装小队后端解析器(Q3 WP2):只注册 "agent-native" 内核 // 工厂(组合根注入 AgentNativeOptions 且模型已配置时)。legacy 的 // "agent-system"(SpawnSubagent 压缩路径)已退役删除;工厂缺席时解析 // fail-closed——步骤失败可见,不静默降级到别的执行面。 func BuildBackends(rc Ctx) *squad.BackendResolver { backends := squad.NewBackendResolver() if AgentNativeOptions != nil { if cfg, ok := AgentNativeOptions(rc); ok { nativeadp.RegisterFull(backends, cfg) } } return backends } // RoleInstructions 小队角色指令来源:app 装配时注入提示词库查找 // (agent.squad.);nil 或未命中 = 使用内联回退文案(测试与骨架兼容)。 var RoleInstructions func(kind string) (string, bool) func builtinRoles() []squad.RoleSpec { instr := func(kind, fallback string) string { if RoleInstructions != nil { if body, ok := RoleInstructions(kind); ok && strings.TrimSpace(body) != "" { return body } } return fallback } // 执行配置显式化(agent-native 后端消费): // 规划/调研/审校一律 BashPermission=deny(无命令执行需求); // 撰稿员需要 run_python/run_command 生成文档——ask 档(每次运行首次使用前人工放行)。 // 工具白名单按角色最小化(Q3 WP2:agent-native 下步骤须自己落盘交付物, // 全员补 write_file);撰稿员技能指向启动种子的三套交付配方。 return []squad.RoleSpec{ {Role: "planner", Name: "规划员", Backend: "agent-native", BashPermission: squad.BashPermissionDeny, Tools: []string{"read_file", "list_dir", "grep", "write_file"}, Keywords: []string{"梳理", "规划", "分析"}, Instructions: instr("planner", "梳理目标与材料,产出结构化要点与执行建议。")}, {Role: "researcher", Name: "调研员", Backend: "agent-native", BashPermission: squad.BashPermissionDeny, Tools: []string{"web_search", "tavily_search", "fetch_url", "read_file", "write_file"}, Keywords: []string{"调研", "检索", "资料"}, Instructions: instr("researcher", "围绕目标调研关键问题,输出简明结论与依据。")}, {Role: "writer", Name: "撰稿员", Backend: "agent-native", BashPermission: squad.BashPermissionAsk, Tools: []string{"read_file", "write_file", "edit_file", "multi_edit", "list_dir", "grep", "glob", "load_skill", "run_python", "run_command"}, Skills: []string{"docx-author", "xlsx-author", "pptx-author"}, Keywords: []string{"撰写", "成文", "报告"}, Instructions: instr("writer", "把前序要点与调研结论整理为清晰成文的交付文档。")}, {Role: "reviewer", Name: "审校员", Backend: "agent-native", BashPermission: squad.BashPermissionDeny, Tools: []string{"read_file", "grep", "glob", "list_dir", "write_file"}, Keywords: []string{"审校", "把关", "终审"}, Instructions: instr("reviewer", "终审交付文档:一致性、完整性与风险提示,给出通过或修改意见。")}, } } func firstNonEmpty(vals ...string) string { for _, v := range vals { if strings.TrimSpace(v) != "" { return v } } return "" }