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>
73 lines
2.4 KiB
Python
73 lines
2.4 KiB
Python
from __future__ import annotations
|
|
|
|
from runtime.tool_specs import (
|
|
ACTION_TOOL_SPECS,
|
|
ALL_TOOL_SPECS,
|
|
FINISH_TASK_SPEC,
|
|
INPUT_TEXT_SPEC,
|
|
LAUNCH_APP_SPEC,
|
|
SWIPE_SPEC,
|
|
TAP_SPEC,
|
|
TERMINATE_APP_SPEC,
|
|
ToolSpec,
|
|
)
|
|
|
|
|
|
def test_action_tool_specs_has_five_entries_and_all_tool_specs_adds_finish_task() -> None:
|
|
assert len(ACTION_TOOL_SPECS) == 5
|
|
assert len(ALL_TOOL_SPECS) == 6
|
|
assert ALL_TOOL_SPECS == [*ACTION_TOOL_SPECS, FINISH_TASK_SPEC]
|
|
assert FINISH_TASK_SPEC not in ACTION_TOOL_SPECS
|
|
|
|
|
|
def test_all_tool_spec_names_are_unique() -> None:
|
|
names = [spec.name for spec in ALL_TOOL_SPECS]
|
|
assert len(names) == len(set(names))
|
|
|
|
|
|
def test_every_tool_spec_schema_forbids_additional_properties() -> None:
|
|
for spec in ALL_TOOL_SPECS:
|
|
assert isinstance(spec, ToolSpec)
|
|
assert spec.parameters["type"] == "object"
|
|
assert spec.parameters["additionalProperties"] is False
|
|
|
|
|
|
def test_tap_spec_requires_x_and_y() -> None:
|
|
assert TAP_SPEC.parameters["required"] == ["x", "y"]
|
|
assert set(TAP_SPEC.parameters["properties"]) == {"x", "y"}
|
|
|
|
|
|
def test_swipe_spec_requires_coordinates_and_makes_duration_optional() -> None:
|
|
assert SWIPE_SPEC.parameters["required"] == ["start_x", "start_y", "end_x", "end_y"]
|
|
assert set(SWIPE_SPEC.parameters["properties"]) == {
|
|
"start_x",
|
|
"start_y",
|
|
"end_x",
|
|
"end_y",
|
|
"duration_ms",
|
|
}
|
|
assert "duration_ms" not in SWIPE_SPEC.parameters["required"]
|
|
assert SWIPE_SPEC.parameters["properties"]["duration_ms"]["default"] == 500
|
|
|
|
|
|
def test_input_text_spec_requires_text() -> None:
|
|
assert INPUT_TEXT_SPEC.parameters["required"] == ["text"]
|
|
assert set(INPUT_TEXT_SPEC.parameters["properties"]) == {"text"}
|
|
|
|
|
|
def test_launch_and_terminate_app_specs_require_app_id() -> None:
|
|
for spec in (LAUNCH_APP_SPEC, TERMINATE_APP_SPEC):
|
|
assert spec.parameters["required"] == ["app_id"]
|
|
assert set(spec.parameters["properties"]) == {"app_id"}
|
|
|
|
|
|
def test_finish_task_spec_requires_success_and_reason() -> None:
|
|
assert FINISH_TASK_SPEC.parameters["required"] == ["success", "reason"]
|
|
assert set(FINISH_TASK_SPEC.parameters["properties"]) == {"success", "reason"}
|
|
assert FINISH_TASK_SPEC.parameters["properties"]["success"]["type"] == "boolean"
|
|
|
|
|
|
def test_no_tool_spec_declares_device_id() -> None:
|
|
for spec in ALL_TOOL_SPECS:
|
|
assert "device_id" not in spec.parameters["properties"]
|