create-windy 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +292 -125
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
- package/template/apps/agent-server/adk_web_agents/services.py +25 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
- package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
- package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
- package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
- package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
- package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
- package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
- package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
- package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
- package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
- package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
- package/template/apps/agent-server/tests/test_report_store.py +286 -0
- package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
- package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
- package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
- package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
- package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
- package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
- package/template/apps/server/src/agent/debug-session-id.ts +37 -0
- package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
- package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
- package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
- package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
- package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
- package/template/apps/server/src/agent/run-authorization.ts +32 -1
- package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +5 -0
- package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
- package/template/apps/server/src/agent/run-facade.test.ts +78 -0
- package/template/apps/server/src/agent/run-facade.ts +33 -3
- package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
- package/template/apps/server/src/agent/run-input-verification.ts +41 -0
- package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
- package/template/apps/server/src/agent/tool-host.test.ts +1 -0
- package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
- package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
- package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
- package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
- package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
- package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
- package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
- package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
- package/template/apps/server/src/license/runtime-service.test.ts +6 -0
- package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
- package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
- package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
- package/template/apps/server/src/module-host/initialize.ts +6 -197
- package/template/apps/server/src/module-host/request-context.ts +1 -0
- package/template/apps/server/src/module-host/scoped-host.ts +214 -0
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
- package/template/apps/server/src/runtime-adk-debug.ts +8 -2
- package/template/apps/server/src/system/operations.test.ts +54 -0
- package/template/apps/server/src/system/operations.ts +23 -1
- package/template/apps/server/src/system/seed.test.ts +4 -0
- package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
- package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
- package/template/apps/web/src/layout/AppSidebar.vue +6 -1
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
- package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
- package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
- package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
- package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
- package/template/apps/web/src/router/error-navigation.ts +22 -0
- package/template/apps/web/src/router/index.ts +12 -23
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
- package/template/apps/web/src/services/operations-api.ts +28 -0
- package/template/docker-compose.agent-debug.yml +1 -1
- package/template/docs/architecture/ai-runtime.md +2 -1
- package/template/docs/architecture/module-host.md +3 -0
- package/template/docs/platform/agent-run-attestation.md +65 -0
- package/template/docs/platform/agent-runtime-observability.md +135 -0
- package/template/docs/platform/agent-runtime.md +47 -3
- package/template/docs/platform/operations-overview.md +13 -0
- package/template/docs/platform/platform-authentication.md +4 -3
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
- package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
- package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
- package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
- package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
- package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
- package/template/packages/database/drizzle/meta/_journal.json +21 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +114 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +10 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
- package/template/packages/modules/src/system-api-control-plane.ts +17 -1
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +8 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
- package/template/packages/modules/src/system-module-catalog.ts +15 -4
- package/template/packages/modules/src/system-modules-access.test.ts +69 -0
- package/template/packages/modules/src/system-modules.ts +13 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
- package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
- package/template/packages/server-sdk/src/module-host.ts +5 -0
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +134 -0
- package/template/scripts/lib/postgres-regression.ts +201 -0
|
@@ -4,8 +4,9 @@ import asyncio
|
|
|
4
4
|
import logging
|
|
5
5
|
import uuid
|
|
6
6
|
|
|
7
|
+
from .approval_resolution import resolve_pending_approval
|
|
7
8
|
from .artifact_input import ArtifactLoader
|
|
8
|
-
from .contracts import
|
|
9
|
+
from .contracts import StartRunRequest
|
|
9
10
|
from .engine import (
|
|
10
11
|
AgentEngineAdapter,
|
|
11
12
|
EngineCompleted,
|
|
@@ -14,19 +15,24 @@ from .engine import (
|
|
|
14
15
|
)
|
|
15
16
|
from .fingerprint import request_fingerprint, resolved_release
|
|
16
17
|
from .lease import cancel_on_lease_loss, lease_failed
|
|
17
|
-
from .
|
|
18
|
-
|
|
18
|
+
from .report_lifecycle import (
|
|
19
|
+
append_and_sync_canonical_event,
|
|
20
|
+
ensure_run_report_safely,
|
|
21
|
+
start_run_report_safely,
|
|
22
|
+
sync_terminal_from_canonical,
|
|
23
|
+
)
|
|
24
|
+
from .report_store import AgentRuntimeReportStore
|
|
25
|
+
from .signal_events import canonical_failure_fields, canonical_signal_event
|
|
26
|
+
from .store import RunRecord, RunStore
|
|
19
27
|
from .telemetry import AgentRunTrace
|
|
20
28
|
|
|
21
29
|
logger = logging.getLogger(__name__)
|
|
22
30
|
|
|
23
31
|
|
|
24
|
-
class RunNotFoundError(Exception):
|
|
25
|
-
pass
|
|
32
|
+
class RunNotFoundError(Exception): ...
|
|
26
33
|
|
|
27
34
|
|
|
28
|
-
class OperationNotFoundError(Exception):
|
|
29
|
-
pass
|
|
35
|
+
class OperationNotFoundError(Exception): ...
|
|
30
36
|
|
|
31
37
|
|
|
32
38
|
class AgentRunService:
|
|
@@ -34,10 +40,12 @@ class AgentRunService:
|
|
|
34
40
|
self,
|
|
35
41
|
engine: AgentEngineAdapter,
|
|
36
42
|
store: RunStore,
|
|
43
|
+
report_store: AgentRuntimeReportStore,
|
|
37
44
|
artifact_loader: ArtifactLoader | None = None,
|
|
38
45
|
) -> None:
|
|
39
46
|
self.engine = engine
|
|
40
47
|
self.store = store
|
|
48
|
+
self.report_store = report_store
|
|
41
49
|
self.artifact_loader = artifact_loader
|
|
42
50
|
self._tasks: set[asyncio.Task[None]] = set()
|
|
43
51
|
self._active_run_ids: set[str] = set()
|
|
@@ -72,6 +80,7 @@ class AgentRunService:
|
|
|
72
80
|
candidate,
|
|
73
81
|
request.model_dump(by_alias=True),
|
|
74
82
|
)
|
|
83
|
+
await ensure_run_report_safely(self.report_store, record.run_id, request)
|
|
75
84
|
if created:
|
|
76
85
|
self._spawn(record, request)
|
|
77
86
|
return record
|
|
@@ -92,14 +101,21 @@ class AgentRunService:
|
|
|
92
101
|
return_exceptions=True,
|
|
93
102
|
)
|
|
94
103
|
await self.store.close()
|
|
104
|
+
await self.report_store.close()
|
|
95
105
|
|
|
96
106
|
async def _recover_once(self) -> None:
|
|
97
107
|
for record, raw in await self.store.recoverable():
|
|
98
108
|
if record.run_id in self._active_run_ids:
|
|
99
109
|
continue
|
|
110
|
+
request = StartRunRequest.model_validate(raw)
|
|
111
|
+
await ensure_run_report_safely(
|
|
112
|
+
self.report_store,
|
|
113
|
+
record.run_id,
|
|
114
|
+
request,
|
|
115
|
+
)
|
|
100
116
|
self._spawn(
|
|
101
117
|
record,
|
|
102
|
-
|
|
118
|
+
request,
|
|
103
119
|
resumed=True,
|
|
104
120
|
)
|
|
105
121
|
|
|
@@ -141,8 +157,13 @@ class AgentRunService:
|
|
|
141
157
|
if record.terminal:
|
|
142
158
|
return record
|
|
143
159
|
await self.engine.cancel(run_id)
|
|
144
|
-
await self.
|
|
145
|
-
await
|
|
160
|
+
await resolve_pending_approval(self.store, record, "cancelled")
|
|
161
|
+
await append_and_sync_canonical_event(
|
|
162
|
+
self.report_store,
|
|
163
|
+
self.store,
|
|
164
|
+
run_id,
|
|
165
|
+
"run.cancelled",
|
|
166
|
+
)
|
|
146
167
|
return await self.get(run_id)
|
|
147
168
|
|
|
148
169
|
async def _execute(
|
|
@@ -162,10 +183,14 @@ class AgentRunService:
|
|
|
162
183
|
lambda completed: cancel_on_lease_loss(completed, execution_task)
|
|
163
184
|
)
|
|
164
185
|
try:
|
|
165
|
-
await self.
|
|
186
|
+
await start_run_report_safely(self.report_store, record.run_id)
|
|
187
|
+
if await append_and_sync_canonical_event(
|
|
188
|
+
self.report_store,
|
|
189
|
+
self.store,
|
|
166
190
|
record.run_id,
|
|
167
191
|
"run.resumed" if resumed else "run.started",
|
|
168
|
-
)
|
|
192
|
+
):
|
|
193
|
+
return
|
|
169
194
|
if invocation.artifacts and not self.artifact_loader:
|
|
170
195
|
raise SafeEngineError(
|
|
171
196
|
"RUNTIME_UNAVAILABLE",
|
|
@@ -217,9 +242,21 @@ class AgentRunService:
|
|
|
217
242
|
async for signal in self.engine.execute(request):
|
|
218
243
|
if isinstance(signal, EngineCompleted):
|
|
219
244
|
await self.store.complete(record.run_id, signal.output)
|
|
245
|
+
await sync_terminal_from_canonical(
|
|
246
|
+
self.report_store,
|
|
247
|
+
self.store,
|
|
248
|
+
record.run_id,
|
|
249
|
+
)
|
|
220
250
|
return
|
|
221
251
|
event_type, fields = canonical_signal_event(signal)
|
|
222
|
-
await
|
|
252
|
+
if await append_and_sync_canonical_event(
|
|
253
|
+
self.report_store,
|
|
254
|
+
self.store,
|
|
255
|
+
record.run_id,
|
|
256
|
+
event_type,
|
|
257
|
+
fields,
|
|
258
|
+
):
|
|
259
|
+
return
|
|
223
260
|
await self._fail(
|
|
224
261
|
record.run_id,
|
|
225
262
|
SafeEngineError(
|
|
@@ -232,7 +269,7 @@ class AgentRunService:
|
|
|
232
269
|
await self._fail(record.run_id, error)
|
|
233
270
|
except TimeoutError:
|
|
234
271
|
current = await self.get(record.run_id)
|
|
235
|
-
await self.
|
|
272
|
+
await resolve_pending_approval(self.store, current, "timed-out")
|
|
236
273
|
await self._fail(
|
|
237
274
|
record.run_id,
|
|
238
275
|
SafeEngineError(
|
|
@@ -302,44 +339,12 @@ class AgentRunService:
|
|
|
302
339
|
self._tasks.discard(task)
|
|
303
340
|
self._active_run_ids.discard(run_id)
|
|
304
341
|
|
|
305
|
-
async def _resolve_pending_approval(
|
|
306
|
-
self,
|
|
307
|
-
record: RunRecord,
|
|
308
|
-
outcome: str,
|
|
309
|
-
) -> None:
|
|
310
|
-
pending: dict[str, object] | None = None
|
|
311
|
-
for event in record.events:
|
|
312
|
-
if event.get("type") == "approval.required":
|
|
313
|
-
pending = event
|
|
314
|
-
elif event.get("type") == "approval.resolved":
|
|
315
|
-
pending = None
|
|
316
|
-
if not pending:
|
|
317
|
-
return
|
|
318
|
-
work_item_id = pending.get("workItemId")
|
|
319
|
-
tool_call_id = pending.get("toolCallId")
|
|
320
|
-
if isinstance(work_item_id, str) and isinstance(tool_call_id, str):
|
|
321
|
-
await self.store.append(
|
|
322
|
-
record.run_id,
|
|
323
|
-
"approval.resolved",
|
|
324
|
-
{
|
|
325
|
-
"workItemId": work_item_id,
|
|
326
|
-
"toolCallId": tool_call_id,
|
|
327
|
-
"outcome": outcome,
|
|
328
|
-
},
|
|
329
|
-
)
|
|
330
|
-
|
|
331
342
|
async def _fail(self, run_id: str, failure: SafeEngineError) -> None:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
request_id=f"request_{uuid.uuid4().hex}",
|
|
336
|
-
retryable=failure.retryable,
|
|
337
|
-
)
|
|
338
|
-
await self.store.append(
|
|
343
|
+
await append_and_sync_canonical_event(
|
|
344
|
+
self.report_store,
|
|
345
|
+
self.store,
|
|
339
346
|
run_id,
|
|
340
347
|
"run.failed",
|
|
341
|
-
|
|
348
|
+
canonical_failure_fields(failure),
|
|
349
|
+
fallback_safe_error_code=failure.code,
|
|
342
350
|
)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
__all__ = ["AgentRunService", "IdempotencyConflictError"]
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from .contracts import AgentError
|
|
3
6
|
from .engine import (
|
|
4
7
|
ApprovalRequired,
|
|
5
8
|
ApprovalResolved,
|
|
6
9
|
OutputDelta,
|
|
10
|
+
SafeEngineError,
|
|
7
11
|
ToolCompleted,
|
|
8
12
|
ToolRequested,
|
|
9
13
|
UsageReported,
|
|
@@ -32,5 +36,18 @@ def canonical_signal_event(signal: object) -> tuple[str, dict[str, object]]:
|
|
|
32
36
|
if isinstance(signal, ToolCompleted):
|
|
33
37
|
return "tool.completed", {"toolCallId": signal.tool_call_id}
|
|
34
38
|
if isinstance(signal, UsageReported):
|
|
35
|
-
return "usage.reported", {
|
|
39
|
+
return "usage.reported", {
|
|
40
|
+
"attemptSequence": signal.attempt_sequence,
|
|
41
|
+
"usage": signal.usage.event_value(),
|
|
42
|
+
}
|
|
36
43
|
raise TypeError("unsupported engine signal")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def canonical_failure_fields(failure: SafeEngineError) -> dict[str, object]:
|
|
47
|
+
error = AgentError(
|
|
48
|
+
code=failure.code,
|
|
49
|
+
message=failure.safe_message,
|
|
50
|
+
request_id=f"request_{uuid.uuid4().hex}",
|
|
51
|
+
retryable=failure.retryable,
|
|
52
|
+
)
|
|
53
|
+
return {"error": error.model_dump(by_alias=True)}
|
|
@@ -62,15 +62,13 @@ async def test_adk_runner_maps_text_usage_and_terminal() -> None:
|
|
|
62
62
|
"你",
|
|
63
63
|
"好",
|
|
64
64
|
]
|
|
65
|
-
usage = [signal
|
|
66
|
-
assert usage ==
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
]
|
|
65
|
+
usage = [signal for signal in signals if isinstance(signal, UsageReported)]
|
|
66
|
+
assert len(usage) == 1
|
|
67
|
+
assert usage[0].attempt_sequence == 1
|
|
68
|
+
assert usage[0].usage.source == "provider-reported"
|
|
69
|
+
assert usage[0].usage.input_tokens == 2
|
|
70
|
+
assert usage[0].usage.output_tokens == 2
|
|
71
|
+
assert usage[0].usage.total_tokens == 4
|
|
74
72
|
assert isinstance(signals[-1], EngineCompleted)
|
|
75
73
|
assert signals[-1].output == {"text": "你好"}
|
|
76
74
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
from google.adk.cli.service_registry import get_service_registry
|
|
6
|
+
from google.adk.events.event import Event
|
|
7
|
+
from google.genai import types
|
|
8
|
+
|
|
9
|
+
from adk_web_agents.services import SESSION_SERVICE_URI
|
|
10
|
+
from southwind_agent_server.adk_debug_sessions import (
|
|
11
|
+
IMPORTED_SESSION_STATE_KEY,
|
|
12
|
+
DebugSessionDetailV1,
|
|
13
|
+
DebugSessionListEnvelopeV1,
|
|
14
|
+
ImportedDebugSessionReadOnlyError,
|
|
15
|
+
WindyDebugSessionClientV1,
|
|
16
|
+
WindyDebugSessionServiceV1,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
SESSION_ID = "wdbg1_" + "a" * 43
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_services_module_registers_official_custom_uri(
|
|
23
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
24
|
+
) -> None:
|
|
25
|
+
monkeypatch.setenv("ADK_DEBUG_PLATFORM_URL", "https://platform.test")
|
|
26
|
+
monkeypatch.setenv("ADK_DEBUG_PLATFORM_TOKEN", "debug-token")
|
|
27
|
+
|
|
28
|
+
service = get_service_registry().create_session_service(SESSION_SERVICE_URI)
|
|
29
|
+
|
|
30
|
+
assert isinstance(service, WindyDebugSessionServiceV1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def test_client_uses_read_only_platform_routes_and_strict_v1_dto() -> None:
|
|
34
|
+
requests: list[httpx.Request] = []
|
|
35
|
+
|
|
36
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
37
|
+
requests.append(request)
|
|
38
|
+
if request.url.path.endswith(SESSION_ID):
|
|
39
|
+
return httpx.Response(200, json=detail_envelope())
|
|
40
|
+
return httpx.Response(200, json=list_envelope())
|
|
41
|
+
|
|
42
|
+
client = WindyDebugSessionClientV1(
|
|
43
|
+
"https://platform.test",
|
|
44
|
+
"debug-token",
|
|
45
|
+
transport=httpx.MockTransport(handler),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
listed = await client.list()
|
|
49
|
+
detail = await client.get(SESSION_ID)
|
|
50
|
+
|
|
51
|
+
assert listed.sessions[0].id == SESSION_ID
|
|
52
|
+
assert detail and detail.messages[1].content == "只读回答"
|
|
53
|
+
assert [request.method for request in requests] == ["GET", "GET"]
|
|
54
|
+
assert all(
|
|
55
|
+
request.headers["authorization"] == "Bearer debug-token" for request in requests
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def test_service_overlays_imported_projection_and_keeps_local_sessions() -> None:
|
|
60
|
+
client = FakeClient()
|
|
61
|
+
service = WindyDebugSessionServiceV1(client)
|
|
62
|
+
local = await service.create_session(
|
|
63
|
+
app_name="canonical_debug",
|
|
64
|
+
user_id="user",
|
|
65
|
+
session_id="local-session",
|
|
66
|
+
state={"topic": "local"},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
listed = await service.list_sessions(app_name="canonical_debug", user_id="user")
|
|
70
|
+
imported = await service.get_session(
|
|
71
|
+
app_name="canonical_debug",
|
|
72
|
+
user_id="user",
|
|
73
|
+
session_id=SESSION_ID,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assert {session.id for session in listed.sessions} == {
|
|
77
|
+
local.id,
|
|
78
|
+
SESSION_ID,
|
|
79
|
+
}
|
|
80
|
+
assert imported
|
|
81
|
+
assert imported.state[IMPORTED_SESSION_STATE_KEY] is True
|
|
82
|
+
assert [event.content.role for event in imported.events if event.content] == [
|
|
83
|
+
"user",
|
|
84
|
+
"model",
|
|
85
|
+
]
|
|
86
|
+
await service.delete_session(
|
|
87
|
+
app_name="canonical_debug", user_id="user", session_id=SESSION_ID
|
|
88
|
+
)
|
|
89
|
+
after_delete = await service.list_sessions(
|
|
90
|
+
app_name="canonical_debug", user_id="user"
|
|
91
|
+
)
|
|
92
|
+
assert [session.id for session in after_delete.sessions] == [local.id]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_list_contract_accepts_all_sessions_after_platform_drains_pages() -> None:
|
|
96
|
+
envelope = DebugSessionListEnvelopeV1.model_validate(
|
|
97
|
+
{
|
|
98
|
+
"schemaVersion": 1,
|
|
99
|
+
"sessions": [
|
|
100
|
+
{
|
|
101
|
+
**summary(),
|
|
102
|
+
"id": f"wdbg1_{index:043d}",
|
|
103
|
+
"title": f"历史研判 {index}",
|
|
104
|
+
}
|
|
105
|
+
for index in range(101)
|
|
106
|
+
],
|
|
107
|
+
"truncated": False,
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
assert len(envelope.sessions) == 101
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def test_imported_projection_rejects_mutation_and_follow_up() -> None:
|
|
115
|
+
service = WindyDebugSessionServiceV1(FakeClient())
|
|
116
|
+
imported = await service.get_session(
|
|
117
|
+
app_name="canonical_debug",
|
|
118
|
+
user_id="user",
|
|
119
|
+
session_id=SESSION_ID,
|
|
120
|
+
)
|
|
121
|
+
assert imported
|
|
122
|
+
|
|
123
|
+
with pytest.raises(ImportedDebugSessionReadOnlyError, match="只读投影"):
|
|
124
|
+
await service.append_event(
|
|
125
|
+
imported,
|
|
126
|
+
Event(
|
|
127
|
+
author="user",
|
|
128
|
+
content=types.Content(role="user", parts=[types.Part(text="继续追问")]),
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
with pytest.raises(ImportedDebugSessionReadOnlyError, match="保留值"):
|
|
132
|
+
await service.create_session(
|
|
133
|
+
app_name="canonical_debug",
|
|
134
|
+
user_id="user",
|
|
135
|
+
session_id=SESSION_ID,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@pytest.mark.parametrize(
|
|
140
|
+
"error",
|
|
141
|
+
[
|
|
142
|
+
pytest.param(httpx.ConnectError("offline"), id="connect-error"),
|
|
143
|
+
pytest.param(ValueError("provider failed"), id="provider-exception"),
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
async def test_remote_failure_does_not_break_normal_local_sessions(
|
|
147
|
+
error: Exception,
|
|
148
|
+
) -> None:
|
|
149
|
+
service = WindyDebugSessionServiceV1(FailingListClient(error))
|
|
150
|
+
local = await service.create_session(
|
|
151
|
+
app_name="canonical_debug", user_id="user", session_id="local-session"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
listed = await service.list_sessions(app_name="canonical_debug", user_id="user")
|
|
155
|
+
|
|
156
|
+
assert [session.id for session in listed.sessions] == [local.id]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class FakeClient:
|
|
160
|
+
async def list(self) -> DebugSessionListEnvelopeV1:
|
|
161
|
+
return DebugSessionListEnvelopeV1.model_validate(list_envelope())
|
|
162
|
+
|
|
163
|
+
async def get(self, session_id: str) -> DebugSessionDetailV1 | None:
|
|
164
|
+
if session_id != SESSION_ID:
|
|
165
|
+
return None
|
|
166
|
+
return DebugSessionDetailV1.model_validate(detail_envelope()["session"])
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class FailingListClient(FakeClient):
|
|
170
|
+
def __init__(self, error: Exception) -> None:
|
|
171
|
+
self._error = error
|
|
172
|
+
|
|
173
|
+
async def list(self) -> DebugSessionListEnvelopeV1:
|
|
174
|
+
raise self._error
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def list_envelope() -> dict[str, object]:
|
|
178
|
+
return {
|
|
179
|
+
"schemaVersion": 1,
|
|
180
|
+
"sessions": [summary()],
|
|
181
|
+
"truncated": False,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def detail_envelope() -> dict[str, object]:
|
|
186
|
+
return {
|
|
187
|
+
"schemaVersion": 1,
|
|
188
|
+
"session": {
|
|
189
|
+
**summary(),
|
|
190
|
+
"messages": [
|
|
191
|
+
{
|
|
192
|
+
"sequence": 1,
|
|
193
|
+
"role": "user",
|
|
194
|
+
"content": "历史问题",
|
|
195
|
+
"createdAt": "2026-07-31T01:00:00.000Z",
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"sequence": 2,
|
|
199
|
+
"role": "assistant",
|
|
200
|
+
"content": "只读回答",
|
|
201
|
+
"createdAt": "2026-07-31T01:00:01.000Z",
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def summary() -> dict[str, object]:
|
|
209
|
+
return {
|
|
210
|
+
"id": SESSION_ID,
|
|
211
|
+
"title": "历史研判",
|
|
212
|
+
"scopeLabel": "当前范围",
|
|
213
|
+
"updatedAt": "2026-07-31T01:00:01.000Z",
|
|
214
|
+
"revision": "2",
|
|
215
|
+
}
|
|
@@ -12,6 +12,7 @@ from adk_web_agents.canonical_debug.agent import (
|
|
|
12
12
|
TOOL_NAME,
|
|
13
13
|
CanonicalDebugLlm,
|
|
14
14
|
build_root_agent,
|
|
15
|
+
ensure_debug_session_writable,
|
|
15
16
|
)
|
|
16
17
|
from southwind_agent_server.adk_debug import (
|
|
17
18
|
MAX_EVENT_FRAME_BYTES,
|
|
@@ -21,6 +22,9 @@ from southwind_agent_server.adk_debug import (
|
|
|
21
22
|
load_text_operation_debug_config,
|
|
22
23
|
sse_events,
|
|
23
24
|
)
|
|
25
|
+
from southwind_agent_server.adk_debug_sessions import (
|
|
26
|
+
ImportedDebugSessionReadOnlyError,
|
|
27
|
+
)
|
|
24
28
|
|
|
25
29
|
OPERATION = TextOperationDebugConfig("customer.copilot.global")
|
|
26
30
|
|
|
@@ -104,6 +108,7 @@ async def test_dispatcher_renders_result_and_bounded_event_summary() -> None:
|
|
|
104
108
|
model = CanonicalDebugLlm(OPERATION)
|
|
105
109
|
request = LlmRequest(
|
|
106
110
|
contents=[
|
|
111
|
+
types.Content(role="user", parts=[types.Part(text="查测试数据")]),
|
|
107
112
|
types.Content(
|
|
108
113
|
role="user",
|
|
109
114
|
parts=[
|
|
@@ -118,7 +123,7 @@ async def test_dispatcher_renders_result_and_bounded_event_summary() -> None:
|
|
|
118
123
|
)
|
|
119
124
|
)
|
|
120
125
|
],
|
|
121
|
-
)
|
|
126
|
+
),
|
|
122
127
|
]
|
|
123
128
|
)
|
|
124
129
|
|
|
@@ -131,6 +136,41 @@ async def test_dispatcher_renders_result_and_bounded_event_summary() -> None:
|
|
|
131
136
|
)
|
|
132
137
|
|
|
133
138
|
|
|
139
|
+
async def test_dispatcher_ignores_previous_turn_tool_response() -> None:
|
|
140
|
+
model = CanonicalDebugLlm(OPERATION)
|
|
141
|
+
request = LlmRequest(
|
|
142
|
+
contents=[
|
|
143
|
+
types.Content(role="user", parts=[types.Part(text="第一轮问题")]),
|
|
144
|
+
types.Content(
|
|
145
|
+
role="user",
|
|
146
|
+
parts=[
|
|
147
|
+
types.Part(
|
|
148
|
+
function_response=types.FunctionResponse(
|
|
149
|
+
name=TOOL_NAME,
|
|
150
|
+
response={
|
|
151
|
+
"runId": "run_previous",
|
|
152
|
+
"answer": "上一轮回答",
|
|
153
|
+
"events": [],
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
],
|
|
158
|
+
),
|
|
159
|
+
types.Content(role="model", parts=[types.Part(text="上一轮回答")]),
|
|
160
|
+
types.Content(role="user", parts=[types.Part(text="第二轮问题")]),
|
|
161
|
+
]
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
responses = [response async for response in model.generate_content_async(request)]
|
|
165
|
+
|
|
166
|
+
response = responses[0]
|
|
167
|
+
assert response.content and response.content.parts
|
|
168
|
+
call = response.content.parts[0].function_call
|
|
169
|
+
assert call
|
|
170
|
+
assert call.name == TOOL_NAME
|
|
171
|
+
assert call.args == {"question": "第二轮问题"}
|
|
172
|
+
|
|
173
|
+
|
|
134
174
|
async def test_unconfigured_project_fails_closed_without_tool() -> None:
|
|
135
175
|
model = CanonicalDebugLlm(None)
|
|
136
176
|
request = LlmRequest(
|
|
@@ -145,6 +185,13 @@ async def test_unconfigured_project_fails_closed_without_tool() -> None:
|
|
|
145
185
|
assert build_root_agent(None).tools == []
|
|
146
186
|
|
|
147
187
|
|
|
188
|
+
def test_imported_session_follow_up_fails_closed() -> None:
|
|
189
|
+
with pytest.raises(ImportedDebugSessionReadOnlyError, match="只读投影"):
|
|
190
|
+
ensure_debug_session_writable(True)
|
|
191
|
+
|
|
192
|
+
ensure_debug_session_writable(False)
|
|
193
|
+
|
|
194
|
+
|
|
148
195
|
def test_project_config_selects_only_operation_key(tmp_path: Path) -> None:
|
|
149
196
|
path = tmp_path / "adk-web-debug.json"
|
|
150
197
|
path.write_text(
|
|
@@ -49,12 +49,64 @@ async def test_provider_stream_and_health_use_bounded_safe_protocol() -> None:
|
|
|
49
49
|
(),
|
|
50
50
|
)
|
|
51
51
|
assert turn.text_deltas == ["你", "好"]
|
|
52
|
-
assert turn.usage ==
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
assert turn.usage.source == "provider-reported"
|
|
53
|
+
assert turn.usage.input_tokens == 2
|
|
54
|
+
assert turn.usage.output_tokens == 2
|
|
55
|
+
assert turn.usage.total_tokens == 4
|
|
56
|
+
assert turn.usage.cached_input_tokens is None
|
|
57
|
+
assert turn.usage.cache_status == "unknown"
|
|
58
|
+
assert turn.usage.first_token_latency_ms is not None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
async def test_provider_reports_cache_reasoning_model_without_fabricating() -> None:
|
|
62
|
+
async def handler(_request: httpx.Request) -> httpx.Response:
|
|
63
|
+
return sse_response(
|
|
64
|
+
[
|
|
65
|
+
{
|
|
66
|
+
"model": "provider-model-v2",
|
|
67
|
+
"choices": [{"delta": {"content": "完成"}}],
|
|
68
|
+
"usage": {
|
|
69
|
+
"prompt_tokens": 10,
|
|
70
|
+
"completion_tokens": 4,
|
|
71
|
+
"total_tokens": 14,
|
|
72
|
+
"prompt_tokens_details": {"cached_tokens": 6},
|
|
73
|
+
"completion_tokens_details": {"reasoning_tokens": 3},
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
turn = await create_provider(httpx.MockTransport(handler)).complete_turn(
|
|
80
|
+
[{"role": "user", "content": "hi"}], ()
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
assert turn.reported_model == "provider-model-v2"
|
|
84
|
+
assert turn.usage.cached_input_tokens == 6
|
|
85
|
+
assert turn.usage.reasoning_tokens == 3
|
|
86
|
+
assert turn.usage.cache_status == "hit"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def test_provider_rejects_impossible_cached_token_fact() -> None:
|
|
90
|
+
async def handler(_request: httpx.Request) -> httpx.Response:
|
|
91
|
+
return sse_response(
|
|
92
|
+
[
|
|
93
|
+
{
|
|
94
|
+
"choices": [{"delta": {"content": "完成"}}],
|
|
95
|
+
"usage": {
|
|
96
|
+
"prompt_tokens": 2,
|
|
97
|
+
"prompt_tokens_details": {"cached_tokens": 3},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
turn = await create_provider(httpx.MockTransport(handler)).complete_turn(
|
|
104
|
+
[{"role": "user", "content": "hi"}], ()
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
assert turn.usage.input_tokens == 2
|
|
108
|
+
assert turn.usage.cached_input_tokens is None
|
|
109
|
+
assert turn.usage.cache_status == "unknown"
|
|
58
110
|
|
|
59
111
|
|
|
60
112
|
async def test_provider_error_never_leaks_body_or_secret() -> None:
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from southwind_agent_server.postgres_report_status import (
|
|
6
|
+
start_or_resume_run,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def test_start_or_resume_abandons_only_unfinished_running_attempts() -> None:
|
|
11
|
+
connection = FakeConnection()
|
|
12
|
+
|
|
13
|
+
abandoned = await start_or_resume_run(
|
|
14
|
+
cast(Any, connection),
|
|
15
|
+
"run_1",
|
|
16
|
+
200.0,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
assert abandoned == 2
|
|
20
|
+
assert len(connection.queries) == 2
|
|
21
|
+
query, parameters = connection.queries[1]
|
|
22
|
+
assert "set outcome = 'abandoned'" in query
|
|
23
|
+
assert "outcome = 'running'" in query
|
|
24
|
+
assert "finished_at is null" in query
|
|
25
|
+
assert parameters == (200.0, 200.0, "run_1")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FakeConnection:
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self.queries: list[tuple[str, tuple[object, ...]]] = []
|
|
31
|
+
|
|
32
|
+
def transaction(self) -> FakeTransaction:
|
|
33
|
+
return FakeTransaction()
|
|
34
|
+
|
|
35
|
+
async def execute(
|
|
36
|
+
self,
|
|
37
|
+
query: str,
|
|
38
|
+
parameters: tuple[object, ...],
|
|
39
|
+
) -> FakeCursor:
|
|
40
|
+
self.queries.append((query, parameters))
|
|
41
|
+
return FakeCursor(2 if "set outcome = 'abandoned'" in query else 1)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FakeTransaction:
|
|
45
|
+
async def __aenter__(self) -> None:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
async def __aexit__(
|
|
49
|
+
self,
|
|
50
|
+
exception_type: object,
|
|
51
|
+
exception: object,
|
|
52
|
+
traceback: object,
|
|
53
|
+
) -> None:
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FakeCursor:
|
|
58
|
+
def __init__(self, rowcount: int) -> None:
|
|
59
|
+
self.rowcount = rowcount
|