Implement cloud-planner-proxy: AI planner routes through Cloud API

Implements all 19 tasks of the cloud-planner-proxy OpenSpec change:

- Cloud API: cloud.planner_config (CloudPlannerConfig, load/build helpers)
  reusing runtime.tool_calling_client provider clients (no new dependency
  needed -- device-cloud-platform already depends on device-agent-runtime).
- Cloud API: new host-scoped POST /internal/v1/hosts/{host_id}/planner/decide
  internal endpoint, reusing existing bearer auth; logs only metadata
  (host id, tool name, latency, error class), never prompt/screenshot
  content.
- Host Agent: new AI_PLANNER_TRANSPORT config (direct default | cloud) and
  host_agent/cloud_planner_client.py::CloudProxyToolCallingClient, a
  synchronous ToolCallingClient implementation (structural, not importing
  runtime) that calls the new endpoint via its own httpx.Client -- avoids
  bridging the async HostAgentClient across the worker-thread boundary
  that AIPlanner.plan() runs in (asyncio.to_thread in lease.py).
- Host Agent wiring: create_execution_factories()/_host_agent_planner()
  select the cloud-proxy client only when AI_PLANNER_TRANSPORT=cloud;
  direct/unset transport is unchanged (still the default).
- Tests: 22 new tests across Cloud API config, the new endpoint, the new
  client, and transport-selection wiring; full non-integration suite
  (492 tests) passes with no regressions.
- Docs: docs/CLOUD_DEPLOYMENT.md documents the cloud transport, its
  trade-offs, and the credential split between Host Agent and Cloud API.

proposal.md/design.md were corrected during implementation to reflect two
findings: no new anthropic/openai dependency is actually needed, and
CloudProxyToolCallingClient uses its own sync httpx.Client rather than a
new HostAgentClient method, per the thread-boundary reasoning above.
This commit is contained in:
2026-07-13 21:27:48 +08:00
parent 1107ace89c
commit a68f609453
15 changed files with 1009 additions and 60 deletions
+30 -2
View File
@@ -365,8 +365,36 @@ fails on its first step (no silent fallback to the stub planner). Set
(e.g. for offline/dev hosts with no provider credentials).
For OpenAI, set `AI_PLANNER_PROVIDER=openai`, choose the deployed model through
`AI_PLANNER_MODEL`, and provide `OPENAI_API_KEY`. Provider credentials belong
only on the Host Agent; the Cloud API does not need them.
`AI_PLANNER_MODEL`, and provide `OPENAI_API_KEY`. This is the **`direct`
transport** (the default): the Host Agent holds provider credentials and
calls Anthropic/OpenAI itself.
### Cloud-proxy transport (`AI_PLANNER_TRANSPORT=cloud`)
Set `AI_PLANNER_TRANSPORT=cloud` on the Host Agent to instead route every
planning decision through the Cloud API's
`POST /internal/v1/hosts/{host_id}/planner/decide` endpoint (the same
host-scoped bearer credential used for heartbeat/claim/renew/result). In this
mode:
- **Credentials move to the Cloud API.** Configure `AI_PLANNER_PROVIDER`,
`AI_PLANNER_MODEL`, `AI_PLANNER_TIMEOUT_SECONDS`, and
`ANTHROPIC_API_KEY`/`OPENAI_API_KEY` on the Cloud API process instead of the
Host Agent -- edge hosts no longer need provider keys at all.
- **Trade-offs to accept before enabling:**
- *Latency*: every planning step now makes a round trip to the Cloud API in
addition to the LLM provider call.
- *Availability coupling*: unlike heartbeat/claim (which tolerate transient
Cloud API outages via retry/backoff), a planning step fails immediately if
the Cloud API or its configured provider is unreachable -- there is no
fallback to the stub planner or to a local direct call.
- *Expanded data path*: goal/scene prompts and screenshots now transit the
Cloud API. The endpoint logs only metadata (host id, resolved tool name,
latency, error class) and never prompt text or screenshot bytes, but the
request bodies themselves do cross the network to the control plane.
`AI_PLANNER_TRANSPORT` unset or `direct` preserves the existing
direct-to-provider behavior with no change.
## Operational Limitations