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,344 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import time
5
+ import uuid
6
+ from collections.abc import Callable
7
+ from dataclasses import replace
8
+ from typing import Literal, Protocol
9
+
10
+ from .contracts import StartRunRequest
11
+ from .report_metrics import AgentRuntimeMetrics
12
+ from .report_presenter import (
13
+ aggregate_summary,
14
+ attempt_value,
15
+ attempts_for,
16
+ filtered_attempts,
17
+ filtered_runs,
18
+ run_value,
19
+ scoped_runs,
20
+ )
21
+ from .report_types import (
22
+ AttemptReport,
23
+ AttemptTerminalOutcome,
24
+ NonTerminalRunStatus,
25
+ ReportFilter,
26
+ RunReport,
27
+ UsageFacts,
28
+ identity,
29
+ retention_seconds,
30
+ validate_retention,
31
+ )
32
+
33
+
34
+ class AgentRuntimeReportStore(Protocol):
35
+ retention_days: int
36
+ metrics: AgentRuntimeMetrics
37
+
38
+ async def ensure_run(
39
+ self, run_id: str, request: StartRunRequest, created_at: float
40
+ ) -> None: ...
41
+
42
+ async def start_run(self, run_id: str, started_at: float) -> None: ...
43
+
44
+ async def set_run_status(
45
+ self,
46
+ run_id: str,
47
+ status: NonTerminalRunStatus,
48
+ updated_at: float,
49
+ ) -> None: ...
50
+
51
+ async def finish_run(
52
+ self,
53
+ run_id: str,
54
+ status: Literal["completed", "failed", "cancelled"],
55
+ finished_at: float,
56
+ safe_error_code: str | None = None,
57
+ ) -> None: ...
58
+
59
+ async def start_attempt(
60
+ self,
61
+ run_id: str,
62
+ tool_round: int,
63
+ deployment_key: str,
64
+ requested_model: str,
65
+ started_at: float,
66
+ ) -> AttemptReport: ...
67
+
68
+ async def finish_attempt(
69
+ self,
70
+ attempt_id: str,
71
+ outcome: AttemptTerminalOutcome,
72
+ usage: UsageFacts,
73
+ finished_at: float,
74
+ reported_model: str | None = None,
75
+ safe_error_code: str | None = None,
76
+ ) -> None: ...
77
+
78
+ async def summary(self, query: ReportFilter) -> dict[str, object]: ...
79
+
80
+ async def list_runs(
81
+ self, query: ReportFilter, page: int, page_size: int
82
+ ) -> dict[str, object]: ...
83
+
84
+ async def get_run(self, run_id: str) -> dict[str, object] | None: ...
85
+
86
+ async def close(self) -> None: ...
87
+
88
+
89
+ class InMemoryAgentRuntimeReportStore:
90
+ def __init__(
91
+ self,
92
+ retention_days: int = 90,
93
+ *,
94
+ now: Callable[[], float] = time.time,
95
+ metrics: AgentRuntimeMetrics | None = None,
96
+ ) -> None:
97
+ validate_retention(retention_days)
98
+ self.retention_days = retention_days
99
+ self._now = now
100
+ self.metrics = metrics or AgentRuntimeMetrics()
101
+ self._runs: dict[str, RunReport] = {}
102
+ self._attempts: dict[str, AttemptReport] = {}
103
+ self._lock = asyncio.Lock()
104
+
105
+ async def ensure_run(
106
+ self, run_id: str, request: StartRunRequest, created_at: float
107
+ ) -> None:
108
+ async with self._lock:
109
+ self._prune()
110
+ context = request.reporting
111
+ candidate = RunReport(
112
+ run_id=run_id,
113
+ operation_key=request.operation_key,
114
+ release_id=request.release.release_id,
115
+ trace_id=request.trace_id,
116
+ actor_id=context.actor_id if context else None,
117
+ actor_type=context.actor_type if context else None,
118
+ department_id=context.department_id if context else None,
119
+ status="accepted",
120
+ safe_error_code=None,
121
+ started_at=None,
122
+ finished_at=None,
123
+ duration_ms=None,
124
+ created_at=created_at,
125
+ updated_at=created_at,
126
+ expires_at=created_at + retention_seconds(self.retention_days),
127
+ )
128
+ existing = self._runs.get(run_id)
129
+ if existing and identity(existing) != identity(candidate):
130
+ raise ValueError("Agent Runtime 报表 Run 身份冲突")
131
+ if existing is None:
132
+ self._runs[run_id] = candidate
133
+
134
+ async def start_run(self, run_id: str, started_at: float) -> None:
135
+ async with self._lock:
136
+ run = self._runs[run_id]
137
+ if run.status in {"completed", "failed", "cancelled"}:
138
+ return
139
+ for attempt_id, attempt in self._attempts.items():
140
+ if attempt.run_id != run_id or attempt.outcome != "running":
141
+ continue
142
+ usage = UsageFacts()
143
+ self._attempts[attempt_id] = replace(
144
+ attempt,
145
+ outcome="abandoned",
146
+ usage=usage,
147
+ duration_ms=max(
148
+ 0,
149
+ round((started_at - attempt.started_at) * 1_000),
150
+ ),
151
+ finished_at=started_at,
152
+ )
153
+ self.metrics.observe_attempt("abandoned", usage)
154
+ self._runs[run_id] = replace(
155
+ run,
156
+ status="running",
157
+ started_at=run.started_at or started_at,
158
+ updated_at=started_at,
159
+ )
160
+
161
+ async def set_run_status(
162
+ self,
163
+ run_id: str,
164
+ status: NonTerminalRunStatus,
165
+ updated_at: float,
166
+ ) -> None:
167
+ async with self._lock:
168
+ run = self._runs[run_id]
169
+ if run.status in {"completed", "failed", "cancelled"}:
170
+ return
171
+ if updated_at < run.updated_at:
172
+ return
173
+ self._runs[run_id] = replace(
174
+ run,
175
+ status=status,
176
+ updated_at=updated_at,
177
+ )
178
+
179
+ async def finish_run(
180
+ self,
181
+ run_id: str,
182
+ status: Literal["completed", "failed", "cancelled"],
183
+ finished_at: float,
184
+ safe_error_code: str | None = None,
185
+ ) -> None:
186
+ async with self._lock:
187
+ run = self._runs[run_id]
188
+ if run.status in {"completed", "failed", "cancelled"}:
189
+ if run.status != status:
190
+ raise ValueError("Agent Runtime 报表终态冲突")
191
+ return
192
+ started = run.started_at or run.created_at
193
+ self._runs[run_id] = replace(
194
+ run,
195
+ status=status,
196
+ safe_error_code=safe_error_code,
197
+ finished_at=finished_at,
198
+ duration_ms=max(0, round((finished_at - started) * 1_000)),
199
+ updated_at=finished_at,
200
+ expires_at=finished_at + retention_seconds(self.retention_days),
201
+ )
202
+ self.metrics.observe_run(status)
203
+
204
+ async def start_attempt(
205
+ self,
206
+ run_id: str,
207
+ tool_round: int,
208
+ deployment_key: str,
209
+ requested_model: str,
210
+ started_at: float,
211
+ ) -> AttemptReport:
212
+ async with self._lock:
213
+ sequence = 1 + max(
214
+ (
215
+ attempt.sequence
216
+ for attempt in self._attempts.values()
217
+ if attempt.run_id == run_id
218
+ ),
219
+ default=0,
220
+ )
221
+ attempt = AttemptReport(
222
+ attempt_id=f"attempt_{uuid.uuid4().hex}",
223
+ run_id=run_id,
224
+ sequence=sequence,
225
+ tool_round=tool_round,
226
+ deployment_key=deployment_key,
227
+ requested_model=requested_model,
228
+ reported_model=None,
229
+ outcome="running",
230
+ usage=UsageFacts(),
231
+ duration_ms=None,
232
+ safe_error_code=None,
233
+ started_at=started_at,
234
+ finished_at=None,
235
+ )
236
+ self._attempts[attempt.attempt_id] = attempt
237
+ return attempt
238
+
239
+ async def finish_attempt(
240
+ self,
241
+ attempt_id: str,
242
+ outcome: AttemptTerminalOutcome,
243
+ usage: UsageFacts,
244
+ finished_at: float,
245
+ reported_model: str | None = None,
246
+ safe_error_code: str | None = None,
247
+ ) -> None:
248
+ async with self._lock:
249
+ attempt = self._attempts[attempt_id]
250
+ if attempt.finished_at is not None:
251
+ if (
252
+ attempt.outcome != outcome
253
+ or attempt.usage != usage
254
+ or attempt.reported_model != reported_model
255
+ or attempt.safe_error_code != safe_error_code
256
+ ):
257
+ raise ValueError("Agent Runtime 报表 Attempt 终态冲突")
258
+ return
259
+ self._attempts[attempt_id] = replace(
260
+ attempt,
261
+ outcome=outcome,
262
+ usage=usage,
263
+ reported_model=reported_model,
264
+ safe_error_code=safe_error_code,
265
+ duration_ms=max(0, round((finished_at - attempt.started_at) * 1_000)),
266
+ finished_at=finished_at,
267
+ )
268
+ self.metrics.observe_attempt(outcome, usage)
269
+
270
+ async def summary(self, query: ReportFilter) -> dict[str, object]:
271
+ async with self._lock:
272
+ self._prune()
273
+ visible_runs = scoped_runs(self._runs.values(), query)
274
+ runs = filtered_runs(visible_runs, query)
275
+ attempts = filtered_attempts(
276
+ self._attempts.values(),
277
+ visible_runs,
278
+ query,
279
+ )
280
+ return aggregate_summary(
281
+ runs,
282
+ attempts,
283
+ self.retention_days,
284
+ min(
285
+ [run.created_at for run in runs]
286
+ + [attempt.started_at for attempt in attempts],
287
+ default=None,
288
+ ),
289
+ self._now(),
290
+ )
291
+
292
+ async def list_runs(
293
+ self, query: ReportFilter, page: int, page_size: int
294
+ ) -> dict[str, object]:
295
+ async with self._lock:
296
+ self._prune()
297
+ runs = sorted(
298
+ filtered_runs(self._runs.values(), query),
299
+ key=lambda run: run.created_at,
300
+ reverse=True,
301
+ )
302
+ start = (page - 1) * page_size
303
+ items = [
304
+ run_value(run, attempts_for(self._attempts.values(), [run]))
305
+ for run in runs[start : start + page_size]
306
+ ]
307
+ return {
308
+ "items": items,
309
+ "total": len(runs),
310
+ "page": page,
311
+ "pageSize": page_size,
312
+ }
313
+
314
+ async def get_run(self, run_id: str) -> dict[str, object] | None:
315
+ async with self._lock:
316
+ self._prune()
317
+ run = self._runs.get(run_id)
318
+ if run is None:
319
+ return None
320
+ attempts = attempts_for(self._attempts.values(), [run])
321
+ return {
322
+ **run_value(run, attempts),
323
+ "releaseId": run.release_id,
324
+ "traceId": run.trace_id,
325
+ "safeErrorCode": run.safe_error_code,
326
+ "attempts": [attempt_value(item) for item in attempts],
327
+ }
328
+
329
+ async def close(self) -> None:
330
+ return None
331
+
332
+ def _prune(self) -> None:
333
+ expired = {
334
+ run_id
335
+ for run_id, run in self._runs.items()
336
+ if run.expires_at <= self._now()
337
+ }
338
+ for run_id in expired:
339
+ self._runs.pop(run_id, None)
340
+ self._attempts = {
341
+ key: value
342
+ for key, value in self._attempts.items()
343
+ if value.run_id not in expired
344
+ }
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import UTC, datetime
5
+ from typing import Literal
6
+
7
+ from .contracts import RunStatus
8
+
9
+ type AttemptTerminalOutcome = Literal[
10
+ "completed", "failed", "cancelled", "blocked", "abandoned"
11
+ ]
12
+ type AttemptOutcome = Literal["running"] | AttemptTerminalOutcome
13
+ type NonTerminalRunStatus = Literal["running", "awaiting-approval"]
14
+ type CacheStatus = Literal["hit", "miss", "unknown"]
15
+ type UsageSource = Literal["provider-reported", "estimated", "unknown"]
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class UsageFacts:
20
+ source: UsageSource = "unknown"
21
+ input_tokens: int | None = None
22
+ output_tokens: int | None = None
23
+ cached_input_tokens: int | None = None
24
+ reasoning_tokens: int | None = None
25
+ total_tokens: int | None = None
26
+ first_token_latency_ms: int | None = None
27
+ cache_status: CacheStatus = "unknown"
28
+
29
+ def event_value(self) -> dict[str, str | int]:
30
+ result: dict[str, str | int] = {
31
+ "source": self.source,
32
+ "cacheStatus": self.cache_status,
33
+ }
34
+ for key, value in (
35
+ ("inputTokens", self.input_tokens),
36
+ ("outputTokens", self.output_tokens),
37
+ ("cachedInputTokens", self.cached_input_tokens),
38
+ ("reasoningTokens", self.reasoning_tokens),
39
+ ("totalTokens", self.total_tokens),
40
+ ("firstTokenLatencyMs", self.first_token_latency_ms),
41
+ ):
42
+ if value is not None:
43
+ result[key] = value
44
+ return result
45
+
46
+
47
+ @dataclass(frozen=True)
48
+ class RunReport:
49
+ run_id: str
50
+ operation_key: str
51
+ release_id: str
52
+ trace_id: str
53
+ actor_id: str | None
54
+ actor_type: str | None
55
+ department_id: str | None
56
+ status: RunStatus
57
+ safe_error_code: str | None
58
+ started_at: float | None
59
+ finished_at: float | None
60
+ duration_ms: int | None
61
+ created_at: float
62
+ updated_at: float
63
+ expires_at: float
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class AttemptReport:
68
+ attempt_id: str
69
+ run_id: str
70
+ sequence: int
71
+ tool_round: int
72
+ deployment_key: str
73
+ requested_model: str
74
+ reported_model: str | None
75
+ outcome: AttemptOutcome
76
+ usage: UsageFacts
77
+ duration_ms: int | None
78
+ safe_error_code: str | None
79
+ started_at: float
80
+ finished_at: float | None
81
+
82
+
83
+ @dataclass(frozen=True)
84
+ class ReportFilter:
85
+ from_timestamp: float
86
+ to_timestamp: float
87
+ selected_actor_id: str | None = None
88
+ scope_restricted: bool = False
89
+ actor_ids: tuple[str, ...] = ()
90
+ department_ids: tuple[str, ...] = ()
91
+
92
+
93
+ def validate_retention(value: int) -> None:
94
+ if not 1 <= value <= 3650:
95
+ raise ValueError("AI Runtime 报表留存天数超出范围")
96
+
97
+
98
+ def retention_seconds(days: int) -> int:
99
+ return days * 24 * 60 * 60
100
+
101
+
102
+ def iso(value: float | None) -> str | None:
103
+ return (
104
+ datetime.fromtimestamp(value, tz=UTC).isoformat().replace("+00:00", "Z")
105
+ if value is not None
106
+ else None
107
+ )
108
+
109
+
110
+ def identity(run: RunReport) -> tuple[str, str, str, str | None]:
111
+ return (run.operation_key, run.release_id, run.trace_id, run.actor_id)
@@ -6,19 +6,26 @@ from .adk_adapter import AdkEngineAdapter
6
6
  from .config import EnvironmentSecretResolver, OpenAiCompatibleConfig
