symfonic.core.callbacks.base¶
base ¶
BaseCallbackHandler -- public no-op base for partial callback handlers.
Subclass this and override ONLY the hooks you need. Every CallbackHandler
protocol method -- REQUIRED (the runtime_checkable Protocol body) AND OPTIONAL
(the documented extension hooks) -- has an async def ...: return None
default here, so a partial subclass NEVER raises AttributeError when the
:class:~symfonic.core.callbacks.manager.CallbackManager dispatches an event
the subclass did not override.
Why this exists (architect verdict, 2026-06-19)::
CallbackManager._dispatch (manager.py) resolves REQUIRED hooks via
``getattr(handler, method)`` with NO default. A duck-typed handler that
implements only some required hooks raises AttributeError on every
other one -- the error is caught + logged by ``_dispatch`` (so the
per-turn log fills with stack traces) and the handler's intended side
effect for the overridden hooks still fires, but the partial handler
spams logs every turn. The fix is THIS base class, NOT a tolerant
``_dispatch`` (a tolerant dispatch would mask genuine typos on
lifecycle hooks).
Usage::
from symfonic.core.callbacks import BaseCallbackHandler
class MyHandler(BaseCallbackHandler):
async def on_agent_end(self, event):
await my_sink.record(event.final_response)
# All other hooks inherit the no-op default -- no boilerplate,
# no AttributeError spam.
This is distinct from :class:NoOpCallbackHandler, which covers only the 8
REQUIRED hooks and is used internally as the zero-overhead default.
BaseCallbackHandler is the PUBLIC subclassing surface and covers the full
required + optional hook set.
BaseCallbackHandler ¶
Public no-op base for CallbackHandler implementations.
Subclass and override only the hooks you need. Satisfies the
:class:~symfonic.core.callbacks.protocol.CallbackHandler protocol via
structural typing (every required method is present and async).
on_before_tool_call
async
¶
Tool-call guard / steering hook. Returns None (proceed).
Returning None executes the tool normally. Override and return
{"action": "skip", "content": str, "is_error": bool} to refuse
the call and hand the model a synthetic result instead (corrective
feedback or a benign cancellation).
Source code in src/symfonic/core/callbacks/base.py
on_response_render
async
¶
Content-rewriter hook (v7.18.0). Returns content unchanged.
Unlike the fire-and-forget hooks, on_response_render RETURNS
the (possibly rewritten) content string. The no-op default
returns content verbatim so a subclass that does not override
it is a pass-through in the rewriter chain.
Source code in src/symfonic/core/callbacks/base.py
on_tool_call_dispatch
async
¶
on_tool_call_dispatch(
event: ToolCallDispatchEvent, state: dict[str, Any]
) -> dict[str, Any] | None
Tool-call rewriter hook (v7.19.0). Returns None (no rewrite).
Returning None is the pass-through contract -- the no-op
default never rewrites a dispatched tool call.