Skip to Content
API classesArchitecturesProtocols + base types

Architecture protocols + base types

from loomflow import Architecture from loomflow.architecture import AgentSession, Dependencies

An 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

AttributeTypeDescription
namestrShort 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.

ParameterTypeDescription
sessionAgentSessionThe mutable per-run state. Architecture writes its progress here.
depsDependenciesBundled protocol implementations. Read-only struct.
promptstrThe 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

FieldTypeDefaultDescription
idstrrequiredRun id (a ULID). Used as the prefix for journal step names and event session ids.
instructionsstrrequiredThe system prompt (already concatenated with any skill catalog).
messageslist[Message][]The conversation so far. Architecture appends; Agent reads the seed messages from memory at run start.
turnsint0Increments as each iteration completes. Compared against Dependencies.max_turns.
outputstr""Final assistant answer. Architecture writes at termination.
cumulative_usageUsageUsage()Aggregate token / cost usage across all model calls in this run.
interruptedboolFalseSet to True when terminating early (budget, max_turns, guardrail, cancellation).
interruption_reasonstr | NoneNoneShort machine-readable reason: "budget:max_tokens", "max_turns_exceeded", "guardrail:<name>", etc.
metadatadict[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

FieldTypeDescription
modelModelThe model adapter to call (already wrapped by RetryingModel if a retry policy is enabled).
memoryMemoryThe memory backend for recall / remember. Already wrapped by AutoExtractMemory if auto_extract=True.
runtimeRuntimeWrap external calls with runtime.step(name, fn, *args) for replay-correct journaling.
toolsToolHostTool dispatch surface.
budgetBudgetCall budget.allows_step(user_id=...) before expensive work; budget.consume(...) after model calls.
permissionsPermissionsPer-tool gate. Architecture calls permissions.check(call, context=..., user_id=...) before dispatch.
hooksHookRegistryhooks.pre_tool(...) / hooks.post_tool(...) dispatch.
telemetryTelemetryasync with telemetry.trace(...); telemetry.emit_metric(...).
audit_logAuditLog | Noneawait audit_log.append(entry). None when not configured.
max_turnsintHard cap on iterations; check session.turns >= max_turns.

Optional fields

FieldTypeDefaultDescription
approval_handlerApprovalHandler | NoneNoneAsync callable that settles PermissionDecision.ask_(...) outcomes from the permissions layer. When unset, ask falls back to deny.
output_schema / effort / strict_effort / prompt_cachingNone / FalseThe resolved per-run values of the matching Agent / run() options, for architectures that make their own model calls.
streamingboolFalseTrue 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.
contextRunContextRunContext()The live run scope (user_id, session_id, run_id, metadata). Forward context.user_id to budget and memory calls.
tool_timeout_sfloat | None120.0Wall-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_charsint50_000Hard cap on tool-result text entering the conversation; longer results are truncated with a marker.
compact_contextCallable[[AgentSession], Awaitable[Event | None]] | NoneNoneSet 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, …offWiring 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:

FieldDefaultWhat it skips
fast_auditTrue_audit(...) calls when audit_log is None.
fast_telemetryTruetelemetry.trace(...) contextmanagers + emit_metric calls when telemetry is NoTelemetry.
fast_permissionsTruePer-tool permissions.check(...) when permissions is the no-op AllowAll.
fast_hooksTruehooks.pre_tool / hooks.post_tool dispatch when no hooks have been registered.
fast_runtimeTrueruntime.step(...) indirection when runtime is InProcRuntime.
fast_budgetTrueBudget checks when the budget is NoBudget.
fast_rate_limitTrueThe per-step rate_limiter.acquire(...) when no limiter is wired.
fast_guardrailsTrueGuardrail checks when no guardrails are wired.
fast_stop_hooksTrueThe stop-hook loop when no stop hooks (or run_until) are set.
fast_tool_summaryTrueTool-result summarisation when no summariser is wired.
fast_snipTrueConversation 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.

ParameterTypeDescription
callToolCallThe pending tool call awaiting approval.
user_idstr | NoneThe 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

loomflow/architecture/base.py

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.

Last updated on