Skip to content
Merged
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
14 changes: 14 additions & 0 deletions crates/goose-cli/src/session/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ mod tests {
id: "test-id".to_string(),
tool_call: Ok(tool_call),
metadata: None,
tool_meta: None,
};

let result = tool_request_to_markdown(&tool_request, true);
Expand All @@ -561,6 +562,7 @@ mod tests {
id: "test-id".to_string(),
tool_call: Ok(tool_call),
metadata: None,
tool_meta: None,
};

let result = tool_request_to_markdown(&tool_request, true);
Expand Down Expand Up @@ -701,6 +703,7 @@ mod tests {
id: "shell-cat".to_string(),
tool_call: Ok(tool_call),
metadata: None,
tool_meta: None,
};

let python_code = r#"#!/usr/bin/env python3
Expand Down Expand Up @@ -754,6 +757,7 @@ if __name__ == "__main__":
id: "git-status".to_string(),
tool_call: Ok(git_status_call),
metadata: None,
tool_meta: None,
};

let git_output = " M src/main.rs\n?? temp.txt\n A new_feature.rs";
Expand Down Expand Up @@ -799,6 +803,7 @@ if __name__ == "__main__":
id: "cargo-build".to_string(),
tool_call: Ok(cargo_build_call),
metadata: None,
tool_meta: None,
};

let build_output = r#" Compiling goose-cli v0.1.0 (/Users/user/goose)
Expand Down Expand Up @@ -850,6 +855,7 @@ warning: unused variable `x`
id: "curl-api".to_string(),
tool_call: Ok(curl_call),
metadata: None,
tool_meta: None,
};

let api_response = r#"{
Expand Down Expand Up @@ -905,6 +911,7 @@ warning: unused variable `x`
id: "editor-write".to_string(),
tool_call: Ok(editor_call),
metadata: None,
tool_meta: None,
};

let text_content = TextContent {
Expand Down Expand Up @@ -952,6 +959,7 @@ warning: unused variable `x`
id: "editor-view".to_string(),
tool_call: Ok(editor_call),
metadata: None,
tool_meta: None,
};

let python_code = r#"import os
Expand Down Expand Up @@ -1008,6 +1016,7 @@ def process_data(data: List[Dict]) -> List[Dict]:
id: "shell-error".to_string(),
tool_call: Ok(error_call),
metadata: None,
tool_meta: None,
};

let error_output = r#"python: can't open file 'nonexistent_script.py': [Errno 2] No such file or directory
Expand Down Expand Up @@ -1050,6 +1059,7 @@ Command failed with exit code 2"#;
id: "script-exec".to_string(),
tool_call: Ok(script_call),
metadata: None,
tool_meta: None,
};

let script_output = r#"Python 3.11.5 (main, Aug 24 2023, 15:18:16) [Clang 14.0.3 ]
Expand Down Expand Up @@ -1103,6 +1113,7 @@ Command failed with exit code 2"#;
id: "multi-cmd".to_string(),
tool_call: Ok(multi_call),
metadata: None,
tool_meta: None,
};

let multi_output = r#"total 24
Expand Down Expand Up @@ -1154,6 +1165,7 @@ drwx------ 3 user staff 96 Dec 6 16:20 com.apple.launchd.abc
id: "grep-search".to_string(),
tool_call: Ok(grep_call),
metadata: None,
tool_meta: None,
};

let grep_output = r#"src/main.rs:15:async fn process_request(req: Request) -> Result<Response> {
Expand Down Expand Up @@ -1204,6 +1216,7 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
id: "json-test".to_string(),
tool_call: Ok(tool_call),
metadata: None,
tool_meta: None,
};

let json_output = r#"{"status": "success", "data": {"count": 42}}"#;
Expand Down Expand Up @@ -1245,6 +1258,7 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
id: "npm-install".to_string(),
tool_call: Ok(npm_call),
metadata: None,
tool_meta: None,
};

let npm_output = r#"added 57 packages, and audited 58 packages in 3s
Expand Down
57 changes: 51 additions & 6 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use axum::{
};
use goose::config::PermissionManager;

