Files
agentic-mobile-control/openspec/changes/cloud-planner-proxy/tasks.md
T
2026-07-14 20:42:27 +08:00

7.6 KiB

1. Cloud API: planner configuration

  • 1.1 Add Cloud API-side planner configuration (provider, model, timeout, provider API keys) analogous to runtime/planner_config.py, loaded from the Cloud API's own process environment (e.g. AI_PLANNER_PROVIDER/AI_PLANNER_MODEL/AI_PLANNER_TIMEOUT_SECONDS, ANTHROPIC_API_KEY/OPENAI_API_KEY), living in packages/cloud-platform/cloud or apps/cloud-api. Done: packages/cloud-platform/cloud/planner_config.py (CloudPlannerConfig/load_cloud_planner_config/ build_cloud_planner_client, reusing runtime.tool_calling_client per D1).
  • 1.2 Add anthropic/openai as explicit dependencies -- not needed: packages/cloud-platform (device-cloud-platform) already depends unconditionally on device-agent-runtime, which declares both SDKs, so they are already installed wherever the Cloud API runs. Verified with uv run python -c "import anthropic, openai" in the workspace venv.

2. Cloud API: planner-decision internal endpoint

  • 2.1 Add request/response Pydantic models to cloud.internal_api.models for a planner-decision call: request carries system_prompt, user_prompt, optional base64 screenshot, tool specs, timeout; response carries resolved tool_name/arguments or a structured error. Done: PlannerDecisionRequest/PlannerDecisionResponse/ PlannerDecisionError/PlannerToolSpecModel.
  • 2.2 Add the internal route (POST /internal/v1/hosts/{host_id}/planner/decide) to the existing internal router, reusing the current host-scoped bearer auth dependency used by heartbeat/claim/renew/result -- no new scope. (Host-scoped, like renew/result, rather than un-scoped, so a request's host_id is verified against the caller's own principal.)
  • 2.3 Implement the route handler by constructing a runtime.tool_calling_client.ToolCallingClient (per D1, via cloud.planner_config.build_cloud_planner_client) from the Cloud API's own planner configuration, calling .decide(...), and translating ToolCallDecision/ToolCallUnavailable into the response model (502 + PlannerDecisionError on failure).
  • 2.4 Ensure the handler does not log or persist raw prompt text or screenshot bytes; only metadata (host id, resolved tool name, latency, error class) may be logged. Done: handler's logger.info calls only ever include host_id/tool_name/latency_seconds/ error_class.

3. Host Agent: cloud-proxy transport

  • 3.1 Add AI_PLANNER_TRANSPORT (cloud default | direct) to apps/device-host-agent/host_agent/config.py. Done: HostAgentConfig.ai_planner_transport + _parse_ai_planner_transport.
  • 3.2 Add a request_planner_decision(...) method to HostAgentClient -- revised during implementation (see design.md D3 implementation note): ToolCallingClient.decide() is synchronous and runs off the main event loop via asyncio.to_thread (host_agent/lease.py), so wrapping the async HostAgentClient would need event-loop bridging. Skipped in favor of 3.3's own synchronous httpx.Client, mirroring HostAgentEnrollmentClient's pattern.
  • 3.3 Add host_agent/cloud_planner_client.py::CloudProxyToolCallingClient implementing the runtime.tool_calling_client.ToolCallingClient Protocol structurally (no import of host_agent/cloud from runtime), calling the planner-decide endpoint via its own synchronous httpx.Client (host-scoped bearer auth, same as HostAgentEnrollmentClient) and raising ToolCallUnavailable on any failure (network error, non-2xx/error response) -- matching D7.

4. Host Agent: wiring

  • 4.1 Update apps/device-host-agent/host_agent/execution.py's _host_agent_planner_config()/create_task_runner() so that when AI_PLANNER_TRANSPORT is unset or set to cloud, the constructed TaskRunner's AIPlanner is built with a CloudProxyToolCallingClient instead of the local provider client, while AI_PLANNER_TRANSPORT=direct remains an explicit direct-to-provider fallback. Done: new _host_agent_planner() helper + host_agent_config param threaded through create_execution_factories() from app.py::create_application(). Manually verified both transports construct the expected client.
  • 4.2 Confirm apps/device-host-agent/tests/test_execution.py::test_runtime_owned_packages_do_not_import_host_or_cloud_concerns-style boundary checks still pass with the new module in place. Confirmed: pytest tests/test_execution.py (4 passed), boundary test specifically re-run and green.

5. Tests

  • 5.1 Unit tests for Cloud API planner configuration loading (defaults, provider/model/timeout parsing) mirroring tests/test_planner_config.py. Done: apps/cloud-api/tests/test_cloud_planner_config.py (8 tests, all passing).
  • 5.2 Unit tests for the planner-decision endpoint: authorized request resolves a decision, unauthenticated/foreign-host request is rejected, provider failure returns a structured error without crashing. Done: tests/test_cloud_planner_decision_endpoint.py (7 tests -- success, screenshot decoding, invalid base64, unauthenticated, foreign-host, host_id mismatch, provider failure -- via injected fake planner_client_factory, mirroring tests/test_host_agent_internal_api.py's router-level pattern).
  • 5.3 Unit tests for CloudProxyToolCallingClient: successful decision round-trip, and each failure mode raises ToolCallUnavailable. Done: apps/device-host-agent/tests/test_cloud_planner_client.py (7 tests, using httpx.MockTransport -- success, screenshot base64-encoding, network error, structured 502 error, unstructured error response, and client-ownership on close()).
  • 5.4 Unit tests for Host Agent wiring: AI_PLANNER_TRANSPORT unset or set to cloud constructs an AIPlanner using CloudProxyToolCallingClient; explicit direct preserves local provider construction. Done: added to apps/device-host-agent/tests/test_execution.py (2 new tests, one parametrized over None/"direct").
  • 5.5 Full non-integration suite (uv run --all-packages pytest -m "not integration") passes with no regressions.

6. Documentation

  • 6.1 Update docs/CLOUD_DEPLOYMENT.md's Runtime AI Planner section with the cloud transport configuration path, its trade-offs (latency, cloud-availability coupling, expanded data path for screenshots/prompts), and the credential split (Cloud API holds provider keys for cloud transport; Host Agent holds them for direct transport).

7. Validation

  • 7.1 openspec validate --strict passes for this change. Done: openspec validate cloud-planner-proxy --strict -> "Change 'cloud-planner-proxy' is valid".
  • 7.2 Ruff check/format and compileall pass for all touched packages. Done: ruff check clean; ruff format applied to 5 files (import wrapping/line-length only, no logic changes) and re-verified via the full non-integration suite (492 passed); python -m compileall clean for packages/cloud-platform/cloud, apps/cloud-api, apps/device-host-agent.

8. Default transport amendment

  • 8.1 Make cloud the Host Agent default for AI_PLANNER_TRANSPORT, retain direct as an explicit fallback, and update the runtime contract, deployment guidance, and regression tests.