Skip to content

Commit 924a173

Browse files
committed
feat(rust-only): better tolerate malformed optional fields in deserialization
Use `serde_with` helpers to default invalid optional values and skip bad vector entries across protocol types. Sometimes agents or clients send something not quite right. If it is an optional field, we can fallback to best-effort and retain as much information as possible without losing everything.
1 parent ce1ad0f commit 924a173

15 files changed

Lines changed: 654 additions & 65 deletions

File tree

Cargo.lock

Lines changed: 435 additions & 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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ derive_more = { version = "2", features = ["from", "display"] }
5555
schemars = { version = "1" }
5656
serde = { version = "1", features = ["derive", "rc"] }
5757
serde_json = { version = "1", features = ["raw_value"] }
58+
serde_with = { version = "3.18.0", features = ["json", "schemars_1"] }
5859
strum = { version = "0.28", features = ["derive"] }
5960

6061
[lints.rust]

clippy.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
allowed-duplicate-crates = [
2+
"hashbrown",
3+
"indexmap",
4+
"schemars",
5+
]

docs/protocol/draft/schema.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2919,6 +2919,9 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte
29192919
</ResponseField>
29202920
<ResponseField name="input" type={<><span><a href="#availablecommandinput">AvailableCommandInput</a></span><span> | null</span></>} >
29212921
Input for the command if required
2922+
2923+
- Default: `null`
2924+
29222925
</ResponseField>
29232926
<ResponseField name="name" type={"string"} required>
29242927
Command name (e.g., `create_plan`, `research_codebase`).

docs/protocol/schema.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,6 +1305,9 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/exte
13051305
</ResponseField>
13061306
<ResponseField name="input" type={<><span><a href="#availablecommandinput">AvailableCommandInput</a></span><span> | null</span></>} >
13071307
Input for the command if required
1308+
1309+
- Default: `null`
1310+
13081311
</ResponseField>
13091312
<ResponseField name="name" type={"string"} required>
13101313
Command name (e.g., `create_plan`, `research_codebase`).

