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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions agent-client-protocol-schema/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 = []
Expand Down
83 changes: 83 additions & 0 deletions agent-client-protocol-schema/src/v2/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<SessionInjectCapabilities>,
/// **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.
Expand Down Expand Up @@ -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<SessionInjectCapabilities>) -> Self {
self.inject = inject.into_option();
self
}

#[cfg(feature = "unstable_session_fork")]
/// Whether the agent supports `session/fork`.
///
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -5184,6 +5231,27 @@ pub enum ClientRequest {
///
/// See protocol docs: [Prompt Lifecycle](https://agentclientprotocol.com/protocol/prompt-lifecycle)
PromptRequest(Box<PromptRequest>),
/// **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<InjectSessionRequest>),
/// **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<RevokeInjectSessionRequest>),
/// **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<ReplaceInjectSessionRequest>),
#[cfg(feature = "unstable_nes")]
/// **UNSTABLE**
///
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -5304,6 +5378,15 @@ pub enum AgentResponse {
SetSessionConfigOptionResponse(Box<SetSessionConfigOptionResponse>),
/// Successful result returned for a `session/prompt` request.
PromptResponse(Box<PromptResponse>),
/// Successful result returned for a `session/inject` request.
#[cfg(feature = "unstable_session_inject")]
InjectSessionResponse(Box<InjectSessionResponse>),
/// Successful result returned for a `session/revoke_inject` request.
#[cfg(feature = "unstable_session_inject")]
RevokeInjectSessionResponse(#[serde(default)] Box<RevokeInjectSessionResponse>),
/// Successful result returned for a `session/replace_inject` request.
#[cfg(feature = "unstable_session_inject")]
ReplaceInjectSessionResponse(Box<ReplaceInjectSessionResponse>),
/// Successful result returned for a `nes/start` request.
#[cfg(feature = "unstable_nes")]
StartNesResponse(Box<StartNesResponse>),
Expand Down
105 changes: 105 additions & 0 deletions agent-client-protocol-schema/src/v2/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<super::MessageId>) -> 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<super::MessageId>) -> 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<super::MessageId>) -> 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.
Expand Down Expand Up @@ -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")]
Expand All @@ -202,6 +255,8 @@ impl From<i32> for ErrorCode {
-32800 => ErrorCode::RequestCancelled,
-32000 => ErrorCode::AuthRequired,
-32002 => ErrorCode::ResourceNotFound,
#[cfg(feature = "unstable_session_inject")]
-32010 => ErrorCode::InjectPreconditionFailed,
_ => ErrorCode::Other(value),
}
}
Expand All @@ -218,6 +273,8 @@ impl From<ErrorCode> for i32 {
ErrorCode::RequestCancelled => -32800,
ErrorCode::AuthRequired => -32000,
ErrorCode::ResourceNotFound => -32002,
#[cfg(feature = "unstable_session_inject")]
ErrorCode::InjectPreconditionFailed => -32010,
ErrorCode::Other(value) => value,
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
},
})
);
}
}
4 changes: 4 additions & 0 deletions agent-client-protocol-schema/src/v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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::*;

Expand Down
Loading