Packages

Multi-agent orchestration strategies for GenAgent. One ensemble process owns N sub-agents under a strategy (Solo, Switchboard, Pool, Pipeline, Supervisor, Debate, Consensus).

Current section

Files

Jump to
gen_agent_ensemble guides workflows switchboard.md
Raw

guides/workflows/switchboard.md

# Switchboard workflow

## Topology

One ensemble process owning N named sub-agents. The caller specifies
which agent should handle each prompt via `opts[:agent]`. No
coordination across agents, no decomposition, no default target.

```
                                +---------------+
                       +------> |   alice       |
                       |        +---------------+
   +-------+   +-------+----+   +---------------+
   |  E.*  |-->| Switchboard|-->|   bob         |
   +-------+   +-------+----+   +---------------+
    (agent: x)         |        +---------------+
                       +------> |   carol       |
                                +---------------+
```

## When to reach for it

- You have a fixed team of specialised agents and want to direct
  questions to specific members explicitly.
- The split is editorial: "ask Alice for API review, ask Bob for
  perf review." A human or an orchestrator picks the target.
- You want each agent to maintain its own long-running conversation
  (Switchboard does, same as Solo).

For parallel fan-out of homogeneous work, use Pool. For LLM-driven
decomposition, use Supervisor. For a single agent, use Solo.

## Config

```elixir
config :gen_agent_ensemble,
  ensembles: [
    [
      name: "reviewers",
      strategy: GenAgentEnsemble.Strategies.Switchboard,
      opts: [
        agents: [
          {"alice", GenAgentEnsemble.Agents.Simple,
            backend: GenAgent.Backends.Anthropic,
            system: "You review API design. Be blunt about naming and shape.",
            model: "claude-sonnet-4-6"},
          {"bob", GenAgentEnsemble.Agents.Simple,
            backend: GenAgent.Backends.Anthropic,
            system: "You review performance. Look for O(n^2), allocation, IO.",
            model: "claude-sonnet-4-6"},
          {"carol", GenAgentEnsemble.Agents.Simple,
            backend: GenAgent.Backends.Anthropic,
            system: "You review tests. Flag missing edge cases and weak assertions.",
            model: "claude-sonnet-4-6"}
        ]
      ]
    ]
  ]
```

Agent names must be unique within the ensemble. Switchboard
namespacing (via the Server) means the same sub-agent name can be
reused in a different ensemble without collision.

## Canonical workflow

### Route a single sync turn

```elixir
iex> E.ask!("reviewers", "is `/api/v2/users?archive=1` a good shape?", agent: "alice")
"Two issues with that URL..."

iex> E.ask!("reviewers", "this query runs n+1 -- how bad?", agent: "bob")
"Bad. Here's the fix..."
```

### Address all three in iex

```elixir
iex> code = File.read!("lib/some_module.ex")
iex> for reviewer <- ~w(alice bob carol) do
...>   {reviewer, E.ask!("reviewers", code, agent: reviewer, timeout: 60_000)}
...> end
```

Sequential because Elixir's `for` is. Each call blocks until its
agent responds, then the next one starts.

### Async parallel round

```elixir
iex> tokens =
...>   for reviewer <- ~w(alice bob carol) do
...>     {reviewer, elem(E.tell("reviewers", code, agent: reviewer), 1)}
...>   end

iex> E.status("reviewers")
{:ok, %{
  agents: ["alice", "bob", "carol"],
  pending_per_agent: %{"alice" => 1, "bob" => 1, "carol" => 1},
  ...
}}

iex> for {name, token} <- tokens do
...>   {name, E.await("reviewers", token, 60_000).text}
...> end
```

All three fire before any can complete -- real parallel fan-out
across the three backend sessions.

### Inspect

```elixir
iex> E.status("reviewers")
{:ok, %{
  session: "reviewers",
  strategy: GenAgentEnsemble.Strategies.Switchboard,
  agents: ["alice", "bob", "carol"],
  pending_per_agent: %{"alice" => 0, "bob" => 0, "carol" => 0},
  ...
}}
```

## Failure modes

| Error                              | Cause                                            |
|------------------------------------|--------------------------------------------------|
| `{:error, :no_agent_specified}`    | `tell`/`ask` called without `opts[:agent]`       |
| `{:error, {:unknown_agent, name}}` | `opts[:agent]` not in the fleet                  |
| `{:error, reason}`                 | The agent's turn errored (that agent only fails) |
| `{:error, {:agent_down, reason}}`  | An agent crashed mid-turn; queued tokens fail    |
| session halts                      | Last agent dies; session exits                   |

A single agent's trouble does not take down the fleet. When the
last agent dies, the whole ensemble halts with
`{:halt, :switchboard_exhausted}`.

## Variations

- **Mix agent types.** Each slot takes its own `module` -- one agent
  can be a Simple wrapper over Anthropic, another a custom
  GenAgent callback, another a Claude-CLI agent with a `cwd`.
- **Specialisation by system prompt.** Same model, different
  prompts gets you most of the benefit for most tasks.
- **External router.** Pair Switchboard with a separate "router"
  Solo that classifies the prompt and picks a target -- then
  forward with `E.ask!("reviewers", text, agent: chosen)`.

## Gotchas

- **No default target.** If you forget `agent:`, you get
  `{:error, :no_agent_specified}` immediately. Intentional -- it
  prevents silent misrouting. If you want a "primary" fallback,
  wrap with your own helper.
- **No broadcast (yet).** v1 routes to exactly one agent per call.
  Hitting all three is a `for` comprehension from the caller. A
  future `tell_all/2` might ship if demand appears.
- **Agent conversations accumulate.** Like Solo, each agent's
  backend session retains history across turns. Input tokens grow
  over time. Stop and re-start the ensemble to reset.
- **Ordering is per-agent FIFO.** Multiple tells to the same agent
  queue in order; their reply tokens come back in the same order.
  Tells to different agents run in parallel -- the order they
  *return* depends on backend latency, not dispatch order.