package engine // team_executor.go implements builtin.TeamExecutor -- the bridge between the // model-callable Team tool (builtin package) and the Team orchestration API // (engine package). It is the creation surface that was missing: NewTeam / // RunWorkers had ZERO callers, so the Agent Teams machinery (router, worker // inboxes, shared task board, the five coordination tools) was fully built but // unreachable. SetupTeamTool wires this executor into the Team tool so the // model can spawn a team; consumers can still call NewTeam / RunWorkers / // RunWorkersSync directly via the Go API. // // Design (mirrors agent_executor.go): builtin defines the TeamExecutor // interface, engine implements it, builtin never imports engine (no cycle). // // team_executor.go 实现 builtin.TeamExecutor -- 连接模型可调的 Team 工具 // (builtin 包) 与 Team 编排 API (engine 包). 这是此前缺失的创建面: NewTeam / // RunWorkers 零调用, 故 Agent Teams 机器 (router / worker inbox / 共享任务板 / // 5 个协调工具) 全建好却不可达. SetupTeamTool 把本执行器接进 Team 工具, 模型 // 即可开小队; 消费者仍可经 Go API 直接调 NewTeam / RunWorkers / RunWorkersSync. // // 设计 (镜像 agent_executor.go): builtin 定义 TeamExecutor 接口, engine 实现, // builtin 不 import engine (无循环依赖). import ( "context" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/inbox" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/permission" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/tasklist" "git.flytoex.net/yuanwei/flyto-agent/core/pkg/tools/builtin" ) // teamExecutor implements builtin.TeamExecutor. It holds the parent Engine, // which becomes the Leader of every team it spawns. // // teamExecutor 实现 builtin.TeamExecutor. 它持有父 Engine, 父 Engine 成为它 // spawn 的每个小队的 Leader. type teamExecutor struct { parentEngine *Engine } // newTeamExecutor creates a teamExecutor bound to parent. func newTeamExecutor(parent *Engine) *teamExecutor { return &teamExecutor{parentEngine: parent} } // RunTeam spawns the requested workers in parallel with the parent engine as // Leader, waits synchronously, and returns the aggregated results. Implements // builtin.TeamExecutor. // // RunTeam 以父引擎为 Leader 并发 spawn 请求的 worker, 同步等待, 返回聚合结果. // 实现 builtin.TeamExecutor. func (te *teamExecutor) RunTeam(ctx context.Context, req builtin.TeamRunRequest) ([]builtin.TeamWorkerResult, error) { pe := te.parentEngine // Optional in-memory shared task board. A persistent / compliance store is // the consumer's job via the NewTeam + tasklist.New(customStore) Go API // (the engine core must not assume a host FS / DB layout -- ADR-0005). // // 可选内存共享任务板. 持久化 / 合规存储归消费者, 经 NewTeam + // tasklist.New(customStore) Go API (引擎核心不假设宿主 FS / DB 布局 -- ADR-0005). var tl *tasklist.TaskList if req.SharedTasks { tl = tasklist.New(tasklist.NewMemoryStore()) defer tl.Close() } // Build the Team struct directly (same package) instead of via NewTeam. // NewTeam mutates the Leader engine (sets incomingInbox / agentName / // teamRouter / teamPermissionHandler) for the consumer-orchestrated case // where the Leader runs its own loop -- but here the Leader is THIS engine, // mid-runLoop inside the Team tool's Execute, so mutating it is wrong. A // fresh router scoped to these workers gives worker<->worker send_message + // the shared task board without touching the live parent engine. // // 直接构造 Team struct (同包) 而非经 NewTeam. NewTeam 为消费者编排 (Leader 跑 // 自己 loop) 的场景会改 Leader 引擎 (设 incomingInbox / agentName / teamRouter / // teamPermissionHandler) -- 但这里 Leader 就是当前引擎, 正阻塞在 Team 工具的 // Execute 里, 改它是错的. 一个 scoped 到这批 worker 的新 router 提供 // worker<->worker send_message + 共享任务板, 且不碰活的父引擎. team := &Team{ cfg: TeamConfig{LeaderEngine: pe, SharedTaskList: tl}, router: inbox.NewRouter(), } // Worker permission posture = the parent engine's, applied SYNCHRONOUSLY. // We deliberately do NOT use RunWorkers' Worker->Leader bubble: it blocks on // a response from a Leader runLoop poll, but the Leader (this engine) is // blocked inside Execute and cannot answer -> deadlock until ctx timeout. // Reusing pe.perms applies the same consumer policy (and the same runtime // "屏蔽" via PermissionHandler deny) without a round-trip. When the parent // gate is off (local default, no handler) the checker stays nil = no gate, // mirroring the parent exactly (local-no-sandbox stance, project_sandbox_local_vs_cloud). // // Worker 权限姿态 = 父引擎的, 同步施加. 故意不用 RunWorkers 的 Worker->Leader // 冒泡: 它阻塞等 Leader runLoop poll 的响应, 但 Leader (当前引擎) 阻塞在 Execute // 里无法应答 -> 死锁到 ctx 超时. 复用 pe.perms 施加同一消费者策略 (以及经 // PermissionHandler deny 的同款运行时 "屏蔽"), 且无往返. 父引擎闸关闭时 (本地 // 默认, 无 handler) checker 保持 nil = 无闸, 与父引擎完全一致 (本地不沙盒立场). var checker permission.Checker if pe.permGateEnabled { checker = pe.perms } specs := make([]WorkerSpec, len(req.Workers)) for i, w := range req.Workers { specs[i] = WorkerSpec{ AgentType: w.AgentType, Prompt: w.Prompt, Description: w.Description, Model: w.Model, } } results, err := team.RunWorkersSync(ctx, specs, checker) if err != nil { return nil, err } out := make([]builtin.TeamWorkerResult, len(results)) for i, r := range results { errStr := "" if r.Error != nil { errStr = r.Error.Error() } out[i] = builtin.TeamWorkerResult{ WorkerID: r.WorkerID, AgentType: r.AgentType, Description: r.Description, Result: r.Result, Error: errStr, DurationMs: r.Duration.Milliseconds(), } } return out, nil } // SetupTeamTool wires the Team tool's executor (mirror SetupAgentExecutor / // bindSkillExecutor). Tool-gated: a no-op when the Team tool is not registered // (cfg.DisableTeamTool removed it, or a custom toolset does not include it), so // it is always safe to call unconditionally. // // SetupTeamTool 接通 Team 工具的执行器 (镜像 SetupAgentExecutor / bindSkillExecutor). // tool-gated: Team 工具未注册时 (cfg.DisableTeamTool 移除了它, 或自定义工具集 // 不含它) no-op, 故无条件调用永远安全. func SetupTeamTool(engine *Engine) { teamTool, ok := engine.tools.Get("Team") if !ok { return } if tt, ok := teamTool.(*builtin.TeamTool); ok { tt.SetExecutor(newTeamExecutor(engine)) } }