7
7
  from .engine import AgentEngineAdapter
8
8
  from .openai_provider import OpenAiCompatibleProvider
9
+ from .postgres_report_store import PostgresAgentRuntimeReportStore
9
10
  from .postgres_store import PostgresRunStore
10
11
  from .provider_audit import ProviderAuditStore
11
12
  from .provider_governance import ProviderRequestGovernance
13
+ from .report_store import (
14
+ AgentRuntimeReportStore,
15
+ InMemoryAgentRuntimeReportStore,
16
+ )
12
17
  from .sqlite_store import SqliteRunStore
13
18
  from .store import InMemoryRunStore, RunStore
14
19
  from .store_cipher import RunStoreCipher
15
20
  from .tool_host import ToolHostClient
16
21
 
17
22
 
18
- def default_engine() -> AgentEngineAdapter:
23
+ def default_engine(
24
+ report_store: AgentRuntimeReportStore | None = None,
25
+ ) -> AgentEngineAdapter:
19
26
  config = OpenAiCompatibleConfig.from_environment()
20
27
  if config is None:
21
- return AdkEngineAdapter()
28
+ return AdkEngineAdapter(report_store=report_store)
22
29
  internal_token = os.environ.get("AGENT_SERVER_INTERNAL_TOKEN", "").strip()
