diff --git a/agent-client-protocol-schema/Cargo.toml b/agent-client-protocol-schema/Cargo.toml index c900c0624..4c41ed346 100644 --- a/agent-client-protocol-schema/Cargo.toml +++ b/agent-client-protocol-schema/Cargo.toml @@ -33,6 +33,7 @@ unstable = [ "unstable_nes", "unstable_plan_operations", "unstable_session_fork", + "unstable_session_inject", "unstable_session_compaction", "unstable_session_notices", "unstable_end_turn_token_usage", @@ -42,6 +43,9 @@ unstable = [ # It introduces a parallel `v2` module with a different wire version, so it # must be opted into explicitly. unstable_protocol_v2 = [] +# Session injection is a v2-only draft extension. Pair it with +# `unstable_protocol_v2` to expose the v2 types. +unstable_session_inject = [] unstable_llm_providers = [] unstable_mcp_over_acp = [] unstable_nes = [] diff --git a/agent-client-protocol-schema/src/v2/agent.rs b/agent-client-protocol-schema/src/v2/agent.rs index bbc7924dd..4506029c6 100644 --- a/agent-client-protocol-schema/src/v2/agent.rs +++ b/agent-client-protocol-schema/src/v2/agent.rs @@ -20,6 +20,14 @@ use super::{ }; use crate::{IntoOption, ProtocolVersion, SkipListener}; +#[cfg(feature = "unstable_session_inject")] +use super::session_inject::{ + InjectSessionRequest, InjectSessionResponse, ReplaceInjectSessionRequest, + ReplaceInjectSessionResponse, RevokeInjectSessionRequest, RevokeInjectSessionResponse, + SESSION_INJECT_METHOD_NAME, SESSION_REPLACE_INJECT_METHOD_NAME, + SESSION_REVOKE_INJECT_METHOD_NAME, SessionInjectCapabilities, +}; + #[cfg(feature = "unstable_mcp_over_acp")] use super::mcp::{ MCP_MESSAGE_METHOD_NAME, MessageMcpNotification, MessageMcpRequest, MessageMcpResponse, @@ -4162,6 +4170,19 @@ pub struct SessionCapabilities { /// /// This capability is not part of the spec yet, and may be removed or changed at any point. /// + /// Mid-turn user-message injection supported by the agent. + /// + /// Optional. Omitted or `null` means the agent does not support + /// `session/inject` or the mandatory `session/revoke_inject` companion method. + #[cfg(feature = "unstable_session_inject")] + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub inject: Option, + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// /// Whether the agent supports `session/fork`. /// /// Optional. Omitted or `null` both mean the agent does not advertise support. @@ -4238,6 +4259,17 @@ impl SessionCapabilities { self } + #[cfg(feature = "unstable_session_inject")] + /// Mid-turn user-message injection supported by the agent. + /// + /// Advertising this capability also requires support for + /// `session/revoke_inject`. + #[must_use] + pub fn inject(mut self, inject: impl IntoOption) -> Self { + self.inject = inject.into_option(); + self + } + #[cfg(feature = "unstable_session_fork")] /// Whether the agent supports `session/fork`. /// @@ -4926,6 +4958,15 @@ pub struct AgentMethodNames { pub session_set_config_option: &'static str, /// Method for sending a prompt to the agent. pub session_prompt: &'static str, + /// Method for injecting a message into a running session. + #[cfg(feature = "unstable_session_inject")] + pub session_inject: &'static str, + /// Method for revoking a pending injected message. + #[cfg(feature = "unstable_session_inject")] + pub session_revoke_inject: &'static str, + /// Method for replacing a pending injected message. + #[cfg(feature = "unstable_session_inject")] + pub session_replace_inject: &'static str, /// Notification for cancelling operations. pub session_cancel: &'static str, /// Method for exchanging MCP-over-ACP messages. @@ -4989,6 +5030,12 @@ pub const AGENT_METHOD_NAMES: AgentMethodNames = AgentMethodNames { session_new: SESSION_NEW_METHOD_NAME, session_set_config_option: SESSION_SET_CONFIG_OPTION_METHOD_NAME, session_prompt: SESSION_PROMPT_METHOD_NAME, + #[cfg(feature = "unstable_session_inject")] + session_inject: SESSION_INJECT_METHOD_NAME, + #[cfg(feature = "unstable_session_inject")] + session_revoke_inject: SESSION_REVOKE_INJECT_METHOD_NAME, + #[cfg(feature = "unstable_session_inject")] + session_replace_inject: SESSION_REPLACE_INJECT_METHOD_NAME, session_cancel: SESSION_CANCEL_METHOD_NAME, #[cfg(feature = "unstable_mcp_over_acp")] mcp_message: MCP_MESSAGE_METHOD_NAME, @@ -5184,6 +5231,27 @@ pub enum ClientRequest { /// /// See protocol docs: [Prompt Lifecycle](https://agentclientprotocol.com/protocol/prompt-lifecycle) PromptRequest(Box), + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// Injects a user message for pending delivery. + #[cfg(feature = "unstable_session_inject")] + InjectSessionRequest(Box), + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// Revokes a pending injected message. Mandatory when injection is advertised. + #[cfg(feature = "unstable_session_inject")] + RevokeInjectSessionRequest(Box), + /// **UNSTABLE** + /// + /// This capability is not part of the spec yet, and may be removed or changed at any point. + /// + /// Replaces pending injected content when `pending.replace` is advertised. + #[cfg(feature = "unstable_session_inject")] + ReplaceInjectSessionRequest(Box), #[cfg(feature = "unstable_nes")] /// **UNSTABLE** /// @@ -5247,6 +5315,12 @@ impl ClientRequest { Self::CloseSessionRequest(_) => AGENT_METHOD_NAMES.session_close, Self::SetSessionConfigOptionRequest(_) => AGENT_METHOD_NAMES.session_set_config_option, Self::PromptRequest(_) => AGENT_METHOD_NAMES.session_prompt, + #[cfg(feature = "unstable_session_inject")] + Self::InjectSessionRequest(_) => AGENT_METHOD_NAMES.session_inject, + #[cfg(feature = "unstable_session_inject")] + Self::RevokeInjectSessionRequest(_) => AGENT_METHOD_NAMES.session_revoke_inject, + #[cfg(feature = "unstable_session_inject")] + Self::ReplaceInjectSessionRequest(_) => AGENT_METHOD_NAMES.session_replace_inject, #[cfg(feature = "unstable_nes")] Self::StartNesRequest(_) => AGENT_METHOD_NAMES.nes_start, #[cfg(feature = "unstable_nes")] @@ -5304,6 +5378,15 @@ pub enum AgentResponse { SetSessionConfigOptionResponse(Box), /// Successful result returned for a `session/prompt` request. PromptResponse(Box), + /// Successful result returned for a `session/inject` request. + #[cfg(feature = "unstable_session_inject")] + InjectSessionResponse(Box), + /// Successful result returned for a `session/revoke_inject` request. + #[cfg(feature = "unstable_session_inject")] + RevokeInjectSessionResponse(#[serde(default)] Box), + /// Successful result returned for a `session/replace_inject` request. + #[cfg(feature = "unstable_session_inject")] + ReplaceInjectSessionResponse(Box), /// Successful result returned for a `nes/start` request. #[cfg(feature = "unstable_nes")] StartNesResponse(Box), diff --git a/agent-client-protocol-schema/src/v2/error.rs b/agent-client-protocol-schema/src/v2/error.rs index a7f719b01..284b64ba4 100644 --- a/agent-client-protocol-schema/src/v2/error.rs +++ b/agent-client-protocol-schema/src/v2/error.rs @@ -127,6 +127,44 @@ impl Error { } } + /// A steer was rejected because the session has no running turn. + #[cfg(feature = "unstable_session_inject")] + #[must_use] + pub fn inject_no_running_turn() -> Self { + Self::from(ErrorCode::InjectPreconditionFailed) + .data(serde_json::json!({ "reason": "no_running_turn" })) + } + + /// A pending injected message was delivered before it could be revoked or replaced. + #[cfg(feature = "unstable_session_inject")] + #[must_use] + pub fn inject_already_delivered(message_id: impl Into) -> Self { + Self::from(ErrorCode::InjectPreconditionFailed).data(serde_json::json!({ + "reason": "already_delivered", + "messageId": message_id.into(), + })) + } + + /// Pending injected-message replacement is not supported. + #[cfg(feature = "unstable_session_inject")] + #[must_use] + pub fn inject_replace_not_supported(message_id: impl Into) -> Self { + Self::from(ErrorCode::InjectPreconditionFailed).data(serde_json::json!({ + "reason": "replace_not_supported", + "messageId": message_id.into(), + })) + } + + /// The injected message ID is unknown for the requested session. + #[cfg(feature = "unstable_session_inject")] + #[must_use] + pub fn inject_unknown_message_id(message_id: impl Into) -> Self { + Self::from(ErrorCode::ResourceNotFound).data(serde_json::json!({ + "reason": "unknown_message_id", + "messageId": message_id.into(), + })) + } + /// Converts a standard error into an internal JSON-RPC error. /// /// The error's string representation is included as additional data. @@ -185,6 +223,21 @@ pub enum ErrorCode { #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))] #[strum(to_string = "Resource not found")] ResourceNotFound, // -32002 + /// **UNSTABLE** + /// + /// This error is not part of the spec yet, and may be removed or changed at any point. + /// + /// A session injection precondition failed. + /// + /// `error.data` uses a `reason` of `already_delivered`, `no_running_turn`, + /// or `replace_not_supported`. Pending-message failures also include the + /// `messageId`. Unknown message IDs instead use `-32002` with + /// `data: { reason: "unknown_message_id", messageId }`. The error data remains + /// open JSON, consistent with the protocol's shared [`Error::data`] field. + #[cfg(feature = "unstable_session_inject")] + #[cfg_attr(feature = "schemars", schemars(transform = error_code_transform))] + #[strum(to_string = "Inject precondition failed")] + InjectPreconditionFailed, // -32010 /// Other undefined error code. #[cfg_attr(feature = "schemars", schemars(untagged))] #[strum(to_string = "Unknown error")] @@ -202,6 +255,8 @@ impl From for ErrorCode { -32800 => ErrorCode::RequestCancelled, -32000 => ErrorCode::AuthRequired, -32002 => ErrorCode::ResourceNotFound, + #[cfg(feature = "unstable_session_inject")] + -32010 => ErrorCode::InjectPreconditionFailed, _ => ErrorCode::Other(value), } } @@ -218,6 +273,8 @@ impl From for i32 { ErrorCode::RequestCancelled => -32800, ErrorCode::AuthRequired => -32000, ErrorCode::ResourceNotFound => -32002, + #[cfg(feature = "unstable_session_inject")] + ErrorCode::InjectPreconditionFailed => -32010, ErrorCode::Other(value) => value, } } @@ -245,6 +302,8 @@ fn error_code_transform(schema: &mut Schema) { "RequestCancelled" => ErrorCode::RequestCancelled, "AuthRequired" => ErrorCode::AuthRequired, "ResourceNotFound" => ErrorCode::ResourceNotFound, + #[cfg(feature = "unstable_session_inject")] + "InjectPreconditionFailed" => ErrorCode::InjectPreconditionFailed, _ => panic!("Unexpected error code name {name}"), }; let mut description = schema @@ -340,4 +399,50 @@ mod tests { ); } } + + #[cfg(feature = "unstable_session_inject")] + #[test] + fn session_inject_errors_include_required_data() { + assert_eq!( + serde_json::to_value(Error::inject_no_running_turn()).unwrap(), + serde_json::json!({ + "code": -32010, + "message": "Inject precondition failed", + "data": { "reason": "no_running_turn" }, + }) + ); + assert_eq!( + serde_json::to_value(Error::inject_already_delivered("message-1")).unwrap(), + serde_json::json!({ + "code": -32010, + "message": "Inject precondition failed", + "data": { + "reason": "already_delivered", + "messageId": "message-1", + }, + }) + ); + assert_eq!( + serde_json::to_value(Error::inject_replace_not_supported("message-1")).unwrap(), + serde_json::json!({ + "code": -32010, + "message": "Inject precondition failed", + "data": { + "reason": "replace_not_supported", + "messageId": "message-1", + }, + }) + ); + assert_eq!( + serde_json::to_value(Error::inject_unknown_message_id("message-1")).unwrap(), + serde_json::json!({ + "code": -32002, + "message": "Resource not found", + "data": { + "reason": "unknown_message_id", + "messageId": "message-1", + }, + }) + ); + } } diff --git a/agent-client-protocol-schema/src/v2/mod.rs b/agent-client-protocol-schema/src/v2/mod.rs index 8104eeef9..a08d73e57 100644 --- a/agent-client-protocol-schema/src/v2/mod.rs +++ b/agent-client-protocol-schema/src/v2/mod.rs @@ -23,6 +23,8 @@ mod plan; mod protocol_level; #[cfg(feature = "schemars")] pub(crate) mod schema_util; +#[cfg(feature = "unstable_session_inject")] +mod session_inject; mod terminal; mod tool_call; @@ -41,6 +43,8 @@ pub use nes::*; pub use plan::*; pub use protocol_level::*; pub use serde_json::value::RawValue; +#[cfg(feature = "unstable_session_inject")] +pub use session_inject::*; pub use terminal::*; pub use tool_call::*; diff --git a/agent-client-protocol-schema/src/v2/session_inject.rs b/agent-client-protocol-schema/src/v2/session_inject.rs new file mode 100644 index 000000000..f8a5424d8 --- /dev/null +++ b/agent-client-protocol-schema/src/v2/session_inject.rs @@ -0,0 +1,601 @@ +//! Unstable ACP v2 session injection types. + +use serde::{Deserialize, Serialize}; +use serde_with::{DefaultOnError, serde_as, skip_serializing_none}; + +#[cfg(feature = "schemars")] +use schemars::Schema; + +use super::{ContentBlock, MessageId, Meta, SessionId}; +use crate::IntoOption; + +/// Method name for injecting a user message into a session. +pub(crate) const SESSION_INJECT_METHOD_NAME: &str = "session/inject"; +/// Method name for revoking a pending injected message. +pub(crate) const SESSION_REVOKE_INJECT_METHOD_NAME: &str = "session/revoke_inject"; +/// Method name for replacing a pending injected message. +pub(crate) const SESSION_REPLACE_INJECT_METHOD_NAME: &str = "session/replace_inject"; + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Delivery mode for a message injected into a session. +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum SessionInjectMode { + /// Deliver after the current turn returns to idle. + Queue, + /// Deliver at the next safe breakpoint in the current turn. + Steer, + /// Custom or future injection mode. + /// + /// Values beginning with `_` are reserved for implementation-specific + /// extensions. Unknown values that do not begin with `_` are reserved for + /// future ACP variants. + #[serde(untagged)] + Other(String), +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// How an agent handles steering that arrives during an LLM stream. +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum SessionInjectSteerInStream { + /// Truncate the in-flight stream and re-prompt with the injected message. + Interrupt, + /// Finish the in-flight stream before delivering the injected message. + Finish, + /// Custom or future stream-steering behavior. + /// + /// Values beginning with `_` are reserved for implementation-specific + /// extensions. Unknown values that do not begin with `_` are reserved for + /// future ACP variants. + #[serde(untagged)] + Other(String), +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Request parameters for injecting a user message into a session. +/// +/// The response acknowledges pending delivery. The agent later delivers the +/// message by emitting a `user_message` session update with the returned +/// [`MessageId`]. +#[serde_as] +#[skip_serializing_none] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = SESSION_INJECT_METHOD_NAME)))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct InjectSessionRequest { + /// The ID of the session that receives the injected message. + pub session_id: SessionId, + /// When the agent should deliver the message. + pub mode: SessionInjectMode, + /// The content blocks that compose the injected user message. + pub content: Vec, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + #[serde(rename = "_meta")] + pub meta: Option, +} + +impl InjectSessionRequest { + /// Builds an injection request with the required fields set. + #[must_use] + pub fn new( + session_id: impl Into, + mode: SessionInjectMode, + content: Vec, + ) -> Self { + Self { + session_id: session_id.into(), + mode, + content, + meta: None, + } + } + + /// Attaches protocol extension metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Response acknowledging that an injected message is pending delivery. +#[serde_as] +#[skip_serializing_none] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = SESSION_INJECT_METHOD_NAME)))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct InjectSessionResponse { + /// Agent-assigned ID for the pending message. + pub message_id: MessageId, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + #[serde(rename = "_meta")] + pub meta: Option, +} + +impl InjectSessionResponse { + /// Builds an injection response with the assigned message ID. + #[must_use] + pub fn new(message_id: impl Into) -> Self { + Self { + message_id: message_id.into(), + meta: None, + } + } + + /// Attaches protocol extension metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Request parameters for revoking a pending injected message. +/// +/// Every agent that advertises session injection must support this method. A +/// successful response guarantees that no future `user_message` update will be +/// emitted for this message ID. If delivery wins the race, the agent returns +/// `-32010` with `data: { reason: "already_delivered", messageId }`. An unknown +/// message ID returns `-32002` with +/// `data: { reason: "unknown_message_id", messageId }`. +#[serde_as] +#[skip_serializing_none] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = SESSION_REVOKE_INJECT_METHOD_NAME)))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct RevokeInjectSessionRequest { + /// The ID of the session that owns the pending message. + pub session_id: SessionId, + /// The ID returned by `session/inject`. + pub message_id: MessageId, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + #[serde(rename = "_meta")] + pub meta: Option, +} + +impl RevokeInjectSessionRequest { + /// Builds a revoke request for a pending injected message. + #[must_use] + pub fn new(session_id: impl Into, message_id: impl Into) -> Self { + Self { + session_id: session_id.into(), + message_id: message_id.into(), + meta: None, + } + } + + /// Attaches protocol extension metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Response confirming that a pending injected message was revoked. +#[serde_as] +#[skip_serializing_none] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = SESSION_REVOKE_INJECT_METHOD_NAME)))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct RevokeInjectSessionResponse { + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + #[serde(rename = "_meta")] + pub meta: Option, +} + +impl RevokeInjectSessionResponse { + /// Builds an empty successful revoke response. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Attaches protocol extension metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Request parameters for replacing the content of a pending injected message. +/// +/// Agents support this optional method only when they advertise +/// `session.inject.pending.replace`. Replacement preserves the message ID, mode, +/// and pending-order position. Already-delivered messages return `-32010` with +/// `data: { reason: "already_delivered", messageId }`; unknown message IDs return +/// `-32002` with `data: { reason: "unknown_message_id", messageId }`. If replace +/// was not advertised, the agent returns method-not-found or `-32010` with +/// `data: { reason: "replace_not_supported", messageId }`. +#[serde_as] +#[skip_serializing_none] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = SESSION_REPLACE_INJECT_METHOD_NAME)))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ReplaceInjectSessionRequest { + /// The ID of the session that owns the pending message. + pub session_id: SessionId, + /// The ID returned by `session/inject`. + pub message_id: MessageId, + /// Complete replacement content for the pending user message. + pub content: Vec, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + #[serde(rename = "_meta")] + pub meta: Option, +} + +impl ReplaceInjectSessionRequest { + /// Builds a replacement request with the required fields set. + #[must_use] + pub fn new( + session_id: impl Into, + message_id: impl Into, + content: Vec, + ) -> Self { + Self { + session_id: session_id.into(), + message_id: message_id.into(), + content, + meta: None, + } + } + + /// Attaches protocol extension metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Response confirming replacement of a pending injected message. +#[serde_as] +#[skip_serializing_none] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "schemars", schemars(extend("x-side" = "agent", "x-method" = SESSION_REPLACE_INJECT_METHOD_NAME)))] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct ReplaceInjectSessionResponse { + /// The unchanged ID of the pending message. + pub message_id: MessageId, + /// The _meta property is reserved by ACP to allow clients and agents to attach additional + /// metadata to their interactions. Implementations MUST NOT make assumptions about values at + /// these keys. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + #[serde(rename = "_meta")] + pub meta: Option, +} + +impl ReplaceInjectSessionResponse { + /// Builds a replacement response for the pending message ID. + #[must_use] + pub fn new(message_id: impl Into) -> Self { + Self { + message_id: message_id.into(), + meta: None, + } + } + + /// Attaches protocol extension metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Capabilities for mid-turn user-message injection. +/// +/// `modes` must be non-empty. When it contains `steer`, `steerInStream` is +/// required and must also be non-empty. Advertising this capability makes +/// `session/revoke_inject` mandatory. +#[serde_as] +#[skip_serializing_none] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[cfg_attr(feature = "schemars", schemars(transform = session_inject_capabilities_transform))] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct SessionInjectCapabilities { + /// Supported injection delivery modes. Must be non-empty. + #[cfg_attr(feature = "schemars", schemars(length(min = 1)))] + pub modes: Vec, + /// Supported handling for steering during an LLM stream. + /// + /// Required and non-empty when `modes` contains `steer`; otherwise optional. + #[cfg_attr(feature = "schemars", schemars(length(min = 1)))] + pub steer_in_stream: Option>, + /// Capabilities for editing pending injected messages. + /// + /// Omitted or `null` means pending replacement is not supported. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub pending: Option, + /// The _meta property is reserved by ACP for extension metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + #[serde(rename = "_meta")] + pub meta: Option, +} + +impl SessionInjectCapabilities { + /// Builds injection capabilities with the required supported modes. + #[must_use] + pub fn new(modes: Vec) -> Self { + Self { + modes, + steer_in_stream: None, + pending: None, + meta: None, + } + } + + /// Declares handling supported for steering during an LLM stream. + #[must_use] + pub fn steer_in_stream( + mut self, + steer_in_stream: impl IntoOption>, + ) -> Self { + self.steer_in_stream = steer_in_stream.into_option(); + self + } + + /// Declares capabilities for pending injected messages. + #[must_use] + pub fn pending(mut self, pending: impl IntoOption) -> Self { + self.pending = pending.into_option(); + self + } + + /// Attaches protocol extension metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +#[cfg(feature = "schemars")] +fn session_inject_capabilities_transform(schema: &mut Schema) { + let condition = serde_json::json!({ + "if": { + "properties": { + "modes": { + "contains": { "const": "steer" } + } + }, + "required": ["modes"] + }, + "then": { + "properties": { + "steerInStream": { + "type": "array", + "minItems": 1 + } + }, + "required": ["steerInStream"] + } + }); + schema.insert("allOf".into(), serde_json::json!([condition])); +} + +/// **UNSTABLE** +/// +/// This capability is not part of the spec yet, and may be removed or changed at any point. +/// +/// Capabilities for pending injected messages. +#[serde_as] +#[skip_serializing_none] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[non_exhaustive] +pub struct SessionInjectPendingCapabilities { + /// Whether `session/replace_inject` is supported. + /// + /// Optional. Omitted or `null` defaults to `false`. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + pub replace: Option, + /// The _meta property is reserved by ACP for extension metadata. + #[serde_as(deserialize_as = "DefaultOnError")] + #[cfg_attr(feature = "schemars", schemars(extend("x-deserialize-default-on-error" = true)))] + #[serde(default)] + #[serde(rename = "_meta")] + pub meta: Option, +} + +impl SessionInjectPendingCapabilities { + /// Builds empty pending-message capabilities. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Sets whether pending content replacement is supported. + #[must_use] + pub fn replace(mut self, replace: impl IntoOption) -> Self { + self.replace = replace.into_option(); + self + } + + /// Attaches protocol extension metadata. + #[must_use] + pub fn meta(mut self, meta: impl IntoOption) -> Self { + self.meta = meta.into_option(); + self + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + use crate::v2::TextContent; + + #[test] + fn serializes_inject_requests_and_capabilities() { + let content = vec![ContentBlock::Text(TextContent::new( + "correct the auth path", + ))]; + let request = + InjectSessionRequest::new("session-1", SessionInjectMode::Steer, content.clone()); + assert_eq!( + serde_json::to_value(request).unwrap(), + json!({ + "sessionId": "session-1", + "mode": "steer", + "content": [{"type": "text", "text": "correct the auth path"}] + }) + ); + + let replace = + ReplaceInjectSessionRequest::new("session-1", MessageId::new("message-1"), content); + assert_eq!( + serde_json::to_value(replace).unwrap()["messageId"], + json!("message-1") + ); + + let capabilities = SessionInjectCapabilities::new(vec![ + SessionInjectMode::Queue, + SessionInjectMode::Steer, + ]) + .steer_in_stream(vec![ + SessionInjectSteerInStream::Interrupt, + SessionInjectSteerInStream::Finish, + ]) + .pending(SessionInjectPendingCapabilities::new().replace(true)); + assert_eq!( + serde_json::to_value(capabilities).unwrap(), + json!({ + "modes": ["queue", "steer"], + "steerInStream": ["interrupt", "finish"], + "pending": {"replace": true} + }) + ); + } + + #[test] + fn routes_injection_methods_and_error_code() { + let content = vec![ContentBlock::Text(TextContent::new("new context"))]; + let inject = crate::v2::ClientRequest::InjectSessionRequest(Box::new( + InjectSessionRequest::new("session-1", SessionInjectMode::Queue, content.clone()), + )); + let revoke = crate::v2::ClientRequest::RevokeInjectSessionRequest(Box::new( + RevokeInjectSessionRequest::new("session-1", "message-1"), + )); + let replace = crate::v2::ClientRequest::ReplaceInjectSessionRequest(Box::new( + ReplaceInjectSessionRequest::new("session-1", "message-1", content), + )); + + assert_eq!(inject.method(), "session/inject"); + assert_eq!(revoke.method(), "session/revoke_inject"); + assert_eq!(replace.method(), "session/replace_inject"); + assert_eq!( + i32::from(crate::v2::ErrorCode::InjectPreconditionFailed), + -32010 + ); + } + + #[cfg(feature = "schemars")] + #[test] + fn capability_schema_enforces_non_empty_and_steer_requirements() { + let schema = + serde_json::to_value(schemars::schema_for!(SessionInjectCapabilities)).unwrap(); + assert_eq!(schema["properties"]["modes"]["minItems"], json!(1)); + assert_eq!(schema["properties"]["steerInStream"]["minItems"], json!(1)); + assert_eq!( + schema["allOf"][0]["then"]["required"], + json!(["steerInStream"]) + ); + assert_eq!( + schema["allOf"][0]["then"]["properties"]["steerInStream"]["type"], + json!("array") + ); + assert_eq!( + schema["allOf"][0]["then"]["properties"]["steerInStream"]["minItems"], + json!(1) + ); + } +} diff --git a/docs/protocol/v2/draft/schema.mdx b/docs/protocol/v2/draft/schema.mdx index fe9e0a3c9..ce1ba8c6b 100644 --- a/docs/protocol/v2/draft/schema.mdx +++ b/docs/protocol/v2/draft/schema.mdx @@ -1140,6 +1140,83 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d Unique identifier for the newly created forked session. + +### session/inject + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Injects a user message for pending delivery. + +#### InjectSessionRequest + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Request parameters for injecting a user message into a session. + +The response acknowledges pending delivery. The agent later delivers the +message by emitting a `user_message` session update with the returned +`MessageId`. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach + additional metadata to their interactions. Implementations MUST NOT make + assumptions about values at these keys. + +ContentBlock[]} + required +> + The content blocks that compose the injected user message. + +SessionInjectMode} + required +> + When the agent should deliver the message. + +SessionId} + required +> + The ID of the session that receives the injected message. + + +#### InjectSessionResponse + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Response acknowledging that an injected message is pending delivery. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach + additional metadata to their interactions. Implementations MUST NOT make + assumptions about values at these keys. + +MessageId} + required +> + Agent-assigned ID for the pending message. + + ### session/list @@ -1350,6 +1427,87 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d + +### session/replace_inject + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Replaces pending injected content when `pending.replace` is advertised. + +#### ReplaceInjectSessionRequest + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Request parameters for replacing the content of a pending injected message. + +Agents support this optional method only when they advertise +`session.inject.pending.replace`. Replacement preserves the message ID, mode, +and pending-order position. Already-delivered messages return `-32010` with +`data: \{ reason: "already_delivered", messageId \}`; unknown message IDs return +`-32002` with `data: \{ reason: "unknown_message_id", messageId \}`. If replace +was not advertised, the agent returns method-not-found or `-32010` with +`data: \{ reason: "replace_not_supported", messageId \}`. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach + additional metadata to their interactions. Implementations MUST NOT make + assumptions about values at these keys. + +ContentBlock[]} + required +> + Complete replacement content for the pending user message. + +MessageId} + required +> + The ID returned by `session/inject`. + +SessionId} + required +> + The ID of the session that owns the pending message. + + +#### ReplaceInjectSessionResponse + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Response confirming replacement of a pending injected message. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach + additional metadata to their interactions. Implementations MUST NOT make + assumptions about values at these keys. + +MessageId} + required +> + The unchanged ID of the pending message. + + ### session/resume @@ -1427,6 +1585,72 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d Initial session configuration options. + +### session/revoke_inject + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Revokes a pending injected message. Mandatory when injection is advertised. + +#### RevokeInjectSessionRequest + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Request parameters for revoking a pending injected message. + +Every agent that advertises session injection must support this method. A +successful response guarantees that no future `user_message` update will be +emitted for this message ID. If delivery wins the race, the agent returns +`-32010` with `data: \{ reason: "already_delivered", messageId \}`. An unknown +message ID returns `-32002` with +`data: \{ reason: "unknown_message_id", messageId \}`. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach + additional metadata to their interactions. Implementations MUST NOT make + assumptions about values at these keys. + +MessageId} + required +> + The ID returned by `session/inject`. + +SessionId} + required +> + The ID of the session that owns the pending message. + + +#### RevokeInjectSessionResponse + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Response confirming that a pending injected message was revoked. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP to allow clients and agents to attach + additional metadata to their interactions. Implementations MUST NOT make + assumptions about values at these keys. + + ### session/set_config_option @@ -4502,6 +4726,21 @@ and use the reserved range (-32000 to -32099) for protocol-specific errors. **Resource not found**: A given resource, such as a file, was not found. + +**Inject precondition failed**: **UNSTABLE** + +This error is not part of the spec yet, and may be removed or changed at any point. + +A session injection precondition failed. + +`error.data` uses a `reason` of `already_delivered`, `no_running_turn`, +or `replace_not_supported`. Pending-message failures also include the +`messageId`. Unknown message IDs instead use `-32002` with +`data: \{ reason: "unknown_message_id", messageId \}`. The error data remains +open JSON, consistent with the protocol's shared `Error::data` field. + + + Other undefined error code. @@ -7665,6 +7904,17 @@ Whether the agent supports `session/fork`. Optional. Omitted or `null` both mean the agent does not advertise support. Supplying `\{\}` means the agent supports forking sessions. + +SessionInjectCapabilities | null} > + **UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Mid-turn user-message injection supported by the agent. + +Optional. Omitted or `null` means the agent does not support +`session/inject` or the mandatory `session/revoke_inject` companion method. + McpCapabilities | null} > MCP capabilities supported by the agent for session lifecycle requests. @@ -8062,6 +8312,117 @@ See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/v2/d +## SessionInjectCapabilities + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Capabilities for mid-turn user-message injection. + +`modes` must be non-empty. When it contains `steer`, `steerInStream` is +required and must also be non-empty. Advertising this capability makes +`session/revoke_inject` mandatory. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP for extension metadata. + +SessionInjectMode[]} required> + Supported injection delivery modes. Must be non-empty. + +SessionInjectPendingCapabilities | null} > + Capabilities for editing pending injected messages. + +Omitted or `null` means pending replacement is not supported. + + +SessionInjectSteerInStream[] | null} > + Supported handling for steering during an LLM stream. + +Required and non-empty when `modes` contains `steer`; otherwise optional. + + + +## SessionInjectMode + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Delivery mode for a message injected into a session. + +**Type:** Union + + + Deliver after the current turn returns to idle. + + + + Deliver at the next safe breakpoint in the current turn. + + + +Custom or future injection mode. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + +## SessionInjectPendingCapabilities + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +Capabilities for pending injected messages. + +**Type:** Object + +**Properties:** + + + The _meta property is reserved by ACP for extension metadata. + + + Whether `session/replace_inject` is supported. + +Optional. Omitted or `null` defaults to `false`. + + + +## SessionInjectSteerInStream + +**UNSTABLE** + +This capability is not part of the spec yet, and may be removed or changed at any point. + +How an agent handles steering that arrives during an LLM stream. + +**Type:** Union + + + Truncate the in-flight stream and re-prompt with the injected message. + + + + Finish the in-flight stream before delivering the injected message. + + + +Custom or future stream-steering behavior. + +Values beginning with `_` are reserved for implementation-specific +extensions. Unknown values that do not begin with `_` are reserved for +future ACP variants. + + + ## SessionListCursor An opaque cursor used to paginate `session/list` results. diff --git a/schema-generator/Cargo.toml b/schema-generator/Cargo.toml index 9ce07c59f..feeefa9e4 100644 --- a/schema-generator/Cargo.toml +++ b/schema-generator/Cargo.toml @@ -13,6 +13,10 @@ workspace = true [features] unstable = ["agent-client-protocol-schema/unstable"] unstable_protocol_v2 = ["agent-client-protocol-schema/unstable_protocol_v2"] +unstable_session_inject = [ + "unstable_protocol_v2", + "agent-client-protocol-schema/unstable_session_inject", +] [dependencies] agent-client-protocol-schema = { workspace = true, features = ["schemars"] } diff --git a/schema-generator/src/main.rs b/schema-generator/src/main.rs index 63690a6a2..866a8f4e6 100644 --- a/schema-generator/src/main.rs +++ b/schema-generator/src/main.rs @@ -1803,6 +1803,9 @@ starting with '$/' it is free to ignore the notification." self.agent.get("SetSessionConfigOptionRequest").unwrap() } "session/prompt" => self.agent.get("PromptRequest").unwrap(), + "session/inject" => self.agent.get("InjectSessionRequest").unwrap(), + "session/revoke_inject" => self.agent.get("RevokeInjectSessionRequest").unwrap(), + "session/replace_inject" => self.agent.get("ReplaceInjectSessionRequest").unwrap(), "session/cancel" => self .agent .get("CancelSessionNotification") diff --git a/schema/v2/meta.unstable.json b/schema/v2/meta.unstable.json index 47d7973cb..a27a8b822 100644 --- a/schema/v2/meta.unstable.json +++ b/schema/v2/meta.unstable.json @@ -9,6 +9,9 @@ "session_new": "session/new", "session_set_config_option": "session/set_config_option", "session_prompt": "session/prompt", + "session_inject": "session/inject", + "session_revoke_inject": "session/revoke_inject", + "session_replace_inject": "session/replace_inject", "session_cancel": "session/cancel", "mcp_message": "mcp/message", "session_list": "session/list", diff --git a/schema/v2/schema.unstable.json b/schema/v2/schema.unstable.json index 6b5b6bd96..f5741ab0a 100644 --- a/schema/v2/schema.unstable.json +++ b/schema/v2/schema.unstable.json @@ -278,6 +278,33 @@ } ] }, + { + "title": "InjectSessionResponse", + "description": "Successful result returned for a `session/inject` request.", + "allOf": [ + { + "$ref": "#/$defs/InjectSessionResponse" + } + ] + }, + { + "title": "RevokeInjectSessionResponse", + "description": "Successful result returned for a `session/revoke_inject` request.", + "allOf": [ + { + "$ref": "#/$defs/RevokeInjectSessionResponse" + } + ] + }, + { + "title": "ReplaceInjectSessionResponse", + "description": "Successful result returned for a `session/replace_inject` request.", + "allOf": [ + { + "$ref": "#/$defs/ReplaceInjectSessionResponse" + } + ] + }, { "title": "StartNesResponse", "description": "Successful result returned for a `nes/start` request.", @@ -3244,6 +3271,33 @@ } ] }, + { + "title": "InjectSessionResponse", + "description": "Successful result returned for a `session/inject` request.", + "allOf": [ + { + "$ref": "#/$defs/InjectSessionResponse" + } + ] + }, + { + "title": "RevokeInjectSessionResponse", + "description": "Successful result returned for a `session/revoke_inject` request.", + "allOf": [ + { + "$ref": "#/$defs/RevokeInjectSessionResponse" + } + ] + }, + { + "title": "ReplaceInjectSessionResponse", + "description": "Successful result returned for a `session/replace_inject` request.", + "allOf": [ + { + "$ref": "#/$defs/ReplaceInjectSessionResponse" + } + ] + }, { "title": "StartNesResponse", "description": "Successful result returned for a `nes/start` request.", @@ -3528,6 +3582,18 @@ ], "x-deserialize-default-on-error": true }, + "inject": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nMid-turn user-message injection supported by the agent.\n\nOptional. Omitted or `null` means the agent does not support\n`session/inject` or the mandatory `session/revoke_inject` companion method.", + "anyOf": [ + { + "$ref": "#/$defs/SessionInjectCapabilities" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, "fork": { "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nWhether the agent supports `session/fork`.\n\nOptional. Omitted or `null` both mean the agent does not advertise support.\nSupplying `{}` means the agent supports forking sessions.", "anyOf": [ @@ -3740,6 +3806,127 @@ } } }, + "SessionInjectCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCapabilities for mid-turn user-message injection.\n\n`modes` must be non-empty. When it contains `steer`, `steerInStream` is\nrequired and must also be non-empty. Advertising this capability makes\n`session/revoke_inject` mandatory.", + "type": "object", + "properties": { + "modes": { + "description": "Supported injection delivery modes. Must be non-empty.", + "type": "array", + "items": { + "$ref": "#/$defs/SessionInjectMode" + }, + "minItems": 1 + }, + "steerInStream": { + "description": "Supported handling for steering during an LLM stream.\n\nRequired and non-empty when `modes` contains `steer`; otherwise optional.", + "type": ["array", "null"], + "items": { + "$ref": "#/$defs/SessionInjectSteerInStream" + }, + "minItems": 1 + }, + "pending": { + "description": "Capabilities for editing pending injected messages.\n\nOmitted or `null` means pending replacement is not supported.", + "anyOf": [ + { + "$ref": "#/$defs/SessionInjectPendingCapabilities" + }, + { + "type": "null" + } + ], + "x-deserialize-default-on-error": true + }, + "_meta": { + "description": "The _meta property is reserved by ACP for extension metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["modes"], + "allOf": [ + { + "if": { + "properties": { + "modes": { + "contains": { + "const": "steer" + } + } + }, + "required": ["modes"] + }, + "then": { + "properties": { + "steerInStream": { + "type": "array", + "minItems": 1 + } + }, + "required": ["steerInStream"] + } + } + ] + }, + "SessionInjectMode": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nDelivery mode for a message injected into a session.", + "anyOf": [ + { + "description": "Deliver after the current turn returns to idle.", + "type": "string", + "const": "queue" + }, + { + "description": "Deliver at the next safe breakpoint in the current turn.", + "type": "string", + "const": "steer" + }, + { + "title": "other", + "description": "Custom or future injection mode.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + ] + }, + "SessionInjectSteerInStream": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nHow an agent handles steering that arrives during an LLM stream.", + "anyOf": [ + { + "description": "Truncate the in-flight stream and re-prompt with the injected message.", + "type": "string", + "const": "interrupt" + }, + { + "description": "Finish the in-flight stream before delivering the injected message.", + "type": "string", + "const": "finish" + }, + { + "title": "other", + "description": "Custom or future stream-steering behavior.\n\nValues beginning with `_` are reserved for implementation-specific\nextensions. Unknown values that do not begin with `_` are reserved for\nfuture ACP variants.", + "type": "string" + } + ] + }, + "SessionInjectPendingCapabilities": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCapabilities for pending injected messages.", + "type": "object", + "properties": { + "replace": { + "description": "Whether `session/replace_inject` is supported.\n\nOptional. Omitted or `null` defaults to `false`.", + "type": ["boolean", "null"], + "x-deserialize-default-on-error": true + }, + "_meta": { + "description": "The _meta property is reserved by ACP for extension metadata.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + } + }, "SessionForkCapabilities": { "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nCapabilities for the `session/fork` method.\n\nSupplying `{}` means the agent supports forking sessions.", "type": "object", @@ -5093,6 +5280,70 @@ "x-side": "agent", "x-method": "session/prompt" }, + "InjectSessionResponse": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse acknowledging that an injected message is pending delivery.", + "type": "object", + "properties": { + "messageId": { + "description": "Agent-assigned ID for the pending message.", + "allOf": [ + { + "$ref": "#/$defs/MessageId" + } + ] + }, + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["messageId"], + "x-side": "agent", + "x-method": "session/inject" + }, + "MessageId": { + "description": "Unique identifier for a message within a session.", + "type": "string" + }, + "RevokeInjectSessionResponse": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse confirming that a pending injected message was revoked.", + "type": "object", + "properties": { + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "x-side": "agent", + "x-method": "session/revoke_inject" + }, + "ReplaceInjectSessionResponse": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nResponse confirming replacement of a pending injected message.", + "type": "object", + "properties": { + "messageId": { + "description": "The unchanged ID of the pending message.", + "allOf": [ + { + "$ref": "#/$defs/MessageId" + } + ] + }, + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["messageId"], + "x-side": "agent", + "x-method": "session/replace_inject" + }, "StartNesResponse": { "description": "Response to `nes/start`.", "type": "object", @@ -5615,6 +5866,13 @@ "format": "int32", "const": -32002 }, + { + "title": "Inject precondition failed", + "description": "**Inject precondition failed**: **UNSTABLE**\n\nThis error is not part of the spec yet, and may be removed or changed at any point.\n\nA session injection precondition failed.\n\n`error.data` uses a `reason` of `already_delivered`, `no_running_turn`,\nor `replace_not_supported`. Pending-message failures also include the\n`messageId`. Unknown message IDs instead use `-32002` with\n`data: { reason: \"unknown_message_id\", messageId }`. The error data remains\nopen JSON, consistent with the protocol's shared [`Error::data`] field.", + "type": "integer", + "format": "int32", + "const": -32010 + }, { "title": "Other", "description": "Other undefined error code.", @@ -6257,10 +6515,6 @@ } ] }, - "MessageId": { - "description": "Unique identifier for a message within a session.", - "type": "string" - }, "ContentChunk": { "description": "A streamed item of message content.", "type": "object", @@ -7703,6 +7957,33 @@ } ] }, + { + "title": "InjectSessionRequest", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nInjects a user message for pending delivery.", + "allOf": [ + { + "$ref": "#/$defs/InjectSessionRequest" + } + ] + }, + { + "title": "RevokeInjectSessionRequest", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRevokes a pending injected message. Mandatory when injection is advertised.", + "allOf": [ + { + "$ref": "#/$defs/RevokeInjectSessionRequest" + } + ] + }, + { + "title": "ReplaceInjectSessionRequest", + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nReplaces pending injected content when `pending.replace` is advertised.", + "allOf": [ + { + "$ref": "#/$defs/ReplaceInjectSessionRequest" + } + ] + }, { "title": "StartNesRequest", "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nStarts an NES session.", @@ -8804,6 +9085,113 @@ "x-side": "agent", "x-method": "session/prompt" }, + "InjectSessionRequest": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for injecting a user message into a session.\n\nThe response acknowledges pending delivery. The agent later delivers the\nmessage by emitting a `user_message` session update with the returned\n[`MessageId`].", + "type": "object", + "properties": { + "sessionId": { + "description": "The ID of the session that receives the injected message.", + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ] + }, + "mode": { + "description": "When the agent should deliver the message.", + "allOf": [ + { + "$ref": "#/$defs/SessionInjectMode" + } + ] + }, + "content": { + "description": "The content blocks that compose the injected user message.", + "type": "array", + "items": { + "$ref": "#/$defs/ContentBlock" + } + }, + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["sessionId", "mode", "content"], + "x-side": "agent", + "x-method": "session/inject" + }, + "RevokeInjectSessionRequest": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for revoking a pending injected message.\n\nEvery agent that advertises session injection must support this method. A\nsuccessful response guarantees that no future `user_message` update will be\nemitted for this message ID. If delivery wins the race, the agent returns\n`-32010` with `data: { reason: \"already_delivered\", messageId }`. An unknown\nmessage ID returns `-32002` with\n`data: { reason: \"unknown_message_id\", messageId }`.", + "type": "object", + "properties": { + "sessionId": { + "description": "The ID of the session that owns the pending message.", + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ] + }, + "messageId": { + "description": "The ID returned by `session/inject`.", + "allOf": [ + { + "$ref": "#/$defs/MessageId" + } + ] + }, + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["sessionId", "messageId"], + "x-side": "agent", + "x-method": "session/revoke_inject" + }, + "ReplaceInjectSessionRequest": { + "description": "**UNSTABLE**\n\nThis capability is not part of the spec yet, and may be removed or changed at any point.\n\nRequest parameters for replacing the content of a pending injected message.\n\nAgents support this optional method only when they advertise\n`session.inject.pending.replace`. Replacement preserves the message ID, mode,\nand pending-order position. Already-delivered messages return `-32010` with\n`data: { reason: \"already_delivered\", messageId }`; unknown message IDs return\n`-32002` with `data: { reason: \"unknown_message_id\", messageId }`. If replace\nwas not advertised, the agent returns method-not-found or `-32010` with\n`data: { reason: \"replace_not_supported\", messageId }`.", + "type": "object", + "properties": { + "sessionId": { + "description": "The ID of the session that owns the pending message.", + "allOf": [ + { + "$ref": "#/$defs/SessionId" + } + ] + }, + "messageId": { + "description": "The ID returned by `session/inject`.", + "allOf": [ + { + "$ref": "#/$defs/MessageId" + } + ] + }, + "content": { + "description": "Complete replacement content for the pending user message.", + "type": "array", + "items": { + "$ref": "#/$defs/ContentBlock" + } + }, + "_meta": { + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.", + "type": ["object", "null"], + "x-deserialize-default-on-error": true, + "additionalProperties": true + } + }, + "required": ["sessionId", "messageId", "content"], + "x-side": "agent", + "x-method": "session/replace_inject" + }, "StartNesRequest": { "description": "Request to start an NES session.", "type": "object",