package squadadapter import ( "fmt" "math" "os" "path/filepath" "strings" agentllm "agent-native/runtime/llm" "agent-native/runtime/tools" nativeadp "agent-squad/adapters/native" "agent-squad/squad" "agent-system-backend/internal/quality" ) // EntryOptions 是小队作为对话入口时的一次运行参数。 type EntryOptions struct { DataDir string Goal string HostTools []tools.Tool OnEvent func(ev squad.ProcessEvent) Stream agentllm.StreamFunc Router squad.Router Checker squad.Checker Hitl squad.HitlPolicy MaxTurns int MaxToolCalls int RepairBudget int // Model 是本次运行的主模型(与 AgentNativeOptions 解析出的路由同源: // 同一份运行快照 + 画像覆盖),不得与工厂配的端点/密钥分属不同属主。 Model string } // EntryResult 是入口跑完后的文本。工件登记由调用方在虚假交付检查之后做。 type EntryResult struct { Summary string Body string Name string } // RunEntry 用小队引擎跑完一条用户目标。简单任务是单步 assistant,复杂任务进 leader。 // 每一步都走 agent-native 内核。 func RunEntry(rc Ctx, opt EntryOptions) (EntryResult, error) { if strings.TrimSpace(opt.Goal) == "" { return EntryResult{}, fmt.Errorf("squad entry: goal text is empty") } cfg, enabled := nativeadp.Config{}, false if AgentNativeOptions != nil { cfg, enabled = AgentNativeOptions(rc) } if opt.Model != "" { cfg.DefaultModel = opt.Model } if opt.Stream != nil { cfg.Stream = opt.Stream if cfg.DefaultModel == "" { cfg.DefaultModel = "test" } enabled = true } if !enabled || cfg.DefaultModel == "" { return EntryResult{}, fmt.Errorf("squad entry: 主模型未配置") } cfg.HostTools = opt.HostTools cfg.OnEvent = opt.OnEvent if opt.MaxTurns > 0 { cfg.MaxTurns = opt.MaxTurns } if cfg.MaxTurns <= 0 { cfg.MaxTurns = math.MaxInt32 } cfg.MaxToolCalls = opt.MaxToolCalls hostNames := make([]string, 0, len(opt.HostTools)) for _, tool := range opt.HostTools { hostNames = append(hostNames, tool.Name()) } registry := squad.NewRoleRegistry() projectRun := rc.Run().ProjectID != "" for _, role := range kernelRoles(roleCaps{ host: hostNames, exec: cfg.Exec != nil, skills: len(cfg.SkillsRoots) > 0, search: cfg.Search != nil, fetch: cfg.Fetch != nil, workspace: !projectRun, }) { if err := registry.Register(role); err != nil { return EntryResult{}, fmt.Errorf("squad entry: register %s: %w", role.Role, err) } } backends := BuildBackends(rc) nativeadp.RegisterFull(backends, cfg) checker := opt.Checker if checker == nil { checker = quality.Checker{} } repair := opt.RepairBudget if repair <= 0 { repair = 2 } runDir := filepath.Join(opt.DataDir, "squad-entry", rc.Run().ID) engine, err := squad.NewEngine(squad.Options{ DataDir: runDir, Registry: registry, Planner: squad.NewRulePlanner(), Backends: backends, DefaultBackend: "agent-native", StepTimeout: -1, // 负数 = 不设单步截止(引擎只在 0 时回落到 10 分钟) HitlPolicy: opt.Hitl, Checker: checker, RepairBudget: repair, Router: opt.Router, Sink: squad.ProgressFunc(func(ev squad.Event) { onSquadEvent(rc, ev) }), }) if err != nil { return EntryResult{}, fmt.Errorf("squad entry: engine: %w", err) } res, err := engine.Run(rc.Context(), squad.Goal{Text: opt.Goal}, squad.RunOptions{}) for err == nil && res != 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 EntryResult{}, 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 EntryResult{}, fmt.Errorf("squad entry: %w", err) } if res == nil { return EntryResult{}, fmt.Errorf("squad entry: empty result") } if res.Plan != nil { _ = rc.UpdatePlan(mapPlan(res.Plan)) } if res.Status != squad.RunCompleted && res.Status != squad.RunDegraded { return EntryResult{Summary: res.Summary}, fmt.Errorf("squad entry: status %s: %s", res.Status, res.Error) } name := "squad-entry.md" if res.Deliverable != "" && !strings.Contains(res.Deliverable, "..") { name = filepath.Base(res.Deliverable) } return EntryResult{Summary: res.Summary, Body: readDeliverable(res), Name: name}, nil } func readDeliverable(res *squad.RunResult) string { if res.WorkDir != "" && res.Deliverable != "" && !strings.Contains(res.Deliverable, "..") { path := filepath.Join(res.WorkDir, filepath.FromSlash(res.Deliverable)) if b, err := os.ReadFile(path); err == nil { return string(b) } } return res.Deliverable } type roleCaps struct { host []string exec bool skills bool search bool fetch bool workspace bool } func kernelRoles(caps roleCaps) []squad.RoleSpec { base := []string{} if caps.workspace { base = []string{"read_file", "write_file", "list_dir", "grep"} } writerTools := append([]string{}, base...) writerTools = append(writerTools, "edit_file", "multi_edit", "glob") if caps.exec { writerTools = append(writerTools, "run_python", "run_command") } if caps.skills { writerTools = append(writerTools, "load_skill") } writerTools = append(writerTools, caps.host...) research := append([]string{}, base...) if caps.search { research = append(research, "web_search") } if caps.fetch { research = append(research, "fetch_url") } instr := func(kind, fallback string) string { if RoleInstructions != nil { if body, ok := RoleInstructions(kind); ok && strings.TrimSpace(body) != "" { return body } } return fallback } writerBash := squad.BashPermissionDeny if caps.exec { writerBash = squad.BashPermissionAsk } writerSkills := []string(nil) if caps.skills { writerSkills = []string{"docx-author", "xlsx-author", "pptx-author"} } return []squad.RoleSpec{ {Role: "leader", Name: "组长", Backend: "agent-native", BashPermission: squad.BashPermissionDeny, Tools: []string{"read_file", "write_file"}, Instructions: instr("leader", "先判断任务该直通还是拆开。复杂任务每次只委派一个成员,并写明技能与最低分。")}, {Role: "assistant", Name: "助手", Backend: "agent-native", BashPermission: writerBash, Tools: writerTools, Skills: writerSkills, Instructions: instr("assistant", "直接完成用户的这一件事。需要成文时按交付物工作流生成并登记,不要只在回复里声称文件已生成。")}, {Role: "planner", Name: "规划员", Backend: "agent-native", BashPermission: squad.BashPermissionDeny, Tools: base, Instructions: instr("planner", "梳理目标与材料,产出结构化要点与执行建议。")}, {Role: "researcher", Name: "调研员", Backend: "agent-native", BashPermission: squad.BashPermissionDeny, Tools: research, Instructions: instr("researcher", "围绕目标调研关键问题,输出简明结论与依据。")}, {Role: "writer", Name: "撰稿员", Backend: "agent-native", BashPermission: writerBash, Tools: writerTools, Skills: writerSkills, Instructions: instr("writer", "把前序要点与调研结论整理为清晰成文的交付文档。")}, {Role: "analyst", Name: "分析员", Backend: "agent-native", BashPermission: squad.BashPermissionDeny, Tools: append(append([]string{}, base...), caps.host...), Instructions: instr("analyst", "按现象、影响、隐性顾虑、建议四层写洞察。开头给一句话洞察,结尾给下一步行动。")}, {Role: "reviewer", Name: "审校员", Backend: "agent-native", BashPermission: squad.BashPermissionDeny, Tools: append(append([]string{}, base...), caps.host...), Instructions: instr("reviewer", "终审交付文档:一致性、完整性与风险提示,给出通过或修改意见。")}, } }