symfonic.core.contracts¶
contracts ¶
Phase 3 contracts — shared types with zero internal dependencies.
ActivationEvent ¶
Bases: BaseModel
A single memory node activated during spreading activation.
AgentEndEvent
dataclass
¶
AgentEndEvent(
run_id: str,
final_response: str | None,
duration_ms: float,
node_count: int | None,
)
Emitted when agent execution completes.
In the streaming path, final_response and node_count are None because stream() does not accumulate final state.
AgentStartEvent
dataclass
¶
Emitted when agent execution begins.
AskUserAnsweredEvent ¶
Bases: BaseModel
Fires when a resume call succeeds and the graph is unpaused.
AskUserExpiredEvent ¶
Bases: BaseModel
Fires when the janitor sweeps an expired checkpoint.
AskUserQuestionEvent ¶
Bases: BaseModel
Emitted when the LLM calls the built-in ask_user tool.
The graph is paused at this point and will only advance when a
matching POST to /api/v1/resume/{pause_token} arrives.
BuildResult
dataclass
¶
BuildResult(
content: str,
status: Literal["success", "partial", "failed"],
warnings: tuple[BuildWarning, ...] = (),
degraded_sections: tuple[str, ...] = (),
)
Result of a prompt build operation.
BuildWarning
dataclass
¶
Warning emitted during prompt build.
CapabilityError ¶
Bases: Exception
Error from an external capability (fetch, search, etc.).
Not a frozen dataclass — exceptions must support traceback assignment. Fields are set as regular attributes for compatibility.
Source code in src/symfonic/core/contracts/errors.py
ConsolidationScheduledEvent ¶
Bases: BaseModel
Emitted after consolidation is scheduled (background task, not yet complete).
ExtensionEvent ¶
Bases: BaseModel
Vendor-specific or custom event. Never dropped, never raises.
FabricationDetectedEvent
dataclass
¶
FabricationDetectedEvent(
run_id: str,
mode: Literal["off", "observe", "revise", "refuse"],
action_taken: Literal[
"none",
"observed",
"revise_forced",
"refuse_replaced",
],
findings: list[dict[str, Any]],
refuse_threshold: float,
max_finding_confidence: float,
original_draft_preview: str,
replacement_preview: str | None,
preview_truncated: bool,
)
Emitted when the fabrication check finds suspect content (v7.0.7).
Fires in observe / revise / refuse modes whenever
:func:~symfonic.agent.middleware.fabrication.detect_fabrication
returns at least one finding. The action_taken field
discriminates whether the detector merely observed, requested a
revision, or replaced the draft with the safe envelope -- so a
single typed event covers the full mode matrix.
The companion ExtensionEvent(type='fabrication_detected'|
'fabrication_blocked') payload that v7.0.6 emitted is preserved
one release for back-compat: handlers that expose
on_fabrication_detected still receive it in v7.0.7 alongside
this typed event. The untyped path is deprecated and will be
removed in v7.1.
Privacy. original_draft_preview and replacement_preview
contain raw LLM output text (truncated at 240 chars; the
preview_truncated flag is True when the full draft was
longer). PII redaction is the handler's responsibility -- the
same stance documented for :class:LLMPreCallEvent (v6.1.7).
Use this event to:
- Count detections per tenant / per mode for SRE dashboards.
- Tune
fabrication_refuse_min_confidencefrom telemetry by bucketingmax_finding_confidenceagainstaction_taken. - Feed fabrication rates per
modeandaction_takeninto production observability stacks.
Attributes:
| Name | Type | Description |
|---|---|---|
run_id |
str
|
The agent run identifier this turn belongs to. |
mode |
Literal['off', 'observe', 'revise', 'refuse']
|
The effective fabrication-check mode that produced the finding ("off" only ever appears here when a future caller forces emission; the standard wiring never fires the event in "off"). |
action_taken |
Literal['none', 'observed', 'revise_forced', 'refuse_replaced']
|
What the wiring did with the findings.
|
findings |
list[dict[str, Any]]
|
List of |
refuse_threshold |
float
|
The |
max_finding_confidence |
float
|
|
original_draft_preview |
str
|
First 240 characters of the draft response that produced the findings. Empty string when the draft was empty (the wiring short-circuits before that, so this is informational only). |
replacement_preview |
str | None
|
First 240 characters of the replacement
text used in |
preview_truncated |
bool
|
|
GraphEdgeCreatedEvent ¶
Bases: BaseModel
Emitted when a new edge is written during consolidation.
GraphNodeCreatedEvent ¶
Bases: BaseModel
Emitted when a new node is written to the graph during consolidation.
InterruptEvent ¶
Bases: BaseModel
Generic pause event emitted when a registered interrupt fires.
Mirrors :class:AskUserQuestionEvent for non-ask_user interrupts.
Fields
name: Registered interrupt name (must match a prior
:meth:SymfonicAgent.register_interrupt call).
pause_token: Opaque JWT carrying the same security envelope as
ask_user (HMAC-signed, scope-bound, request-hash-bound,
expiry-bounded, single-use via the DB-backed
interrupt_consumed_tokens table).
payload: JSON-serialized payload matching the registered
payload_schema.
expires_at: Token expiry (UTC). Resume after this raises.
interrupt_id: Client-side correlation id, format i-<12hex>.
InterruptExpiredEvent ¶
Bases: BaseModel
Emitted when the janitor sweeps an expired generic-interrupt token.
Analogous to AskUserExpiredEvent.
InterruptRegistration
dataclass
¶
InterruptRegistration(
name: str,
payload_schema: type[BaseModel],
response_schema: type[BaseModel],
validate_response: Callable[
[BaseModel, BaseModel], None
]
| None = None,
cross_scope_allowed: bool = False,
built_in: bool = False,
metadata: dict[str, Any] = dict(),
)
Boot-time registration record for a named interrupt point.
Held in a per-agent registry (SymfonicAgent._registered_interrupts)
and consulted by the engine's mint path AND the resume router so
the same metadata controls both ends of the pause/resume cycle.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Stable identifier (e.g. |
payload_schema |
type[BaseModel]
|
Pydantic |
response_schema |
type[BaseModel]
|
Pydantic |
validate_response |
Callable[[BaseModel, BaseModel], None] | None
|
Optional cross-validator that receives
|
cross_scope_allowed |
bool
|
When True, the resume endpoint accepts a token issued under one scope to be redeemed under another. Logs an audit entry on each cross-scope redemption. When False (default) same-scope binding is enforced. |
built_in |
bool
|
True for |
InterruptResolvedEvent ¶
Bases: BaseModel
Emitted when a generic interrupt is resumed successfully.
Companion to InterruptEvent; analogous to
AskUserAnsweredEvent.
LLMEndEvent
dataclass
¶
LLMEndEvent(
model: str,
output: str,
usage: dict,
run_id: str = "",
node_name: str = "",
pricing_unknown: bool = False,
duration_ms: float = 0.0,
started_at_utc: str = "",
iteration_index: int = 0,
turn_index: int = 0,
)
Fired when the LLM returns a response.
node_name (v7.4.3) discriminates between LLM call sites so consumers
can attribute usage / cost / spans back to the originating subsystem
(e.g. "react", "metacognition_critic", "context_window_summariser",
"insight_extractor"). The default empty string preserves byte-identical
payloads for pre-v7.4.3 handlers that do not inspect the field.
pricing_unknown (v7.4.5) is a programmatic discriminator that fires
True when the model name had no entry in the pricing registry at
typed-usage materialisation time -- letting downstream consumers tell
"known model, $0 turn" apart from "unknown model, cost defaulted to
$0". Default False preserves byte-identical payloads for pre-7.4.5
handlers; emitters that build the event from a raw usage dict can
leave the flag alone and rely on :attr:typed_usage to surface the
discriminator on demand.
duration_ms / started_at_utc (v7.15.0) carry wall-clock latency
and the UTC ISO-8601 timestamp of the LLM invocation. Both default to
zero / empty so pre-v7.15.0 emission paths that did not capture timing
preserve byte-identical payloads. Production emitters use the
:func:symfonic.core.callbacks.emit.llm_timing context manager which
populates them exception-safely.
iteration_index (v7.15.0) is the 0-indexed React loop iteration
that produced this LLM call. 0 is the natural sentinel for
non-React sites (each fires once per consolidation cycle) and for the
first React iteration. React emission sites compute it as
sum(1 for m in state["messages"] if isinstance(m, AIMessage))
BEFORE the current ainvoke.
typed_usage
property
¶
Return a typed TokenUsage built from the raw usage dict.
The returned :class:TokenUsage carries its own pricing_unknown
flag derived from the live pricing registry at access time.
LLMPreCallEvent
dataclass
¶
LLMPreCallEvent(
model: str,
messages: list,
tools: list,
run_id: str,
invocation_params: dict = dict(),
node_name: str = "",
)
Fired right before the LLM provider call, after system-message consolidation and tool binding. Represents the exact payload the provider is about to receive.
Use this to log the wire-accurate prompt (e.g. for gateway trace
events, compliance capture, replay fixtures). For the engine-view
(pre-consolidation) prompt, use :class:LLMStartEvent instead.
messages here is the post-consolidation list: a single leading
:class:~langchain_core.messages.SystemMessage (when any system
content is present) followed by the non-system conversation
messages in order.
tools is a list of {"name": str, "description": str,
"schema": dict} records describing the resolved tool definitions
the provider will see. Empty when no tools are bound.
invocation_params carries provider-native invocation kwargs
(temperature, max_tokens, tool_choice, etc.) when the underlying
LangChain chat-model adapter exposes them via
_get_invocation_params. Empty {} when the adapter does not
surface them.
.. note:: Payload fields may contain sensitive content (PII, credentials embedded in prompts, customer data). PII redaction is the callback author's responsibility -- this event ships the wire-accurate payload verbatim by design.
LLMStartEvent
dataclass
¶
LLMStartEvent(
model: str,
messages: list,
run_id: str = "",
system_prompt: str = "",
node_name: str = "",
turn_index: int = 0,
)
Fired when the LLM is invoked with the full message list.
This is the engine-view event: messages is the conversation
history exactly as the react node received it, BEFORE system-message
consolidation and BEFORE tool binding. Use this to debug the
engine-side prompt assembly (memory hydration, context window
composition, etc.).
For the wire-accurate payload the provider is about to receive,
use :class:LLMPreCallEvent instead.
MemoryExtractedEvent ¶
Bases: BaseModel
Emitted when an extraction block is parsed from the stream.
MessageEndEvent ¶
Bases: BaseModel
End of message.
MessageStartEvent ¶
Bases: BaseModel
Start of a new message.
NodeEndEvent
dataclass
¶
Emitted after a graph node completes successfully.
NodeErrorEvent
dataclass
¶
Emitted when a graph node raises an exception.
NodeStartEvent
dataclass
¶
Emitted before a graph node executes.
ResponseCompleteEvent ¶
Bases: BaseModel
Final clean response text — equivalent of AgentResponse.final_response.
SearchResultEvent ¶
Bases: BaseModel
Search result from external capability.
TextDeltaEvent ¶
Bases: BaseModel
Incremental text chunk from LLM.
ThinkingDeltaEvent ¶
Bases: BaseModel
Incremental thinking/reasoning chunk.
ToolCallDeltaEvent ¶
Bases: BaseModel
Incremental tool call arguments.
ToolCallPolicy
dataclass
¶
ToolCallPolicy(
name: str,
match: Callable[[DispatchContext], bool],
when_tool: str | frozenset[str] | None = None,
redirect_to: str | None = None,
model: ModelConfig | None = None,
args_transform: Callable[
[dict[str, Any], DispatchContext], dict[str, Any]
]
| None = None,
guard: Callable[[DispatchContext], bool] | None = None,
)
Declarative {intent -> model + tool-redirect} rule (v8.1.0).
Fields:
name-- required, non-empty. For logging / inspection / test assertions.match-- the intent gate,Callable[[DispatchContext], bool]. Runs at BOTH seams against an equivalentDispatchContext(the adopter classifies intent offmessages[-1].content/scope/resolved_tool_names). Core ships NO intent classifier -- intent is adopter business logic.when_tool-- POST-model emitted-tool filter.str|frozenset[str]|None.Nonematches any emitted tool. NO meaning at the PRE-model seam (no tool exists yet there). A SEPARATE field frommatchsomatchstays a pure intent predicate reusable at both seams.redirect_to-- POST-model target tool name. Setting it makes the policy a redirect policy. Subject to the palette gate (an unrouted target is dropped with a WARNING; the original dispatches).model-- PRE-model per-intent model override (ModelConfig). Applied ONLY at the PRE-model seam.args_transform-- optional POST-model arg rewriter,Callable[[args, ctx], args].Nonepasses args through.guard-- optional deterministic precondition,Callable[[DispatchContext], bool]. Distinct frommatch:match= "is this the right intent?";guard= "is the precondition satisfiable?" (e.g. the stored table exists for THIS scope).guardgates the REDIRECT only -- it does NOT gate model-selection (a missing data source must not change which model reasons). The policy redirects only whenmatch(ctx)AND (guard is None or guard(ctx)).
matches_tool ¶
True when tool_name satisfies when_tool (POST-model).
Source code in src/symfonic/core/contracts/tool_policy.py
ToolCallStartEvent ¶
Bases: BaseModel
Tool call initiated by LLM.
ToolProgressEvent ¶
Bases: BaseModel
Intermediate progress emitted by an async-generator tool.
Yielded between ToolCallStartEvent and the final
ToolResultEvent. Carries an opaque payload; the tool author
chooses the shape. sequence is monotonically increasing (0-indexed)
per tool_call_id so a consumer can detect drops.
The engine emits this when a registered tool is an
inspect.isasyncgenfunction: each non-final yield becomes one
ToolProgressEvent; the last yield becomes the existing
ToolResultEvent. Regular async def tools see zero
behaviour change.
ToolResultEvent ¶
Bases: BaseModel
Result returned from tool execution.
UsageEvent ¶
Bases: BaseModel
Token usage report.