symfonic.core.callbacks.manager¶
manager ¶
CallbackManager -- fan-out dispatch to multiple CallbackHandler instances.
Composite pattern: CallbackManager itself satisfies CallbackHandler protocol. Errors in one handler do not propagate to other handlers. Zero-cost when no handlers registered (empty list = immediate return).
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.