System Events¶
DCAF automatically emits intermittent_update stream events for key moments in
agent execution — when the model starts reasoning, when a tool begins executing,
and when a skill is accessed.
This page explains how to configure, customise, and extend those built-in signals.
What Are System Events?¶
While your agent is running, DCAF emits transient status messages to the client UI at certain points in the lifecycle. These are called system events — they are generated by the framework itself, not by your tool code.
By default, three are active:
| When it fires | Default text |
|---|---|
| Model begins reasoning | "Thinking..." |
| A tool begins executing | "Calling tool: {tool_name}" |
| A skill is accessed | "Loading skill: {skill_name}" |
These appear on the client as intermittent_update stream events — the same type
that emit_update() sends from your code. The UI shows
them while the agent is working and clears them when the next piece of content
arrives.
The SystemEvent Descriptor¶
Each system event is represented by a SystemEvent object. Import the pre-built
constants — these are the intended public API:
from dcaf.core import THINKING, THINKING_COMPLETE, TOOL_STARTED, TOOL_COMPLETED, TOOL_FAILED, SKILL_LOADED
| Constant | Fires when | Default text | Available variables | Default |
|---|---|---|---|---|
THINKING |
Model begins reasoning | "Thinking..." |
— | ✓ on |
THINKING_COMPLETE |
Model finishes reasoning | "Done thinking" |
— | off |
TOOL_STARTED |
A tool begins executing | "Calling tool: {tool_name}" |
{tool_name} |
✓ on |
TOOL_COMPLETED |
A tool finishes successfully | "Done: {tool_name}" |
{tool_name} |
off |
TOOL_FAILED |
A tool raises an exception | "Failed: {tool_name}" |
{tool_name}, {error} |
off |
SKILL_LOADED |
A skill is accessed by the agent | "Loading skill: {skill_name}" |
{skill_name} |
✓ on |
THINKING, TOOL_STARTED, and SKILL_LOADED are on by default. All others
are off by default — opt in when you want them.
Configuring System Events¶
Pass system_events to the Agent constructor.
Keep the defaults (no configuration needed)¶
Disable all system events¶
Choose specific events¶
from dcaf.core import THINKING, TOOL_STARTED, TOOL_COMPLETED, SKILL_LOADED
agent = Agent(
tools=[...],
system_events=[THINKING, TOOL_STARTED, SKILL_LOADED, TOOL_COMPLETED],
)
Only the events in the list will fire. The default three (THINKING,
TOOL_STARTED, SKILL_LOADED) are not added automatically when you pass a
list — you are taking full control.
Customising the Text¶
Simple text override¶
Replace the default string with your own. Template variables in {curly_braces}
are still supported:
from dcaf.core import THINKING, TOOL_STARTED, TOOL_COMPLETED
agent = Agent(
tools=[...],
system_events=[
THINKING.with_text("Working..."),
TOOL_STARTED.with_text("Running {tool_name}..."),
TOOL_COMPLETED.with_text("{tool_name} complete"),
],
)
Custom formatter function¶
For dynamic logic — branching, lookups, or anything a template can't express:
from dcaf.core import TOOL_STARTED
agent = Agent(
tools=[...],
system_events=[
TOOL_STARTED.with_formatter(lambda d: f"▶ {d['tool_name']}"),
],
)
The formatter receives a dict containing the available variables for that event
(see the table above) and must return a str.
Internationalisation (i18n)¶
Because .with_formatter() accepts any callable, you can plug in any translation
system. The formatter receives the same data dict regardless of language —
all you supply is the function.
The example below shows a generic pattern. translate() is your own function
(a gettext wrapper, a dict lookup, an API call — whatever fits your project):
from dcaf.core import THINKING, THINKING_COMPLETE, TOOL_STARTED, TOOL_COMPLETED, TOOL_FAILED
# Your translation function — DCAF doesn't care how it works
def translate(key: str, **kwargs) -> str:
return translations[locale][key].format(**kwargs)
agent = Agent(
tools=[...],
system_events=[
THINKING.with_formatter(
lambda _d: translate("agent.thinking")
),
THINKING_COMPLETE.with_formatter(
lambda _d: translate("agent.thinking_done")
),
TOOL_STARTED.with_formatter(
lambda d: translate("agent.tool_started", tool_name=d["tool_name"])
),
TOOL_COMPLETED.with_formatter(
lambda d: translate("agent.tool_done", tool_name=d["tool_name"])
),
TOOL_FAILED.with_formatter(
lambda d: translate("agent.tool_failed",
tool_name=d["tool_name"],
error=d.get("error", ""))
),
],
)
The formatters are plain Python functions — they can branch on locale, call a
remote service, or read from any data structure. DCAF passes the event data dict
and uses whatever string you return.
Opting Into Optional Events¶
The off-by-default events let you bracket each phase of the lifecycle:
from dcaf.core import THINKING, THINKING_COMPLETE, TOOL_STARTED, TOOL_COMPLETED, TOOL_FAILED
agent = Agent(
tools=[...],
system_events=[
THINKING,
THINKING_COMPLETE,
TOOL_STARTED,
TOOL_COMPLETED,
TOOL_FAILED.with_text("Error in {tool_name}: {error}"),
],
)
With this configuration the client receives events for the full lifecycle:
THINKING fires → "Thinking..."
THINKING_COMPLETE fires → "Done thinking"
TOOL_STARTED fires → "Calling tool: list_pods"
TOOL_COMPLETED fires → "Done: list_pods"
TOOL_STARTED fires → "Calling tool: delete_pod"
TOOL_FAILED fires → "Error in delete_pod: permission denied"
Relationship to emit_update()¶
System events and user-defined events are complementary. System events fire automatically from the framework; user-defined events let your tool code push any message at any time:
from dcaf.core import emit_update, tool
@tool(description="Search for pods")
def search_pods(query: str) -> str:
# Your code pushing a status update (user-defined)
emit_update(f"Searching for: {query}")
results = _do_search(query)
emit_update("Search complete", content={"count": len(results)})
return format_results(results)
# The framework automatically adds:
# "Calling tool: search_pods" ← system event (TOOL_STARTED)
# before your tool runs
See Emitting Events for the full emit_update() /
emit() reference.
Quick Reference¶
| Goal | Code |
|---|---|
| Keep defaults | Agent(tools=[...]) |
| Disable all | Agent(..., system_events=False) |
| Choose events | Agent(..., system_events=[THINKING, TOOL_STARTED, SKILL_LOADED]) |
| Custom text | TOOL_STARTED.with_text("Running {tool_name}...") |
| Custom skill text | SKILL_LOADED.with_text("Consulting: {skill_name}") |
| Custom formatter | TOOL_STARTED.with_formatter(lambda d: ...) |
| Add tool completion | system_events=[..., TOOL_COMPLETED] |
| Bracket reasoning | system_events=[THINKING, THINKING_COMPLETE, TOOL_STARTED] |
| Full lifecycle | system_events=[THINKING, THINKING_COMPLETE, TOOL_STARTED, TOOL_COMPLETED, TOOL_FAILED, SKILL_LOADED] |
See Also¶
- Emitting Events —
emit_update()/emit()for user-defined updates - Event Subscriptions —
@agent.on()for reacting to events in code - Streaming Responses — full event type reference