23
30
  if not internal_token:
24
31
  raise ValueError("Agent Server Provider 已配置但内部 Token 缺失")
@@ -34,6 +41,7 @@ def default_engine() -> AgentEngineAdapter:
34
41
  retention_days=provider_audit_retention_days(),
35
42
  )
36
43
  ),
44
+ report_store,
37
45
  )
38
46
 
39
47
 
@@ -50,6 +58,16 @@ def default_store() -> RunStore:
50
58
  )
51
59
 
52
60
 
61
+ def default_report_store() -> AgentRuntimeReportStore:
62
+ database_url = os.environ.get("AGENT_DATABASE_URL", "").strip()
63
+ retention_days = report_retention_days()
64
+ return (
65
+ PostgresAgentRuntimeReportStore(database_url, retention_days)
66
+ if database_url
67
+ else InMemoryAgentRuntimeReportStore(retention_days)
68
+ )
69
+
70
+
53
71
  def required_environment(key: str) -> str:
54
72
  value = os.environ.get(key, "").strip()
55
73
  if not value:
@@ -66,3 +84,14 @@ def provider_audit_retention_days() -> int:
66
84
  if not 1 <= value <= 3650:
67
85
  raise ValueError("AI_PROVIDER_AUDIT_RETENTION_DAYS 超出允许范围")
68
86
  return value
87
+
88
+
89
+ def report_retention_days() -> int:
90
+ raw = os.environ.get("AI_RUNTIME_REPORT_RETENTION_DAYS", "90")
91
+ try:
92
+ value = int(raw)
93
+ except ValueError as error:
94
+ raise ValueError("AI_RUNTIME_REPORT_RETENTION_DAYS 必须是整数") from error
95
+ if not 1 <= value <= 3650:
96
+ raise ValueError("AI_RUNTIME_REPORT_RETENTION_DAYS 超出允许范围")
97
+ return value