huskies: merge 855
This commit is contained in:
@@ -93,6 +93,13 @@ pub(super) fn parse_usage_metadata(response: &Value) -> Option<TokenUsage> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http::context::AppContext;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn test_app_ctx() -> Arc<AppContext> {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
Arc::new(AppContext::new_test(tmp.path().to_path_buf()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_system_instruction_uses_args() {
|
||||
@@ -107,7 +114,7 @@ mod tests {
|
||||
prompt: "Do the thing".to_string(),
|
||||
cwd: "/tmp/wt".to_string(),
|
||||
inactivity_timeout_secs: 300,
|
||||
mcp_port: 3001,
|
||||
app_ctx: Some(test_app_ctx()),
|
||||
session_id_to_resume: None,
|
||||
fresh_prompt: None,
|
||||
};
|
||||
@@ -126,7 +133,7 @@ mod tests {
|
||||
prompt: "Do the thing".to_string(),
|
||||
cwd: "/tmp/wt".to_string(),
|
||||
inactivity_timeout_secs: 300,
|
||||
mcp_port: 3001,
|
||||
app_ctx: Some(test_app_ctx()),
|
||||
session_id_to_resume: None,
|
||||
fresh_prompt: None,
|
||||
};
|
||||
|
||||
@@ -1,45 +1,25 @@
|
||||
//! MCP tool fetching, schema conversion, and tool invocation for the Gemini runtime.
|
||||
use reqwest::Client;
|
||||
//! MCP tool schema conversion for the Gemini runtime.
|
||||
//!
|
||||
//! Tool definitions are loaded directly from `list_tools()` and tool
|
||||
//! invocations go through `dispatch_tool_call()` — no HTTP round-trip.
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::slog;
|
||||
|
||||
use crate::http::mcp::tools_list::list_tools;
|
||||
|
||||
use super::api::GeminiFunctionDeclaration;
|
||||
|
||||
// ── MCP tool fetching ────────────────────────────────────────────────
|
||||
// ── MCP tool loading ────────────────────────────────────────────────
|
||||
|
||||
/// Fetch MCP tool definitions from huskies' MCP server and convert
|
||||
/// them to Gemini function declaration format.
|
||||
pub(super) async fn fetch_and_convert_mcp_tools(
|
||||
client: &Client,
|
||||
mcp_base: &str,
|
||||
) -> Result<Vec<GeminiFunctionDeclaration>, String> {
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
"params": {}
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(mcp_base)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to fetch MCP tools: {e}"))?;
|
||||
|
||||
let body: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse MCP tools response: {e}"))?;
|
||||
|
||||
let tools = body["result"]["tools"]
|
||||
.as_array()
|
||||
.ok_or_else(|| "No tools array in MCP response".to_string())?;
|
||||
/// Load MCP tool definitions directly and convert to Gemini function
|
||||
/// declaration format.
|
||||
pub(super) fn convert_mcp_tools_to_gemini() -> Vec<GeminiFunctionDeclaration> {
|
||||
let tools = list_tools();
|
||||
|
||||
let mut declarations = Vec::new();
|
||||
|
||||
for tool in tools {
|
||||
for tool in &tools {
|
||||
let name = tool["name"].as_str().unwrap_or("").to_string();
|
||||
let description = tool["description"].as_str().unwrap_or("").to_string();
|
||||
|
||||
@@ -47,9 +27,6 @@ pub(super) async fn fetch_and_convert_mcp_tools(
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert MCP inputSchema (JSON Schema) to Gemini parameters
|
||||
// (OpenAPI-subset schema). They are structurally compatible for
|
||||
// simple object schemas.
|
||||
let parameters = convert_mcp_schema_to_gemini(tool.get("inputSchema"));
|
||||
|
||||
declarations.push(GeminiFunctionDeclaration {
|
||||
@@ -63,54 +40,7 @@ pub(super) async fn fetch_and_convert_mcp_tools(
|
||||
"[gemini] Loaded {} MCP tools as function declarations",
|
||||
declarations.len()
|
||||
);
|
||||
Ok(declarations)
|
||||
}
|
||||
|
||||
/// Call an MCP tool via huskies' MCP server.
|
||||
pub(super) async fn call_mcp_tool(
|
||||
client: &Client,
|
||||
mcp_base: &str,
|
||||
tool_name: &str,
|
||||
args: &Value,
|
||||
) -> Result<String, String> {
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": tool_name,
|
||||
"arguments": args
|
||||
}
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(mcp_base)
|
||||
.json(&request)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("MCP tool call failed: {e}"))?;
|
||||
|
||||
let body: Value = response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to parse MCP tool response: {e}"))?;
|
||||
|
||||
if let Some(error) = body.get("error") {
|
||||
let msg = error["message"].as_str().unwrap_or("Unknown MCP error");
|
||||
return Err(format!("MCP tool '{tool_name}' error: {msg}"));
|
||||
}
|
||||
|
||||
// MCP tools/call returns { result: { content: [{ type: "text", text: "..." }] } }
|
||||
let content = &body["result"]["content"];
|
||||
if let Some(arr) = content.as_array() {
|
||||
let texts: Vec<&str> = arr.iter().filter_map(|c| c["text"].as_str()).collect();
|
||||
if !texts.is_empty() {
|
||||
return Ok(texts.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to serializing the entire result.
|
||||
Ok(body["result"].to_string())
|
||||
declarations
|
||||
}
|
||||
|
||||
// ── Schema conversion ────────────────────────────────────────────────
|
||||
|
||||
@@ -7,6 +7,7 @@ use serde_json::json;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::agent_log::AgentLogWriter;
|
||||
use crate::http::mcp::dispatch::dispatch_tool_call;
|
||||
use crate::slog;
|
||||
|
||||
use super::super::{AgentEvent, TokenUsage};
|
||||
@@ -16,7 +17,7 @@ mod api;
|
||||
mod mcp;
|
||||
|
||||
use api::{build_generate_content_request, build_system_instruction, parse_usage_metadata};
|
||||
use mcp::{call_mcp_tool, fetch_and_convert_mcp_tools};
|
||||
use mcp::convert_mcp_tools_to_gemini;
|
||||
|
||||
// ── Internal types ───────────────────────────────────────────────────
|
||||
|
||||
@@ -79,14 +80,16 @@ impl AgentRuntime for GeminiRuntime {
|
||||
.unwrap_or_else(|| "gemini-2.5-pro".to_string())
|
||||
};
|
||||
|
||||
let mcp_port = ctx.mcp_port;
|
||||
let mcp_base = format!("http://localhost:{mcp_port}/mcp");
|
||||
let app_ctx = ctx
|
||||
.app_ctx
|
||||
.clone()
|
||||
.ok_or_else(|| "Gemini runtime requires app_ctx to be set".to_string())?;
|
||||
|
||||
let client = Client::new();
|
||||
let cancelled = Arc::clone(&self.cancelled);
|
||||
|
||||
// Step 1: Fetch MCP tool definitions and convert to Gemini format.
|
||||
let gemini_tools = fetch_and_convert_mcp_tools(&client, &mcp_base).await?;
|
||||
// Step 1: Load MCP tool definitions and convert to Gemini format.
|
||||
let gemini_tools = convert_mcp_tools_to_gemini();
|
||||
|
||||
// Step 2: Build the initial conversation contents.
|
||||
let system_instruction = build_system_instruction(&ctx);
|
||||
@@ -276,7 +279,7 @@ impl AgentRuntime for GeminiRuntime {
|
||||
text: format!("\n[Tool call: {}]\n", fc.name),
|
||||
});
|
||||
|
||||
let tool_result = call_mcp_tool(&client, &mcp_base, &fc.name, &fc.args).await;
|
||||
let tool_result = dispatch_tool_call(&fc.name, fc.args.clone(), &app_ctx).await;
|
||||
|
||||
let response_value = match &tool_result {
|
||||
Ok(result) => {
|
||||
@@ -348,6 +351,12 @@ impl AgentRuntime for GeminiRuntime {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http::context::AppContext;
|
||||
|
||||
fn test_app_ctx() -> Arc<AppContext> {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
Arc::new(AppContext::new_test(tmp.path().to_path_buf()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_runtime_stop_sets_cancelled() {
|
||||
@@ -368,7 +377,7 @@ mod tests {
|
||||
prompt: "test".to_string(),
|
||||
cwd: "/tmp".to_string(),
|
||||
inactivity_timeout_secs: 300,
|
||||
mcp_port: 3001,
|
||||
app_ctx: Some(test_app_ctx()),
|
||||
session_id_to_resume: None,
|
||||
fresh_prompt: None,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user