feat(goose-acp): enable parallel sessions with isolated agent state (#6392)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::agents::types::SharedProvider;
|
||||
use crate::session_context::SESSION_ID_HEADER;
|
||||
use rmcp::model::{
|
||||
Content, CreateElicitationRequestParam, CreateElicitationResult, ElicitationAction, ErrorCode,
|
||||
JsonObject,
|
||||
Extensions, JsonObject, Meta,
|
||||
};
|
||||
/// MCP client implementation for Goose
|
||||
use rmcp::{
|
||||
@@ -37,20 +37,25 @@ pub type BoxError = Box<dyn std::error::Error + Sync + Send>;
|
||||
|
||||
pub type Error = rmcp::ServiceError;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct McpMeta {
|
||||
pub session_id: String,
|
||||
}
|
||||
|
||||
impl McpMeta {
|
||||
pub fn new(session_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
session_id: session_id.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn inject_into_extensions(&self, extensions: Extensions) -> Extensions {
|
||||
inject_session_id_into_extensions(extensions, &self.session_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait McpClientTrait: Send + Sync {
|
||||
async fn list_resources(
|
||||
&self,
|
||||
next_cursor: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ListResourcesResult, Error>;
|
||||
|
||||
async fn read_resource(
|
||||
&self,
|
||||
uri: &str,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ReadResourceResult, Error>;
|
||||
|
||||
async fn list_tools(
|
||||
&self,
|
||||
next_cursor: Option<String>,
|
||||
@@ -61,27 +66,50 @@ pub trait McpClientTrait: Send + Sync {
|
||||
&self,
|
||||
name: &str,
|
||||
arguments: Option<JsonObject>,
|
||||
meta: McpMeta,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<CallToolResult, Error>;
|
||||
|
||||
fn get_info(&self) -> Option<&InitializeResult>;
|
||||
|
||||
async fn list_resources(
|
||||
&self,
|
||||
_next_cursor: Option<String>,
|
||||
_cancel_token: CancellationToken,
|
||||
) -> Result<ListResourcesResult, Error> {
|
||||
Err(Error::TransportClosed)
|
||||
}
|
||||
|
||||
async fn read_resource(
|
||||
&self,
|
||||
_uri: &str,
|
||||
_cancel_token: CancellationToken,
|
||||
) -> Result<ReadResourceResult, Error> {
|
||||
Err(Error::TransportClosed)
|
||||
}
|
||||
|
||||
async fn list_prompts(
|
||||
&self,
|
||||
next_cursor: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<ListPromptsResult, Error>;
|
||||
_next_cursor: Option<String>,
|
||||
_cancel_token: CancellationToken,
|
||||
) -> Result<ListPromptsResult, Error> {
|
||||
Err(Error::TransportClosed)
|
||||
}
|
||||
|
||||
async fn get_prompt(
|
||||
&self,
|
||||
name: &str,
|
||||
arguments: Value,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<GetPromptResult, Error>;
|
||||
_name: &str,
|
||||
_arguments: Value,
|
||||
_cancel_token: CancellationToken,
|
||||
) -> Result<GetPromptResult, Error> {
|
||||
Err(Error::TransportClosed)
|
||||
}
|
||||
|
||||
async fn subscribe(&self) -> mpsc::Receiver<ServerNotification>;
|
||||
async fn subscribe(&self) -> mpsc::Receiver<ServerNotification> {
|
||||
mpsc::channel(1).1
|
||||
}
|
||||
|
||||
fn get_info(&self) -> Option<&InitializeResult>;
|
||||
|
||||
async fn get_moim(&self) -> Option<String> {
|
||||
async fn get_moim(&self, _session_id: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -379,7 +407,7 @@ impl McpClientTrait for McpClient {
|
||||
ClientRequest::ListResourcesRequest(ListResourcesRequest {
|
||||
params: Some(PaginatedRequestParam { cursor }),
|
||||
method: Default::default(),
|
||||
extensions: inject_session_into_extensions(Default::default()),
|
||||
extensions: inject_current_session_id_into_extensions(Default::default()),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
@@ -403,7 +431,7 @@ impl McpClientTrait for McpClient {
|
||||
uri: uri.to_string(),
|
||||
},
|
||||
method: Default::default(),
|
||||
extensions: inject_session_into_extensions(Default::default()),
|
||||
extensions: inject_current_session_id_into_extensions(Default::default()),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
@@ -425,7 +453,7 @@ impl McpClientTrait for McpClient {
|
||||
ClientRequest::ListToolsRequest(ListToolsRequest {
|
||||
params: Some(PaginatedRequestParam { cursor }),
|
||||
method: Default::default(),
|
||||
extensions: inject_session_into_extensions(Default::default()),
|
||||
extensions: inject_current_session_id_into_extensions(Default::default()),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
@@ -441,6 +469,7 @@ impl McpClientTrait for McpClient {
|
||||
&self,
|
||||
name: &str,
|
||||
arguments: Option<JsonObject>,
|
||||
meta: McpMeta,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<CallToolResult, Error> {
|
||||
let res = self
|
||||
@@ -451,7 +480,7 @@ impl McpClientTrait for McpClient {
|
||||
arguments,
|
||||
},
|
||||
method: Default::default(),
|
||||
extensions: inject_session_into_extensions(Default::default()),
|
||||
extensions: meta.inject_into_extensions(Default::default()),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
@@ -473,7 +502,7 @@ impl McpClientTrait for McpClient {
|
||||
ClientRequest::ListPromptsRequest(ListPromptsRequest {
|
||||
params: Some(PaginatedRequestParam { cursor }),
|
||||
method: Default::default(),
|
||||
extensions: inject_session_into_extensions(Default::default()),
|
||||
extensions: inject_current_session_id_into_extensions(Default::default()),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
@@ -503,7 +532,7 @@ impl McpClientTrait for McpClient {
|
||||
arguments,
|
||||
},
|
||||
method: Default::default(),
|
||||
extensions: inject_session_into_extensions(Default::default()),
|
||||
extensions: inject_current_session_id_into_extensions(Default::default()),
|
||||
}),
|
||||
cancel_token,
|
||||
)
|
||||
@@ -522,29 +551,34 @@ impl McpClientTrait for McpClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces session ID, case-insensitively, in Extensions._meta.
|
||||
fn inject_session_into_extensions(
|
||||
mut extensions: rmcp::model::Extensions,
|
||||
) -> rmcp::model::Extensions {
|
||||
use rmcp::model::Meta;
|
||||
/// Injects the given session_id into Extensions._meta.
|
||||
fn inject_session_id_into_extensions(mut extensions: Extensions, session_id: &str) -> Extensions {
|
||||
let mut meta_map = extensions
|
||||
.get::<Meta>()
|
||||
.map(|meta| meta.0.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(session_id) = crate::session_context::current_session_id() {
|
||||
let mut meta_map = extensions
|
||||
.get::<Meta>()
|
||||
.map(|meta| meta.0.clone())
|
||||
.unwrap_or_default();
|
||||
// JsonObject is case-sensitive, so we use retain for case-insensitive removal
|
||||
meta_map.retain(|k, _| !k.eq_ignore_ascii_case(SESSION_ID_HEADER));
|
||||
|
||||
// JsonObject is case-sensitive, so we use retain for case-insensitive removal
|
||||
meta_map.retain(|k, _| !k.eq_ignore_ascii_case(SESSION_ID_HEADER));
|
||||
|
||||
meta_map.insert(SESSION_ID_HEADER.to_string(), Value::String(session_id));
|
||||
|
||||
extensions.insert(Meta(meta_map));
|
||||
}
|
||||
meta_map.insert(
|
||||
SESSION_ID_HEADER.to_string(),
|
||||
Value::String(session_id.to_string()),
|
||||
);
|
||||
|
||||
extensions.insert(Meta(meta_map));
|
||||
extensions
|
||||
}
|
||||
|
||||
/// Injects session ID from task-local context into Extensions._meta.
|
||||
fn inject_current_session_id_into_extensions(extensions: Extensions) -> Extensions {
|
||||
if let Some(session_id) = crate::session_context::current_session_id() {
|
||||
inject_session_id_into_extensions(extensions, &session_id)
|
||||
} else {
|
||||
extensions
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -556,7 +590,7 @@ mod tests {
|
||||
|
||||
let session_id = "test-session-789";
|
||||
crate::session_context::with_session_id(Some(session_id.to_string()), async {
|
||||
let extensions = inject_session_into_extensions(Default::default());
|
||||
let extensions = inject_current_session_id_into_extensions(Default::default());
|
||||
let meta = extensions.get::<Meta>().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@@ -573,7 +607,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_session_id_in_mcp_when_absent() {
|
||||
let extensions = inject_session_into_extensions(Default::default());
|
||||
let extensions = inject_current_session_id_into_extensions(Default::default());
|
||||
let meta = extensions.get::<Meta>();
|
||||
|
||||
assert!(meta.is_none());
|
||||
@@ -585,9 +619,9 @@ mod tests {
|
||||
|
||||
let session_id = "consistent-session-id";
|
||||
crate::session_context::with_session_id(Some(session_id.to_string()), async {
|
||||
let ext1 = inject_session_into_extensions(Default::default());
|
||||
let ext2 = inject_session_into_extensions(Default::default());
|
||||
let ext3 = inject_session_into_extensions(Default::default());
|
||||
let ext1 = inject_current_session_id_into_extensions(Default::default());
|
||||
let ext2 = inject_current_session_id_into_extensions(Default::default());
|
||||
let ext3 = inject_current_session_id_into_extensions(Default::default());
|
||||
|
||||
for ext in [&ext1, &ext2, &ext3] {
|
||||
assert_eq!(
|
||||
@@ -620,7 +654,7 @@ mod tests {
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let extensions = inject_session_into_extensions(extensions);
|
||||
let extensions = inject_current_session_id_into_extensions(extensions);
|
||||
let meta = extensions.get::<Meta>().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user