Orchestration — MICROSTEP execution plan (separate session)
Excluded from the 2026-07-04 fix session. Execute in a dedicated later session. Follow steps in order; do not redesign. Same 5 gates, run separately.
Confirmed decisions (from Q&A)
- Topology: per-run selector (D). Existing
ForegroundOrchestrator/TaskOrchestrator/swarmrunAll/workflows are the backends the user chooses among at run start. - Models: config-only role→model map (A). Roles define
model/provider/allowed_tools. - Pool caps: bounded agent pool with
max_agents_per_modelandmax_total_agents— acquire blocks/queues when a cap is hit. - Tabs: one tab per agent instance + one orchestrator tab + one Summary tab (aggregate stats).
- Steering: target driven by active tab — Summary → broadcast to all; Orchestrator/Agent → that target only. Orchestrator may also post messages into managed agents on its own.
- Persistence: fully event-sourced & resumable (A) under
.goa/orchestrator/<run-id>/. - Execution layer: layered ABOVE swarm (A) — reuse
runAll/runOne/prepareConfigas the primitive; swarm tool stays. - Goal binding: optional (B) — a run may be goal-less or bound to a
core/goalgoal (per-agent + aggregate budgets).
References (read fully before coding)
multiagent/agent_pool.go,agent_tool.go,agent_driven_tools.go,orchestrator.go,foreground_orchestrator.go(InjectSteering/checkSteering),task_orchestrator.go,pipeline.go,pipelines.go,runner.go,workflow_registry.go,workflow_tool.go,mode_resolver.go.core/swarm/(mode.go, injection.go, enter/exit_reminder.md),tools/swarm/agent_swarm.go(prepareConfig/runAll/runOne).core/goal/(model.go, mode.go, store.go, budget.go, injection.go, outcome.go).core/steering.go(SteeringQueue — generalized by fix B5),core/event,core/agentmanager_events.go.tui/goal/panel.go,tui/swarm/renderer.go,tui/status.go,tui/footer_data.go,tui/compositor.go,tui/model.go.- kimi-code patterns: append dynamic content as user-role message on top of history;
mergeConsecutiveUserMessages.
Phase 0 — Foundations & config schema
- Create package
core/orchestrator/(new). Adddoc.godescribing the package and the per-run topology selector. - Config schema (
config/config.go): add anOrchestratorsection:
Implement structorchestrator: roles: <name>: { model: <id>, provider: <id>, allowed_tools: [<names>] } pool: max_total_agents: 8 max_agents_per_model: { <model_id>: <int> } defaults: topology: hub | fanout | pipelineOrchestratorConfigwithRoles map[string]OrchestratorRole,Pool.OrchestratorPoolConfig,Defaults.Topology. Add defaults inconfig/defaults.go. Add validation inconfig/config_validate.go(role model must exist in configured models; caps ≥1). Wire into merge (config/config_merge.go). - Add config completion entries in
core/commands/config_completion.go:orchestrator.roles.*,orchestrator.pool.max_total_agents,orchestrator.pool.max_agents_per_model.*,orchestrator.defaults.topology(enum: hub/fanout/pipeline). - Tests:
config/orchestrator_test.go— parse/merge/validate/default; unknown model → error.
Phase 1 — Bounded agent pool with caps
- Add
core/orchestrator/pool.go:type BoundedAgentPool structwrapping*multiagent.AgentPool. Fields:totalCap int,perModelCap map[string]int, current countstotal int,perModel map[string]int,mu sync.Mutex,waiters []chan struct{}. Acquire(roleName string) (*AgentHandle, error): resolve role→model fromOrchestratorConfig.Roles; block (register waiter) whiletotal>=totalCapORperModel[model]>=perModelCap[model]; on success increment counts andpool.GetOrCreate(role)(reuse multiagent). Return handle.Release(h *AgentHandle): decrement counts, signal the head waiter (FIFO). Useselect{ default }on signal so a non-blocking notify never blocks.- Tests
core/orchestrator/pool_test.go(-race): concurrent Acquire up to caps; assert block then release unblocks; per-model cap respected; total cap respected.
Phase 2 — AgentHandle & live state
- Add
core/orchestrator/handle.go:type AgentHandle struct { ID, Role, Model string; agent *agentic.Agent; stats *AgentStats; events chan Event; steering *core.SteeringQueue; done chan struct{} }. AgentStats:{Turns, TokensIn, TokensOut, CacheRead, CacheCreation, ToolCalls int; Status AgentStatus; StartedAt, UpdatedAt time.Time}. Updated from provider usage events (subscribe to the existing usage/cache hook used byinternal/app/stats.go).- Per-agent steering queue: each handle owns a
*core.SteeringQueue(generalized from B5). MethodsSteer(text)(Append) anddrainSteering()(Flush). - Tests: stats update on synthetic usage event; steering append/drain.
Phase 3 — Orchestrator runtime (topology selector)
- Add
core/orchestrator/orchestrator.go:type Orchestrator structwithcfg OrchestratorConfig,pool *BoundedAgentPool,topology Topology,goal *goal.Goal(optional),store *EventStore,agents map[string]*AgentHandle,orchestratorAgent *agentic.Agent,eventBus chan Event. Topologyenum:Hub,Fanout,Pipelinewith a factoryNewTopology(name, deps) Topologymapping to existing primitives:Hub→ delegation via aDelegateToolthat callspool.Acquire(role)+runOne.Fanout→ reusetools/swarm/agent_swarm.go::runAllwith a constructedmultiagent.AgentConfig.Pipeline→ reusemultiagent/pipeline.go/ForegroundOrchestratorstages.
DelegateTool(core/orchestrator/delegate_tool.go, implementsagentic.Tool): input{role, task, [model_override?]}; callspool.Acquire(role), emitsAgentStarted, runs the sub-agent (stream forwarded asAgentMessageevents), on finish emitsAgentFinished{outcome}thenpool.Release. The orchestrator agent has this tool available.Run(ctx, objective):- If goal-bound: create/load goal via
core/goal(reuseGoalManager), inject static reminder (B3a model) into orchestrator system prompt. - Acquire orchestrator agent (role
orchestrator, its own model fromcfg.Roles["orchestrator"]). - Drive the chosen topology until the goal reaches a terminal state (goal-bound) or the orchestrator emits a terminal message (goal-less).
- Between cycles: drain orchestrator steering queue + each agent's steering queue; inject as appended user-role messages (kimi-code style, same helper as B3a/B5).
- If goal-bound: create/load goal via
- Orchestrator→agent posting: the orchestrator may call
DelegateToolwith a steering payload that targets an existing agent handle (AgentSteeredevent) — allowed per Q3b. - Tests:
orchestrator_test.gowith a scripted/fake provider (multiagent/test_provider_test.gois the model):- Hub: 1 delegation round-trip emits Started/Message/Finished.
- Fanout: N agents run, all events emitted, counts released.
- Pipeline: stages run in order, each feeds the next.
- Caps: exceeding
max_total_agentsblocks then proceeds on Release.
Phase 4 — Event sourcing & resumability
- Add
core/orchestrator/store.go:EventStore(NDJSON under.goa/orchestrator/<run-id>/events.jsonl), mirroringcore/goal/store.go. Event types:RunStarted,AgentStarted,AgentMessage,AgentSteered,AgentStats,AgentFinished,RunFinished. Append on every state transition. - On
NewOrchestrator,os.MkdirAll(".goa/orchestrator/<run-id>", 0o755); append-only event log; snapshot agent stats periodically (reusecore/goal/serialize.gopatterns). Resume(runID): replay events to rebuildagentsmap, stats, steering queues, and goal state; resume the run from the last non-terminal event. Crashed mid-flight agents are markedCrashedand re-acquired.- Tests: append/replay round-trip; resume after a synthetic crash restores agent set + stats + goal; idempotent.
- List API:
ListRuns()scans.goa/orchestrator/*/events.jsonlfor the TUI run picker.
Phase 5 — TUI: orchestrator view + tabs + summary
- New package
tui/orchestrator/.type View structimplementingtui.Component: holds tabs[]Tab(Summary,Orchestrator, then one per agent),active int, a local chat viewport per tab (reusetui/chat_viewport.go), and a steeringEditor. - Tab content:
- Summary tab: a table (reuse
tui/status.gostyling per row) — one row per agent: Role, Model, Turns, Tokens, CacheRead%, Status. Plus the goal header (reusetui/goal/panel.go) when goal-bound. - Orchestrator tab: orchestrator chat transcript (streamed
OrchestratorMessageevents). - Agent tab: that agent's streamed conversation + a compact stats header.
- Summary tab: a table (reuse
- Steering input: a dedicated
Editorbound to the active tab. On submit:- Summary tab → broadcast:
orch.SteerAll(text)(Append to every agent queue + orchestrator). - Orchestrator tab →
orch.OrchestratorSteer(text). - Agent tab →
agentHandle.Steer(text). Show pending steering pinned above the input (reusefooter_data.SteeringPendingfrom B5, generalized to per-target).
- Summary tab → broadcast:
- Event subscription: the View subscribes to the orchestrator eventBus. ALL events are posted onto the TUI
commandLoopas messages (do NOT touch component state from goroutines — preserve the single-owner invariant; see R1 findings). Each event updates the relevant tab's viewport + the summary table + stats. - Open/close: a keybinding (e.g.
Ctrl+O) or/orchestrateslash command toggles orchestrator mode. On open, show run picker (existing runs fromListRuns()) + "New run" + topology selector + optional goal bind. Register the command incore/commands/(init()self-register per AGENTS.md). - Cursor/focus: the orchestrator view's steering Editor owns the cursor (apply the B6 compositor fix —
CURSOR_MARKERhonored for the topmost component). Tab switching via←/→. - Tests
tui/orchestrator/*_test.go: tab switching; summary table renders N agents; steering submit routes to the correct target based on active tab; events from a fake orchestrator update the view; all state mutations happen on a single goroutine (assert via a commandLoop stub).
Phase 6 — Goal binding integration
BindGoal(runID, goalID or objective): if objective given, create goal viacore/goal.GoalManager; inject static reminder into the orchestrator system prompt (B3a) and per-agent sub-objectives.- Budget enforcement: per-agent budget + aggregate. Reuse
core/goal/budget.go. When aggregate budget exhausted → orchestrator receives a budget-band reminder (dynamic, appended user message) and must callUpdateGoal(blocked). - Completion: agents report sub-results; orchestrator synthesizes and calls
UpdateGoal(complete)only when all sub-results satisfy the completion criterion (reusecore/goal/outcome.go). - Tests: goal-bound run reaches
completeonly after all agents finish; budget exhaustion forcesblocked; aggregate vs per-agent budget accounting correct.
Phase 7 — Wiring & command surface
- Wire
core/orchestratorintointernal/app/subsystems.go: construct the pool fromcfg.Orchestrator, expose the orchestrator manager oncore.Context. - Slash commands (
core/commands/orchestrator*.go, self-register):/orchestrate new [topology] [goal <id|objective>],/orchestrate list,/orchestrate resume <run-id>,/orchestrate steer <agent-id|all|orchestrator> <text>. Add completions. - Headless mode:
--orchestrate <run-id>flag inbootstrap.goto resume a run headless (mirrors--goal). - Telemetry: emit orchestrator lifecycle events via the existing telemetry client (
core/goal/telemetry.gopattern).
Phase 8 — Validation & gates
- End-to-end interactive_shell scenarios:
- New hub run with 2 distinct-model agents + goal; switch tabs; watch live stats; steer one agent; broadcast from Summary; confirm orchestrator posts back to an agent; reach
complete. - Fanout run of 3 agents; Summary tab shows all; kill terminal mid-run → restart →
Resumerestores the run. - Pipeline run; stage outputs feed next stage.
- Caps: configure
max_total_agents: 2, launch 3 → third blocks until one finishes.
- New hub run with 2 distinct-model agents + goal; switch tabs; watch live stats; steer one agent; broadcast from Summary; confirm orchestrator posts back to an agent; reach
- Stability:
-raceacross all the above; no goroutine leak (R1 methodology). - Perf: confirm per-event TUI update cost is bounded (R2 methodology); summary table re-renders only changed rows.
- Run all 5 gates separately; fix any new violation.
- Update README/docs with orchestrator mode usage; commit.
Risk notes (carry into execution)
- Funnel ALL orchestrator events through the TUI
commandLoop(post as messages) — never mutate component state from event goroutines (preserves R1 single-owner invariants). - Keep orchestrator/agent system prompts byte-stable across turns; deliver dynamic progress (stats, budget band) as appended user-role messages (B3a model) so prompt cache stays warm.
- Per-model + total caps must release on all exit paths (panic, ctx cancel, crash) — use
defer pool.Release(h)and arecover()in the run wrapper.