Skip to main content

agentos_client/
json_rpc.rs

1//! JSON-RPC 2.0 types used by the ACP session layer.
2//!
3//! Ported from `packages/core/src/json-rpc.ts`. `result`/`params`/`data` are opaque JSON
4//! (`serde_json::Value`). JSON-RPC errors are NOT Rust `Err`; session methods return a
5//! [`JsonRpcResponse`] whose `error` field may be populated.
6
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10/// A JSON-RPC id: a number, a string, or null.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(untagged)]
13pub enum JsonRpcId {
14    Number(i64),
15    String(String),
16    Null,
17}
18
19/// A JSON-RPC 2.0 response. `result` and `error` are mutually exclusive in practice but both are
20/// optional on the wire.
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct JsonRpcResponse {
23    pub jsonrpc: String,
24    pub id: Option<JsonRpcId>,
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub result: Option<Value>,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub error: Option<JsonRpcError>,
29}
30
31/// A JSON-RPC 2.0 error object. `data` may carry an [`AcpTimeoutErrorData`] or arbitrary JSON.
32#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
33pub struct JsonRpcError {
34    pub code: i64,
35    pub message: String,
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub data: Option<Value>,
38}
39
40/// Structured `data` for an ACP timeout error (`kind: "acp_timeout"`).
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
42pub struct AcpTimeoutErrorData {
43    pub kind: String,
44    pub method: String,
45    pub id: Option<JsonRpcId>,
46    #[serde(rename = "timeoutMs")]
47    pub timeout_ms: f64,
48    #[serde(default, rename = "exitCode", skip_serializing_if = "Option::is_none")]
49    pub exit_code: Option<i32>,
50    #[serde(default, skip_serializing_if = "Option::is_none")]
51    pub killed: Option<bool>,
52    #[serde(
53        default,
54        rename = "transportState",
55        skip_serializing_if = "Option::is_none"
56    )]
57    pub transport_state: Option<String>,
58    #[serde(rename = "recentActivity")]
59    pub recent_activity: Vec<String>,
60}
61
62/// Structured `data` for an "unknown session" error (`kind: "unknown_session"`).
63///
64/// Mirrors `UnknownSessionErrorData` in `packages/core/src/json-rpc.ts`. The
65/// sidecar normalizes an adapter's native "no such session" error from
66/// `session/load` into this shape so resume orchestration can distinguish "the
67/// store didn't survive the wake — fall through to a fresh session" from a
68/// transport/timeout error (which must propagate).
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct UnknownSessionErrorData {
71    pub kind: String,
72    /// Optional metadata. The discriminator is `kind` alone — the sidecar's
73    /// normalized error carries only `kind`, so this stays optional to keep the
74    /// sidecar and client contracts aligned with the TS mirror.
75    #[serde(rename = "sessionId", skip_serializing_if = "Option::is_none", default)]
76    pub session_id: Option<String>,
77}
78
79/// Whether a JSON-RPC error's `data` is an [`UnknownSessionErrorData`]
80/// (discriminated by `kind == "unknown_session"`; `sessionId` is optional).
81/// Mirrors the TS `isUnknownSessionErrorData()` discriminator.
82pub fn is_unknown_session(error: &JsonRpcError) -> bool {
83    error
84        .data
85        .as_ref()
86        .and_then(|data| data.as_object())
87        .is_some_and(|data| data.get("kind").and_then(Value::as_str) == Some("unknown_session"))
88}
89
90/// A JSON-RPC 2.0 notification (no id). `params` is opaque JSON.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92pub struct JsonRpcNotification {
93    pub jsonrpc: String,
94    pub method: String,
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub params: Option<Value>,
97}