feat(swarm): agent_swarm — one prompt over many items, paced as a fleet
Ports kimi-code's AgentSwarm: a `prompt_template` containing `{{item}}`
expanded over an `items` list into up to 128 subagents, run to completion
and returned as one aggregate. Kigi already exceeds upstream on planning,
verification, isolation and merge via /graph; what it lacked was cheap
immediate fan-out. Entirely client-side — no new backend surface.
The engine is three pure pieces plus a runner: `plan` validates and
expands (every fault reported before a single member starts — a
half-launched swarm is expensive to unwind), `schedule` is the launch
ramp as testable arithmetic, `run` drives it against the existing
`SubagentBackend`. Reuses the single-spawn coordinator rather than
inventing a batch API.
Load-bearing decisions, each the result of a defect found in review:
- `backgrounded` is its own outcome. A member that outlives the 600s
foreground budget is detached by the coordinator and KEEPS RUNNING;
reporting it as failed invites the model to relaunch its item, putting
a second agent on the same files. It is never offered for resume.
- `InFlightGuard` cancels live members on Drop. Send-now cancels the turn
WITHOUT cancelling subagents and aborts the task; the dropped receivers
read as "parent gone" and each child re-attaches itself. There is no
cooperative path to use instead — `Cancellation` is constructed nowhere
in the tree — so Drop is the only seam that fires.
- Retries and wall clock are both bounded. The swarm blocks the caller's
turn, so every wait needs a ceiling it cannot argue past; stragglers at
the deadline are reported as still-running, with their ids.
- `ToolKind::AgentSwarm` is its own variant: `TemplateRenderer`'s
`by_kind` map holds one tool name per kind, so sharing `Task` would
silently redirect `${{ tools.by_kind.task }}` in other tools' prompts.
- An explicitly requested model that cannot be validated is refused, as
the task tool already does — one loud error beats `items.len()` quiet
ones. Depth stays capped at 1: upstream's unlimited nesting is a
hazard, not a feature.
- `SubagentResult.rate_limited` is classified where the typed ACP error
code is still in hand; a scheduler re-deriving it from a formatted
string would stop adapting the day the wording changed.
- Aggregate output is clamped per member (head+tail, loss stated):
native tool output is truncated nowhere downstream.
42 agent_swarm tests. The fake backend awaits, so the concurrency and
ordering assertions can actually fail; the cap test also proves the
fixture can exceed the cap.
This commit is contained in:
@@ -2157,7 +2157,10 @@ fn extract_variant(tc: &acp::ToolCall) -> Option<&str> {
|
||||
}
|
||||
/// Twin without the optional-toolset spelling.
|
||||
fn is_task_variant(variant: Option<&str>) -> bool {
|
||||
matches!(variant, Some("Task"))
|
||||
// The swarm belongs here for the same reason `Task` does: its members each
|
||||
// raise their own SubagentBlock, and it must register the blocking wait or
|
||||
// a call that holds the turn until every member finishes shows no spinner.
|
||||
matches!(variant, Some("Task" | "AgentSwarm"))
|
||||
}
|
||||
/// Twin without the optional-toolset spelling.
|
||||
fn is_write_variant(variant: Option<&str>) -> bool {
|
||||
@@ -2190,8 +2193,10 @@ fn is_goal_tool(tc: &acp::ToolCall) -> bool {
|
||||
/// SubagentSpawned notification) provides better visibility. Covers the
|
||||
/// `task` / `Task` / `spawn_subagent` ids and Task-family variant tags.
|
||||
fn is_task_tool(tc: &acp::ToolCall) -> bool {
|
||||
matches!(tc.title.as_str(), "task" | "Task" | "spawn_subagent")
|
||||
|| is_task_variant(extract_variant(tc))
|
||||
matches!(
|
||||
tc.title.as_str(),
|
||||
"task" | "Task" | "spawn_subagent" | "agent_swarm"
|
||||
) || is_task_variant(extract_variant(tc))
|
||||
}
|
||||
/// Check if a tool call is a scheduler tool (scheduler_create/delete/list).
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user