Why: F13 - the squad factory read the default account with an empty project, pairing the run user's model name with another account's endpoint and credentials; direct and squad paths disagreed on reasoning split and route contract. Evidence: app/runtime/squadadapter tests pass (verify-u10b.json); run-scoped snapshot, project override, profile override and fail-closed cases added with synthetic keys. Impact: squad execution now uses the same effective-config rules as the direct path; default setups unchanged. Refs: feature/agent-stack-audit-remediation-b1 U10
238 lines
8.0 KiB
Go
238 lines
8.0 KiB
Go
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", "终审交付文档:一致性、完整性与风险提示,给出通过或修改意见。")},
|
|
}
|
|
}
|