mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
fix(google): Fix 400 Bad Request error with Gemini 3 thought signatures (#6035)
Signed-off-by: rabi <ramishra@redhat.com>
This commit is contained in:
@@ -536,7 +536,7 @@ mod tests {
|
||||
let tool_request = ToolRequest {
|
||||
id: "test-id".to_string(),
|
||||
tool_call: Ok(tool_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let result = tool_request_to_markdown(&tool_request, true);
|
||||
@@ -560,7 +560,7 @@ mod tests {
|
||||
let tool_request = ToolRequest {
|
||||
id: "test-id".to_string(),
|
||||
tool_call: Ok(tool_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let result = tool_request_to_markdown(&tool_request, true);
|
||||
@@ -580,6 +580,7 @@ mod tests {
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "test-id".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -605,6 +606,7 @@ mod tests {
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "test-id".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -698,7 +700,7 @@ mod tests {
|
||||
let tool_request = ToolRequest {
|
||||
id: "shell-cat".to_string(),
|
||||
tool_call: Ok(tool_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let python_code = r#"#!/usr/bin/env python3
|
||||
@@ -716,6 +718,7 @@ if __name__ == "__main__":
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "shell-cat".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -750,7 +753,7 @@ if __name__ == "__main__":
|
||||
let tool_request = ToolRequest {
|
||||
id: "git-status".to_string(),
|
||||
tool_call: Ok(git_status_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let git_output = " M src/main.rs\n?? temp.txt\n A new_feature.rs";
|
||||
@@ -762,6 +765,7 @@ if __name__ == "__main__":
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "git-status".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -794,7 +798,7 @@ if __name__ == "__main__":
|
||||
let _tool_request = ToolRequest {
|
||||
id: "cargo-build".to_string(),
|
||||
tool_call: Ok(cargo_build_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let build_output = r#" Compiling goose-cli v0.1.0 (/Users/user/goose)
|
||||
@@ -816,6 +820,7 @@ warning: unused variable `x`
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "cargo-build".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -844,7 +849,7 @@ warning: unused variable `x`
|
||||
let _tool_request = ToolRequest {
|
||||
id: "curl-api".to_string(),
|
||||
tool_call: Ok(curl_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let api_response = r#"{
|
||||
@@ -868,6 +873,7 @@ warning: unused variable `x`
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "curl-api".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -898,7 +904,7 @@ warning: unused variable `x`
|
||||
let tool_request = ToolRequest {
|
||||
id: "editor-write".to_string(),
|
||||
tool_call: Ok(editor_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let text_content = TextContent {
|
||||
@@ -909,6 +915,7 @@ warning: unused variable `x`
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "editor-write".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -944,7 +951,7 @@ warning: unused variable `x`
|
||||
let _tool_request = ToolRequest {
|
||||
id: "editor-view".to_string(),
|
||||
tool_call: Ok(editor_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let python_code = r#"import os
|
||||
@@ -971,6 +978,7 @@ def process_data(data: List[Dict]) -> List[Dict]:
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "editor-view".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -999,7 +1007,7 @@ def process_data(data: List[Dict]) -> List[Dict]:
|
||||
let _tool_request = ToolRequest {
|
||||
id: "shell-error".to_string(),
|
||||
tool_call: Ok(error_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let error_output = r#"python: can't open file 'nonexistent_script.py': [Errno 2] No such file or directory
|
||||
@@ -1013,6 +1021,7 @@ Command failed with exit code 2"#;
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "shell-error".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -1040,7 +1049,7 @@ Command failed with exit code 2"#;
|
||||
let tool_request = ToolRequest {
|
||||
id: "script-exec".to_string(),
|
||||
tool_call: Ok(script_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let script_output = r#"Python 3.11.5 (main, Aug 24 2023, 15:18:16) [Clang 14.0.3 ]
|
||||
@@ -1058,6 +1067,7 @@ Command failed with exit code 2"#;
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "script-exec".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -1092,7 +1102,7 @@ Command failed with exit code 2"#;
|
||||
let _tool_request = ToolRequest {
|
||||
id: "multi-cmd".to_string(),
|
||||
tool_call: Ok(multi_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let multi_output = r#"total 24
|
||||
@@ -1110,6 +1120,7 @@ drwx------ 3 user staff 96 Dec 6 16:20 com.apple.launchd.abc
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "multi-cmd".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -1142,7 +1153,7 @@ drwx------ 3 user staff 96 Dec 6 16:20 com.apple.launchd.abc
|
||||
let tool_request = ToolRequest {
|
||||
id: "grep-search".to_string(),
|
||||
tool_call: Ok(grep_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let grep_output = r#"src/main.rs:15:async fn process_request(req: Request) -> Result<Response> {
|
||||
@@ -1158,6 +1169,7 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "grep-search".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -1191,7 +1203,7 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
|
||||
let _tool_request = ToolRequest {
|
||||
id: "json-test".to_string(),
|
||||
tool_call: Ok(tool_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let json_output = r#"{"status": "success", "data": {"count": 42}}"#;
|
||||
@@ -1203,6 +1215,7 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "json-test".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
@@ -1231,7 +1244,7 @@ src/middleware.rs:12:async fn auth_middleware(req: Request, next: Next) -> Resul
|
||||
let tool_request = ToolRequest {
|
||||
id: "npm-install".to_string(),
|
||||
tool_call: Ok(npm_call),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let npm_output = r#"added 57 packages, and audited 58 packages in 3s
|
||||
@@ -1249,6 +1262,7 @@ found 0 vulnerabilities"#;
|
||||
annotations: None,
|
||||
};
|
||||
let tool_response = ToolResponse {
|
||||
metadata: None,
|
||||
id: "npm-install".to_string(),
|
||||
tool_result: Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(text_content.raw.text)],
|
||||
|
||||
@@ -33,7 +33,8 @@ use crate::context_mgmt::{
|
||||
check_if_compaction_needed, compact_messages, DEFAULT_COMPACTION_THRESHOLD,
|
||||
};
|
||||
use crate::conversation::message::{
|
||||
ActionRequiredData, Message, MessageContent, SystemNotificationType, ToolRequest,
|
||||
ActionRequiredData, Message, MessageContent, ProviderMetadata, SystemNotificationType,
|
||||
ToolRequest,
|
||||
};
|
||||
use crate::conversation::{debug_conversation_fix, fix_conversation, Conversation};
|
||||
use crate::mcp_utils::ToolResult;
|
||||
@@ -349,7 +350,7 @@ impl Agent {
|
||||
for request in &permission_check_result.denied {
|
||||
if let Some(response_msg) = request_to_response_map.get(&request.id) {
|
||||
let mut response = response_msg.lock().await;
|
||||
*response = response.clone().with_tool_response(
|
||||
*response = response.clone().with_tool_response_with_metadata(
|
||||
request.id.clone(),
|
||||
Ok(CallToolResult {
|
||||
content: vec![rmcp::model::Content::text(DECLINED_RESPONSE)],
|
||||
@@ -357,6 +358,7 @@ impl Agent {
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
}),
|
||||
request.metadata.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1059,8 +1061,10 @@ impl Agent {
|
||||
.collect();
|
||||
|
||||
let mut request_to_response_map = HashMap::new();
|
||||
let mut request_metadata: HashMap<String, Option<ProviderMetadata>> = HashMap::new();
|
||||
for (idx, request) in frontend_requests.iter().chain(remaining_requests.iter()).enumerate() {
|
||||
request_to_response_map.insert(request.id.clone(), tool_response_messages[idx].clone());
|
||||
request_metadata.insert(request.id.clone(), request.metadata.clone());
|
||||
}
|
||||
|
||||
for (idx, request) in frontend_requests.iter().enumerate() {
|
||||
@@ -1078,7 +1082,7 @@ impl Agent {
|
||||
for request in remaining_requests.iter() {
|
||||
if let Some(response_msg) = request_to_response_map.get(&request.id) {
|
||||
let mut response = response_msg.lock().await;
|
||||
*response = response.clone().with_tool_response(
|
||||
*response = response.clone().with_tool_response_with_metadata(
|
||||
request.id.clone(),
|
||||
Ok(CallToolResult {
|
||||
content: vec![Content::text(CHAT_MODE_TOOL_SKIPPED_RESPONSE)],
|
||||
@@ -1086,6 +1090,7 @@ impl Agent {
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
request.metadata.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1177,8 +1182,9 @@ impl Agent {
|
||||
all_install_successful = false;
|
||||
}
|
||||
if let Some(response_msg) = request_to_response_map.get(&request_id) {
|
||||
let metadata = request_metadata.get(&request_id).and_then(|m| m.as_ref());
|
||||
let mut response = response_msg.lock().await;
|
||||
*response = response.clone().with_tool_response(request_id, output);
|
||||
*response = response.clone().with_tool_response_with_metadata(request_id, output, metadata);
|
||||
}
|
||||
}
|
||||
ToolStreamItem::Message(msg) => {
|
||||
@@ -1200,11 +1206,30 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve thinking content from the original response
|
||||
// Gemini (and other thinking models) require thinking to be echoed back
|
||||
let thinking_content: Vec<MessageContent> = response.content.iter()
|
||||
.filter(|c| matches!(c, MessageContent::Thinking(_)))
|
||||
.cloned()
|
||||
.collect();
|
||||
if !thinking_content.is_empty() {
|
||||
let thinking_msg = Message::new(
|
||||
response.role.clone(),
|
||||
response.created,
|
||||
thinking_content,
|
||||
).with_id(format!("msg_{}", Uuid::new_v4()));
|
||||
messages_to_add.push(thinking_msg);
|
||||
}
|
||||
|
||||
for (idx, request) in frontend_requests.iter().chain(remaining_requests.iter()).enumerate() {
|
||||
if request.tool_call.is_ok() {
|
||||
let request_msg = Message::assistant()
|
||||
.with_id(format!("msg_{}", Uuid::new_v4()))
|
||||
.with_tool_request(request.id.clone(), request.tool_call.clone());
|
||||
.with_tool_request_with_metadata(
|
||||
request.id.clone(),
|
||||
request.tool_call.clone(),
|
||||
request.metadata.as_ref(),
|
||||
);
|
||||
messages_to_add.push(request_msg);
|
||||
let final_response = tool_response_messages[idx]
|
||||
.lock().await.clone();
|
||||
|
||||
@@ -120,7 +120,7 @@ impl Agent {
|
||||
// User declined - update the specific response message for this request
|
||||
if let Some(response_msg) = request_to_response_map.get(&request.id) {
|
||||
let mut response = response_msg.lock().await;
|
||||
*response = response.clone().with_tool_response(
|
||||
*response = response.clone().with_tool_response_with_metadata(
|
||||
request.id.clone(),
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![Content::text(DECLINED_RESPONSE)],
|
||||
@@ -128,6 +128,7 @@ impl Agent {
|
||||
is_error: Some(true),
|
||||
meta: None,
|
||||
}),
|
||||
request.metadata.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -155,7 +156,11 @@ impl Agent {
|
||||
|
||||
if let Some((id, result)) = self.tool_result_rx.lock().await.recv().await {
|
||||
let mut response = message_tool_response.lock().await;
|
||||
*response = response.clone().with_tool_response(id, result);
|
||||
*response = response.clone().with_tool_response_with_metadata(
|
||||
id,
|
||||
result,
|
||||
tool_request.metadata.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ where
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Provider-specific metadata for tool requests/responses.
|
||||
/// Allows providers to store custom data without polluting the core model.
|
||||
pub type ProviderMetadata = serde_json::Map<String, serde_json::Value>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(ToSchema)]
|
||||
@@ -62,7 +66,8 @@ pub struct ToolRequest {
|
||||
#[schema(value_type = Object)]
|
||||
pub tool_call: ToolResult<CallToolRequestParam>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thought_signature: Option<String>,
|
||||
#[schema(value_type = Object)]
|
||||
pub metadata: Option<ProviderMetadata>,
|
||||
}
|
||||
|
||||
impl ToolRequest {
|
||||
@@ -89,6 +94,9 @@ pub struct ToolResponse {
|
||||
#[serde(with = "tool_result_serde::call_tool_result")]
|
||||
#[schema(value_type = Object)]
|
||||
pub tool_result: ToolResult<CallToolResult>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = Object)]
|
||||
pub metadata: Option<ProviderMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@@ -250,19 +258,19 @@ impl MessageContent {
|
||||
MessageContent::ToolRequest(ToolRequest {
|
||||
id: id.into(),
|
||||
tool_call,
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tool_request_with_signature<S1: Into<String>, S2: Into<String>>(
|
||||
id: S1,
|
||||
pub fn tool_request_with_metadata<S: Into<String>>(
|
||||
id: S,
|
||||
tool_call: ToolResult<CallToolRequestParam>,
|
||||
thought_signature: Option<S2>,
|
||||
metadata: Option<&ProviderMetadata>,
|
||||
) -> Self {
|
||||
MessageContent::ToolRequest(ToolRequest {
|
||||
id: id.into(),
|
||||
tool_call,
|
||||
thought_signature: thought_signature.map(|s| s.into()),
|
||||
metadata: metadata.cloned(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -270,6 +278,19 @@ impl MessageContent {
|
||||
MessageContent::ToolResponse(ToolResponse {
|
||||
id: id.into(),
|
||||
tool_result,
|
||||
metadata: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tool_response_with_metadata<S: Into<String>>(
|
||||
id: S,
|
||||
tool_result: ToolResult<CallToolResult>,
|
||||
metadata: Option<&ProviderMetadata>,
|
||||
) -> Self {
|
||||
MessageContent::ToolResponse(ToolResponse {
|
||||
id: id.into(),
|
||||
tool_result,
|
||||
metadata: metadata.cloned(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -641,6 +662,17 @@ impl Message {
|
||||
self.with_content(MessageContent::tool_request(id, tool_call))
|
||||
}
|
||||
|
||||
pub fn with_tool_request_with_metadata<S: Into<String>>(
|
||||
self,
|
||||
id: S,
|
||||
tool_call: ToolResult<CallToolRequestParam>,
|
||||
metadata: Option<&ProviderMetadata>,
|
||||
) -> Self {
|
||||
self.with_content(MessageContent::tool_request_with_metadata(
|
||||
id, tool_call, metadata,
|
||||
))
|
||||
}
|
||||
|
||||
/// Add a tool response to the message
|
||||
pub fn with_tool_response<S: Into<String>>(
|
||||
self,
|
||||
@@ -650,6 +682,17 @@ impl Message {
|
||||
self.with_content(MessageContent::tool_response(id, result))
|
||||
}
|
||||
|
||||
pub fn with_tool_response_with_metadata<S: Into<String>>(
|
||||
self,
|
||||
id: S,
|
||||
result: ToolResult<CallToolResult>,
|
||||
metadata: Option<&ProviderMetadata>,
|
||||
) -> Self {
|
||||
self.with_content(MessageContent::tool_response_with_metadata(
|
||||
id, result, metadata,
|
||||
))
|
||||
}
|
||||
|
||||
/// Add an action required message for tool confirmation
|
||||
pub fn with_action_required<S: Into<String>>(
|
||||
self,
|
||||
|
||||
@@ -9,10 +9,25 @@ use rmcp::model::{
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::conversation::message::{Message, MessageContent};
|
||||
use crate::conversation::message::{Message, MessageContent, ProviderMetadata};
|
||||
use serde_json::{json, Map, Value};
|
||||
use std::ops::Deref;
|
||||
|
||||
pub const THOUGHT_SIGNATURE_KEY: &str = "thoughtSignature";
|
||||
|
||||
pub fn metadata_with_signature(signature: &str) -> ProviderMetadata {
|
||||
let mut map = ProviderMetadata::new();
|
||||
map.insert(THOUGHT_SIGNATURE_KEY.to_string(), json!(signature));
|
||||
map
|
||||
}
|
||||
|
||||
pub fn get_thought_signature(metadata: &Option<ProviderMetadata>) -> Option<&str> {
|
||||
metadata
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(THOUGHT_SIGNATURE_KEY))
|
||||
.and_then(|v| v.as_str())
|
||||
}
|
||||
|
||||
/// Convert internal Message format to Google's API message specification
|
||||
pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
||||
messages
|
||||
@@ -58,8 +73,8 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
||||
let mut part = Map::new();
|
||||
part.insert("functionCall".to_string(), json!(function_call_part));
|
||||
|
||||
if let Some(signature) = &request.thought_signature {
|
||||
part.insert("thoughtSignature".to_string(), json!(signature));
|
||||
if let Some(signature) = get_thought_signature(&request.metadata) {
|
||||
part.insert(THOUGHT_SIGNATURE_KEY.to_string(), json!(signature));
|
||||
}
|
||||
|
||||
parts.push(json!(part));
|
||||
@@ -117,15 +132,44 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
||||
if text.is_empty() {
|
||||
text = "Tool call is done.".to_string();
|
||||
}
|
||||
parts.push(json!({
|
||||
"functionResponse": {
|
||||
"name": response.id,
|
||||
"response": {"content": {"text": text}},
|
||||
}}
|
||||
));
|
||||
let mut part = Map::new();
|
||||
let mut function_response = Map::new();
|
||||
function_response.insert("name".to_string(), json!(response.id));
|
||||
function_response.insert(
|
||||
"response".to_string(),
|
||||
json!({"content": {"text": text}}),
|
||||
);
|
||||
part.insert(
|
||||
"functionResponse".to_string(),
|
||||
json!(function_response),
|
||||
);
|
||||
if let Some(signature) = get_thought_signature(&response.metadata) {
|
||||
part.insert(
|
||||
THOUGHT_SIGNATURE_KEY.to_string(),
|
||||
json!(signature),
|
||||
);
|
||||
}
|
||||
parts.push(json!(part));
|
||||
}
|
||||
Err(e) => {
|
||||
parts.push(json!({"text":format!("Error: {}", e)}));
|
||||
let mut part = Map::new();
|
||||
let mut function_response = Map::new();
|
||||
function_response.insert("name".to_string(), json!(response.id));
|
||||
function_response.insert(
|
||||
"response".to_string(),
|
||||
json!({"content": {"text": format!("Error: {}", e)}}),
|
||||
);
|
||||
part.insert(
|
||||
"functionResponse".to_string(),
|
||||
json!(function_response),
|
||||
);
|
||||
if let Some(signature) = get_thought_signature(&response.metadata) {
|
||||
part.insert(
|
||||
THOUGHT_SIGNATURE_KEY.to_string(),
|
||||
json!(signature),
|
||||
);
|
||||
}
|
||||
parts.push(json!(part));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,15 +326,30 @@ pub fn response_to_message(response: Value) -> Result<Message> {
|
||||
.and_then(|parts| parts.as_array())
|
||||
.unwrap_or(&binding);
|
||||
|
||||
// Track the last seen thought signature to use as fallback for function calls without one
|
||||
// This handles cases where Google's API returns multiple function calls but only includes
|
||||
// thoughtSignature on some of them
|
||||
let mut last_signature: Option<String> = None;
|
||||
|
||||
let has_function_calls = parts.iter().any(|p| p.get("functionCall").is_some());
|
||||
|
||||
for part in parts {
|
||||
let thought_signature = part
|
||||
.get("thoughtSignature")
|
||||
let signature = part
|
||||
.get(THOUGHT_SIGNATURE_KEY)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
if signature.is_some() {
|
||||
last_signature = signature.clone();
|
||||
}
|
||||
|
||||
if let Some(text) = part.get("text").and_then(|v| v.as_str()) {
|
||||
if let Some(sig) = thought_signature {
|
||||
content.push(MessageContent::thinking(text.to_string(), sig));
|
||||
// Text is "thinking" only if:
|
||||
// 1. It has a signature AND
|
||||
// 2. The response also contains function calls (meaning this is reasoning before acting)
|
||||
// If there are no function calls, this is the final response and should be shown
|
||||
if let (Some(sig), true) = (&signature, has_function_calls) {
|
||||
content.push(MessageContent::thinking(text.to_string(), sig.clone()));
|
||||
} else {
|
||||
content.push(MessageContent::text(text.to_string()));
|
||||
}
|
||||
@@ -316,16 +375,17 @@ pub fn response_to_message(response: Value) -> Result<Message> {
|
||||
content.push(MessageContent::tool_request(id, Err(error)));
|
||||
} else {
|
||||
let parameters = function_call.get("args");
|
||||
if let Some(params) = parameters {
|
||||
content.push(MessageContent::tool_request_with_signature(
|
||||
id,
|
||||
Ok(CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(params.clone())),
|
||||
}),
|
||||
thought_signature,
|
||||
));
|
||||
}
|
||||
let arguments = parameters.map(|params| object(params.clone()));
|
||||
let effective_signature = signature.as_deref().or(last_signature.as_deref());
|
||||
let metadata = effective_signature.map(metadata_with_signature);
|
||||
content.push(MessageContent::tool_request_with_metadata(
|
||||
id,
|
||||
Ok(CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments,
|
||||
}),
|
||||
metadata.as_ref(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -919,4 +979,66 @@ mod tests {
|
||||
assert_eq!(regular_field["type"], "number");
|
||||
assert_eq!(regular_field["description"], "A regular number field");
|
||||
}
|
||||
|
||||
fn google_response(parts: Vec<Value>) -> Value {
|
||||
json!({"candidates": [{"content": {"role": "model", "parts": parts}}]})
|
||||
}
|
||||
|
||||
fn tool_result(text: &str) -> CallToolResult {
|
||||
CallToolResult {
|
||||
content: vec![Content::text(text)],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thought_signature_roundtrip() {
|
||||
const SIG: &str = "thought_sig_abc";
|
||||
|
||||
let response_with_tools = google_response(vec![
|
||||
json!({"text": "Let me think...", "thoughtSignature": SIG}),
|
||||
json!({"functionCall": {"name": "shell", "args": {"cmd": "ls"}}, "thoughtSignature": SIG}),
|
||||
json!({"functionCall": {"name": "read", "args": {}}}),
|
||||
]);
|
||||
|
||||
let native = response_to_message(response_with_tools).unwrap();
|
||||
assert_eq!(native.content.len(), 3, "Expected thinking + 2 tool calls");
|
||||
|
||||
let thinking = native.content[0]
|
||||
.as_thinking()
|
||||
.expect("Text with function calls should be Thinking");
|
||||
assert_eq!(thinking.signature, SIG);
|
||||
|
||||
let req1 = native.content[1]
|
||||
.as_tool_request()
|
||||
.expect("Second part should be ToolRequest");
|
||||
let req2 = native.content[2]
|
||||
.as_tool_request()
|
||||
.expect("Third part should be ToolRequest");
|
||||
assert_eq!(get_thought_signature(&req1.metadata), Some(SIG));
|
||||
assert_eq!(
|
||||
get_thought_signature(&req2.metadata),
|
||||
Some(SIG),
|
||||
"Should inherit"
|
||||
);
|
||||
|
||||
let tool_response = Message::user().with_tool_response_with_metadata(
|
||||
req1.id.clone(),
|
||||
Ok(tool_result("output")),
|
||||
req1.metadata.as_ref(),
|
||||
);
|
||||
let google_out = format_messages(&[native, tool_response]);
|
||||
assert_eq!(google_out[0]["parts"][0]["thoughtSignature"], SIG);
|
||||
assert_eq!(google_out[1]["parts"][0]["thoughtSignature"], SIG);
|
||||
|
||||
let final_response =
|
||||
google_response(vec![json!({"text": "Done!", "thoughtSignature": SIG})]);
|
||||
let final_native = response_to_message(final_response).unwrap();
|
||||
assert!(
|
||||
final_native.content[0].as_text().is_some(),
|
||||
"Text-only = final answer"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ mod tests {
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({"command": "rm -rf /"})),
|
||||
}),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
}];
|
||||
|
||||
let results = inspector.inspect(&tool_requests, &[]).await.unwrap();
|
||||
|
||||
@@ -295,7 +295,7 @@ mod tests {
|
||||
name: "test_tool".into(),
|
||||
arguments: Some(object!({})),
|
||||
}),
|
||||
thought_signature: None,
|
||||
metadata: None,
|
||||
};
|
||||
|
||||
let permission_result = PermissionCheckResult {
|
||||
|
||||
@@ -5561,9 +5561,8 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"thoughtSignature": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"toolCall": {
|
||||
"type": "object"
|
||||
@@ -5580,6 +5579,9 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object"
|
||||
},
|
||||
"toolResult": {
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -969,7 +969,9 @@ export type ToolPermission = {
|
||||
|
||||
export type ToolRequest = {
|
||||
id: string;
|
||||
thoughtSignature?: string | null;
|
||||
metadata?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
toolCall: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
@@ -977,6 +979,9 @@ export type ToolRequest = {
|
||||
|
||||
export type ToolResponse = {
|
||||
id: string;
|
||||
metadata?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
toolResult: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user