Change is complete (20/20 tasks). Deltas synced: MODIFIED the agent-runtime "Pluggable dual-provider tool-calling abstraction" requirement (added transport selection), and created a new main spec openspec/specs/cloud-planner-proxy/spec.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.6 KiB
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 inpackages/cloud-platform/cloudorapps/cloud-api. Done:packages/cloud-platform/cloud/planner_config.py(CloudPlannerConfig/load_cloud_planner_config/build_cloud_planner_client, reusingruntime.tool_calling_clientper D1). - 1.2
Add-- not needed:anthropic/openaias explicit dependenciespackages/cloud-platform(device-cloud-platform) already depends unconditionally ondevice-agent-runtime, which declares both SDKs, so they are already installed wherever the Cloud API runs. Verified withuv 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.modelsfor a planner-decision call: request carriessystem_prompt,user_prompt, optional base64 screenshot, tool specs, timeout; response carries resolvedtool_name/argumentsor 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'shost_idis verified against the caller's own principal.) - 2.3 Implement the route handler by constructing a
runtime.tool_calling_client.ToolCallingClient(per D1, viacloud.planner_config.build_cloud_planner_client) from the Cloud API's own planner configuration, calling.decide(...), and translatingToolCallDecision/ToolCallUnavailableinto the response model (502 +PlannerDecisionErroron 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.infocalls only ever includehost_id/tool_name/latency_seconds/error_class.
3. Host Agent: cloud-proxy transport
- 3.1 Add
AI_PLANNER_TRANSPORT(clouddefault |direct) toapps/device-host-agent/host_agent/config.py. Done:HostAgentConfig.ai_planner_transport+_parse_ai_planner_transport. - 3.2
Add a-- revised during implementation (see design.md D3 implementation note):request_planner_decision(...)method toHostAgentClientToolCallingClient.decide()is synchronous and runs off the main event loop viaasyncio.to_thread(host_agent/lease.py), so wrapping the asyncHostAgentClientwould need event-loop bridging. Skipped in favor of 3.3's own synchronoushttpx.Client, mirroringHostAgentEnrollmentClient's pattern. - 3.3 Add
host_agent/cloud_planner_client.py::CloudProxyToolCallingClientimplementing theruntime.tool_calling_client.ToolCallingClientProtocol structurally (no import ofhost_agent/cloudfromruntime), calling the planner-decide endpoint via its own synchronoushttpx.Client(host-scoped bearer auth, same asHostAgentEnrollmentClient) and raisingToolCallUnavailableon 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 whenAI_PLANNER_TRANSPORTis unset or set tocloud, the constructedTaskRunner'sAIPlanneris built with aCloudProxyToolCallingClientinstead of the local provider client, whileAI_PLANNER_TRANSPORT=directremains an explicit direct-to-provider fallback. Done: new_host_agent_planner()helper +host_agent_configparam threaded throughcreate_execution_factories()fromapp.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 fakeplanner_client_factory, mirroringtests/test_host_agent_internal_api.py's router-level pattern). - 5.3 Unit tests for
CloudProxyToolCallingClient: successful decision round-trip, and each failure mode raisesToolCallUnavailable. Done:apps/device-host-agent/tests/test_cloud_planner_client.py(7 tests, usinghttpx.MockTransport-- success, screenshot base64-encoding, network error, structured 502 error, unstructured error response, and client-ownership onclose()). - 5.4 Unit tests for Host Agent wiring:
AI_PLANNER_TRANSPORTunset or set tocloudconstructs anAIPlannerusingCloudProxyToolCallingClient; explicitdirectpreserves local provider construction. Done: added toapps/device-host-agent/tests/test_execution.py(2 new tests, one parametrized overNone/"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 thecloudtransport configuration path, its trade-offs (latency, cloud-availability coupling, expanded data path for screenshots/prompts), and the credential split (Cloud API holds provider keys forcloudtransport; Host Agent holds them fordirecttransport).
7. Validation
- 7.1
openspec validate --strictpasses for this change. Done:openspec validate cloud-planner-proxy --strict-> "Change 'cloud-planner-proxy' is valid". - 7.2 Ruff check/format and
compileallpass for all touched packages. Done:ruff checkclean;ruff formatapplied to 5 files (import wrapping/line-length only, no logic changes) and re-verified via the full non-integration suite (492 passed);python -m compileallclean forpackages/cloud-platform/cloud,apps/cloud-api,apps/device-host-agent.
8. Default transport amendment
- 8.1 Make
cloudthe Host Agent default forAI_PLANNER_TRANSPORT, retaindirectas an explicit fallback, and update the runtime contract, deployment guidance, and regression tests.