Compare commits

...
5 Commits
Author SHA1 Message Date
q792602257andClaude Opus 4.6 56f3f96363 chore(openspec): archive database-llm-provider-management
Tests / Test passed: 794
Change is complete (17/17 tasks). Deltas synced: MODIFIED the
cloud-planner-proxy "Endpoint resolves exactly one tool-call decision"
requirement to resolve provider config from the active database profile,
and created a new main spec openspec/specs/llm-provider-management/spec.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 23:32:08 +08:00
q792602257andClaude Opus 4.6 8e37b965aa chore(openspec): archive cloud-planner-proxy
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>
2026-07-14 23:30:25 +08:00
q792602257andClaude Opus 4.6 c01dd4c6b2 chore(openspec): archive ai-planner-runtime
Change is complete (22/22 tasks) and its delta spec has been synced
into a new main spec openspec/specs/agent-runtime/spec.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 23:28:17 +08:00
q792602257andClaude Opus 4.6 9669b52498 chore(openspec): archive uv-workspace-packaging
Change is complete (17/17 tasks) and its delta spec has been synced
into a new main spec openspec/specs/workspace-packaging/spec.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 21:24:59 +08:00
q792602257andClaude Opus 4.6 fa10cccf71 chore(openspec): archive downgrade-python-3-13-paddleocr
Change is complete (19/19 tasks) and its delta spec has been synced
into a new main spec openspec/specs/python-runtime-baseline/spec.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-14 21:18:57 +08:00
32 changed files with 403 additions and 0 deletions
+127
View File
@@ -0,0 +1,127 @@
# agent-runtime Specification
## Purpose
Define the behavior of the AI Planner runtime, including LLM-driven action
selection, completion signaling, provider abstraction, default-disabling, and
the narrow screenshot exception to the Perception Boundary.
## Requirements
### Requirement: LLM-driven Planner selects exactly one grounded action per turn
The system SHALL provide a Planner implementation that, given a goal, the
current Scene, and recent task history, uses native LLM tool/function
calling to select exactly one action (or the completion signal defined
below) per `plan()` invocation, grounding any coordinates in the current
turn's Scene element bounds.
#### Scenario: Planner selects a single action for the current turn
- **WHEN** the AI Planner is invoked with a goal and the current Scene
- **THEN** it returns at most one `PlannedStep`, whose action and arguments
come from exactly one tool call chosen by the underlying LLM for that turn
#### Scenario: Planner re-decides every turn from the current Scene
- **WHEN** the AI Planner is invoked again after a prior step has executed
- **THEN** its decision is grounded in the newly observed Scene for that
turn, not in coordinates or assumptions carried over from a previous turn
### Requirement: Explicit finish_task completion and failure signal
The system SHALL treat task completion and task failure as explicit,
model-driven signals via a dedicated `finish_task(success, reason)` tool,
rather than inferring either outcome from the model declining to call any
tool.
#### Scenario: Model signals successful completion
- **WHEN** the model calls `finish_task` with `success=True`
- **THEN** the Planner returns an empty step list and the task is marked
completed
#### Scenario: Model signals it cannot complete the goal
- **WHEN** the model calls `finish_task` with `success=False` and a `reason`
- **THEN** the task is marked failed with that reason, without attempting
any further planning steps
### Requirement: Pluggable dual-provider tool-calling abstraction
The system SHALL support at least two interchangeable LLM providers
(Anthropic native tool use and OpenAI function calling) for the AI
Planner's decision calls, selectable via configuration, with both providers
constrained to return exactly one tool call per request. Independently of
provider selection, the system SHALL support at least two transports for
making that decision call -- direct-to-provider (the tool-calling client
calls the provider's SDK itself, using locally configured credentials) and
cloud-proxy (the tool-calling client calls the Cloud Control Plane's
planner-decision endpoint, which calls the provider using cloud-held
credentials) -- selectable via configuration without requiring any change
to `AIPlanner`'s own decision logic.
#### Scenario: Provider selected via configuration
- **WHEN** the AI Planner is configured with a given provider identifier
- **THEN** it constructs and uses the tool-calling client for that provider
without requiring any change to `AIPlanner`'s own decision logic
#### Scenario: Provider response resolves to a single decision
- **WHEN** either supported provider returns a response to a tool-calling
request
- **THEN** the response is parsed into exactly one tool name and one
arguments object, regardless of which provider produced it
#### Scenario: Transport selected via configuration
- **WHEN** the Host Agent is configured with a given transport (direct or
cloud-proxy)
- **THEN** `AIPlanner` is constructed with the tool-calling client for that
transport, and its own decision logic is unchanged regardless of which
transport is in effect
#### Scenario: Cloud-proxy transport is the default
- **WHEN** no transport is explicitly configured
- **THEN** the AI Planner uses the cloud-proxy transport and the Cloud
Control Plane's planner-decision endpoint
#### Scenario: Direct transport remains available by explicit configuration
- **WHEN** the Host Agent is configured with the direct transport
- **THEN** the AI Planner uses the direct-to-provider transport with locally
configured credentials
#### Scenario: Cloud-proxy transport resolves a decision without a local provider client
- **WHEN** the Host Agent is configured with the cloud-proxy transport
- **THEN** its tool-calling client sends the decision request to the Cloud
Control Plane's planner-decision endpoint instead of constructing a local
Anthropic or OpenAI SDK client
### Requirement: AI Planner is disabled by default and additive to the existing Planner
The system SHALL default to the existing non-LLM Planner unless the AI
Planner is explicitly enabled via configuration, and SHALL NOT alter the
existing Planner's behavior, dependencies, or any caller's construction of
`TaskRunner` when left disabled.
#### Scenario: AI Planner disabled (default)
- **WHEN** `TaskRunner` is constructed without an explicit `planner` and
without the AI Planner enabled in configuration
- **THEN** it uses the existing non-LLM Planner, unchanged from before this
capability existed
#### Scenario: AI Planner enabled via configuration
- **WHEN** `TaskRunner` is constructed without an explicit `planner` and with
the AI Planner enabled in configuration
- **THEN** it uses the AI Planner, configured with the selected provider and
model
### Requirement: Screenshot access is a Planner-only, narrow exception to the Perception Boundary
The system SHALL allow the AI Planner, and only the AI Planner, to receive
the current step's raw screenshot bytes alongside the Scene for
vision-grounded decision-making, while every other perception consumer
SHALL continue to receive only the Scene.
#### Scenario: Planner receives both Scene and screenshot
- **WHEN** a screenshot for the current step is available
- **THEN** the AI Planner's decision call includes both the Scene JSON and
the raw screenshot bytes for that step
#### Scenario: Screenshot unavailable does not block planning
- **WHEN** a screenshot for the current step cannot be obtained
- **THEN** the AI Planner still produces a decision using the Scene alone,
and this is not treated as a task failure
#### Scenario: No other consumer receives raw screenshot bytes
- **WHEN** any component other than the AI Planner (for example, `api`,
`tools`, `perception`, or `storage`) consumes perception output
- **THEN** it receives only the Scene, never raw screenshot bytes
@@ -0,0 +1,72 @@
# cloud-planner-proxy Specification
## Purpose
Define the Cloud Control Plane's planner-decision proxy endpoint, which
allows a Host Agent to delegate AI Planner LLM calls to the cloud using
cloud-held provider credentials, removing the need for Host Agent-held
provider API keys.
## Requirements
### Requirement: Cloud Control Plane exposes an authenticated planner-decision endpoint
The Cloud Control Plane SHALL expose an internal endpoint that accepts an AI Planner tool-calling decision request from an authenticated Host Agent, and SHALL require the same host-scoped bearer credential already used for heartbeat, claim, lease renewal, and result reporting -- no separate credential or enrollment step.
#### Scenario: Authenticated host requests a planner decision
- **WHEN** a Host Agent presents its existing valid host-scoped credentials
with a planner-decision request
- **THEN** the Cloud Control Plane accepts and processes the request for
that host
#### Scenario: Unauthenticated or foreign-host request is rejected
- **WHEN** a request omits valid host-scoped credentials, or presents
credentials bound to a different host than the one referenced in the
request
- **THEN** the Cloud Control Plane rejects the request without invoking any
LLM provider
### Requirement: Endpoint resolves exactly one tool-call decision using cloud-held provider configuration
The Cloud Control Plane SHALL use its own configured LLM provider, model, and credentials -- not any value supplied by the requesting Host Agent -- to resolve a planner-decision request to exactly one tool name and one arguments object, within the request's timeout. The endpoint SHALL resolve the active database-managed Provider profile for every request and use its provider, model, timeout, configured base URL, and encrypted cloud-held credential. It SHALL NOT read Cloud API planner Provider/model/timeout/API-key environment variables.
#### Scenario: Provider returns a usable decision
- **WHEN** the configured provider responds to a planner-decision request
with a tool call
- **THEN** the Cloud Control Plane returns exactly one resolved tool name
and arguments object to the requesting Host Agent
#### Scenario: Configured provider is unreachable or misconfigured
- **WHEN** the Cloud Control Plane's configured provider call fails (for
example, invalid credentials, provider error, or timeout)
- **THEN** the endpoint returns a structured failure response rather than a
fabricated decision, and does not crash the Cloud Control Plane process
#### Scenario: Database profile is the only planner configuration source
- **WHEN** the Cloud API process has legacy planner environment variables
- **THEN** subsequent planner-decision requests use only the active database
profile and do not read a legacy Provider credential for that decision
### Requirement: Cloud-proxy transport removes the need for Host Agent-held provider credentials
A Host Agent using the cloud-proxy transport for its AI Planner SHALL be able to execute AI-planned tasks without any locally configured LLM provider API key.
#### Scenario: Host configured for cloud-proxy transport has no local provider key
- **WHEN** a Host Agent is configured to use the cloud-proxy transport and
has no `ANTHROPIC_API_KEY`/`OPENAI_API_KEY` set in its own environment
- **THEN** it can still obtain AI Planner decisions by calling the Cloud
Control Plane's planner-decision endpoint
### Requirement: Planner-decision requests are not durably persisted
The Cloud Control Plane SHALL process planner-decision requests, including any screenshot and prompt text they carry, without durably persisting that screenshot or prompt content; only request metadata (such as host identifier, resolved tool name, latency, and error classification) may be retained for observability.
#### Scenario: Request handling completes without storing prompt or screenshot content
- **WHEN** the Cloud Control Plane finishes handling a planner-decision
request
- **THEN** the raw prompt text and screenshot bytes from that request are
not present in any durable store or log the Cloud Control Plane retains
### Requirement: Planner-proxy failures do not silently substitute a default action
The Cloud Control Plane SHALL report a failure to the requesting Host Agent when it cannot resolve a planner-decision request to a valid tool call, rather than returning a default, guessed, or previously cached decision.
#### Scenario: Endpoint cannot resolve a decision
- **WHEN** the configured provider does not return a usable tool call for a
planner-decision request
- **THEN** the Cloud Control Plane's response indicates failure, and the
requesting Host Agent treats the planner call for that turn as failed
@@ -0,0 +1,121 @@
# llm-provider-management Specification
## Purpose
Define how the Cloud Control Plane stores, protects, and activates
administrator-managed LLM Provider profiles in a durable database, so that
cloud-transport planner decisions resolve provider, model, timeout, base URL,
and API key from a single active database profile instead of from environment
variables.
## Requirements
### Requirement: Cloud-wide LLM Provider profiles are durable and validated
The Cloud Control Plane SHALL persist administrator-managed LLM Provider
profiles with a unique name, provider type, model, timeout, enabled state,
revision, and timestamps. A profile's provider type SHALL be either
`anthropic` or `openai-compatible`; either profile MAY specify an absolute
HTTP(S) base URL and SHALL use its official provider endpoint when it does not.
#### Scenario: Administrator creates an OpenAI-compatible profile
- **WHEN** an authorized administrator submits a unique profile name,
`openai-compatible` provider type, model, valid timeout, API key, and an
optional valid base URL
- **THEN** the Cloud Control Plane persists an enabled profile with a new
revision and returns its non-secret metadata
#### Scenario: Administrator creates an Anthropic profile with a custom base URL
- **WHEN** an authorized administrator submits a unique profile name,
`anthropic` provider type, model, valid timeout, API key, and a valid
Anthropic-compatible base URL
- **THEN** the Cloud Control Plane persists the normalized URL with the profile
and returns its non-secret metadata
#### Scenario: Invalid profile configuration is rejected
- **WHEN** an administrator submits an unsupported provider type, blank model,
non-positive timeout, duplicate name, or invalid base URL
- **THEN** the Cloud Control Plane rejects the write without creating or
changing a profile
### Requirement: Provider API keys are encrypted and never disclosed
The Cloud Control Plane SHALL encrypt Provider API keys before persistence
using a deployment-held encryption key, and SHALL not expose plaintext keys in
read responses, validation errors, audit records, or application logs.
#### Scenario: Provider profile is listed after creation
- **WHEN** an authorized administrator lists Provider profiles after creating
one with an API key
- **THEN** every response reports only key-presence and rotation metadata and
does not contain the submitted API key or its ciphertext
#### Scenario: Encryption configuration is unavailable
- **WHEN** a database-managed profile is created, rotated, activated, or
resolved without a valid deployment encryption key
- **THEN** the operation fails with a controlled configuration error that does
not reveal an API key
### Requirement: Administrators can manage and activate Provider profiles
The Cloud Control Plane SHALL expose session-CSRF-protected and scope-guarded
operations to list, create, update, rotate a key, enable, disable, activate,
and delete inactive LLM Provider profiles. Mutating operations SHALL require
the `llm-providers:admin` scope and record a non-secret audit event.
#### Scenario: Non-administrator attempts to modify a profile
- **WHEN** a principal without `llm-providers:admin` invokes a Provider
mutation endpoint
- **THEN** the Cloud Control Plane rejects the request before decrypting or
modifying a Provider credential
#### Scenario: Administrator switches the active profile
- **WHEN** an authorized administrator activates an enabled profile
- **THEN** the Cloud Control Plane atomically selects that profile as the one
Cloud-wide active profile and records the activation without retaining an
API key in the audit event
#### Scenario: Administrator attempts to retire the active profile
- **WHEN** an administrator attempts to disable or delete the active profile
before activating a replacement
- **THEN** the Cloud Control Plane rejects the operation and preserves the
active profile selection
### Requirement: Activation dynamically selects one Provider for cloud transport
The Cloud Control Plane SHALL resolve the active database-managed profile for
each Cloud planner decision and SHALL apply an activation to subsequent
requests without requiring a Cloud API restart or any Host Agent
reconfiguration.
#### Scenario: A Host requests a decision after a model switch
- **WHEN** an administrator activates a different enabled profile and a
cloud-transport Host Agent submits its next planner-decision request
- **THEN** the Cloud Control Plane calls that profile's provider, model,
timeout, base URL, and API key while the Host Agent continues using the same
Cloud Control Plane endpoint
#### Scenario: Active OpenAI-compatible profile is used
- **WHEN** the active profile is OpenAI-compatible and includes a base URL
- **THEN** the Cloud Control Plane makes the existing OpenAI Chat Completions
tool-calling request to that base URL using the profile's decrypted API key
and returns the resulting single tool-call decision
#### Scenario: Active Anthropic profile is used with a custom base URL
- **WHEN** the active profile is Anthropic and includes a base URL
- **THEN** the Cloud Control Plane makes the existing Anthropic native
tool-calling request to that base URL using the profile's decrypted API key
and returns the resulting single tool-call decision
### Requirement: Cloud planner Provider configuration is database-only
The Cloud Control Plane SHALL resolve Cloud planner Provider, model, timeout,
base URL, and API key from the active database profile and SHALL NOT read
planner Provider/model/timeout or Provider API-key environment variables.
When no usable active profile exists, it SHALL fail a planner request with a
structured unavailable response.
#### Scenario: Active profile is unavailable
- **WHEN** the active database Provider profile is missing, disabled, or
cannot be decrypted
- **THEN** the planner decision fails without invoking a legacy
environment-configured Provider
#### Scenario: Legacy environment variables are present
- **WHEN** the Cloud API process has legacy planner Provider or API-key
environment variables but an active database profile exists
- **THEN** the planner decision uses only the active database profile
@@ -0,0 +1,27 @@
## Purpose
Define the supported Python interpreter version range for the project workspace, container images, and CI pipeline, ensuring all environments converge on a single Python baseline.
## Requirements
### Requirement: Workspace-wide supported Python version
The system SHALL declare a single, consistent supported Python interpreter version range across the `uv` workspace root and every workspace member (`apps/device-host-agent`, `apps/cloud-api`, `packages/cloud-platform`), such that the range admits Python 3.13 and excludes Python 3.14, so that `paddlepaddle` (which has no Python 3.14 wheel available on PyPI) can be installed in every environment that follows the project's documented setup.
#### Scenario: Workspace resolves and syncs on Python 3.13
- **WHEN** an operator runs `uv python install 3.13` followed by `uv sync --locked --all-packages` in the repository root
- **THEN** the sync succeeds and provisions a `.venv` using a Python 3.13 interpreter, with `uv.lock` resolved against that interpreter
#### Scenario: A Python 3.14 interpreter is rejected
- **WHEN** `uv` attempts to resolve or sync the workspace against a Python 3.14 interpreter
- **THEN** `uv` reports the interpreter as incompatible with the declared `requires-python` range, rather than silently proceeding
### Requirement: Container and CI images match the declared Python baseline
The system SHALL build its Docker image and Jenkins CI pipeline using a base image whose Python version matches the workspace's declared `requires-python` range, so that containerized and CI environments never diverge from what local development targets.
#### Scenario: Docker image build uses the supported Python version
- **WHEN** the project's `Dockerfile` is built
- **THEN** the resulting image's base image is `astral-sh/uv:python3.13-bookworm-slim`
#### Scenario: CI pipeline uses the supported Python version
- **WHEN** the Jenkins pipeline defined in `Jenkinsfile` executes
- **THEN** it runs inside the `astral-sh/uv:python3.13-bookworm-slim` image
@@ -0,0 +1,56 @@
## Purpose
Define how the Python repository is structured as a uv workspace, including the shared lockfile, one-way dependency direction between the Runtime and cloud platform packages, import-path and behavior compatibility during the packaging migration, independent buildability of workspace members, and the boundary that keeps the frontend JavaScript project outside the Python workspace.
## Requirements
### Requirement: Repository uses a uv workspace with a shared lockfile
The repository SHALL define a uv workspace containing the root `device-agent-runtime` project and the extracted `device-cloud-platform` project, and SHALL commit one generated `uv.lock` that resolves all workspace members.
#### Scenario: Clean workspace synchronization
- **WHEN** a developer synchronizes the repository from a clean checkout using uv
- **THEN** uv installs the workspace members and their locked dependencies without requiring manual editable-install commands
#### Scenario: Lockfile consistency check
- **WHEN** CI validates dependency metadata without updating dependencies
- **THEN** the committed lockfile is accepted as current for every workspace member
### Requirement: Cloud packaging has an explicit one-way Runtime dependency
The `device-cloud-platform` project SHALL declare `device-agent-runtime` as a uv workspace dependency, and the `device-agent-runtime` project MUST NOT depend on `device-cloud-platform` or import the `cloud` package.
#### Scenario: Cloud modules consume Runtime contracts
- **WHEN** the cloud platform is installed through the workspace
- **THEN** its imports of existing Runtime, workflow, storage, and driver contracts resolve through the declared Runtime workspace dependency
#### Scenario: Runtime remains cloud-independent
- **WHEN** dependency-direction tests inspect Runtime-owned source packages
- **THEN** no Runtime-owned package imports `cloud` or requires the cloud distribution to execute existing local capabilities
### Requirement: Existing cloud import paths and behavior remain compatible
The packaging migration SHALL preserve the existing `cloud.*` Python import paths and SHALL NOT change REST, MCP, scheduling, dispatch, persistence, or SDK behavior.
#### Scenario: Existing cloud tests run after extraction
- **WHEN** the existing cloud test suite runs from the workspace root after the package is moved
- **THEN** the tests import the same `cloud.*` modules and retain their pre-migration behavior
#### Scenario: Local Runtime runs without cloud application members
- **WHEN** a developer starts or tests the root Runtime project without starting a cloud application
- **THEN** existing Runtime entry points and local device behavior remain available
### Requirement: Workspace members are independently buildable and selectable
Each Python workspace member SHALL contain valid build metadata, SHALL produce an installable distribution, and SHALL support member-scoped uv commands while root-level tests remain supported.
#### Scenario: Build all distributions
- **WHEN** CI builds the Runtime and cloud platform workspace members
- **THEN** each build produces a wheel whose declared packages can be imported in an isolated environment
#### Scenario: Run a member-scoped command
- **WHEN** a developer selects a workspace member with uv's package selection option
- **THEN** the command executes with that member's declared dependencies and workspace sources
### Requirement: Frontend package management remains separate
The Vue/Vite `console` project SHALL remain managed by its existing JavaScript package manager and SHALL NOT become a Python uv workspace member.
#### Scenario: Synchronize Python workspace
- **WHEN** a developer runs uv synchronization at the repository root
- **THEN** uv does not install or modify the console's JavaScript dependencies