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(
176                "tools",
177                &self
178                    .tools
179                    .iter()
180                    .map(|t| t.schema().name.as_str())
181                    .collect::<Vec<_>>(),
182            )
183            .finish()
184    }
185}
186
187impl AgentContext {
188    pub fn new(messages: Vec<AgentMessage>) -> Self {
189        Self {
190            system_prompt: String::new(),
191            messages,
192            tools: Vec::new(),
193        }
194    }
195}
196
197/// Replacement runtime state returned by `prepare_next_turn`. `None` on any
198/// field means "keep current".
199#[derive(Debug, Clone, Default)]
200pub struct AgentLoopTurnUpdate {
201    pub context: Option<AgentContext>,
202    pub model: Option<rpi_ai::model::Model>,
203    pub thinking_level: Option<rpi_ai::types::ThinkingLevel>,
204}
205
206/// Public agent state snapshot. Mirrors TS `AgentState` (the readable subset).
207/// `tools`/`messages` clone on read so callers can't mutate internal state.
208#[derive(Clone)]
209pub struct AgentState {
210    pub system_prompt: String,
211    pub model: rpi_ai::model::Model,
212    pub thinking_level: rpi_ai::types::ThinkingLevel,
213    pub tools: Vec<Arc<dyn crate::agent_tool::AgentTool>>,
214    pub messages: Vec<AgentMessage>,
215    pub is_streaming: bool,
216    pub streaming_message: Option<AgentMessage>,
217    pub pending_tool_calls: HashSet<String>,
218    pub error_message: Option<String>,
219}
220
221impl std::fmt::Debug for AgentState {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        f.debug_struct("AgentState")
224            .field("system_prompt", &self.system_prompt)
225            .field("model", &self.model)
226            .field("thinking_level", &self.thinking_level)
227            .field(
228                "tools",
229                &self
230                    .tools
231                    .iter()
232                    .map(|t| t.schema().name.as_str())
233                    .collect::<Vec<_>>(),
234            )
235            .field("messages", &self.messages)
236            .field("is_streaming", &self.is_streaming)
237            .field("streaming_message", &self.streaming_message)
238            .field("pending_tool_calls", &self.pending_tool_calls)
239            .field("error_message", &self.error_message)
240            .finish()
241    }
242}
243
244impl Default for AgentState {
245    fn default() -> Self {
246        Self {
247            system_prompt: String::new(),
248            model: default_model(),
249            thinking_level: rpi_ai::types::ThinkingLevel::Off,
250            tools: Vec::new(),
251            messages: Vec::new(),
252            is_streaming: false,
253            streaming_message: None,
254            pending_tool_calls: HashSet::new(),
255            error_message: None,
256        }
257    }
258}
259
260/// The placeholder model used when none is configured. Mirrors TS `DEFAULT_MODEL`.
261pub(crate) fn default_model() -> rpi_ai::model::Model {
262    rpi_ai::model::Model::new(
263        "unknown",
264        "unknown",
265        rpi_ai::types::Api::Other("unknown".into()),
266        "unknown",
267        "",
268    )
269}