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.
Files changed (78) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +865 -305
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +3 -3
  5. package/template/README.md +4 -0
  6. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +2 -0
  7. package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +19 -0
  8. package/template/apps/agent-server/tests/test_multimodal.py +30 -0
  9. package/template/apps/agent-server/tests/test_postgres_store.py +1 -0
  10. package/template/apps/agent-server/tests/test_report_routes.py +86 -0
  11. package/template/apps/agent-server/tests/test_report_store.py +62 -1
  12. package/template/apps/agent-server/tests/test_sqlite_store.py +1 -0
  13. package/template/apps/server/src/agent/remote-runtime.test.ts +1 -0
  14. package/template/apps/server/src/agent/run-audit.ts +1 -0
  15. package/template/apps/server/src/agent/run-contracts.ts +1 -0
  16. package/template/apps/server/src/agent/run-facade.ts +2 -2
  17. package/template/apps/server/src/agent/run-operation-registry.ts +7 -0
  18. package/template/apps/server/src/agent/run-output-schema.ts +15 -0
  19. package/template/apps/server/src/agent/run-release.ts +11 -0
  20. package/template/apps/server/src/agent/supervisor-output-schema.test.ts +95 -0
  21. package/template/apps/server/src/agent/vision-input.ts +37 -2
  22. package/template/apps/server/src/ai-runtime-report/remote-parser.ts +10 -0
  23. package/template/apps/server/src/ai-runtime-report/service.test.ts +1 -0
  24. package/template/apps/server/src/license/pairing-http.ts +138 -0
  25. package/template/apps/server/src/license/pairing-routes.test.ts +282 -0
  26. package/template/apps/server/src/license/pairing-routes.ts +257 -0
  27. package/template/apps/server/src/license/pairing-service.test.ts +320 -0
  28. package/template/apps/server/src/license/pairing-service.ts +321 -0
  29. package/template/apps/server/src/license/restriction.test.ts +11 -0
  30. package/template/apps/server/src/license/restriction.ts +5 -0
  31. package/template/apps/server/src/license/routes.ts +8 -1
  32. package/template/apps/server/src/license/runtime-service.ts +20 -0
  33. package/template/apps/web/package.json +6 -4
  34. package/template/apps/web/src/components/license/LicensePairingCard.vue +177 -0
  35. package/template/apps/web/src/components/license/LicensePairingCard.webtest.ts +113 -0
  36. package/template/apps/web/src/composables/useLicensePairing.ts +168 -0
  37. package/template/apps/web/src/composables/useLicenseRuntime.ts +11 -0
  38. package/template/apps/web/src/pages/license/LicensePage.vue +8 -0
  39. package/template/apps/web/src/pages/system/AiRuntimePage.vue +3 -1
  40. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +43 -31
  41. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +15 -4
  42. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.vue +48 -0
  43. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeModelUsageList.webtest.ts +32 -0
  44. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +2 -1
  45. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.webtest.ts +70 -0
  46. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +29 -2
  47. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.webtest.ts +56 -0
  48. package/template/apps/web/src/services/ai-runtime-api.ts +7 -10
  49. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +115 -2
  50. package/template/apps/web/src/services/license-api.ts +60 -0
  51. package/template/apps/web/src/style.webtest.ts +5 -6
  52. package/template/docs/architecture/ai-runtime.md +5 -0
  53. package/template/docs/license/license-pairing-activation.md +67 -0
  54. package/template/docs/operations/incremental-delivery.md +6 -6
  55. package/template/docs/platform/agent-runtime-observability.md +6 -0
  56. package/template/docs/platform/agent-supervisor-output-contract.md +97 -0
  57. package/template/packages/ai-client/package.json +1 -1
  58. package/template/packages/ai-client/src/agent/contracts/final-answer.test.ts +67 -0
  59. package/template/packages/ai-client/src/agent/contracts/final-answer.ts +53 -0
  60. package/template/packages/ai-client/src/agent/contracts/index.ts +1 -0
  61. package/template/packages/modules/index.ts +1 -0
  62. package/template/packages/modules/package.json +1 -1
  63. package/template/packages/modules/src/system-admin-ui-catalog.ts +5 -1
  64. package/template/packages/modules/src/system-agent-supervisor.test.ts +48 -0
  65. package/template/packages/modules/src/system-agent-supervisor.ts +101 -0
  66. package/template/packages/modules/src/system-api-control-plane.ts +37 -0
  67. package/template/packages/modules/src/system-license-pairing.test.ts +44 -0
  68. package/template/packages/modules/src/system-module-catalog.ts +8 -18
  69. package/template/packages/modules/src/system-module-menu-selection.ts +15 -0
  70. package/template/packages/modules/src/system-module-schema-catalog.ts +14 -0
  71. package/template/packages/modules/src/system-modules.ts +2 -0
  72. package/template/packages/server-sdk/index.ts +5 -0
  73. package/template/packages/server-sdk/package.json +3 -1
  74. package/template/packages/server-sdk/src/ai-operation-registration.ts +4 -0
  75. package/template/packages/server-sdk/src/system-agent-supervisor.test.ts +87 -0
  76. package/template/packages/server-sdk/src/system-agent-supervisor.ts +89 -0
  77. package/template/packages/shared/package.json +1 -1
  78. package/template/packages/shared/src/ai-runtime-report.ts +19 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "9176c4dc2db60979b5ce7fd89639da90648c49b9",
4
- "generatorVersion": "0.6.9",
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.0"
36
+ "version": "0.1.1"
37
37
  },
38
38
  {
39
39
  "name": "system.operations",
@@ -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"
@@ -96,6 +96,7 @@ def fixture(suffix: str) -> RunRecord:
96
96
  "workflowVersion": "workflow-1",
97
97
  "promptVersion": "prompt-1",
98
98
  "routeVersion": "route-1",
99
+ "outputSchemaVersion": "output-schema-1",
99
100
  "toolSchemaVersion": "schema-1",
100
101
  "manifestDigest": "0" * 64,
101
102
  },
@@ -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
- validateOutputSchema(plan.outputSchema, operation.definition);
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 type { ModuleAiOperationDefinition } from "@southwind-ai/modules";
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) return;
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",
@@ -254,6 +254,7 @@ function runDetail(departmentId: string): InternalAiRuntimeRunDetail {
254
254
  finishedAt: "2026-07-31T00:00:01.000Z",
255
255
  durationMs: 1_000,
256
256
  attemptCount: 0,
257
+ modelDeployments: [],
257
258
  models: [],
258
259
  cacheStatus: "unknown",
259
260
  usage: {