create-windy 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +292 -125
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
- package/template/apps/agent-server/adk_web_agents/services.py +25 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
- package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
- package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
- package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
- package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
- package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
- package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
- package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
- package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
- package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
- package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
- package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
- package/template/apps/agent-server/tests/test_report_store.py +286 -0
- package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
- package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
- package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
- package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
- package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
- package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
- package/template/apps/server/src/agent/debug-session-id.ts +37 -0
- package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
- package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
- package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
- package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
- package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
- package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
- package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
- package/template/apps/server/src/agent/remote-runtime.ts +1 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
- package/template/apps/server/src/agent/run-authorization.ts +32 -1
- package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +5 -0
- package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
- package/template/apps/server/src/agent/run-facade.test.ts +78 -0
- package/template/apps/server/src/agent/run-facade.ts +33 -3
- package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
- package/template/apps/server/src/agent/run-input-verification.ts +41 -0
- package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
- package/template/apps/server/src/agent/tool-host.test.ts +1 -0
- package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
- package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
- package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
- package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
- package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
- package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
- package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
- package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
- package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
- package/template/apps/server/src/license/runtime-service.test.ts +6 -0
- package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
- package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
- package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
- package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
- package/template/apps/server/src/module-host/initialize.ts +6 -197
- package/template/apps/server/src/module-host/request-context.ts +1 -0
- package/template/apps/server/src/module-host/scoped-host.ts +214 -0
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
- package/template/apps/server/src/runtime-adk-debug.ts +8 -2
- package/template/apps/server/src/system/operations.test.ts +54 -0
- package/template/apps/server/src/system/operations.ts +23 -1
- package/template/apps/server/src/system/seed.test.ts +4 -0
- package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
- package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
- package/template/apps/web/src/layout/AppSidebar.vue +6 -1
- package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
- package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
- package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
- package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
- package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
- package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
- package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
- package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
- package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
- package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
- package/template/apps/web/src/router/error-navigation.ts +22 -0
- package/template/apps/web/src/router/index.ts +12 -23
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
- package/template/apps/web/src/services/operations-api.ts +28 -0
- package/template/docker-compose.agent-debug.yml +1 -1
- package/template/docs/architecture/ai-runtime.md +2 -1
- package/template/docs/architecture/module-host.md +3 -0
- package/template/docs/platform/agent-run-attestation.md +65 -0
- package/template/docs/platform/agent-runtime-observability.md +135 -0
- package/template/docs/platform/agent-runtime.md +47 -3
- package/template/docs/platform/operations-overview.md +13 -0
- package/template/docs/platform/platform-authentication.md +4 -3
- package/template/packages/config/package.json +1 -1
- package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
- package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
- package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
- package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
- package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
- package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
- package/template/packages/database/drizzle/meta/_journal.json +21 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +114 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-routes.ts +10 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
- package/template/packages/modules/src/system-api-control-plane.ts +17 -1
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +8 -0
- package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
- package/template/packages/modules/src/system-module-catalog.ts +15 -4
- package/template/packages/modules/src/system-modules-access.test.ts +69 -0
- package/template/packages/modules/src/system-modules.ts +13 -0
- package/template/packages/modules/src/system-permissions.ts +2 -0
- package/template/packages/server-sdk/index.ts +11 -0
- package/template/packages/server-sdk/package.json +2 -1
- package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
- package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
- package/template/packages/server-sdk/src/module-host.ts +5 -0
- package/template/packages/shared/index.ts +3 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +134 -0
- package/template/scripts/lib/postgres-regression.ts +201 -0
|
@@ -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
|
+
]
|