loom.ai.runtime¶
Live agent runtime: one entered lifecycle, shared clients, bounded runs.
The lifecycle lives in _lifecycle, the shared MCP
sessions and their tool-filter validation in _mcp, the
per-run limits in _limits and the health vocabulary in
_health. This package is the whole public surface.
Nothing here imports FastAPI or Starlette: the HTTP surface lives in
loom.ai.fastapi and this package stays usable from any transport.
The classes here are experimental and may change within a major line; the
artifact format they run is not. See loom.ai for the distinction.
- class loom.ai.runtime.AgentHealth(*, status, checks=mappingproxy({}), detail=None)[source]¶
Bases:
LoomFrozenStructCached health of one agent and of its live dependencies.
- Parameters:
- status¶
Aggregate state, the worst of every check.
- Type:
Literal[‘ok’, ‘degraded’, ‘unavailable’]
- checks¶
Per-dependency state, keyed
"model","mcp:<server>","a2a:<agent>"or"sql:<connection>", always by the name the deployment registered rather than by URL. Internal topology: only an authenticated caller ever sees it (FR-029c).- Type:
- class loom.ai.runtime.AgentRuntime(*, plans, config, engine_provider, deps, container, sql_config=None, mcp_client_factory=None, a2a_client_factory=None, use_case_mcp=())[source]¶
Bases:
objectOwns the live agents of one worker: clients, engines and their limits.
Usable only as an async context manager, and only from the task that entered it: every live client is opened through one
AsyncExitStackcreated in__aenter__, so closing happens in strict reverse order in the same task. That is what keeps a session-affine client (MCP over a framed transport) from being closed by a task that never opened it.- Parameters:
plans (Sequence[AgentPlan]) – Compiled plans this worker serves.
config (AiConfig) – Deployment configuration of the AI pillar.
engine_provider (AgentEngineProvider) – Provider building one engine per plan, exactly once.
deps (DepsFactory) – Per-invocation dependency factory handed to every engine.
container (LoomContainer) – Application container the engines resolve services from.
sql_config (SqlConfig | None) – Live
sql:configuration, re-verified at start-up against what the plans were compiled against (FR-046).mcp_client_factory (McpClientFactory | None) – Builds the client of one MCP capability. Required when any plan declares an
mcpcapability: without it the declared tool filters cannot be validated, so start-up fails closed rather than serving unvalidated grants. A missing factory is a wiring bug, not an offline network, soai.remote_clients: optionaldoes not tolerate it either.a2a_client_factory (A2AClientFactory | None) – Builds the client of one A2A capability, with the same fail-closed rule.
use_case_mcp (Sequence[UseCaseMcpGrant]) – Every
Mcp()marker binding a declaring use case carries, already verified and compiled — seeUseCaseMcpGrant. Each one’s server joins the set of clients this runtime opens, even when no agent plan names it. A server named only this way stays outside the background health probe: this runtime never reports it as an/healthcheck entry — see_probe_forever(), which iterates the per-plan slots. What a caller observes when such a server is down is decided bymcp_marker_resolver(): a tolerated-unreachable server resolves to a handle whose every call raisesTOOL_UNAVAILABLE, never a bind-time failure.
Runs emit no span of their own: the transport owns observability, because only it knows the route, the method and the status code a run is attributed to. Over HTTP that owner is
bind_agent_endpoints().- Raises:
AgentCompilationError – From
__aenter__, aggregating every start-up failure — an unreachable server (named as the deployment registered it, never by URL; tolerated underai.remote_clients: optional), a tool filter matching nothing, two grants of one MCP server name describing different connections, or a SQL connection whose read-only state drifted.- Parameters:
plans (Sequence[AgentPlan])
config (AiConfig)
engine_provider (AgentEngineProvider)
deps (DepsFactory)
container (LoomContainer)
sql_config (SqlConfig | None)
mcp_client_factory (McpClientFactory | None)
a2a_client_factory (A2AClientFactory | None)
use_case_mcp (Sequence[UseCaseMcpGrant])
Example:
async with AgentRuntime( plans=plans, config=ai_config, engine_provider=provider, deps=deps, container=container, ) as runtime: result = await runtime.run("analyst", prompt, identity=identity)
- capability_kinds(name)[source]¶
Return the capability kinds one agent was granted.
Lets a transport state what a route exposes without reaching into the compiled plan, which carries resolved handles and instructions.
- async run(name, prompt, *, identity, conversation_id=None, output_type=None, state=None)[source]¶
Run one agent to completion.
- Parameters:
name (str) – Agent to run.
prompt (str) – Caller prompt.
identity (Identity) – Verified caller; every capability call runs as them.
conversation_id (str | None) – Opaque value the application supplies; selects the conversation the loader use case receives, when the artifact declares one, and is copied verbatim into the output hook’s command. Never read by loom.
output_type (type[Any] | None) – When given, decode this run’s answer into this type instead of the plan’s own declared output (T304); the plan’s own output check does not run.
None— the default — runs the plan’s declared shape exactly as before this parameter existed. Not part ofAgentEngine’s ownrun/run_stream, whose signature is pinned to exactlyprompt,identityandconversation: an engine opts into shape overrides through the separate, optionalrun_stream_shapedcapability instead (see_grants).state (object | None) – This run’s state, already decoded by whichever boundary received it and passed as a normalised mapping, or
None. Resolved against name’s declared shape before this call reaches the engine (FR-009, FR-010):Noneon a stateful artefact whose every field carries a default carries the shape’s own declared defaults;Noneagainst a shape with a field that has no default fails the call, since there is no default to fill it with; a non-Nonevalue against an artefact declaring no shape fails the call.
- Returns:
The decoded output, the run’s usage, its
interaction_id, the output hook’s result and the run’s new messages.- Raises:
KeyError – When no agent is named name.
ValueError – When
conversation_idis empty or longer thanCONVERSATION_ID_MAX_LENGTH.AgentRunError – When the run is refused (
TOO_MANY_RUNS), the conversation cannot be loaded (CONVERSATION_LOAD_FAILED,CONVERSATION_LOAD_TIMEOUT),stateis given against an artefact declaring none (STATE_UNDECLARED),stateis omitted against a shape with a field that has no default (STATE_REQUIRED), breaches a declared limit, or ends in a failure event.
- Return type:
- run_stream(name, prompt, *, identity, conversation_id=None, state=None)[source]¶
Run one agent, streaming its supervised events.
The stream is an async context manager so the engine connection behind it closes deterministically instead of waiting for the collector.
- Parameters:
name (str) – Agent to run.
prompt (str) – Caller prompt.
identity (Identity) – Verified caller; every capability call runs as them.
conversation_id (str | None) – Opaque value the application supplies; selects the conversation the loader use case receives, when the artifact declares one, and is copied verbatim into the output hook’s command. Never read by loom.
state (object | None) – This run’s state; see
run()’s ownstatefor what it carries and how it is resolved against name’s declared shape.
- Returns:
An async context manager yielding the limit-supervised events; the terminal event carries the run’s
interaction_id.- Raises:
KeyError – When no agent is named name.
ValueError – On entry, when
conversation_idis empty or longer thanCONVERSATION_ID_MAX_LENGTH.AgentRunError – On entry, when the worker’s
max_concurrent_runsis already taken (TOO_MANY_RUNS), the conversation cannot be loaded (CONVERSATION_LOAD_FAILED,CONVERSATION_LOAD_TIMEOUT),stateis given against an artefact declaring none (STATE_UNDECLARED), orstateis omitted against a shape with a field that has no default (STATE_REQUIRED).
- Return type:
AbstractAsyncContextManager[AsyncIterator[TextDeltaEvent | ToolCallEvent | ToolResultEvent | ErrorEvent | FinalEvent]]
- async health(name)[source]¶
Return the cached health of one agent — never network I/O per call.
- state_shape(name)[source]¶
Return one agent’s declared state shape.
Read by
_decode_state()to parse a request’s rawstatebytes against the artefact’s own shape (FR-008), and available before this runtime is entered — a compiled plan already carries its shape, unlike a grant, which is resolved only once the runtime opens its clients.- Parameters:
name (str) – Agent whose declared shape is read.
- Returns:
The plan’s
StateShape, orNonefor an artefact declaring neitherdeps_typenordeps_schema.- Raises:
KeyError – When no agent is named name.
- Return type:
StateShape | None
- use_case_mcp_grant(server)[source]¶
Return the shared substrate grant of one server
Mcp()markers declared.This is the server-keyed substrate — live session, full unfiltered catalogue — every binding on server shares; it is not any one caller’s filtered view, and its own capability carries an explicitly empty
include/exclude(see_build_use_case_grants()). A caller’s own filter comes from its ownMcpBinding, passed to the resolver by the executor at resolution time, never from the value this method returns.- Parameters:
server (str) – MCP server name a marker declared.
- Returns:
The resolved shared grant, or
Nonewhen server was never declared by any use case, or its connection was tolerated as unreachable underai.remote_clients: optional.- Raises:
RuntimeError – When the runtime was never entered. This follows
_require_slot’s own rule rather than calling it: that method calls_require_planfirst, which would raiseKeyErroron a server name that is not an agent name.- Return type:
McpGrant | None
- class loom.ai.runtime.ConcurrentMcpSession[source]¶
Bases:
objectDeclares an
McpSessionimplementation already safe for concurrent calls.A JSON-RPC session is one framed stream: two callers writing into it at the same time can interleave their frames, and a caller cancelled mid-call can leave the stream desynchronised for whoever is waiting beside it. That is why a session gets serialised behind one lock by default. A session that already guards its own frames — one that multiplexes concurrent calls by matching each response back to its own request id, rather than writing straight through a single unmatched stream — declares that guarantee by also subclassing this, in addition to implementing
McpSession, and the runtime leaves it unwrapped.This class carries no members: subclassing it is the declaration. A session that does not subclass it is treated exactly as every
McpSessionwas before this class existed — wrapped and serialised — because not declaring the guarantee is the safe default, never a failure.A subclass keeping this promise must (a) tolerate overlapping calls by matching each response back to its own request id — the multiplexing that makes the guarantee true in the first place — and (b) must not shield a call from its own caller’s cancellation the way the runtime’s locked wrapper does: with no shared frame to desynchronise, there is nothing left to drain, and shielding would only stop the plan’s own
tool_timeout_msfrom bounding the call.
- class loom.ai.runtime.McpSession(*args, **kwargs)[source]¶
Bases:
ProtocolMinimal MCP session the runtime needs from any client library.
Migration (breaking, from v1.16.1):
list_toolsused to return tool names (tuple[str, ...]) andcall_toolused to return the server’s structured content directly (object). Both shapes shipped, so a third-party session implementing this Protocol has to update both methods.list_toolsnow returnsMcpToolInfoso a caller can see which tools publish an output schema, andcall_toolnow returnsMcpToolCallResultso a caller can see the server’s own failure flag instead of having it silently folded into a successful-looking return. Nothing else about the Protocol moved.- async list_tools()[source]¶
Return the tools the server exposes.
- Returns:
Every tool the server advertises, before any declared filter is applied, each carrying whether it publishes an output schema.
- Return type:
tuple[McpToolInfo, …]
Bases:
objectSerialises every call to one MCP session shared by concurrent runs.
A JSON-RPC session is a single framed stream: two overlapping calls interleave their frames, and a caller cancelled mid-frame leaves the session desynchronised for its neighbours. Both are prevented here — one lock per session, and the in-flight call shielded and drained to completion before the lock is released, after which the cancellation is re-raised to the caller that asked for it.
- Parameters:
session (McpSession) – The live session to guard.
label (str) – Human-readable name used in log messages.
Return the tools the server exposes, serialised with every other call.
- Returns:
Every tool the underlying session advertises.
- Return type:
tuple[McpToolInfo, …]
Invoke one tool, serialised with every other call on this session.
- Parameters:
- Returns:
The tool’s result.
- Raises:
asyncio.CancelledError – When the caller is cancelled. The in-flight call still runs to completion, so the session stays usable.
- Return type:
- class loom.ai.runtime.UseCaseMcpGrant(capability, usecase, parameter)[source]¶
Bases:
objectOne verified
Mcp()marker binding, compiled into a runtime input.Built by
loom.rest.fastapi.auto(_resolve_ai) once the marker’s server name has already been verified againstai.mcp_servers, and handed toAgentRuntimeso the server it names joins the set the runtime opens even when no agent plan declares it.capabilitycarries this binding’s owninclude— read only by_use_case_filter_issues(), to check it at start-up against the server’s real tool list. At call time the resolver never reads thisincludeback: it receives its own from the resolvingMcpBinding, via the executor, which is why one instance of this class exists per binding rather than per server, even though start-up and call time end up checking the same value on two different objects. The shared grant a server’s live session and full catalogue are read from is a separate, server-keyedMcpGrantbuilt by_build_use_case_grants(), whose own capability carries noincludeat all.usecasenames the declaring use case, and is also carried intoFilterTarget.agentfor the server this binding lists on its own (_use_case_filter_targets()) — carried, not read: nothing on that path reads the field back.parameteris read in one place only: to name the offending signature when the marker’s ownincludematches no tool the server publishes.- Parameters:
capability (CompiledMcpCapability)
usecase (str)
parameter (str)