symfonic.core¶
core ¶
symfonic.core -- Agent Graph Framework core primitives (LOW-LEVEL).
Public API re-exports only. Internal modules should not be imported directly by consumer code.
Which layer do I want?
* symfonic.core (this package) is the low-level engine: AgentGraph /
AgentRuntime, tools, providers, and config primitives. Use it only
when you are building your own orchestration.
* symfonic.agent is the HIGH-LEVEL, batteries-included layer most users
want: SymfonicAgent (memory hydration + consolidation, sub-agents,
plugins, streaming, the FastAPI bridge). Start there:
from symfonic.agent import SymfonicAgent.
* Run symfonic guide (CLI) for the full decision tree and quickstarts.
AgentConfig
dataclass
¶
AgentConfig(
model: ModelConfig = ModelConfig(),
compaction: CompactionConfig = CompactionConfig(),
max_conversation_messages: int = 200,
recursion_limit: int = 50,
max_agent_depth: int = 2,
ask_user_enabled: bool = False,
role_models: dict[str, ModelConfig] = dict(),
experimental_interrupt: bool = False,
procedural_enforce_preconditions: bool = False,
)
Top-level agent configuration combining model and compaction settings.
with_anthropic
classmethod
¶
with_anthropic(
model_name: str = "claude-sonnet-4-5", **kwargs: object
) -> tuple[AgentConfig, AnthropicProvider]
Convenience constructor. Returns (config, provider) tuple.
Source code in src/symfonic/core/config.py
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.
AgentGraph ¶
Graph definition factory. Accepts topology and compiles into CompiledGraph.
Not responsible for execution or configuration — that's AgentRuntime. Config is passed at compile time, not at construction time.
Source code in src/symfonic/core/graph.py
add_context_loader ¶
add_context_loader(
fn: Callable[..., Any],
required: bool = False,
timeout_seconds: float = 5.0,
allow_collision_keys: set[str] | None = None,
) -> AgentGraph
Register a context loader. Returns self for chaining.
Source code in src/symfonic/core/graph.py
add_tool ¶
Register a single tool with metadata. Returns self for chaining.
add_tools ¶
compile ¶
compile(
deps: BaseAgentDeps,
config: AgentConfig | None = None,
checkpointer: Any | None = None,
) -> Any
Validate capabilities, freeze registry, wire topology.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
deps
|
BaseAgentDeps
|
Capability registry with registered providers. |
required |
config
|
AgentConfig | None
|
Agent configuration. Defaults to AgentConfig() if not provided. |
None
|
checkpointer
|
Any | None
|
Optional LangGraph checkpointer for persistence/resume. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
unknown preset name |
ConfigurationError
|
capability flags violated |
MissingCapabilityError
|
required capability absent from deps |
Source code in src/symfonic/core/graph.py
register_preset
classmethod
¶
set_active_skills_getter ¶
Register the async callable the v7.7.4
:class:PreconditionGateNode uses to query active procedural
skills.
Signature: async def getter(state: dict) -> list[skill_entry].
Typical implementation queries
ProceduralLayer.query_skills(scope, "", include_drafts=False).
The engine wires this automatically; callers building a graph
by hand (no SymfonicAgent) register their own getter when
opting into procedural_enforce_preconditions.
Returns self for chaining.
Source code in src/symfonic/core/graph.py
unregister_preset
classmethod
¶
Remove a custom preset. Raises KeyError if not found.
Source code in src/symfonic/core/graph.py
with_preset
classmethod
¶
AgentRuntime ¶
AgentRuntime(
graph: AgentGraph,
deps: BaseAgentDeps,
config: AgentConfig | None = None,
timeout_seconds: float | None = None,
checkpointer: Any | None = None,
)
Execute a compiled graph for a single agent invocation.
Eagerly compiles the graph in init -- raises immediately on misconfiguration (missing capabilities, invalid preset, etc.).
Source code in src/symfonic/core/runtime.py
run
async
¶
run(
query: str,
*,
callbacks: Sequence[CallbackHandler] | None = None,
**state_overrides: Any,
) -> dict[str, Any]
Execute the graph and return final state dict with final_response.
v7.18.0 (Option B emission-ordering): the caller may pass
_suppress_agent_end=True in state_overrides to disable
the success-path on_agent_end dispatch in this method. The
engine layer (SymfonicAgent.run) sets this flag so it can
emit on_response_render AFTER the full transformation chain
(synthesize-on-empty-final salvage -> fabrication -> metacog ->
extraction-stripping) and THEN emit on_agent_end with the
rewritten content. Composability rule (locked):
- salvage -> fabrication -> metacog -> extraction-stripping -> on_response_render -> on_agent_end.
- The rewriter runs at most ONCE per turn.
The error-path on_agent_end STILL fires from runtime --
engine cannot intercept the BaseException, and adopters relying
on a forced-failure terminator event must still receive it.
_suppress_agent_end is a runtime-internal flag and is NOT
part of the public state_overrides surface; adopter code
that passes arbitrary keys to runtime.run() ignores it
per the legacy default-False contract.
Source code in src/symfonic/core/runtime.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
stream
async
¶
stream(
query: str,
*,
callbacks: Sequence[CallbackHandler] | None = None,
**state_overrides: Any,
) -> AsyncGenerator[Any, None]
Yields stream events. Emits AgentStartEvent/AgentEndEvent via callbacks.
Source code in src/symfonic/core/runtime.py
stream_events
async
¶
stream_events(
query: str,
*,
callbacks: Sequence[CallbackHandler] | None = None,
**state_overrides: Any,
) -> AsyncGenerator[StreamEvent, None]
Yield typed StreamEvent objects from astream_events(v2).
Calls _workflow.astream_events and pipes each raw LangGraph
event through EventTranspiler, yielding typed StreamEvent
objects (TextDeltaEvent, ToolCallStartEvent, ThinkingDeltaEvent,
etc.). AgentStartEvent / AgentEndEvent are dispatched via the
callback manager as in stream().
Source code in src/symfonic/core/runtime.py
AgentStartEvent
dataclass
¶
Emitted when agent execution begins.
BaseAgentDeps ¶
Capability Registry for agent dependencies.
Register capabilities by type and retrieve them via get/require/has. ObservabilityHook is always registered with a NoOp default.
Supports constructor kwargs for convenience::
deps = BaseAgentDeps(ModelProvider=my_provider, DocumentStore=my_store)
Kwarg keys are resolved by matching against known capability types (Protocol classes) defined in this package.
Source code in src/symfonic/core/deps.py
get ¶
has ¶
list_capabilities ¶
register ¶
Register a capability by its type. Returns self for chaining.
registered_types ¶
require ¶
Required: returns impl or raises MissingCapabilityError.
validate_required ¶
Raises MissingCapabilityError listing all absent types.
Source code in src/symfonic/core/deps.py
BaseAgentState ¶
Bases: TypedDict
Typed state contract for all nodes in the graph.
Used as a TypedDict schema for LangGraph's StateGraph.
total=False makes all fields optional at construction time.
CallbackHandler ¶
Bases: Protocol
Async callback protocol for agent graph lifecycle events.
Implement this protocol to receive notifications about agent start/end, node start/end/error, LLM invocation events, and token composition breakdowns during graph execution.
CallbackManager ¶
Fan-out dispatcher for multiple CallbackHandler instances.
Satisfies the CallbackHandler protocol (composite pattern). When the handler list is empty, all methods return immediately (zero-cost).
Source code in src/symfonic/core/callbacks/manager.py
add ¶
describe_hook_handlers ¶
Introspection helper for adopter triage (v7.18.1).
Returns [(handler_qualname, implements_hook), ...] for every
registered handler. Adopters debugging "which of my handlers is
responsible for has_hook returning True?" call this to
verify the registration without diffing internal state.
Example::
>>> mgr.describe_hook_handlers("on_response_render")
[('myadopter.SlackRenderCallback', True),
('symfonic.telemetry.MetricsHandler', False)]
Zero-cost when called outside a hot path -- intended for diagnostic logs and ad-hoc REPL introspection, not steady-state dispatch. The handler descriptor includes module + qualname so a forked CallbackManager handler shows up immediately as a non-symfonic module path.
Source code in src/symfonic/core/callbacks/manager.py
has_hook ¶
True when at least one handler implements the given hook.
Used at emission sites to skip event construction entirely for
OPTIONAL hooks (e.g. on_llm_pre_call, on_user_correction)
when no registered handler implements them. Preserves the
zero-cost guarantee for optional extensions.
Source code in src/symfonic/core/callbacks/manager.py
merge ¶
Return a new CallbackManager combining both handler lists.
on_agent_end
async
¶
on_agent_start
async
¶
on_before_tool_call
async
¶
Consult guard handlers before a tool executes; return a verdict.
The steering / guard seam (companion to the on_tool_call_dispatch
rewriter). Handlers are consulted in registration order and the FIRST
handler that returns a skip verdict wins — a guard veto
short-circuits the chain. Handlers that return None /
{"action": "proceed"} defer to the next handler.
Return value delivered to :class:InstrumentedToolNode:
None-> no guard skipped the call; execute normally.{"action": "skip", "content": str, "is_error": bool}-> the tool node synthesizes aToolMessagefromcontentinstead of executing.is_errordefaults toTrue.
Malformed verdicts (missing/empty content on a skip) are
dropped with a WARNING and treated as proceed so a buggy guard
cannot silently swallow a call. A handler that raises is logged and
skipped (the chain proceeds) — error isolation matches the other
optional hooks.
Source code in src/symfonic/core/callbacks/manager.py
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 | |
on_fabrication_detected
async
¶
Dispatch FabricationDetectedEvent to handlers that implement it.
on_fabrication_detected is an OPTIONAL hook (the dispatch
was implicit in v7.0.6 via raw getattr; v7.0.7 promotes it
to a typed event with proper manager-level fan-out). Handlers
that do not implement the method are silently skipped.
Preserves the zero-cost invariant: callers gate construction
of :class:~symfonic.core.contracts.callbacks.FabricationDetectedEvent
on :meth:has_hook ("on_fabrication_detected") so no
event objects are built when no handler listens.
Source code in src/symfonic/core/callbacks/manager.py
on_llm_end
async
¶
on_llm_pre_call
async
¶
Dispatch LLMPreCallEvent to handlers that implement it.
on_llm_pre_call is an OPTIONAL hook (added in v6.1.5); not
every handler implements it, so we fan out only to handlers that
define the attribute. Preserves the zero-cost invariant: if no
handler implements the hook the loop is a no-op.
Source code in src/symfonic/core/callbacks/manager.py
on_llm_start
async
¶
on_node_end
async
¶
on_node_error
async
¶
on_node_start
async
¶
on_procedure_selection
async
¶
Dispatch ProcedureSelectionEvent to handlers that implement it (v7.20.0 T-7.20.0.9).
on_procedure_selection is an OPTIONAL hook; fans out only
to handlers that define the attribute. Preserves the zero-
cost invariant: the engine's force-resolver gates event
construction on has_hook("on_procedure_selection") so no
event objects are built when no handler listens.
Adopters subscribe to capture per-procedure billing
attribution, A/B-test bucket assignment, audit trail entries,
and production routing telemetry. See
:class:~symfonic.core.contracts.callbacks.ProcedureSelectionEvent.
Source code in src/symfonic/core/callbacks/manager.py
on_react_loop_end
async
¶
Dispatch ReactLoopEndEvent to handlers that implement it (v7.17.0).
on_react_loop_end is an OPTIONAL hook fired by the React node
when the loop terminates with an empty final_response AFTER
the v7.17.0 content-block salvage attempt. Handlers that do not
implement it are silently skipped. Preserves the zero-cost
invariant: the react node gates event construction on
:meth:has_hook ("on_react_loop_end") so no event objects
are built when no handler listens.
Source code in src/symfonic/core/callbacks/manager.py
on_response_render
async
¶
Dispatch ResponseRenderEvent to handlers that implement it.
Unique among optional hooks: on_response_render RETURNS
the rewritten string. The runtime substitutes the returned
value for the original final_response BEFORE on_agent_end
dispatch.
Chaining semantics for multiple handlers (registration order):
- First handler receives
content=original_content. - Each subsequent handler receives the previous handler's return value.
- The final return value is delivered to the runtime.
Error isolation: if a handler raises, the exception is logged
and that handler's contribution is SKIPPED (the chain proceeds
with the previous content). This matches the error-isolation
contract of :meth:_dispatch_optional -- a broken rewriter
must not stall the runtime.
Handlers that do not implement the method are silently skipped.
Preserves the zero-cost invariant: emission sites gate
construction of :class:~symfonic.core.contracts.callbacks.
ResponseRenderEvent on :meth:has_hook
("on_response_render") so no event objects are built when
no handler listens.
v7.18.0 (Option B; Jarvio Slack-formatting adopter ask).
Source code in src/symfonic/core/callbacks/manager.py
on_token_composition
async
¶
on_tool_call_dispatch
async
¶
on_tool_call_dispatch(
event: ToolCallDispatchEvent, state: dict[str, Any]
) -> dict[str, Any] | None
Dispatch ToolCallDispatchEvent to handlers that implement it.
Unique among optional hooks (alongside on_response_render):
on_tool_call_dispatch RETURNS the rewritten {"tool_name",
"args"} dict. The React node mutates ai_message.tool_calls
in place before the AIMessage returns to LangGraph for dispatch.
Return-value semantics:
None-> pass-through (no rewrite contribution).dict[str, Any]with keys{"tool_name": str, "args": dict}-> rewrite. The dict MUST NOT containcall_id-- the engine owns it.tool_nameMUST be a string;argsMUST be a dict. Malformed returns are dropped with a structured WARNING (see the type-validation block below).
Chaining semantics for multiple handlers (registration order):
- First handler receives the original event (original
tool_name/args). - Each handler that returns a dict re-emits a SHALLOW-COPIED
event with the rewritten
tool_name/argsso the next handler sees the chain's current state. - Handlers that return
Noneare pass-throughs (no contribution; the next handler sees the previous state). - The final return value is delivered to the React node.
Error isolation: a handler that raises is logged and SKIPPED
(the chain proceeds with the previous state). Matches the
error-isolation contract of _dispatch_optional -- a broken
rewriter must not stall the React loop.
Validation gate: the React node verifies the final
tool_name against event.available_tools and DROPS
rewrites that target unregistered tools. This method does
NOT enforce the gate -- it lives at the call site so the
dropped-rewrite WARNING carries the React node context.
Handlers that do not implement the method are silently
skipped. Preserves the zero-cost invariant: emission sites
gate construction of :class:~symfonic.core.contracts.callbacks.
ToolCallDispatchEvent on :meth:has_hook
("on_tool_call_dispatch") so no event objects are built
when no handler listens.
v7.19.0 (Jarvio SL-02 CSV-export -> slack_upload_file + SL-04 per-user memory title shape).
Source code in src/symfonic/core/callbacks/manager.py
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | |
on_tool_result
async
¶
Dispatch ToolResultCallbackEvent to handlers that implement it (v8.6.4).
on_tool_result is an OPTIONAL hook fired by the tool node
(InstrumentedToolNode) AFTER a tool returns, carrying the
produced result, call_id, and a deferred flag. Handlers that
do not implement it are silently skipped via
:meth:_dispatch_optional. Preserves the zero-cost invariant:
the emit site gates event construction on
:meth:has_hook ("on_tool_result") so no event objects are
built when no handler listens.
Source code in src/symfonic/core/callbacks/manager.py
on_tool_routing_decision
async
¶
Dispatch ToolRoutingDecisionEvent to handlers that implement it.
on_tool_routing_decision is an OPTIONAL hook (added in
v7.0.1); fans out only to handlers that define the attribute.
Preserves the zero-cost invariant: when tool_routing_mode
is off the engine never constructs the event OR calls
this method.
Source code in src/symfonic/core/callbacks/manager.py
CompactionConfig
dataclass
¶
CompactionConfig(
context_window_tokens: int | None = None,
max_history_share: float = 0.5,
chars_per_token: int = 4,
keep_recent_messages: int = 10,
max_chunk_tokens: int = 30000,
compaction_model: str = "claude-haiku-4-5",
soft_threshold_tokens: int = 4000,
enable_memory_flush: bool = True,
)
Configuration for conversation compaction/summarization.
context_window_tokens is now optional (v7.1.2). When None
(the new default), ContextWindowNode falls back to
ModelConfig.context_window if set, then to the 200_000 safety
net (Claude-sized) -- this gives non-Claude callers a way to pin
the right budget without overriding CompactionConfig. Set
explicitly (e.g. CompactionConfig(context_window_tokens=50_000))
to force a tighter or wider ceiling than the model declares.
ContextCollisionError ¶
Bases: ValueError
Raised when two context loaders write the same state key.
GraphPreset ¶
Bases: Protocol
Protocol for graph topology presets.
LLMLifecycleHook ¶
Bases: Protocol
Async instrumentation for LLM call start/end lifecycle.
MissingCapabilityError ¶
Bases: Exception
Raised when a required capability is not registered in BaseAgentDeps.
Accepts either a single type or a list of types (for bulk validation). Always produces a self-explanatory message with the registration hint.
Source code in src/symfonic/core/deps.py
ModelConfig
dataclass
¶
ModelConfig(
model_name: str = "claude-sonnet-4-5",
temperature: float = 1.0,
max_tokens: int = 16384,
max_retries: int = 5,
top_p: float | None = None,
top_k: int | None = None,
thinking: dict[str, object] | None = None,
extra_headers: dict[str, str] = dict(),
context_window: int | None = None,
timeout_seconds: float | None = None,
http_client: Any | None = None,
)
LLM model configuration. Provider-agnostic.
context_window (added in v7.1.2) declares the model's native
context-window budget in tokens. When set, ContextWindowNode
uses it as the hard ceiling unless CompactionConfig.context_window_tokens
explicitly overrides it. None (default) preserves pre-v7.1.2
behaviour: the node falls through to the 200_000 safety net.
Typical values: Claude 3/4 = 200_000, GPT-4 Turbo = 128_000,
GPT-4 = 8_192, Llama-3 32k = 32_000.
http_client
class-attribute
instance-attribute
¶
Underlying HTTP client (typically httpx.AsyncClient) for true
per-chunk idle bound. Threaded to providers whose LangChain class
accepts the kwarg -- http_async_client on OpenAI / DeepSeek /
Kimi. Providers whose class does NOT accept it (Anthropic, Google,
Ollama at the time of v7.25.0) emit a one-shot
:class:HttpClientUnsupportedWarning on first encounter and ignore
the field; timeout_seconds still threads. Typed Any so
symfonic-core does not import httpx at module load -- adopters who
don't pin a client pay no import cost. None (default) preserves
the provider's own default client lifecycle.
timeout_seconds
class-attribute
instance-attribute
¶
Total-request timeout in seconds. Threaded to the provider's
LangChain-class kwarg whose name varies by provider:
default_request_timeout on Anthropic, timeout on
OpenAI / DeepSeek / Kimi / Google. Coarse-grained; bounds the
entire HTTP round-trip but does NOT enforce per-chunk idle.
None (default) leaves the provider's own default in place
(typically 600 for the OpenAI family; httpx-driven for Anthropic).
top_k
class-attribute
instance-attribute
¶
Top-k sampling: sample only from the k most likely tokens.
Honoured by Anthropic, Google, and Ollama (their LangChain classes
expose top_k). The OpenAI chat API has NO top_k parameter,
so OpenAI / DeepSeek / Kimi ignore it and emit a one-shot warning.
None leaves the provider default.
top_p
class-attribute
instance-attribute
¶
Nucleus sampling: keep the smallest token set whose cumulative
probability >= top_p (0-1). Threaded to every provider's
LangChain class (all accept top_p). None leaves the
provider default. Note: most providers advise tuning EITHER
temperature OR top_p, not both.
ModelProvider ¶
Bases: Protocol
Protocol for LLM model construction.
supports_forced_tool_choice ¶
v7.8.5: provider-level introspection for the v7.8.3
procedural_force_first_action_tool lever.
Returns True when the provider can honour
llm.bind_tools(tools, tool_choice=<name>) under the given
ModelConfig. Returns False when the provider would
reject forced tool_choice (Anthropic's API returns 400 when
thinking is enabled alongside tool_choice={type:tool,...};
see Anthropic's published extended-thinking constraint).
Default implementation returns True -- providers that have
a no-force constraint override. The engine's
_maybe_resolve_forced_tool_choice consults this BEFORE
stamping forced_tool_choice into the LangGraph state, so
adopters who configure thinking see a clean WARN log and
a forced no-op rather than a runtime 400 from the API
boundary.
Implementations MUST be pure functions of the passed
ModelConfig -- no I/O, no provider state mutation -- so
the engine can call this on every turn without cost.
v7.8.3 architectural-line preservation: the framework
constrains the choice set when it can; abstains cleanly when
it cannot. Auto-disabling config.thinking would cross the
line (reasoning is model-owned).
Source code in src/symfonic/core/providers.py
NodeEndEvent
dataclass
¶
Emitted after a graph node completes successfully.
NodeErrorEvent
dataclass
¶
Emitted when a graph node raises an exception.
NodeLifecycleHook ¶
Bases: Protocol
Async instrumentation for graph-node start/end/error lifecycle.
NodeStartEvent
dataclass
¶
Emitted before a graph node executes.
ObservabilityHook ¶
Bases: NodeLifecycleHook, LLMLifecycleHook, ToolLifecycleHook, Protocol
Composed protocol -- union of all lifecycle hooks.
Backward-compatible: any class implementing all 7 methods satisfies this protocol. New consumers should prefer the granular protocols when they only need a subset of methods.
TenantScope ¶
Bases: BaseModel
Immutable hierarchical N-level multi-tenant request scope.
Canonical representation is the root-first path (path[0] broadest,
path[-1] most-specific). The legacy tenant_id / sub_tenant_id
fields are RETAINED for backward compatibility and, when no explicit path
override is supplied, ARE the 1-/2-level path. When an explicit N-level
path is supplied via the factories, tenant_id / sub_tenant_id read
back off the path.
Isolation (design §5.d): a query at path P sees a memory at path Q iff Q is
a prefix of P. Enforced via :meth:ancestor_prefix_paths exact-IN at the
backend, NEVER a string prefix scan.
path
property
¶
Root-first ordered path (canonical hierarchical representation).
Derived from the flat fields when no explicit override is set
- tenant_id only → 1-level
(tenant:tenant_id,) -
- sub_tenant_id → 2-level
(tenant:tenant_id, sub_tenant:sub)
- sub_tenant_id → 2-level
scope_depth
property
¶
Number of levels in the path (1-based; root-only path is depth 1).
scope_path
property
¶
Materialised US-delimited, root-first path string for the backend.
org\x1facme\x1fbrand\x1fmyhalos\x1fconversation\x1fc-123.
This is the stored column/field value (design §5.b).
ancestor_prefix_paths ¶
The exact-IN list for the isolation filter (design §5.b/§5.d).
Returns the materialised scope_path string of EVERY prefix of this
scope's path (root → … → self), inclusive. A query at path P uses
this list as WHERE scope_path = ANY(list) so it matches a stored
memory at path Q iff Q is a prefix of P — exact set membership, never
a LIKE 'prefix%' scan (which would leak acme-corp to acme).
Source code in src/symfonic/core/scope.py
child ¶
Return a new scope with one more (deeper) level appended.
org_scope.child("brand", "myhalos").child("conversation", "c-123")
builds the full hierarchy. Namespace and inherits_from are preserved.
The result preserves the receiver's concrete type (type(self)) so
chaining .child() off a subclass such as
:class:~symfonic.agent.types.FrameworkTenantScope keeps the subclass
(and its converter methods) instead of silently downcasting to the
base TenantScope (P1 — subclass-drop on chaining).
Source code in src/symfonic/core/scope.py
from_path
classmethod
¶
from_path(
path: list[ScopeLevel] | tuple[ScopeLevel, ...],
*,
namespace: str | None = None,
) -> TenantScope
Construct an N-level scope from an explicit root-first path.
Source code in src/symfonic/core/scope.py
from_request
classmethod
¶
from_request(
tenant_id: str,
*,
sub_tenant_id: str | None = None,
namespace: str | None = None,
path: list[ScopeLevel]
| tuple[ScopeLevel, ...]
| None = None,
) -> TenantScope
Construct a scope from request-edge fields (blessed adopter entry).
Supply path for N-level hierarchies, or the flat fields for the
legacy 1-/2-level shape.
Source code in src/symfonic/core/scope.py
from_state_dict
classmethod
¶
Reconstruct a :class:TenantScope from a state-dict mapping.
Accepts UPPER_SNAKE and lower_snake casings (UPPER wins). Reads an
explicit path / PATH key (list of {kind, id} dicts) when
present — the N-level form; otherwise derives from the legacy flat
fields. inherits_from is read recursively (v7.22 compat).
Source code in src/symfonic/core/scope.py
narrow ¶
Create a new scope narrowed to a sub-tenant (legacy 2-level shim).
Retained byte-identically from v7.22: sets both sub_tenant_id and
namespace to the supplied value and preserves inherits_from.
For N-level hierarchies prefer :meth:child.
Source code in src/symfonic/core/scope.py
root
classmethod
¶
Construct a 1-level root scope (kind:id,).
to_state_dict ¶
Convert to state-dict fields for BaseAgentState injection.
Backward-compatible: a legacy 1-/2-level scope (no explicit path
override) emits EXACTLY the pre-v8.0 keys
(tenant_id + sub_tenant_id [+ namespace] [+
inherits_from]) — byte-identical. An explicit N-level path
additionally emits a path key (list of {kind, id} dicts) so
the hierarchy round-trips.
Source code in src/symfonic/core/scope.py
ToolLifecycleHook ¶
Bases: Protocol
Async instrumentation for tool invocation start/end lifecycle.
ToolRegistry ¶
Tool registration, filtering, cost estimation, and freeze.
Frozen automatically by AgentGraph.compile(). After freeze, register() raises RuntimeError.
Source code in src/symfonic/core/tools/registry.py
all_tools ¶
cost_usd_for_invocation ¶
Calculate total cost for a set of tool invocations.
Returns None if any tool has unknown cost.
Source code in src/symfonic/core/tools/registry.py
filter ¶
filter(
allowed_names: list[str] | None = None,
categories: list[ToolCategory] | None = None,
max_safety_level: SafetyLevel = SafetyLevel.DANGEROUS,
agent_visible_only: bool = False,
available_capabilities: set[type] | None = None,
debug: bool = False,
) -> list[BaseTool]
Filter tools by criteria.
When debug=True, each excluded tool emits a logger.debug() message.
Source code in src/symfonic/core/tools/registry.py
freeze ¶
get_registration ¶
register ¶
Register a single tool with optional metadata. Returns self for chaining.
Raises SymfonicAgentError(code='bad_request') if tool was
produced by @symfonic_tool and the experimental flag is off.
Source code in src/symfonic/core/tools/registry.py
register_many ¶
Register multiple tools with default metadata. Returns self for chaining.
set_experimental_tool_decorator ¶
Deprecated no-op. @symfonic_tool is GA and needs no gate.
Retained so existing engine wiring and adopter code that toggled
the former experimental gate keep working. The enabled value
is ignored — decorator-produced tools always register.
Source code in src/symfonic/core/tools/registry.py
tools_within_budget ¶
Return tools whose cost is within budget.
If include_unknown_cost is True, tools with no cost info are included.
Source code in src/symfonic/core/tools/registry.py
symfonic_tool ¶
symfonic_tool(
*,
scope: ScopeLiteral = "public",
routing_mode: RoutingMode = "observe",
visibility: Visibility = "visible",
name: str | None = None,
description: str | None = None,
) -> Callable[[Callable[..., Any]], StructuredTool]
Decorate a Python callable into a symfonic-compatible tool.
The resulting object is a :class:~langchain_core.tools.StructuredTool
accepted by :meth:~symfonic.core.tools.registry.ToolRegistry.register
without any further plumbing.
Parameters¶
scope:
Logical visibility scope. "public" (default), "internal",
or "admin". Symfonic does not yet enforce scope at the
invocation tier; the field is captured today so policy code can
consume it without a future signature change.
routing_mode:
Per-tool override for the framework's dynamic tool-routing
verdict. Defaults to "observe"; "enforce" causes the
router to actually narrow the manifest, "off" opts out of
routing entirely.
visibility:
"visible" (default) keeps the tool in the agent-visible
manifest; "hidden" flips the registry's
visible_to_agents field to False so the tool is bound
to the LangGraph runtime but never offered to the LLM in the
manifest. Useful for system-level tools (telemetry emitters,
feature-flag readers) that should be callable by orchestration
code but never selected by the model.
name:
Override the tool name. Defaults to fn.__name__.
description:
Override the description. Defaults to the function's docstring.
Returns¶
Callable
A decorator that, applied to a callable, returns a
:class:StructuredTool with the symfonic metadata attached.
Notes¶
- The decorated callable's async / sync / async-generator
semantics are preserved on the underlying tool's
func/coroutineslots.inspect.isasyncgenfunctioncontinues to returnTruefor generator tools so Phase 3's :func:~symfonic.core.tools.streaming.wrap_async_gen_toolworks unchanged. - Registration requires no flag (GA). The resulting tool registers
through
ToolRegistry.registerlike any LangChain tool; itsscope/routing_mode/visibilitymetadata is lifted into theToolRegistrationautomatically.
Source code in src/symfonic/core/tools/decorator.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | |