diff --git a/docs/docs.json b/docs/docs.json index a5e89575..14f2dbd3 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -58,6 +58,7 @@ "protocol/file-system", "protocol/terminals", "protocol/agent-plan", + "protocol/extensibility", "protocol/schema" ] }, diff --git a/docs/protocol/extensibility.mdx b/docs/protocol/extensibility.mdx new file mode 100644 index 00000000..5749587d --- /dev/null +++ b/docs/protocol/extensibility.mdx @@ -0,0 +1,127 @@ +--- +title: "Extensibility" +description: "Adding custom data and capabilities" +--- + +The Agent Client Protocol provides built-in extension mechanisms that allow implementations to add custom functionality while maintaining compatibility with the core protocol. These mechanisms ensure that Agents and Clients can innovate without breaking interoperability. + +## The `_meta` Field + +All types in the protocol include a `_meta` field that implementations can use to attach custom information. This includes requests, responses, notifications, and even nested types like content blocks, tool calls, plan entries, and capability objects. + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "session/prompt", + "params": { + "sessionId": "sess_abc123def456", + "prompt": [ + { + "type": "text", + "text": "Hello, world!" + } + ], + "_meta": { + "zed.dev/debugMode": true + } + } +} +``` + +Implementations **MUST NOT** add any custom fields at the root of a type that's part of the specification. All possible names are reserved for future protocol versions. + +## Extension Methods + +The protocol reserves any method name starting with an underscore (`_`) for custom extensions. This allows implementations to add new functionality without the risk of conflicting with future protocol versions. + +Extension methods follow standard [JSON-RPC 2.0](https://www.jsonrpc.org/specification) semantics: + +- **[Requests](https://www.jsonrpc.org/specification#request_object)** - Include an `id` field and expect a response +- **[Notifications](https://www.jsonrpc.org/specification#notification)** - Omit the `id` field and are one-way (fire-and-forget) + +### Custom Requests + +In addition to the requests specified by the protocol, implementations **MAY** expose and call custom JSON-RPC requests as long as their name starts with an underscore (`_`). + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "_zed.dev/workspace/buffers", + "params": { + "language": "rust" + } +} +``` + +Upon receiveing a custom request, implementations **MUST** respond accordingly with the provided `id`: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "buffers": [ + { "id": 0, "path": "/home/user/project/src/main.rs" }, + { "id": 1, "path": "/home/user/project/src/editor.rs" } + ] + } +} +``` + +If the receiveing end doesn't recognize the custom method name, it should respond with the standard "Method not found" error: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32601, + "message": "Method not found" + } +} +``` + +To avoid such cases, extensions **SHOULD** advertise their [custom capabilities](#advertising-custom-capabilities) so that callers can check their availability first and adapt their interface accordingly. + +### Custom Notifications + +Custom notifications are regular JSON-RPC notifications that start with an underscore (`_`). Like all notifications, they omit the `id` field: + +```json +{ + "jsonrpc": "2.0", + "method": "_zed.dev/file_opened", + "params": { + "path": "/home/user/project/src/editor.rs" + } +} +``` + +Unlike with custom requests, implementations **SHOULD** ignore unrecognized notifications. + +## Advertising Custom Capabilities + +Implementations **SHOULD** use the `_meta` field in capability objects to advertise support for extensions and their methods: + +```json +{ + "jsonrpc": "2.0", + "id": 0, + "result": { + "protocolVersion": 1, + "agentCapabilities": { + "loadSession": true, + "_meta": { + "zed.dev": { + "workspace": true, + "fileNotifications": true + } + } + } + } +} +``` + +This allows implementations to negotiate custom features during initialization without breaking compatibility with standard Clients and Agents. diff --git a/docs/protocol/initialization.mdx b/docs/protocol/initialization.mdx index 405fb9a1..88ce91e1 100644 --- a/docs/protocol/initialization.mdx +++ b/docs/protocol/initialization.mdx @@ -99,6 +99,8 @@ Capabilities are high-level and are not attached to a specific base protocol con Capabilities may specify the availability of protocol methods, notifications, or a subset of their parameters. They may also signal behaviors of the Agent or Client implementation. +Implementations can also [advertise custom capabilities](./extensibility#advertising-custom-capabilities) using the `_meta` field to indicate support for protocol extensions. + ### Client Capabilities The Client **SHOULD** specify whether it supports the following capabilities: diff --git a/docs/protocol/overview.mdx b/docs/protocol/overview.mdx index 487fb251..c340de26 100644 --- a/docs/protocol/overview.mdx +++ b/docs/protocol/overview.mdx @@ -149,8 +149,19 @@ All methods follow standard JSON-RPC 2.0 [error handling](https://www.jsonrpc.or - Errors include an `error` object with `code` and `message` - Notifications never receive responses (success or error) +## Extensibility + +The protocol provides built-in mechanisms for adding custom functionality while maintaining compatibility: + +- Add custom data using `_meta` fields +- Create custom methods with `_method` and `_notification` +- Advertise custom capabilities during initialization + +Learn about [protocol extensibility](./extensibility) to understand how to use these mechanisms. + ## Next Steps - Learn about [Initialization](./initialization) to understand version and capability negotiation - Understand [Session Setup](./session-setup) for creating and loading sessions - Review the [Prompt Turn](./prompt-turn) lifecycle +- Explore [Extensibility](./extensibility) to add custom features diff --git a/docs/protocol/schema.mdx b/docs/protocol/schema.mdx index 08afa666..b24163ea 100644 --- a/docs/protocol/schema.mdx +++ b/docs/protocol/schema.mdx @@ -32,6 +32,9 @@ Specifies which authentication method to use. **Properties:** + + Extension point for implementations + AuthMethodId} @@ -67,6 +70,9 @@ See protocol docs: [Initialization](https://agentclientprotocol.com/protocol/ini **Properties:** + + Extension point for implementations + ClientCapabilities} > Capabilities supported by the client. @@ -89,6 +95,9 @@ See protocol docs: [Initialization](https://agentclientprotocol.com/protocol/ini **Properties:** + + Extension point for implementations + AgentCapabilities} > Capabilities supported by the agent. @@ -135,6 +144,9 @@ See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/promp **Properties:** + + Extension point for implementations + SessionId} @@ -170,6 +182,9 @@ See protocol docs: [Loading Sessions](https://agentclientprotocol.com/protocol/s **Properties:** + + Extension point for implementations + The working directory for this session. @@ -222,6 +237,9 @@ See protocol docs: [Creating a Session](https://agentclientprotocol.com/protocol **Properties:** + + Extension point for implementations + The working directory for this session. Must be an absolute path. @@ -250,6 +268,9 @@ See protocol docs: [Creating a Session](https://agentclientprotocol.com/protocol **Properties:** + + Extension point for implementations + SessionModeState | null} > **UNSTABLE** @@ -291,6 +312,9 @@ See protocol docs: [User Message](https://agentclientprotocol.com/protocol/promp **Properties:** + + Extension point for implementations + ContentBlock[]} required> The blocks of content that compose the user's message. @@ -321,6 +345,9 @@ See protocol docs: [Check for Completion](https://agentclientprotocol.com/protoc **Properties:** + + Extension point for implementations + StopReason} @@ -357,6 +384,9 @@ Only available if the client supports the `fs.readTextFile` capability. **Properties:** + + Extension point for implementations + Maximum number of lines to read. @@ -396,6 +426,9 @@ Only available if the client supports the `fs.writeTextFile` capability. **Properties:** + + Extension point for implementations + The text content to write to the file. @@ -436,6 +469,9 @@ See protocol docs: [Requesting Permission](https://agentclientprotocol.com/proto **Properties:** + + Extension point for implementations + + Extension point for implementations + RequestPermissionOutcome} @@ -508,6 +547,9 @@ See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protoc **Properties:** + + Extension point for implementations + SessionId} @@ -549,6 +591,9 @@ Request to create a new terminal and execute a command. **Properties:** + + Extension point for implementations + "string"[]} > Array of command arguments. @@ -586,6 +631,9 @@ Response containing the ID of the created terminal. **Properties:** + + Extension point for implementations + The unique identifier for the created terminal. @@ -614,6 +662,9 @@ Request to kill a terminal command without releasing the terminal. **Properties:** + + Extension point for implementations + SessionId} @@ -643,6 +694,9 @@ Request to get the current output and status of a terminal. **Properties:** + + Extension point for implementations + SessionId} @@ -662,6 +716,9 @@ Response containing the terminal output and exit status. **Properties:** + + Extension point for implementations + + Extension point for implementations + SessionId} @@ -732,6 +792,9 @@ Request to wait for a terminal command to exit. **Properties:** + + Extension point for implementations + SessionId} @@ -751,6 +814,9 @@ Response containing the exit status of a terminal command. **Properties:** + + Extension point for implementations + The process exit code (may be null if terminated by signal). @@ -774,6 +840,9 @@ See protocol docs: [Agent Capabilities](https://agentclientprotocol.com/protocol **Properties:** + + Extension point for implementations + Whether the agent supports `session/load`. @@ -801,6 +870,9 @@ Optional annotations for the client. The client can use annotations to inform ho **Properties:** + + Extension point for implementations + @@ -813,6 +885,9 @@ Audio provided to or from an LLM. **Properties:** + + Extension point for implementations + + Extension point for implementations + Optional description providing more details about this authentication method. @@ -863,6 +941,9 @@ Information about a command. **Properties:** + + Extension point for implementations + Human-readable description of what the command does. @@ -907,6 +988,9 @@ Binary resource contents. **Properties:** + + Extension point for implementations + @@ -924,6 +1008,9 @@ See protocol docs: [Client Capabilities](https://agentclientprotocol.com/protoco **Properties:** + + Extension point for implementations + FileSystemCapability} > File system capabilities supported by the client. Determines which file operations the agent can request. @@ -965,6 +1052,9 @@ All agents MUST support text content blocks in prompts. + + Extension point for implementations + + + Extension point for implementations + + + Extension point for implementations + + + Extension point for implementations + + + Extension point for implementations + + Extension point for implementations + + + Extension point for implementations + @@ -1140,6 +1248,9 @@ Resource content that can be embedded in a message. + + Extension point for implementations + @@ -1155,6 +1266,9 @@ An environment variable to set when launching an MCP server. **Properties:** + + Extension point for implementations + The name of the environment variable. @@ -1172,6 +1286,9 @@ See protocol docs: [FileSystem](https://agentclientprotocol.com/protocol/initial **Properties:** + + Extension point for implementations + Whether the Client supports `fs/read_text_file` requests. @@ -1193,6 +1310,9 @@ An HTTP header to set when making requests to the MCP server. **Properties:** + + Extension point for implementations + The name of the HTTP header. @@ -1208,6 +1328,9 @@ An image provided to or from an LLM. **Properties:** + + Extension point for implementations + + Extension point for implementations + SessionModeState | null} > **UNSTABLE** @@ -1246,6 +1372,9 @@ MCP capabilities supported by the agent **Properties:** + + Extension point for implementations + Agent supports `McpServer::Http`. @@ -1385,6 +1514,9 @@ An option presented to the user when requesting permission. **Properties:** + + Extension point for implementations + PermissionOptionKind} @@ -1447,6 +1579,9 @@ See protocol docs: [Agent Plan](https://agentclientprotocol.com/protocol/agent-p **Properties:** + + Extension point for implementations + PlanEntry[]} required> The list of tasks to be accomplished. @@ -1467,6 +1602,9 @@ See protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent **Properties:** + + Extension point for implementations + Human-readable description of what this task aims to accomplish. @@ -1546,6 +1684,9 @@ See protocol docs: [Prompt Capabilities](https://agentclientprotocol.com/protoco **Properties:** + + Extension point for implementations + Agent supports `ContentBlock::Audio`. @@ -1590,6 +1731,9 @@ Response containing the contents of a text file. **Properties:** + + Extension point for implementations + ## RequestPermissionOutcome @@ -1639,6 +1783,9 @@ A resource that the server is capable of reading, included in a prompt or tool c **Properties:** + + Extension point for implementations + + Extension point for implementations + + Extension point for implementations + + + Extension point for implementations + + + Extension point for implementations + Replace the content collection. @@ -1924,6 +2083,9 @@ See protocol docs: [Agent Plan](https://agentclientprotocol.com/protocol/agent-p + + Extension point for implementations + PlanEntry[]} required> The list of tasks to be accomplished. @@ -2025,6 +2187,9 @@ Exit status of a terminal command. **Properties:** + + Extension point for implementations + The process exit code (may be null if terminated by signal). @@ -2043,6 +2208,9 @@ Text provided to or from an LLM. **Properties:** + + Extension point for implementations + + Extension point for implementations + @@ -2081,6 +2252,9 @@ See protocol docs: [Tool Calls](https://agentclientprotocol.com/protocol/tool-ca **Properties:** + + Extension point for implementations + + + Extension point for implementations + The new content after modification. @@ -2216,6 +2393,9 @@ See protocol docs: [Following the Agent](https://agentclientprotocol.com/protoco **Properties:** + + Extension point for implementations + Optional line number within the file. @@ -2264,6 +2444,9 @@ See protocol docs: [Updating](https://agentclientprotocol.com/protocol/tool-call **Properties:** + + Extension point for implementations + Replace the content collection. diff --git a/rust/acp.rs b/rust/acp.rs index d2459afb..fb66ed2e 100644 --- a/rust/acp.rs +++ b/rust/acp.rs @@ -54,6 +54,7 @@ mod agent; mod client; mod content; mod error; +mod ext; mod plan; mod rpc; #[cfg(test)] @@ -66,6 +67,7 @@ pub use agent::*; pub use client::*; pub use content::*; pub use error::*; +pub use ext::*; pub use plan::*; pub use stream_broadcast::{ StreamMessage, StreamMessageContent, StreamMessageDirection, StreamReceiver, @@ -232,6 +234,32 @@ impl Agent for ClientSideConnection { Some(ClientNotification::CancelNotification(notification)), ) } + + async fn ext_method( + &self, + method: Arc, + params: Arc, + ) -> Result, Error> { + self.conn + .request( + format!("_{method}"), + Some(ClientRequest::ExtMethodRequest(ExtMethod { + method, + params, + })), + ) + .await + } + + async fn ext_notification(&self, method: Arc, params: Arc) -> Result<(), Error> { + self.conn.notify( + format!("_{method}"), + Some(ClientNotification::ExtNotification(ExtMethod { + method, + params, + })), + ) + } } /// Marker type representing the client side of an ACP connection. @@ -276,7 +304,16 @@ impl Side for ClientSide { TERMINAL_WAIT_FOR_EXIT_METHOD_NAME => serde_json::from_str(params.get()) .map(AgentRequest::WaitForTerminalExitRequest) .map_err(Into::into), - _ => Err(Error::method_not_found()), + _ => { + if let Some(custom_method) = method.strip_prefix('_') { + Ok(AgentRequest::ExtMethodRequest(ExtMethod { + method: custom_method.into(), + params: RawValue::from_string(params.get().to_string())?.into(), + })) + } else { + Err(Error::method_not_found()) + } + } } } @@ -290,7 +327,16 @@ impl Side for ClientSide { SESSION_UPDATE_NOTIFICATION => serde_json::from_str(params.get()) .map(AgentNotification::SessionNotification) .map_err(Into::into), - _ => Err(Error::method_not_found()), + _ => { + if let Some(custom_method) = method.strip_prefix('_') { + Ok(AgentNotification::ExtNotification(ExtMethod { + method: custom_method.into(), + params: RawValue::from_string(params.get().to_string())?.into(), + })) + } else { + Err(Error::method_not_found()) + } + } } } } @@ -330,6 +376,10 @@ impl MessageHandler for T { self.kill_terminal_command(args).await?; Ok(ClientResponse::KillTerminalResponse) } + AgentRequest::ExtMethodRequest(args) => { + let response = self.ext_method(args.method, args.params).await?; + Ok(ClientResponse::ExtMethodResponse(response)) + } } } @@ -338,6 +388,9 @@ impl MessageHandler for T { AgentNotification::SessionNotification(notification) => { self.session_notification(notification).await?; } + AgentNotification::ExtNotification(args) => { + self.ext_notification(args.method, args.params).await?; + } } Ok(()) } @@ -497,6 +550,29 @@ impl Client for AgentSideConnection { Some(AgentNotification::SessionNotification(notification)), ) } + + async fn ext_method( + &self, + method: Arc, + params: Arc, + ) -> Result, Error> { + self.conn + .request( + format!("_{method}"), + Some(AgentRequest::ExtMethodRequest(ExtMethod { method, params })), + ) + .await + } + + async fn ext_notification(&self, method: Arc, params: Arc) -> Result<(), Error> { + self.conn.notify( + format!("_{method}"), + Some(AgentNotification::ExtNotification(ExtMethod { + method, + params, + })), + ) + } } /// Marker type representing the agent side of an ACP connection. @@ -536,7 +612,16 @@ impl Side for AgentSide { SESSION_PROMPT_METHOD_NAME => serde_json::from_str(params.get()) .map(ClientRequest::PromptRequest) .map_err(Into::into), - _ => Err(Error::method_not_found()), + _ => { + if let Some(custom_method) = method.strip_prefix('_') { + Ok(ClientRequest::ExtMethodRequest(ExtMethod { + method: custom_method.into(), + params: RawValue::from_string(params.get().to_string())?.into(), + })) + } else { + Err(Error::method_not_found()) + } + } } } @@ -550,7 +635,16 @@ impl Side for AgentSide { SESSION_CANCEL_METHOD_NAME => serde_json::from_str(params.get()) .map(ClientNotification::CancelNotification) .map_err(Into::into), - _ => Err(Error::method_not_found()), + _ => { + if let Some(custom_method) = method.strip_prefix('_') { + Ok(ClientNotification::ExtNotification(ExtMethod { + method: custom_method.into(), + params: RawValue::from_string(params.get().to_string())?.into(), + })) + } else { + Err(Error::method_not_found()) + } + } } } } @@ -583,6 +677,10 @@ impl MessageHandler for T { let response = self.set_session_mode(args).await?; Ok(AgentResponse::SetSessionModeResponse(response)) } + ClientRequest::ExtMethodRequest(args) => { + let response = self.ext_method(args.method, args.params).await?; + Ok(AgentResponse::ExtMethodResponse(response)) + } } } @@ -591,6 +689,9 @@ impl MessageHandler for T { ClientNotification::CancelNotification(notification) => { self.cancel(notification).await?; } + ClientNotification::ExtNotification(args) => { + self.ext_notification(args.method, args.params).await?; + } } Ok(()) } diff --git a/rust/agent.rs b/rust/agent.rs index 25655c8f..0b9da83e 100644 --- a/rust/agent.rs +++ b/rust/agent.rs @@ -8,7 +8,9 @@ use std::{path::PathBuf, sync::Arc}; use anyhow::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use crate::ext::ExtMethod; use crate::{ClientCapabilities, ContentBlock, Error, ProtocolVersion, SessionId}; /// Defines the interface that all ACP-compliant agents must implement. @@ -114,6 +116,30 @@ pub trait Agent { /// /// See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation) fn cancel(&self, args: CancelNotification) -> impl Future>; + + /// Handles extension method requests from the client. + /// + /// Extension methods provide a way to add custom functionality while maintaining + /// protocol compatibility. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + fn ext_method( + &self, + method: Arc, + params: Arc, + ) -> impl Future, Error>>; + + /// Handles extension notifications from the client. + /// + /// Extension notifications provide a way to send one-way messages for custom functionality + /// while maintaining protocol compatibility. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + fn ext_notification( + &self, + method: Arc, + params: Arc, + ) -> impl Future>; } // Initialize @@ -132,6 +158,9 @@ pub struct InitializeRequest { /// Capabilities supported by the client. #[serde(default)] pub client_capabilities: ClientCapabilities, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Response from the initialize method. @@ -154,6 +183,9 @@ pub struct InitializeResponse { /// Authentication methods supported by the agent. #[serde(default)] pub auth_methods: Vec, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // Authentication @@ -168,6 +200,9 @@ pub struct AuthenticateRequest { /// The ID of the authentication method to use. /// Must be one of the methods advertised in the initialize response. pub method_id: AuthMethodId, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Unique identifier for an authentication method. @@ -185,6 +220,9 @@ pub struct AuthMethod { pub name: String, /// Optional description providing more details about this authentication method. pub description: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // New session @@ -200,6 +238,9 @@ pub struct NewSessionRequest { pub cwd: PathBuf, /// List of MCP (Model Context Protocol) servers the agent should connect to. pub mcp_servers: Vec, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Response from creating a new session. @@ -218,6 +259,9 @@ pub struct NewSessionResponse { /// This field is not part of the spec, and may be removed or changed at any point. #[serde(default, skip_serializing_if = "Option::is_none")] pub modes: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // Load session @@ -237,6 +281,9 @@ pub struct LoadSessionRequest { pub cwd: PathBuf, /// The ID of the session to load. pub session_id: SessionId, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Response from loading an existing session. @@ -248,6 +295,9 @@ pub struct LoadSessionResponse { /// This field is not part of the spec, and may be removed or changed at any point. #[serde(default, skip_serializing_if = "Option::is_none")] pub modes: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // Session modes @@ -260,6 +310,9 @@ pub struct LoadSessionResponse { pub struct SessionModeState { pub current_mode_id: SessionModeId, pub available_modes: Vec, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// **UNSTABLE** @@ -272,6 +325,9 @@ pub struct SessionMode { pub name: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// **UNSTABLE** @@ -296,6 +352,9 @@ impl std::fmt::Display for SessionModeId { pub struct SetSessionModeRequest { pub session_id: SessionId, pub mode_id: SessionModeId, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// **UNSTABLE** @@ -364,6 +423,9 @@ pub struct EnvVariable { pub name: String, /// The value to set for the environment variable. pub value: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// An HTTP header to set when making requests to the MCP server. @@ -374,6 +436,9 @@ pub struct HttpHeader { pub name: String, /// The value to set for the HTTP header. pub value: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // Prompt @@ -403,6 +468,9 @@ pub struct PromptRequest { /// as it avoids extra round-trips and allows the message to include /// pieces of context from sources the agent may not have access to. pub prompt: Vec, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Response from processing a user prompt. @@ -414,6 +482,9 @@ pub struct PromptRequest { pub struct PromptResponse { /// Indicates why the agent stopped processing the turn. pub stop_reason: StopReason, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Reasons why an agent stops processing a prompt turn. @@ -462,6 +533,9 @@ pub struct AgentCapabilities { /// MCP capabilities supported by the agent. #[serde(default)] pub mcp_capabilities: McpCapabilities, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Prompt capabilities supported by the agent in `session/prompt` requests. @@ -476,7 +550,7 @@ pub struct AgentCapabilities { /// the agent can process. /// /// See protocol docs: [Prompt Capabilities](https://agentclientprotocol.com/protocol/initialization#prompt-capabilities) -#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct PromptCapabilities { /// Agent supports [`ContentBlock::Image`]. @@ -491,10 +565,13 @@ pub struct PromptCapabilities { /// in prompt requests for pieces of context that are referenced in the message. #[serde(default)] pub embedded_context: bool, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// MCP capabilities supported by the agent -#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)] +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct McpCapabilities { /// Agent supports [`McpServer::Http`]. @@ -503,6 +580,9 @@ pub struct McpCapabilities { /// Agent supports [`McpServer::Sse`]. #[serde(default)] pub sse: bool, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // Method schema @@ -574,6 +654,7 @@ pub enum ClientRequest { #[cfg(feature = "unstable")] SetSessionModeRequest(SetSessionModeRequest), PromptRequest(PromptRequest), + ExtMethodRequest(ExtMethod), } /// All possible responses that an agent can send to a client. @@ -593,6 +674,7 @@ pub enum AgentResponse { #[cfg(feature = "unstable")] SetSessionModeResponse(SetSessionModeResponse), PromptResponse(PromptResponse), + ExtMethodResponse(#[schemars(with = "serde_json::Value")] Arc), } /// All possible notifications that a client can send to an agent. @@ -606,6 +688,7 @@ pub enum AgentResponse { #[schemars(extend("x-docs-ignore" = true))] pub enum ClientNotification { CancelNotification(CancelNotification), + ExtNotification(ExtMethod), } /// Notification to cancel ongoing operations for a session. @@ -617,6 +700,9 @@ pub enum ClientNotification { pub struct CancelNotification { /// The ID of the session to cancel operations for. pub session_id: SessionId, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } #[cfg(test)] @@ -633,6 +719,7 @@ mod test_serialization { env: vec![EnvVariable { name: "API_KEY".to_string(), value: "secret123".to_string(), + meta: None, }], }; @@ -680,10 +767,12 @@ mod test_serialization { HttpHeader { name: "Authorization".to_string(), value: "Bearer token123".to_string(), + meta: None, }, HttpHeader { name: "Content-Type".to_string(), value: "application/json".to_string(), + meta: None, }, ], }; @@ -731,6 +820,7 @@ mod test_serialization { headers: vec![HttpHeader { name: "X-API-Key".to_string(), value: "apikey456".to_string(), + meta: None, }], }; diff --git a/rust/client.rs b/rust/client.rs index 25be6932..6908f578 100644 --- a/rust/client.rs +++ b/rust/client.rs @@ -8,9 +8,11 @@ use std::{fmt, path::PathBuf, sync::Arc}; use anyhow::Result; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; #[cfg(feature = "unstable")] use crate::SessionModeId; +use crate::ext::ExtMethod; use crate::{ContentBlock, Error, Plan, SessionId, ToolCall, ToolCallUpdate}; /// Defines the interface that ACP-compliant clients must implement. @@ -143,6 +145,32 @@ pub trait Client { &self, args: KillTerminalCommandRequest, ) -> impl Future>; + + /// Handles extension method requests from the agent. + /// + /// Allows the Agent to send an arbitrary request that is not part of the ACP spec. + /// Extension methods provide a way to add custom functionality while maintaining + /// protocol compatibility. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + fn ext_method( + &self, + method: Arc, + params: Arc, + ) -> impl Future, Error>>; + + /// Handles extension notifications from the agent. + /// + /// Allows the Agent to send an arbitrary notification that is not part of the ACP spec. + /// Extension notifications provide a way to send one-way messages for custom functionality + /// while maintaining protocol compatibility. + /// + /// See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + fn ext_notification( + &self, + method: Arc, + params: Arc, + ) -> impl Future>; } // Session updates @@ -160,6 +188,9 @@ pub struct SessionNotification { pub session_id: SessionId, /// The actual update content. pub update: SessionUpdate, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Different types of updates that can be sent during session processing. @@ -208,6 +239,9 @@ pub struct AvailableCommand { pub description: String, /// Input for the command if required pub input: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] @@ -239,6 +273,9 @@ pub struct RequestPermissionRequest { pub tool_call: ToolCallUpdate, /// Available permission options for the user to choose from. pub options: Vec, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// An option presented to the user when requesting permission. @@ -251,6 +288,9 @@ pub struct PermissionOption { pub name: String, /// Hint about the nature of this permission option. pub kind: PermissionOptionKind, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Unique identifier for a permission option. @@ -288,6 +328,9 @@ pub struct RequestPermissionResponse { /// The user's decision on the permission request. // This extra-level is unfortunately needed because the output must be an object pub outcome: RequestPermissionOutcome, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// The outcome of a permission request. @@ -325,6 +368,9 @@ pub struct WriteTextFileRequest { pub path: PathBuf, /// The text content to write to the file. pub content: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // Read text file @@ -346,6 +392,9 @@ pub struct ReadTextFileRequest { /// Maximum number of lines to read. #[serde(default, skip_serializing_if = "Option::is_none")] pub limit: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Response containing the contents of a text file. @@ -353,6 +402,9 @@ pub struct ReadTextFileRequest { #[serde(rename_all = "camelCase")] pub struct ReadTextFileResponse { pub content: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // Terminals @@ -396,6 +448,9 @@ pub struct CreateTerminalRequest { /// specified limit. #[serde(default, skip_serializing_if = "Option::is_none")] pub output_byte_limit: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Response containing the ID of the created terminal. @@ -405,6 +460,9 @@ pub struct CreateTerminalRequest { pub struct CreateTerminalResponse { /// The unique identifier for the created terminal. pub terminal_id: TerminalId, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Request to get the current output and status of a terminal. @@ -416,6 +474,9 @@ pub struct TerminalOutputRequest { pub session_id: SessionId, /// The ID of the terminal to get output from. pub terminal_id: TerminalId, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Response containing the terminal output and exit status. @@ -429,6 +490,9 @@ pub struct TerminalOutputResponse { pub truncated: bool, /// Exit status if the command has completed. pub exit_status: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Request to release a terminal and free its resources. @@ -440,6 +504,9 @@ pub struct ReleaseTerminalRequest { pub session_id: SessionId, /// The ID of the terminal to release. pub terminal_id: TerminalId, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Request to kill a terminal command without releasing the terminal. @@ -451,6 +518,9 @@ pub struct KillTerminalCommandRequest { pub session_id: SessionId, /// The ID of the terminal to kill. pub terminal_id: TerminalId, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Request to wait for a terminal command to exit. @@ -462,6 +532,9 @@ pub struct WaitForTerminalExitRequest { pub session_id: SessionId, /// The ID of the terminal to wait for. pub terminal_id: TerminalId, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Response containing the exit status of a terminal command. @@ -472,6 +545,9 @@ pub struct WaitForTerminalExitResponse { /// The exit status of the terminal command. #[serde(flatten)] pub exit_status: TerminalExitStatus, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Exit status of a terminal command. @@ -482,6 +558,9 @@ pub struct TerminalExitStatus { pub exit_code: Option, /// The signal that terminated the process (may be null if exited normally). pub signal: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // Capabilities @@ -503,6 +582,9 @@ pub struct ClientCapabilities { /// Whether the Client support all `terminal/*` methods. #[serde(default)] pub terminal: bool, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// File system capabilities that a client may support. @@ -517,6 +599,9 @@ pub struct FileSystemCapability { /// Whether the Client supports `fs/write_text_file` requests. #[serde(default)] pub write_text_file: bool, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } // Method schema @@ -596,6 +681,7 @@ pub enum AgentRequest { ReleaseTerminalRequest(ReleaseTerminalRequest), WaitForTerminalExitRequest(WaitForTerminalExitRequest), KillTerminalCommandRequest(KillTerminalCommandRequest), + ExtMethodRequest(ExtMethod), } /// All possible responses that a client can send to an agent. @@ -616,6 +702,7 @@ pub enum ClientResponse { ReleaseTerminalResponse, WaitForTerminalExitResponse(WaitForTerminalExitResponse), KillTerminalResponse, + ExtMethodResponse(#[schemars(with = "serde_json::Value")] Arc), } /// All possible notifications that an agent can send to a client. @@ -627,6 +714,8 @@ pub enum ClientResponse { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(untagged)] #[schemars(extend("x-docs-ignore" = true))] +#[allow(clippy::large_enum_variant)] pub enum AgentNotification { SessionNotification(SessionNotification), + ExtNotification(ExtMethod), } diff --git a/rust/content.rs b/rust/content.rs index 5475510c..7c0089f1 100644 --- a/rust/content.rs +++ b/rust/content.rs @@ -59,6 +59,9 @@ pub struct TextContent { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, pub text: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } impl> From for ContentBlock { @@ -66,6 +69,7 @@ impl> From for ContentBlock { Self::Text(TextContent { annotations: None, text: value.into(), + meta: None, }) } } @@ -80,6 +84,9 @@ pub struct ImageContent { pub mime_type: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub uri: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Audio provided to or from an LLM. @@ -90,6 +97,9 @@ pub struct AudioContent { pub data: String, #[serde(rename = "mimeType")] pub mime_type: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// The contents of a resource, embedded into a prompt or tool call result. @@ -98,6 +108,9 @@ pub struct EmbeddedResource { #[serde(default, skip_serializing_if = "Option::is_none")] pub annotations: Option, pub resource: EmbeddedResourceResource, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Resource content that can be embedded in a message. @@ -115,6 +128,9 @@ pub struct TextResourceContents { pub mime_type: Option, pub text: String, pub uri: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Binary resource contents. @@ -124,6 +140,9 @@ pub struct BlobResourceContents { #[serde(rename = "mimeType", default, skip_serializing_if = "Option::is_none")] pub mime_type: Option, pub uri: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// A resource that the server is capable of reading, included in a prompt or tool call result. @@ -141,6 +160,9 @@ pub struct ResourceLink { #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, pub uri: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Optional annotations for the client. The client can use annotations to inform how objects are used or displayed @@ -156,6 +178,9 @@ pub struct Annotations { pub last_modified: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub priority: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// The sender or recipient of messages and data in a conversation. diff --git a/rust/error.rs b/rust/error.rs index 4469d8cd..6a772492 100644 --- a/rust/error.rs +++ b/rust/error.rs @@ -33,6 +33,9 @@ pub struct Error { /// This may include debugging information or context-specific details. #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } impl Error { @@ -45,6 +48,7 @@ impl Error { code, message, data: None, + meta: None, } } @@ -105,6 +109,9 @@ pub struct ErrorCode { pub code: i32, /// The standard error message for this code. pub message: &'static str, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } impl ErrorCode { @@ -113,24 +120,28 @@ impl ErrorCode { pub const PARSE_ERROR: ErrorCode = ErrorCode { code: -32700, message: "Parse error", + meta: None, }; /// The JSON sent is not a valid Request object. pub const INVALID_REQUEST: ErrorCode = ErrorCode { code: -32600, message: "Invalid Request", + meta: None, }; /// The method does not exist or is not available. pub const METHOD_NOT_FOUND: ErrorCode = ErrorCode { code: -32601, message: "Method not found", + meta: None, }; /// Invalid method parameter(s). pub const INVALID_PARAMS: ErrorCode = ErrorCode { code: -32602, message: "Invalid params", + meta: None, }; /// Internal JSON-RPC error. @@ -138,6 +149,7 @@ impl ErrorCode { pub const INTERNAL_ERROR: ErrorCode = ErrorCode { code: -32603, message: "Internal error", + meta: None, }; /// Authentication is required before this operation can be performed. @@ -145,6 +157,7 @@ impl ErrorCode { pub const AUTH_REQUIRED: ErrorCode = ErrorCode { code: -32000, message: "Authentication required", + meta: None, }; } diff --git a/rust/example_agent.rs b/rust/example_agent.rs index 39d36258..ff439f8d 100644 --- a/rust/example_agent.rs +++ b/rust/example_agent.rs @@ -12,9 +12,10 @@ //! cargo build --example agent && cargo run --example client -- target/debug/examples/agent //! ``` -use std::cell::Cell; +use std::{cell::Cell, sync::Arc}; use agent_client_protocol::{self as acp, Client, SessionNotification}; +use serde_json::{json, value::RawValue}; use tokio::sync::{mpsc, oneshot}; use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _}; @@ -44,6 +45,7 @@ impl acp::Agent for ExampleAgent { protocol_version: acp::V1, agent_capabilities: acp::AgentCapabilities::default(), auth_methods: Vec::new(), + meta: None, }) } @@ -62,6 +64,7 @@ impl acp::Agent for ExampleAgent { Ok(acp::NewSessionResponse { session_id: acp::SessionId(session_id.to_string().into()), modes: None, + meta: None, }) } @@ -70,7 +73,10 @@ impl acp::Agent for ExampleAgent { arguments: acp::LoadSessionRequest, ) -> Result { log::info!("Received load session request {arguments:?}"); - Ok(acp::LoadSessionResponse { modes: None }) + Ok(acp::LoadSessionResponse { + modes: None, + meta: None, + }) } async fn prompt( @@ -85,6 +91,7 @@ impl acp::Agent for ExampleAgent { SessionNotification { session_id: arguments.session_id.clone(), update: acp::SessionUpdate::AgentMessageChunk { content }, + meta: None, }, tx, )) @@ -93,6 +100,7 @@ impl acp::Agent for ExampleAgent { } Ok(acp::PromptResponse { stop_reason: acp::StopReason::EndTurn, + meta: None, }) } @@ -100,6 +108,32 @@ impl acp::Agent for ExampleAgent { log::info!("Received cancel request {args:?}"); Ok(()) } + + async fn ext_method( + &self, + method: std::sync::Arc, + params: Arc, + ) -> Result, acp::Error> { + log::info!( + "Received extension method call: method={}, params={:?}", + method, + params + ); + Ok(serde_json::value::to_raw_value(&json!({"example": "response"}))?.into()) + } + + async fn ext_notification( + &self, + method: std::sync::Arc, + params: Arc, + ) -> Result<(), acp::Error> { + log::info!( + "Received extension notification: method={}, params={:?}", + method, + params + ); + Ok(()) + } } #[tokio::main(flavor = "current_thread")] diff --git a/rust/example_client.rs b/rust/example_client.rs index 6fe7299a..c6432ae7 100644 --- a/rust/example_client.rs +++ b/rust/example_client.rs @@ -12,8 +12,11 @@ //! cargo build --example agent && cargo run --example client -- target/debug/examples/agent //! ``` +use std::sync::Arc; + use agent_client_protocol::{self as acp, Agent}; use anyhow::bail; +use serde_json::value::RawValue; use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt}; struct ExampleClient {} @@ -98,6 +101,22 @@ impl acp::Client for ExampleClient { } Ok(()) } + + async fn ext_method( + &self, + _method: std::sync::Arc, + _params: Arc, + ) -> Result, acp::Error> { + Err(acp::Error::method_not_found()) + } + + async fn ext_notification( + &self, + _method: std::sync::Arc, + _params: Arc, + ) -> Result<(), acp::Error> { + Err(acp::Error::method_not_found()) + } } #[tokio::main(flavor = "current_thread")] @@ -141,12 +160,14 @@ async fn main() -> anyhow::Result<()> { conn.initialize(acp::InitializeRequest { protocol_version: acp::V1, client_capabilities: acp::ClientCapabilities::default(), + meta: None, }) .await?; let response = conn .new_session(acp::NewSessionRequest { mcp_servers: Vec::new(), cwd: std::env::current_dir()?, + meta: None, }) .await?; @@ -157,6 +178,7 @@ async fn main() -> anyhow::Result<()> { .prompt(acp::PromptRequest { session_id: response.session_id.clone(), prompt: vec![line.into()], + meta: None, }) .await; if let Err(e) = result { diff --git a/rust/ext.rs b/rust/ext.rs new file mode 100644 index 00000000..72a0472f --- /dev/null +++ b/rust/ext.rs @@ -0,0 +1,15 @@ +//! Extension types and constants for protocol extensibility. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; +use std::sync::Arc; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(transparent)] +#[schemars(with = "serde_json::Value")] +pub struct ExtMethod { + #[serde(skip)] // this is used for routing, but when serializing we only want the params + pub method: Arc, + pub params: Arc, +} diff --git a/rust/plan.rs b/rust/plan.rs index 068f1fd9..da723a63 100644 --- a/rust/plan.rs +++ b/rust/plan.rs @@ -23,6 +23,9 @@ pub struct Plan { /// When updating a plan, the agent must send a complete list of all entries /// with their current status. The client replaces the entire plan with each update. pub entries: Vec, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// A single entry in the execution plan. @@ -40,6 +43,9 @@ pub struct PlanEntry { pub priority: PlanEntryPriority, /// Current execution status of this task. pub status: PlanEntryStatus, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Priority levels for plan entries. diff --git a/rust/rpc.rs b/rust/rpc.rs index 4d815bd4..40db962a 100644 --- a/rust/rpc.rs +++ b/rust/rpc.rs @@ -94,17 +94,20 @@ where pub fn notify( &self, - method: &'static str, + method: impl Into>, params: Option, ) -> Result<(), Error> { self.outgoing_tx - .unbounded_send(OutgoingMessage::Notification { method, params }) + .unbounded_send(OutgoingMessage::Notification { + method: method.into(), + params, + }) .map_err(|_| Error::internal_error().with_data("failed to send notification")) } pub fn request( &self, - method: &'static str, + method: impl Into>, params: Option, ) -> impl Future> { let (tx, rx) = oneshot::channel(); @@ -125,7 +128,11 @@ where if self .outgoing_tx - .unbounded_send(OutgoingMessage::Request { id, method, params }) + .unbounded_send(OutgoingMessage::Request { + id, + method: method.into(), + params, + }) .is_err() { self.pending_responses.lock().remove(&id); @@ -309,7 +316,7 @@ enum IncomingMessage { pub enum OutgoingMessage { Request { id: i32, - method: &'static str, + method: Arc, #[serde(skip_serializing_if = "Option::is_none")] params: Option, }, @@ -319,7 +326,7 @@ pub enum OutgoingMessage { result: ResponseResult, }, Notification { - method: &'static str, + method: Arc, #[serde(skip_serializing_if = "Option::is_none")] params: Option, }, diff --git a/rust/rpc_tests.rs b/rust/rpc_tests.rs index d416bfca..5e905cd9 100644 --- a/rust/rpc_tests.rs +++ b/rust/rpc_tests.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use serde_json::json; use std::sync::{Arc, Mutex}; use crate::*; @@ -9,15 +10,17 @@ struct TestClient { file_contents: Arc>>, written_files: Arc>>, session_notifications: Arc>>, + extension_notifications: Arc)>>>, } impl TestClient { fn new() -> Self { Self { - permission_responses: Arc::new(Mutex::new(vec![])), + permission_responses: Arc::new(Mutex::new(Vec::new())), file_contents: Arc::new(Mutex::new(std::collections::HashMap::new())), - written_files: Arc::new(Mutex::new(vec![])), - session_notifications: Arc::new(Mutex::new(vec![])), + written_files: Arc::new(Mutex::new(Vec::new())), + session_notifications: Arc::new(Mutex::new(Vec::new())), + extension_notifications: Arc::new(Mutex::new(Vec::new())), } } @@ -30,6 +33,13 @@ impl TestClient { } } +macro_rules! raw_json { + ($($json:tt)+) => {{ + let response = serde_json::json!($($json)+); + serde_json::value::to_raw_value(&response).unwrap().into() + }}; +} + impl Client for TestClient { async fn request_permission( &self, @@ -40,7 +50,10 @@ impl Client for TestClient { let outcome = responses .pop() .unwrap_or(RequestPermissionOutcome::Cancelled); - Ok(RequestPermissionResponse { outcome }) + Ok(RequestPermissionResponse { + outcome, + meta: None, + }) } async fn write_text_file(&self, arguments: WriteTextFileRequest) -> Result<(), Error> { @@ -60,7 +73,10 @@ impl Client for TestClient { .get(&arguments.path) .cloned() .unwrap_or_else(|| "default content".to_string()); - Ok(ReadTextFileResponse { content }) + Ok(ReadTextFileResponse { + content, + meta: None, + }) } async fn session_notification(&self, args: SessionNotification) -> Result<(), Error> { @@ -96,6 +112,32 @@ impl Client for TestClient { ) -> Result { unimplemented!() } + + async fn ext_method( + &self, + method: std::sync::Arc, + params: Arc, + ) -> Result, Error> { + match dbg!(method.as_ref()) { + "example.com/ping" => Ok(raw_json!({ + "response": "pong", + "params": params + })), + _ => Err(Error::method_not_found()), + } + } + + async fn ext_notification( + &self, + method: std::sync::Arc, + params: Arc, + ) -> Result<(), Error> { + self.extension_notifications + .lock() + .unwrap() + .push((method.to_string(), params)); + Ok(()) + } } #[derive(Clone)] @@ -103,6 +145,7 @@ struct TestAgent { sessions: Arc>>, prompts_received: Arc>>, cancellations_received: Arc>>, + extension_notifications: Arc)>>>, } type PromptReceived = (SessionId, Vec); @@ -111,8 +154,9 @@ impl TestAgent { fn new() -> Self { Self { sessions: Arc::new(Mutex::new(std::collections::HashSet::new())), - prompts_received: Arc::new(Mutex::new(vec![])), - cancellations_received: Arc::new(Mutex::new(vec![])), + prompts_received: Arc::new(Mutex::new(Vec::new())), + cancellations_received: Arc::new(Mutex::new(Vec::new())), + extension_notifications: Arc::new(Mutex::new(Vec::new())), } } } @@ -123,6 +167,7 @@ impl Agent for TestAgent { protocol_version: arguments.protocol_version, agent_capabilities: Default::default(), auth_methods: vec![], + meta: None, }) } @@ -139,11 +184,15 @@ impl Agent for TestAgent { Ok(NewSessionResponse { session_id, modes: None, + meta: None, }) } async fn load_session(&self, _: LoadSessionRequest) -> Result { - Ok(LoadSessionResponse { modes: None }) + Ok(LoadSessionResponse { + modes: None, + meta: None, + }) } #[cfg(feature = "unstable")] @@ -161,6 +210,7 @@ impl Agent for TestAgent { .push((arguments.session_id, arguments.prompt)); Ok(PromptResponse { stop_reason: StopReason::EndTurn, + meta: None, }) } @@ -171,6 +221,35 @@ impl Agent for TestAgent { .push(args.session_id); Ok(()) } + + async fn ext_method( + &self, + method: Arc, + params: Arc, + ) -> Result, Error> { + dbg!(); + match dbg!(method.as_ref()) { + "example.com/echo" => { + let response = serde_json::json!({ + "echo": params + }); + Ok(serde_json::value::to_raw_value(&response)?.into()) + } + _ => Err(Error::method_not_found()), + } + } + + async fn ext_notification( + &self, + method: std::sync::Arc, + params: Arc, + ) -> Result<(), Error> { + self.extension_notifications + .lock() + .unwrap() + .push((method.to_string(), params)); + Ok(()) + } } // Helper function to create a bidirectional connection @@ -220,6 +299,7 @@ async fn test_initialize() { .initialize(InitializeRequest { protocol_version: VERSION, client_capabilities: Default::default(), + meta: None, }) .await; @@ -244,6 +324,7 @@ async fn test_basic_session_creation() { .new_session(NewSessionRequest { mcp_servers: vec![], cwd: std::path::PathBuf::from("/test"), + meta: None, }) .await .expect("new_session failed"); @@ -273,6 +354,7 @@ async fn test_bidirectional_file_operations() { path: test_path.clone(), line: None, limit: None, + meta: None, }) .await .expect("read_text_file failed"); @@ -285,6 +367,7 @@ async fn test_bidirectional_file_operations() { session_id: session_id.clone(), path: test_path.clone(), content: "Updated content".to_string(), + meta: None, }) .await; @@ -312,8 +395,10 @@ async fn test_session_notifications() { content: ContentBlock::Text(TextContent { annotations: None, text: "Hello from user".to_string(), + meta: None, }), }, + meta: None, }) .await .expect("session_notification failed"); @@ -325,8 +410,10 @@ async fn test_session_notifications() { content: ContentBlock::Text(TextContent { annotations: None, text: "Hello from agent".to_string(), + meta: None, }), }, + meta: None, }) .await .expect("session_notification failed"); @@ -356,6 +443,7 @@ async fn test_cancel_notification() { agent_conn .cancel(CancelNotification { session_id: session_id.clone(), + meta: None, }) .await .expect("cancel failed"); @@ -396,6 +484,7 @@ async fn test_concurrent_operations() { path, line: None, limit: None, + meta: None, }); read_futures.push(future); } @@ -431,6 +520,7 @@ async fn test_full_conversation_flow() { .new_session(NewSessionRequest { mcp_servers: vec![], cwd: std::path::PathBuf::from("/test"), + meta: None, }) .await .expect("new_session failed"); @@ -441,12 +531,14 @@ async fn test_full_conversation_flow() { let user_prompt = vec![ContentBlock::Text(TextContent { annotations: None, text: "Please analyze the file and summarize it".to_string(), + meta: None, })]; agent_conn .prompt(PromptRequest { session_id: session_id.clone(), prompt: user_prompt, + meta: None, }) .await .expect("prompt failed"); @@ -459,8 +551,10 @@ async fn test_full_conversation_flow() { content: ContentBlock::Text(TextContent { annotations: None, text: "I'll analyze the file for you. ".to_string(), + meta: None, }), }, + meta: None, }) .await .expect("session_notification failed"); @@ -479,10 +573,13 @@ async fn test_full_conversation_flow() { locations: vec![ToolCallLocation { path: std::path::PathBuf::from("/test/data.txt"), line: None, + meta: None, }], raw_input: None, raw_output: None, + meta: None, }), + meta: None, }) .await .expect("session_notification failed"); @@ -498,22 +595,27 @@ async fn test_full_conversation_flow() { locations: Some(vec![ToolCallLocation { path: std::path::PathBuf::from("/test/data.txt"), line: None, + meta: None, }]), ..Default::default() - } + }, + meta: None, }, options: vec![ PermissionOption { id: PermissionOptionId(Arc::from("allow-once")), name: "Allow once".to_string(), kind: PermissionOptionKind::AllowOnce, + meta: None, }, PermissionOption { id: PermissionOptionId(Arc::from("reject-once")), name: "Reject".to_string(), kind: PermissionOptionKind::RejectOnce, + meta: None, }, ], + meta: None, }) .await .expect("request_permission failed"); @@ -536,7 +638,9 @@ async fn test_full_conversation_flow() { status: Some(ToolCallStatus::InProgress), ..Default::default() }, + meta: None, }), + meta: None, }) .await .expect("session_notification failed"); @@ -553,11 +657,14 @@ async fn test_full_conversation_flow() { content: ContentBlock::Text(TextContent { annotations: None, text: "File contents: Lorem ipsum dolor sit amet".to_string(), + meta: None, }), }]), ..Default::default() }, + meta: None, }), + meta: None, }) .await .expect("session_notification failed"); @@ -570,8 +677,10 @@ async fn test_full_conversation_flow() { content: ContentBlock::Text(TextContent { annotations: None, text: "Based on the file contents, here's my summary: The file contains placeholder text commonly used in the printing industry.".to_string(), + meta: None, }), }, + meta: None, }) .await .expect("session_notification failed"); @@ -631,9 +740,10 @@ async fn test_notification_wire_format() { // Test client -> agent notification wire format let outgoing_msg = JsonRpcMessage::wrap(OutgoingMessage::::Notification { - method: "cancel", + method: "cancel".into(), params: Some(ClientNotification::CancelNotification(CancelNotification { session_id: SessionId("test-123".into()), + meta: None, })), }); @@ -652,7 +762,7 @@ async fn test_notification_wire_format() { // Test agent -> client notification wire format let outgoing_msg = JsonRpcMessage::wrap(OutgoingMessage::::Notification { - method: "sessionUpdate", + method: "sessionUpdate".into(), params: Some(AgentNotification::SessionNotification( SessionNotification { session_id: SessionId("test-456".into()), @@ -660,8 +770,10 @@ async fn test_notification_wire_format() { content: ContentBlock::Text(TextContent { annotations: None, text: "Hello".to_string(), + meta: None, }), }, + meta: None, }, )), }); @@ -685,3 +797,85 @@ async fn test_notification_wire_format() { }) ); } + +#[tokio::test] +async fn test_extension_methods_and_notifications() { + let local_set = tokio::task::LocalSet::new(); + local_set + .run_until(async { + let client = TestClient::new(); + let agent = TestAgent::new(); + + // Store references to the client and agent to check notifications later + let client_ref = client.clone(); + let agent_ref = agent.clone(); + + let (client_conn, agent_conn) = create_connection_pair(client, agent).await; + + // Test agent calling client extension method + let client_response = agent_conn + .ext_method("example.com/ping".into(), raw_json!({"data": "test"})) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(client_response).unwrap(), + serde_json::json!({ + "response": "pong", + "params": {"data": "test"} + }) + ); + + // Test client calling agent extension method + let agent_response = client_conn + .ext_method("example.com/echo".into(), raw_json!({"message": "hello"})) + .await + .unwrap(); + + assert_eq!( + serde_json::to_value(agent_response).unwrap(), + serde_json::json!({ + "echo": {"message": "hello"} + }) + ); + + // Test extension notifications + agent_conn + .ext_notification( + "example.com/client/notify".into(), + raw_json!({"info": "client notification"}), + ) + .await + .unwrap(); + + client_conn + .ext_notification( + "example.com/agent/notify".into(), + raw_json!({"info": "agent notification"}), + ) + .await + .unwrap(); + + // Yield to allow notifications to be processed + tokio::task::yield_now().await; + + // Verify client received the notification + let client_notifications = client_ref.extension_notifications.lock().unwrap(); + assert_eq!(client_notifications.len(), 1); + assert_eq!(client_notifications[0].0, "example.com/client/notify"); + assert_eq!( + serde_json::to_value(&client_notifications[0].1).unwrap(), + serde_json::json!({"info": "client notification"}) + ); + + // Verify agent received the notification + let agent_notifications = agent_ref.extension_notifications.lock().unwrap(); + assert_eq!(agent_notifications.len(), 1); + assert_eq!(agent_notifications[0].0, "example.com/agent/notify"); + assert_eq!( + serde_json::to_value(&agent_notifications[0].1).unwrap(), + serde_json::json!({"info": "agent notification"}) + ); + }) + .await; +} diff --git a/rust/stream_broadcast.rs b/rust/stream_broadcast.rs index 930316bb..ecc13819 100644 --- a/rust/stream_broadcast.rs +++ b/rust/stream_broadcast.rs @@ -125,7 +125,7 @@ impl StreamSender { message: match message { OutgoingMessage::Request { id, method, params } => StreamMessageContent::Request { id: *id, - method: (*method).into(), + method: method.clone(), params: serde_json::to_value(params).ok(), }, OutgoingMessage::Response { id, result } => StreamMessageContent::Response { @@ -137,7 +137,7 @@ impl StreamSender { }, OutgoingMessage::Notification { method, params } => { StreamMessageContent::Notification { - method: (*method).into(), + method: method.clone(), params: serde_json::to_value(params).ok(), } } @@ -256,7 +256,7 @@ impl From> for StreamM message: match message { OutgoingMessage::Request { id, method, params } => StreamMessageContent::Request { id, - method: method.into(), + method, params: serde_json::to_value(params).ok(), }, OutgoingMessage::Response { id, result } => StreamMessageContent::Response { @@ -268,7 +268,7 @@ impl From> for StreamM }, OutgoingMessage::Notification { method, params } => { StreamMessageContent::Notification { - method: method.into(), + method, params: serde_json::to_value(params).ok(), } } diff --git a/rust/tool_call.rs b/rust/tool_call.rs index d104fc10..fb5e2240 100644 --- a/rust/tool_call.rs +++ b/rust/tool_call.rs @@ -45,6 +45,9 @@ pub struct ToolCall { /// Raw output returned by the tool. #[serde(default, skip_serializing_if = "Option::is_none")] pub raw_output: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } impl ToolCall { @@ -90,6 +93,9 @@ pub struct ToolCallUpdate { /// Fields being updated. #[serde(flatten)] pub fields: ToolCallUpdateFields, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// Optional fields that can be updated in a tool call. @@ -122,6 +128,9 @@ pub struct ToolCallUpdateFields { /// Update the raw output. #[serde(default, skip_serializing_if = "Option::is_none")] pub raw_output: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// If a given tool call doesn't exist yet, allows for attempting to construct @@ -141,7 +150,9 @@ impl TryFrom for ToolCall { locations, raw_input, raw_output, + meta: _, }, + meta: _, } = update; Ok(Self { @@ -156,6 +167,7 @@ impl TryFrom for ToolCall { locations: locations.unwrap_or_default(), raw_input, raw_output, + meta: None, }) } } @@ -171,6 +183,7 @@ impl From for ToolCallUpdate { locations, raw_input, raw_output, + meta: _, } = value; Self { id, @@ -182,7 +195,9 @@ impl From for ToolCallUpdate { locations: Some(locations), raw_input, raw_output, + meta: None, }, + meta: None, } } } @@ -317,6 +332,9 @@ pub struct Diff { pub old_text: Option, /// The new content after modification. pub new_text: String, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } /// A file location being accessed or modified by a tool. @@ -333,4 +351,7 @@ pub struct ToolCallLocation { /// Optional line number within the file. #[serde(default, skip_serializing_if = "Option::is_none")] pub line: Option, + /// Extension point for implementations + #[serde(skip_serializing_if = "Option::is_none", rename = "_meta")] + pub meta: Option, } diff --git a/schema/schema.json b/schema/schema.json index c6a0e96b..86dea572 100644 --- a/schema/schema.json +++ b/schema/schema.json @@ -3,6 +3,9 @@ "AgentCapabilities": { "description": "Capabilities supported by the agent.\n\nAdvertised during initialization to inform the client about\navailable features and content types.\n\nSee protocol docs: [Agent Capabilities](https://agentclientprotocol.com/protocol/initialization#agent-capabilities)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "loadSession": { "default": false, "description": "Whether the agent supports `session/load`.", @@ -33,6 +36,9 @@ { "$ref": "#/$defs/SessionNotification", "title": "SessionNotification" + }, + { + "title": "ExtNotification" } ], "description": "All possible notifications that an agent can send to a client.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly - use the notification methods on the [`Client`] trait instead.\n\nNotifications do not expect a response.", @@ -71,6 +77,9 @@ { "$ref": "#/$defs/KillTerminalCommandRequest", "title": "KillTerminalCommandRequest" + }, + { + "title": "ExtMethodRequest" } ], "description": "All possible requests that an agent can send to a client.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly - instead, use the methods on the [`Client`] trait.\n\nThis enum encompasses all method calls from agent to client.", @@ -101,6 +110,9 @@ { "$ref": "#/$defs/PromptResponse", "title": "PromptResponse" + }, + { + "title": "ExtMethodResponse" } ], "description": "All possible responses that an agent can send to a client.\n\nThis enum is used internally for routing RPC responses. You typically won't need\nto use this directly - the responses are handled automatically by the connection.\n\nThese are responses to the corresponding ClientRequest variants.", @@ -109,6 +121,9 @@ "Annotations": { "description": "Optional annotations for the client. The client can use annotations to inform how objects are used or displayed", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "audience": { "items": { "$ref": "#/$defs/Role" @@ -128,6 +143,9 @@ "AudioContent": { "description": "Audio provided to or from an LLM.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -151,6 +169,9 @@ "AuthMethod": { "description": "Describes an available authentication method.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "description": { "description": "Optional description providing more details about this authentication method.", "type": ["string", "null"] @@ -174,6 +195,9 @@ "AuthenticateRequest": { "description": "Request parameters for the authenticate method.\n\nSpecifies which authentication method to use.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "methodId": { "$ref": "#/$defs/AuthMethodId", "description": "The ID of the authentication method to use.\nMust be one of the methods advertised in the initialize response." @@ -187,6 +211,9 @@ "AvailableCommand": { "description": "Information about a command.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "description": { "description": "Human-readable description of what the command does.", "type": "string" @@ -229,6 +256,9 @@ "BlobResourceContents": { "description": "Binary resource contents.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "blob": { "type": "string" }, @@ -245,6 +275,9 @@ "CancelNotification": { "description": "Notification to cancel ongoing operations for a session.\n\nSee protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "sessionId": { "$ref": "#/$defs/SessionId", "description": "The ID of the session to cancel operations for." @@ -258,6 +291,9 @@ "ClientCapabilities": { "description": "Capabilities supported by the client.\n\nAdvertised during initialization to inform the agent about\navailable features and methods.\n\nSee protocol docs: [Client Capabilities](https://agentclientprotocol.com/protocol/initialization#client-capabilities)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "fs": { "$ref": "#/$defs/FileSystemCapability", "default": { @@ -279,6 +315,9 @@ { "$ref": "#/$defs/CancelNotification", "title": "CancelNotification" + }, + { + "title": "ExtNotification" } ], "description": "All possible notifications that a client can send to an agent.\n\nThis enum is used internally for routing RPC notifications. You typically won't need\nto use this directly - use the notification methods on the [`Agent`] trait instead.\n\nNotifications do not expect a response.", @@ -309,6 +348,9 @@ { "$ref": "#/$defs/PromptRequest", "title": "PromptRequest" + }, + { + "title": "ExtMethodRequest" } ], "description": "All possible requests that a client can send to an agent.\n\nThis enum is used internally for routing RPC requests. You typically won't need\nto use this directly - instead, use the methods on the [`Agent`] trait.\n\nThis enum encompasses all method calls from client to agent.", @@ -347,6 +389,9 @@ { "title": "KillTerminalResponse", "type": "null" + }, + { + "title": "ExtMethodResponse" } ], "description": "All possible responses that a client can send to an agent.\n\nThis enum is used internally for routing RPC responses. You typically won't need\nto use this directly - the responses are handled automatically by the connection.\n\nThese are responses to the corresponding AgentRequest variants.", @@ -358,6 +403,9 @@ { "description": "Plain text content\n\nAll agents MUST support text content blocks in prompts.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -382,6 +430,9 @@ { "description": "Images for visual context or analysis.\n\nRequires the `image` prompt capability when included in prompts.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -412,6 +463,9 @@ { "description": "Audio data for transcription or analysis.\n\nRequires the `audio` prompt capability when included in prompts.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -439,6 +493,9 @@ { "description": "References to resources that the agent can access.\n\nAll agents MUST support resource links in prompts.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -479,6 +536,9 @@ { "description": "Complete resource contents embedded directly in the message.\n\nPreferred for including context as it avoids extra round-trips.\n\nRequires the `embeddedContext` prompt capability when included in prompts.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -505,6 +565,9 @@ "CreateTerminalRequest": { "description": "Request to create a new terminal and execute a command.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "args": { "description": "Array of command arguments.", "items": { @@ -546,6 +609,9 @@ "CreateTerminalResponse": { "description": "Response containing the ID of the created terminal.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "terminalId": { "description": "The unique identifier for the created terminal.", "type": "string" @@ -559,6 +625,9 @@ "EmbeddedResource": { "description": "The contents of a resource, embedded into a prompt or tool call result.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -592,6 +661,9 @@ "EnvVariable": { "description": "An environment variable to set when launching an MCP server.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "name": { "description": "The name of the environment variable.", "type": "string" @@ -607,6 +679,9 @@ "FileSystemCapability": { "description": "File system capabilities that a client may support.\n\nSee protocol docs: [FileSystem](https://agentclientprotocol.com/protocol/initialization#filesystem)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "readTextFile": { "default": false, "description": "Whether the Client supports `fs/read_text_file` requests.", @@ -623,6 +698,9 @@ "HttpHeader": { "description": "An HTTP header to set when making requests to the MCP server.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "name": { "description": "The name of the HTTP header.", "type": "string" @@ -638,6 +716,9 @@ "ImageContent": { "description": "An image provided to or from an LLM.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -664,6 +745,9 @@ "InitializeRequest": { "description": "Request parameters for the initialize method.\n\nSent by the client to establish connection and negotiate capabilities.\n\nSee protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "clientCapabilities": { "$ref": "#/$defs/ClientCapabilities", "default": { @@ -688,6 +772,9 @@ "InitializeResponse": { "description": "Response from the initialize method.\n\nContains the negotiated protocol version and agent capabilities.\n\nSee protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "agentCapabilities": { "$ref": "#/$defs/AgentCapabilities", "default": { @@ -725,6 +812,9 @@ "KillTerminalCommandRequest": { "description": "Request to kill a terminal command without releasing the terminal.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "sessionId": { "$ref": "#/$defs/SessionId", "description": "The session ID for this request." @@ -742,6 +832,9 @@ "LoadSessionRequest": { "description": "Request parameters for loading an existing session.\n\nOnly available if the Agent supports the `loadSession` capability.\n\nSee protocol docs: [Loading Sessions](https://agentclientprotocol.com/protocol/session-setup#loading-sessions)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "cwd": { "description": "The working directory for this session.", "type": "string" @@ -766,6 +859,9 @@ "LoadSessionResponse": { "description": "Response from loading an existing session.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "modes": { "anyOf": [ { @@ -783,6 +879,9 @@ "McpCapabilities": { "description": "MCP capabilities supported by the agent", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "http": { "default": false, "description": "Agent supports [`McpServer::Http`].", @@ -886,6 +985,9 @@ "NewSessionRequest": { "description": "Request parameters for creating a new session.\n\nSee protocol docs: [Creating a Session](https://agentclientprotocol.com/protocol/session-setup#creating-a-session)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "cwd": { "description": "The working directory for this session. Must be an absolute path.", "type": "string" @@ -906,6 +1008,9 @@ "NewSessionResponse": { "description": "Response from creating a new session.\n\nSee protocol docs: [Creating a Session](https://agentclientprotocol.com/protocol/session-setup#creating-a-session)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "modes": { "anyOf": [ { @@ -930,6 +1035,9 @@ "PermissionOption": { "description": "An option presented to the user when requesting permission.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "kind": { "$ref": "#/$defs/PermissionOptionKind", "description": "Hint about the nature of this permission option." @@ -978,6 +1086,9 @@ "Plan": { "description": "An execution plan for accomplishing complex tasks.\n\nPlans consist of multiple entries representing individual tasks or goals.\nAgents report plans to clients to provide visibility into their execution strategy.\nPlans can evolve during execution as the agent discovers new requirements or completes tasks.\n\nSee protocol docs: [Agent Plan](https://agentclientprotocol.com/protocol/agent-plan)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "entries": { "description": "The list of tasks to be accomplished.\n\nWhen updating a plan, the agent must send a complete list of all entries\nwith their current status. The client replaces the entire plan with each update.", "items": { @@ -992,6 +1103,9 @@ "PlanEntry": { "description": "A single entry in the execution plan.\n\nRepresents a task or goal that the assistant intends to accomplish\nas part of fulfilling the user's request.\nSee protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent-plan#plan-entries)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "content": { "description": "Human-readable description of what this task aims to accomplish.", "type": "string" @@ -1051,6 +1165,9 @@ "PromptCapabilities": { "description": "Prompt capabilities supported by the agent in `session/prompt` requests.\n\nBaseline agent functionality requires support for [`ContentBlock::Text`]\nand [`ContentBlock::ResourceLink`] in prompt requests.\n\nOther variants must be explicitly opted in to.\nCapabilities for different types of content in prompt requests.\n\nIndicates which content types beyond the baseline (text and resource links)\nthe agent can process.\n\nSee protocol docs: [Prompt Capabilities](https://agentclientprotocol.com/protocol/initialization#prompt-capabilities)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "audio": { "default": false, "description": "Agent supports [`ContentBlock::Audio`].", @@ -1072,6 +1189,9 @@ "PromptRequest": { "description": "Request parameters for sending a user prompt to the agent.\n\nContains the user's message and any additional context.\n\nSee protocol docs: [User Message](https://agentclientprotocol.com/protocol/prompt-turn#1-user-message)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "prompt": { "description": "The blocks of content that compose the user's message.\n\nAs a baseline, the Agent MUST support [`ContentBlock::Text`] and [`ContentBlock::ResourceLink`],\nwhile other variants are optionally enabled via [`PromptCapabilities`].\n\nThe Client MUST adapt its interface according to [`PromptCapabilities`].\n\nThe client MAY include referenced pieces of context as either\n[`ContentBlock::Resource`] or [`ContentBlock::ResourceLink`].\n\nWhen available, [`ContentBlock::Resource`] is preferred\nas it avoids extra round-trips and allows the message to include\npieces of context from sources the agent may not have access to.", "items": { @@ -1092,6 +1212,9 @@ "PromptResponse": { "description": "Response from processing a user prompt.\n\nSee protocol docs: [Check for Completion](https://agentclientprotocol.com/protocol/prompt-turn#4-check-for-completion)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "stopReason": { "$ref": "#/$defs/StopReason", "description": "Indicates why the agent stopped processing the turn." @@ -1112,6 +1235,9 @@ "ReadTextFileRequest": { "description": "Request to read content from a text file.\n\nOnly available if the client supports the `fs.readTextFile` capability.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "limit": { "description": "Maximum number of lines to read.", "format": "uint32", @@ -1141,6 +1267,9 @@ "ReadTextFileResponse": { "description": "Response containing the contents of a text file.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "content": { "type": "string" } @@ -1151,6 +1280,9 @@ "ReleaseTerminalRequest": { "description": "Request to release a terminal and free its resources.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "sessionId": { "$ref": "#/$defs/SessionId", "description": "The session ID for this request." @@ -1199,6 +1331,9 @@ "RequestPermissionRequest": { "description": "Request for user permission to execute a tool call.\n\nSent when the agent needs authorization before performing a sensitive operation.\n\nSee protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/tool-calls#requesting-permission)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "options": { "description": "Available permission options for the user to choose from.", "items": { @@ -1223,6 +1358,9 @@ "RequestPermissionResponse": { "description": "Response to a permission request.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "outcome": { "$ref": "#/$defs/RequestPermissionOutcome", "description": "The user's decision on the permission request." @@ -1236,6 +1374,9 @@ "ResourceLink": { "description": "A resource that the server is capable of reading, included in a prompt or tool call result.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -1281,6 +1422,9 @@ "SessionMode": { "description": "**UNSTABLE**\n\nThis type is not part of the spec, and may be removed or changed at any point.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "description": { "type": ["string", "null"] }, @@ -1301,6 +1445,9 @@ "SessionModeState": { "description": "**UNSTABLE**\n\nThis type is not part of the spec, and may be removed or changed at any point.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "availableModes": { "items": { "$ref": "#/$defs/SessionMode" @@ -1317,6 +1464,9 @@ "SessionNotification": { "description": "Notification containing a session update from the agent.\n\nUsed to stream real-time progress and results during prompt processing.\n\nSee protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "sessionId": { "$ref": "#/$defs/SessionId", "description": "The ID of the session this update pertains to." @@ -1379,6 +1529,9 @@ { "description": "Notification that a new tool call has been initiated.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "content": { "description": "Content produced by the tool call.", "items": { @@ -1426,6 +1579,9 @@ { "description": "Update on the status or results of a tool call.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "content": { "description": "Replace the content collection.", "items": { @@ -1487,6 +1643,9 @@ { "description": "The agent's execution plan for complex tasks.\nSee protocol docs: [Agent Plan](https://agentclientprotocol.com/protocol/agent-plan)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "entries": { "description": "The list of tasks to be accomplished.\n\nWhen updating a plan, the agent must send a complete list of all entries\nwith their current status. The client replaces the entire plan with each update.", "items": { @@ -1540,6 +1699,9 @@ "SetSessionModeRequest": { "description": "**UNSTABLE**\n\nThis type is not part of the spec, and may be removed or changed at any point.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "modeId": { "$ref": "#/$defs/SessionModeId" }, @@ -1588,6 +1750,9 @@ "TerminalExitStatus": { "description": "Exit status of a terminal command.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "exitCode": { "description": "The process exit code (may be null if terminated by signal).", "format": "uint32", @@ -1604,6 +1769,9 @@ "TerminalOutputRequest": { "description": "Request to get the current output and status of a terminal.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "sessionId": { "$ref": "#/$defs/SessionId", "description": "The session ID for this request." @@ -1621,6 +1789,9 @@ "TerminalOutputResponse": { "description": "Response containing the terminal output and exit status.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "exitStatus": { "anyOf": [ { @@ -1649,6 +1820,9 @@ "TextContent": { "description": "Text provided to or from an LLM.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "annotations": { "anyOf": [ { @@ -1669,6 +1843,9 @@ "TextResourceContents": { "description": "Text-based resource contents.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "mimeType": { "type": ["string", "null"] }, @@ -1685,6 +1862,9 @@ "ToolCall": { "description": "Represents a tool call that the language model has requested.\n\nTool calls are actions that the agent executes on behalf of the language model,\nsuch as reading files, executing code, or fetching data from external sources.\n\nSee protocol docs: [Tool Calls](https://agentclientprotocol.com/protocol/tool-calls)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "content": { "description": "Content produced by the tool call.", "items": { @@ -1746,6 +1926,9 @@ { "description": "File modification shown as a diff.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "newText": { "description": "The new content after modification.", "type": "string" @@ -1789,6 +1972,9 @@ "ToolCallLocation": { "description": "A file location being accessed or modified by a tool.\n\nEnables clients to implement \"follow-along\" features that track\nwhich files the agent is working with in real-time.\n\nSee protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/tool-calls#following-the-agent)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "line": { "description": "Optional line number within the file.", "format": "uint32", @@ -1831,6 +2017,9 @@ "ToolCallUpdate": { "description": "An update to an existing tool call.\n\nUsed to report progress and results as tools execute. All fields except\nthe tool call ID are optional - only changed fields need to be included.\n\nSee protocol docs: [Updating](https://agentclientprotocol.com/protocol/tool-calls#updating)", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "content": { "description": "Replace the content collection.", "items": { @@ -1943,6 +2132,9 @@ "WaitForTerminalExitRequest": { "description": "Request to wait for a terminal command to exit.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "sessionId": { "$ref": "#/$defs/SessionId", "description": "The session ID for this request." @@ -1960,6 +2152,9 @@ "WaitForTerminalExitResponse": { "description": "Response containing the exit status of a terminal command.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "exitCode": { "description": "The process exit code (may be null if terminated by signal).", "format": "uint32", @@ -1978,6 +2173,9 @@ "WriteTextFileRequest": { "description": "Request to write content to a text file.\n\nOnly available if the client supports the `fs.writeTextFile` capability.", "properties": { + "_meta": { + "description": "Extension point for implementations" + }, "content": { "description": "The text content to write to the file.", "type": "string" diff --git a/typescript/acp.test.ts b/typescript/acp.test.ts index 76a0fd35..abe83767 100644 --- a/typescript/acp.test.ts +++ b/typescript/acp.test.ts @@ -544,4 +544,226 @@ describe("Connection", () => { expect(response.authMethods).toHaveLength(1); expect(response.authMethods?.[0].id).toBe("oauth"); }); + + it("handles extension methods and notifications", async () => { + const extensionLog: string[] = []; + + // Create client with extension method support + class TestClient implements Client { + async writeTextFile( + _: WriteTextFileRequest, + ): Promise { + return null; + } + async readTextFile( + _: ReadTextFileRequest, + ): Promise { + return { content: "test" }; + } + async requestPermission( + _: RequestPermissionRequest, + ): Promise { + return { + outcome: { + outcome: "selected", + optionId: "allow", + }, + }; + } + async sessionUpdate(_: SessionNotification): Promise { + // no-op + } + async extMethod( + method: string, + params: Record, + ): Promise> { + if (method === "example.com/ping") { + return { response: "pong", params }; + } + throw new Error(`Unknown method: ${method}`); + } + async extNotification( + method: string, + params: Record, + ): Promise { + extensionLog.push(`client extNotification: ${method}`); + } + } + + // Create agent with extension method support + class TestAgent implements Agent { + async initialize(_: InitializeRequest): Promise { + return { + protocolVersion: PROTOCOL_VERSION, + agentCapabilities: { loadSession: false }, + }; + } + async newSession(_: NewSessionRequest): Promise { + return { sessionId: "test-session" }; + } + async authenticate(_: AuthenticateRequest): Promise { + // no-op + } + async prompt(_: PromptRequest): Promise { + return { stopReason: "end_turn" }; + } + async cancel(_: CancelNotification): Promise { + // no-op + } + async extMethod( + method: string, + params: Record, + ): Promise> { + if (method === "example.com/echo") { + return { echo: params }; + } + throw new Error(`Unknown method: ${method}`); + } + async extNotification( + method: string, + params: Record, + ): Promise { + extensionLog.push(`agent extNotification: ${method}`); + } + } + + const agentConnection = new ClientSideConnection( + () => new TestClient(), + clientToAgent.writable, + agentToClient.readable, + ); + + const clientConnection = new AgentSideConnection( + () => new TestAgent(), + agentToClient.writable, + clientToAgent.readable, + ); + + // Test agent calling client extension method + const clientResponse = await clientConnection.extMethod( + "example.com/ping", + { + data: "test", + }, + ); + expect(clientResponse).toEqual({ + response: "pong", + params: { data: "test" }, + }); + + // Test client calling agent extension method + const agentResponse = await agentConnection.extMethod("example.com/echo", { + message: "hello", + }); + expect(agentResponse).toEqual({ echo: { message: "hello" } }); + + // Test extension notifications + await clientConnection.extNotification("example.com/client/notify", { + info: "client notification", + }); + await agentConnection.extNotification("example.com/agent/notify", { + info: "agent notification", + }); + + // Wait a bit for async handlers + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Verify notifications were logged + expect(extensionLog).toContain( + "client extNotification: example.com/client/notify", + ); + expect(extensionLog).toContain( + "agent extNotification: example.com/agent/notify", + ); + }); + + it("handles optional extension methods correctly", async () => { + // Create client WITHOUT extension methods + class TestClientWithoutExtensions implements Client { + async writeTextFile( + _: WriteTextFileRequest, + ): Promise { + return null; + } + async readTextFile( + _: ReadTextFileRequest, + ): Promise { + return { content: "test" }; + } + async requestPermission( + _: RequestPermissionRequest, + ): Promise { + return { + outcome: { + outcome: "selected", + optionId: "allow", + }, + }; + } + async sessionUpdate(_: SessionNotification): Promise { + // no-op + } + // Note: No extMethod or extNotification implemented + } + + // Create agent WITHOUT extension methods + class TestAgentWithoutExtensions implements Agent { + async initialize(_: InitializeRequest): Promise { + return { + protocolVersion: PROTOCOL_VERSION, + agentCapabilities: { loadSession: false }, + }; + } + async newSession(_: NewSessionRequest): Promise { + return { sessionId: "test-session" }; + } + async authenticate(_: AuthenticateRequest): Promise { + // no-op + } + async prompt(_: PromptRequest): Promise { + return { stopReason: "end_turn" }; + } + async cancel(_: CancelNotification): Promise { + // no-op + } + // Note: No extMethod or extNotification implemented + } + + const agentConnection = new ClientSideConnection( + () => new TestClientWithoutExtensions(), + clientToAgent.writable, + agentToClient.readable, + ); + + const clientConnection = new AgentSideConnection( + () => new TestAgentWithoutExtensions(), + agentToClient.writable, + clientToAgent.readable, + ); + + // Test that calling extension methods on connections without them throws method not found + try { + await clientConnection.extMethod("example.com/ping", { data: "test" }); + expect.fail("Should have thrown method not found error"); + } catch (error: any) { + expect(error.code).toBe(-32601); // Method not found + expect(error.data.method).toBe("_example.com/ping"); // Should show full method name with underscore + } + + try { + await agentConnection.extMethod("example.com/echo", { message: "hello" }); + expect.fail("Should have thrown method not found error"); + } catch (error: any) { + expect(error.code).toBe(-32601); // Method not found + expect(error.data.method).toBe("_example.com/echo"); // Should show full method name with underscore + } + + // Notifications should be ignored when not implemented (no error thrown) + await clientConnection.extNotification("example.com/notify", { + info: "test", + }); + await agentConnection.extNotification("example.com/notify", { + info: "test", + }); + }); }); diff --git a/typescript/acp.ts b/typescript/acp.ts index 5481e254..39c5bc35 100644 --- a/typescript/acp.ts +++ b/typescript/acp.ts @@ -34,7 +34,7 @@ export class AgentSideConnection { ) { const agent = toAgent(this); - const handler = async ( + const requestHandler = async ( method: string, params: unknown, ): Promise => { @@ -77,16 +77,49 @@ export class AgentSideConnection { const validatedParams = schema.promptRequestSchema.parse(params); return agent.prompt(validatedParams as schema.PromptRequest); } + default: + if (method.startsWith("_")) { + if (!agent.extMethod) { + throw RequestError.methodNotFound(method); + } + return agent.extMethod( + method.substring(1), + params as Record, + ); + } + throw RequestError.methodNotFound(method); + } + }; + + const notificationHandler = async ( + method: string, + params: unknown, + ): Promise => { + switch (method) { case schema.AGENT_METHODS.session_cancel: { const validatedParams = schema.cancelNotificationSchema.parse(params); return agent.cancel(validatedParams as schema.CancelNotification); } default: + if (method.startsWith("_")) { + if (!agent.extNotification) { + return; + } + return agent.extNotification( + method.substring(1), + params as Record, + ); + } throw RequestError.methodNotFound(method); } }; - this.#connection = new Connection(handler, input, output); + this.#connection = new Connection( + requestHandler, + notificationHandler, + input, + output, + ); } /** @@ -190,6 +223,30 @@ export class AgentSideConnection { this.#connection, ); } + + /** + * Extension method + * + * Allows the Agent to send an arbitrary request that is not part of the ACP spec. + */ + async extMethod( + method: string, + params: Record, + ): Promise> { + return await this.#connection.sendRequest(`_${method}`, params); + } + + /** + * Extension notification + * + * Allows the Agent to send an arbitrary notification that is not part of the ACP spec. + */ + async extNotification( + method: string, + params: Record, + ): Promise { + return await this.#connection.sendNotification(`_${method}`, params); + } } /** @@ -323,7 +380,7 @@ export class ClientSideConnection implements Agent { ) { const client = toClient(this); - const handler = async ( + const requestHandler = async ( method: string, params: unknown, ): Promise => { @@ -349,13 +406,6 @@ export class ClientSideConnection implements Agent { validatedParams as schema.RequestPermissionRequest, ); } - case schema.CLIENT_METHODS.session_update: { - const validatedParams = - schema.sessionNotificationSchema.parse(params); - return client.sessionUpdate( - validatedParams as schema.SessionNotification, - ); - } case schema.CLIENT_METHODS.terminal_create: { const validatedParams = schema.createTerminalRequestSchema.parse(params); @@ -392,11 +442,55 @@ export class ClientSideConnection implements Agent { ); } default: + // Handle extension methods (any method starting with '_') + if (method.startsWith("_")) { + const customMethod = method.substring(1); + if (!client.extMethod) { + throw RequestError.methodNotFound(method); + } + return client.extMethod( + customMethod, + params as Record, + ); + } + throw RequestError.methodNotFound(method); + } + }; + + const notificationHandler = async ( + method: string, + params: unknown, + ): Promise => { + switch (method) { + case schema.CLIENT_METHODS.session_update: { + const validatedParams = + schema.sessionNotificationSchema.parse(params); + return client.sessionUpdate( + validatedParams as schema.SessionNotification, + ); + } + default: + // Handle extension notifications (any method starting with '_') + if (method.startsWith("_")) { + const customMethod = method.substring(1); + if (!client.extNotification) { + return; + } + return client.extNotification( + customMethod, + params as Record, + ); + } throw RequestError.methodNotFound(method); } }; - this.#connection = new Connection(handler, input, output); + this.#connection = new Connection( + requestHandler, + notificationHandler, + input, + output, + ); } /** @@ -538,6 +632,30 @@ export class ClientSideConnection implements Agent { params, ); } + + /** + * Extension method + * + * Allows the Client to send an arbitrary request that is not part of the ACP spec. + */ + async extMethod( + method: string, + params: Record, + ): Promise> { + return await this.#connection.sendRequest(`_${method}`, params); + } + + /** + * Extension notification + * + * Allows the Client to send an arbitrary notification that is not part of the ACP spec. + */ + async extNotification( + method: string, + params: Record, + ): Promise { + return await this.#connection.sendNotification(`_${method}`, params); + } } type AnyMessage = AnyRequest | AnyResponse | AnyNotification; @@ -579,22 +697,26 @@ type PendingResponse = { reject: (error: ErrorResponse) => void; }; -type MethodHandler = (method: string, params: unknown) => Promise; +type RequestHandler = (method: string, params: unknown) => Promise; +type NotificationHandler = (method: string, params: unknown) => Promise; class Connection { #pendingResponses: Map = new Map(); #nextRequestId: number = 0; - #handler: MethodHandler; + #requestHandler: RequestHandler; + #notificationHandler: NotificationHandler; #peerInput: WritableStream; #writeQueue: Promise = Promise.resolve(); #textEncoder: TextEncoder; constructor( - handler: MethodHandler, + requestHandler: RequestHandler, + notificationHandler: NotificationHandler, peerInput: WritableStream, peerOutput: ReadableStream, ) { - this.#handler = handler; + this.#requestHandler = requestHandler; + this.#notificationHandler = notificationHandler; this.#peerInput = peerInput; this.#textEncoder = new TextEncoder(); this.#receive(peerOutput); @@ -654,7 +776,7 @@ class Connection { async #processMessage(message: AnyMessage) { if ("method" in message && "id" in message) { // It's a request - const response = await this.#tryCallHandler( + const response = await this.#tryCallRequestHandler( message.method, message.params, ); @@ -669,7 +791,7 @@ class Connection { }); } else if ("method" in message) { // It's a notification - const response = await this.#tryCallHandler( + const response = await this.#tryCallNotificationHandler( message.method, message.params, ); @@ -684,12 +806,12 @@ class Connection { } } - async #tryCallHandler( + async #tryCallRequestHandler( method: string, params: unknown, ): Promise> { try { - const result = await this.#handler(method, params); + const result = await this.#requestHandler(method, params); return { result: result ?? null }; } catch (error: unknown) { if (error instanceof RequestError) { @@ -723,6 +845,45 @@ class Connection { } } + async #tryCallNotificationHandler( + method: string, + params: unknown, + ): Promise> { + try { + await this.#notificationHandler(method, params); + return { result: null }; + } catch (error: unknown) { + if (error instanceof RequestError) { + return error.toResult(); + } + + if (error instanceof z.ZodError) { + return RequestError.invalidParams(error.format()).toResult(); + } + + let details; + + if (error instanceof Error) { + details = error.message; + } else if ( + typeof error === "object" && + error != null && + "message" in error && + typeof error.message === "string" + ) { + details = error.message; + } + + try { + return RequestError.internalError( + details ? JSON.parse(details) : {}, + ).toResult(); + } catch (_err) { + return RequestError.internalError({ details }).toResult(); + } + } + } + #handleResponse(response: AnyResponse) { const pendingResponse = this.#pendingResponses.get(response.id); if (pendingResponse) { @@ -976,6 +1137,29 @@ export interface Client { * @see {@link https://agentclientprotocol.com/protocol/terminals#killing-commands | Killing Commands} */ killTerminal?(params: schema.KillTerminalCommandRequest): Promise; + + /** + * Extension method + * + * Allows the Agent to send an arbitrary request that is not part of the ACP spec. + * + * To help avoid conflicts, it's a good practice to prefix extension + * methods with a unique identifier such as domain name. + */ + extMethod?( + method: string, + params: Record, + ): Promise>; + + /** + * Extension notification + * + * Allows the Agent to send an arbitrary notification that is not part of the ACP spec. + */ + extNotification?( + method: string, + params: Record, + ): Promise; } /** @@ -1080,4 +1264,27 @@ export interface Agent { * See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation) */ cancel(params: schema.CancelNotification): Promise; + + /** + * Extension method + * + * Allows the Client to send an arbitrary request that is not part of the ACP spec. + * + * To help avoid conflicts, it's a good practice to prefix extension + * methods with a unique identifier such as domain name. + */ + extMethod?( + method: string, + params: Record, + ): Promise>; + + /** + * Extension notification + * + * Allows the Client to send an arbitrary notification that is not part of the ACP spec. + */ + extNotification?( + method: string, + params: Record, + ): Promise; } diff --git a/typescript/schema.ts b/typescript/schema.ts index 0c6a3fea..6809f055 100644 --- a/typescript/schema.ts +++ b/typescript/schema.ts @@ -48,7 +48,8 @@ export type ClientRequest = | TerminalOutputRequest | ReleaseTerminalRequest | WaitForTerminalExitRequest - | KillTerminalCommandRequest; + | KillTerminalCommandRequest + | ExtMethodRequest; /** * Content produced by a tool call. * @@ -77,11 +78,23 @@ export type ToolCallContent = */ content: | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; text: string; type: "text"; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; data: string; mimeType: string; @@ -89,12 +102,24 @@ export type ToolCallContent = uri?: string | null; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; data: string; mimeType: string; type: "audio"; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; description?: string | null; mimeType?: string | null; @@ -105,6 +130,12 @@ export type ToolCallContent = uri: string; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; resource: EmbeddedResourceResource; type: "resource"; @@ -112,6 +143,12 @@ export type ToolCallContent = type: "content"; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The new content after modification. */ @@ -184,7 +221,8 @@ export type ClientResponse = | TerminalOutputResponse | ReleaseTerminalResponse | WaitForTerminalExitResponse - | KillTerminalResponse; + | KillTerminalResponse + | ExtMethodResponse; export type WriteTextFileResponse = null; export type ReleaseTerminalResponse = null; export type KillTerminalResponse = null; @@ -197,7 +235,7 @@ export type KillTerminalResponse = null; * Notifications do not expect a response. */ /** @internal */ -export type ClientNotification = CancelNotification; +export type ClientNotification = CancelNotification | ExtNotification; /** * All possible requests that a client can send to an agent. * @@ -213,7 +251,8 @@ export type AgentRequest = | NewSessionRequest | LoadSessionRequest | SetSessionModeRequest - | PromptRequest; + | PromptRequest + | ExtMethodRequest1; /** * Configuration for connecting to an MCP (Model Context Protocol) server. * @@ -296,11 +335,23 @@ export type SessionId = string; */ export type ContentBlock = | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; text: string; type: "text"; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; data: string; mimeType: string; @@ -308,12 +359,24 @@ export type ContentBlock = uri?: string | null; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; data: string; mimeType: string; type: "audio"; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; description?: string | null; mimeType?: string | null; @@ -324,6 +387,12 @@ export type ContentBlock = uri: string; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; annotations?: Annotations | null; resource: EmbeddedResourceResource; type: "resource"; @@ -343,7 +412,8 @@ export type AgentResponse = | NewSessionResponse | LoadSessionResponse | SetSessionModeResponse - | PromptResponse; + | PromptResponse + | ExtMethodResponse1; export type AuthenticateResponse = null; /** * All possible notifications that an agent can send to a client. @@ -354,7 +424,7 @@ export type AuthenticateResponse = null; * Notifications do not expect a response. */ /** @internal */ -export type AgentNotification = SessionNotification; +export type AgentNotification = SessionNotification | ExtNotification1; export type AvailableCommandInput = UnstructuredCommandInput; /** @@ -363,6 +433,12 @@ export type AvailableCommandInput = UnstructuredCommandInput; * Only available if the client supports the `fs.writeTextFile` capability. */ export interface WriteTextFileRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The text content to write to the file. */ @@ -382,6 +458,12 @@ export interface WriteTextFileRequest { * Only available if the client supports the `fs.readTextFile` capability. */ export interface ReadTextFileRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Maximum number of lines to read. */ @@ -407,6 +489,12 @@ export interface ReadTextFileRequest { * See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/tool-calls#requesting-permission) */ export interface RequestPermissionRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Available permission options for the user to choose from. */ @@ -421,6 +509,12 @@ export interface RequestPermissionRequest { * An option presented to the user when requesting permission. */ export interface PermissionOption { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Hint about the nature of this permission option. */ @@ -438,6 +532,12 @@ export interface PermissionOption { * Details about the tool call requiring permission. */ export interface ToolCallUpdate { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Replace the content collection. */ @@ -479,6 +579,12 @@ export interface ToolCallUpdate { * Optional annotations for the client. The client can use annotations to inform how objects are used or displayed */ export interface Annotations { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; audience?: Role[] | null; lastModified?: string | null; priority?: number | null; @@ -487,6 +593,12 @@ export interface Annotations { * Text-based resource contents. */ export interface TextResourceContents { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; mimeType?: string | null; text: string; uri: string; @@ -495,6 +607,12 @@ export interface TextResourceContents { * Binary resource contents. */ export interface BlobResourceContents { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; blob: string; mimeType?: string | null; uri: string; @@ -508,6 +626,12 @@ export interface BlobResourceContents { * See protocol docs: [Following the Agent](https://agentclientprotocol.com/protocol/tool-calls#following-the-agent) */ export interface ToolCallLocation { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Optional line number within the file. */ @@ -521,6 +645,12 @@ export interface ToolCallLocation { * Request to create a new terminal and execute a command. */ export interface CreateTerminalRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Array of command arguments. */ @@ -557,6 +687,12 @@ export interface CreateTerminalRequest { * An environment variable to set when launching an MCP server. */ export interface EnvVariable { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The name of the environment variable. */ @@ -570,6 +706,12 @@ export interface EnvVariable { * Request to get the current output and status of a terminal. */ export interface TerminalOutputRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The session ID for this request. */ @@ -583,6 +725,12 @@ export interface TerminalOutputRequest { * Request to release a terminal and free its resources. */ export interface ReleaseTerminalRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The session ID for this request. */ @@ -596,6 +744,12 @@ export interface ReleaseTerminalRequest { * Request to wait for a terminal command to exit. */ export interface WaitForTerminalExitRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The session ID for this request. */ @@ -609,6 +763,12 @@ export interface WaitForTerminalExitRequest { * Request to kill a terminal command without releasing the terminal. */ export interface KillTerminalCommandRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The session ID for this request. */ @@ -618,16 +778,31 @@ export interface KillTerminalCommandRequest { */ terminalId: string; } +export interface ExtMethodRequest { + [k: string]: unknown; +} /** * Response containing the contents of a text file. */ export interface ReadTextFileResponse { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; content: string; } /** * Response to a permission request. */ export interface RequestPermissionResponse { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The user's decision on the permission request. */ @@ -647,6 +822,12 @@ export interface RequestPermissionResponse { * Response containing the ID of the created terminal. */ export interface CreateTerminalResponse { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The unique identifier for the created terminal. */ @@ -656,6 +837,12 @@ export interface CreateTerminalResponse { * Response containing the terminal output and exit status. */ export interface TerminalOutputResponse { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Exit status if the command has completed. */ @@ -673,6 +860,12 @@ export interface TerminalOutputResponse { * Exit status of a terminal command. */ export interface TerminalExitStatus { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The process exit code (may be null if terminated by signal). */ @@ -686,6 +879,12 @@ export interface TerminalExitStatus { * Response containing the exit status of a terminal command. */ export interface WaitForTerminalExitResponse { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The process exit code (may be null if terminated by signal). */ @@ -695,17 +894,29 @@ export interface WaitForTerminalExitResponse { */ signal?: string | null; } +export interface ExtMethodResponse { + [k: string]: unknown; +} /** * Notification to cancel ongoing operations for a session. * * See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation) */ export interface CancelNotification { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The ID of the session to cancel operations for. */ sessionId: string; } +export interface ExtNotification { + [k: string]: unknown; +} /** * Request parameters for the initialize method. * @@ -714,6 +925,12 @@ export interface CancelNotification { * See protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization) */ export interface InitializeRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; clientCapabilities?: ClientCapabilities; /** * The latest protocol version supported by the client. @@ -724,6 +941,12 @@ export interface InitializeRequest { * Capabilities supported by the client. */ export interface ClientCapabilities { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; fs?: FileSystemCapability; /** * Whether the Client support all `terminal/*` methods. @@ -735,6 +958,12 @@ export interface ClientCapabilities { * Determines which file operations the agent can request. */ export interface FileSystemCapability { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Whether the Client supports `fs/read_text_file` requests. */ @@ -750,6 +979,12 @@ export interface FileSystemCapability { * Specifies which authentication method to use. */ export interface AuthenticateRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The ID of the authentication method to use. * Must be one of the methods advertised in the initialize response. @@ -762,6 +997,12 @@ export interface AuthenticateRequest { * See protocol docs: [Creating a Session](https://agentclientprotocol.com/protocol/session-setup#creating-a-session) */ export interface NewSessionRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The working directory for this session. Must be an absolute path. */ @@ -775,6 +1016,12 @@ export interface NewSessionRequest { * An HTTP header to set when making requests to the MCP server. */ export interface HttpHeader { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The name of the HTTP header. */ @@ -815,6 +1062,12 @@ export interface Stdio { * See protocol docs: [Loading Sessions](https://agentclientprotocol.com/protocol/session-setup#loading-sessions) */ export interface LoadSessionRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The working directory for this session. */ @@ -834,6 +1087,12 @@ export interface LoadSessionRequest { * This type is not part of the spec, and may be removed or changed at any point. */ export interface SetSessionModeRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; modeId: SessionModeId; sessionId: SessionId; } @@ -845,6 +1104,12 @@ export interface SetSessionModeRequest { * See protocol docs: [User Message](https://agentclientprotocol.com/protocol/prompt-turn#1-user-message) */ export interface PromptRequest { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The blocks of content that compose the user's message. * @@ -880,6 +1145,9 @@ export interface PromptRequest { */ sessionId: string; } +export interface ExtMethodRequest1 { + [k: string]: unknown; +} /** * Response from the initialize method. * @@ -888,6 +1156,12 @@ export interface PromptRequest { * See protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization) */ export interface InitializeResponse { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; agentCapabilities?: AgentCapabilities; /** * Authentication methods supported by the agent. @@ -905,6 +1179,12 @@ export interface InitializeResponse { * Capabilities supported by the agent. */ export interface AgentCapabilities { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Whether the agent supports `session/load`. */ @@ -916,6 +1196,12 @@ export interface AgentCapabilities { * MCP capabilities supported by the agent. */ export interface McpCapabilities { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Agent supports [`McpServer::Http`]. */ @@ -929,6 +1215,12 @@ export interface McpCapabilities { * Prompt capabilities supported by the agent. */ export interface PromptCapabilities { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Agent supports [`ContentBlock::Audio`]. */ @@ -949,6 +1241,12 @@ export interface PromptCapabilities { * Describes an available authentication method. */ export interface AuthMethod { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Optional description providing more details about this authentication method. */ @@ -968,6 +1266,12 @@ export interface AuthMethod { * See protocol docs: [Creating a Session](https://agentclientprotocol.com/protocol/session-setup#creating-a-session) */ export interface NewSessionResponse { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * **UNSTABLE** * @@ -987,6 +1291,12 @@ export interface NewSessionResponse { * This type is not part of the spec, and may be removed or changed at any point. */ export interface SessionModeState { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; availableModes: SessionMode[]; currentModeId: SessionModeId; } @@ -996,6 +1306,12 @@ export interface SessionModeState { * This type is not part of the spec, and may be removed or changed at any point. */ export interface SessionMode { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; description?: string | null; id: SessionModeId; name: string; @@ -1004,6 +1320,12 @@ export interface SessionMode { * Response from loading an existing session. */ export interface LoadSessionResponse { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * **UNSTABLE** * @@ -1023,6 +1345,12 @@ export interface SetSessionModeResponse {} * See protocol docs: [Check for Completion](https://agentclientprotocol.com/protocol/prompt-turn#4-check-for-completion) */ export interface PromptResponse { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Indicates why the agent stopped processing the turn. */ @@ -1033,6 +1361,9 @@ export interface PromptResponse { | "refusal" | "cancelled"; } +export interface ExtMethodResponse1 { + [k: string]: unknown; +} /** * Notification containing a session update from the agent. * @@ -1041,6 +1372,12 @@ export interface PromptResponse { * See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output) */ export interface SessionNotification { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The ID of the session this update pertains to. */ @@ -1062,6 +1399,12 @@ export interface SessionNotification { sessionUpdate: "agent_thought_chunk"; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Content produced by the tool call. */ @@ -1113,6 +1456,12 @@ export interface SessionNotification { toolCallId: string; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Replace the content collection. */ @@ -1152,6 +1501,12 @@ export interface SessionNotification { toolCallId: string; } | { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * The list of tasks to be accomplished. * @@ -1178,6 +1533,12 @@ export interface SessionNotification { * See protocol docs: [Plan Entries](https://agentclientprotocol.com/protocol/agent-plan#plan-entries) */ export interface PlanEntry { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Human-readable description of what this task aims to accomplish. */ @@ -1196,6 +1557,12 @@ export interface PlanEntry { * Information about a command. */ export interface AvailableCommand { + /** + * Extension point for implementations + */ + _meta?: { + [k: string]: unknown; + }; /** * Human-readable description of what the command does. */ @@ -1218,9 +1585,13 @@ export interface UnstructuredCommandInput { */ hint: string; } +export interface ExtNotification1 { + [k: string]: unknown; +} /** @internal */ export const writeTextFileRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), content: z.string(), path: z.string(), sessionId: z.string(), @@ -1228,6 +1599,7 @@ export const writeTextFileRequestSchema = z.object({ /** @internal */ export const readTextFileRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), limit: z.number().optional().nullable(), line: z.number().optional().nullable(), path: z.string(), @@ -1236,33 +1608,41 @@ export const readTextFileRequestSchema = z.object({ /** @internal */ export const terminalOutputRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), sessionId: z.string(), terminalId: z.string(), }); /** @internal */ export const releaseTerminalRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), sessionId: z.string(), terminalId: z.string(), }); /** @internal */ export const waitForTerminalExitRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), sessionId: z.string(), terminalId: z.string(), }); /** @internal */ export const killTerminalCommandRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), sessionId: z.string(), terminalId: z.string(), }); +/** @internal */ +export const extMethodRequestSchema = z.record(z.unknown()); + /** @internal */ export const roleSchema = z.union([z.literal("assistant"), z.literal("user")]); /** @internal */ export const textResourceContentsSchema = z.object({ + _meta: z.record(z.unknown()).optional(), mimeType: z.string().optional().nullable(), text: z.string(), uri: z.string(), @@ -1270,6 +1650,7 @@ export const textResourceContentsSchema = z.object({ /** @internal */ export const blobResourceContentsSchema = z.object({ + _meta: z.record(z.unknown()).optional(), blob: z.string(), mimeType: z.string().optional().nullable(), uri: z.string(), @@ -1302,11 +1683,13 @@ export const writeTextFileResponseSchema = z.null(); /** @internal */ export const readTextFileResponseSchema = z.object({ + _meta: z.record(z.unknown()).optional(), content: z.string(), }); /** @internal */ export const requestPermissionResponseSchema = z.object({ + _meta: z.record(z.unknown()).optional(), outcome: z.union([ z.object({ outcome: z.literal("cancelled"), @@ -1320,6 +1703,7 @@ export const requestPermissionResponseSchema = z.object({ /** @internal */ export const createTerminalResponseSchema = z.object({ + _meta: z.record(z.unknown()).optional(), terminalId: z.string(), }); @@ -1328,6 +1712,7 @@ export const releaseTerminalResponseSchema = z.null(); /** @internal */ export const waitForTerminalExitResponseSchema = z.object({ + _meta: z.record(z.unknown()).optional(), exitCode: z.number().optional().nullable(), signal: z.string().optional().nullable(), }); @@ -1335,18 +1720,30 @@ export const waitForTerminalExitResponseSchema = z.object({ /** @internal */ export const killTerminalResponseSchema = z.null(); +/** @internal */ +export const extMethodResponseSchema = z.record(z.unknown()); + /** @internal */ export const cancelNotificationSchema = z.object({ + _meta: z.record(z.unknown()).optional(), sessionId: z.string(), }); +/** @internal */ +export const extNotificationSchema = z.record(z.unknown()); + /** @internal */ export const authenticateRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), methodId: z.string(), }); +/** @internal */ +export const extMethodRequest1Schema = z.record(z.unknown()); + /** @internal */ export const httpHeaderSchema = z.object({ + _meta: z.record(z.unknown()).optional(), name: z.string(), value: z.string(), }); @@ -1359,6 +1756,7 @@ export const sessionIdSchema = z.string(); /** @internal */ export const annotationsSchema = z.object({ + _meta: z.record(z.unknown()).optional(), audience: z.array(roleSchema).optional().nullable(), lastModified: z.string().optional().nullable(), priority: z.number().optional().nullable(), @@ -1378,6 +1776,7 @@ export const setSessionModeResponseSchema = z.object({}); /** @internal */ export const promptResponseSchema = z.object({ + _meta: z.record(z.unknown()).optional(), stopReason: z.union([ z.literal("end_turn"), z.literal("max_tokens"), @@ -1387,6 +1786,12 @@ export const promptResponseSchema = z.object({ ]), }); +/** @internal */ +export const extMethodResponse1Schema = z.record(z.unknown()); + +/** @internal */ +export const extNotification1Schema = z.record(z.unknown()); + /** @internal */ export const unstructuredCommandInputSchema = z.object({ hint: z.string(), @@ -1394,6 +1799,7 @@ export const unstructuredCommandInputSchema = z.object({ /** @internal */ export const permissionOptionSchema = z.object({ + _meta: z.record(z.unknown()).optional(), kind: z.union([ z.literal("allow_once"), z.literal("allow_always"), @@ -1409,11 +1815,13 @@ export const toolCallContentSchema = z.union([ z.object({ content: z.union([ z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), text: z.string(), type: z.literal("text"), }), z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), data: z.string(), mimeType: z.string(), @@ -1421,12 +1829,14 @@ export const toolCallContentSchema = z.union([ uri: z.string().optional().nullable(), }), z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), data: z.string(), mimeType: z.string(), type: z.literal("audio"), }), z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), description: z.string().optional().nullable(), mimeType: z.string().optional().nullable(), @@ -1437,6 +1847,7 @@ export const toolCallContentSchema = z.union([ uri: z.string(), }), z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), resource: embeddedResourceResourceSchema, type: z.literal("resource"), @@ -1445,6 +1856,7 @@ export const toolCallContentSchema = z.union([ type: z.literal("content"), }), z.object({ + _meta: z.record(z.unknown()).optional(), newText: z.string(), oldText: z.string().optional().nullable(), path: z.string(), @@ -1458,24 +1870,28 @@ export const toolCallContentSchema = z.union([ /** @internal */ export const toolCallLocationSchema = z.object({ + _meta: z.record(z.unknown()).optional(), line: z.number().optional().nullable(), path: z.string(), }); /** @internal */ export const envVariableSchema = z.object({ + _meta: z.record(z.unknown()).optional(), name: z.string(), value: z.string(), }); /** @internal */ export const terminalExitStatusSchema = z.object({ + _meta: z.record(z.unknown()).optional(), exitCode: z.number().optional().nullable(), signal: z.string().optional().nullable(), }); /** @internal */ export const fileSystemCapabilitySchema = z.object({ + _meta: z.record(z.unknown()).optional(), readTextFile: z.boolean().optional(), writeTextFile: z.boolean().optional(), }); @@ -1507,6 +1923,7 @@ export const mcpServerSchema = z.union([ /** @internal */ export const setSessionModeRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), modeId: sessionModeIdSchema, sessionId: sessionIdSchema, }); @@ -1514,11 +1931,13 @@ export const setSessionModeRequestSchema = z.object({ /** @internal */ export const contentBlockSchema = z.union([ z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), text: z.string(), type: z.literal("text"), }), z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), data: z.string(), mimeType: z.string(), @@ -1526,12 +1945,14 @@ export const contentBlockSchema = z.union([ uri: z.string().optional().nullable(), }), z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), data: z.string(), mimeType: z.string(), type: z.literal("audio"), }), z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), description: z.string().optional().nullable(), mimeType: z.string().optional().nullable(), @@ -1542,6 +1963,7 @@ export const contentBlockSchema = z.union([ uri: z.string(), }), z.object({ + _meta: z.record(z.unknown()).optional(), annotations: annotationsSchema.optional().nullable(), resource: embeddedResourceResourceSchema, type: z.literal("resource"), @@ -1550,6 +1972,7 @@ export const contentBlockSchema = z.union([ /** @internal */ export const authMethodSchema = z.object({ + _meta: z.record(z.unknown()).optional(), description: z.string().optional().nullable(), id: z.string(), name: z.string(), @@ -1557,12 +1980,14 @@ export const authMethodSchema = z.object({ /** @internal */ export const mcpCapabilitiesSchema = z.object({ + _meta: z.record(z.unknown()).optional(), http: z.boolean().optional(), sse: z.boolean().optional(), }); /** @internal */ export const promptCapabilitiesSchema = z.object({ + _meta: z.record(z.unknown()).optional(), audio: z.boolean().optional(), embeddedContext: z.boolean().optional(), image: z.boolean().optional(), @@ -1570,6 +1995,7 @@ export const promptCapabilitiesSchema = z.object({ /** @internal */ export const sessionModeSchema = z.object({ + _meta: z.record(z.unknown()).optional(), description: z.string().optional().nullable(), id: sessionModeIdSchema, name: z.string(), @@ -1577,12 +2003,14 @@ export const sessionModeSchema = z.object({ /** @internal */ export const sessionModeStateSchema = z.object({ + _meta: z.record(z.unknown()).optional(), availableModes: z.array(sessionModeSchema), currentModeId: sessionModeIdSchema, }); /** @internal */ export const planEntrySchema = z.object({ + _meta: z.record(z.unknown()).optional(), content: z.string(), priority: z.union([z.literal("high"), z.literal("medium"), z.literal("low")]), status: z.union([ @@ -1596,10 +2024,14 @@ export const planEntrySchema = z.object({ export const availableCommandInputSchema = unstructuredCommandInputSchema; /** @internal */ -export const clientNotificationSchema = cancelNotificationSchema; +export const clientNotificationSchema = z.union([ + cancelNotificationSchema, + extNotificationSchema, +]); /** @internal */ export const createTerminalRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), args: z.array(z.string()).optional(), command: z.string(), cwd: z.string().optional().nullable(), @@ -1610,6 +2042,7 @@ export const createTerminalRequestSchema = z.object({ /** @internal */ export const terminalOutputResponseSchema = z.object({ + _meta: z.record(z.unknown()).optional(), exitStatus: terminalExitStatusSchema.optional().nullable(), output: z.string(), truncated: z.boolean(), @@ -1617,12 +2050,14 @@ export const terminalOutputResponseSchema = z.object({ /** @internal */ export const newSessionRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), cwd: z.string(), mcpServers: z.array(mcpServerSchema), }); /** @internal */ export const loadSessionRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), cwd: z.string(), mcpServers: z.array(mcpServerSchema), sessionId: z.string(), @@ -1630,23 +2065,27 @@ export const loadSessionRequestSchema = z.object({ /** @internal */ export const promptRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), prompt: z.array(contentBlockSchema), sessionId: z.string(), }); /** @internal */ export const newSessionResponseSchema = z.object({ + _meta: z.record(z.unknown()).optional(), modes: sessionModeStateSchema.optional().nullable(), sessionId: z.string(), }); /** @internal */ export const loadSessionResponseSchema = z.object({ + _meta: z.record(z.unknown()).optional(), modes: sessionModeStateSchema.optional().nullable(), }); /** @internal */ export const toolCallUpdateSchema = z.object({ + _meta: z.record(z.unknown()).optional(), content: z.array(toolCallContentSchema).optional().nullable(), kind: toolKindSchema.optional().nullable(), locations: z.array(toolCallLocationSchema).optional().nullable(), @@ -1659,12 +2098,14 @@ export const toolCallUpdateSchema = z.object({ /** @internal */ export const clientCapabilitiesSchema = z.object({ + _meta: z.record(z.unknown()).optional(), fs: fileSystemCapabilitySchema.optional(), terminal: z.boolean().optional(), }); /** @internal */ export const agentCapabilitiesSchema = z.object({ + _meta: z.record(z.unknown()).optional(), loadSession: z.boolean().optional(), mcpCapabilities: mcpCapabilitiesSchema.optional(), promptCapabilities: promptCapabilitiesSchema.optional(), @@ -1672,6 +2113,7 @@ export const agentCapabilitiesSchema = z.object({ /** @internal */ export const availableCommandSchema = z.object({ + _meta: z.record(z.unknown()).optional(), description: z.string(), input: availableCommandInputSchema.optional().nullable(), name: z.string(), @@ -1687,10 +2129,12 @@ export const clientResponseSchema = z.union([ releaseTerminalResponseSchema, waitForTerminalExitResponseSchema, killTerminalResponseSchema, + extMethodResponseSchema, ]); /** @internal */ export const requestPermissionRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), options: z.array(permissionOptionSchema), sessionId: z.string(), toolCall: toolCallUpdateSchema, @@ -1698,12 +2142,14 @@ export const requestPermissionRequestSchema = z.object({ /** @internal */ export const initializeRequestSchema = z.object({ + _meta: z.record(z.unknown()).optional(), clientCapabilities: clientCapabilitiesSchema.optional(), protocolVersion: z.number(), }); /** @internal */ export const initializeResponseSchema = z.object({ + _meta: z.record(z.unknown()).optional(), agentCapabilities: agentCapabilitiesSchema.optional(), authMethods: z.array(authMethodSchema).optional(), protocolVersion: z.number(), @@ -1711,6 +2157,7 @@ export const initializeResponseSchema = z.object({ /** @internal */ export const sessionNotificationSchema = z.object({ + _meta: z.record(z.unknown()).optional(), sessionId: z.string(), update: z.union([ z.object({ @@ -1726,6 +2173,7 @@ export const sessionNotificationSchema = z.object({ sessionUpdate: z.literal("agent_thought_chunk"), }), z.object({ + _meta: z.record(z.unknown()).optional(), content: z.array(toolCallContentSchema).optional(), kind: z .union([ @@ -1757,6 +2205,7 @@ export const sessionNotificationSchema = z.object({ toolCallId: z.string(), }), z.object({ + _meta: z.record(z.unknown()).optional(), content: z.array(toolCallContentSchema).optional().nullable(), kind: toolKindSchema.optional().nullable(), locations: z.array(toolCallLocationSchema).optional().nullable(), @@ -1768,6 +2217,7 @@ export const sessionNotificationSchema = z.object({ toolCallId: z.string(), }), z.object({ + _meta: z.record(z.unknown()).optional(), entries: z.array(planEntrySchema), sessionUpdate: z.literal("plan"), }), @@ -1792,6 +2242,7 @@ export const clientRequestSchema = z.union([ releaseTerminalRequestSchema, waitForTerminalExitRequestSchema, killTerminalCommandRequestSchema, + extMethodRequestSchema, ]); /** @internal */ @@ -1802,6 +2253,7 @@ export const agentRequestSchema = z.union([ loadSessionRequestSchema, setSessionModeRequestSchema, promptRequestSchema, + extMethodRequest1Schema, ]); /** @internal */ @@ -1812,10 +2264,14 @@ export const agentResponseSchema = z.union([ loadSessionResponseSchema, setSessionModeResponseSchema, promptResponseSchema, + extMethodResponse1Schema, ]); /** @internal */ -export const agentNotificationSchema = sessionNotificationSchema; +export const agentNotificationSchema = z.union([ + sessionNotificationSchema, + extNotification1Schema, +]); /** @internal */ export const agentClientProtocolSchema = z.union([