create-windy 0.4.0 → 0.5.0

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 (194) hide show
  1. package/dist/cli.js +263 -121
  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/settings/drizzle-watermark-repository.integration.test.ts +4 -0
  96. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
  97. package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
  98. package/template/apps/server/src/settings/watermark-routes.ts +29 -1
  99. package/template/apps/server/src/system/operations.test.ts +54 -0
  100. package/template/apps/server/src/system/operations.ts +23 -1
  101. package/template/apps/server/src/system/seed.test.ts +4 -0
  102. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  103. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  104. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
  105. package/template/apps/web/src/composables/watermark-settings.ts +40 -11
  106. package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
  107. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  108. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  109. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
  110. package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
  111. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
  112. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  113. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  114. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  115. package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
  116. package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
  117. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
  118. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
  119. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  120. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  121. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  122. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  123. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  124. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  125. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  126. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  127. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  128. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  129. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  130. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  131. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  132. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  133. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  134. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  135. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  136. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  137. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  138. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  139. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  140. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  141. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  142. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  143. package/template/apps/web/src/router/error-navigation.ts +22 -0
  144. package/template/apps/web/src/router/index.ts +12 -23
  145. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  146. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  147. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  148. package/template/apps/web/src/services/operations-api.ts +28 -0
  149. package/template/docker-compose.agent-debug.yml +1 -1
  150. package/template/docs/architecture/ai-runtime.md +2 -1
  151. package/template/docs/architecture/module-host.md +3 -0
  152. package/template/docs/platform/agent-run-attestation.md +65 -0
  153. package/template/docs/platform/agent-runtime-observability.md +135 -0
  154. package/template/docs/platform/agent-runtime.md +47 -3
  155. package/template/docs/platform/operations-overview.md +13 -0
  156. package/template/docs/platform/platform-authentication.md +4 -3
  157. package/template/docs/platform/platform-shell-settings.md +15 -10
  158. package/template/docs/platform/web-system-crud.md +1 -1
  159. package/template/packages/config/package.json +1 -1
  160. package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
  161. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  162. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  163. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  164. package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
  165. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  166. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  167. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  168. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  169. package/template/packages/database/package.json +1 -1
  170. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  171. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  172. package/template/packages/database/src/schema/platform-settings.ts +3 -0
  173. package/template/packages/jobs/package.json +1 -1
  174. package/template/packages/modules/package.json +1 -1
  175. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  176. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  177. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  178. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  179. package/template/packages/modules/src/system-features.ts +8 -0
  180. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  181. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  182. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  183. package/template/packages/modules/src/system-modules.ts +13 -0
  184. package/template/packages/modules/src/system-permissions.ts +2 -0
  185. package/template/packages/server-sdk/index.ts +11 -0
  186. package/template/packages/server-sdk/package.json +2 -1
  187. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  188. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  189. package/template/packages/server-sdk/src/module-host.ts +5 -0
  190. package/template/packages/shared/index.ts +3 -0
  191. package/template/packages/shared/package.json +1 -1
  192. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  193. package/template/packages/shared/src/platform-settings.ts +17 -0
  194. package/template/scripts/lib/postgres-regression.ts +201 -0
