alwith_api/protocol.rs
1// Generated by sync-runtime-protocol.ts from the Runtime binary; do not edit.
2//! Types in the `alwith-runtime-v1` frame protocol: session run state, lifecycle frames, journal
3//! frames,
4//! agent metadata and Runtime events.
5//! Canonical wire types owned by Runtime. Client bindings are exported from this definition.
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use specta::Type;
10
11/// The `error` in a response frame, in JSON-RPC shape. Runtime's own errors use the codes below;
12/// ACP errors returned by the agent (`acpRequest` etc.) pass through unchanged, `code`/`data` come
13/// from the agent.
14#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
15pub struct ResponseError {
16 pub code: i64,
17 pub message: String,
18 /// Data attached to an agent error (e.g. `{details}`, `{authMethods}`); null if absent.
19 #[serde(default)]
20 #[specta(type = Option<specta_typescript::Unknown>)]
21 pub data: Option<Value>,
22}
23
24impl ResponseError {
25 /// Runtime failed to execute (no such agent, not the owner, agent exited...).
26 pub const RUNTIME: i64 = -32000;
27 /// Connection lost: Runtime exited or cannot be written to. Generated locally by the client,
28 /// never sent by Runtime.
29 pub const CONNECTION_LOST: i64 = -32001;
30 pub const METHOD_NOT_FOUND: i64 = -32601;
31 pub const INVALID_PARAMS: i64 = -32602;
32
33 pub fn new(code: i64, message: impl Into<String>) -> Self {
34 Self { code, message: message.into(), data: None }
35 }
36
37 pub fn runtime(message: impl Into<String>) -> Self {
38 Self::new(Self::RUNTIME, message)
39 }
40
41 pub fn connection_lost(message: impl Into<String>) -> Self {
42 Self::new(Self::CONNECTION_LOST, message)
43 }
44
45 pub fn method_not_found(method: &str) -> Self {
46 Self::new(Self::METHOD_NOT_FOUND, format!("unknown runtime method {method}"))
47 }
48
49 pub fn invalid_params(detail: impl std::fmt::Display) -> Self {
50 Self::new(Self::INVALID_PARAMS, format!("invalid params: {detail}"))
51 }
52
53 /// The frame's `error` object -> as is; if it is not that shape, the whole value becomes the
54 /// message.
55 pub fn parse(error: &Value) -> Self {
56 serde_json::from_value::<Self>(error.clone()).unwrap_or_else(|_| Self::runtime(error.to_string()))
57 }
58}
59
60impl std::fmt::Display for ResponseError {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.write_str(&self.message)
63 }
64}
65
66impl std::error::Error for ResponseError {}
67
68impl From<String> for ResponseError {
69 fn from(message: String) -> Self {
70 Self::runtime(message)
71 }
72}
73
74impl From<&str> for ResponseError {
75 fn from(message: &str) -> Self {
76 Self::runtime(message)
77 }
78}
79
80impl From<ResponseError> for String {
81 fn from(error: ResponseError) -> Self {
82 error.message
83 }
84}
85
86/// Agent requests still unanswered at attach time (`session/request_permission` etc.); the new
87/// owner answers them with `acpRespond` using the same `id`.
88#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
89pub struct PendingAgentRequest {
90 #[specta(type = specta_typescript::Unknown)]
91 pub id: Value,
92 pub method: String,
93 #[specta(type = specta_typescript::Unknown)]
94 pub params: Value,
95}
96
97/// Session run state. The first three are values of ACP v2 `StateUpdate` directly (reported by the
98/// agent, no longer derived by Runtime from traffic);
99/// `Done` is a product state we synthesize ourselves: the protocol has no notion of "finished but
100/// unseen". It = idle + unread,
101/// synthesized by `upsert_run_state` on a running/done -> idle transition and consumed only by
102/// `markRead`.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Type)]
104#[serde(rename_all = "snake_case")]
105pub enum RunState {
106 Running,
107 RequiresAction,
108 Done,
109 Idle,
110}
111
112#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
113#[serde(rename_all = "camelCase")]
114pub struct SessionTask {
115 pub id: String,
116 pub title: String,
117 pub background: bool,
118 pub since: i64,
119}
120
121#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
122#[serde(rename_all = "camelCase")]
123pub struct SessionRunState {
124 pub session_id: String,
125 pub state: RunState,
126 pub reusable_draft: bool,
127 #[serde(rename = "owner")]
128 pub owner_id: String,
129 /// Session working directory (params.cwd of session/new|load|resume|fork). Empty string = the
130 /// init request carried none
131 /// (e.g. a session tracked only from a prompt after a process restart). Used for mobile list
132 /// display.
133 pub cwd: String,
134 /// Session title (session_info_update.title in session/update, same source as the desktop chat
135 /// title).
136 /// Empty string = the agent has not produced a title yet. Used for mobile list display.
137 pub title: String,
138 /// Time the current state was entered (epoch ms). Kept when the same state continues.
139 pub since: i64,
140 pub tasks: Vec<SessionTask>,
141 pub listening_ports: Vec<u16>,
142 pub execution_id: Option<String>,
143 pub execution_role: Option<String>,
144 pub last_activity_at: i64,
145 /// Monotonic per session: moves when the state or the interruption changes (not with activity
146 /// in the same state), so a version names one attention state. `markRead` carries the version
147 /// it confirms; a confirmation of an older version does not clear a newer `done`.
148 pub version: u64,
149 /// Why the Runtime holds this session in `requires_action` without the agent asking: the
150 /// session was `running` when the Runtime stopped, so its turn was cut short. Cleared by the
151 /// next state the agent reports; `None` otherwise.
152 pub interruption: Option<String>,
153}
154
155#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
156#[serde(rename_all = "camelCase")]
157pub struct SessionLifecycleFrame {
158 pub sequence: u64,
159 pub recorded_at: i64,
160 pub line: String,
161}
162
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct AttachInfo {
166 pub agent_id: String,
167 /// The core this agent was spawned with (authoritative AgentMetadata value), filled by
168 /// `Runtime::attach`.
169 pub engine: String,
170 /// The agent's most recently confirmed provider (startup config or a successful providers/set).
171 /// attach does not spawn,
172 /// so the frontend uses this to mark the current provider (the provider identity sent
173 /// with every prompt depends on it).
174 /// The registry holds no process metadata; filled by `Runtime::attach` (the only public exit).
175 pub provider: String,
176 /// Provider configuration acknowledged by this process; absent for unmanaged launches.
177 pub provider_revision: Option<u64>,
178 /// Agent requests still unanswered, for the new owner to answer. Sourced from the inbound
179 /// replay buffer, which may have been flushed in extreme cases.
180 pub pending_agent_requests: Vec<PendingAgentRequest>,
181}
182
183#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
184#[serde(rename_all = "lowercase")]
185pub enum FrameDirection {
186 /// agent -> Runtime (session/update, request_permission, etc.).
187 In,
188 /// Runtime -> agent (session/prompt, etc.).
189 Out,
190}
191
192/// One frame in the journal. `line` is the raw ACP JSON-RPC text (before the Runtime envelope).
193#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
194#[serde(rename_all = "camelCase")]
195pub struct SessionEventFrame {
196 pub sequence: u64,
197 pub recorded_at: i64,
198 pub direction: FrameDirection,
199 pub line: String,
200}
201
202#[derive(Clone, Debug, PartialEq, Eq)]
203pub struct AgentMetadata {
204 pub owner_id: String,
205 /// Which core was started (desktop engine manifest key; Runtime only passes it through). attach
206 /// does not spawn, so the frontend can only learn it here.
207 pub engine: String,
208 pub provider: String,
209 /// Provider configuration acknowledged by this process; absent for unmanaged launches.
210 pub provider_revision: Option<u64>,
211 pub model: String,
212 pub connection: String,
213}
214
215#[derive(Clone, Debug, Serialize, Deserialize)]
216#[serde(tag = "type", rename_all = "camelCase", rename_all_fields = "camelCase")]
217pub enum RuntimeEvent {
218 /// stdio protocol diagnostics; params of outbound providers/set and auth/login are redacted.
219 /// The engine receives the raw secret params; observers and replay records never see them.
220 Wire {
221 agent_id: String,
222 outbound: bool,
223 lines: Vec<String>,
224 },
225 /// ACP frames output by the agent (agent -> app), batched every 16ms, each already carrying the
226 /// `_alwithRuntime` envelope.
227 Inbound {
228 agent_id: String,
229 lines: Vec<String>,
230 },
231 /// A frame the app sent to the agent. Frames entering the session journal carry `session_id`
232 /// and `sequence` (see `AcpNotification`).
233 Outbound {
234 agent_id: String,
235 line: String,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 session_id: Option<String>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 sequence: Option<u64>,
240 },
241 Stderr {
242 agent_id: String,
243 line: String,
244 },
245 /// The agent process has been reaped. There is never a second process running under the same
246 /// agentId (`start` rejects while it is alive),
247 /// so this event is unambiguous: receiving it means "my agent is gone".
248 Exit {
249 agent_id: String,
250 },
251 RunStates {
252 sessions: Vec<SessionRunState>,
253 },
254 /// ACP notification from the agent (`session/update` etc.), already unframed by Runtime;
255 /// coexists with the same frame in `inbound`.
256 ///
257 /// Frames that entered the session journal carry `session_id` and `sequence` (same number as
258 /// the same frame in `sessionEvents`):
259 /// the client uses it to detect missed frames (a sequence gap) and refill via `sessionEvents`.
260 /// Frames not journaled have neither field.
261 AcpNotification {
262 agent_id: String,
263 method: String,
264 params: Value,
265 #[serde(default, skip_serializing_if = "Option::is_none")]
266 session_id: Option<String>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 sequence: Option<u64>,
269 },
270 /// ACP request from the agent (`session/request_permission`, `elicitation/create`, `fs/*`...),
271 /// the app answers with `acpRespond` using the same `id`. Sequence fields as in
272 /// `AcpNotification`.
273 AcpAgentRequest {
274 agent_id: String,
275 id: Value,
276 method: String,
277 params: Value,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 session_id: Option<String>,
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 sequence: Option<u64>,
282 },
283}