Skip to main content

opi_agent/
sdk.rs

1//! SDK embedding surface for programmatic agent control.
2//!
3//! **Unstable 0.x API** — these types may change between minor versions without
4//! notice. Embedders MUST pin an exact version and test against upgrades.
5//!
6//! # Overview
7//!
8//! This module provides shared command, response, and event types used by both
9//! the RPC JSONL protocol (stdin/stdout) and the programmatic embedding API.
10//! By centralising the protocol types here, the coding agent's RPC runner and
11//! downstream embedders share the same definitions without duplicating logic.
12//!
13//! # Commands
14//!
15//! [`SdkCommand`] covers the full set of operations: prompt, continue, steer,
16//! follow_up, abort, set_model, set_thinking_level, compact, session_info,
17//! and quit. Each variant carries an optional `id` for request/response
18//! correlation.
19//!
20//! # Responses
21//!
22//! [`SdkResponse`] produces the standard JSON response envelope (`type:
23//! "response"`, `success`, optional `id`/`error`/`data` fields) used by the
24//! RPC protocol. Embedders can also consume it directly for structured results.
25//!
26//! # Events
27//!
28//! [`agent_event_to_value`] converts an [`AgentEvent`]
29//! to a [`serde_json::Value`] for JSONL emission or structured inspection.
30
31use crate::event::AgentEvent;
32
33// ---------------------------------------------------------------------------
34// Schema version
35// ---------------------------------------------------------------------------
36
37/// SDK/RPC protocol schema version. Clients and embedders MUST check this
38/// before processing commands or events.
39///
40/// This is an **unstable 0.x** protocol. The version will remain at 2 until
41/// the SDK surface stabilises; breaking changes bump the major version.
42pub const SDK_SCHEMA_VERSION: u32 = 2;
43
44// ---------------------------------------------------------------------------
45// Command types
46// ---------------------------------------------------------------------------
47
48/// An SDK command for controlling the agent programmatically.
49///
50/// This is the canonical command type shared between the RPC runner and the
51/// embedding API. It round-trips through JSON with `#[serde(tag = "type")]`.
52///
53/// Each variant carries an optional `id` field for request/response
54/// correlation in multiplexed scenarios.
55#[allow(non_camel_case_types)]
56#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
57#[serde(tag = "type")]
58pub enum SdkCommand {
59    /// Send a user prompt, streaming agent events.
60    prompt {
61        #[serde(default, skip_serializing_if = "Option::is_none")]
62        id: Option<String>,
63        message: String,
64    },
65    /// Continue conversation with additional text.
66    #[serde(rename = "continue")]
67    continue_ {
68        #[serde(default, skip_serializing_if = "Option::is_none")]
69        id: Option<String>,
70        message: String,
71    },
72    /// Queue a steering message during agent operation.
73    steer {
74        #[serde(default, skip_serializing_if = "Option::is_none")]
75        id: Option<String>,
76        message: String,
77    },
78    /// Queue a follow-up message for after agent stops.
79    follow_up {
80        #[serde(default, skip_serializing_if = "Option::is_none")]
81        id: Option<String>,
82        message: String,
83    },
84    /// Cancel current agent operation.
85    abort {
86        #[serde(default, skip_serializing_if = "Option::is_none")]
87        id: Option<String>,
88    },
89    /// Switch provider:model.
90    set_model {
91        #[serde(default, skip_serializing_if = "Option::is_none")]
92        id: Option<String>,
93        model: String,
94    },
95    /// Set thinking/reasoning level.
96    set_thinking_level {
97        #[serde(default, skip_serializing_if = "Option::is_none")]
98        id: Option<String>,
99        level: String,
100    },
101    /// Trigger manual compaction.
102    compact {
103        #[serde(default, skip_serializing_if = "Option::is_none")]
104        id: Option<String>,
105    },
106    /// Query session metadata.
107    session_info {
108        #[serde(default, skip_serializing_if = "Option::is_none")]
109        id: Option<String>,
110    },
111    /// Shut down the session.
112    quit {
113        #[serde(default, skip_serializing_if = "Option::is_none")]
114        id: Option<String>,
115    },
116}
117
118impl SdkCommand {
119    /// Return the optional correlation id.
120    pub fn id(&self) -> Option<&str> {
121        match self {
122            Self::prompt { id, .. }
123            | Self::continue_ { id, .. }
124            | Self::steer { id, .. }
125            | Self::follow_up { id, .. }
126            | Self::abort { id }
127            | Self::set_model { id, .. }
128            | Self::set_thinking_level { id, .. }
129            | Self::compact { id }
130            | Self::session_info { id }
131            | Self::quit { id } => id.as_deref(),
132        }
133    }
134
135    /// Return the command name for response correlation.
136    pub fn command_name(&self) -> &'static str {
137        match self {
138            Self::prompt { .. } => "prompt",
139            Self::continue_ { .. } => "continue",
140            Self::steer { .. } => "steer",
141            Self::follow_up { .. } => "follow_up",
142            Self::abort { .. } => "abort",
143            Self::set_model { .. } => "set_model",
144            Self::set_thinking_level { .. } => "set_thinking_level",
145            Self::compact { .. } => "compact",
146            Self::session_info { .. } => "session_info",
147            Self::quit { .. } => "quit",
148        }
149    }
150
151    /// Whether this is the quit command.
152    pub fn is_quit(&self) -> bool {
153        matches!(self, Self::quit { .. })
154    }
155}
156
157// ---------------------------------------------------------------------------
158// Response types
159// ---------------------------------------------------------------------------
160
161/// A structured SDK/RPC response.
162///
163/// Serialises to the standard JSONL response envelope:
164/// ```json
165/// {"type":"response","command":"prompt","success":true,"id":"42"}
166/// ```
167///
168/// For errors:
169/// ```json
170/// {"type":"response","command":"set_model","success":false,"error":"..."}
171/// ```
172///
173/// For success with data:
174/// ```json
175/// {"type":"response","command":"session_info","success":true,"data":{...}}
176/// ```
177#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
178pub struct SdkResponse {
179    /// Always `"response"`.
180    #[serde(default = "response_type")]
181    r#type: String,
182    /// The command name this response correlates to.
183    pub command: String,
184    /// Whether the command succeeded.
185    pub success: bool,
186    /// Optional correlation id matching the command's `id`.
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub id: Option<String>,
189    /// Error message (only when `success` is false).
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub error: Option<String>,
192    /// Response data payload (only when `success` is true).
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub data: Option<serde_json::Value>,
195}
196
197impl SdkResponse {
198    /// Build a success response.
199    pub fn success(id: Option<&str>, command: &str) -> Self {
200        Self {
201            r#type: response_type(),
202            command: command.to_owned(),
203            success: true,
204            id: id.map(|s| s.to_owned()),
205            error: None,
206            data: None,
207        }
208    }
209
210    /// Build a success response with a data payload.
211    pub fn success_with_data(id: Option<&str>, command: &str, data: serde_json::Value) -> Self {
212        Self {
213            r#type: response_type(),
214            command: command.to_owned(),
215            success: true,
216            id: id.map(|s| s.to_owned()),
217            error: None,
218            data: Some(data),
219        }
220    }
221
222    /// Build an error response.
223    pub fn error(id: Option<&str>, command: &str, message: &str) -> Self {
224        Self {
225            r#type: response_type(),
226            command: command.to_owned(),
227            success: false,
228            id: id.map(|s| s.to_owned()),
229            error: Some(message.to_owned()),
230            data: None,
231        }
232    }
233}
234
235fn response_type() -> String {
236    "response".to_owned()
237}
238
239// ---------------------------------------------------------------------------
240// Event conversion
241// ---------------------------------------------------------------------------
242
243/// Convert an [`AgentEvent`] to a [`serde_json::Value`] for JSONL emission
244/// or structured inspection.
245///
246/// Reuses the existing `AgentEvent` serde serialization (which includes the
247/// `"type"` tag). Falls back to a generic error payload if serialization fails.
248pub fn agent_event_to_value(event: &AgentEvent) -> serde_json::Value {
249    match serde_json::to_value(event) {
250        Ok(v) => v,
251        Err(_) => serde_json::json!({
252            "type": "SdkSerializationError",
253            "message": "failed to serialize agent event",
254        }),
255    }
256}