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:
Rabi Mishra
2025-12-17 19:08:46 +05:30
committed by GitHub
parent b401222b89
commit fc8a4b4adf
9 changed files with 273 additions and 57 deletions
+28 -14
View File
@@ -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)],
+30 -5
View File
@@ -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();
+7 -2
View File
@@ -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(),
);
}
}
}
+49 -6
View File
@@ -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,
+146 -24
View File
@@ -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();
+1 -1
View File
@@ -295,7 +295,7 @@ mod tests {
name: "test_tool".into(),
arguments: Some(object!({})),
}),
thought_signature: None,
metadata: None,
};
let permission_result = PermissionCheckResult {
+5 -3
View File
@@ -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"
}
+6 -1
View File
@@ -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;
};