feat(cloud): add targeted task governance foundation
Tests / Test passed: 659

This commit is contained in:
2026-07-13 22:21:12 +08:00
parent a3ba94be04
commit 2cd314b183
41 changed files with 2099 additions and 15 deletions
+8
View File
@@ -43,6 +43,7 @@ from cloud.pool import DevicePool
from cloud.scheduler import TaskScheduler
from cloud.schema import require_current_schema
from cloud.sdk.api import create_cloud_router
from cloud.sdk.governance_api import create_governance_router
from cloud.sdk.user_api import create_user_auth_router
from cloud.user_auth import USER_CSRF_COOKIE, USER_SESSION_COOKIE, UserAuthService, UserAuthSettings
from core.models import utc_now
@@ -288,11 +289,18 @@ def create_app(
config=control_config,
)
)
app.include_router(
create_governance_router(
repository=repository,
auth_provider=auth_provider,
)
)
app.include_router(
create_internal_router(
pool=pool,
auth_provider=auth_provider,
lease_duration_seconds=control_config.lease_duration_seconds,
scheduler=scheduler,
)
)
@@ -14,6 +14,7 @@ from cloud.internal_api.models import (
DeviceSnapshotModel,
HeartbeatResponse,
HostEnrollmentResponse,
HostTaskSubmissionResponse,
LeaseRenewalResponse,
TerminalResultResponse,
)
@@ -146,6 +147,7 @@ class HostAgentClient:
devices: list[DeviceSnapshotModel],
*,
address: str | None = None,
policy_revision: int = 0,
) -> HeartbeatResponse:
response = await self._request(
"PUT",
@@ -154,10 +156,28 @@ class HostAgentClient:
"host_id": self.config.host_id,
"address": address,
"devices": [device.model_dump(mode="json") for device in devices],
"policy_revision": policy_revision,
},
)
return HeartbeatResponse.model_validate(response.json())
async def submit_self_task(
self,
*,
goal: str,
device_id: str | None = None,
) -> HostTaskSubmissionResponse:
response = await self._request(
"POST",
f"/internal/v1/hosts/{self.config.host_id}/tasks",
json={
"host_id": self.config.host_id,
"goal": goal,
"device_id": device_id,
},
)
return HostTaskSubmissionResponse.model_validate(response.json())
async def claim(self) -> AssignmentModel | None:
response = await self._request(
"POST",
@@ -27,7 +27,7 @@ import httpx
from cloud.internal_api.models import PlannerDecisionError, PlannerDecisionResponse
from host_agent.config import HostAgentConfig
from runtime.tool_calling_client import ToolCallDecision, ToolCallUnavailable
from runtime.tool_calling_client import ToolCallDecision, ToolCallUnavailable, ToolCallUsage
from runtime.tool_specs import ToolSpec
@@ -85,6 +85,22 @@ class CloudProxyToolCallingClient:
return ToolCallDecision(
tool_name=decoded.tool_name,
arguments=dict(decoded.arguments),
usage=(
ToolCallUsage(
input_tokens=decoded.input_tokens,
output_tokens=decoded.output_tokens,
total_tokens=decoded.total_tokens,
)
if any(
value is not None
for value in (
decoded.input_tokens,
decoded.output_tokens,
decoded.total_tokens,
)
)
else None
),
)
raise ToolCallUnavailable(_error_detail(response))
@@ -45,13 +45,19 @@ class HeartbeatSynchronizer:
self._sleep = sleep
self.status_tracker = status_tracker
self.on_sync = on_sync
self.policy_revision = 0
self.policy = None
async def sync_once(self) -> HeartbeatResponse:
snapshot = build_device_snapshot(self.manager)
response = await self.client.heartbeat(
snapshot,
address=self.address,
policy_revision=self.policy_revision,
)
self.policy_revision = response.policy_revision
if response.policy is not None:
self.policy = response.policy
if self.status_tracker is not None:
self.status_tracker.mark_heartbeat(ok=True, device_count=len(snapshot))
if self.on_sync is not None:
@@ -58,7 +58,7 @@ def test_heartbeat_synchronizer_runs_at_configured_interval_until_stopped() -> N
calls: list[list[str]] = []
class FakeClient:
async def heartbeat(self, devices, *, address=None):
async def heartbeat(self, devices, *, address=None, policy_revision=0):
calls.append([device.device_id for device in devices])
return HeartbeatResponse(
host_id="host-a",
@@ -96,7 +96,7 @@ def test_sync_once_notifies_status_tracker_and_on_sync_with_device_count() -> No
)
class FakeClient:
async def heartbeat(self, devices, *, address=None):
async def heartbeat(self, devices, *, address=None, policy_revision=0):
return HeartbeatResponse(
host_id="host-a",
accepted_devices=len(devices),