Skip to main content

oxi_agent/advisor/
types.rs

1//! Advisor core types — ported from omp `advise-tool.ts`.
2//!
3//! Pure data: severity, note, delivery channel. No host/runtime dependency.
4//!
5//! # Attribution
6//!
7//! Translated to Rust from omp (oh-my-pi), which is MIT licensed
8//! (Copyright (c) 2025 Mario Zechner; Copyright (c) 2025-2026 Can Bölük).
9//! oxi's translation remains under oxi's own MIT license.
10
11use serde::{Deserialize, Serialize};
12
13/// Advisor note severity. omp `AdvisorSeverity`. Omitting it (in the tool
14/// schema) is treated as a plain `nit`.
15#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum AdvisorSeverity {
18    #[default]
19    /// Non-urgent cleanup, refactor, missed opportunity. Rides the aside queue.
20    Nit,
21    /// Agent might be heading wrong or missed something material. Interrupts.
22    Concern,
23    /// Stop and reconsider. Interrupts.
24    Blocker,
25}
26
27impl AdvisorSeverity {
28    /// Lowercase id matching omp's schema strings.
29    #[must_use]
30    pub const fn as_str(self) -> &'static str {
31        match self {
32            AdvisorSeverity::Nit => "nit",
33            AdvisorSeverity::Concern => "concern",
34            AdvisorSeverity::Blocker => "blocker",
35        }
36    }
37
38    /// Parse a lowercase id. `None` for anything else.
39    #[must_use]
40    pub fn from_id(s: &str) -> Option<Self> {
41        Some(match s {
42            "nit" => AdvisorSeverity::Nit,
43            "concern" => AdvisorSeverity::Concern,
44            "blocker" => AdvisorSeverity::Blocker,
45            _ => return None,
46        })
47    }
48
49    /// Dedupe rank — `nit < concern < blocker`. A new call passes the dedupe
50    /// gate only when its rank strictly exceeds the recorded one (a real
51    /// escalation). omp `ADVISOR_SEVERITY_RANK`.
52    #[must_use]
53    pub const fn rank(self) -> u8 {
54        match self {
55            AdvisorSeverity::Nit => 1,
56            AdvisorSeverity::Concern => 2,
57            AdvisorSeverity::Blocker => 3,
58        }
59    }
60}
61
62/// One queued advice note. omp `AdvisorNote`.
63#[derive(Debug, Clone)]
64pub struct AdvisorNote {
65    /// The advice text, terse and specific.
66    pub note: String,
67    /// Severity; `None` means a plain nit (omp treats an omitted severity as nit).
68    pub severity: Option<AdvisorSeverity>,
69}
70
71impl AdvisorNote {
72    /// Rank for dedupe. `None` severity defers to `nit` (omp `advisorSeverityRank`).
73    #[must_use]
74    pub fn rank(&self) -> u8 {
75        self.severity.unwrap_or_default().rank()
76    }
77}
78
79/// How an advisor note reaches the primary agent. omp `AdvisorDeliveryChannel`.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum AdvisorDeliveryChannel {
82    /// Non-interrupting `<advisory>` card rendered in the transcript; the run
83    /// continues. Used for `nit`, and for `concern`/`blocker` during the
84    /// post-interrupt immune-turn window.
85    Aside,
86    /// Injected into the primary agent's steering queue — acted on at the next
87    /// turn boundary, or (while streaming) mid-turn.
88    Steer,
89    /// Visible card, but does not resume a stopped run. Used after a deliberate
90    /// user interrupt while the agent is idle/aborting.
91    Preserve,
92}
93
94/// Inputs to [`crate::advisor::channels::resolve_delivery_channel`].
95/// omp `resolveAdvisorDeliveryChannel` opts.
96#[derive(Debug, Clone, Copy, Default)]
97pub struct DeliveryOpts {
98    /// The note's severity (`None` = nit).
99    pub severity: Option<AdvisorSeverity>,
100    /// Latched `true` when the user deliberately interrupted; suppresses
101    /// `concern`/`blocker` auto-resume until the user next resumes.
102    pub auto_resume_suppressed: bool,
103    /// Whether the primary agent is currently streaming a turn.
104    pub streaming: bool,
105    /// Whether an interrupted turn is still being torn down.
106    pub aborting: bool,
107    /// Whether the post-interrupt immune-turn cooldown window is active.
108    pub interrupt_immune_turn_active: bool,
109}
110
111/// omp `ADVISOR_GUIDANCE` — behavioral framing carried as a tag attribute on
112/// every rendered `<advisory>` block. The primary agent's system prompt never
113/// mentions advisories, so this is its only cue for how to treat them.
114pub const ADVISOR_GUIDANCE: &str = "weigh, don't blindly obey";
115
116/// Dedupe key for an advisor note — trim + collapse internal whitespace.
117/// omp `advisorNoteDedupeKey`. (Phonetic/content normalization for the
118/// *emission* gate lives in [`crate::advisor::emission_guard`].)
119#[must_use]
120pub(crate) fn note_dedupe_key(note: &str) -> String {
121    note.split_whitespace().collect::<Vec<_>>().join(" ")
122}