Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,63 @@ All paths in the protocol should be absolute
- Run `npm run check` to make sure the json and zod schemas gets generated properly

Never write readme files related to the conversation unless explicitly asked to.

## Conventional Commits

This repository uses **Conventional Commits** for automated releases via release-plz. All commit messages should follow this format:

```
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]
```

### Commit Types

- **feat:** A new feature (triggers minor version bump)
- **fix:** A bug fix (triggers patch version bump)
- **docs:** Documentation only changes
- **style:** Code style changes (formatting, missing semicolons, etc.)
- **refactor:** Code changes that neither fix bugs nor add features
- **perf:** Performance improvements
- **test:** Adding or updating tests
- **chore:** Maintenance tasks, dependency updates, etc.
- **ci:** CI/CD configuration changes
- **build:** Build system or external dependency changes

### Breaking Changes

Add `!` after the type to indicate breaking changes (triggers major version bump):

```
feat!: change API to use async traits
```

Or include `BREAKING CHANGE:` in the footer:

```
feat: redesign conductor protocol

BREAKING CHANGE: conductor now requires explicit capability registration
```

### Examples

```
feat(schema): add support for dynamic proxy chains
fix(unstable): resolve deadlock in message routing
docs(rfd): update README with installation instructions
chore: bump tokio to 1.40
```

### Scope Guidelines

Common scopes for this repository:

- `schema` - Anything that touches the actual generated schema
- `unstable` - Any changes that would only touch unstable features
- `unstable-v2` - Any changes that would only touch v2
- `rust` - Any changes that only touch Rust code and how Rust handles the data with no changes to the schema
- `rfd` - Any changes that only touch RFD documentation
3 changes: 1 addition & 2 deletions agent-client-protocol-schema/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ unstable = [
]
# Protocol v2 is intentionally NOT part of the `unstable` umbrella.
# It introduces a parallel `v2` module and (eventually) a different wire
# version, so it must be opted into explicitly to avoid silently
# changing `ProtocolVersion::LATEST` for `unstable` users.
# version, so it must be opted into explicitly.
unstable_protocol_v2 = []
unstable_auth_methods = []
unstable_cancel_request = []
Expand Down
8 changes: 3 additions & 5 deletions agent-client-protocol-schema/src/v2/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10136,11 +10136,9 @@ mod tests {
}

#[test]
fn protocol_version_v1_constant_is_unchanged_by_feature_flag() {
// Guards against `LATEST` accidentally being re-pointed to V2 in the
// future; the contract is that `LATEST` is always the latest **stable**
// version, even when the v2 draft feature is enabled.
assert_eq!(ProtocolVersion::LATEST, ProtocolVersion::V1);
fn protocol_version_constants_remain_explicit() {
assert_eq!(ProtocolVersion::V1.as_u16(), 1);
assert_eq!(ProtocolVersion::V2.as_u16(), 2);
}

/// `?` bubbles a [`ProtocolConversionError`] into a [`v1::Error`] without
Expand Down
13 changes: 6 additions & 7 deletions agent-client-protocol-schema/src/v2/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
//! Agent Client Protocol version 2 draft types.
//!
//! **EXPERIMENTAL.** This module is gated behind the `unstable_protocol_v2`
//! feature, is not part of the [`unstable`] umbrella, and is **not**
//! advertised by [`crate::ProtocolVersion::LATEST`]. The wire format is
//! currently identical to v1 (the default crate-root types) and the types
//! here exist only as a place to evolve v2 without disturbing the stable v1
//! API. The wire format intentionally diverges from v1 as draft v2 RFDs land.
//! Both the type definitions and the [`conversion`] helpers may change at any
//! time.
//! feature, is not part of the [`unstable`] umbrella, and must be selected
//! explicitly with [`crate::ProtocolVersion::V2`]. The wire format is
//! currently identical to v1 (the default crate-root types) and the types here
//! exist only as a place to evolve v2 without disturbing the stable v1 API. The
//! wire format intentionally diverges from v1 as draft v2 RFDs land. Both the
//! type definitions and the [`conversion`] helpers may change at any time.
//!
//! [`unstable`]: https://docs.rs/crate/agent-client-protocol-schema/latest/features

Expand Down
90 changes: 27 additions & 63 deletions agent-client-protocol-schema/src/version.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
use derive_more::{Display, From};
use schemars::JsonSchema;
use serde::Serialize;
use serde::{Deserialize, Serialize};

/// Protocol version identifier.
///
/// This version is only bumped for breaking changes.
/// Non-breaking changes should be introduced via capabilities.
#[derive(
Debug, Clone, Copy, Serialize, JsonSchema, PartialEq, Eq, PartialOrd, Ord, From, Display,
Debug,
Clone,
Copy,
Serialize,
Deserialize,
JsonSchema,
PartialEq,
Eq,
PartialOrd,
Ord,
From,
Display,
)]
pub struct ProtocolVersion(u16);

