Architecture protocols + base types
from loomflow import Architecture
from loomflow.architecture import AgentSession, DependenciesAn architecture is the strategy an Agent uses to work through a
task (ReAct, plan-and-execute, a supervisor with workers, and so on).
You only need this page if you are writing your own architecture or
reading the built-in ones. It covers the three pieces involved: the
Architecture protocol every strategy implements, the per-run state
it updates (AgentSession), and the bundle of backends the Agent
hands it on each run (Dependencies).
For the conceptual overview see Architectures. For implementing a custom architecture see Custom architectures.
Architecture
from typing import Protocol, runtime_checkable
from collections.abc import AsyncIterator
@runtime_checkable
class Architecture(Protocol):
name: str
def declared_workers(self) -> dict[str, "Agent"]: ...
async def run(
self,
session: AgentSession,
deps: Dependencies,
prompt: str,
) -> AsyncIterator[Event]:
...The strategy that drives the agent loop. The Agent calls
architecture.run(session, deps, prompt) and consumes the events it
yields. The Agent itself emits the setup event (Event.started)
and the teardown event (Event.completed); architectures yield only
the events that happen during iteration.
Attributes
| Attribute | Type | Description |
|---|---|---|
name | str | Short identifier used in telemetry / logging (e.g. "react", "supervisor", "my-strategy"). |
Methods
declared_workers
def declared_workers(self) -> dict[str, Agent]: ...Names of any sub-agents the architecture delegates to, mapped to
their Agent instances. Used by AgentGraph
for visualization. Empty dict for single-agent loops.
run
async def run(
self,
session: AgentSession,
deps: Dependencies,
prompt: str,
) -> AsyncIterator[Event]: ...Async generator yielding Events as iteration progresses. Mutates
session in place (messages, turns, output, cumulative_usage,
interrupted, interruption_reason, metadata).
The Agent may call run again on the same session when a stop
hook (for example run_until=) asks it to keep going. On re-entry,
session.messages is not empty and prompt is a new user turn:
append it as a Role.USER message rather than starting over.
| Parameter | Type | Description |
|---|---|---|
session | AgentSession | The mutable per-run state. Architecture writes its progress here. |
deps | Dependencies | Bundled protocol implementations. Read-only struct. |
prompt | str | The user’s message for this pass (the opening prompt, or a stop hook’s follow-up on re-entry). |
AgentSession
@dataclass
class AgentSession:
id: str
instructions: str
messages: list[Message] = field(default_factory=list)
turns: int = 0
output: str = ""
cumulative_usage: Usage = field(default_factory=Usage)
interrupted: bool = False
interruption_reason: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)Mutable per-run state shared between Agent and the architecture.
The Agent constructs this once per run, the architecture mutates it
as iteration progresses, and the Agent reads the final state to
build a RunResult.
Fields
| Field | Type | Default | Description |
|---|---|---|---|
id | str | required | Run id (a ULID). Used as the prefix for journal step names and event session ids. |
instructions | str | required | The system prompt (already concatenated with any skill catalog). |
messages | list[Message] | [] | The conversation so far. Architecture appends; Agent reads the seed messages from memory at run start. |
turns | int | 0 | Increments as each iteration completes. Compared against Dependencies.max_turns. |
output | str | "" | Final assistant answer. Architecture writes at termination. |
cumulative_usage | Usage | Usage() | Aggregate token / cost usage across all model calls in this run. |
interrupted | bool | False | Set to True when terminating early (budget, max_turns, guardrail, cancellation). |
interruption_reason | str | None | None | Short machine-readable reason: "budget:max_tokens", "max_turns_exceeded", "guardrail:<name>", etc. |
metadata | dict[str, Any] | {} | Free-form dict for architecture-specific state (planners stash plans, debate stashes round transcripts, etc.). |
Dependencies
@dataclass
class Dependencies:
model: Model
memory: Memory
runtime: Runtime
tools: ToolHost
budget: Budget
permissions: Permissions
hooks: HookRegistry
telemetry: Telemetry
audit_log: AuditLog | None
max_turns: int
approval_handler: ApprovalHandler | None = None
output_schema: Any | None = None
effort: str | None = None
strict_effort: bool = False
prompt_caching: Any = None
streaming: bool = False
context: RunContext = field(default_factory=RunContext)
tool_timeout_s: float | None = 120.0
tool_result_max_chars: int = 50_000
compact_context: Callable[[AgentSession], Awaitable[Event | None]] | None = None
# ...plus the settings for optional features (rate limiter,
# guardrails, tool-result summariser, run_until checker, snip,
# tool search, memory budget) and the fast_* flags below.Bundled protocol implementations passed to every architecture.
Constructed once per run from the Agent’s configured backends.
Architectures treat this as read-only. They call methods on the
contained protocols but don’t mutate the struct.
Core fields
| Field | Type | Description |
|---|---|---|
model | Model | The model adapter to call (already wrapped by RetryingModel if a retry policy is enabled). |
memory | Memory | The memory backend for recall / remember. Already wrapped by AutoExtractMemory if auto_extract=True. |
runtime | Runtime | Wrap external calls with runtime.step(name, fn, *args) for replay-correct journaling. |
tools | ToolHost | Tool dispatch surface. |
budget | Budget | Call budget.allows_step(user_id=...) before expensive work; budget.consume(...) after model calls. |
permissions | Permissions | Per-tool gate. Architecture calls permissions.check(call, context=..., user_id=...) before dispatch. |
hooks | HookRegistry | hooks.pre_tool(...) / hooks.post_tool(...) dispatch. |
telemetry | Telemetry | async with telemetry.trace(...); telemetry.emit_metric(...). |
audit_log | AuditLog | None | await audit_log.append(entry). None when not configured. |
max_turns | int | Hard cap on iterations; check session.turns >= max_turns. |
Optional fields
| Field | Type | Default | Description |
|---|---|---|---|
approval_handler | ApprovalHandler | None | None | Async callable that settles PermissionDecision.ask_(...) outcomes from the permissions layer. When unset, ask falls back to deny. |
output_schema / effort / strict_effort / prompt_caching | None / False | The resolved per-run values of the matching Agent / run() options, for architectures that make their own model calls. | |
streaming | bool | False | True when the consumer reads from agent.stream(). Architectures should preserve real-time event arrival when set. When False (the default for agent.run()), batching events for fewer task-group / channel allocations is allowed on the hot path. |
context | RunContext | RunContext() | The live run scope (user_id, session_id, run_id, metadata). Forward context.user_id to budget and memory calls. |
tool_timeout_s | float | None | 120.0 | Wall-clock cap on a single tool call. On timeout the tool returns an error result so the model can react. None disables it. |
tool_result_max_chars | int | 50_000 | Hard cap on tool-result text entering the conversation; longer results are truncated with a marker. |
compact_context | Callable[[AgentSession], Awaitable[Event | None]] | None | None | Set when the agent has auto_compact_at_tokens. Await it before each model call: it compacts session.messages when they are past the threshold and returns the auto_compacted event to yield (auto_compact.failed when compaction failed, None when nothing was done). ReAct does this. |
rate_limiter, guardrails, tool_result_summarizer, goal_checker, snip_window, tool_search, memory_token_budget, … | off | Wiring for the matching optional Agent features. The shared helpers in loomflow.architecture.helpers read these for you. |
Fast-mode flags
Set automatically by the Agent based on which features are wired.
When a layer is a no-op default, the hot path skips its call sites
entirely:
| Field | Default | What it skips |
|---|---|---|
fast_audit | True | _audit(...) calls when audit_log is None. |
fast_telemetry | True | telemetry.trace(...) contextmanagers + emit_metric calls when telemetry is NoTelemetry. |
fast_permissions | True | Per-tool permissions.check(...) when permissions is the no-op AllowAll. |
fast_hooks | True | hooks.pre_tool / hooks.post_tool dispatch when no hooks have been registered. |
fast_runtime | True | runtime.step(...) indirection when runtime is InProcRuntime. |
fast_budget | True | Budget checks when the budget is NoBudget. |
fast_rate_limit | True | The per-step rate_limiter.acquire(...) when no limiter is wired. |
fast_guardrails | True | Guardrail checks when no guardrails are wired. |
fast_stop_hooks | True | The stop-hook loop when no stop hooks (or run_until) are set. |
fast_tool_summary | True | Tool-result summarisation when no summariser is wired. |
fast_snip | True | Conversation snipping when snip_window=0. |
As soon as a real implementation is wired, the matching flag turns
False and the integration point becomes active. You never set these
flags yourself.
ApprovalHandler (type alias)
ApprovalHandler = Callable[[ToolCall, str | None], Awaitable[ApprovalDecision | bool]]Async callable that settles a PermissionDecision.ask_(...) from
permissions.
| Parameter | Type | Description |
|---|---|---|
call | ToolCall | The pending tool call awaiting approval. |
user_id | str | None | The active RunContext.user_id, so the handler can scope the approval to the right user / channel. |
Returns True to allow the call or False to deny it, an
action name ("allow", "deny", "edit", "remember_allow",
"remember_deny"), or an ApprovalDecision for richer outcomes
(edit the arguments, remember the answer for the rest of the run, or
deny with a reason). Anything else, such as "yes" or
{"approved": True}, denies, and so does a handler that raises.
(Before 0.17 the result was read with bool(), so "deny" approved
the call.) See Approval handlers.
Source
Reading further. ReAct is the reference implementation and
exercises every protocol method. Read react.py
once to see how the pieces fit together, then the multi-agent shapes
in supervisor.py
and swarm.py.