use base64::Engine;
use goose::agents::ExtensionConfig;
use goose::config::{Config, GooseMode};
use goose::model::ModelConfig;
Expand Down Expand Up @@ -94,9 +95,15 @@ pub struct ReadResourceRequest {
uri: String,
}

#[derive(Serialize, Deserialize, utoipa::ToSchema)]
#[derive(Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ReadResourceResponse {
html: String,
uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
mime_type: Option<String>,
text: String,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
meta: Option<serde_json::Map<String, Value>>,
}

#[derive(Deserialize, utoipa::ToSchema)]
Expand Down Expand Up @@ -587,21 +594,59 @@ async fn read_resource(
State(state): State<Arc<AppState>>,
Json(payload): Json<ReadResourceRequest>,
) -> Result<Json<ReadResourceResponse>, StatusCode> {
use rmcp::model::ResourceContents;

let agent = state
.get_agent_for_route(payload.session_id.clone())
.await?;

let html = agent
let read_result = agent
.extension_manager
.read_ui_resource(
.read_resource(
&payload.uri,
&payload.extension_name,
CancellationToken::default(),
)
.await
.map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;

Ok(Json(ReadResourceResponse { html }))
let content = read_result
.contents
.into_iter()
.next()
.ok_or(StatusCode::NOT_FOUND)?;

let (uri, mime_type, text, meta) = match content {
ResourceContents::TextResourceContents {
uri,
mime_type,
text,
meta,
} => (uri, mime_type, text, meta),
ResourceContents::BlobResourceContents {
uri,
mime_type,
blob,
meta,
} => {
let decoded = match base64::engine::general_purpose::STANDARD.decode(&blob) {
Ok(bytes) => {
String::from_utf8(bytes).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
}
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
};
(uri, mime_type, decoded, meta)
}
};

let meta_map = meta.map(|m| m.0);

Ok(Json(ReadResourceResponse {
uri,
mime_type,
text,
meta: meta_map,
}))
}

#[utoipa::path(
Expand Down Expand Up @@ -649,7 +694,7 @@ async fn call_tool(
content: result.content,
structured_content: result.structured_content,
is_error: result.is_error.unwrap_or(false),
_meta: None,
_meta: result.meta.and_then(|m| serde_json::to_value(m).ok()),
}))
}

Expand Down
1 change: 1 addition & 0 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,7 @@ impl Agent {
request.id.clone(),
request.tool_call.clone(),
request.metadata.as_ref(),
request.tool_meta.clone(),
);
messages_to_add.push(request_msg);
let final_response = tool_response_messages[idx]
Expand Down
100 changes: 33 additions & 67 deletions crates/goose/src/agents/extension_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ use crate::oauth::oauth_flow;
use crate::prompt_template;
use crate::subprocess::configure_command_no_window;
use rmcp::model::{
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, RawContent,
Resource, ResourceContents, ServerInfo, Tool,
CallToolRequestParam, Content, ErrorCode, ErrorData, GetPromptResult, Prompt, Resource,
ResourceContents, ServerInfo, Tool,
};
use rmcp::transport::auth::AuthClient;
use schemars::_private::NoSerialize;
Expand Down Expand Up @@ -713,14 +713,13 @@ impl ExtensionManager {
input_schema: tool.input_schema,
annotations: tool.annotations,
output_schema: tool.output_schema,
icons: None,
title: None,
meta: None,
icons: tool.icons,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh - so will show the tool it is adding?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, this is just for completion sake. we're not really using icons as far as I can tell, but dropping these values on the floor can only be bad :)

title: tool.title,
meta: tool.meta,
});
}
}

// Exit loop when there are no more pages
if client_tools.next_cursor.is_none() {
break;
}
Expand Down Expand Up @@ -773,7 +772,7 @@ impl ExtensionManager {
}

