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,304 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from psycopg import AsyncConnection
6
+ from psycopg_pool import AsyncConnectionPool
7
+
8
+ from .postgres_report_queries import (
9
+ attempt_filter_clause,
10
+ filter_clause,
11
+ )
12
+ from .report_types import ReportFilter, iso
13
+
14
+ type DatabaseConnection = AsyncConnection[dict[str, Any]]
15
+ type DatabasePool = AsyncConnectionPool[DatabaseConnection]
16
+
17
+
18
+ async def summary(
19
+ pool: DatabasePool,
20
+ query: ReportFilter,
21
+ retention_days: int,
22
+ generated_at: float,
23
+ ) -> dict[str, object]:
24
+ run_clause, run_parameters = filter_clause(query, generated_at, "r")
25
+ attempt_clause, attempt_parameters = attempt_filter_clause(
26
+ query,
27
+ generated_at,
28
+ )
29
+ async with pool.connection() as connection:
30
+ run_row = await fetch_one(
31
+ connection,
32
+ f"""
33
+ select count(*) as total,
34
+ count(*) filter (where r.status = 'completed') as completed,
35
+ count(*) filter (where r.status = 'failed') as failed,
36
+ count(*) filter (where r.status = 'cancelled') as cancelled,
37
+ min(r.created_at) as available_from
38
+ from agent_runtime_report_runs r {run_clause}
39
+ """,
40
+ run_parameters,
41
+ )
42
+ attempt_row = await fetch_one(
43
+ connection,
44
+ f"""
45
+ select count(*) as attempts,
46
+ min(a.started_at) as available_from,
47
+ count(*) filter (where a.cache_status = 'hit') as cache_hit,
48
+ count(*) filter (where a.cache_status = 'miss') as cache_miss,
49
+ count(*) filter (where a.cache_status = 'unknown') as cache_unknown,
50
+ count(*) filter (
51
+ where a.total_tokens is not null
52
+ ) as known_total_tokens,
53
+ sum(a.input_tokens) as input_tokens,
54
+ sum(a.output_tokens) as output_tokens,
55
+ sum(a.cached_input_tokens) as cached_input_tokens,
56
+ sum(a.reasoning_tokens) as reasoning_tokens,
57
+ sum(a.total_tokens) as total_tokens,
58
+ coalesce(sum(a.input_tokens) filter (
59
+ where a.input_tokens is not null
60
+ and a.cached_input_tokens is not null
61
+ ), 0) as paired_input_tokens,
62
+ coalesce(sum(a.cached_input_tokens) filter (
63
+ where a.input_tokens is not null
64
+ and a.cached_input_tokens is not null
65
+ ), 0) as paired_cached_tokens
66
+ from agent_runtime_report_attempts a
67
+ join agent_runtime_report_runs r on r.run_id = a.run_id
68
+ {attempt_clause}
69
+ """,
70
+ attempt_parameters,
71
+ )
72
+ model_rows = await fetch_all(
73
+ connection,
74
+ f"""
75
+ select a.deployment_key,
76
+ coalesce(a.reported_model, a.requested_model) as model,
77
+ count(*) as attempts,
78
+ sum(a.input_tokens) as input_tokens,
79
+ sum(a.output_tokens) as output_tokens,
80
+ sum(a.cached_input_tokens) as cached_input_tokens,
81
+ sum(a.reasoning_tokens) as reasoning_tokens,
82
+ sum(a.total_tokens) as total_tokens,
83
+ count(*) filter (
84
+ where a.usage_source = 'unknown'
85
+ ) as unknown_usage_count
86
+ from agent_runtime_report_attempts a
87
+ join agent_runtime_report_runs r on r.run_id = a.run_id
88
+ {attempt_clause}
89
+ group by a.deployment_key,
90
+ coalesce(a.reported_model, a.requested_model)
91
+ order by a.deployment_key,
92
+ coalesce(a.reported_model, a.requested_model)
93
+ """,
94
+ attempt_parameters,
95
+ )
96
+ activity_rows = await fetch_all(
97
+ connection,
98
+ f"""
99
+ with run_activity as (
100
+ select to_char(
101
+ to_timestamp(r.created_at) at time zone 'UTC',
102
+ 'YYYY-MM-DD'
103
+ ) as date,
104
+ count(*) as runs,
105
+ count(*) filter (
106
+ where r.status = 'completed'
107
+ ) as completed,
108
+ count(*) filter (where r.status = 'failed') as failed
109
+ from agent_runtime_report_runs r
110
+ {run_clause}
111
+ group by 1
112
+ ),
113
+ attempt_activity as (
114
+ select to_char(
115
+ to_timestamp(a.started_at) at time zone 'UTC',
116
+ 'YYYY-MM-DD'
117
+ ) as date,
118
+ count(*) as attempts,
119
+ count(a.total_tokens) as known_token_attempts,
120
+ sum(a.total_tokens) as total_tokens
121
+ from agent_runtime_report_attempts a
122
+ join agent_runtime_report_runs r on r.run_id = a.run_id
123
+ {attempt_clause}
124
+ group by 1
125
+ ),
126
+ activity_dates as (
127
+ select date from run_activity
128
+ union
129
+ select date from attempt_activity
130
+ )
131
+ select d.date,
132
+ coalesce(ra.runs, 0) as runs,
133
+ coalesce(ra.completed, 0) as completed,
134
+ coalesce(ra.failed, 0) as failed,
135
+ coalesce(aa.attempts, 0) as attempts,
136
+ aa.total_tokens
137
+ from activity_dates d
138
+ left join run_activity ra on ra.date = d.date
139
+ left join attempt_activity aa on aa.date = d.date
140
+ order by d.date
141
+ """,
142
+ (*run_parameters, *attempt_parameters),
143
+ )
144
+ return present_summary(
145
+ run_row,
146
+ attempt_row,
147
+ model_rows,
148
+ activity_rows,
149
+ retention_days,
150
+ generated_at,
151
+ )
152
+
153
+
154
+ def present_summary(
155
+ run: dict[str, Any],
156
+ attempt: dict[str, Any],
157
+ models: list[dict[str, Any]],
158
+ activity: list[dict[str, Any]],
159
+ retention_days: int,
160
+ generated_at: float,
161
+ ) -> dict[str, object]:
162
+ total = integer(run, "total")
163
+ completed = integer(run, "completed")
164
+ failed = integer(run, "failed")
165
+ cancelled = integer(run, "cancelled")
166
+ attempts = integer(attempt, "attempts")
167
+ cache_hit = integer(attempt, "cache_hit")
168
+ cache_miss = integer(attempt, "cache_miss")
169
+ paired_input = integer(attempt, "paired_input_tokens")
170
+ available_values = [
171
+ value
172
+ for value in (
173
+ optional_float(run, "available_from"),
174
+ optional_float(attempt, "available_from"),
175
+ )
176
+ if value is not None
177
+ ]
178
+ return {
179
+ "generatedAt": iso(generated_at),
180
+ "reportingAvailableFrom": iso(
181
+ min(available_values) if available_values else None
182
+ ),
183
+ "retentionDays": retention_days,
184
+ "runs": {
185
+ "total": total,
186
+ "completed": completed,
187
+ "failed": failed,
188
+ "cancelled": cancelled,
189
+ "inProgress": total - completed - failed - cancelled,
190
+ "successRate": (
191
+ completed / (completed + failed) if completed + failed else None
192
+ ),
193
+ },
194
+ "cache": {
195
+ "hit": cache_hit,
196
+ "miss": cache_miss,
197
+ "unknown": integer(attempt, "cache_unknown"),
198
+ "hitRate": (
199
+ cache_hit / (cache_hit + cache_miss) if cache_hit + cache_miss else None
200
+ ),
201
+ "tokenRate": (
202
+ integer(attempt, "paired_cached_tokens") / paired_input
203
+ if paired_input > 0
204
+ else None
205
+ ),
206
+ "coverageRate": ((cache_hit + cache_miss) / attempts if attempts else 0),
207
+ },
208
+ "tokens": {
209
+ "input": aggregate_token(attempt, "input_tokens", attempts),
210
+ "output": aggregate_token(attempt, "output_tokens", attempts),
211
+ "cachedInput": aggregate_token(
212
+ attempt,
213
+ "cached_input_tokens",
214
+ attempts,
215
+ ),
216
+ "reasoning": aggregate_token(
217
+ attempt,
218
+ "reasoning_tokens",
219
+ attempts,
220
+ ),
221
+ "total": aggregate_token(attempt, "total_tokens", attempts),
222
+ "coverageRate": (
223
+ integer(attempt, "known_total_tokens") / attempts if attempts else 0
224
+ ),
225
+ },
226
+ "byModel": [present_model(row) for row in models],
227
+ "activity": [present_activity(row) for row in activity],
228
+ }
229
+
230
+
231
+ def present_model(row: dict[str, Any]) -> dict[str, object]:
232
+ return {
233
+ "deploymentKey": text(row, "deployment_key"),
234
+ "model": text(row, "model"),
235
+ "attempts": integer(row, "attempts"),
236
+ "inputTokens": optional_integer(row, "input_tokens"),
237
+ "outputTokens": optional_integer(row, "output_tokens"),
238
+ "cachedInputTokens": optional_integer(row, "cached_input_tokens"),
239
+ "reasoningTokens": optional_integer(row, "reasoning_tokens"),
240
+ "totalTokens": optional_integer(row, "total_tokens"),
241
+ "unknownUsageCount": integer(row, "unknown_usage_count"),
242
+ }
243
+
244
+
245
+ def present_activity(row: dict[str, Any]) -> dict[str, object]:
246
+ attempts = integer(row, "attempts")
247
+ known_attempts = integer(row, "known_token_attempts")
248
+ return {
249
+ "date": text(row, "date"),
250
+ "runs": integer(row, "runs"),
251
+ "completed": integer(row, "completed"),
252
+ "failed": integer(row, "failed"),
253
+ "totalTokens": (
254
+ optional_integer(row, "total_tokens")
255
+ if attempts and known_attempts == attempts
256
+ else None
257
+ ),
258
+ }
259
+
260
+
261
+ async def fetch_one(
262
+ connection: DatabaseConnection,
263
+ query: str,
264
+ parameters: tuple[object, ...],
265
+ ) -> dict[str, Any]:
266
+ row = await (await connection.execute(query, parameters)).fetchone()
267
+ if row is None:
268
+ raise RuntimeError("AI Runtime 报表聚合查询未返回结果")
269
+ return row
270
+
271
+
272
+ async def fetch_all(
273
+ connection: DatabaseConnection,
274
+ query: str,
275
+ parameters: tuple[object, ...],
276
+ ) -> list[dict[str, Any]]:
277
+ return list(await (await connection.execute(query, parameters)).fetchall())
278
+
279
+
280
+ def integer(row: dict[str, Any], key: str) -> int:
281
+ return int(row[key])
282
+
283
+
284
+ def optional_integer(row: dict[str, Any], key: str) -> int | None:
285
+ value = row[key]
286
+ return int(value) if value is not None else None
287
+
288
+
289
+ def aggregate_token(
290
+ row: dict[str, Any],
291
+ key: str,
292
+ attempt_count: int,
293
+ ) -> int | None:
294
+ value = optional_integer(row, key)
295
+ return value if value is not None else (0 if attempt_count == 0 else None)
296
+
297
+
298
+ def text(row: dict[str, Any], key: str) -> str:
299
+ return str(row[key])
300
+
301
+
302
+ def optional_float(row: dict[str, Any], key: str) -> float | None:
303
+ value = row[key]
304
+ return float(value) if value is not None else None
@@ -0,0 +1,217 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from typing import Literal, cast
6
+
7
+ from .contracts import StartRunRequest
8
+ from .report_store import AgentRuntimeReportStore
9
+ from .report_types import (
10
+ AttemptReport,
11
+ AttemptTerminalOutcome,
12
+ NonTerminalRunStatus,
13
+ UsageFacts,
14
+ )
15
+ from .store import CanonicalEvent, RunRecord, RunStore
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ async def ensure_run_report_safely(
21
+ store: AgentRuntimeReportStore,
22
+ run_id: str,
23
+ request: StartRunRequest,
24
+ ) -> None:
25
+ try:
26
+ await store.ensure_run(run_id, request, time.time())
27
+ except Exception:
28
+ reporting_failure(store, "run_ensure", run_id)
29
+
30
+
31
+ async def start_run_report_safely(
32
+ store: AgentRuntimeReportStore,
33
+ run_id: str,
34
+ ) -> None:
35
+ try:
36
+ await store.start_run(run_id, time.time())
37
+ except Exception:
38
+ reporting_failure(store, "run_start", run_id)
39
+
40
+
41
+ async def finish_run_report_safely(
42
+ store: AgentRuntimeReportStore,
43
+ run_id: str,
44
+ status: Literal["completed", "failed", "cancelled"],
45
+ safe_error_code: str | None = None,
46
+ ) -> None:
47
+ """Keep a reporting outage from changing the canonical Run terminal state."""
48
+ try:
49
+ await store.finish_run(
50
+ run_id,
51
+ status,
52
+ time.time(),
53
+ safe_error_code,
54
+ )
55
+ except Exception:
56
+ reporting_failure(
57
+ store,
58
+ "run_terminal",
59
+ run_id,
60
+ {"report_status": status},
61
+ )
62
+
63
+
64
+ async def sync_terminal_from_canonical(
65
+ report_store: AgentRuntimeReportStore,
66
+ canonical_store: RunStore,
67
+ run_id: str,
68
+ fallback_safe_error_code: str | None = None,
69
+ ) -> RunRecord | None:
70
+ record = await canonical_store.get(run_id)
71
+ if record is None or not record.terminal:
72
+ return None
73
+ status = cast(Literal["completed", "failed", "cancelled"], record.status)
74
+ safe_error_code = None
75
+ if status == "failed":
76
+ safe_error_code = canonical_error_code(record) or fallback_safe_error_code
77
+ await finish_run_report_safely(
78
+ report_store,
79
+ run_id,
80
+ status,
81
+ safe_error_code,
82
+ )
83
+ return record
84
+
85
+
86
+ async def sync_canonical_event(
87
+ report_store: AgentRuntimeReportStore,
88
+ canonical_store: RunStore,
89
+ run_id: str,
90
+ event: CanonicalEvent | None,
91
+ fallback_safe_error_code: str | None = None,
92
+ ) -> RunRecord | None:
93
+ event_type = event.get("type") if event else None
94
+ if event is None or event_type in {
95
+ "run.completed",
96
+ "run.failed",
97
+ "run.cancelled",
98
+ }:
99
+ return await sync_terminal_from_canonical(
100
+ report_store,
101
+ canonical_store,
102
+ run_id,
103
+ fallback_safe_error_code,
104
+ )
105
+ if isinstance(event_type, str):
106
+ await sync_status(report_store, run_id, event_type)
107
+ return None
108
+
109
+
110
+ async def append_and_sync_canonical_event(
111
+ report_store: AgentRuntimeReportStore,
112
+ canonical_store: RunStore,
113
+ run_id: str,
114
+ event_type: str,
115
+ fields: dict[str, object] | None = None,
116
+ fallback_safe_error_code: str | None = None,
117
+ ) -> RunRecord | None:
118
+ event = await canonical_store.append(run_id, event_type, fields)
119
+ return await sync_canonical_event(
120
+ report_store,
121
+ canonical_store,
122
+ run_id,
123
+ event,
124
+ fallback_safe_error_code,
125
+ )
126
+
127
+
128
+ async def sync_status(
129
+ store: AgentRuntimeReportStore,
130
+ run_id: str,
131
+ event_type: str,
132
+ ) -> None:
133
+ status: NonTerminalRunStatus
134
+ if event_type == "approval.required":
135
+ status = "awaiting-approval"
136
+ elif event_type == "approval.resolved":
137
+ status = "running"
138
+ else:
139
+ return
140
+ try:
141
+ await store.set_run_status(run_id, status, time.time())
142
+ except Exception:
143
+ reporting_failure(
144
+ store,
145
+ "run_status",
146
+ run_id,
147
+ {"report_status": status},
148
+ )
149
+
150
+
151
+ def canonical_error_code(record: RunRecord) -> str | None:
152
+ for event in reversed(record.events):
153
+ if event.get("type") != "run.failed":
154
+ continue
155
+ error = event.get("error")
156
+ if isinstance(error, dict):
157
+ code = error.get("code")
158
+ if isinstance(code, str):
159
+ return code
160
+ return None
161
+
162
+
163
+ async def start_attempt_report_safely(
164
+ store: AgentRuntimeReportStore,
165
+ run_id: str,
166
+ tool_round: int,
167
+ deployment_key: str,
168
+ requested_model: str,
169
+ ) -> AttemptReport | None:
170
+ try:
171
+ return await store.start_attempt(
172
+ run_id,
173
+ tool_round,
174
+ deployment_key,
175
+ requested_model,
176
+ time.time(),
177
+ )
178
+ except Exception:
179
+ reporting_failure(store, "attempt_start", run_id)
180
+ return None
181
+
182
+
183
+ async def finish_attempt_report_safely(
184
+ store: AgentRuntimeReportStore,
185
+ attempt: AttemptReport | None,
186
+ outcome: AttemptTerminalOutcome,
187
+ usage: UsageFacts,
188
+ *,
189
+ reported_model: str | None = None,
190
+ safe_error_code: str | None = None,
191
+ ) -> None:
192
+ if attempt is None:
193
+ return
194
+ try:
195
+ await store.finish_attempt(
196
+ attempt.attempt_id,
197
+ outcome,
198
+ usage,
199
+ time.time(),
200
+ reported_model,
201
+ safe_error_code,
202
+ )
203
+ except Exception:
204
+ reporting_failure(store, "attempt_terminal", attempt.run_id)
205
+
206
+
207
+ def reporting_failure(
208
+ store: AgentRuntimeReportStore,
209
+ stage: str,
210
+ run_id: str,
211
+ extra: dict[str, str] | None = None,
212
+ ) -> None:
213
+ store.metrics.observe_reporting_failure(stage)
214
+ logger.exception(
215
+ "Agent Runtime 报表写入失败",
216
+ extra={"run_id": run_id, "report_stage": stage, **(extra or {})},
217
+ )
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import defaultdict
4
+ from threading import Lock
5
+
6
+ from .report_types import AttemptTerminalOutcome, UsageFacts
7
+
8
+
9
+ class AgentRuntimeMetrics:
10
+ """In-process Prometheus counters with deliberately low-cardinality labels."""
11
+
12
+ def __init__(self) -> None:
13
+ self._values: defaultdict[tuple[str, ...], int] = defaultdict(int)
14
+ self._lock = Lock()
15
+
16
+ def observe_run(self, outcome: str) -> None:
17
+ with self._lock:
18
+ self._values[("runs", outcome)] += 1
19
+
20
+ def observe_reporting_failure(self, stage: str) -> None:
21
+ with self._lock:
22
+ self._values[("reporting_failures", stage)] += 1
23
+
24
+ def observe_attempt(
25
+ self,
26
+ outcome: AttemptTerminalOutcome,
27
+ usage: UsageFacts,
28
+ ) -> None:
29
+ with self._lock:
30
+ self._values[("attempts", outcome)] += 1
31
+ self._values[("cache", usage.cache_status)] += 1
32
+ self._values[("usage", usage.source)] += 1
33
+ for kind, value in (
34
+ ("input", usage.input_tokens),
35
+ ("output", usage.output_tokens),
36
+ ("cached_input", usage.cached_input_tokens),
37
+ ("reasoning", usage.reasoning_tokens),
38
+ ("total", usage.total_tokens),
39
+ ):
40
+ if value is not None:
41
+ self._values[("tokens", kind, usage.source)] += value
42
+
43
+ def render(self) -> str:
44
+ with self._lock:
45
+ values = dict(self._values)
46
+ lines = [
47
+ "# HELP southwind_agent_runtime_runs_total Terminal Agent runs.",
48
+ "# TYPE southwind_agent_runtime_runs_total counter",
49
+ ]
50
+ lines.extend(
51
+ metric(
52
+ "southwind_agent_runtime_runs_total",
53
+ {"outcome": outcome},
54
+ values.get(("runs", outcome), 0),
55
+ )
56
+ for outcome in ("completed", "failed", "cancelled")
57
+ )
58
+ lines.extend(
59
+ (
60
+ "# HELP southwind_agent_runtime_attempts_total Provider attempts.",
61
+ "# TYPE southwind_agent_runtime_attempts_total counter",
62
+ )
63
+ )
64
+ lines.extend(
65
+ metric(
66
+ "southwind_agent_runtime_attempts_total",
67
+ {"outcome": outcome},
68
+ values.get(("attempts", outcome), 0),
69
+ )
70
+ for outcome in (
71
+ "completed",
72
+ "failed",
73
+ "cancelled",
74
+ "blocked",
75
+ "abandoned",
76
+ )
77
+ )
78
+ lines.extend(
79
+ (
80
+ "# HELP southwind_agent_runtime_cache_attempts_total Cache facts.",
81
+ "# TYPE southwind_agent_runtime_cache_attempts_total counter",
82
+ )
83
+ )
84
+ lines.extend(
85
+ metric(
86
+ "southwind_agent_runtime_cache_attempts_total",
87
+ {"status": status},
88
+ values.get(("cache", status), 0),
89
+ )
90
+ for status in ("hit", "miss", "unknown")
91
+ )
92
+ lines.extend(
93
+ (
94
+ "# HELP southwind_agent_runtime_usage_attempts_total Usage coverage.",
95
+ "# TYPE southwind_agent_runtime_usage_attempts_total counter",
96
+ )
97
+ )
98
+ lines.extend(
99
+ metric(
100
+ "southwind_agent_runtime_usage_attempts_total",
101
+ {"source": source},
102
+ values.get(("usage", source), 0),
103
+ )
104
+ for source in ("provider-reported", "estimated", "unknown")
105
+ )
106
+ lines.extend(
107
+ (
108
+ "# HELP southwind_agent_runtime_reporting_failures_total "
109
+ "Internal reporting write failures.",
110
+ "# TYPE southwind_agent_runtime_reporting_failures_total counter",
111
+ )
112
+ )
113
+ lines.extend(
114
+ metric(
115
+ "southwind_agent_runtime_reporting_failures_total",
116
+ {"stage": stage},
117
+ values.get(("reporting_failures", stage), 0),
118
+ )
119
+ for stage in (
120
+ "run_ensure",
121
+ "run_start",
122
+ "run_status",
123
+ "run_terminal",
124
+ "attempt_start",
125
+ "attempt_terminal",
126
+ )
127
+ )
128
+ lines.extend(
129
+ (
130
+ "# HELP southwind_agent_runtime_tokens_total Reported token usage.",
131
+ "# TYPE southwind_agent_runtime_tokens_total counter",
132
+ )
133
+ )
134
+ for key, value in sorted(values.items()):
135
+ if len(key) == 3 and key[0] == "tokens":
136
+ _, kind, source = key
137
+ lines.append(
138
+ metric(
139
+ "southwind_agent_runtime_tokens_total",
140
+ {"kind": kind, "source": source},
141
+ value,
142
+ )
143
+ )
144
+ return "\n".join(lines) + "\n"
145
+
146
+
147
+ def metric(name: str, labels: dict[str, str], value: int) -> str:
148
+ encoded = ",".join(f'{key}="{item}"' for key, item in labels.items())
149
+ return f"{name}{{{encoded}}} {value}"