symfonic.platform.host¶
host ¶
Who owns an Agent, and for how long — the contracts (task-1-2-1).
PLAT-ADR's rule holds here as everywhere in this package: the platform derives who the caller is, decides whether the request may proceed, and hands the request to the public facade; it implements nothing an invocation does. A host composes and caches agents. It never takes a turn, never reaches into one, and never learns what a capability does.
Why this exists. symfonic.Agent is deliberately minimal: a provider,
instructions, a model, tools, capabilities. Its invocations take a prompt and
nothing about tenancy, and the published facade decision forbids adding a
per-turn scope — an agent is bound to one scope or it is not scoped at all.
A multi-tenant product therefore needs one agent per scope, and something has
to own that mapping. Today the generated scaffold owns it by building a single
SymfonicAgent and deriving a scope per request, which is the shape this
package replaces.
The four contracts the host fixes, because everything built later consumes them:
- Resource ownership. Pools, backends and embedders belong to the host, not to an agent. Agents are cheap and per-scope; a connection pool is not. An agent that owned a pool would open one per tenant and close none.
- Tenant resolution. One agent per scope, cached deterministically, keyed on the scope's canonical identity — the same identity memory writes, flushes and forgets with. Two callers naming the same scope get the same agent; two scopes never share one.
- Immutability. Composition is an argument, not a mutation. Nothing is
loaded into an agent after it is built, which is what makes a compiled plan
trustworthy — and it is why
load_plugin()has no equivalent here. - Shutdown. Closing is idempotent and closes shared resources exactly once. Closing while turns are in flight has a declared answer rather than a race.
Not here: transport, sessions, checkpoints, workers, observability. Each is a separate slice, and each consumes this one. Fixing the composition root first is what stops them from each inventing their own.
AgentComposer ¶
Bases: Protocol
Turns a scope and the process's shared resources into one Agent.
The adopter's composition root, expressed as a callable so the host never learns which capabilities a deployment folds. It receives the scope because capabilities are scope-bound at construction -- a memory capability is built for one tenant -- and that is precisely why an agent cannot be shared across scopes.
Synchronous on purpose. Composition assembles objects the host already opened; a composer that needed to await would be opening a resource of its own, which is the ownership this contract places elsewhere.
HostClosed ¶
Bases: RuntimeError
Raised when an agent is requested from a host that has been closed.
A distinct type rather than a bare RuntimeError because the caller's
correct response differs: a closed host during shutdown is expected and
should end the request, while a closed host during normal operation is a
lifecycle bug in the composition root. A message alone cannot be branched
on without matching strings, which is the pattern this codebase has been
removing.
PlatformAgentHost ¶
Bases: Protocol
Owns the process's shared resources and the registry over them.
The composition root a generated project's main builds once and closes
once. Everything a request needs comes from here; nothing a request does
happens here.
aclose
async
¶
Close the registry and then the shared resources, exactly once.
Idempotent, because shutdown paths run twice more often than they run cleanly: a lifespan that closes and an atexit that closes are both correct and both fire.
Source code in src/symfonic/platform/host.py
agent_for
async
¶
resume
async
¶
SharedResources ¶
Bases: Protocol
What every agent in a process borrows and none of them owns.
Deliberately opaque: the host holds it, hands it to the composer, and closes it. Naming its members here would make the host know what a deployment's memory stack is, which is the coupling this package exists to avoid -- an adopter with a different backend implements this and changes nothing else.
TenantAgentRegistry ¶
Bases: Protocol
The scope -> agent mapping, and the only place agents are created.
Concurrency is part of the contract, not an implementation detail: two coroutines asking for the same scope at once receive the same agent, and the composer runs once. A registry that raced would give one tenant two agents with two capability sets over one store, and the second would silently win.
aclose
async
¶
agent_for
async
¶
The agent bound to scope, composing it on first use.
Raises:
| Type | Description |
|---|---|
HostClosed
|
if the registry has been closed. |