create-windy 0.6.9 → 0.6.11
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/README.md +4 -1
- package/dist/cli.js +865 -305
- package/package.json +1 -1
- package/template/.windy-template.json +3 -3
- package/template/README.md +4 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +2 -0
- package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +19 -0
- package/template/apps/agent-server/tests/test_multimodal.py +30 -0
- package/template/apps/agent-server/tests/test_postgres_store.py +1 -0
- package/template/apps/agent-server/tests/test_report_routes.py +86 -0
- package/template/apps/agent-server/tests/test_report_store.py +62 -1
- package/template/apps/agent-server/tests/test_sqlite_store.py +1 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +1 -0
- package/template/apps/server/src/agent/run-audit.ts +1 -0
- package/template/apps/server/src/agent/run-contracts.ts +1 -0
- package/template/apps/server/src/agent/run-facade.ts +2 -2
- package/template/apps/server/src/agent/run-operation-registry.ts +7 -0
- package/template/apps/server/src/agent/run-output-schema.ts +15 -0
- package/template/apps/server/src/agent/run-release.ts +11 -0
- package/template/apps/server/src/agent/supervisor-output-schema.test.ts +95 -0
- package/template/apps/server/src/agent/vision-input.ts +37 -2
- package/template/apps/server/src/ai-runtime-report/remote-parser.ts +10 -0
- package/template/apps/server/src/ai-runtime-report/service.test.ts +1 -0
- package/template/apps/server/src/license/pairing-http.ts +138 -0
- package/template/apps/server/src/license/pairing-routes.test.ts +282 -0
- package/template/apps/server/src/license/pairing-routes.ts +257 -0
- package/template/apps/server/src/license/pairing-service.test.ts +320 -0
- package/template/apps/server/src/license/pairing-service.ts +321 -0
- package/template/apps/server/src/license/restriction.test.ts +11 -0
- package/template/apps/server/src/license/restriction.ts +5 -0
- package/template/apps/server/src/license/routes.ts +8 -1
- package/template/apps/server/src/license/runtime-service.ts +20 -0
- package/template/apps/web/package.json +6 -4
- package/template/apps/web/src/components/license/LicensePairingCard.vue +177 -0
- package/template/apps/web/src/components/license/LicensePairingCard.webtest.ts +113 -0
- package/template/apps/web/src/composables/useLicensePairing.ts +168 -0
- package/template/apps/web/src/composables/useLicenseRuntime.ts +11 -0
- package/template/apps/web/src/pages/license/LicensePage.vue +8 -0
- package/template/apps/web/src/pages/system/AiRuntimePage.vue +3 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +43 -31
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +15 -4
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.vue +48 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.webtest.ts +32 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +2 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.webtest.ts +70 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +29 -2
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.webtest.ts +56 -0
- package/template/apps/web/src/services/ai-runtime-api.ts +7 -10
- package/template/apps/web/src/services/ai-runtime-api.webtest.ts +115 -2
- package/template/apps/web/src/services/license-api.ts +60 -0
- package/template/apps/web/src/style.webtest.ts +5 -6
- package/template/docs/architecture/ai-runtime.md +5 -0
- package/template/docs/license/license-pairing-activation.md +67 -0
- package/template/docs/operations/incremental-delivery.md +6 -6
- package/template/docs/platform/agent-runtime-observability.md +6 -0
- package/template/docs/platform/agent-supervisor-output-contract.md +97 -0
- package/template/packages/ai-client/package.json +1 -1
- package/template/packages/ai-client/src/agent/contracts/final-answer.test.ts +67 -0
- package/template/packages/ai-client/src/agent/contracts/final-answer.ts +53 -0
- package/template/packages/ai-client/src/agent/contracts/index.ts +1 -0
- package/template/packages/modules/index.ts +1 -0
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-admin-ui-catalog.ts +5 -1
- package/template/packages/modules/src/system-agent-supervisor.test.ts +48 -0
- package/template/packages/modules/src/system-agent-supervisor.ts +101 -0
- package/template/packages/modules/src/system-api-control-plane.ts +37 -0
- package/template/packages/modules/src/system-license-pairing.test.ts +44 -0
- package/template/packages/modules/src/system-module-catalog.ts +8 -18
- package/template/packages/modules/src/system-module-menu-selection.ts +15 -0
- package/template/packages/modules/src/system-module-schema-catalog.ts +14 -0
- package/template/packages/modules/src/system-modules.ts +2 -0
- package/template/packages/server-sdk/index.ts +5 -0
- package/template/packages/server-sdk/package.json +3 -1
- package/template/packages/server-sdk/src/ai-operation-registration.ts +4 -0
- package/template/packages/server-sdk/src/system-agent-supervisor.test.ts +87 -0
- package/template/packages/server-sdk/src/system-agent-supervisor.ts +89 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/ai-runtime-report.ts +19 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"templateCommit": "
|
|
4
|
-
"generatorVersion": "0.6.
|
|
3
|
+
"templateCommit": "c1a0ff6aad5361fef49ab53d31a05c7b399d8130",
|
|
4
|
+
"generatorVersion": "0.6.11",
|
|
5
5
|
"modules": [
|
|
6
6
|
{
|
|
7
7
|
"name": "system",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"name": "system.agent",
|
|
36
|
-
"version": "0.1.
|
|
36
|
+
"version": "0.1.1"
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
"name": "system.operations",
|
package/template/README.md
CHANGED
|
@@ -35,6 +35,10 @@ Drizzle migration,再启动 Server 与 Web;不要跳过 migration 后直接
|
|
|
35
35
|
BuildKit 中的 Bun 包下载缓存,只下载新增或发生变化的依赖;冷安装每 15 秒输出一次
|
|
36
36
|
不含 registry 地址或凭据的进度心跳。
|
|
37
37
|
|
|
38
|
+
Windows 10/11 可在原生 PowerShell 中配合 Docker Desktop 直接运行 `bun run dev` 与
|
|
39
|
+
`bun run dev:build`,不需要 WSL。Windows 是本地开发宿主;生产交付仍使用 Linux
|
|
40
|
+
amd64 容器。
|
|
41
|
+
|
|
38
42
|
| 服务 | 地址 |
|
|
39
43
|
| ---- | ---- |
|
|
40
44
|
| Web | http://localhost:18746 |
|
|
@@ -135,6 +135,7 @@ class RuntimeRelease(CanonicalModel):
|
|
|
135
135
|
workflow_version: str = Field(min_length=1, max_length=80)
|
|
136
136
|
prompt_version: str = Field(min_length=1, max_length=80)
|
|
137
137
|
route_version: str = Field(min_length=1, max_length=80)
|
|
138
|
+
output_schema_version: str = Field(min_length=1, max_length=80)
|
|
138
139
|
tool_schema_version: str = Field(min_length=1, max_length=80)
|
|
139
140
|
manifest_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
|
140
141
|
|
|
@@ -166,6 +167,7 @@ def legacy_release() -> RuntimeRelease:
|
|
|
166
167
|
workflow_version="legacy",
|
|
167
168
|
prompt_version="legacy",
|
|
168
169
|
route_version="legacy",
|
|
170
|
+
output_schema_version="legacy",
|
|
169
171
|
tool_schema_version="legacy",
|
|
170
172
|
manifest_digest="0" * 64,
|
|
171
173
|
)
|
|
@@ -237,12 +237,29 @@ def run_value(run: RunReport, attempts: list[AttemptReport]) -> dict[str, object
|
|
|
237
237
|
"finishedAt": iso(run.finished_at),
|
|
238
238
|
"durationMs": run.duration_ms,
|
|
239
239
|
"attemptCount": len(attempts),
|
|
240
|
+
"modelDeployments": model_deployments(attempts),
|
|
240
241
|
"models": models,
|
|
241
242
|
"cacheStatus": cache,
|
|
242
243
|
"usage": usage_value(attempts),
|
|
243
244
|
}
|
|
244
245
|
|
|
245
246
|
|
|
247
|
+
def model_deployments(attempts: Iterable[AttemptReport]) -> list[dict[str, object]]:
|
|
248
|
+
"""按首次 Attempt 顺序保留真实的模型/deployment 组合及出现次数。"""
|
|
249
|
+
groups: dict[tuple[str, str], int] = {}
|
|
250
|
+
for item in attempts:
|
|
251
|
+
key = (item.deployment_key, item.reported_model or item.requested_model)
|
|
252
|
+
groups[key] = groups.get(key, 0) + 1
|
|
253
|
+
return [
|
|
254
|
+
{
|
|
255
|
+
"deploymentKey": deployment_key,
|
|
256
|
+
"model": model,
|
|
257
|
+
"attemptCount": attempt_count,
|
|
258
|
+
}
|
|
259
|
+
for (deployment_key, model), attempt_count in groups.items()
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
|
|
246
263
|
def usage_value(attempts: list[AttemptReport]) -> dict[str, int | None]:
|
|
247
264
|
def total(field: str) -> int | None:
|
|
248
265
|
values = [
|
|
@@ -267,6 +284,8 @@ def attempt_value(item: AttemptReport) -> dict[str, object]:
|
|
|
267
284
|
"sequence": item.sequence,
|
|
268
285
|
"toolRound": item.tool_round,
|
|
269
286
|
"deploymentKey": item.deployment_key,
|
|
287
|
+
"requestedModel": item.requested_model,
|
|
288
|
+
"reportedModel": item.reported_model,
|
|
270
289
|
"model": item.reported_model or item.requested_model,
|
|
271
290
|
"outcome": item.outcome,
|
|
272
291
|
"cacheStatus": item.usage.cache_status,
|
|
@@ -49,3 +49,33 @@ def test_typed_image_part_and_json_schema_output() -> None:
|
|
|
49
49
|
with pytest.raises(SafeEngineError) as invalid:
|
|
50
50
|
final_output('{"name":42}', schema)
|
|
51
51
|
assert invalid.value.code == "OUTPUT_VALIDATION_FAILED"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_supervisor_final_answer_schema_rejects_intermediate_text() -> None:
|
|
55
|
+
schema: dict[str, JsonValue] = {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"properties": {
|
|
58
|
+
"finalAnswer": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"properties": {
|
|
61
|
+
"schemaVersion": {"const": 1},
|
|
62
|
+
"text": {"type": "string", "minLength": 1},
|
|
63
|
+
},
|
|
64
|
+
"required": ["schemaVersion", "text"],
|
|
65
|
+
"additionalProperties": False,
|
|
66
|
+
},
|
|
67
|
+
"nodes": {"type": "array"},
|
|
68
|
+
},
|
|
69
|
+
"required": ["finalAnswer"],
|
|
70
|
+
"additionalProperties": False,
|
|
71
|
+
}
|
|
72
|
+
assert final_output(
|
|
73
|
+
'{"finalAnswer":{"schemaVersion":1,"text":"最终回答"},"nodes":[]}',
|
|
74
|
+
schema,
|
|
75
|
+
) == {
|
|
76
|
+
"finalAnswer": {"schemaVersion": 1, "text": "最终回答"},
|
|
77
|
+
"nodes": [],
|
|
78
|
+
}
|
|
79
|
+
with pytest.raises(SafeEngineError) as invalid:
|
|
80
|
+
final_output('{"nodes":[{"text":"中间节点","total":2}]}', schema)
|
|
81
|
+
assert invalid.value.code == "OUTPUT_VALIDATION_FAILED"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
import pytest
|
|
5
|
+
from fastapi import FastAPI
|
|
6
|
+
|
|
7
|
+
from southwind_agent_server.contracts import RunReportingContext, StartRunRequest
|
|
8
|
+
from southwind_agent_server.report_routes import create_report_router
|
|
9
|
+
from southwind_agent_server.report_store import InMemoryAgentRuntimeReportStore
|
|
10
|
+
from southwind_agent_server.report_types import UsageFacts
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def test_internal_run_routes_project_model_deployments_and_facts(
|
|
14
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
15
|
+
) -> None:
|
|
16
|
+
monkeypatch.setattr("southwind_agent_server.report_routes.time.time", lambda: 400.0)
|
|
17
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
18
|
+
await store.ensure_run(
|
|
19
|
+
"run_1",
|
|
20
|
+
StartRunRequest(
|
|
21
|
+
operation_key="business.answer",
|
|
22
|
+
input=None,
|
|
23
|
+
reporting=RunReportingContext(
|
|
24
|
+
actor_id="actor_1", actor_type="user", department_id=None
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
100.0,
|
|
28
|
+
)
|
|
29
|
+
attempt = await store.start_attempt(
|
|
30
|
+
"run_1", 1, "ai.primary", "requested-model", 101.0
|
|
31
|
+
)
|
|
32
|
+
await store.finish_attempt(
|
|
33
|
+
attempt.attempt_id,
|
|
34
|
+
"completed",
|
|
35
|
+
UsageFacts(total_tokens=12),
|
|
36
|
+
102.0,
|
|
37
|
+
"provider-model-v2",
|
|
38
|
+
)
|
|
39
|
+
application = FastAPI()
|
|
40
|
+
application.include_router(create_report_router(store, "internal-token"))
|
|
41
|
+
|
|
42
|
+
async with httpx.AsyncClient(
|
|
43
|
+
transport=httpx.ASGITransport(app=application), base_url="http://test"
|
|
44
|
+
) as client:
|
|
45
|
+
headers = {"Authorization": "Bearer internal-token"}
|
|
46
|
+
listed = await client.get(
|
|
47
|
+
"/api/internal/ai-runtime-report/runs?range=retained&scopeMode=all",
|
|
48
|
+
headers=headers,
|
|
49
|
+
)
|
|
50
|
+
detail = await client.get(
|
|
51
|
+
"/api/internal/ai-runtime-report/runs/run_1", headers=headers
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
assert listed.status_code == 200
|
|
55
|
+
assert listed.json()["items"][0]["modelDeployments"] == [
|
|
56
|
+
{
|
|
57
|
+
"deploymentKey": "ai.primary",
|
|
58
|
+
"model": "provider-model-v2",
|
|
59
|
+
"attemptCount": 1,
|
|
60
|
+
}
|
|
61
|
+
]
|
|
62
|
+
assert detail.status_code == 200
|
|
63
|
+
assert detail.json()["attempts"] == [
|
|
64
|
+
{
|
|
65
|
+
"attemptId": attempt.attempt_id,
|
|
66
|
+
"sequence": 1,
|
|
67
|
+
"toolRound": 1,
|
|
68
|
+
"deploymentKey": "ai.primary",
|
|
69
|
+
"requestedModel": "requested-model",
|
|
70
|
+
"reportedModel": "provider-model-v2",
|
|
71
|
+
"model": "provider-model-v2",
|
|
72
|
+
"outcome": "completed",
|
|
73
|
+
"cacheStatus": "unknown",
|
|
74
|
+
"usageSource": "unknown",
|
|
75
|
+
"inputTokens": None,
|
|
76
|
+
"outputTokens": None,
|
|
77
|
+
"cachedInputTokens": None,
|
|
78
|
+
"reasoningTokens": None,
|
|
79
|
+
"totalTokens": 12,
|
|
80
|
+
"firstTokenLatencyMs": None,
|
|
81
|
+
"durationMs": 1000,
|
|
82
|
+
"safeErrorCode": None,
|
|
83
|
+
"startedAt": "1970-01-01T00:01:41Z",
|
|
84
|
+
"finishedAt": "1970-01-01T00:01:42Z",
|
|
85
|
+
}
|
|
86
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Literal
|
|
3
|
+
from typing import Any, Literal, cast
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
@@ -284,3 +284,64 @@ async def test_completely_unknown_aggregate_tokens_stay_null() -> None:
|
|
|
284
284
|
}
|
|
285
285
|
assert summary["byModel"][0]["totalTokens"] is None # type: ignore[index]
|
|
286
286
|
assert summary["activity"][0]["totalTokens"] is None # type: ignore[index]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
async def test_run_projection_keeps_model_deployment_combinations_and_facts() -> None:
|
|
290
|
+
store = InMemoryAgentRuntimeReportStore(now=lambda: 400.0)
|
|
291
|
+
await store.ensure_run("run_empty", request("actor_1"), 90.0)
|
|
292
|
+
await store.ensure_run("run_1", request("actor_1"), 100.0)
|
|
293
|
+
|
|
294
|
+
first = await store.start_attempt(
|
|
295
|
+
"run_1", 1, "ai.primary", "requested-primary", 101.0
|
|
296
|
+
)
|
|
297
|
+
await store.start_attempt("run_1", 2, "ai.fallback", "requested-fallback", 102.0)
|
|
298
|
+
third = await store.start_attempt(
|
|
299
|
+
"run_1", 3, "ai.primary", "requested-primary", 103.0
|
|
300
|
+
)
|
|
301
|
+
await store.finish_attempt(
|
|
302
|
+
first.attempt_id,
|
|
303
|
+
"completed",
|
|
304
|
+
UsageFacts(),
|
|
305
|
+
104.0,
|
|
306
|
+
"provider-primary-v2",
|
|
307
|
+
)
|
|
308
|
+
await store.finish_attempt(third.attempt_id, "failed", UsageFacts(), 105.0)
|
|
309
|
+
|
|
310
|
+
page = await store.list_runs(ReportFilter(0, 500), 1, 20)
|
|
311
|
+
items = cast(list[dict[str, Any]], page["items"])
|
|
312
|
+
item = items[0]
|
|
313
|
+
assert item["modelDeployments"] == [
|
|
314
|
+
{
|
|
315
|
+
"deploymentKey": "ai.primary",
|
|
316
|
+
"model": "provider-primary-v2",
|
|
317
|
+
"attemptCount": 1,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"deploymentKey": "ai.fallback",
|
|
321
|
+
"model": "requested-fallback",
|
|
322
|
+
"attemptCount": 1,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"deploymentKey": "ai.primary",
|
|
326
|
+
"model": "requested-primary",
|
|
327
|
+
"attemptCount": 1,
|
|
328
|
+
},
|
|
329
|
+
]
|
|
330
|
+
assert items[1]["modelDeployments"] == []
|
|
331
|
+
|
|
332
|
+
detail = await store.get_run("run_1")
|
|
333
|
+
assert detail is not None
|
|
334
|
+
attempts = cast(list[dict[str, Any]], detail["attempts"])
|
|
335
|
+
assert [
|
|
336
|
+
(
|
|
337
|
+
attempt["sequence"],
|
|
338
|
+
attempt["deploymentKey"],
|
|
339
|
+
attempt["requestedModel"],
|
|
340
|
+
attempt["reportedModel"],
|
|
341
|
+
)
|
|
342
|
+
for attempt in attempts
|
|
343
|
+
] == [
|
|
344
|
+
(1, "ai.primary", "requested-primary", "provider-primary-v2"),
|
|
345
|
+
(2, "ai.fallback", "requested-fallback", None),
|
|
346
|
+
(3, "ai.primary", "requested-primary", None),
|
|
347
|
+
]
|
|
@@ -174,6 +174,7 @@ def fixture(run_id: str, idempotency_key: str) -> RunRecord:
|
|
|
174
174
|
"workflowVersion": "workflow-1",
|
|
175
175
|
"promptVersion": "prompt-1",
|
|
176
176
|
"routeVersion": "route-1",
|
|
177
|
+
"outputSchemaVersion": "output-schema-1",
|
|
177
178
|
"toolSchemaVersion": "schema-1",
|
|
178
179
|
"manifestDigest": "0" * 64,
|
|
179
180
|
},
|
|
@@ -210,6 +210,7 @@ function runtimeRelease(): AgentRuntimeOperation["release"] {
|
|
|
210
210
|
workflowVersion: "workflow-1",
|
|
211
211
|
promptVersion: "prompt-1",
|
|
212
212
|
routeVersion: "route-1",
|
|
213
|
+
outputSchemaVersion: "output-schema-1",
|
|
213
214
|
toolSchemaVersion: "schema-1",
|
|
214
215
|
manifestDigest: "0".repeat(64),
|
|
215
216
|
};
|
|
@@ -41,6 +41,7 @@ export function auditAgentRun(input: {
|
|
|
41
41
|
workflowVersion: input.release.workflowVersion,
|
|
42
42
|
promptVersion: input.release.promptVersion,
|
|
43
43
|
routeVersion: input.release.routeVersion,
|
|
44
|
+
outputSchemaVersion: input.release.outputSchemaVersion,
|
|
44
45
|
toolSchemaVersion: input.release.toolSchemaVersion,
|
|
45
46
|
traceId: input.traceId,
|
|
46
47
|
},
|
|
@@ -56,6 +56,7 @@ export interface AgentRuntimeRelease {
|
|
|
56
56
|
readonly workflowVersion: string;
|
|
57
57
|
readonly promptVersion: string;
|
|
58
58
|
readonly routeVersion: string;
|
|
59
|
+
readonly outputSchemaVersion: string;
|
|
59
60
|
readonly toolSchemaVersion: string;
|
|
60
61
|
readonly manifestDigest: string;
|
|
61
62
|
}
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
} from "./provider-input-governance.js";
|
|
31
31
|
import {
|
|
32
32
|
rotateVisionArtifacts,
|
|
33
|
-
validateOutputSchema,
|
|
34
33
|
validateVisionArtifacts,
|
|
35
34
|
} from "./vision-input.js";
|
|
36
35
|
import { requireVisionOperationReadiness } from "./vision-readiness.js";
|
|
@@ -55,6 +54,7 @@ import {
|
|
|
55
54
|
buildRegisteredOperations,
|
|
56
55
|
type RegisteredOperation,
|
|
57
56
|
} from "./run-operation-registry.js";
|
|
57
|
+
import { validateRegisteredOutputSchema } from "./run-output-schema.js";
|
|
58
58
|
export { AgentRunFacadeError } from "./run-errors.js";
|
|
59
59
|
|
|
60
60
|
type ServerRuntime = ReturnType<typeof createServerRuntime>;
|
|
@@ -157,7 +157,7 @@ export class AgentRunFacade {
|
|
|
157
157
|
400,
|
|
158
158
|
);
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
validateRegisteredOutputSchema(plan.outputSchema, operation);
|
|
161
161
|
const rotatedArtifacts = await rotateVisionArtifacts(
|
|
162
162
|
artifacts,
|
|
163
163
|
operation.definition,
|
|
@@ -13,6 +13,7 @@ export interface RegisteredOperation {
|
|
|
13
13
|
feature: ReturnType<typeof findFeature>;
|
|
14
14
|
tools: AgentRuntimeOperation["allowedTools"];
|
|
15
15
|
release: AgentRuntimeOperation["release"];
|
|
16
|
+
outputSchema?: NonNullable<ModuleManifest["jsonSchemas"]>[number]["schema"];
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function buildRegisteredOperations(
|
|
@@ -26,6 +27,11 @@ export function buildRegisteredOperations(
|
|
|
26
27
|
]),
|
|
27
28
|
);
|
|
28
29
|
const features = modules.flatMap(({ features }) => features);
|
|
30
|
+
const schemas = new Map(
|
|
31
|
+
modules.flatMap(({ jsonSchemas }) =>
|
|
32
|
+
(jsonSchemas ?? []).map((schema) => [schema.key, schema.schema] as const),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
29
35
|
return new Map(
|
|
30
36
|
modules.flatMap((module) =>
|
|
31
37
|
(module.aiOperations ?? []).map((definition) => {
|
|
@@ -37,6 +43,7 @@ export function buildRegisteredOperations(
|
|
|
37
43
|
...handler,
|
|
38
44
|
feature: findFeature(features, definition.featureKey),
|
|
39
45
|
tools: toolDefinitions(modules, definition.allowedToolKeys),
|
|
46
|
+
outputSchema: schemas.get(definition.outputSchemaKey),
|
|
40
47
|
release: buildAgentRuntimeRelease(
|
|
41
48
|
modules,
|
|
42
49
|
module,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
|
|
2
|
+
import type { RegisteredOperation } from "./run-operation-registry.js";
|
|
3
|
+
import { validateOutputSchema } from "./vision-input.js";
|
|
4
|
+
|
|
5
|
+
export function validateRegisteredOutputSchema(
|
|
6
|
+
schema: ModuleAiJsonValue | undefined,
|
|
7
|
+
operation: RegisteredOperation,
|
|
8
|
+
): void {
|
|
9
|
+
validateOutputSchema(
|
|
10
|
+
schema,
|
|
11
|
+
operation.definition,
|
|
12
|
+
operation.registration?.supervisor,
|
|
13
|
+
operation.outputSchema,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -20,6 +20,7 @@ export function buildAgentRuntimeRelease(
|
|
|
20
20
|
const coordinates = {
|
|
21
21
|
operationVersion: module.version,
|
|
22
22
|
...versions,
|
|
23
|
+
outputSchemaVersion: outputSchemaDigest(modules, definition),
|
|
23
24
|
toolSchemaVersion: operationSchemaDigest(modules, definition),
|
|
24
25
|
manifestDigest: digest(module),
|
|
25
26
|
};
|
|
@@ -29,6 +30,16 @@ export function buildAgentRuntimeRelease(
|
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function outputSchemaDigest(
|
|
34
|
+
modules: readonly ModuleManifest[],
|
|
35
|
+
definition: ModuleAiOperationDefinition,
|
|
36
|
+
): string {
|
|
37
|
+
const schema = modules
|
|
38
|
+
.flatMap(({ jsonSchemas }) => jsonSchemas ?? [])
|
|
39
|
+
.find(({ key }) => key === definition.outputSchemaKey)?.schema;
|
|
40
|
+
return digest(schema);
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
function operationSchemaDigest(
|
|
33
44
|
modules: readonly ModuleManifest[],
|
|
34
45
|
definition: ModuleAiOperationDefinition,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
composeSystemAgentFinalAnswerSchemaV1,
|
|
4
|
+
type ModuleAiOperationDefinition,
|
|
5
|
+
} from "@southwind-ai/modules";
|
|
6
|
+
import { validateOutputSchema } from "./vision-input.js";
|
|
7
|
+
|
|
8
|
+
const definition: ModuleAiOperationDefinition = {
|
|
9
|
+
key: "business.copilot",
|
|
10
|
+
kind: "agent",
|
|
11
|
+
featureKey: "business.copilot",
|
|
12
|
+
permissionKey: "business.copilot.execute",
|
|
13
|
+
auditAction: "business.copilot.execute",
|
|
14
|
+
inputSchemaKey: "business.copilot.input.v1",
|
|
15
|
+
outputSchemaKey: "business.copilot.output.v1",
|
|
16
|
+
requiredCapabilities: ["text-generation"],
|
|
17
|
+
allowedToolKeys: [],
|
|
18
|
+
dataClassification: "internal",
|
|
19
|
+
execution: "interactive",
|
|
20
|
+
providerEgress: { mode: "configured-endpoint", deploymentKey: "ai.default" },
|
|
21
|
+
providerDataPolicy: {
|
|
22
|
+
inputFields: [
|
|
23
|
+
{
|
|
24
|
+
jsonPointer: "/prompt",
|
|
25
|
+
classification: "internal",
|
|
26
|
+
dlpAction: "redact",
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
envelopeFields: [
|
|
30
|
+
{
|
|
31
|
+
field: "instructions",
|
|
32
|
+
classification: "internal",
|
|
33
|
+
dlpAction: "redact",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
field: "user-prompt",
|
|
37
|
+
classification: "internal",
|
|
38
|
+
dlpAction: "redact",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
field: "output-schema",
|
|
42
|
+
classification: "internal",
|
|
43
|
+
dlpAction: "redact",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
toolNetworkPolicy: { mode: "forbidden" },
|
|
48
|
+
limits: {
|
|
49
|
+
maxDurationSeconds: 60,
|
|
50
|
+
maxInputBytes: 4_096,
|
|
51
|
+
maxOutputBytes: 1_048_576,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const supervisor = {
|
|
56
|
+
contractVersion: 1,
|
|
57
|
+
finalAnswerSchemaVersion: 1,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
describe("Supervisor output schema admission", () => {
|
|
61
|
+
test("只接受与 Manifest 完全一致的 canonical Schema", () => {
|
|
62
|
+
const schema = composeSystemAgentFinalAnswerSchemaV1({
|
|
63
|
+
properties: { citations: { type: "array" } },
|
|
64
|
+
});
|
|
65
|
+
expect(() =>
|
|
66
|
+
validateOutputSchema(schema, definition, supervisor, schema),
|
|
67
|
+
).not.toThrow();
|
|
68
|
+
expect(() =>
|
|
69
|
+
validateOutputSchema(
|
|
70
|
+
composeSystemAgentFinalAnswerSchemaV1(),
|
|
71
|
+
definition,
|
|
72
|
+
supervisor,
|
|
73
|
+
schema,
|
|
74
|
+
),
|
|
75
|
+
).toThrow("与 Manifest 声明不一致");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("拒绝缺失 Schema 或 legacy text Schema", () => {
|
|
79
|
+
expect(() =>
|
|
80
|
+
validateOutputSchema(undefined, definition, supervisor),
|
|
81
|
+
).toThrow("必须提供 typed 输出 Schema");
|
|
82
|
+
expect(() =>
|
|
83
|
+
validateOutputSchema(
|
|
84
|
+
{
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: { text: { type: "string" } },
|
|
87
|
+
required: ["text"],
|
|
88
|
+
additionalProperties: false,
|
|
89
|
+
},
|
|
90
|
+
definition,
|
|
91
|
+
supervisor,
|
|
92
|
+
),
|
|
93
|
+
).toThrow("canonical finalAnswer V1");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
assertSystemAgentFinalAnswerSchemaV1,
|
|
3
|
+
type ModuleAiOperationDefinition,
|
|
4
|
+
} from "@southwind-ai/modules";
|
|
2
5
|
import type {
|
|
3
6
|
ModuleAiArtifactRef,
|
|
7
|
+
ModuleAiOperationHandlerRegistration,
|
|
4
8
|
ModuleAiJsonValue,
|
|
5
9
|
} from "@southwind-ai/server-sdk";
|
|
6
10
|
import type { AiArtifactRuntime } from "../ai-artifact/runtime.js";
|
|
@@ -56,8 +60,13 @@ export async function rotateVisionArtifacts(
|
|
|
56
60
|
export function validateOutputSchema(
|
|
57
61
|
schema: ModuleAiJsonValue | undefined,
|
|
58
62
|
definition: ModuleAiOperationDefinition,
|
|
63
|
+
supervisor?: ModuleAiOperationHandlerRegistration["supervisor"],
|
|
64
|
+
declaredSchema?: ModuleAiJsonValue,
|
|
59
65
|
) {
|
|
60
|
-
if (schema === undefined)
|
|
66
|
+
if (schema === undefined) {
|
|
67
|
+
if (supervisor) throw invalid("Supervisor 必须提供 typed 输出 Schema");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
61
70
|
if (
|
|
62
71
|
!schema ||
|
|
63
72
|
typeof schema !== "object" ||
|
|
@@ -70,6 +79,16 @@ export function validateOutputSchema(
|
|
|
70
79
|
400,
|
|
71
80
|
);
|
|
72
81
|
}
|
|
82
|
+
if (supervisor) {
|
|
83
|
+
try {
|
|
84
|
+
assertSystemAgentFinalAnswerSchemaV1(schema);
|
|
85
|
+
} catch {
|
|
86
|
+
throw invalid("Supervisor 输出 Schema 缺少 canonical finalAnswer V1");
|
|
87
|
+
}
|
|
88
|
+
if (canonicalJson(schema) !== canonicalJson(declaredSchema)) {
|
|
89
|
+
throw invalid("Supervisor 运行时输出 Schema 与 Manifest 声明不一致");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
73
92
|
if (
|
|
74
93
|
definition.providerEgress?.mode === "configured-endpoint" &&
|
|
75
94
|
!definition.providerDataPolicy?.envelopeFields.some(
|
|
@@ -84,6 +103,22 @@ export function validateOutputSchema(
|
|
|
84
103
|
}
|
|
85
104
|
}
|
|
86
105
|
|
|
106
|
+
function canonicalJson(value: unknown): string {
|
|
107
|
+
return JSON.stringify(canonicalValue(value));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function canonicalValue(value: unknown): unknown {
|
|
111
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
112
|
+
if (value !== null && typeof value === "object") {
|
|
113
|
+
return Object.fromEntries(
|
|
114
|
+
Object.entries(value as Record<string, unknown>)
|
|
115
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
116
|
+
.map(([key, item]) => [key, canonicalValue(item)]),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
87
122
|
export function validateVisionArtifacts(
|
|
88
123
|
artifacts: readonly Pick<
|
|
89
124
|
ModuleAiArtifactRef,
|
|
@@ -111,6 +111,14 @@ function parseReportRun(value: unknown): InternalAiRuntimeRunListItem {
|
|
|
111
111
|
finishedAt: nullableText(item.finishedAt),
|
|
112
112
|
durationMs: nullableInteger(item.durationMs),
|
|
113
113
|
attemptCount: integer(item.attemptCount),
|
|
114
|
+
modelDeployments: array(item.modelDeployments).map((modelDeployment) => {
|
|
115
|
+
const combination = record(modelDeployment);
|
|
116
|
+
return {
|
|
117
|
+
deploymentKey: text(combination.deploymentKey),
|
|
118
|
+
model: text(combination.model),
|
|
119
|
+
attemptCount: integer(combination.attemptCount),
|
|
120
|
+
};
|
|
121
|
+
}),
|
|
114
122
|
models: array(item.models).map(text),
|
|
115
123
|
cacheStatus: oneOf(item.cacheStatus, ["hit", "miss", "unknown"]),
|
|
116
124
|
usage: parseUsage(item.usage),
|
|
@@ -124,6 +132,8 @@ function parseAttempt(value: unknown): AiRuntimeAttemptDetail {
|
|
|
124
132
|
sequence: integer(item.sequence),
|
|
125
133
|
toolRound: integer(item.toolRound),
|
|
126
134
|
deploymentKey: text(item.deploymentKey),
|
|
135
|
+
requestedModel: text(item.requestedModel),
|
|
136
|
+
reportedModel: nullableText(item.reportedModel),
|
|
127
137
|
model: text(item.model),
|
|
128
138
|
outcome: oneOf(item.outcome, [
|
|
129
139
|
"running",
|