diff --git a/Cargo.lock b/Cargo.lock index e36d91c55570..8aa1e4e4ffc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,19 +30,33 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.4.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2526e80463b9742afed4829aedd6ae5632d6db778c6cc1fecb80c960c3521b" +checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13" dependencies = [ + "agent-client-protocol-schema", "anyhow", "async-broadcast", "async-trait", + "derive_more", "futures", "log", - "parking_lot", + "serde", + "serde_json", +] + +[[package]] +name = "agent-client-protocol-schema" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cbb9a55080793c01e5e97c6fb0f1032d1f2460f7e3869225813cfa29001585" +dependencies = [ + "anyhow", + "derive_more", "schemars", "serde", "serde_json", + "strum", ] [[package]] @@ -1657,7 +1671,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" dependencies = [ "async-trait", - "convert_case", + "convert_case 0.6.0", "json5", "nom", "pathdiff", @@ -1738,6 +1752,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -2175,6 +2198,29 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "derive_more" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.111", + "unicode-xid", +] + [[package]] name = "devgen-tree-sitter-swift" version = "0.21.0" @@ -7278,6 +7324,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "subtle" version = "2.6.1" @@ -8338,6 +8405,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unicode_categories" version = "0.1.1" diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 6f573d97eb1e..bb25e245449f 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -19,7 +19,7 @@ goose = { path = "../goose" } goose-bench = { path = "../goose-bench" } goose-mcp = { path = "../goose-mcp" } rmcp = { workspace = true } -agent-client-protocol = "0.4.0" +agent-client-protocol = "0.9.0" clap = { version = "4.4", features = ["derive"] } cliclack = "0.3.5" console = "0.15.8" diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs index 89242bc6560b..0f3a53a6be2f 100644 --- a/crates/goose-cli/src/commands/acp.rs +++ b/crates/goose-cli/src/commands/acp.rs @@ -1,6 +1,6 @@ use agent_client_protocol::{ - self as acp, Client, EmbeddedResource, ImageContent, SessionNotification, TextContent, - ToolCallContent, + self as acp, Client, Content, ContentChunk, EmbeddedResource, ExtResponse, ImageContent, + ProtocolVersion, SessionNotification, TextContent, ToolCallContent, }; use anyhow::Result; use goose::agents::{Agent, SessionConfig}; @@ -37,11 +37,7 @@ struct GooseAcpAgent { /// Create a ToolCallLocation with common defaults fn create_tool_location(path: &str, line: Option) -> acp::ToolCallLocation { - acp::ToolCallLocation { - path: path.into(), - line, - meta: None, - } + acp::ToolCallLocation::new(path).line(line) } /// Extract file locations from tool request and response @@ -320,6 +316,7 @@ impl GooseAcpAgent { } } acp::ContentBlock::Audio(..) => (), + _ => (), } } @@ -338,13 +335,12 @@ impl GooseAcpAgent { let (tx, rx) = oneshot::channel(); self.session_update_tx .send(( - SessionNotification { - session_id: session_id.clone(), - update: acp::SessionUpdate::AgentMessageChunk { - content: text.text.clone().into(), - }, - meta: None, - }, + SessionNotification::new( + session_id.clone(), + acp::SessionUpdate::AgentMessageChunk(ContentChunk::new( + acp::ContentBlock::Text(TextContent::new(text.text.clone())), + )), + ), tx, )) .map_err(|_| acp::Error::internal_error())?; @@ -363,13 +359,14 @@ impl GooseAcpAgent { let (tx, rx) = oneshot::channel(); self.session_update_tx .send(( - SessionNotification { - session_id: session_id.clone(), - update: acp::SessionUpdate::AgentThoughtChunk { - content: thinking.thinking.clone().into(), - }, - meta: None, - }, + SessionNotification::new( + session_id.clone(), + acp::SessionUpdate::AgentThoughtChunk(ContentChunk::new( + acp::ContentBlock::Text(TextContent::new( + thinking.thinking.clone(), + )), + )), + ), tx, )) .map_err(|_| acp::Error::internal_error())?; @@ -410,21 +407,16 @@ impl GooseAcpAgent { let (tx, rx) = oneshot::channel(); self.session_update_tx .send(( - SessionNotification { - session_id: session_id.clone(), - update: acp::SessionUpdate::ToolCall(acp::ToolCall { - id: acp::ToolCallId(acp_tool_id.clone().into()), - title: format_tool_name(&tool_name), - kind: acp::ToolKind::default(), - status: acp::ToolCallStatus::Pending, - content: Vec::new(), - locations: Vec::new(), // Will be populated in handle_tool_response - raw_input: None, - raw_output: None, - meta: None, - }), - meta: None, - }, + SessionNotification::new( + session_id.clone(), + acp::SessionUpdate::ToolCall( + acp::ToolCall::new( + acp::ToolCallId::new(acp_tool_id.clone()), + format_tool_name(&tool_name), + ) + .status(acp::ToolCallStatus::Pending), + ), + ), tx, )) .map_err(|_| acp::Error::internal_error())?; @@ -462,24 +454,20 @@ impl GooseAcpAgent { let (tx, rx) = oneshot::channel(); self.session_update_tx .send(( - SessionNotification { - session_id: session_id.clone(), - update: acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate { - id: acp::ToolCallId(acp_tool_id.clone().into()), - fields: acp::ToolCallUpdateFields { - status: Some(status), - content: Some(content), - locations: if locations.is_empty() { + SessionNotification::new( + session_id.clone(), + acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new( + acp::ToolCallId::new(acp_tool_id.clone()), + acp::ToolCallUpdateFields::new() + .status(status) + .content(content) + .locations(if locations.is_empty() { None } else { Some(locations) - }, - ..Default::default() - }, - meta: None, - }), - meta: None, - }, + }), + )), + ), tx, )) .map_err(|_| acp::Error::internal_error())?; @@ -497,56 +485,37 @@ fn build_tool_call_content(tool_result: &ToolResult) -> Vec Some(ToolCallContent::Content { - content: acp::ContentBlock::Text(TextContent { - annotations: None, - text: val.text.clone(), - meta: None, - }), - }), - RawContent::Image(val) => Some(ToolCallContent::Content { - content: acp::ContentBlock::Image(ImageContent { - annotations: None, - data: val.data.clone(), - mime_type: val.mime_type.clone(), - uri: None, - meta: None, - }), - }), - RawContent::Resource(val) => Some(ToolCallContent::Content { - content: acp::ContentBlock::Resource(EmbeddedResource { - annotations: None, - resource: match &val.resource { - ResourceContents::TextResourceContents { - mime_type, - text, - uri, - .. - } => acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents { - mime_type: mime_type.clone(), - text: text.clone(), - uri: uri.clone(), - meta: None, - }, - ), - ResourceContents::BlobResourceContents { - mime_type, - blob, - uri, - .. - } => acp::EmbeddedResourceResource::BlobResourceContents( - acp::BlobResourceContents { - mime_type: mime_type.clone(), - blob: blob.clone(), - uri: uri.clone(), - meta: None, - }, - ), - }, - meta: None, - }), - }), + RawContent::Text(val) => Some(ToolCallContent::Content(Content::new( + acp::ContentBlock::Text(TextContent::new(val.text.clone())), + ))), + RawContent::Image(val) => Some(ToolCallContent::Content(Content::new( + acp::ContentBlock::Image(ImageContent::new( + val.data.clone(), + val.mime_type.clone(), + )), + ))), + RawContent::Resource(val) => Some(ToolCallContent::Content(Content::new( + acp::ContentBlock::Resource(EmbeddedResource::new(match &val.resource { + ResourceContents::TextResourceContents { + mime_type, + text, + uri, + .. + } => acp::EmbeddedResourceResource::TextResourceContents( + acp::TextResourceContents::new(text.clone(), uri.clone()) + .mime_type(mime_type.clone()), + ), + ResourceContents::BlobResourceContents { + mime_type, + blob, + uri, + .. + } => acp::EmbeddedResourceResource::BlobResourceContents( + acp::BlobResourceContents::new(blob.clone(), uri.clone()) + .mime_type(mime_type.clone()), + ), + })), + ))), RawContent::Audio(_) => { // Audio content is not supported in ACP ContentBlock, skip it None @@ -570,28 +539,17 @@ impl acp::Agent for GooseAcpAgent { info!("ACP: Received initialize request {:?}", args); // Advertise Goose's capabilities - let agent_capabilities = acp::AgentCapabilities { - load_session: true, - prompt_capabilities: acp::PromptCapabilities { - image: true, // Goose supports image inputs via providers - audio: false, // TODO: Add audio support when providers support it - embedded_context: true, // Goose can handle embedded context resources - meta: None, - }, - mcp_capabilities: acp::McpCapabilities { - http: false, // TODO: Add MCP HTTP support if needed - sse: false, // TODO: Add MCP SSE support if needed - meta: None, - }, - meta: None, - }; - - Ok(acp::InitializeResponse { - protocol_version: acp::V1, - agent_capabilities, - auth_methods: Vec::new(), - meta: None, - }) + let agent_capabilities = acp::AgentCapabilities::new() + .load_session(true) + .prompt_capabilities( + acp::PromptCapabilities::new() + .image(true) + .embedded_context(true), + ); + Ok( + acp::InitializeResponse::new(ProtocolVersion::V1) + .agent_capabilities(agent_capabilities), + ) } async fn authenticate( @@ -599,7 +557,7 @@ impl acp::Agent for GooseAcpAgent { args: acp::AuthenticateRequest, ) -> Result { info!("ACP: Received authenticate request {:?}", args); - Ok(acp::AuthenticateResponse { meta: None }) + Ok(acp::AuthenticateResponse::new()) } async fn new_session( @@ -627,11 +585,7 @@ impl acp::Agent for GooseAcpAgent { info!("Created new ACP/goose session {}", goose_session.id); - Ok(acp::NewSessionResponse { - session_id: acp::SessionId(goose_session.id.into()), - modes: None, - meta: None, - }) + Ok(acp::NewSessionResponse::new(goose_session.id)) } async fn load_session( @@ -681,21 +635,17 @@ impl acp::Agent for GooseAcpAgent { match content_item { MessageContent::Text(text) => { let update = match message.role { - Role::User => acp::SessionUpdate::UserMessageChunk { - content: text.text.clone().into(), - }, - Role::Assistant => acp::SessionUpdate::AgentMessageChunk { - content: text.text.clone().into(), - }, + Role::User => acp::SessionUpdate::UserMessageChunk(ContentChunk::new( + text.text.clone().into(), + )), + Role::Assistant => acp::SessionUpdate::AgentMessageChunk( + ContentChunk::new(text.text.clone().into()), + ), }; let (tx, rx) = oneshot::channel(); self.session_update_tx .send(( - SessionNotification { - session_id: args.session_id.clone(), - update, - meta: None, - }, + SessionNotification::new(args.session_id.clone(), update), tx, )) .map_err(|_| acp::Error::internal_error())?; @@ -713,13 +663,12 @@ impl acp::Agent for GooseAcpAgent { let (tx, rx) = oneshot::channel(); self.session_update_tx .send(( - SessionNotification { - session_id: args.session_id.clone(), - update: acp::SessionUpdate::AgentThoughtChunk { - content: thinking.thinking.clone().into(), - }, - meta: None, - }, + SessionNotification::new( + args.session_id.clone(), + acp::SessionUpdate::AgentThoughtChunk(ContentChunk::new( + thinking.thinking.clone().into(), + )), + ), tx, )) .map_err(|_| acp::Error::internal_error())?; @@ -737,10 +686,7 @@ impl acp::Agent for GooseAcpAgent { info!("Loaded ACP session {}", session_id); - Ok(acp::LoadSessionResponse { - modes: None, - meta: None, - }) + Ok(acp::LoadSessionResponse::new()) } async fn prompt(&self, args: acp::PromptRequest) -> Result { @@ -810,14 +756,11 @@ impl acp::Agent for GooseAcpAgent { session.cancel_token = None; } - Ok(acp::PromptResponse { - stop_reason: if was_cancelled { - acp::StopReason::Cancelled - } else { - acp::StopReason::EndTurn - }, - meta: None, - }) + Ok(acp::PromptResponse::new(if was_cancelled { + acp::StopReason::Cancelled + } else { + acp::StopReason::EndTurn + })) } async fn cancel(&self, args: acp::CancelNotification) -> Result<(), acp::Error> { @@ -842,20 +785,14 @@ impl acp::Agent for GooseAcpAgent { &self, _args: acp::SetSessionModeRequest, ) -> Result { - // TODO: Implement session modes if needed Err(acp::Error::method_not_found()) } - async fn ext_method( - &self, - _args: acp::ExtRequest, - ) -> Result, acp::Error> { - // TODO: Implement extension methods if needed + async fn ext_method(&self, _args: acp::ExtRequest) -> Result { Err(acp::Error::method_not_found()) } async fn ext_notification(&self, _args: acp::ExtNotification) -> Result<(), acp::Error> { - // TODO: Implement extension notifications if needed Ok(()) } } @@ -917,21 +854,14 @@ mod tests { let mut file = NamedTempFile::new()?; file.write_all(content.as_bytes())?; - let link = ResourceLink { - annotations: None, - description: None, - mime_type: None, - name: file - .path() + let link = ResourceLink::new( + file.path() .file_name() .unwrap() .to_string_lossy() .to_string(), - size: None, - title: None, - uri: format!("file://{}", file.path().to_str().unwrap()), - meta: None, - }; + format!("file://{}", file.path().to_str().unwrap()), + ); Ok((link, file)) } diff --git a/crates/goose-mcp/src/computercontroller/mod.rs b/crates/goose-mcp/src/computercontroller/mod.rs index 8042975b9d90..3e258af0c794 100644 --- a/crates/goose-mcp/src/computercontroller/mod.rs +++ b/crates/goose-mcp/src/computercontroller/mod.rs @@ -1122,7 +1122,7 @@ impl ComputerControllerServer { let json_params = params .params .as_ref() - .map(|p| serde_json::to_value(p).unwrap_or_else(|_| serde_json::Value::Null)); + .map(|p| serde_json::to_value(p).unwrap_or(serde_json::Value::Null)); let result = crate::computercontroller::docx_tool::docx_tool( path,