Extending and embedding · 17

Events

A turn streams events: riffer’s stream events unchanged — text and reasoning deltas, tool calls, token usage — plus riffer-rig’s own events below. Runtime#prompt yields them, --json prints them, and a host renders them. Match on the events you know and ignore the rest; new events can be added in a minor release.

Every rig event has type, its snake_case name, and to_h, its fields with the type included, so each one prints as a JSON record as-is.

Rig events

Event Carries When
session_start id, reason (:new, :restore, :reload) opens the first prompt of a new or restored session, and the first after a rebuild
session_end reason (:reload, :close) opens the first prompt after a rebuild, just before its session_start; :close reaches only hooks, as nothing streams after close
command_output command, text a command called ctx.say
skill_activated name a skill was run with /skill:<name>
notify message, level every notice the host was given, such as an extension failing to load
before_tool_call call_id, tool, args, at a tool is about to run (below)
after_tool_call call_id, tool, args, result, at, duration a tool call has finished (below)
job_exit job_id, command, exit_status, signal, duration, output_path the model is told a background job exited, one per job
turn_end stop_reason, usage, cost always the last event of a prompt

turn_end’s stop_reason is riffer’s outcome (:completed, :length, :max_steps, :guardrail_blocked, …) or :cancelled after a cancel. usage is the turn’s riffer TokenUsage, and cost its price in USD, or nil when the model is not priced. In to_h, and so in a JSON record, the cost is usage’s cost key, left out when nil. A consumer never needs ask to learn how a streamed turn ended.

Tool call events

When the model asks for tools, riffer first announces every call of the response with ToolCallDone. The tools then run one at a time, each between a before_tool_call and an after_tool_call:

model response
  ├─ ToolCallDone        call 1
  ├─ ToolCallDone        call 2
  ├─ before_tool_call    call 1 ─┐
  │                              │ tool 1 runs   (duration)
  ├─ after_tool_call     call 1 ─┘
  ├─ before_tool_call    call 2 ─┐
  │                              │ tool 2 runs
  └─ after_tool_call     call 2 ─┘
next model response

Every call gets exactly one after_tool_call, whatever happened to it:

The call… before_tool_call after_tool_call’s result
ran yes, with the arguments it ran with the tool’s response; error_type set if it failed
named a tool that doesn’t exist yes an :unknown_tool error
was blocked by a hook no a :blocked error, with the arguments the model sent
had arguments that aren’t a JSON object no a :validation_error, with empty args

args is a hash with symbol keys. result is a Riffer::Tools::Response: result.error? says whether the call failed. call_id matches the call’s ToolCallDone. at is a monotonic timestamp in seconds, comparable only within one process, and duration is how long the tool itself ran, leaving out time spent in hooks; a call that never ran has 0.0. The timestamps come from the Runtime’s clock:, which a test can replace.

These are the same event objects the before_tool_call and after_tool_call hooks receive.

Custom events

A command can send its own events to whoever ran it with ctx.emit(event). Any object with type and to_h qualifies (the RBS interface Riffer::Rig::Events::_Event).