package builtin // team_tool.go implements the Team tool -- the model-callable entry point that // lets an agent autonomously spawn a TEAM of N specialized sub-agents that run // IN PARALLEL, can message each other over a shared bus, and (optionally) share // a task board, then returns the aggregated results. // // This is the missing creation surface for the Agent Teams machinery: the five // coordination tools (send_message + shared_task_*) are already registered in // every engine, but they no-op ("not in a Team") because nothing ever created // a Team context. The Team tool creates that context (the current engine // becomes the Leader, the workers become its teammates), lighting them up. // // Relationship to the Agent tool (avoid overlap ambiguity): // - Agent: delegate ONE sub-task to ONE sub-agent (sync / background / worktree). // - Team: fan out N sub-agents that run in PARALLEL and can coordinate // (peer messaging + optional shared task board), aggregated report. // // Decoupling: like AgentTool/SkillTool, TeamTool depends only on the // TeamExecutor interface (defined here, implemented in the engine package), so // builtin never imports engine (no import cycle). The executor is injected via // SetExecutor after engine.New (see engine.SetupTeamTool). // // team_tool.go 实现 Team 工具 -- 模型可调的创建入口, 让 agent 自主开一个 N 人 // 并行小队 (N 个专用子 agent 并发跑, 经共享总线互发消息, 可选共享任务板), // 汇总结果返回. // // 这是 Agent Teams 机器缺失的创建面: 5 个协调工具 (send_message + shared_task_*) // 每个引擎都已注册, 但因为从没有人创建 Team 上下文, 它们全返回 "not in a Team" // 空转. Team 工具创建该上下文 (当前引擎成为 Leader, worker 成为它的队友), // 把它们点活. // // 与 Agent 工具的分工 (避免重叠歧义): // - Agent: 委派单个子任务给单个子 agent (sync / background / worktree). // - Team: 并行扇出 N 个能协调 (互发消息 + 可选共享任务板) 的子 agent, 汇总. // // 解耦: 同 AgentTool/SkillTool, TeamTool 只依赖 TeamExecutor 接口 (定义在此, // engine 包实现), builtin 不 import engine (无循环依赖). 执行器由 engine.New // 之后经 SetExecutor 注入 (见 engine.SetupTeamTool). import ( "context" "encoding/json" "fmt" "strings" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/permission" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/tools" ) // TeamWorkerSpec is one worker's spec (model input + executor argument). // // TeamWorkerSpec 是单个 worker 的规格 (模型输入 + 执行器入参). type TeamWorkerSpec struct { // Prompt is the task assigned to this worker. Prompt 分配给该 worker 的任务. Prompt string `json:"prompt"` // AgentType selects the worker's capability profile ("" = general-purpose). // AgentType 选择 worker 的能力档 ("" = general-purpose). AgentType string `json:"agent_type,omitempty"` // Description is a short label for tracking. Description 是追踪用的短标签. Description string `json:"description,omitempty"` // Model overrides the worker model ("" = inherit). Model 覆盖 worker 模型 ("" = 继承). Model string `json:"model,omitempty"` } // TeamRunRequest is the Team execution request passed to the executor. // // TeamRunRequest 是传给执行器的 Team 执行请求. type TeamRunRequest struct { // Workers is the set of workers to spawn in parallel. Workers 是并发 spawn 的 worker 集合. Workers []TeamWorkerSpec // SharedTasks, when true, gives the team an in-memory shared task board // (the shared_task_* tools become usable among workers). For a persistent / // compliance store, consumers use the engine.NewTeam + tasklist.New(store) // Go API instead. // // SharedTasks 为 true 时给小队一块内存共享任务板 (shared_task_* 工具在 worker // 间可用). 要持久化 / 合规存储, 消费者改用 engine.NewTeam + tasklist.New(store) // Go API. SharedTasks bool } // TeamWorkerResult is one worker's result. It mirrors engine.WorkerResult but // carries Error as a string (empty = success) so the result type does not cross // the package boundary with a Go error value. // // TeamWorkerResult 是单个 worker 的结果. 镜像 engine.WorkerResult, 但 Error 用 // string 携带 (空串 = 成功), 让结果类型不带 Go error 值跨包. type TeamWorkerResult struct { WorkerID string AgentType string Description string Result string Error string DurationMs int64 } // TeamExecutor is the Team tool's executor interface (implemented in the engine // package via dependency inversion, same pattern as AgentExecutor / // SkillExecutor -- builtin defines the interface, engine implements it). // // TeamExecutor 是 Team 工具的执行器接口 (engine 包经依赖倒置实现, 与 // AgentExecutor / SkillExecutor 同一模式 -- builtin 定义接口, engine 实现). type TeamExecutor interface { // RunTeam spawns the workers in parallel with the current engine as Leader, // waits synchronously for all of them, and returns their results. // // RunTeam 以当前引擎为 Leader 并发 spawn workers, 同步等待全部完成, 返回结果. RunTeam(ctx context.Context, req TeamRunRequest) ([]TeamWorkerResult, error) } // TeamTool is the model-callable Team tool. TeamTool 是模型可调的 Team 工具. type TeamTool struct { executor TeamExecutor } // NewTeamTool creates a TeamTool. executor is nil until SetExecutor is called // (by engine.SetupTeamTool), mirroring AgentTool/SkillTool. // // NewTeamTool 创建 TeamTool. executor 初始为 nil, 由 engine.SetupTeamTool 经 // SetExecutor 注入, 与 AgentTool/SkillTool 一致. func NewTeamTool() *TeamTool { return &TeamTool{} } // SetExecutor injects the Team executor. 注入 Team 执行器. func (t *TeamTool) SetExecutor(exec TeamExecutor) { t.executor = exec } // Name returns the tool name. The capitalized "Team" matches Agent/Skill and // keeps the prompt-cache tool-name key stable. // // Name 返回工具名. 大写 "Team" 与 Agent/Skill 一致, 保持 prompt cache 工具名 key 稳定. func (t *TeamTool) Name() string { return "Team" } // Description returns the tool description. func (t *TeamTool) Description(_ context.Context) string { return "Spawn a team of specialized sub-agents that run IN PARALLEL and report back. " + "Use this when a task splits into several independent pieces that can be worked at the same time " + "(e.g. explore three directories at once, draft and review concurrently). " + "Each worker is an isolated sub-agent with its own context window; workers can message each other " + "and, with shared_tasks enabled, share a task board. " + "Differs from the Agent tool: Agent delegates ONE sub-task to ONE sub-agent; Team fans out N workers in parallel and aggregates their results. " + "Returns each worker's final response (or its error)." } // InputSchema returns the JSON Schema input definition. func (t *TeamTool) InputSchema() json.RawMessage { return json.RawMessage(`{ "type": "object", "properties": { "workers": { "type": "array", "description": "The workers to spawn in parallel. Provide at least one.", "items": { "type": "object", "properties": { "prompt": { "type": "string", "description": "The task prompt for this worker" }, "agent_type": { "type": "string", "description": "Optional agent type (e.g. 'Explore', 'general-purpose'). Determines the worker's tool subset. Defaults to 'general-purpose'." }, "description": { "type": "string", "description": "A short label describing this worker's task (used for tracking)" }, "model": { "type": "string", "description": "Optional model override for this worker (defaults to the parent model)" } }, "required": ["prompt"] } }, "shared_tasks": { "type": "boolean", "description": "When true, the team gets a shared task board so workers can publish/claim/complete tasks among themselves. Default false." } }, "required": ["workers"] }`) } // Metadata returns the tool metadata. func (t *TeamTool) Metadata() tools.Metadata { return tools.Metadata{ ConcurrencySafe: false, ReadOnly: false, Destructive: false, SearchHint: "team parallel workers fan-out multi-agent orchestrate concurrent", PermissionClass: permission.PermClassGeneric, AuditOperation: "invoke", } } // teamInput is the Team tool's input. teamInput 是 Team 工具的输入. type teamInput struct { Workers []TeamWorkerSpec `json:"workers"` SharedTasks bool `json:"shared_tasks,omitempty"` } // Execute spawns the team and returns the aggregated worker results. func (t *TeamTool) Execute(ctx context.Context, input json.RawMessage, progress tools.ProgressFunc) (*tools.Result, error) { var in teamInput if err := json.Unmarshal(input, &in); err != nil { return &tools.Result{Output: fmt.Sprintf("team: invalid input: %v", err), IsError: true}, nil } if len(in.Workers) == 0 { return &tools.Result{Output: "team: at least one worker is required (workers array is empty)", IsError: true}, nil } for i, w := range in.Workers { if strings.TrimSpace(w.Prompt) == "" { return &tools.Result{Output: fmt.Sprintf("team: worker[%d] prompt is required", i), IsError: true}, nil } } // executor not injected: honest failure (病根 #1, same as Agent/Skill). With // SetupTeamTool wired in engine.New this path is normally unreachable, but a // disabled tool (DisableTeamTool) or a misconfigured embedder must see a // failure, not a fake success. // // executor 未注入: 诚实失败 (病根 #1, 同 Agent/Skill). 现 SetupTeamTool 已在 // engine.New 接线, 此路常态不可达, 但被禁用的工具 (DisableTeamTool) 或误配的 // 消费方必须看到失败而非假成功. if t.executor == nil { return &tools.Result{Output: "Team tool is not available: executor not configured " + "(it may be disabled via cfg.DisableTeamTool, or the embedder never wired SetupTeamTool).", IsError: true}, nil } if progress != nil { progress(0.1, fmt.Sprintf("Spawning a team of %d workers", len(in.Workers))) } results, err := t.executor.RunTeam(ctx, TeamRunRequest{Workers: in.Workers, SharedTasks: in.SharedTasks}) if err != nil { return &tools.Result{Output: fmt.Sprintf("team execution failed: %v", err), IsError: true}, nil } if progress != nil { progress(1.0, "Team completed") } // Aggregate. Per-worker failures are reported in the text; IsError is set // only when EVERY worker failed (a total wipeout), so a partial failure // still returns the workers that succeeded rather than discarding all. // // 聚合. 每个 worker 的失败在文本里报出; IsError 仅当**所有** worker 都失败 // (全军覆没) 时置位, 这样部分失败仍返回成功的那些 worker 而非全丢. var sb strings.Builder failCount := 0 fmt.Fprintf(&sb, "Team run complete: %d worker(s).\n\n", len(results)) for i, r := range results { label := r.Description if label == "" { label = r.AgentType } fmt.Fprintf(&sb, "--- Worker %d [%s] (agent_type=%s, %dms) ---\n", i+1, label, r.AgentType, r.DurationMs) if r.Error != "" { failCount++ fmt.Fprintf(&sb, "FAILED: %s\n", r.Error) } else { fmt.Fprintf(&sb, "%s\n", r.Result) } sb.WriteString("\n") } allFailed := len(results) > 0 && failCount == len(results) return &tools.Result{ Output: sb.String(), IsError: allFailed, Data: map[string]any{ "worker_count": len(results), "fail_count": failCount, }, }, nil }