Skip to content

Commit 9567f90

Browse files
authored
Rust: Support all valid JSON RPC ids (#142)
* Rust: Support all valid JSON RPC ids * prep 0.4.6
1 parent d2d1fae commit 9567f90

9 files changed

Lines changed: 88 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 0.4.6 (2025-10-10)
4+
5+
### Protocol
6+
7+
- No changes
8+
9+
### Rust
10+
11+
- Fix: support all valid JSON-RPC ids (int, string, null)
12+
313
## 0.4.5 (2025-10-02)
414

515
### Protocol

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "agent-client-protocol"
33
authors = ["Zed <hi@zed.dev>"]
4-
version = "0.4.5"
4+
version = "0.4.6"
55
edition = "2024"
66
license = "Apache-2.0"
77
description = "A protocol for standardizing communication between code editors and AI coding agents"

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@agentclientprotocol/sdk",
3-
"version": "0.4.5",
3+
"version": "0.4.6",
44
"publishConfig": {
55
"access": "public"
66
},

rust/rpc.rs

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::{
44
rc::Rc,
55
sync::{
66
Arc,
7-
atomic::{AtomicI32, Ordering},
7+
atomic::{AtomicI64, Ordering},
88
},
99
};
1010

@@ -29,8 +29,8 @@ use crate::{Error, StreamReceiver};
2929

3030
pub struct RpcConnection<Local: Side, Remote: Side> {
3131
outgoing_tx: UnboundedSender<OutgoingMessage<Local, Remote>>,
32-
pending_responses: Arc<Mutex<HashMap<i32, PendingResponse>>>,
33-
next_id: AtomicI32,
32+
pending_responses: Arc<Mutex<HashMap<Id, PendingResponse>>>,
33+
next_id: AtomicI64,
3434
broadcast: StreamBroadcast,
3535
}
3636

@@ -81,7 +81,7 @@ where
8181
let this = Self {
8282
outgoing_tx,
8383
pending_responses,
84-
next_id: AtomicI32::new(0),
84+
next_id: AtomicI64::new(0),
8585
broadcast,
8686
};
8787

@@ -112,8 +112,9 @@ where
112112
) -> impl Future<Output = Result<Out, Error>> {
113113
let (tx, rx) = oneshot::channel();
114114
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
115+
let id = Id::Number(id);
115116
self.pending_responses.lock().insert(
116-
id,
117+
id.clone(),
117118
PendingResponse {
118119
deserialize: |value| {
119120
serde_json::from_str::<Out>(value.get())
@@ -129,7 +130,7 @@ where
129130
if self
130131
.outgoing_tx
131132
.unbounded_send(OutgoingMessage::Request {
132-
id,
133+
id: id.clone(),
133134
method: method.into(),
134135
params,
135136
})
@@ -153,7 +154,7 @@ where
153154
mut outgoing_rx: UnboundedReceiver<OutgoingMessage<Local, Remote>>,
154155
mut outgoing_bytes: impl Unpin + AsyncWrite,
155156
incoming_bytes: impl Unpin + AsyncRead,
156-
pending_responses: Arc<Mutex<HashMap<i32, PendingResponse>>>,
157+
pending_responses: Arc<Mutex<HashMap<Id, PendingResponse>>>,
157158
broadcast: StreamSender,
158159
) -> Result<()> {
159160
// TODO: Create nicer abstraction for broadcast
@@ -187,7 +188,7 @@ where
187188
// Request
188189
match Local::decode_request(method, message.params) {
189190
Ok(request) => {
190-
broadcast.incoming_request(id, method, &request);
191+
broadcast.incoming_request(id.clone(), method, &request);
191192
incoming_tx.unbounded_send(IncomingMessage::Request { id, request }).ok();
192193
}
193194
Err(err) => {
@@ -222,7 +223,7 @@ where
222223
pending_response.respond.send(result).ok();
223224
}
224225
} else {
225-
log::error!("received response for unknown request id: {id}");
226+
log::error!("received response for unknown request id: {id:?}");
226227
}
227228
} else if let Some(method) = message.method {
228229
// Notification
@@ -297,31 +298,41 @@ where
297298
}
298299
}
299300

301+
/// JSON RPC Request Id
302+
#[derive(Debug, PartialEq, Clone, Hash, Eq, Deserialize, Serialize, PartialOrd, Ord)]
303+
#[serde(deny_unknown_fields)]
304+
#[serde(untagged)]
305+
pub enum Id {
306+
Null,
307+
Number(i64),
308+
Str(String),
309+
}
310+
300311
#[derive(Deserialize)]
301312
struct RawIncomingMessage<'a> {
302-
id: Option<i32>,
313+
id: Option<Id>,
303314
method: Option<&'a str>,
304315
params: Option<&'a RawValue>,
305316
result: Option<&'a RawValue>,
306317
error: Option<Error>,
307318
}
308319

309320
enum IncomingMessage<Local: Side> {
310-
Request { id: i32, request: Local::InRequest },
321+
Request { id: Id, request: Local::InRequest },
311322
Notification { notification: Local::InNotification },
312323
}
313324

314325
#[derive(Serialize, Deserialize, Clone)]
315326
#[serde(untagged)]
316327
pub enum OutgoingMessage<Local: Side, Remote: Side> {
317328
Request {
318-
id: i32,
329+
id: Id,
319330
method: Arc<str>,
320331
#[serde(skip_serializing_if = "Option::is_none")]
321332
params: Option<Remote::InRequest>,
322333
},
323334
Response {
324-
id: i32,
335+
id: Id,
325336
#[serde(flatten)]
326337
result: ResponseResult<Local::OutResponse>,
327338
},
@@ -400,3 +411,42 @@ pub trait MessageHandler<Local: Side> {
400411
notification: Local::InNotification,
401412
) -> impl Future<Output = Result<(), Error>>;
402413
}
414+
415+
#[cfg(test)]
416+
mod tests {
417+
use super::*;
418+
419+
use serde_json::{Number, Value};
420+
421+
#[test]
422+
fn id_deserialization() {
423+
let id = serde_json::from_value::<Id>(Value::Null).unwrap();
424+
assert_eq!(id, Id::Null);
425+
426+
let id =
427+
serde_json::from_value::<Id>(Value::Number(Number::from_u128(1).unwrap())).unwrap();
428+
assert_eq!(id, Id::Number(1));
429+
430+
let id =
431+
serde_json::from_value::<Id>(Value::Number(Number::from_i128(-1).unwrap())).unwrap();
432+
assert_eq!(id, Id::Number(-1));
433+
434+
let id = serde_json::from_value::<Id>(Value::String("id".to_owned())).unwrap();
435+
assert_eq!(id, Id::Str("id".to_owned()));
436+
}
437+
438+
#[test]
439+
fn id_serialization() {
440+
let id = serde_json::to_value(Id::Null).unwrap();
441+
assert_eq!(id, Value::Null);
442+
443+
let id = serde_json::to_value(Id::Number(1)).unwrap();
444+
assert_eq!(id, Value::Number(Number::from_u128(1).unwrap()));
445+
446+
let id = serde_json::to_value(Id::Number(-1)).unwrap();
447+
assert_eq!(id, Value::Number(Number::from_i128(-1).unwrap()));
448+
449+
let id = serde_json::to_value(Id::Str("id".to_owned())).unwrap();
450+
assert_eq!(id, Value::String("id".to_owned()));
451+
}
452+
}

rust/stream_broadcast.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use serde_json::value::RawValue;
1212

1313
use crate::{
1414
Error,
15-
rpc::{OutgoingMessage, ResponseResult, Side},
15+
rpc::{Id, OutgoingMessage, ResponseResult, Side},
1616
};
1717

1818
/// A message that flows through the RPC stream.
@@ -50,7 +50,7 @@ pub enum StreamMessageContent {
5050
/// A JSON-RPC request message.
5151
Request {
5252
/// The unique identifier for this request.
53-
id: i32,
53+
id: Id,
5454
/// The name of the method being called.
5555
method: Arc<str>,
5656
/// Optional parameters for the method.
@@ -59,7 +59,7 @@ pub enum StreamMessageContent {
5959
/// A JSON-RPC response message.
6060
Response {
6161
/// The ID of the request this response is for.
62-
id: i32,
62+
id: Id,
6363
/// The result of the request (success or error).
6464
result: Result<Option<serde_json::Value>, Error>,
6565
},
@@ -124,12 +124,12 @@ impl StreamSender {
124124
direction: StreamMessageDirection::Outgoing,
125125
message: match message {
126126
OutgoingMessage::Request { id, method, params } => StreamMessageContent::Request {
127-
id: *id,
127+
id: id.clone(),
128128
method: method.clone(),
129129
params: serde_json::to_value(params).ok(),
130130
},
131131
OutgoingMessage::Response { id, result } => StreamMessageContent::Response {
132-
id: *id,
132+
id: id.clone(),
133133
result: match result {
134134
ResponseResult::Result(value) => Ok(serde_json::to_value(value).ok()),
135135
ResponseResult::Error(error) => Err(error.clone()),
@@ -150,7 +150,7 @@ impl StreamSender {
150150
/// Broadcasts an incoming request to all receivers.
151151
pub(crate) fn incoming_request(
152152
&self,
153-
id: i32,
153+
id: Id,
154154
method: impl Into<Arc<str>>,
155155
params: &impl Serialize,
156156
) {
@@ -171,7 +171,7 @@ impl StreamSender {
171171
}
172172

173173
/// Broadcasts an incoming response to all receivers.
174-
pub(crate) fn incoming_response(&self, id: i32, result: Result<Option<&RawValue>, &Error>) {
174+
pub(crate) fn incoming_response(&self, id: Id, result: Result<Option<&RawValue>, &Error>) {
175175
if self.0.receiver_count() == 0 {
176176
return;
177177
}

typescript/acp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ export class ClientSideConnection implements Agent {
690690
export type { AnyMessage } from "./jsonrpc.js";
691691

692692
class Connection {
693-
#pendingResponses: Map<string | number, PendingResponse> = new Map();
693+
#pendingResponses: Map<string | number | null, PendingResponse> = new Map();
694694
#nextRequestId: number = 0;
695695
#requestHandler: RequestHandler;
696696
#notificationHandler: NotificationHandler;

typescript/jsonrpc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,14 @@ export type AnyMessage = AnyRequest | AnyResponse | AnyNotification;
66

77
export type AnyRequest = {
88
jsonrpc: "2.0";
9-
id: string | number;
9+
id: string | number | null;
1010
method: string;
1111
params?: unknown;
1212
};
1313

1414
export type AnyResponse = {
1515
jsonrpc: "2.0";
16-
id: string | number;
16+
id: string | number | null;
1717
} & Result<unknown>;
1818

1919
export type AnyNotification = {

0 commit comments

Comments
 (0)