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}