mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-16 12:15:20 +00:00
fix(pipeline): treat gemini tool_calls+empty-args as truncation retry
Gemini returns finish_reason="tool_calls" (not "length") even when the thinking budget exhausted max_tokens before tool arguments could be emitted. Extend the existing truncation retry condition to detect this case via an allowlist of mutating tools (write_file, edit, exec, create_image, read_file) that virtually never legitimately call with empty args. Nullary tools (datetime, heartbeat) pass through unchanged. Trace: 019d8f33-2de1-7ab2-9a32-9df92cd610dd
This commit is contained in:
1 parent
5c229bed59
commit
6544999779
1 file changed
+37
-2
@@ -69,9 +69,14 @@ func (s *ThinkStage) Execute(ctx context.Context, state *RunState) error {
|
||||
state.Think.TotalUsage.ThinkingTokens += resp.Usage.ThinkingTokens
|
||||
}
|
||||
|
||||
// 6. Handle truncation: only retry when tool call args are truncated or malformed.
|
||||
// 6. Handle truncation: retry when tool call args are truncated or malformed.
|
||||
// Gemini returns finish_reason="tool_calls" (not "length") even when the thinking
|
||||
// budget exhausted max_tokens before args could be emitted — detect via empty
|
||||
// args on allowlisted mutating tools. Nullary tools (datetime, heartbeat) skip
|
||||
// the heuristic so their legitimate empty-args calls pass through.
|
||||
// Text-only truncation (no tool calls) is a valid long answer — deliver it.
|
||||
truncated := resp.FinishReason == "length" && len(resp.ToolCalls) > 0
|
||||
truncated := len(resp.ToolCalls) > 0 && (resp.FinishReason == "length" ||
|
||||
(resp.FinishReason == "tool_calls" && toolCallsHaveMissingRequiredArgs(resp.ToolCalls)))
|
||||
parseErr := !truncated && toolCallsHaveParseErrors(resp.ToolCalls)
|
||||
if truncated || parseErr {
|
||||
state.Think.TruncRetries++
|
||||
@@ -157,3 +162,33 @@ func toolCallsHaveParseErrors(calls []providers.ToolCall) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// mutatingToolsRequireArgs is the static allowlist of tools where empty
|
||||
// arguments are virtually never legitimate. Production telemetry (30d) shows
|
||||
// 1/211 tool_call spans had empty args (the Gemini-3 budget-exhaustion trace);
|
||||
// datetime/heartbeat/web_search always carry args. Conservative scope — expand
|
||||
// only with telemetry justification.
|
||||
var mutatingToolsRequireArgs = map[string]struct{}{
|
||||
"write_file": {},
|
||||
"edit": {},
|
||||
"exec": {},
|
||||
"create_image": {},
|
||||
"read_file": {},
|
||||
}
|
||||
|
||||
// toolCallsHaveMissingRequiredArgs returns true when any call in the batch
|
||||
// targets a mutating tool from the allowlist but carries empty Arguments.
|
||||
// This is the Gemini-3 truncation signal: finish_reason="tool_calls" with
|
||||
// len(args)==0 on a tool we know requires params means the budget ran out
|
||||
// before args could be emitted.
|
||||
func toolCallsHaveMissingRequiredArgs(calls []providers.ToolCall) bool {
|
||||
for _, tc := range calls {
|
||||
if _, requires := mutatingToolsRequireArgs[tc.Name]; !requires {
|
||||
continue
|
||||
}
|
||||
if len(tc.Arguments) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in new issue
Block a user