diff --git a/crates/zclaw-hands/src/hands/researcher.rs b/crates/zclaw-hands/src/hands/researcher.rs index 7294307..6df2ad8 100644 --- a/crates/zclaw-hands/src/hands/researcher.rs +++ b/crates/zclaw-hands/src/hands/researcher.rs @@ -252,38 +252,32 @@ impl ResearcherHand { dependencies: vec!["network".to_string()], input_schema: Some(serde_json::json!({ "type": "object", - "oneOf": [ - { - "properties": { - "action": { "const": "search" }, - "query": { - "type": "object", - "properties": { - "query": { "type": "string" }, - "engine": { "type": "string", "enum": ["searxng", "google", "bing", "duckduckgo", "auto"] }, - "depth": { "type": "string", "enum": ["quick", "standard", "deep"] }, - "maxResults": { "type": "integer" } - }, - "required": ["query"] - } - }, - "required": ["action", "query"] + "properties": { + "action": { + "type": "string", + "enum": ["search", "fetch", "report", "summarize"], + "description": "Action to perform: search (web search), fetch (get URL content), report (deep research), summarize (multiple URLs)" }, - { - "properties": { - "action": { "const": "fetch" }, - "url": { "type": "string" } - }, - "required": ["action", "url"] + "query": { + "type": "string", + "description": "Search query string for search/report actions" }, - { - "properties": { - "action": { "const": "report" }, - "query": { "$ref": "#/properties/query" } - }, - "required": ["action", "query"] + "url": { + "type": "string", + "description": "URL to fetch content from" + }, + "urls": { + "type": "array", + "items": { "type": "string" }, + "description": "List of URLs to summarize" + }, + "engine": { + "type": "string", + "enum": ["auto", "searxng", "google", "bing", "duckduckgo"], + "description": "Search engine preference" } - ] + }, + "description": "Provide 'query' for search/report, or 'url' for fetch, or 'urls' for summarize" })), tags: vec!["research".to_string(), "web".to_string(), "search".to_string()], enabled: true, @@ -310,7 +304,7 @@ impl ResearcherHand { let keys: Vec<&str> = input.as_object() .map(|obj| obj.keys().map(|k| k.as_str()).collect()) .unwrap_or_default(); - tracing::warn!(target: "researcher", ?keys, %input, "infer_action examining input"); + tracing::debug!(target: "researcher", ?keys, %input, "infer_action examining input"); // Check for action field with wrong value if let Some(action) = input.get("action").and_then(|v| v.as_str()) { @@ -364,12 +358,27 @@ impl ResearcherHand { } } } + // Check for injected fallback query from loop_runner (when LLM sends empty args) + if let Some(fallback) = input.get("_fallback_query").and_then(|v| v.as_str()) { + if !fallback.trim().is_empty() { + tracing::debug!(target: "researcher", query = %fallback, "Using fallback user message as search query"); + return Ok(ResearcherAction::Search { query: ResearchQuery { + query: fallback.to_string(), + engine: SearchEngine::Auto, + depth: ResearchDepth::Standard, + max_results: 10, + include_related: false, + time_limit_secs: 60, + }}); + } + } + // Last resort: if any string field looks like a search query if let Some(obj) = input.as_object() { for (key, val) in obj { if let Some(s) = val.as_str() { if s.len() > 2 && !s.starts_with("http") && key != "action" && key != "engine" { - tracing::warn!(target: "researcher", key = %key, value = %s, "Using fallback field as query"); + tracing::debug!(target: "researcher", key = %key, value = %s, "Using fallback field as query"); return Ok(ResearcherAction::Search { query: ResearchQuery { query: s.to_string(), engine: SearchEngine::Auto, @@ -1144,12 +1153,12 @@ impl Hand for ResearcherHand { } async fn execute(&self, _context: &HandContext, input: Value) -> Result { - tracing::info!(target: "researcher", input = %input, "Researcher hand received input"); + tracing::debug!(target: "researcher", input = %input, "Researcher hand received input"); // Try strict deserialization first, then fall back to inference let action: ResearcherAction = match serde_json::from_value(input.clone()) { Ok(a) => a, Err(e) => { - tracing::warn!(target: "researcher", error = %e, input = %input, "Strict deserialization failed, trying inference"); + tracing::debug!(target: "researcher", error = %e, input = %input, "Strict deserialization failed, trying inference"); Self::infer_action(&input)? } }; diff --git a/crates/zclaw-runtime/src/driver/openai.rs b/crates/zclaw-runtime/src/driver/openai.rs index 964a455..6fa4238 100644 --- a/crates/zclaw-runtime/src/driver/openai.rs +++ b/crates/zclaw-runtime/src/driver/openai.rs @@ -208,7 +208,7 @@ impl LlmDriver for OpenAiDriver { tracing::debug!("[OpenAI:stream] SSE #{}: {}", sse_event_count, &data[..data.len().min(300)]); } if data == "[DONE]" { - tracing::debug!("[OpenAI:stream] Received [DONE], total SSE events: {}, raw bytes: {}", sse_event_count, raw_bytes_total); + tracing::debug!("[OpenAI:stream] Received [DONE], total SSE events: {}, raw bytes: {}, tool_calls: {:?}", sse_event_count, raw_bytes_total, accumulated_tool_calls); // Emit ToolUseEnd for all accumulated tool calls (skip invalid ones with empty name) for (id, (name, args)) in &accumulated_tool_calls { @@ -264,7 +264,7 @@ impl LlmDriver for OpenAiDriver { // Handle tool calls if let Some(tool_calls) = &delta.tool_calls { - tracing::trace!("[OpenAI] Received tool_calls delta: {:?}", tool_calls); + tracing::debug!("[OpenAI] Received tool_calls delta: {:?}", tool_calls); for tc in tool_calls { // Tool call start - has id and name if let Some(id) = &tc.id { diff --git a/crates/zclaw-runtime/src/loop_runner.rs b/crates/zclaw-runtime/src/loop_runner.rs index 7f40522..0b5ab4c 100644 --- a/crates/zclaw-runtime/src/loop_runner.rs +++ b/crates/zclaw-runtime/src/loop_runner.rs @@ -380,6 +380,26 @@ impl AgentLoop { if abort_result.is_some() { break; } + + // GLM and other models sometimes send tool calls with empty arguments `{}` + // Inject the last user message as a fallback query so the tool can infer intent. + let input = if input.as_object().map_or(false, |obj| obj.is_empty()) { + if let Some(last_user_msg) = messages.iter().rev().find_map(|m| { + if let Message::User { content } = m { + Some(content.clone()) + } else { + None + } + }) { + tracing::info!("[AgentLoop] Tool '{}' received empty input, injecting user message as fallback query", name); + serde_json::json!({ "_fallback_query": last_user_msg }) + } else { + input + } + } else { + input + }; + // Check tool call safety — via middleware chain { let mw_ctx_ref = middleware::MiddlewareContext {