// Function that gets executed for read_resource tool
pub async fn read_resource(
pub async fn read_resource_tool(
&self,
params: Value,
cancellation_token: CancellationToken,
Expand All @@ -784,14 +783,17 @@ impl ExtensionManager {

// If extension name is provided, we can just look it up
if extension_name.is_some() {
let result = self
.read_resource_from_extension(
uri,
extension_name.unwrap(),
cancellation_token.clone(),
true,
)
let read_result = self
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not your code but if thing.is_some() { ... thing.unwrap() ... } should be an if let Some(inner) = thing {...

.read_resource(uri, extension_name.unwrap(), cancellation_token.clone())
.await?;

let mut result = Vec::new();
for content in read_result.contents {
if let ResourceContents::TextResourceContents { text, .. } = content {
let content_str = format!("{}\n\n{}", uri, text);
result.push(Content::text(content_str));
}
}
return Ok(result);
}

Expand All @@ -804,16 +806,20 @@ impl ExtensionManager {
let extension_names: Vec<String> = self.extensions.lock().await.keys().cloned().collect();

for extension_name in extension_names {
let result = self
.read_resource_from_extension(
uri,
&extension_name,
cancellation_token.clone(),
true,
)
let read_result = self
.read_resource(uri, &extension_name, cancellation_token.clone())
.await;
match result {
Ok(result) => return Ok(result),
match read_result {
Ok(read_result) => {
let mut result = Vec::new();
for content in read_result.contents {
if let ResourceContents::TextResourceContents { text, .. } = content {
let content_str = format!("{}\n\n{}", uri, text);
result.push(Content::text(content_str));
}
}
return Ok(result);
}
Err(_) => continue,
}
}
Expand All @@ -839,13 +845,12 @@ impl ExtensionManager {
))
}

async fn read_resource_from_extension(
pub async fn read_resource(
&self,
uri: &str,
extension_name: &str,
cancellation_token: CancellationToken,
format_with_uri: bool,
) -> Result<Vec<Content>, ErrorData> {
) -> Result<rmcp::model::ReadResourceResult, ErrorData> {
let available_extensions = self
.extensions
.lock()
Expand All @@ -865,7 +870,7 @@ impl ExtensionManager {
.ok_or(ErrorData::new(ErrorCode::INVALID_PARAMS, error_msg, None))?;

let client_guard = client.lock().await;
let read_result = client_guard
client_guard
.read_resource(uri, cancellation_token)
.await
.map_err(|_| {
Expand All @@ -874,21 +879,7 @@ impl ExtensionManager {
format!("Could not read resource with uri: {}", uri),
None,
)
})?;

let mut result = Vec::new();
for content in read_result.contents {
if let ResourceContents::TextResourceContents { text, .. } = content {
let content_str = if format_with_uri {
format!("{}\n\n{}", uri, text)
} else {
text
};
result.push(Content::text(content_str));
}
}

Ok(result)
})
}

pub async fn get_ui_resources(&self) -> Result<Vec<(String, Resource)>, ErrorData> {
Expand Down Expand Up @@ -925,31 +916,6 @@ impl ExtensionManager {
Ok(ui_resources)
}

pub async fn read_ui_resource(
&self,
uri: &str,
extension_name: &str,
cancellation_token: CancellationToken,
) -> Result<String, ErrorData> {
let contents = self
.read_resource_from_extension(uri, extension_name, cancellation_token, false)
.await?;

contents
.into_iter()
.find_map(|c| match c.raw {
RawContent::Text(text_content) => Some(text_content.text),
_ => None,
})
.ok_or_else(|| {
ErrorData::new(
ErrorCode::RESOURCE_NOT_FOUND,
format!("No text content in resource '{}'", uri),
None,
)
})
}

async fn list_resources_from_extension(
&self,
extension_name: &str,
Expand Down
2 changes: 1 addition & 1 deletion crates/goose/src/agents/extension_manager_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ impl ExtensionManagerClient {
.unwrap_or(serde_json::Value::Object(serde_json::Map::new()));

match extension_manager
.read_resource(params, tokio_util::sync::CancellationToken::default())
.read_resource_tool(params, tokio_util::sync::CancellationToken::default())
.await
{
Ok(content) => Ok(content),
Expand Down
Loading
Loading