Files
agentic-mobile-control/openspec/changes/planner-reflection-history/tasks.md
T
q792602257 a5aeb8889c
Tests / Test failed: 2, passed: 849
feat(runtime): add planner reflection history with rationale and thinking
- ToolCallDecision captures thinking blocks and pre-tool text output
- AnthropicToolCallingClient supports optional extended thinking (budget_tokens + beta header)
- PlannedStep carries rationale and thinking from each LLM decision
- WorldEvent replaces scene_summary with rationale/thinking/page fields (backward-compatible)
- AI planner system prompt instructs reflection before each tool call
- _history_summary() emits compact {page, rationale, action, success} dicts
- Cloud DB migration 0011 adds nullable rationale/thinking columns to planner_decision_log
- OpenAI client extracts reasoning_content into thinking field
2026-07-15 12:43:22 +08:00

5.4 KiB

1. ToolCallDecision — capture thinking and text output

  • 1.1 Add thinking: str | None = None and text_output: str | None = None fields to ToolCallDecision in runtime/tool_calling_client.py
  • 1.2 Update _decision_from_anthropic_response() to iterate all content blocks: collect first thinking-type block text into thinking, concatenate all text-type blocks into text_output, continue to tool_use as before
  • 1.3 Update _decision_from_openai_response() to extract reasoning_content from the first choice's message into thinking when present
  • 1.4 Add unit tests: Anthropic response with thinking block → decision.thinking populated; Anthropic response with text block → decision.text_output populated; response with only tool_use → both None; OpenAI response with reasoning_contentdecision.thinking populated

2. PlannerConfig — extended thinking configuration

  • 2.1 Add thinking_budget_tokens: int | None = None to PlannerConfig in runtime/planner_config.py
  • 2.2 Add AI_PLANNER_THINKING_BUDGET_TOKENS env var constant and _parse_thinking_budget() helper; wire into load_config()
  • 2.3 Update AnthropicToolCallingClient._create_message(): when thinking_budget_tokens is set, add thinking: {type: "enabled", budget_tokens: N} to kwargs, add interleaved-thinking-2025-05-14 beta header via betas parameter, and enforce max_tokens >= thinking_budget_tokens + 1
  • 2.4 Add unit tests: load_config() with env var set → thinking_budget_tokens parsed; _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 = None and thinking: str | None = None to the PlannedStep dataclass in runtime/planner.py
  • 3.2 Update AIPlanner.plan() in runtime/ai_planner.py: populate PlannedStep.rationale = decision.text_output and PlannedStep.thinking = decision.thinking

4. Planner prompts — reflection instruction

  • 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
  • 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)

5. History format — compact rationale-based representation

  • 5.1 Update _history_summary() in runtime/ai_planner.py to 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 populated WorldState returns list of compact dicts without scene element data

6. WorldEvent — rationale, thinking, page fields

  • 6.1 Update WorldEvent in world/models.py: make scene_summary optional (SemanticScene | Scene | None = None), add rationale: str | None = None, thinking: str | None = None, page: str | None = None
  • 6.2 Update WorldEvent.to_dict() to include rationale, thinking, page fields; keep scene_summary serialisation for backward compatibility
  • 6.3 Update WorldModel._append_history() in world/model.py to pass rationale=step.rationale, thinking=step.thinking, page=world_state.current_page when constructing WorldEvent; scene_summary becomes optional (pass None or existing value as appropriate)
  • 6.4 Add unit tests: WorldEvent with rationale/thinking/page → to_dict() includes those fields; WorldEvent with all-None optional fields → readable without error; _append_history() picks up current_page from WorldState at time of call

7. Cloud DB — planner_decision_log extension

  • 7.1 Add thinking and rationale nullable TEXT columns to the planner_decision_log table in packages/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 = None and rationale: str | None = None to PlannerDecisionRecord in packages/cloud-platform/cloud/internal_api/models.py
  • 7.4 Update record_planner_decision() in packages/cloud-platform/cloud/internal_api/api.py to write thinking and rationale from the incoming PlannerDecisionRecord to 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 check and ruff format --check on all modified files
  • 8.3 Run python -m compileall on modified packages
  • 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