impl ProtocolVersion {
/// Version `0` of the protocol.
///
/// This was a pre-release version that shouldn't be used in production.
/// It is used as a fallback for any request whose version cannot be parsed
/// as a valid version, and should likely be treated as unsupported.
/// It should likely be treated as unsupported.
pub const V0: Self = Self(0);
/// Version `1` of the protocol.
///
Expand All @@ -25,16 +35,18 @@ impl ProtocolVersion {
/// Version `2` of the protocol.
///
/// This is an unstable draft used for protocol iteration. It is only
/// available when the `unstable_protocol_v2` feature is enabled and is
/// **not** advertised by [`ProtocolVersion::LATEST`] yet — callers must
/// opt into V2 explicitly.
/// available when the `unstable_protocol_v2` feature is enabled and must
/// be selected explicitly.
#[cfg(feature = "unstable_protocol_v2")]
pub const V2: Self = Self(2);
/// The latest stable supported version of the protocol.
///
/// Currently this is version `1`. Enabling the `unstable_protocol_v2`
/// feature exposes `ProtocolVersion::V2` but does **not** change the
/// value of `LATEST` — v2 will only become the latest once it stabilizes.
/// Currently this is version `1`.
///
/// This shorthand is intentionally unavailable when the
/// `unstable_protocol_v2` feature is enabled, so code that opts into the
/// v2 draft must choose `V1` or `V2` explicitly.
#[cfg(not(feature = "unstable_protocol_v2"))]
pub const LATEST: Self = Self::V1;

/// Returns the numeric protocol version.
Expand All @@ -50,56 +62,6 @@ impl ProtocolVersion {
}
}

use serde::{Deserialize, Deserializer};

impl<'de> Deserialize<'de> for ProtocolVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::{self, Visitor};
use std::fmt;

struct ProtocolVersionVisitor;

impl Visitor<'_> for ProtocolVersionVisitor {
type Value = ProtocolVersion;

fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a protocol version number or string")
}

fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
match u16::try_from(value) {
Ok(value) => Ok(ProtocolVersion(value)),
Err(_) => Err(E::custom(format!("protocol version {value} is too large"))),
}
}

fn visit_str<E>(self, _value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
// Old versions used strings, we consider all of those version 0
Ok(ProtocolVersion::V0)
}

fn visit_string<E>(self, _value: String) -> Result<Self::Value, E>
where
E: de::Error,
{
// Old versions used strings, we consider all of those version 0
Ok(ProtocolVersion::V0)
}
}

deserializer.deserialize_any(ProtocolVersionVisitor)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -112,10 +74,10 @@ mod tests {
}

#[test]
fn test_deserialize_string() {
fn test_deserialize_string_errors() {
let json = "\"1.0.0\"";
let version: ProtocolVersion = serde_json::from_str(json).unwrap();
assert_eq!(version, ProtocolVersion::new(0));
let result: Result<ProtocolVersion, _> = serde_json::from_str(json);
assert!(result.is_err());
}

#[test]
Expand Down Expand Up @@ -143,6 +105,8 @@ mod tests {
fn test_as_u16() {
assert_eq!(ProtocolVersion::V0.as_u16(), 0);
assert_eq!(ProtocolVersion::V1.as_u16(), 1);

#[cfg(not(feature = "unstable_protocol_v2"))]
assert_eq!(ProtocolVersion::LATEST.as_u16(), 1);

#[cfg(feature = "unstable_protocol_v2")]
Expand Down
2 changes: 1 addition & 1 deletion docs/rfds/elicitation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ Clients declare elicitation support during the `initialize` phase via `ClientCap
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"protocolVersion": 1,
"clientCapabilities": {
"fs": {
"readTextFile": true,
Expand Down
7 changes: 3 additions & 4 deletions schema-generator/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,9 @@ fn write_schema(schema_value: &serde_json::Value, schema_dir: &Path, docs_protoc
.unwrap_or_else(|e| panic!("Failed to write {schema_file}: {e}"));

// The version embedded in `meta*.json` reflects the protocol version the
// *schema itself describes*, not `ProtocolVersion::LATEST` (which always
// tracks the latest **stable** version). Generating with the
// `unstable_protocol_v2` feature emits v2-shaped types, so the metadata
// file must advertise version 2 to stay consistent with its contents.
// *schema itself describes*. Generating with the `unstable_protocol_v2`
// feature emits v2-shaped types, so the metadata file must advertise
// version 2 to stay consistent with its contents.
#[cfg(feature = "unstable_protocol_v2")]
let schema_protocol_version = ProtocolVersion::V2;
#[cfg(not(feature = "unstable_protocol_v2"))]
Expand Down