Skip to main content

rpi_agent/
types.rs

1//! Mirrors `packages/agent/src/types.ts` — the public type contract of the
2//! agent layer: tool results, hook payloads, agent state, events.
3//!
4//! The TS source declares `AgentMessage = Message | Custom` via declaration
5//! merging; the Rust port models it as an open [`AgentMessage`] enum (see
6//! [`crate::message`]). Everything else here is a straight port of the TS
7//! interfaces, adapted to Rust ownership/async idioms.
8
9use rpi_ai::types::{AssistantMessage, ImageContent, TextContent, ToolResultMessage, Usage};
10use std::collections::HashSet;
11use std::sync::Arc;
12
13use crate::message::AgentMessage;
14
15/// How a batch of tool calls from one assistant message are executed.
16///
17/// - `Sequential`: each call is prepared, executed, finalized before the next starts.
18/// - `Parallel`: calls are prepared sequentially, then allowed tools execute
19///   concurrently. `tool_execution_end` fires in completion order; tool-result
20///   `MessageEnd` fires later in assistant source order.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum ToolExecutionMode {
23    Sequential,
24    #[default]
25    Parallel,
26}
27
28/// Controls how many queued user messages are injected at a drain point.
29/// `All` drains every queued message; `OneAtATime` drains only the oldest.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub enum QueueMode {
32    All,
33    #[default]
34    OneAtATime,
35}
36
37/// Result returned from a tool `execute`. Mirrors TS `AgentToolResult<T>`:
38/// `content` goes back to the model, `details` are structured log/UI payload,
39/// `usage` is optionally reported, and `terminate` hints the batch should stop.
40#[derive(Debug, Clone, Default)]
41pub struct AgentToolResult {
42    pub content: Vec<TextContentOrImage>,
43    pub details: serde_json::Value,
44    pub usage: Option<Usage>,
45    pub added_tool_names: Vec<String>,
46    pub terminate: bool,
47}
48/// an enum so tool authors stay within the provider-content language without
49/// pulling in the full `Content` union (which adds Thinking/ToolCall).
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum TextContentOrImage {
52    Text(TextContent),
53    Image(ImageContent),
54}
55
56impl TextContentOrImage {
57    pub fn text<S: Into<String>>(s: S) -> Self {
58        TextContentOrImage::Text(TextContent {
59            kind: rpi_ai::types::TextContentType,
60            text: s.into(),
61            text_signature: None,
62        })
63    }
64}
65
66impl AgentToolResult {
67    /// Convenience: a single text block, empty details.
68    pub fn text(message: impl Into<String>) -> Self {
69        Self {
70            content: vec![TextContentOrImage::text(message)],
71            details: serde_json::Value::Null,
72            usage: None,
73            added_tool_names: Vec::new(),
74            terminate: false,
75        }
76    }
77
78    /// Convenience: an error text block. `is_error` is carried on the
79    /// `ToolResultMessage`, not the result; this just builds the content.
80    pub fn error_text(message: impl Into<String>) -> Self {
81        Self::text(message)
82    }
83
84    pub fn into_content(self) -> Vec<rpi_ai::types::Content> {
85        self.content
86            .into_iter()
87            .map(|c| match c {
88                TextContentOrImage::Text(t) => rpi_ai::types::Content::Text(t),
89                TextContentOrImage::Image(i) => rpi_ai::types::Content::Image(i),
90            })
91            .collect()
92    }
93}
94
95impl From<AgentToolResult> for Result<AgentToolResult, crate::AgentError> {
96    fn from(r: AgentToolResult) -> Self {
97        Ok(r)
98    }
99}
100
101/// Partial result pushed by a tool's `on_update` callback during execution.
102/// Mirrors TS `AgentToolUpdateCallback<T>` payload.
103pub type ToolResultPartial = AgentToolResult;
104
105/// Result of a `before_tool_call` hook. `block` prevents execution; the loop
106/// emits an error tool result with `reason` (or a default) instead. `terminate`
107/// participates in the batch early-termination rule (all results must set it).
108///
109/// `args` is the Rust equivalent of TS `beforeToolCall` mutating the validated
110/// args object in place: JS callbacks receive `args` by reference and write to
111/// it; Rust hands the hook an immutable `&serde_json::Value`, so to rewrite the
112/// args the hook returns them here. Replacement args are applied **without
113/// re-validation** — mirroring TS, where the mutation happens after
114/// `validateToolArguments` and is never re-checked. `None` keeps the validated
115/// args.
116#[derive(Debug, Clone, Default)]
117pub struct BeforeToolCallResult {
118    pub block: bool,
119    pub reason: Option<String>,
120    pub terminate: bool,
121    pub args: Option<serde_json::Value>,
122}
123
124/// Partial override returned from `after_tool_call`. Field-by-field merge:
125/// provided values replace the executed result's fields; omitted fields keep
126/// the original. No deep merge.
127#[derive(Debug, Clone, Default)]
128pub struct AfterToolCallResult {
129    pub content: Option<Vec<TextContentOrImage>>,
130    pub details: Option<serde_json::Value>,
131    pub is_error: Option<bool>,
132    pub usage: Option<Usage>,
133    pub terminate: Option<bool>,
134}
135
136/// Context passed to `before_tool_call`. Mirrors TS `BeforeToolCallContext`.
137pub struct BeforeToolCallContext<'a> {
138    pub assistant_message: &'a AssistantMessage,
139    pub tool_call: &'a rpi_ai::types::ToolCall,
140    pub args: &'a serde_json::Value,
141    pub context: &'a AgentContext,
142}
143
144/// Context passed to `after_tool_call`. Mirrors TS `AfterToolCallContext`.
145pub struct AfterToolCallContext<'a> {
146    pub assistant_message: &'a AssistantMessage,
147    pub tool_call: &'a rpi_ai::types::ToolCall,
148    pub args: &'a serde_json::Value,
149    pub result: &'a AgentToolResult,
150    pub is_error: bool,
151    pub context: &'a AgentContext,
152}
153
154/// Context passed to `should_stop_after_turn` / `prepare_next_turn`.
155pub struct ShouldStopAfterTurnContext<'a> {
156    pub message: &'a AssistantMessage,
157    pub tool_results: &'a [ToolResultMessage],
158    pub context: &'a AgentContext,
159    pub new_messages: &'a [AgentMessage],
160}
161
162/// A context snapshot handed to the low-level loop. Mirrors TS `AgentContext`.
163#[derive(Clone, Default)]
164pub struct AgentContext {
165    pub system_prompt: String,
166    pub messages: Vec<AgentMessage>,
167    pub tools: Vec<Arc<dyn crate::agent_tool::AgentTool>>,
168}
169
170impl std::fmt::Debug for AgentContext {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.debug_struct("AgentContext")
173            .field("system_prompt", &self.system_prompt)
174            .field("messages", &self.messages)
175            .field("tools", &self.tools.iter().map(|t| t.schema().name.as_str()).collect::<Vec<_>>())
176            .finish()
177    }
178}
179
180impl AgentContext {
181    pub fn new(messages: Vec<AgentMessage>) -> Self {
182        Self {
183            system_prompt: String::new(),
184            messages,
185            tools: Vec::new(),
186        }
187    }
188}
189
190/// Replacement runtime state returned by `prepare_next_turn`. `None` on any
191/// field means "keep current".
192#[derive(Debug, Clone, Default)]
193pub struct AgentLoopTurnUpdate {
194    pub context: Option<AgentContext>,
195    pub model: Option<rpi_ai::model::Model>,
196    pub thinking_level: Option<rpi_ai::types::ThinkingLevel>,
197}
198
199/// Public agent state snapshot. Mirrors TS `AgentState` (the readable subset).
200/// `tools`/`messages` clone on read so callers can't mutate internal state.
201#[derive(Clone)]
202pub struct AgentState {
203    pub system_prompt: String,
204    pub model: rpi_ai::model::Model,
205    pub thinking_level: rpi_ai::types::ThinkingLevel,
206    pub tools: Vec<Arc<dyn crate::agent_tool::AgentTool>>,
207    pub messages: Vec<AgentMessage>,
208    pub is_streaming: bool,
209    pub streaming_message: Option<AgentMessage>,
210    pub pending_tool_calls: HashSet<String>,
211    pub error_message: Option<String>,
212}
213
214impl std::fmt::Debug for AgentState {
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        f.debug_struct("AgentState")
217            .field("system_prompt", &self.system_prompt)
218            .field("model", &self.model)
219            .field("thinking_level", &self.thinking_level)
220            .field("tools", &self.tools.iter().map(|t| t.schema().name.as_str()).collect::<Vec<_>>())
221            .field("messages", &self.messages)
222            .field("is_streaming", &self.is_streaming)
223            .field("streaming_message", &self.streaming_message)
224            .field("pending_tool_calls", &self.pending_tool_calls)
225            .field("error_message", &self.error_message)
226            .finish()
227    }
228}
229
230impl Default for AgentState {
231    fn default() -> Self {
232        Self {
233            system_prompt: String::new(),
234            model: default_model(),
235            thinking_level: rpi_ai::types::ThinkingLevel::Off,
236            tools: Vec::new(),
237            messages: Vec::new(),
238            is_streaming: false,
239            streaming_message: None,
240            pending_tool_calls: HashSet::new(),
241            error_message: None,
242        }
243    }
244}
245
246/// The placeholder model used when none is configured. Mirrors TS `DEFAULT_MODEL`.
247pub(crate) fn default_model() -> rpi_ai::model::Model {
248    rpi_ai::model::Model::new("unknown", "unknown", rpi_ai::types::Api::Other("unknown".into()), "unknown", "")
249}