schema/schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@
442442
"type": "null"
443443
}
444444
],
445+
"default": null,
445446
"description": "Input for the command if required"
446447
},
447448
"name": {

schema/schema.unstable.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,7 @@
789789
"type": "null"
790790
}
791791
],
792+
"default": null,
792793
"description": "Input for the command if required"
793794
},
794795
"name": {

src/agent.rs

Lines changed: 73 additions & 20 deletions
Large diffs are not rendered by default.

src/client.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::{path::PathBuf, sync::Arc};
88
use derive_more::{Display, From};
99
use schemars::JsonSchema;
1010
use serde::{Deserialize, Serialize};
11+
use serde_with::{DefaultOnError, VecSkipError, serde_as};
1112

1213
#[cfg(feature = "unstable_elicitation")]
1314
use crate::elicitation::{
@@ -152,11 +153,13 @@ impl CurrentModeUpdate {
152153
}
153154

154155
/// Session configuration options have been updated.
156+
#[serde_as]
155157
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
156158
#[serde(rename_all = "camelCase")]
157159
#[non_exhaustive]
158160
pub struct ConfigOptionUpdate {
159161
/// The full set of configuration options and their current values.
162+
#[serde_as(deserialize_as = "VecSkipError<_>")]
160163
pub config_options: Vec<SessionConfigOption>,
161164
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
162165
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
@@ -249,6 +252,7 @@ impl SessionInfoUpdate {
249252
///
250253
/// Context window and cost update for a session.
251254
#[cfg(feature = "unstable_session_usage")]
255+
#[serde_as]
252256
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
253257
#[serde(rename_all = "camelCase")]
254258
#[non_exhaustive]
@@ -258,7 +262,8 @@ pub struct UsageUpdate {
258262
/// Total context window size in tokens.
259263
pub size: u64,
260264
/// Cumulative session cost (optional).
261-
#[serde(skip_serializing_if = "Option::is_none")]
265+
#[serde_as(deserialize_as = "DefaultOnError")]
266+
#[serde(default, skip_serializing_if = "Option::is_none")]
262267
pub cost: Option<Cost>,
263268
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
264269
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
@@ -395,11 +400,13 @@ impl ContentChunk {
395400
}
396401

397402
/// Available commands are ready or have changed
403+
#[serde_as]
398404
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
399405
#[serde(rename_all = "camelCase")]
400406
#[non_exhaustive]
401407
pub struct AvailableCommandsUpdate {
402408
/// Commands the agent can execute
409+
#[serde_as(deserialize_as = "VecSkipError<_>")]
403410
pub available_commands: Vec<AvailableCommand>,
404411
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
405412
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
@@ -432,6 +439,7 @@ impl AvailableCommandsUpdate {
432439
}
433440

434441
/// Information about a command.
442+
#[serde_as]
435443
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
436444
#[serde(rename_all = "camelCase")]
437445
#[non_exhaustive]
@@ -441,6 +449,8 @@ pub struct AvailableCommand {
441449
/// Human-readable description of what the command does.
442450
pub description: String,
443451
/// Input for the command if required
452+
#[serde_as(deserialize_as = "DefaultOnError")]
453+
#[serde(default)]
444454
pub input: Option<AvailableCommandInput>,
445455
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
446456
/// metadata to their interactions. Implementations MUST NOT make assumptions about values at
@@ -1473,6 +1483,7 @@ impl TerminalExitStatus {
14731483
/// available features and methods.
14741484
///
14751485
/// See protocol docs: [Client Capabilities](https://agentclientprotocol.com/protocol/initialization#client-capabilities)
1486+
#[serde_as]
14761487
#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
14771488
#[serde(rename_all = "camelCase")]
14781489
#[non_exhaustive]
@@ -1501,6 +1512,7 @@ pub struct ClientCapabilities {
15011512
/// Elicitation capabilities supported by the client.
15021513
/// Determines which elicitation modes the agent may use.
15031514
#[cfg(feature = "unstable_elicitation")]
1515+
#[serde_as(deserialize_as = "DefaultOnError")]
15041516
#[serde(default, skip_serializing_if = "Option::is_none")]
15051517
pub elicitation: Option<ElicitationCapabilities>,
15061518
/// **UNSTABLE**
@@ -1509,14 +1521,16 @@ pub struct ClientCapabilities {
15091521
///
15101522
/// NES (Next Edit Suggestions) capabilities supported by the client.
15111523
#[cfg(feature = "unstable_nes")]
1512-
#[serde(skip_serializing_if = "Option::is_none")]
1524+
#[serde_as(deserialize_as = "DefaultOnError")]
1525+
#[serde(default, skip_serializing_if = "Option::is_none")]
15131526
pub nes: Option<ClientNesCapabilities>,
15141527
/// **UNSTABLE**
15151528
///
15161529
/// This capability is not part of the spec yet, and may be removed or changed at any point.
15171530
///
15181531
/// The position encodings supported by the client, in order of preference.
15191532
#[cfg(feature = "unstable_nes")]
1533+
#[serde_as(deserialize_as = "VecSkipError<_>")]
15201534
#[serde(default, skip_serializing_if = "Vec::is_empty")]
15211535
pub position_encodings: Vec<PositionEncodingKind>,
15221536

src/content.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
1212
use schemars::JsonSchema;
1313
use serde::{Deserialize, Serialize};
14+
use serde_with::{DefaultOnError, VecSkipError, serde_as};
1415

1516
use crate::{IntoOption, Meta};
1617

@@ -59,10 +60,12 @@ pub enum ContentBlock {
5960
}
6061

6162
/// Text provided to or from an LLM.
63+
#[serde_as]
6264
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
6365
#[non_exhaustive]
6466
pub struct TextContent {
65-
#[serde(skip_serializing_if = "Option::is_none")]
67+
#[serde_as(deserialize_as = "DefaultOnError")]
68+
#[serde(default, skip_serializing_if = "Option::is_none")]
6669
pub annotations: Option<Annotations>,
6770
pub text: String,
6871
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
@@ -109,11 +112,13 @@ impl<T: Into<String>> From<T> for ContentBlock {
109112
}
110113

111114
/// An image provided to or from an LLM.
115+
#[serde_as]
112116
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
113117
#[serde(rename_all = "camelCase")]
114118
#[non_exhaustive]
115119
pub struct ImageContent {
116-
#[serde(skip_serializing_if = "Option::is_none")]
120+
#[serde_as(deserialize_as = "DefaultOnError")]
121+
#[serde(default, skip_serializing_if = "Option::is_none")]
117122
pub annotations: Option<Annotations>,
118123
pub data: String,
119124
pub mime_type: String,
@@ -165,11 +170,13 @@ impl ImageContent {
165170
}
166171

167172
/// Audio provided to or from an LLM.
173+
#[serde_as]
168174
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
169175
#[serde(rename_all = "camelCase")]
170176
#[non_exhaustive]
171177
pub struct AudioContent {
172-
#[serde(skip_serializing_if = "Option::is_none")]
178+
#[serde_as(deserialize_as = "DefaultOnError")]
179+
#[serde(default, skip_serializing_if = "Option::is_none")]
173180
pub annotations: Option<Annotations>,
174181
pub data: String,
175182
pub mime_type: String,
@@ -212,10 +219,12 @@ impl AudioContent {
212219
}
213220

214221
/// The contents of a resource, embedded into a prompt or tool call result.
222+
#[serde_as]
215223
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
216224
#[non_exhaustive]
217225
pub struct EmbeddedResource {
218-
#[serde(skip_serializing_if = "Option::is_none")]
226+
#[serde_as(deserialize_as = "DefaultOnError")]
227+
#[serde(default, skip_serializing_if = "Option::is_none")]
219228
pub annotations: Option<Annotations>,
220229
pub resource: EmbeddedResourceResource,
221230
/// The _meta property is reserved by ACP to allow clients and agents to attach additional
@@ -359,11 +368,13 @@ impl BlobResourceContents {
359368
}
360369

361370
/// A resource that the server is capable of reading, included in a prompt or tool call result.
371+
#[serde_as]
362372
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
363373
#[serde(rename_all = "camelCase")]
364374
#[non_exhaustive]
365375
pub struct ResourceLink {
366-
#[serde(skip_serializing_if = "Option::is_none")]
376+
#[serde_as(deserialize_as = "DefaultOnError")]
377+
#[serde(default, skip_serializing_if = "Option::is_none")]
367378
pub annotations: Option<Annotations>,
368379
#[serde(skip_serializing_if = "Option::is_none")]
369380
pub description: Option<String>,
@@ -442,11 +453,13 @@ impl ResourceLink {
442453
}
443454

444455
/// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed
456+
#[serde_as]
445457
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, Default)]
446458
#[serde(rename_all = "camelCase")]
447459
#[non_exhaustive]
448460
pub struct Annotations {
449-
#[serde(skip_serializing_if = "Option::is_none")]
461+
#[serde_as(deserialize_as = "Option<VecSkipError<_>>")]
462+
#[serde(default, skip_serializing_if = "Option::is_none")]
450463
pub audience: Option<Vec<Role>>,
451464
#[serde(skip_serializing_if = "Option::is_none")]
452465
pub last_modified: Option<String>,

0 commit comments

Comments
 (0)