fix(planner): allow rationale/thinking by using tool_choice=auto
Tests / Test passed: 855

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:
2026-07-15 13:43:39 +08:00
parent 24992fc9fb
commit 367fd0d412
4 changed files with 250 additions and 16 deletions
@@ -20,7 +20,7 @@
## 4. Planner prompts — reflection instruction
- [x] 4.1 Update `PLANNER_SYSTEM_PROMPT` in `runtime/planner_prompts.py` to instruct the AI to output a short text block (1–2 sentences) before each tool call: first assessing whether the previous action achieved its intended effect (or noting "first step" if no history), then stating the intent of the current action
- [x] 4.2 Verify via prompt review that the instruction is consistent with `tool_choice: {type: "any"}` (text blocks are already allowed before tool_use blocks — no API change needed)
- [x] 4.2 ~~Verify via prompt review that the instruction is consistent with `tool_choice: {type: "any"}` (text blocks are already allowed before tool_use blocks — no API change needed)~~**superseded, see section 9**: this assumption was wrong; forced `tool_choice` suppresses text/thinking entirely
## 5. History format — compact rationale-based representation
@@ -48,4 +48,15 @@
- [x] 8.2 Run `ruff check` and `ruff format --check` on all modified files
- [x] 8.3 Run `python -m compileall` on modified packages
- [x] 8.4 Run `openspec validate --strict --change "planner-reflection-history"` and confirm all artifacts pass validation
- [ ] 8.5 Manual smoke test (optional, requires Host Agent + Appium + iPhone): verify that a live task run produces non-null `rationale` in `WorldEvent` history and that the history prompt passed to the LLM is in compact format
- [x] 8.5 Manual smoke test (requires Host Agent + Appium + iPhone): ran a live task and found `rationale`/`thinking` were **always** `None` in `WorldEvent` history — root cause diagnosed and fixed in section 9 below
## 9. Bug fix — forced `tool_choice` was suppressing rationale/thinking (found via 8.5)
- [x] 9.1 `AnthropicToolCallingClient._create_message()`: add required `forced: bool` param; `tool_choice` is `{"type": "auto", "disable_parallel_tool_use": True}` when `forced=False`, `{"type": "any", "disable_parallel_tool_use": True}` when `forced=True`; thinking/`betas` kwargs only added when `forced=False`
- [x] 9.2 `AnthropicToolCallingClient.decide()`: call with `forced=False` first; if `_anthropic_response_has_tool_use(response)` is `False`, retry once with `forced=True`; parse whichever response has the tool call
- [x] 9.3 `OpenAIToolCallingClient._create_completion()`: add required `forced: bool` param; `tool_choice` is `"auto"` when `forced=False`, `"required"` when `forced=True`
- [x] 9.4 `OpenAIToolCallingClient.decide()`: same auto-then-forced-retry pattern using `_openai_response_has_tool_call()`
- [x] 9.5 `_decision_from_openai_response()`: extract `message.content` into `text_output` (previously never captured for OpenAI, forced or not)
- [x] 9.6 Add/rename unit tests in `tests/test_tool_calling_client.py` covering: default request uses `tool_choice: "auto"`; retry sequence when first response has no tool call (Anthropic and OpenAI); thinking/`betas` dropped on the forced retry; OpenAI `text_output` capture
- [x] 9.7 Update `design.md` (D1 correction + new D8) and this file to document the bug and fix
- [x] 9.8 Re-run `uv run --all-packages pytest -m "not integration"`, `ruff check`/`ruff format --check`, `python -m compileall`, and `openspec validate --strict --change "planner-reflection-history"` after the fix