symfonic¶
symfonic ¶
symfonic — the canonical entry point.
from symfonic import Agent
agent = Agent(AnthropicProvider())
result = await agent.run("Say hello.")
That is the whole of the simple API: one required dependency, no
FrameworkConfig, no tenant scope, no context manager, no background flush,
and nothing printed about capabilities you did not ask for.
Everything here is resolved through a PEP 562 module __getattr__ and the
module body executes no package import (FAC-1). Two consequences, both
deliberate:
import symfonic.memorycosts exactly what it cost before this module existed — the top level taxes nobody.from symfonic import Agentimports only the facade chain, which is what makes the import-hermeticity contract (QUI-1: nosymfonic.memory*, nofastapi, nomcp, …) mechanically checkable rather than aspirational.
__all__ is exactly the twelve names in T2.1.1's facade-surface.json.
A thirteenth needs a preservation-matrix decision (API-ADR §2 A3) and a
deprecation-registry row, which is the point of writing it down here.
Stability. Every new name enters at tier 2 (provisional), per API-ADR
§1's rule for new surfaces: the import path is already stable, the shape may
still change at a minor with a registry entry, and promotion to tier 1 is
gated on W2 exit. Attachment and StructuredOutputError are tier-1
classes whose canonical paths are unchanged — the top-level alias is an
additional path, never a move, so nothing here deprecates anything.
Reach for symfonic.agent.SymfonicAgent instead when you need HMS memory,
tenant scope, sessions, checkpoints and resume, sub-agents, or the full
callback surface. It is untouched and stays tier 1.
Agent ¶
Agent(model_provider: Any, *, instructions: str | None = None, model: str | ModelConfig | None = None, tools: Sequence[Any] = (), capabilities: Sequence[CapabilityConfig] = (), max_model_rounds: int | None = None)
Bases: AgentContinuationMixin
A stateless, tool-capable agent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model_provider
|
Any
|
Any object satisfying |
required |
instructions
|
str | None
|
The system prompt, used verbatim. |
None
|
tools
|
Sequence[Any]
|
A |
()
|
capabilities
|
Sequence[CapabilityConfig]
|
The typed optional-behavior seam. Absence means
disabled; there are no boolean feature flags. Anything with a
|
()
|
max_model_rounds
|
int | None
|
How many provider round trips one |
None
|
Construction validates and stores, and does nothing else (FAC-8): no
connection, no task, no file, no environment read, no chat model. The
first side effect of an Agent is the provider call inside run().
That is what makes async with and close() optional (LIF-1) — an
agent that acquired nothing leaks nothing.
One instance may serve unlimited concurrent run/stream calls on
one event loop, because no per-invocation state is stored on it (FAC-9).
It is not documented as safe to share across event loops.
Source code in src/symfonic/agent/facade.py
capabilities
property
¶
Names actually folded into this agent's invocation plan.
composition_manifest
property
¶
Payload-free attestation of the capability fold this agent retained.
close
async
¶
Release facade-owned resources. Idempotent, and never raises.
In W1 the facade owns nothing, so this only marks the instance closed.
It never closes anything the adopter passed in — the provider outlives
the agent. An in-flight run() is allowed to finish; close()
does not cancel it (LIF-4).
Source code in src/symfonic/agent/facade.py
run
async
¶
run(prompt: str, *, attachments: Sequence[Attachment] = (), history: Sequence[Message] = (), state: Mapping[str, Any] | None = None, session_id: str = '', output_type: type[BaseModel] | None = None) -> AgentResult[Any]
Run one non-streaming turn.
history is how a stateless agent takes a second turn: the adopter
holds the transcript and passes result.messages back.
Source code in src/symfonic/agent/facade.py
stream ¶
stream(prompt: str, *, attachments: Sequence[Attachment] = (), history: Sequence[Message] = (), state: Mapping[str, Any] | None = None, session_id: str = '', output_type: type[BaseModel] | None = None) -> AsyncIterator[AgentEvent]
Run one streaming turn.
A plain def, not an async generator function, on purpose: an async
generator defers argument validation to the first __anext__, which
would surface a ConfigurationError after the caller believed the
stream had started. This validates eagerly and raises at the call site,
while async for event in agent.stream(...) still reads identically
(EVT-8).
Source code in src/symfonic/agent/facade.py
AgentEvent ¶
Bases: BaseModel
One event of an Agent.stream() (EVT-1…EVT-10).
A projection of the same invocation the non-streaming path runs — not a
second pipeline. Field population per kind:
============== ===================================================
kind populated fields
============== ===================================================
thinking text (reasoning delta)
text_delta text (answer delta)
tool_call tool_call with result/error both None
tool_result tool_call with result or error set
done result
error error
cancelled (none)
ask_user interrupt
interrupt interrupt
stage stage, and stage.counts where it counted
============== ===================================================
stage is what a capability's execution reported: which stage ran, in
which phase, for which capability, with what outcome and why. It shares
the dense index with everything else, so a consumer can place a
retrieval against the round it fed. It carries counts and never contents
-- see :class:StageRecord.
AgentResult ¶
Bases: BaseModel, Generic[OutputT]
The result of one Agent.run() (RES-1…RES-8).
text is never None — a run that produced no text yields "".
AgentResponse.final_response's None-vs-"" tri-state has no
meaning a caller can act on and forces a null check on the happiest path.
messages is the whole turn in order and is designed to be fed straight
back as the next call's history; that round trip is how a stateless
agent takes a second turn.
Attachment ¶
Bases: BaseModel
Non-text content to send alongside a query.
The semantic query string is used for HMS hydration and embeddings;
attachments are forwarded to the LLM as additional content blocks but do
NOT participate in memory hydration or tool routing in v1.
Attributes:
| Name | Type | Description |
|---|---|---|
kind |
Literal['image', 'document']
|
Content type -- |
source_type |
Literal['url', 'base64']
|
How |
data |
str
|
URL string or base64-encoded content. |
media_type |
str
|
MIME type (e.g. |
filename |
str | None
|
Optional filename for documents / PDFs. |
CapabilityConfig ¶
Bases: Protocol
Marker protocol for the API-ADR A5 optional-behavior seam (FAC-7).
Optional behavior attaches by passing a typed configuration object, and absence means disabled — there are no boolean feature flags on the facade, ever. W1 registers zero capabilities, so the parameter exists solely so the first real capability is an additive change rather than a signature change.
ConfigurationError ¶
Bases: SymfonicError
Raised when capability flags are violated at compile time.
Promoted from symfonic.core.graph (FERR-2). The facade raises it for
bad constructor input, an unregistrable capability, and a provider that
cannot satisfy a requested structured output — always before the first
provider call (FERR-3).
ContractViolationError ¶
Bases: SymfonicError
Raised when a documented facade contract is misused.
W1's only use is LIF-4: run()/stream() after close(). Closed
is terminal and there is no reopen, so continuing would be a silent
lie about the agent's state.
A PRE_MODEL, POST_MODEL, PRE_TOOL, POST_TOOL or
FINALIZE stage handler may preserve an adopter-defined refusal type
across the :class:symfonic.Agent boundary by subclassing this error and
setting preserve_contract_identity = True on the subclass. Without
that explicit opt-in, a handler exception is wrapped in this base type and
retained as its __cause__. PROMPT_ASSEMBLY always wraps, including
opted-in subclasses, through its separate prompt-assembly failure guard.
Message ¶
Bases: BaseModel
One typed conversation message (RES-2).
content is text. Attachments are deliberately not replayed through
history — the catalogued rule is that they do not participate in
hydration or routing, so a round trip through history is lossless for
text and documented-lossy for attachments.
StructuredOutputError ¶
Bases: SymfonicAgentError
Raised when a structured-output extraction cannot be satisfied.
Carries code="unprocessable" so the FastAPI layer maps it to a 422
rather than a generic 500 — the request was well-formed, but the model
could not produce a value matching the requested schema.
Source code in src/symfonic/agent/structured.py
SymfonicError ¶
Bases: Exception
Root of the simple-facade error taxonomy (FERR-1).
Never raised directly. It exists so an adopter can write one
except SymfonicError and catch every failure the facade is
documented to raise — and nothing else. Provider SDK exceptions
deliberately do not inherit from it: W1 propagates them unchanged
rather than shipping a half-built wrapper that swallows the provider's
own diagnostics (FERR-4).
TokenUsage ¶
Bases: BaseModel
What the provider reported (RES-4).
0 means "unreported", not "free": the facade never estimates token
counts, so a provider that reports nothing leaves the zero value.
ToolCall ¶
Bases: BaseModel
One tool invocation and its outcome (RES-3).
On a completed run exactly one of result / error is non-None.
Both are None only on the in-flight tool_call stream event, which
is what makes id the join key to the matching tool_result.