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.
Files changed (174) hide show
  1. package/dist/cli.js +292 -125
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
  5. package/template/apps/agent-server/adk_web_agents/services.py +25 -0
  6. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
  7. package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
  8. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
  9. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
  10. package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
  11. package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
  15. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
  18. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
  25. package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
  26. package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
  29. package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
  31. package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
  32. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
  33. package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
  34. package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
  35. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
  36. package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
  37. package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
  38. package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
  39. package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
  40. package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
  41. package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
  42. package/template/apps/agent-server/tests/test_report_store.py +286 -0
  43. package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
  44. package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
  45. package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
  46. package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
  47. package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
  48. package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
  49. package/template/apps/server/src/agent/debug-session-id.ts +37 -0
  50. package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
  51. package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
  52. package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
  53. package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
  54. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
  55. package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
  56. package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
  57. package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
  58. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  59. package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
  60. package/template/apps/server/src/agent/run-authorization.ts +32 -1
  61. package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
  62. package/template/apps/server/src/agent/run-contracts.ts +5 -0
  63. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
  64. package/template/apps/server/src/agent/run-facade.test.ts +78 -0
  65. package/template/apps/server/src/agent/run-facade.ts +33 -3
  66. package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
  67. package/template/apps/server/src/agent/run-input-verification.ts +41 -0
  68. package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
  69. package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
  70. package/template/apps/server/src/agent/tool-host.test.ts +1 -0
  71. package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
  72. package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
  73. package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
  74. package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
  75. package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
  76. package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
  77. package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
  78. package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
  79. package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
  80. package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
  81. package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
  82. package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
  83. package/template/apps/server/src/license/runtime-service.test.ts +6 -0
  84. package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
  85. package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
  86. package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
  87. package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
  88. package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
  89. package/template/apps/server/src/module-host/initialize.ts +6 -197
  90. package/template/apps/server/src/module-host/request-context.ts +1 -0
  91. package/template/apps/server/src/module-host/scoped-host.ts +214 -0
  92. package/template/apps/server/src/module-host.ts +1 -0
  93. package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
  94. package/template/apps/server/src/runtime-adk-debug.ts +8 -2
  95. package/template/apps/server/src/system/operations.test.ts +54 -0
  96. package/template/apps/server/src/system/operations.ts +23 -1
  97. package/template/apps/server/src/system/seed.test.ts +4 -0
  98. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  99. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  100. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  101. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  102. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  103. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  104. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  105. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  106. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  107. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  108. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  109. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  110. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  111. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  112. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  113. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  114. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  115. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  116. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  117. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  118. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  119. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  120. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  121. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  122. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  123. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  124. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  125. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  126. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  127. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  128. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  129. package/template/apps/web/src/router/error-navigation.ts +22 -0
  130. package/template/apps/web/src/router/index.ts +12 -23
  131. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  132. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  133. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  134. package/template/apps/web/src/services/operations-api.ts +28 -0
  135. package/template/docker-compose.agent-debug.yml +1 -1
  136. package/template/docs/architecture/ai-runtime.md +2 -1
  137. package/template/docs/architecture/module-host.md +3 -0
  138. package/template/docs/platform/agent-run-attestation.md +65 -0
  139. package/template/docs/platform/agent-runtime-observability.md +135 -0
  140. package/template/docs/platform/agent-runtime.md +47 -3
  141. package/template/docs/platform/operations-overview.md +13 -0
  142. package/template/docs/platform/platform-authentication.md +4 -3
  143. package/template/packages/config/package.json +1 -1
  144. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  145. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  146. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  147. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  148. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  149. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  150. package/template/packages/database/drizzle/meta/_journal.json +21 -0
  151. package/template/packages/database/package.json +1 -1
  152. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  153. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  154. package/template/packages/jobs/package.json +1 -1
  155. package/template/packages/modules/package.json +1 -1
  156. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  157. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  158. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  159. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  160. package/template/packages/modules/src/system-features.ts +8 -0
  161. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  162. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  163. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  164. package/template/packages/modules/src/system-modules.ts +13 -0
  165. package/template/packages/modules/src/system-permissions.ts +2 -0
  166. package/template/packages/server-sdk/index.ts +11 -0
  167. package/template/packages/server-sdk/package.json +2 -1
  168. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  169. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  170. package/template/packages/server-sdk/src/module-host.ts +5 -0
  171. package/template/packages/shared/index.ts +3 -0
  172. package/template/packages/shared/package.json +1 -1
  173. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  174. 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 AgentError, StartRunRequest
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 .signal_events import canonical_signal_event
18
- from .store import IdempotencyConflictError, RunRecord, RunStore
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
- StartRunRequest.model_validate(raw),
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._resolve_pending_approval(record, "cancelled")
145
- await self.store.append(run_id, "run.cancelled")
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.store.append(
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 self.store.append(record.run_id, event_type, fields)
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._resolve_pending_approval(current, "timed-out")
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
- error = AgentError(
333
- code=failure.code,
334
- message=failure.safe_message,
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
- {"error": error.model_dump(by_alias=True)},
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", {"usage": signal.usage}
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.usage for signal in signals if isinstance(signal, UsageReported)]
66
- assert usage == [
67
- {
68
- "source": "provider-reported",
69
- "inputTokens": 2,
70
- "outputTokens": 2,
71
- "totalTokens": 4,
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
- "source": "provider-reported",
54
- "inputTokens": 2,
55
- "outputTokens": 2,
56
- "totalTokens": 4,
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