@@ -0,0 +1,298 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, cast
4
+
5
+ from southwind_agent_server.postgres_report_summary import (
6
+ present_summary,
7
+ summary,
8
+ )
9
+ from southwind_agent_server.report_types import ReportFilter
10
+
11
+
12
+ async def test_summary_uses_bounded_sql_aggregates_and_projects_result() -> None:
13
+ connection = FakeConnection()
14
+ result = await summary(
15
+ cast(Any, FakePool(connection)),
16
+ ReportFilter(
17
+ 0,
18
+ 500,
19
+ selected_actor_id="actor_visible",
20
+ scope_restricted=True,
21
+ actor_ids=("actor_visible",),
22
+ department_ids=("department_visible",),
23
+ ),
24
+ retention_days=90,
25
+ generated_at=400,
26
+ )
27
+
28
+ assert len(connection.queries) == 4
29
+ rendered_sql = "\n".join(query for query, _ in connection.queries)
30
+ assert "select *" not in rendered_sql.lower()
31
+ assert "run_id = any" not in rendered_sql.lower()
32
+ assert "group by a.deployment_key" in rendered_sql
33
+ assert "group by 1" in rendered_sql
34
+ assert "at time zone 'UTC'" in rendered_sql
35
+ unbounded_attempt_rollup = (
36
+ "from agent_runtime_report_attempts\n group by run_id"
37
+ )
38
+ assert unbounded_attempt_rollup not in rendered_sql
39
+ assert "with run_activity as" in rendered_sql
40
+ assert "attempt_activity as" in rendered_sql
41
+ for index, (query, parameters) in enumerate(connection.queries):
42
+ assert "r.actor_id = %s" in query
43
+ assert "r.department_id = any(%s)" in query
44
+ expected = (
45
+ 0,
46
+ 500,
47
+ 400,
48
+ "actor_visible",
49
+ ["actor_visible"],
50
+ ["department_visible"],
51
+ )
52
+ assert parameters == (expected * 2 if index == 3 else expected)
53
+ if index > 0:
54
+ assert "a.started_at >= %s" in query
55
+ assert "a.started_at <= %s" in query
56
+ assert "a.finished_at is not null" in query
57
+
58
+ assert result == {
59
+ "generatedAt": "1970-01-01T00:06:40Z",
60
+ "reportingAvailableFrom": "1970-01-01T00:01:40Z",
61
+ "retentionDays": 90,
62
+ "runs": {
63
+ "total": 3,
64
+ "completed": 1,
65
+ "failed": 1,
66
+ "cancelled": 1,
67
+ "inProgress": 0,
68
+ "successRate": 0.5,
69
+ },
70
+ "cache": {
71
+ "hit": 1,
72
+ "miss": 1,
73
+ "unknown": 1,
74
+ "hitRate": 0.5,
75
+ "tokenRate": 0.5,
76
+ "coverageRate": 2 / 3,
77
+ },
78
+ "tokens": {
79
+ "input": 30,
80
+ "output": 10,
81
+ "cachedInput": 5,
82
+ "reasoning": 2,
83
+ "total": 40,
84
+ "coverageRate": 2 / 3,
85
+ },
86
+ "byModel": [
87
+ {
88
+ "deploymentKey": "ai.openai-compatible",
89
+ "model": "qwen",
90
+ "attempts": 3,
91
+ "inputTokens": 30,
92
+ "outputTokens": 10,
93
+ "cachedInputTokens": 5,
94
+ "reasoningTokens": 2,
95
+ "totalTokens": 40,
96
+ "unknownUsageCount": 1,
97
+ }
98
+ ],
99
+ "activity": [
100
+ {
101
+ "date": "2026-07-30",
102
+ "runs": 3,
103
+ "completed": 1,
104
+ "failed": 1,
105
+ "totalTokens": None,
106
+ },
107
+ {
108
+ "date": "2026-07-31",
109
+ "runs": 0,
110
+ "completed": 0,
111
+ "failed": 0,
112
+ "totalTokens": None,
113
+ },
114
+ ],
115
+ }
116
+
117
+
118
+ def test_summary_keeps_completely_unknown_token_facts_null() -> None:
119
+ result = present_summary(
120
+ {
121
+ "total": 1,
122
+ "completed": 1,
123
+ "failed": 0,
124
+ "cancelled": 0,
125
+ "available_from": 100,
126
+ },
127
+ {
128
+ "attempts": 1,
129
+ "available_from": 100,
130
+ "cache_hit": 0,
131
+ "cache_miss": 0,
132
+ "cache_unknown": 1,
133
+ "known_total_tokens": 0,
134
+ "input_tokens": None,
135
+ "output_tokens": None,
136
+ "cached_input_tokens": None,
137
+ "reasoning_tokens": None,
138
+ "total_tokens": None,
139
+ "paired_input_tokens": 0,
140
+ "paired_cached_tokens": 0,
141
+ },
142
+ [
143
+ {
144
+ "deployment_key": "ai.openai-compatible",
145
+ "model": "qwen",
146
+ "attempts": 1,
147
+ "input_tokens": None,
148
+ "output_tokens": None,
149
+ "cached_input_tokens": None,
150
+ "reasoning_tokens": None,
151
+ "total_tokens": None,
152
+ "unknown_usage_count": 1,
153
+ }
154
+ ],
155
+ [
156
+ {
157
+ "date": "2026-07-31",
158
+ "runs": 1,
159
+ "completed": 1,
160
+ "failed": 0,
161
+ "attempts": 1,
162
+ "known_token_attempts": 0,
163
+ "total_tokens": None,
164
+ }
165
+ ],
166
+ 90,
167
+ 400,
168
+ )
169
+
170
+ assert result["tokens"] == {
171
+ "input": None,
172
+ "output": None,
173
+ "cachedInput": None,
174
+ "reasoning": None,
175
+ "total": None,
176
+ "coverageRate": 0,
177
+ }
178
+ assert result["byModel"][0]["totalTokens"] is None # type: ignore[index]
179
+ assert result["activity"][0]["totalTokens"] is None # type: ignore[index]
180
+
181
+
182
+ class FakePool:
183
+ def __init__(self, connection: FakeConnection) -> None:
184
+ self.value = connection
185
+
186
+ def connection(self) -> FakeLease:
187
+ return FakeLease(self.value)
188
+
189
+
190
+ class FakeLease:
191
+ def __init__(self, connection: FakeConnection) -> None:
192
+ self.value = connection
193
+
194
+ async def __aenter__(self) -> FakeConnection:
195
+ return self.value
196
+
197
+ async def __aexit__(
198
+ self,
199
+ exception_type: object,
200
+ exception: object,
201
+ traceback: object,
202
+ ) -> None:
203
+ return None
204
+
205
+
206
+ class FakeConnection:
207
+ def __init__(self) -> None:
208
+ self.queries: list[tuple[str, tuple[object, ...]]] = []
209
+
210
+ async def execute(
211
+ self,
212
+ query: str,
213
+ parameters: tuple[object, ...],
214
+ ) -> FakeCursor:
215
+ self.queries.append((query, parameters))
216
+ if "min(r.created_at) as available_from" in query:
217
+ return FakeCursor(
218
+ [
219
+ {
220
+ "total": 3,
221
+ "completed": 1,
222
+ "failed": 1,
223
+ "cancelled": 1,
224
+ "available_from": 100,
225
+ }
226
+ ]
227
+ )
228
+ if "as paired_input_tokens" in query:
229
+ return FakeCursor(
230
+ [
231
+ {
232
+ "attempts": 3,
233
+ "available_from": 200,
234
+ "cache_hit": 1,
235
+ "cache_miss": 1,
236
+ "cache_unknown": 1,
237
+ "known_total_tokens": 2,
238
+ "input_tokens": 30,
239
+ "output_tokens": 10,
240
+ "cached_input_tokens": 5,
241
+ "reasoning_tokens": 2,
242
+ "total_tokens": 40,
243
+ "paired_input_tokens": 10,
244
+ "paired_cached_tokens": 5,
245
+ }
246
+ ]
247
+ )
248
+ if "group by a.deployment_key" in query:
249
+ return FakeCursor(
250
+ [
251
+ {
252
+ "deployment_key": "ai.openai-compatible",
253
+ "model": "qwen",
254
+ "attempts": 3,
255
+ "input_tokens": 30,
256
+ "output_tokens": 10,
257
+ "cached_input_tokens": 5,
258
+ "reasoning_tokens": 2,
259
+ "total_tokens": 40,
260
+ "unknown_usage_count": 1,
261
+ }
262
+ ]
263
+ )
264
+ if "with run_activity as" in query:
265
+ return FakeCursor(
266
+ [
267
+ {
268
+ "date": "2026-07-30",
269
+ "runs": 3,
270
+ "completed": 1,
271
+ "failed": 1,
272
+ "attempts": 0,
273
+ "known_token_attempts": 0,
274
+ "total_tokens": None,
275
+ },
276
+ {
277
+ "date": "2026-07-31",
278
+ "runs": 0,
279
+ "completed": 0,
280
+ "failed": 0,
281
+ "attempts": 3,
282
+ "known_token_attempts": 2,
283
+ "total_tokens": 40,
284
+ },
285
+ ]
286
+ )
287
+ raise AssertionError("unexpected SQL")
288
+
289
+
290
+ class FakeCursor:
291
+ def __init__(self, rows: list[dict[str, Any]]) -> None:
292
+ self.rows = rows
293
+
294
+ async def fetchone(self) -> dict[str, Any] | None:
295
+ return self.rows[0] if self.rows else None
296
+
297
+ async def fetchall(self) -> list[dict[str, Any]]:
298
+ return self.rows
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+ import pytest
5
+
6
+ from southwind_agent_server.adk_adapter import AdkEngineAdapter
7
+ from southwind_agent_server.contracts import StartRunRequest
8
+ from southwind_agent_server.engine import AgentEngineAdapter, SafeEngineError
9
+ from southwind_agent_server.openai_engine import (
10
+ OpenAiCompatibleEngineAdapter,
11
+ )
12
+ from southwind_agent_server.report_store import (
13
+ InMemoryAgentRuntimeReportStore,
14
+ )
15
+ from southwind_agent_server.tool_host import ToolHostClient
16
+ from tests.openai_provider_test_support import (
17
+ create_provider,
18
+ governance,
19
+ runtime_request,
20
+ sse_response,
21
+ )
22
+
23
+
24
+ @pytest.mark.parametrize("adapter_kind", ["direct", "adk"])
25
+ async def test_blocked_response_preserves_provider_usage(
26
+ adapter_kind: str,
27
+ ) -> None:
28
+ async def provider_handler(_request: httpx.Request) -> httpx.Response:
29
+ return sse_response(
30
+ [
31
+ {
32
+ "model": "provider-model-v2",
33
+ "choices": [
34
+ {
35
+ "delta": {
36
+ "tool_calls": [
37
+ {
38
+ "index": 0,
39
+ "id": "call_bad",
40
+ "function": {
41
+ "name": "business.admin.delete",
42
+ "arguments": "{}",
43
+ },
44
+ }
45
+ ]
46
+ }
47
+ }
48
+ ],
49
+ "usage": {
50
+ "prompt_tokens": 10,
51
+ "completion_tokens": 2,
52
+ "total_tokens": 12,
53
+ "prompt_tokens_details": {"cached_tokens": 5},
54
+ },
55
+ }
56
+ ]
57
+ )
58
+
59
+ report = InMemoryAgentRuntimeReportStore(now=lambda: 200.0)
60
+ await report.ensure_run(
61
+ "run_1",
62
+ StartRunRequest(operation_key="business.answer", input=None),
63
+ 100.0,
64
+ )
65
+ await report.start_run("run_1", 101.0)
66
+ provider = create_provider(httpx.MockTransport(provider_handler))
67
+ adapter: AgentEngineAdapter
68
+ if adapter_kind == "direct":
69
+ adapter = OpenAiCompatibleEngineAdapter(
70
+ provider,
71
+ ToolHostClient("internal-token"),
72
+ governance(),
73
+ report,
74
+ )
75
+ else:
76
+ adapter = AdkEngineAdapter(
77
+ provider,
78
+ ToolHostClient("internal-token"),
79
+ governance(),
80
+ report,
81
+ )
82
+
83
+ with pytest.raises(SafeEngineError) as captured:
84
+ async for _ in adapter.execute(
85
+ runtime_request("business.records.list"),
86
+ ):
87
+ pass
88
+
89
+ detail = await report.get_run("run_1")
90
+ assert detail is not None
91
+ attempt = detail["attempts"][0] # type: ignore[index]
92
+
93
+ assert captured.value.code == "TOOL_DENIED"
94
+ assert attempt["outcome"] == "blocked"
95
+ assert attempt["usageSource"] == "provider-reported"
96
+ assert attempt["inputTokens"] == 10
97
+ assert attempt["outputTokens"] == 2
98
+ assert attempt["cachedInputTokens"] == 5
99
+ assert attempt["totalTokens"] == 12
100
+ assert attempt["cacheStatus"] == "hit"
101
+ assert attempt["firstTokenLatencyMs"] is not None
102
+ assert attempt["model"] == "provider-model-v2"
103
+
104
+
105
+ async def test_adk_cancel_after_response_preserves_provider_usage() -> None:
106
+ adapter: AdkEngineAdapter
107
+
108
+ async def provider_handler(_request: httpx.Request) -> httpx.Response:
109
+ await adapter.cancel("run_1")
110
+ return sse_response(
111
+ [
112
+ {
113
+ "model": "provider-model-v2",
114
+ "choices": [{"delta": {"content": "late"}}],
115
+ "usage": {
116
+ "prompt_tokens": 3,
117
+ "completion_tokens": 1,
118
+ "total_tokens": 4,
119
+ },
120
+ }
121
+ ]
122
+ )
123
+
124
+ report = InMemoryAgentRuntimeReportStore(now=lambda: 200.0)
125
+ await report.ensure_run(
126
+ "run_1",
127
+ StartRunRequest(operation_key="business.answer", input=None),
128
+ 100.0,
129
+ )
130
+ await report.start_run("run_1", 101.0)
131
+ adapter = AdkEngineAdapter(
132
+ create_provider(httpx.MockTransport(provider_handler)),
133
+ ToolHostClient("internal-token"),
134
+ governance(),
135
+ report,
136
+ )
137
+
138
+ with pytest.raises(SafeEngineError) as captured:
139
+ async for _ in adapter.execute(
140
+ runtime_request("business.records.list"),
141
+ ):
142
+ pass
143
+
144
+ detail = await report.get_run("run_1")
145
+ assert detail is not None
146
+ attempt = detail["attempts"][0] # type: ignore[index]
147
+ assert captured.value.code == "RUN_CANCELLED"
148
+ assert attempt["outcome"] == "cancelled"
149
+ assert attempt["usageSource"] == "provider-reported"
150
+ assert attempt["totalTokens"] == 4
151
+ assert attempt["firstTokenLatencyMs"] is not None
152
+ assert attempt["model"] == "provider-model-v2"
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ from southwind_agent_server.contracts import (
4
+ RunReportingContext,
5
+ StartRunRequest,
6
+ )
7
+ from southwind_agent_server.report_store import (
8
+ InMemoryAgentRuntimeReportStore,
9
+ )
10
+ from southwind_agent_server.report_types import ReportFilter, UsageFacts
11
+
12
+ DAY = 24 * 60 * 60
13
+
14
+
15
+ def request() -> StartRunRequest:
16
+ return StartRunRequest(
17
+ operation_key="business.answer",
18
+ input=None,
19
+ reporting=RunReportingContext(
20
+ actor_id="actor_1",
21
+ actor_type="user",
22
+ ),
23
+ )
24
+
25
+
26
+ async def test_running_attempt_is_visible_but_excluded_from_summary() -> None:
27
+ store = InMemoryAgentRuntimeReportStore(now=lambda: 4 * DAY)
28
+ await store.ensure_run("run_1", request(), 0)
29
+ await store.start_run("run_1", 1)
30
+ attempt = await store.start_attempt(
31
+ "run_1",
32
+ 1,
33
+ "ai.openai-compatible",
34
+ "requested-model",
35
+ 2 * DAY,
36
+ )
37
+
38
+ detail = await store.get_run("run_1")
39
+ summary = await store.summary(ReportFilter(0, 3 * DAY))
40
+
41
+ assert detail is not None
42
+ assert detail["attempts"][0]["outcome"] == "running" # type: ignore[index]
43
+ assert detail["attempts"][0]["finishedAt"] is None # type: ignore[index]
44
+ assert summary["cache"] == {
45
+ "hit": 0,
46
+ "miss": 0,
47
+ "unknown": 0,
48
+ "hitRate": None,
49
+ "tokenRate": None,
50
+ "coverageRate": 0,
51
+ }
52
+ assert summary["tokens"]["coverageRate"] == 0 # type: ignore[index]
53
+ assert summary["byModel"] == []
54
+
55
+ await store.start_run("run_1", 2 * DAY + 10)
56
+ recovered = await store.get_run("run_1")
57
+
58
+ assert recovered is not None
59
+ recovered_attempt = recovered["attempts"][0] # type: ignore[index]
60
+ assert recovered_attempt["outcome"] == "abandoned"
61
+ assert recovered_attempt["finishedAt"] is not None
62
+ assert attempt.outcome == "running"
63
+
64
+
65
+ async def test_attempt_metrics_use_attempt_day_not_run_day() -> None:
66
+ store = InMemoryAgentRuntimeReportStore(now=lambda: 4 * DAY)
67
+ await store.ensure_run("run_1", request(), 0)
68
+ await store.start_run("run_1", 1)
69
+ attempt = await store.start_attempt(
70
+ "run_1",
71
+ 1,
72
+ "ai.openai-compatible",
73
+ "requested-model",
74
+ 2 * DAY,
75
+ )
76
+ await store.finish_attempt(
77
+ attempt.attempt_id,
78
+ "completed",
79
+ UsageFacts(
80
+ source="provider-reported",
81
+ input_tokens=10,
82
+ output_tokens=2,
83
+ total_tokens=12,
84
+ cache_status="miss",
85
+ ),
86
+ 2 * DAY + 1,
87
+ "reported-model",
88
+ )
89
+ await store.finish_run("run_1", "completed", 2 * DAY + 2)
90
+
91
+ attempt_window = await store.summary(
92
+ ReportFilter(DAY, 3 * DAY),
93
+ )
94
+ whole_window = await store.summary(
95
+ ReportFilter(-1, 3 * DAY),
96
+ )
97
+
98
+ assert attempt_window["runs"]["total"] == 0 # type: ignore[index]
99
+ assert attempt_window["reportingAvailableFrom"] == "1970-01-03T00:00:00Z"
100
+ assert attempt_window["tokens"]["total"] == 12 # type: ignore[index]
101
+ assert attempt_window["byModel"][0]["attempts"] == 1 # type: ignore[index]
102
+ assert attempt_window["activity"] == [
103
+ {
104
+ "date": "1970-01-03",
105
+ "runs": 0,
106
+ "completed": 0,
107
+ "failed": 0,
108
+ "totalTokens": 12,
109
+ }
110
+ ]
111
+ assert whole_window["activity"] == [
112
+ {
113
+ "date": "1970-01-01",
114
+ "runs": 1,
115
+ "completed": 1,
116
+ "failed": 0,
117
+ "totalTokens": None,
118
+ },
119
+ {
120
+ "date": "1970-01-03",
121
+ "runs": 0,
122
+ "completed": 0,
123
+ "failed": 0,
124
+ "totalTokens": 12,
125
+ },
126
+ ]