Replaces the stub Planner's fixed describe_screen/[] behavior with a real decision-maker: AIPlanner uses native tool/function calling (Anthropic or OpenAI, pluggable via AI_PLANNER_PROVIDER) to select exactly one grounded action per turn, with an explicit finish_task(success, reason) tool for completion/failure instead of an ambiguous "no tool call" signal. Default disabled (AI_PLANNER_ENABLED=false) and additive; TaskRunner falls back to the existing stub Planner unchanged when disabled. Amends CONSTITUTION.md's Perception Boundary with one narrow exception: only the AI Planner may receive the current step's raw screenshot bytes alongside Scene, for vision-grounded coordinate grounding. Also fixes a latent gap in TaskRunner.run(): observe/plan exceptions are now caught per iteration and turned into a failed task with a failure_reason, instead of propagating uncaught. openspec change: ai-planner-runtime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4.7 KiB
4.7 KiB
1. Configuration and tool face
- 1.1 Create
runtime/planner_config.py:PlannerConfig(enabled,provider,model,timeout),load_config()readingAI_PLANNER_ENABLED/AI_PLANNER_PROVIDER/AI_PLANNER_MODEL/AI_PLANNER_TIMEOUT_SECONDS,enableddefaultingFalse - 1.2 Create
runtime/tool_specs.py: vendor-neutralToolSpecdataclass plusTAP_SPEC/SWIPE_SPEC/INPUT_TEXT_SPEC/LAUNCH_APP_SPEC/TERMINATE_APP_SPEC/FINISH_TASK_SPEC, matching real tool signatures, exported asACTION_TOOL_SPECS(5) andALL_TOOL_SPECS(6) - 1.3 Create
runtime/planner_prompts.py:PLANNER_SYSTEM_PROMPTandplanner_user_prompt(*, goal, scene_json, history_summary)
2. Dual-provider tool-calling client
- 2.1 Create
runtime/tool_calling_client.py:ToolCallDecision,ToolCallUnavailable,ToolCallingClientProtocol - 2.2 Implement
AnthropicToolCallingClient(lazy SDK import, injectable transport, forced single tool call viatool_choice={"type": "any", "disable_parallel_tool_use": True}, image content block when a screenshot is present) - 2.3 Implement
OpenAIToolCallingClient(lazy SDK import, injectable transport,max_completion_tokens, forced single tool call viatool_choice="required"+parallel_tool_calls=False,image_urldata-URI block when a screenshot is present) - 2.4 Implement
build_client(config)provider selection
3. AI Planner and runtime wiring
- 3.1 Create
runtime/ai_planner.py::AIPlanner(single-step decision,finish_task→ empty plan /TaskFailedError,goal_reached()permanentlyFalse) - 3.2 Add
screenshot: bytes | None = Noneto baseruntime/planner.py::Planner.plan() - 3.3 Wire
runtime/task.py::TaskRunner:planner_configconstructor param,_default_planner()(AI Planner when enabled, stub otherwise), generalize_planner_accepts_world()into_planner_accepts(name), add_planning_screenshot(), conditionally injectscreenshot=in_plan() - 3.4 Wrap
TaskRunner.run()'s per-iteration observe+plan in try/except, marking the taskstatus="failed"with afailure_reasonon any exception instead of propagating it uncaught
4. Constitution amendment
- 4.1 Amend
docs/CONSTITUTION.md's Perception Boundary section with the narrow, Planner-only screenshot exception
5. openspec change artifacts
- 5.1 Write
proposal.md,design.md(with explicit Constitution Compliance section),tasks.md - 5.2 Write
specs/agent-runtime/spec.md(## ADDED Requirementsonly)
6. Tests
- 6.1
tests/test_planner_config.py: env var parsing, defaults,enableddefaultsFalse, invalid/negative timeout falls back to default - 6.2
tests/test_tool_specs.py: eachToolSpec.parametersschema (required fields,additionalProperties: False),ALL_TOOL_SPECShas exactly 6 entries - 6.3
tests/test_tool_calling_client.py: fake-transport tests per provider — forced single-tool-call fields present, image block present/absent based on screenshot, successful response parses toToolCallDecision, malformed/error response raisesToolCallUnavailable - 6.4
tests/test_ai_planner.py: fakeToolCallingClient— action decision → singlePlannedStep;finish_task(success=True)→[];finish_task(success=False, reason=...)→ raisesTaskFailedError;goal_reached()alwaysFalse - 6.5 Added
tests/test_ai_planner_task_runner.py(kepttests/test_task_loop.pyuntouched rather than extending it): planner exception mid-task → task endsstatus="failed"with afailure_reason(not stuckrunning); observer exception is caught the same way; a narrow-signature Planner does not receive an unexpectedscreenshotkwarg while one that declares it does;PlannerConfig(enabled=False)(default) still yields the stubPlanner;PlannerConfig(enabled=True)without an explicitplanner=yields anAIPlanner - 6.6
tests/test_ai_planner_integration.py(@pytest.mark.integration, skipped without a real API key): one real call per provider
7. Verification
- 7.1 Ran
pytest -m "not integration": 314 passed, 6 deselected (up from the pre-existing 269-test baseline plus the 45 new tests added by this change) - 7.2 Confirmed default-off behavior: no
AI_PLANNER_*environment variables set in the shell →TaskRunner()constructsplanner_config=PlannerConfig(enabled=False, ...)andtype(runner.planner) is Planner;uvicorn api.rest:create_app --factorystarts cleanly with no API key configured