Skip to main content

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}