mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
fix: support Gemini 3's thought signatures (#5806)
Signed-off-by: Salvatore Testa <sal@withpersona.com>
This commit is contained in:
@@ -512,6 +512,7 @@ mod tests {
|
||||
let tool_request = ToolRequest {
|
||||
id: "test-id".to_string(),
|
||||
tool_call: Ok(tool_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let result = tool_request_to_markdown(&tool_request, true);
|
||||
@@ -535,6 +536,7 @@ mod tests {
|
||||
let tool_request = ToolRequest {
|
||||
id: "test-id".to_string(),
|
||||
tool_call: Ok(tool_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let result = tool_request_to_markdown(&tool_request, true);
|
||||
@@ -662,6 +664,7 @@ mod tests {
|
||||
let tool_request = ToolRequest {
|
||||
id: "shell-cat".to_string(),
|
||||
tool_call: Ok(tool_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let python_code = r#"#!/usr/bin/env python3
|
||||
@@ -708,6 +711,7 @@ if __name__ == "__main__":
|
||||
let tool_request = ToolRequest {
|
||||
id: "git-status".to_string(),
|
||||
tool_call: Ok(git_status_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let git_output = " M src/main.rs\n?? temp.txt\n A new_feature.rs";
|
||||
@@ -746,6 +750,7 @@ if __name__ == "__main__":
|
||||
let _tool_request = ToolRequest {
|
||||
id: "cargo-build".to_string(),
|
||||
tool_call: Ok(cargo_build_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let build_output = r#" Compiling goose-cli v0.1.0 (/Users/user/goose)
|
||||
@@ -790,6 +795,7 @@ warning: unused variable `x`
|
||||
let _tool_request = ToolRequest {
|
||||
id: "curl-api".to_string(),
|
||||
tool_call: Ok(curl_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let api_response = r#"{
|
||||
@@ -838,6 +844,7 @@ warning: unused variable `x`
|
||||
let tool_request = ToolRequest {
|
||||
id: "editor-write".to_string(),
|
||||
tool_call: Ok(editor_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let text_content = TextContent {
|
||||
@@ -878,6 +885,7 @@ warning: unused variable `x`
|
||||
let _tool_request = ToolRequest {
|
||||
id: "editor-view".to_string(),
|
||||
tool_call: Ok(editor_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let python_code = r#"import os
|
||||
@@ -927,6 +935,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,
|
||||
};
|
||||
|
||||
let error_output = r#"python: can't open file 'nonexistent_script.py': [Errno 2] No such file or directory
|
||||
@@ -962,6 +971,7 @@ Command failed with exit code 2"#;
|
||||
let tool_request = ToolRequest {
|
||||
id: "script-exec".to_string(),
|
||||
tool_call: Ok(script_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let script_output = r#"Python 3.11.5 (main, Aug 24 2023, 15:18:16) [Clang 14.0.3 ]
|
||||
@@ -1008,6 +1018,7 @@ Command failed with exit code 2"#;
|
||||
let _tool_request = ToolRequest {
|
||||
id: "multi-cmd".to_string(),
|
||||
tool_call: Ok(multi_call),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let multi_output = r#"total 24
|
||||
@@ -1052,6 +1063,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,
|
||||
};
|
||||
|
||||
let grep_output = r#"src/main.rs:15:async fn process_request(req: Request) -> Result<Response> {
|
||||
@@ -1095,6 +1107,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,
|
||||
};
|
||||
|
||||
let json_output = r#"{"status": "success", "data": {"count": 42}}"#;
|
||||
@@ -1129,6 +1142,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,
|
||||
};
|
||||
|
||||
let npm_output = r#"added 57 packages, and audited 58 packages in 3s
|
||||
|
||||
@@ -53,6 +53,8 @@ pub struct ToolRequest {
|
||||
#[serde(with = "tool_result_serde")]
|
||||
#[schema(value_type = Object)]
|
||||
pub tool_call: ToolResult<CallToolRequestParam>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thought_signature: Option<String>,
|
||||
}
|
||||
|
||||
impl ToolRequest {
|
||||
@@ -201,6 +203,19 @@ impl MessageContent {
|
||||
MessageContent::ToolRequest(ToolRequest {
|
||||
id: id.into(),
|
||||
tool_call,
|
||||
thought_signature: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tool_request_with_signature<S1: Into<String>, S2: Into<String>>(
|
||||
id: S1,
|
||||
tool_call: ToolResult<CallToolRequestParam>,
|
||||
thought_signature: Option<S2>,
|
||||
) -> Self {
|
||||
MessageContent::ToolRequest(ToolRequest {
|
||||
id: id.into(),
|
||||
tool_call,
|
||||
thought_signature: thought_signature.map(|s| s.into()),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,14 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
parts.push(json!({
|
||||
"functionCall": function_call_part
|
||||
}));
|
||||
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));
|
||||
}
|
||||
|
||||
parts.push(json!(part));
|
||||
}
|
||||
Err(e) => {
|
||||
parts.push(json!({"text":format!("Error: {}", e)}));
|
||||
@@ -121,6 +126,12 @@ pub fn format_messages(messages: &[Message]) -> Vec<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
MessageContent::Thinking(thinking) => {
|
||||
let mut part = Map::new();
|
||||
part.insert("text".to_string(), json!(thinking.thinking));
|
||||
part.insert("thoughtSignature".to_string(), json!(thinking.signature));
|
||||
parts.push(json!(part));
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
@@ -269,8 +280,17 @@ pub fn response_to_message(response: Value) -> Result<Message> {
|
||||
.unwrap_or(&binding);
|
||||
|
||||
for part in parts {
|
||||
let thought_signature = part
|
||||
.get("thoughtSignature")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
if let Some(text) = part.get("text").and_then(|v| v.as_str()) {
|
||||
content.push(MessageContent::text(text.to_string()));
|
||||
if let Some(sig) = thought_signature {
|
||||
content.push(MessageContent::thinking(text.to_string(), sig));
|
||||
} else {
|
||||
content.push(MessageContent::text(text.to_string()));
|
||||
}
|
||||
} else if let Some(function_call) = part.get("functionCall") {
|
||||
let id: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
@@ -294,12 +314,13 @@ pub fn response_to_message(response: Value) -> Result<Message> {
|
||||
} else {
|
||||
let parameters = function_call.get("args");
|
||||
if let Some(params) = parameters {
|
||||
content.push(MessageContent::tool_request(
|
||||
content.push(MessageContent::tool_request_with_signature(
|
||||
id,
|
||||
Ok(CallToolRequestParam {
|
||||
name: name.into(),
|
||||
arguments: Some(object(params.clone())),
|
||||
}),
|
||||
thought_signature,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ pub const GOOGLE_KNOWN_MODELS: &[&str] = &[
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-preview-image-generation",
|
||||
"gemini-2.0-flash-lite",
|
||||
"gemini-3-pro-preview",
|
||||
];
|
||||
|
||||
pub const GOOGLE_DOC_URL: &str = "https://ai.google.dev/gemini-api/docs/models";
|
||||
|
||||
@@ -113,6 +113,7 @@ mod tests {
|
||||
name: "shell".into(),
|
||||
arguments: Some(object!({"command": "rm -rf /"})),
|
||||
}),
|
||||
thought_signature: None,
|
||||
}];
|
||||
|
||||
let results = inspector.inspect(&tool_requests, &[]).await.unwrap();
|
||||
|
||||
@@ -295,6 +295,7 @@ mod tests {
|
||||
name: "test_tool".into(),
|
||||
arguments: Some(object!({})),
|
||||
}),
|
||||
thought_signature: None,
|
||||
};
|
||||
|
||||
let permission_result = PermissionCheckResult {
|
||||
|
||||
@@ -19,7 +19,7 @@ PROVIDERS=(
|
||||
"xai:grok-3"
|
||||
"openai:gpt-4o:gpt-4o-mini:gpt-3.5-turbo:gpt-5"
|
||||
"anthropic:claude-sonnet-4-5-20250929:claude-opus-4-1-20250805"
|
||||
"google:gemini-2.5-pro:gemini-2.5-flash"
|
||||
"google:gemini-2.5-pro:gemini-2.5-flash:gemini-3-pro-preview"
|
||||
"tetrate:claude-sonnet-4-20250514"
|
||||
)
|
||||
|
||||
|
||||
@@ -4808,6 +4808,10 @@
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"thoughtSignature": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"toolCall": {
|
||||
"type": "object"
|
||||
}
|
||||
|
||||
@@ -860,6 +860,7 @@ export type ToolPermission = {
|
||||
|
||||
export type ToolRequest = {
|
||||
id: string;
|
||||
thoughtSignature?: string | null;
|
||||
toolCall: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user