create-windy 0.4.1 → 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.
- package/dist/cli.js +258 -121
- 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,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Literal, cast
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from southwind_agent_server.app import create_app
|
|
10
|
+
from southwind_agent_server.contracts import StartRunRequest
|
|
11
|
+
from southwind_agent_server.deterministic import DeterministicEngineAdapter
|
|
12
|
+
from southwind_agent_server.engine import EngineCompleted
|
|
13
|
+
from southwind_agent_server.openai_engine import OpenAiCompatibleEngineAdapter
|
|
14
|
+
from southwind_agent_server.report_store import InMemoryAgentRuntimeReportStore
|
|
15
|
+
from southwind_agent_server.report_types import (
|
|
16
|
+
AttemptReport,
|
|
17
|
+
AttemptTerminalOutcome,
|
|
18
|
+
UsageFacts,
|
|
19
|
+
)
|
|
20
|
+
from southwind_agent_server.tool_host import ToolHostClient
|
|
21
|
+
from tests.openai_provider_test_support import (
|
|
22
|
+
create_provider,
|
|
23
|
+
governance,
|
|
24
|
+
runtime_request,
|
|
25
|
+
sse_response,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def test_report_store_outage_does_not_rewrite_canonical_terminal() -> None:
|
|
30
|
+
report_store = UnavailableRunReportStore()
|
|
31
|
+
application = create_app(
|
|
32
|
+
DeterministicEngineAdapter(),
|
|
33
|
+
heartbeat_seconds=0.01,
|
|
34
|
+
report_store=report_store,
|
|
35
|
+
)
|
|
36
|
+
async with httpx.AsyncClient(
|
|
37
|
+
transport=httpx.ASGITransport(app=application),
|
|
38
|
+
base_url="http://agent.test",
|
|
39
|
+
) as client:
|
|
40
|
+
response = await client.post(
|
|
41
|
+
"/api/agent/runs",
|
|
42
|
+
headers={"Idempotency-Key": "report-store-outage"},
|
|
43
|
+
json={"operationKey": "test.echo", "input": {"text": "completed"}},
|
|
44
|
+
)
|
|
45
|
+
run_id = str(cast(dict[str, object], response.json())["runId"])
|
|
46
|
+
event_response = await client.get(f"/api/agent/runs/{run_id}/events")
|
|
47
|
+
|
|
48
|
+
emitted = events(event_response)
|
|
49
|
+
assert emitted[-1]["type"] == "run.completed"
|
|
50
|
+
assert "run.failed" not in {event["type"] for event in emitted}
|
|
51
|
+
rendered = report_store.metrics.render()
|
|
52
|
+
for stage in ("run_ensure", "run_start", "run_terminal"):
|
|
53
|
+
assert (
|
|
54
|
+
f'southwind_agent_runtime_reporting_failures_total{{stage="{stage}"}} 1'
|
|
55
|
+
in rendered
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.parametrize("failure_stage", ["attempt_start", "attempt_terminal"])
|
|
60
|
+
async def test_report_store_outage_does_not_block_model_request(
|
|
61
|
+
failure_stage: Literal["attempt_start", "attempt_terminal"],
|
|
62
|
+
) -> None:
|
|
63
|
+
provider_calls = 0
|
|
64
|
+
|
|
65
|
+
async def provider_handler(_request: httpx.Request) -> httpx.Response:
|
|
66
|
+
nonlocal provider_calls
|
|
67
|
+
provider_calls += 1
|
|
68
|
+
return sse_response([{"choices": [{"delta": {"content": "完成"}}]}])
|
|
69
|
+
|
|
70
|
+
report_store = FailingAttemptReportStore(failure_stage)
|
|
71
|
+
engine = OpenAiCompatibleEngineAdapter(
|
|
72
|
+
create_provider(httpx.MockTransport(provider_handler)),
|
|
73
|
+
ToolHostClient("internal-token"),
|
|
74
|
+
governance(),
|
|
75
|
+
report_store,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
signals = [
|
|
79
|
+
signal
|
|
80
|
+
async for signal in engine.execute(runtime_request("business.records.list"))
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
assert provider_calls == 1
|
|
84
|
+
assert isinstance(signals[-1], EngineCompleted)
|
|
85
|
+
assert (
|
|
86
|
+
"southwind_agent_runtime_reporting_failures_total"
|
|
87
|
+
f'{{stage="{failure_stage}"}} 1' in report_store.metrics.render()
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class UnavailableRunReportStore(InMemoryAgentRuntimeReportStore):
|
|
92
|
+
async def ensure_run(
|
|
93
|
+
self,
|
|
94
|
+
run_id: str,
|
|
95
|
+
request: StartRunRequest,
|
|
96
|
+
created_at: float,
|
|
97
|
+
) -> None:
|
|
98
|
+
raise RuntimeError("report store unavailable")
|
|
99
|
+
|
|
100
|
+
async def start_run(self, run_id: str, started_at: float) -> None:
|
|
101
|
+
raise RuntimeError("report store unavailable")
|
|
102
|
+
|
|
103
|
+
async def set_run_status(
|
|
104
|
+
self,
|
|
105
|
+
run_id: str,
|
|
106
|
+
status: Literal["running", "awaiting-approval"],
|
|
107
|
+
updated_at: float,
|
|
108
|
+
) -> None:
|
|
109
|
+
raise RuntimeError("report store unavailable")
|
|
110
|
+
|
|
111
|
+
async def finish_run(
|
|
112
|
+
self,
|
|
113
|
+
run_id: str,
|
|
114
|
+
status: Literal["completed", "failed", "cancelled"],
|
|
115
|
+
finished_at: float,
|
|
116
|
+
safe_error_code: str | None = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
raise RuntimeError("report store unavailable")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class FailingAttemptReportStore(InMemoryAgentRuntimeReportStore):
|
|
122
|
+
def __init__(
|
|
123
|
+
self,
|
|
124
|
+
failure_stage: Literal["attempt_start", "attempt_terminal"],
|
|
125
|
+
) -> None:
|
|
126
|
+
super().__init__()
|
|
127
|
+
self.failure_stage = failure_stage
|
|
128
|
+
|
|
129
|
+
async def start_attempt(
|
|
130
|
+
self,
|
|
131
|
+
run_id: str,
|
|
132
|
+
tool_round: int,
|
|
133
|
+
deployment_key: str,
|
|
134
|
+
requested_model: str,
|
|
135
|
+
started_at: float,
|
|
136
|
+
) -> AttemptReport:
|
|
137
|
+
if self.failure_stage == "attempt_start":
|
|
138
|
+
raise RuntimeError("report store unavailable")
|
|
139
|
+
return await super().start_attempt(
|
|
140
|
+
run_id,
|
|
141
|
+
tool_round,
|
|
142
|
+
deployment_key,
|
|
143
|
+
requested_model,
|
|
144
|
+
started_at,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
async def finish_attempt(
|
|
148
|
+
self,
|
|
149
|
+
attempt_id: str,
|
|
150
|
+
outcome: AttemptTerminalOutcome,
|
|
151
|
+
usage: UsageFacts,
|
|
152
|
+
finished_at: float,
|
|
153
|
+
reported_model: str | None = None,
|
|
154
|
+
safe_error_code: str | None = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
if self.failure_stage == "attempt_terminal":
|
|
157
|
+
raise RuntimeError("report store unavailable")
|
|
158
|
+
await super().finish_attempt(
|
|
159
|
+
attempt_id,
|
|
160
|
+
outcome,
|
|
161
|
+
usage,
|
|
162
|
+
finished_at,
|
|
163
|
+
reported_model,
|
|
164
|
+
safe_error_code,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def events(response: httpx.Response) -> list[dict[str, object]]:
|
|
169
|
+
result: list[dict[str, object]] = []
|
|
170
|
+
for frame in response.text.split("\n\n"):
|
|
171
|
+
data = next(
|
|
172
|
+
(
|
|
173
|
+
line.removeprefix("data: ")
|
|
174
|
+
for line in frame.splitlines()
|
|
175
|
+
if line.startswith("data: ")
|
|
176
|
+
),
|
|
177
|
+
None,
|
|
178
|
+
)
|
|
179
|
+
if data is not None:
|
|
180
|
+
result.append(json.loads(data))
|
|
181
|
+
return result
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from southwind_agent_server.contracts import (
|
|
8
|
+
RunReportingContext,
|
|
9
|
+
StartRunRequest,
|
|
10
|
+
)
|
|
11
|
+
from southwind_agent_server.postgres_report_insert import (
|
|
12
|
+
RUN_INSERT_SQL,
|
|
13
|
+
run_insert_values,
|
|
14
|
+
)
|
|
15
|
+
from southwind_agent_server.report_lifecycle import sync_status
|
|
16
|
+
from southwind_agent_server.report_store import InMemoryAgentRuntimeReportStore
|
|
17
|
+
from southwind_agent_server.report_types import ReportFilter, UsageFacts
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def request(actor_id: str) -> StartRunRequest:
|
|
21
|
+
return StartRunRequest(
|
|
22
|
+
operation_key="business.answer",
|
|
23
|
+
input={"secret": "must-not-be-reported"},
|
|
24
|
+
reporting=RunReportingContext(
|
|
25
|
+
actor_id=actor_id,
|
|
26
|
+
actor_type="user",
|
|
27
|
+
department_id="department_1",
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_postgres_run_insert_binds_every_persisted_timestamp() -> None:
|
|
33
|
+
values = run_insert_values("run_1", request("actor_1"), 100.0, 90)
|
|
34
|
+
|
|
35
|
+
assert RUN_INSERT_SQL.count("%s") == len(values)
|
|
36
|
+
assert values[-3:] == (100.0, 100.0, 7_776_100.0)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def test_summary_rates_exclude_cancelled_and_unknown_cache() -> None:
|
|
40
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
41
|
+
values: tuple[tuple[str, str, Literal["completed", "failed", "cancelled"]], ...] = (
|
|
42
|
+
("run_completed", "actor_1", "completed"),
|
|
43
|
+
("run_failed", "actor_2", "failed"),
|
|
44
|
+
("run_cancelled", "actor_3", "cancelled"),
|
|
45
|
+
)
|
|
46
|
+
for run_id, actor_id, status in values:
|
|
47
|
+
await store.ensure_run(run_id, request(actor_id), 100.0)
|
|
48
|
+
await store.start_run(run_id, 101.0)
|
|
49
|
+
await store.finish_run(run_id, status, 102.0)
|
|
50
|
+
|
|
51
|
+
attempts = (
|
|
52
|
+
("run_completed", UsageFacts(cache_status="hit")),
|
|
53
|
+
("run_failed", UsageFacts(cache_status="miss")),
|
|
54
|
+
("run_cancelled", UsageFacts()),
|
|
55
|
+
)
|
|
56
|
+
for run_id, usage in attempts:
|
|
57
|
+
attempt = await store.start_attempt(
|
|
58
|
+
run_id,
|
|
59
|
+
1,
|
|
60
|
+
"ai.openai-compatible",
|
|
61
|
+
"requested-model",
|
|
62
|
+
101.0,
|
|
63
|
+
)
|
|
64
|
+
await store.finish_attempt(
|
|
65
|
+
attempt.attempt_id,
|
|
66
|
+
"completed",
|
|
67
|
+
usage,
|
|
68
|
+
102.0,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
summary = await store.summary(ReportFilter(0, 500))
|
|
72
|
+
|
|
73
|
+
assert summary["runs"] == {
|
|
74
|
+
"total": 3,
|
|
75
|
+
"completed": 1,
|
|
76
|
+
"failed": 1,
|
|
77
|
+
"cancelled": 1,
|
|
78
|
+
"inProgress": 0,
|
|
79
|
+
"successRate": 0.5,
|
|
80
|
+
}
|
|
81
|
+
assert summary["cache"] == {
|
|
82
|
+
"hit": 1,
|
|
83
|
+
"miss": 1,
|
|
84
|
+
"unknown": 1,
|
|
85
|
+
"hitRate": 0.5,
|
|
86
|
+
"tokenRate": None,
|
|
87
|
+
"coverageRate": 2 / 3,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def test_approval_status_transitions_and_terminal_never_regresses() -> None:
|
|
92
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
93
|
+
await store.ensure_run("run_1", request("actor_1"), 100.0)
|
|
94
|
+
await store.start_run("run_1", 101.0)
|
|
95
|
+
|
|
96
|
+
await sync_status(store, "run_1", "approval.required")
|
|
97
|
+
waiting = await store.get_run("run_1")
|
|
98
|
+
assert waiting is not None
|
|
99
|
+
assert waiting["status"] == "awaiting-approval"
|
|
100
|
+
|
|
101
|
+
await sync_status(store, "run_1", "approval.resolved")
|
|
102
|
+
resumed = await store.get_run("run_1")
|
|
103
|
+
assert resumed is not None
|
|
104
|
+
assert resumed["status"] == "running"
|
|
105
|
+
|
|
106
|
+
await store.finish_run("run_1", "completed", 104.0)
|
|
107
|
+
await sync_status(store, "run_1", "approval.required")
|
|
108
|
+
terminal = await store.get_run("run_1")
|
|
109
|
+
assert terminal is not None
|
|
110
|
+
assert terminal["status"] == "completed"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def test_unknown_usage_stays_null_and_empty_history_has_no_origin() -> None:
|
|
114
|
+
empty = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
115
|
+
empty_summary = await empty.summary(ReportFilter(0, 500))
|
|
116
|
+
assert empty_summary["reportingAvailableFrom"] is None
|
|
117
|
+
|
|
118
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
119
|
+
await store.ensure_run("run_1", request("actor_secret"), 100.0)
|
|
120
|
+
attempt = await store.start_attempt(
|
|
121
|
+
"run_1",
|
|
122
|
+
1,
|
|
123
|
+
"ai.openai-compatible",
|
|
124
|
+
"requested-model",
|
|
125
|
+
101.0,
|
|
126
|
+
)
|
|
127
|
+
await store.finish_attempt(
|
|
128
|
+
attempt.attempt_id,
|
|
129
|
+
"completed",
|
|
130
|
+
UsageFacts(),
|
|
131
|
+
102.0,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
detail = await store.get_run("run_1")
|
|
135
|
+
|
|
136
|
+
assert detail is not None
|
|
137
|
+
assert detail["usage"] == {
|
|
138
|
+
"inputTokens": None,
|
|
139
|
+
"outputTokens": None,
|
|
140
|
+
"cachedInputTokens": None,
|
|
141
|
+
"reasoningTokens": None,
|
|
142
|
+
"totalTokens": None,
|
|
143
|
+
}
|
|
144
|
+
rendered_metrics = store.metrics.render()
|
|
145
|
+
assert "actor_secret" not in rendered_metrics
|
|
146
|
+
assert "requested-model" not in rendered_metrics
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
async def test_reporting_origin_is_limited_to_filter_and_scope() -> None:
|
|
150
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
151
|
+
await store.ensure_run("run_other", request("actor_other"), 100.0)
|
|
152
|
+
await store.ensure_run("run_visible", request("actor_visible"), 200.0)
|
|
153
|
+
|
|
154
|
+
selected = await store.summary(
|
|
155
|
+
ReportFilter(0, 500, selected_actor_id="actor_visible")
|
|
156
|
+
)
|
|
157
|
+
scoped = await store.summary(
|
|
158
|
+
ReportFilter(
|
|
159
|
+
0,
|
|
160
|
+
500,
|
|
161
|
+
scope_restricted=True,
|
|
162
|
+
actor_ids=("actor_visible",),
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
empty = await store.summary(
|
|
166
|
+
ReportFilter(
|
|
167
|
+
0,
|
|
168
|
+
500,
|
|
169
|
+
scope_restricted=True,
|
|
170
|
+
actor_ids=("actor_missing",),
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
assert selected["reportingAvailableFrom"] == "1970-01-01T00:03:20Z"
|
|
175
|
+
assert scoped["reportingAvailableFrom"] == "1970-01-01T00:03:20Z"
|
|
176
|
+
assert empty["reportingAvailableFrom"] is None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
async def test_attempt_terminal_replay_rejects_changed_facts() -> None:
|
|
180
|
+
store = InMemoryAgentRuntimeReportStore()
|
|
181
|
+
attempt = await store.start_attempt(
|
|
182
|
+
"run_1",
|
|
183
|
+
1,
|
|
184
|
+
"ai.openai-compatible",
|
|
185
|
+
"requested-model",
|
|
186
|
+
100.0,
|
|
187
|
+
)
|
|
188
|
+
usage = UsageFacts(
|
|
189
|
+
source="provider-reported",
|
|
190
|
+
input_tokens=10,
|
|
191
|
+
total_tokens=10,
|
|
192
|
+
)
|
|
193
|
+
await store.finish_attempt(
|
|
194
|
+
attempt.attempt_id,
|
|
195
|
+
"completed",
|
|
196
|
+
usage,
|
|
197
|
+
101.0,
|
|
198
|
+
"reported-model",
|
|
199
|
+
)
|
|
200
|
+
await store.finish_attempt(
|
|
201
|
+
attempt.attempt_id,
|
|
202
|
+
"completed",
|
|
203
|
+
usage,
|
|
204
|
+
102.0,
|
|
205
|
+
"reported-model",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
with pytest.raises(ValueError, match="Attempt 终态冲突"):
|
|
209
|
+
await store.finish_attempt(
|
|
210
|
+
attempt.attempt_id,
|
|
211
|
+
"completed",
|
|
212
|
+
usage,
|
|
213
|
+
103.0,
|
|
214
|
+
"different-model",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def test_token_rates_only_use_attempts_with_matching_known_facts() -> None:
|
|
219
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
220
|
+
await store.ensure_run("run_1", request("actor_1"), 100.0)
|
|
221
|
+
for usage in (
|
|
222
|
+
UsageFacts(
|
|
223
|
+
source="provider-reported",
|
|
224
|
+
input_tokens=10,
|
|
225
|
+
cached_input_tokens=None,
|
|
226
|
+
total_tokens=None,
|
|
227
|
+
cache_status="unknown",
|
|
228
|
+
),
|
|
229
|
+
UsageFacts(
|
|
230
|
+
source="provider-reported",
|
|
231
|
+
input_tokens=10,
|
|
232
|
+
cached_input_tokens=5,
|
|
233
|
+
total_tokens=15,
|
|
234
|
+
cache_status="hit",
|
|
235
|
+
),
|
|
236
|
+
):
|
|
237
|
+
attempt = await store.start_attempt(
|
|
238
|
+
"run_1",
|
|
239
|
+
1,
|
|
240
|
+
"ai.openai-compatible",
|
|
241
|
+
"requested-model",
|
|
242
|
+
101.0,
|
|
243
|
+
)
|
|
244
|
+
await store.finish_attempt(
|
|
245
|
+
attempt.attempt_id,
|
|
246
|
+
"completed",
|
|
247
|
+
usage,
|
|
248
|
+
102.0,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
summary = await store.summary(ReportFilter(0, 500))
|
|
252
|
+
|
|
253
|
+
assert summary["cache"]["tokenRate"] == 0.5 # type: ignore[index]
|
|
254
|
+
assert summary["tokens"]["coverageRate"] == 0.5 # type: ignore[index]
|
|
255
|
+
assert summary["activity"][0]["totalTokens"] is None # type: ignore[index]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def test_completely_unknown_aggregate_tokens_stay_null() -> None:
|
|
259
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
260
|
+
await store.ensure_run("run_1", request("actor_1"), 100.0)
|
|
261
|
+
attempt = await store.start_attempt(
|
|
262
|
+
"run_1",
|
|
263
|
+
1,
|
|
264
|
+
"ai.openai-compatible",
|
|
265
|
+
"requested-model",
|
|
266
|
+
101.0,
|
|
267
|
+
)
|
|
268
|
+
await store.finish_attempt(
|
|
269
|
+
attempt.attempt_id,
|
|
270
|
+
"completed",
|
|
271
|
+
UsageFacts(),
|
|
272
|
+
102.0,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
summary = await store.summary(ReportFilter(0, 500))
|
|
276
|
+
|
|
277
|
+
assert summary["tokens"] == {
|
|
278
|
+
"input": None,
|
|
279
|
+
"output": None,
|
|
280
|
+
"cachedInput": None,
|
|
281
|
+
"reasoning": None,
|
|
282
|
+
"total": None,
|
|
283
|
+
"coverageRate": 0,
|
|
284
|
+
}
|
|
285
|
+
assert summary["byModel"][0]["totalTokens"] is None # type: ignore[index]
|
|
286
|
+
assert summary["activity"][0]["totalTokens"] is None # type: ignore[index]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from southwind_agent_server.postgres_report_summary import present_summary
|
|
4
|
+
from southwind_agent_server.report_presenter import aggregate_summary
|
|
5
|
+
from southwind_agent_server.report_types import (
|
|
6
|
+
AttemptReport,
|
|
7
|
+
RunReport,
|
|
8
|
+
UsageFacts,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_postgres_and_in_memory_summary_present_same_dual_timeline() -> None:
|
|
13
|
+
run = RunReport(
|
|
14
|
+
run_id="run_1",
|
|
15
|
+
operation_key="business.answer",
|
|
16
|
+
release_id="release_1",
|
|
17
|
+
trace_id="trace_1",
|
|
18
|
+
actor_id="actor_1",
|
|
19
|
+
actor_type="user",
|
|
20
|
+
department_id=None,
|
|
21
|
+
status="completed",
|
|
22
|
+
safe_error_code=None,
|
|
23
|
+
started_at=1,
|
|
24
|
+
finished_at=2,
|
|
25
|
+
duration_ms=1_000,
|
|
26
|
+
created_at=0,
|
|
27
|
+
updated_at=2,
|
|
28
|
+
expires_at=1_000,
|
|
29
|
+
)
|
|
30
|
+
usage = UsageFacts(
|
|
31
|
+
source="provider-reported",
|
|
32
|
+
input_tokens=10,
|
|
33
|
+
output_tokens=2,
|
|
34
|
+
cached_input_tokens=5,
|
|
35
|
+
total_tokens=12,
|
|
36
|
+
cache_status="hit",
|
|
37
|
+
)
|
|
38
|
+
attempt = AttemptReport(
|
|
39
|
+
attempt_id="attempt_1",
|
|
40
|
+
run_id="run_1",
|
|
41
|
+
sequence=1,
|
|
42
|
+
tool_round=1,
|
|
43
|
+
deployment_key="ai.openai-compatible",
|
|
44
|
+
requested_model="requested",
|
|
45
|
+
reported_model="reported",
|
|
46
|
+
outcome="completed",
|
|
47
|
+
usage=usage,
|
|
48
|
+
duration_ms=1_000,
|
|
49
|
+
safe_error_code=None,
|
|
50
|
+
started_at=172_800,
|
|
51
|
+
finished_at=172_801,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
memory = aggregate_summary([run], [attempt], 90, 0, 400)
|
|
55
|
+
postgres = present_summary(
|
|
56
|
+
{
|
|
57
|
+
"total": 1,
|
|
58
|
+
"completed": 1,
|
|
59
|
+
"failed": 0,
|
|
60
|
+
"cancelled": 0,
|
|
61
|
+
"available_from": 0,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"attempts": 1,
|
|
65
|
+
"available_from": 172_800,
|
|
66
|
+
"cache_hit": 1,
|
|
67
|
+
"cache_miss": 0,
|
|
68
|
+
"cache_unknown": 0,
|
|
69
|
+
"known_total_tokens": 1,
|
|
70
|
+
"input_tokens": 10,
|
|
71
|
+
"output_tokens": 2,
|
|
72
|
+
"cached_input_tokens": 5,
|
|
73
|
+
"reasoning_tokens": None,
|
|
74
|
+
"total_tokens": 12,
|
|
75
|
+
"paired_input_tokens": 10,
|
|
76
|
+
"paired_cached_tokens": 5,
|
|
77
|
+
},
|
|
78
|
+
[
|
|
79
|
+
{
|
|
80
|
+
"deployment_key": "ai.openai-compatible",
|
|
81
|
+
"model": "reported",
|
|
82
|
+
"attempts": 1,
|
|
83
|
+
"input_tokens": 10,
|
|
84
|
+
"output_tokens": 2,
|
|
85
|
+
"cached_input_tokens": 5,
|
|
86
|
+
"reasoning_tokens": None,
|
|
87
|
+
"total_tokens": 12,
|
|
88
|
+
"unknown_usage_count": 0,
|
|
89
|
+
}
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
{
|
|
93
|
+
"date": "1970-01-01",
|
|
94
|
+
"runs": 1,
|
|
95
|
+
"completed": 1,
|
|
96
|
+
"failed": 0,
|
|
97
|
+
"attempts": 0,
|
|
98
|
+
"known_token_attempts": 0,
|
|
99
|
+
"total_tokens": None,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"date": "1970-01-03",
|
|
103
|
+
"runs": 0,
|
|
104
|
+
"completed": 0,
|
|
105
|
+
"failed": 0,
|
|
106
|
+
"attempts": 1,
|
|
107
|
+
"known_token_attempts": 1,
|
|
108
|
+
"total_tokens": 12,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
90,
|
|
112
|
+
400,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
assert postgres == memory
|