Skip to main content

everruns_core/
llm_error_hook.rs

1// LLM error hook seam
2//
3// An in-process capability hook for reacting to a *terminal* LLM error (a turn
4// that failed and will not be retried). This is the platform seam that lets a
5// capability encapsulate error-recovery behavior — augmenting the user-facing
6// error copy and/or performing a side effect — without the reason atom hard-
7// coding any capability-specific logic. It belongs to the same in-process hook
8// family as `ToolCallHook`, `MessageFilterProvider`, and `output_guardrails`
9// (typed traits returned from a `Capability::*` seam and invoked in-process with
10// host services) — as opposed to the user-hook system (`user_hook_types`), which
11// runs user-authored shell commands. The reason atom collects the hooks from the
12// active capabilities and invokes each generically.
13//
14// The first consumer is `usage_limit_auto_continue`, which schedules a
15// continuation after a provider usage limit resets, but the seam is deliberately
16// provider- and behavior-agnostic so other extensions can be built the same way.
17
18use async_trait::async_trait;
19use std::sync::Arc;
20
21use crate::traits::SessionScheduleStore;
22use crate::typed_id::SessionId;
23use crate::user_facing_error::UserFacingErrorFields;
24use serde_json::Value;
25
26/// Host services made available to error hooks. Extend as new hooks need more
27/// surface; today only the session schedule store is exposed (each field is
28/// optional so a hook degrades to a no-op when its service is absent).
29#[derive(Clone, Default)]
30pub struct LlmErrorHookServices {
31    pub schedule_store: Option<Arc<dyn SessionScheduleStore>>,
32}
33
34/// Context handed to a capability's [`LlmErrorHook`] on a terminal LLM error.
35pub struct LlmErrorContext<'a> {
36    /// Session whose turn failed.
37    pub session_id: SessionId,
38    /// Classified user-facing error code (e.g. `provider_usage_limit_reached`).
39    pub error_code: &'a str,
40    /// Structured fields captured by the classifier (e.g. `resets_at`).
41    pub error_fields: &'a UserFacingErrorFields,
42    /// The contributing capability's per-agent config JSON (may be `Null`).
43    pub config: &'a Value,
44    /// Host services the hook may use to perform side effects.
45    pub services: &'a LlmErrorHookServices,
46}
47
48/// Result of an [`LlmErrorHook`]: extra fields to merge into the user-facing
49/// error (for example to unlock capability-specific message copy). The default
50/// is a no-op that changes nothing.
51#[derive(Debug, Default, PartialEq, Eq)]
52pub struct LlmErrorHookOutcome {
53    /// Fields to merge into the `UserFacingError` before it is rendered/emitted.
54    pub extra_error_fields: UserFacingErrorFields,
55}
56
57impl LlmErrorHookOutcome {
58    /// An outcome that changes nothing.
59    pub fn noop() -> Self {
60        Self::default()
61    }
62
63    /// Add a field to merge into the user-facing error.
64    pub fn with_error_field(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
65        self.extra_error_fields.insert(key.into(), value.into());
66        self
67    }
68}
69
70/// A capability's reaction to a terminal LLM error. Provided via
71/// `Capability::llm_error_hook`. Runs only on the terminal (non-retried)
72/// error path, before the user-facing error message is emitted.
73#[async_trait]
74pub trait LlmErrorHook: Send + Sync {
75    async fn on_llm_error(&self, ctx: &LlmErrorContext<'_>) -> LlmErrorHookOutcome;
76}