Forced tool_choice ("any"/"required") makes both Anthropic and OpenAI
skip any text/thinking block before the tool call, which silently made
rationale and thinking always None despite the planner-reflection-history
change's capture code being correct. Switch the primary call to
tool_choice="auto" (Anthropic: type=auto, disable_parallel_tool_use=true;
OpenAI: "auto") so the model can emit its reflection text, and add a
one-time forced retry (Anthropic "any", OpenAI "required", thinking
disabled) if the model responds without a tool call, guaranteeing a step
never stalls. Also add OpenAI text_output capture from message.content,
which was never extracted before (Anthropic-only gap).
Update planner-reflection-history design.md/tasks.md to document the bug
found during the pending manual smoke test (task 8.5) and the fix (new
section 9).
This commit is contained in:
@@ -84,7 +84,24 @@ class AnthropicToolCallingClient:
|
||||
screenshot,
|
||||
tools,
|
||||
timeout=timeout,
|
||||
forced=False,
|
||||
)
|
||||
if not _anthropic_response_has_tool_use(response):
|
||||
# tool_choice="any"/"tool" forces Claude to skip any preceding
|
||||
# text block, so the reflection instructed by the system
|
||||
# prompt requires tool_choice="auto". That leaves a small
|
||||
# chance the model responds without calling a tool at all;
|
||||
# retry once with tool_choice="any" to guarantee progress.
|
||||
# The forced retry cannot carry rationale/thinking (Anthropic
|
||||
# rejects thinking combined with forced tool_choice).
|
||||
response = self._create_message(
|
||||
system_prompt,
|
||||
user_prompt,
|
||||
screenshot,
|
||||
tools,
|
||||
timeout=timeout,
|
||||
forced=True,
|
||||
)
|
||||
return _decision_from_anthropic_response(
|
||||
response,
|
||||
system_prompt=system_prompt,
|
||||
@@ -103,13 +120,16 @@ class AnthropicToolCallingClient:
|
||||
tools: list[ToolSpec],
|
||||
*,
|
||||
timeout: float,
|
||||
forced: bool,
|
||||
) -> Any:
|
||||
client = self._client()
|
||||
budget = self._thinking_budget_tokens
|
||||
# Forced tool_choice is incompatible with extended thinking.
|
||||
budget = self._thinking_budget_tokens if not forced else None
|
||||
# Enforce max_tokens >= budget + 1 when thinking is enabled.
|
||||
max_tokens = self.max_tokens
|
||||
if budget is not None:
|
||||
max_tokens = max(max_tokens, budget + 1)
|
||||
tool_choice_type = "any" if forced else "auto"
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"max_tokens": max_tokens,
|
||||
@@ -128,7 +148,10 @@ class AnthropicToolCallingClient:
|
||||
}
|
||||
],
|
||||
"tools": [_anthropic_tool(spec) for spec in tools],
|
||||
"tool_choice": {"type": "any", "disable_parallel_tool_use": True},
|
||||
"tool_choice": {
|
||||
"type": tool_choice_type,
|
||||
"disable_parallel_tool_use": True,
|
||||
},
|
||||
}
|
||||
if budget is not None:
|
||||
kwargs["thinking"] = {"type": "enabled", "budget_tokens": budget}
|
||||
@@ -188,7 +211,21 @@ class OpenAIToolCallingClient:
|
||||
screenshot,
|
||||
tools,
|
||||
timeout=timeout,
|
||||
forced=False,
|
||||
)
|
||||
if not _openai_response_has_tool_call(response):
|
||||
# tool_choice="required" forces a function call and suppresses
|
||||
# any reflection text, so capturing rationale requires
|
||||
# tool_choice="auto". Retry once, forcing tool use, if the
|
||||
# model responds without calling a tool at all.
|
||||
response = self._create_completion(
|
||||
system_prompt,
|
||||
user_prompt,
|
||||
screenshot,
|
||||
tools,
|
||||
timeout=timeout,
|
||||
forced=True,
|
||||
)
|
||||
return _decision_from_openai_response(
|
||||
response,
|
||||
system_prompt=system_prompt,
|
||||
@@ -207,6 +244,7 @@ class OpenAIToolCallingClient:
|
||||
tools: list[ToolSpec],
|
||||
*,
|
||||
timeout: float,
|
||||
forced: bool,
|
||||
) -> Any:
|
||||
client = self._client()
|
||||
kwargs = {
|
||||
@@ -218,7 +256,7 @@ class OpenAIToolCallingClient:
|
||||
{"role": "user", "content": _openai_content(user_prompt, screenshot)},
|
||||
],
|
||||
"tools": [_openai_tool(spec) for spec in tools],
|
||||
"tool_choice": "required",
|
||||
"tool_choice": "required" if forced else "auto",
|
||||
"parallel_tool_calls": False,
|
||||
}
|
||||
chat = getattr(client, "chat", None)
|
||||
@@ -282,6 +320,13 @@ def _anthropic_tool(spec: ToolSpec) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _anthropic_response_has_tool_use(response: Any) -> bool:
|
||||
content = _value(response, "content")
|
||||
if not isinstance(content, list):
|
||||
return False
|
||||
return any(_value(block, "type") == "tool_use" for block in content)
|
||||
|
||||
|
||||
def _decision_from_anthropic_response(
|
||||
response: Any,
|
||||
*,
|
||||
@@ -346,6 +391,15 @@ def _openai_tool(spec: ToolSpec) -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _openai_response_has_tool_call(response: Any) -> bool:
|
||||
choices = _value(response, "choices")
|
||||
if not isinstance(choices, list) or not choices:
|
||||
return False
|
||||
message = _value(choices[0], "message")
|
||||
tool_calls = _value(message, "tool_calls")
|
||||
return isinstance(tool_calls, list) and len(tool_calls) > 0
|
||||
|
||||
|
||||
def _decision_from_openai_response(
|
||||
response: Any,
|
||||
*,
|
||||
@@ -367,6 +421,10 @@ def _decision_from_openai_response(
|
||||
# Extract reasoning_content from o-series models when present.
|
||||
raw_reasoning = _value(message, "reasoning_content")
|
||||
thinking: str | None = raw_reasoning if isinstance(raw_reasoning, str) else None
|
||||
# Pre-tool reflection text (rationale), when the model emits content
|
||||
# alongside the tool call under tool_choice="auto".
|
||||
raw_content = _value(message, "content")
|
||||
text_output: str | None = raw_content if isinstance(raw_content, str) else None
|
||||
return ToolCallDecision(
|
||||
tool_name=name,
|
||||
arguments=arguments,
|
||||
@@ -374,6 +432,7 @@ def _decision_from_openai_response(
|
||||
system_prompt=system_prompt,
|
||||
user_prompt=user_prompt,
|
||||
thinking=thinking,
|
||||
text_output=text_output,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user