143 lines
7.6 KiB
Markdown
143 lines
7.6 KiB
Markdown
## 1. Cloud API: planner configuration
|
|
|
|
- [x] 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).
|
|
- [x] 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
|
|
|
|
- [x] 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`.
|
|
- [x] 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.)
|
|
- [x] 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).
|
|
- [x] 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
|
|
|
|
- [x] 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`.
|
|
- [x] 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.
|
|
- [x] 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
|
|
|
|
- [x] 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.
|
|
- [x] 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
|
|
|
|
- [x] 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).
|
|
- [x] 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).
|
|
- [x] 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()`).
|
|
- [x] 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"`).
|
|
- [x] 5.5 Full non-integration suite (`uv run --all-packages pytest -m
|
|
"not integration"`) passes with no regressions.
|
|
|
|
## 6. Documentation
|
|
|
|
- [x] 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
|
|
|
|
- [x] 7.1 `openspec validate --strict` passes for this change.
|
|
Done: `openspec validate cloud-planner-proxy --strict` -> "Change
|
|
'cloud-planner-proxy' is valid".
|
|
- [x] 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
|
|
|
|
- [x] 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.
|