8.1 KiB
8.1 KiB
1. ToolCallDecision — capture thinking and text output
- 1.1 Add
thinking: str | None = Noneandtext_output: str | None = Nonefields toToolCallDecisioninruntime/tool_calling_client.py - 1.2 Update
_decision_from_anthropic_response()to iterate all content blocks: collect firstthinking-type block text intothinking, concatenate alltext-type blocks intotext_output, continue totool_useas before - 1.3 Update
_decision_from_openai_response()to extractreasoning_contentfrom the first choice's message intothinkingwhen present - 1.4 Add unit tests: Anthropic response with thinking block →
decision.thinkingpopulated; Anthropic response with text block →decision.text_outputpopulated; response with onlytool_use→ bothNone; OpenAI response withreasoning_content→decision.thinkingpopulated
2. PlannerConfig — extended thinking configuration
- 2.1 Add
thinking_budget_tokens: int | None = NonetoPlannerConfiginruntime/planner_config.py - 2.2 Add
AI_PLANNER_THINKING_BUDGET_TOKENSenv var constant and_parse_thinking_budget()helper; wire intoload_config() - 2.3 Update
AnthropicToolCallingClient._create_message(): whenthinking_budget_tokensis set, addthinking: {type: "enabled", budget_tokens: N}to kwargs, addinterleaved-thinking-2025-05-14beta header viabetasparameter, and enforcemax_tokens >= thinking_budget_tokens + 1 - 2.4 Add unit tests:
load_config()with env var set →thinking_budget_tokensparsed;_create_message()with budget set → kwargs include thinking param and beta header; without budget → no thinking param in kwargs
3. PlannedStep — carry rationale and thinking
- 3.1 Add
rationale: str | None = Noneandthinking: str | None = Noneto thePlannedStepdataclass inruntime/planner.py - 3.2 Update
AIPlanner.plan()inruntime/ai_planner.py: populatePlannedStep.rationale = decision.text_outputandPlannedStep.thinking = decision.thinking
4. Planner prompts — reflection instruction
- 4.1 Update
PLANNER_SYSTEM_PROMPTinruntime/planner_prompts.pyto 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 - 4.2
Verify via prompt review that the instruction is consistent with— superseded, see section 9: this assumption was wrong; forcedtool_choice: {type: "any"}(text blocks are already allowed before tool_use blocks — no API change needed)tool_choicesuppresses text/thinking entirely
5. History format — compact rationale-based representation
- 5.1 Update
_history_summary()inruntime/ai_planner.pyto return compact dicts{page, rationale, action, success}instead of[event.to_dict() for event in world.history] - 5.2 Add unit test:
_history_summary()with a populatedWorldStatereturns list of compact dicts without scene element data
6. WorldEvent — rationale, thinking, page fields
- 6.1 Update
WorldEventinworld/models.py: makescene_summaryoptional (SemanticScene | Scene | None = None), addrationale: str | None = None,thinking: str | None = None,page: str | None = None - 6.2 Update
WorldEvent.to_dict()to includerationale,thinking,pagefields; keepscene_summaryserialisation for backward compatibility - 6.3 Update
WorldModel._append_history()inworld/model.pyto passrationale=step.rationale,thinking=step.thinking,page=world_state.current_pagewhen constructingWorldEvent;scene_summarybecomes optional (passNoneor existing value as appropriate) - 6.4 Add unit tests:
WorldEventwith rationale/thinking/page →to_dict()includes those fields;WorldEventwith all-None optional fields → readable without error;_append_history()picks upcurrent_pagefromWorldStateat time of call
7. Cloud DB — planner_decision_log extension
- 7.1 Add
thinkingandrationalenullable TEXT columns to theplanner_decision_logtable inpackages/cloud-platform/cloud/db_models.py - 7.2 Create a new Alembic migration (e.g.
0010_planner_decision_log_reflection) adding the two nullable columns; verify it applies cleanly on both SQLite and PostgreSQL migration paths - 7.3 Add
thinking: str | None = Noneandrationale: str | None = NonetoPlannerDecisionRecordinpackages/cloud-platform/cloud/internal_api/models.py - 7.4 Update
record_planner_decision()inpackages/cloud-platform/cloud/internal_api/api.pyto writethinkingandrationalefrom the incomingPlannerDecisionRecordto the new columns - 7.5 Add unit tests: decision record with rationale/thinking → both stored; decision record with nulls → stored without error; existing rows queried → both columns read as NULL without error
8. End-to-end verification
- 8.1 Run full non-integration test suite (
uv run --all-packages pytest -m "not integration") and confirm no regressions - 8.2 Run
ruff checkandruff format --checkon all modified files - 8.3 Run
python -m compileallon modified packages - 8.4 Run
openspec validate --strict --change "planner-reflection-history"and confirm all artifacts pass validation - 8.5 Manual smoke test (requires Host Agent + Appium + iPhone): ran a live task and found
rationale/thinkingwere alwaysNoneinWorldEventhistory — root cause diagnosed and fixed in section 9 below
9. Bug fix — forced tool_choice was suppressing rationale/thinking (found via 8.5)
- 9.1
AnthropicToolCallingClient._create_message(): add requiredforced: boolparam;tool_choiceis{"type": "auto", "disable_parallel_tool_use": True}whenforced=False,{"type": "any", "disable_parallel_tool_use": True}whenforced=True; thinking/betaskwargs only added whenforced=False - 9.2
AnthropicToolCallingClient.decide(): call withforced=Falsefirst; if_anthropic_response_has_tool_use(response)isFalse, retry once withforced=True; parse whichever response has the tool call - 9.3
OpenAIToolCallingClient._create_completion(): add requiredforced: boolparam;tool_choiceis"auto"whenforced=False,"required"whenforced=True - 9.4
OpenAIToolCallingClient.decide(): same auto-then-forced-retry pattern using_openai_response_has_tool_call() - 9.5
_decision_from_openai_response(): extractmessage.contentintotext_output(previously never captured for OpenAI, forced or not) - 9.6 Add/rename unit tests in
tests/test_tool_calling_client.pycovering: default request usestool_choice: "auto"; retry sequence when first response has no tool call (Anthropic and OpenAI); thinking/betasdropped on the forced retry; OpenAItext_outputcapture - 9.7 Update
design.md(D1 correction + new D8) and this file to document the bug and fix - 9.8 Re-run
uv run --all-packages pytest -m "not integration",ruff check/ruff format --check,python -m compileall, andopenspec validate --strict --change "planner-reflection-history"after the fix
10. Required reusable action metadata (post-completion amendment)
- 10.1 Require
purposeandexpected_outcomein every device-action tool schema; extract them from executable arguments intoToolCallDecisionandPlannedStep. - 10.2 Return rationale, thinking, purpose, and expected outcome through the Cloud planner response; persist the new action metadata in
planner_decision_logwith an additive migration and expose it through the task decision API. - 10.3 Preserve executable arguments, purpose, and expected outcome in
WorldEvent, Timeline records, and synthesizedFlowStepvalues; include available metadata in skill embedding text for semantic retrieval. - 10.4 Render available rationale, thinking, purpose, and expected outcome in Cloud Console planner-decision history.
- 10.5 Add focused regression tests and run the relevant Python, frontend, migration, lint/format, and OpenSpec validation checks.