Pool chains — a worked example¶
A pool chain is a dynamic workflow built from several bounded local
action spaces (pools) connected by gateway
agents. The builder defines the local worlds and the transition points;
the runtime path emerges from agent decisions and ends when a legitimate
terminal agent calls conclude.
This page works through a single scenario — a product-investigation
workflow with three local worlds — first as a progressive chain and
then as a recombining variant. Read Dynamic graph
(AgentPool + conclude) first for the primitives and the
Plan vs AgentPool (static vs dynamic) duality.
The scenario¶
Three local worlds, each a pool:
Discovery Pool: scout, analyst, gateway_to_build
Build Pool: architect, implementer, tester, gateway_to_release
Release Pool: reviewer, approver, publisher
Rules:
gateway_to_buildbelongs to Discovery but carries the Build pool's route tool — it is the only way into Build.gateway_to_releasebelongs to Build but carries the Release pool's route tool — it is the only way into Release.- Only
approverorpublishermayconclude. Nobody else can end the whole chain. - Every member reaches its own pool through
pool.as_tool()with an explicit name:ask_discovery_pool,ask_build_pool,ask_release_pool.
stateDiagram-v2
[*] --> Discovery
Discovery --> Build: gateway_to_build
Build --> Release: gateway_to_release
Release --> [*]: conclude (approver / publisher)
Progressive (non-recombining) chain¶
from lazybridge import Agent, AgentPool, LLMEngine, conclude
discovery_pool = AgentPool(max_depth=8)
build_pool = AgentPool(max_depth=8)
release_pool = AgentPool(max_depth=8)
# --- Discovery local world -------------------------------------------------
scout = Agent(
name="scout",
description="Gathers raw evidence and source material for the question.",
engine=LLMEngine("claude-haiku-4-5", max_tool_calls_per_turn=1),
tools=[discovery_pool.as_tool("ask_discovery_pool")],
)
analyst = Agent(
name="analyst",
description="Turns scout evidence into structured findings and open questions.",
engine=LLMEngine("claude-haiku-4-5", max_tool_calls_per_turn=1),
tools=[discovery_pool.as_tool("ask_discovery_pool")],
)
gateway_to_build = Agent(
name="gateway_to_build",
description="Gateway from Discovery into Build. Crosses only when requirements are stable.",
engine=LLMEngine(
"claude-opus-4-8",
system=(
"You belong to Discovery but can enter Build. "
"Stay in Discovery while requirements are unclear. "
"Call the build pool only when findings are stable enough to "
"design, implement, or test."
),
max_tool_calls_per_turn=1,
),
tools=[
discovery_pool.as_tool("ask_discovery_pool"),
build_pool.as_tool("ask_build_pool"),
],
)
# --- Build local world -----------------------------------------------------
architect = Agent(
name="architect",
description="Designs the solution from stable requirements.",
engine=LLMEngine("claude-opus-4-8", max_tool_calls_per_turn=1),
tools=[build_pool.as_tool("ask_build_pool")],
)
implementer = Agent(
name="implementer",
description="Implements the architect's design.",
engine=LLMEngine("claude-opus-4-8", max_tool_calls_per_turn=1),
tools=[build_pool.as_tool("ask_build_pool")],
)
tester = Agent(
name="tester",
description="Validates the implementation against the findings.",
engine=LLMEngine("claude-haiku-4-5", max_tool_calls_per_turn=1),
tools=[build_pool.as_tool("ask_build_pool")],
)
gateway_to_release = Agent(
name="gateway_to_release",
description="Gateway from Build into Release. Crosses only when build is tested and green.",
engine=LLMEngine(
"claude-opus-4-8",
system=(
"You belong to Build but can enter Release. "
"Call the release pool only when the implementation is built and tested."
),
max_tool_calls_per_turn=1,
),
tools=[
build_pool.as_tool("ask_build_pool"),
release_pool.as_tool("ask_release_pool"),
],
)
# --- Release local world ---------------------------------------------------
reviewer = Agent(
name="reviewer",
description="Reviews the release candidate; routes to approver or back for fixes.",
engine=LLMEngine("claude-opus-4-8", max_tool_calls_per_turn=1),
tools=[release_pool.as_tool("ask_release_pool")],
)
approver = Agent(
name="approver",
description="Approves the release. A legitimate terminal agent.",
engine=LLMEngine("claude-opus-4-8", max_tool_calls_per_turn=1),
tools=[release_pool.as_tool("ask_release_pool"), conclude],
)
publisher = Agent(
name="publisher",
description="Publishes the approved result and concludes the chain.",
engine=LLMEngine("claude-opus-4-8", max_tool_calls_per_turn=1),
tools=[release_pool.as_tool("ask_release_pool"), conclude],
)
# --- Register each pool AFTER its members exist ----------------------------
# A forward gateway is registered ONLY in its source pool. It is reachable
# from the pool it leaves, and carries the next pool's route tool to step
# forward — but destination agents cannot select it, so there is no path back.
discovery_pool.register(scout, analyst, gateway_to_build)
build_pool.register(architect, implementer, tester, gateway_to_release)
release_pool.register(reviewer, approver, publisher)
result = scout("Should we ship a usage-based pricing tier? Investigate, build a plan, release it.")
print(result.text()) # whatever approver/publisher passed to conclude(...)
What this expresses:
- The builder does not enumerate every transition. No edge list maps
scout → analystorarchitect → tester. Those routes are chosen at runtime inside each pool. - Agents choose the next specialist by routing inside their local
pool. Discovery members only see
ask_discovery_pool. - A phase transition occurs only when a gateway agent is selected and
decides to call the next pool.
gateway_to_buildis the only door from Discovery to Build;gateway_to_releasethe only door onward. - This is a progressive, non-recombining chain: it flows
Discovery → Build → Release and terminates at
conclude. Because each forward gateway is registered only in its source pool, agents in a later world cannot select it, so there is no route back to an earlier world — which makes the chain easy to trace. (If a gateway is also registered in its destination pool, that destination becomes able to route back through it — which is exactly how the recombining variant below is built.)
Recombining (reversible-boundary) variant¶
Replace the one-way gateway_to_build with a reversible boundary
controller that can send work back to Discovery when Build exposes
ambiguity. Only the boundary changes; the rest of the graph is the same.
boundary_discovery_build = Agent(
name="boundary_discovery_build",
description=(
"Reversible boundary between Discovery and Build. "
"Enter Build when requirements are stable; "
"return to Discovery when Build exposes missing or contradictory requirements."
),
engine=LLMEngine(
"claude-opus-4-8",
system=(
"You are the reversible boundary between Discovery and Build. "
"Use the discovery pool for clarification and evidence. "
"Use the build pool for design, implementation, and testing. "
"Return to discovery if build work reveals ambiguity. "
"Make progress on each pass; do not bounce without new information. "
"You are not a terminal agent — do not conclude."
),
max_tool_calls_per_turn=1,
),
tools=[
discovery_pool.as_tool("ask_discovery_pool"),
build_pool.as_tool("ask_build_pool"),
],
)
discovery_pool.register(scout, analyst, boundary_discovery_build)
build_pool.register(boundary_discovery_build, architect, implementer, tester, gateway_to_release)
stateDiagram-v2
[*] --> Discovery
Discovery --> Build: boundary_discovery_build (forward)
Build --> Discovery: boundary returns on ambiguity
Build --> Release: gateway_to_release
Release --> [*]: conclude (approver / publisher)
What this changes:
- A reversible gateway can move forward or backward between local
worlds.
boundary_discovery_buildcan re-enter Discovery when Build surfaces a gap. - This makes the chain recombining / recurrent: execution can revisit Discovery, so the same pool can be a state more than once.
- It is still bounded — by pool membership, by
max_depthon each pool, and by terminalconcludeplacement onapprover/publisheronly. The boundary explicitly is not a terminal agent.
This is not a static DAG. The path is selected at runtime inside builder-defined local worlds. It is also not a strict Markov process unless the full context, memory, store, and conversation history are treated as part of the state — two visits to Discovery differ by the evidence accumulated in between.
Notes and cautions¶
- A reversible boundary is a high-authority node — it controls movement between worlds and carries context across phases. Keep its prompt explicit about when to stay, cross, return, and (never, here) conclude, and keep its progress criteria sharp to avoid oscillation.
- Keep
concludeon terminal roles only. If discovery or build members could conclude, the chain could end before reaching Release. - Keep
max_depthlow on recombining chains; pair it with route tracing so a boundary that bounces without progress is visible. - When an outer lifecycle must stay deterministic (fixed phase
boundaries, checkpoints, typed hand-offs), wrap the pool chain in a
Planrather than expressing that lifecycle as more gateways.
See also¶
- Dynamic graph (AgentPool + conclude) — the primitives, gateway and reversible-gateway patterns, the state-process model, and the structural-scoping caveat.
- Plan — the static workflow runtime for when the path must be known, validated, typed, and checkpointed.
- Multi-agent graphs — API reference for
AgentPool,conclude,ConcludeSignal.