feat(hooks): fire message_submit + on_error too (#455 observer-only)
Completes the observer-only slice of #455 by wiring the two remaining `HookEvent` variants that were defined but never fired: * `MessageSubmit` fires from `dispatch_user_message` before the message is handed to the engine. Hook context carries `message` so observers can log every prompt the user submits, redact for compliance audit, or page on `/wipe-database`-style content. Read-only. * `OnError` fires from `apply_engine_error_to_app` before the error cell reaches the transcript. Hook context carries `error`. Useful for paging on auth / billing / invalid- request failures without tailing the audit log. Combined with the prior `tool_call_before` / `tool_call_after` wiring, every `HookEvent` variant now has a live producer: `SessionStart`, `SessionEnd`, `MessageSubmit`, `ToolCallBefore`, `ToolCallAfter`, `ModeChange`, `OnError`. The `/hooks events` listing already enumerates them with their on-fire semantics. Hooks remain read-only observers in this slice. Mutation is v0.8.9 follow-up because it needs a synchronous-gate contract that would change semantics for every hook surface — including the lifecycle events that have shipped for many releases.
This commit is contained in:
+16
-14
@@ -251,20 +251,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
discover which events to target in `[[hooks.hooks]]` entries
|
||||
without reading source. Ordered lifecycle → per-tool →
|
||||
situational, stable across releases.
|
||||
- **`tool_call_before` + `tool_call_after` hooks fire** (#455
|
||||
observer-only slice) — both events were defined in the
|
||||
`HookEvent` enum but never fired from production code. The
|
||||
TUI now fires them from `tool_routing.rs` whenever a tool
|
||||
starts or finishes. Hook context carries `tool_name`,
|
||||
`tool_args` (before), or `tool_result` + success flag
|
||||
(after) — observers can log every tool call, send Slack
|
||||
pings, or audit. Hooks remain read-only in this slice;
|
||||
argument / result mutation is a v0.8.9 follow-up because it
|
||||
needs a synchronous-gate contract that doesn't exist today.
|
||||
`/hooks events` already enumerates these events; users can
|
||||
wire them via `[[hooks.hooks]]` entries with
|
||||
`event = "tool_call_before"` or `event = "tool_call_after"`
|
||||
immediately.
|
||||
- **`tool_call_before` / `tool_call_after` / `message_submit` /
|
||||
`on_error` hooks all fire now** (#455 observer-only slice) —
|
||||
these events were defined in the `HookEvent` enum but never
|
||||
fired from production code. Wired through:
|
||||
`tool_call_before` and `tool_call_after` fire from
|
||||
`tool_routing.rs`; `message_submit` fires from
|
||||
`dispatch_user_message` before engine dispatch; `on_error`
|
||||
fires from `apply_engine_error_to_app` before the error cell
|
||||
reaches the transcript. Hook contexts populate the relevant
|
||||
fields (`tool_name` + `tool_args` / `tool_result`,
|
||||
`message`, `error`). Hooks remain read-only in this slice;
|
||||
argument / result / message mutation is a v0.8.9 follow-up
|
||||
because it needs a synchronous-gate contract that doesn't
|
||||
exist today. Combined with the existing `session_start` /
|
||||
`session_end` / `mode_change` events, every variant in the
|
||||
`HookEvent` enum now has a live producer.
|
||||
- **RLM tool family** (#512) — `rlm` tool cards map to
|
||||
`ToolFamily::Rlm` and render `rlm`, not `swarm`. Stale "swarm"
|
||||
wording cleaned out of docs / comments / tests.
|
||||
|
||||
@@ -2574,6 +2574,16 @@ pub(crate) fn apply_engine_error_to_app(
|
||||
app.streaming_state.reset();
|
||||
app.streaming_message_index = None;
|
||||
app.streaming_thinking_active_entry = None;
|
||||
|
||||
// #455 (observer-only): fire `on_error` hooks so operators can
|
||||
// page on auth / billing / invalid-request failures without
|
||||
// tailing the audit log. Read-only — the hook can react but not
|
||||
// suppress the error from reaching the transcript.
|
||||
{
|
||||
let context = app.base_hook_context().with_error(&message);
|
||||
let _ = app.execute_hooks(crate::hooks::HookEvent::OnError, &context);
|
||||
}
|
||||
|
||||
app.add_message(HistoryCell::Error {
|
||||
message: message.clone(),
|
||||
severity,
|
||||
@@ -2880,6 +2890,15 @@ async fn dispatch_user_message(
|
||||
engine_handle: &EngineHandle,
|
||||
message: QueuedMessage,
|
||||
) -> Result<()> {
|
||||
// #455 (observer-only): fire `message_submit` hooks before
|
||||
// dispatch. Hooks see the user's display text via the
|
||||
// `with_message` builder. Read-only — they can log, audit, or
|
||||
// notify but cannot mutate the message that goes to the engine.
|
||||
{
|
||||
let context = app.base_hook_context().with_message(&message.display);
|
||||
let _ = app.execute_hooks(crate::hooks::HookEvent::MessageSubmit, &context);
|
||||
}
|
||||
|
||||
// Set immediately to prevent double-dispatch before TurnStarted event arrives.
|
||||
app.is_loading = true;
|
||||
app.last_send_at = Some(Instant::now());
|
||||
|
||||
Reference in New Issue
Block a user