create-windy 0.8.2 → 0.8.3

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 (86) hide show
  1. package/dist/cli.js +18 -1
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/server/src/agent/admin-catalog-bootstrap.ts +25 -0
  5. package/template/apps/server/src/agent/agent-session-bootstrap.ts +7 -8
  6. package/template/apps/server/src/agent/agent-session-facade.ts +12 -0
  7. package/template/apps/server/src/agent/agent-session-validation.ts +43 -0
  8. package/template/apps/server/src/agent/operation-catalog.test.ts +202 -0
  9. package/template/apps/server/src/agent/operation-catalog.ts +135 -0
  10. package/template/apps/server/src/agent/run-browser-visibility.test.ts +59 -0
  11. package/template/apps/server/src/agent/run-browser-visibility.ts +11 -0
  12. package/template/apps/server/src/agent/run-operation-policy.ts +1 -0
  13. package/template/apps/server/src/agent/run-route-response.ts +35 -0
  14. package/template/apps/server/src/agent/run-routes.ts +46 -43
  15. package/template/apps/server/src/agent/runtime-bootstrap.ts +21 -20
  16. package/template/apps/server/src/agent/runtime-disabled-composition.ts +28 -0
  17. package/template/apps/server/src/agent/session-projection-facade.ts +8 -9
  18. package/template/apps/server/src/agent/session-projection-governance.ts +10 -0
  19. package/template/apps/server/src/agent/session-projection-view.ts +19 -16
  20. package/template/apps/server/src/agent/session-recording-reader.contract.test.ts +131 -0
  21. package/template/apps/server/src/agent/session-recording-reader.ts +62 -0
  22. package/template/apps/server/src/agent/tool-catalog-audit.ts +22 -0
  23. package/template/apps/server/src/agent/tool-catalog-availability.ts +21 -0
  24. package/template/apps/server/src/agent/tool-catalog-routes.test.ts +103 -0
  25. package/template/apps/server/src/agent/tool-catalog-routes.ts +62 -0
  26. package/template/apps/server/src/agent/tool-catalog.test.ts +122 -0
  27. package/template/apps/server/src/agent/tool-catalog.ts +44 -0
  28. package/template/apps/server/src/agent/tool-registry.ts +45 -14
  29. package/template/apps/server/src/agent/workbench-bootstrap.ts +39 -0
  30. package/template/apps/server/src/agent/workbench-event-projection.ts +115 -0
  31. package/template/apps/server/src/agent/workbench-facade.contract.test.ts +189 -0
  32. package/template/apps/server/src/agent/workbench-facade.ts +339 -0
  33. package/template/apps/server/src/agent/workbench-recording.contract.test.ts +328 -0
  34. package/template/apps/server/src/agent/workbench-routes.ts +97 -0
  35. package/template/apps/server/src/agent/workbench-run-integration.test.ts +231 -0
  36. package/template/apps/server/src/agent/workbench-run-reader.ts +187 -0
  37. package/template/apps/server/src/agent/workbench-safe-result.ts +61 -0
  38. package/template/apps/server/src/agent/workbench-sse.ts +50 -0
  39. package/template/apps/server/src/agent-workflow/query-service.ts +36 -0
  40. package/template/apps/server/src/agent-workflow/recording-projection.contract.test.ts +196 -0
  41. package/template/apps/server/src/agent-workflow/recording-projection.ts +86 -0
  42. package/template/apps/web/src/pages/system/AgentDebugPage.vue +14 -0
  43. package/template/apps/web/src/pages/system/AgentSessionPage.vue +29 -8
  44. package/template/apps/web/src/pages/system/AgentToolsPage.vue +76 -0
  45. package/template/apps/web/src/pages/system/components/agent-workbench/AgentWorkbench.vue +236 -0
  46. package/template/apps/web/src/pages/system/components/agent-workbench/DraftLeaveDialog.vue +34 -0
  47. package/template/apps/web/src/pages/system/components/agent-workbench/ReplayEventDetail.vue +175 -0
  48. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +142 -0
  49. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.webtest.ts +242 -0
  50. package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +97 -0
  51. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +100 -0
  52. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +55 -0
  53. package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +81 -0
  54. package/template/apps/web/src/pages/system/components/agent-workbench/WorkbenchSessionList.vue +57 -0
  55. package/template/apps/web/src/pages/system/components/agent-workbench/catalog-presenter.ts +20 -0
  56. package/template/apps/web/src/pages/system/components/agent-workbench/replay-presenter.ts +94 -0
  57. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.ts +298 -0
  58. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.webtest.ts +164 -0
  59. package/template/apps/web/src/pages/system/composables/useSessionReplay.ts +48 -0
  60. package/template/apps/web/src/router/index.ts +4 -0
  61. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  62. package/template/apps/web/src/services/agent-admin-api.ts +155 -0
  63. package/template/apps/web/src/services/agent-admin-api.webtest.ts +120 -0
  64. package/template/apps/web/src/services/agent-workbench-event.ts +60 -0
  65. package/template/docs/architecture/agent-harness.md +4 -0
  66. package/template/packages/modules/src/ai-operation-composition.ts +9 -0
  67. package/template/packages/modules/src/ai-operation-visibility.test.ts +33 -0
  68. package/template/packages/modules/src/manifest.ts +2 -0
  69. package/template/packages/modules/src/system-admin-routes.ts +16 -0
  70. package/template/packages/modules/src/system-admin-ui-catalog.ts +7 -1
  71. package/template/packages/modules/src/system-agent-admin-bindings.ts +42 -0
  72. package/template/packages/modules/src/system-agent-admin-menus.ts +26 -0
  73. package/template/packages/modules/src/system-agent-admin-registration.test.ts +52 -0
  74. package/template/packages/modules/src/system-agent-audit-actions.ts +5 -0
  75. package/template/packages/modules/src/system-ai-api-bindings.ts +2 -0
  76. package/template/packages/modules/src/system-audit-actions.ts +25 -0
  77. package/template/packages/modules/src/system-features.ts +19 -0
  78. package/template/packages/modules/src/system-module-catalog.ts +14 -12
  79. package/template/packages/modules/src/system-module-ownership.ts +12 -0
  80. package/template/packages/modules/src/system-modules.ts +2 -0
  81. package/template/packages/modules/src/system-permissions.ts +5 -0
  82. package/template/packages/server-sdk/index.ts +2 -0
  83. package/template/packages/server-sdk/src/agent-session-provider.ts +19 -0
  84. package/template/packages/shared/index.ts +2 -0
  85. package/template/packages/shared/src/agent-admin-catalog.ts +73 -0
  86. package/template/packages/shared/src/agent-workbench.ts +100 -0
@@ -0,0 +1,103 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { featureKey } from "@southwind-ai/shared";
3
+ import { Elysia } from "elysia";
4
+ import { createStaticRouteAuthorizer } from "../module-host/static-route-authorization.js";
5
+ import type { RouteApp } from "../system/routes.js";
6
+ import {
7
+ createRunFacadeFixture,
8
+ runFacadeManifest,
9
+ } from "./run-facade.test-support.js";
10
+ import { registerAgentAdminCatalogRoutes } from "./tool-catalog-routes.js";
11
+ import { GovernedToolRegistry } from "./tool-registry.js";
12
+
13
+ async function fixture() {
14
+ const run = await createRunFacadeFixture();
15
+ const module = runFacadeManifest();
16
+ module.features.push(
17
+ ...["system.agent-tools", "system.agent-debug"].map((key) => ({
18
+ ...module.features[0]!,
19
+ key: featureKey(key),
20
+ })),
21
+ );
22
+ module.apiPermissions.push(
23
+ ...["tools", "operations"].map((kind) => ({
24
+ method: "GET" as const,
25
+ path: `/api/agent/admin/${kind}`,
26
+ permissionKey:
27
+ kind === "tools"
28
+ ? "system.agent-tools.read"
29
+ : "system.agent-debug.read",
30
+ featureKey: featureKey(
31
+ kind === "tools" ? "system.agent-tools" : "system.agent-debug",
32
+ ),
33
+ })),
34
+ );
35
+ const registry = new GovernedToolRegistry(
36
+ [module],
37
+ new Map([
38
+ [
39
+ "business.records.list",
40
+ {
41
+ scopeMode: "request" as const,
42
+ execute: () => ({ items: [] }),
43
+ },
44
+ ],
45
+ ]),
46
+ run.runtime,
47
+ );
48
+ const app = new Elysia().derive(() => ({ guardContext: run.context }));
49
+ app.group("/api", (group) => {
50
+ registerAgentAdminCatalogRoutes(group as unknown as RouteApp, {
51
+ modules: [module],
52
+ hosts: run.hosts,
53
+ runtime: run.runtime,
54
+ registry,
55
+ authorizer: createStaticRouteAuthorizer(run.runtime, [module]),
56
+ runtimeAvailable: () => true,
57
+ });
58
+ return group;
59
+ });
60
+ return { ...run, app };
61
+ }
62
+
63
+ describe("Manifest-authorized Admin catalog endpoints", () => {
64
+ test("Read permission is required and is independent from execution permission", async () => {
65
+ const { app, context, runtime } = await fixture();
66
+ const request = () =>
67
+ app.handle(new Request("http://localhost/api/agent/admin/tools"));
68
+ expect((await request()).status).toBe(403);
69
+ context.actor.permissionKeys = ["system.agent-tools.read"];
70
+ const response = await request();
71
+ expect(response.status).toBe(200);
72
+ const items = await response.json();
73
+ expect(items[0]).toMatchObject({
74
+ key: "business.records.list",
75
+ allowed: false,
76
+ availabilityReason: "permission-denied",
77
+ });
78
+ expect(runtime.auditSink.list().at(-1)).toMatchObject({
79
+ action: "system.agent-tools.read",
80
+ metadata: { itemCount: 1 },
81
+ });
82
+ });
83
+
84
+ test("Operation metadata access cannot start runs and successful reads are audited", async () => {
85
+ const { app, context, remote, prepared, runtime } = await fixture();
86
+ context.actor.permissionKeys = ["system.agent-debug.read"];
87
+ const response = await app.handle(
88
+ new Request("http://localhost/api/agent/admin/operations"),
89
+ );
90
+ expect(response.status).toBe(200);
91
+ const items = await response.json();
92
+ expect(items[0]).toMatchObject({
93
+ key: "business.answer",
94
+ selectable: false,
95
+ });
96
+ expect(remote.started).toHaveLength(0);
97
+ expect(prepared).toHaveLength(0);
98
+ expect(runtime.auditSink.list().at(-1)).toMatchObject({
99
+ action: "system.agent-debug.read",
100
+ metadata: { itemCount: 1 },
101
+ });
102
+ });
103
+ });
@@ -0,0 +1,62 @@
1
+ import type { ModuleManifest } from "@southwind-ai/modules";
2
+ import type { RequestGuardContext } from "../guards.js";
3
+ import type { StaticRouteAuthorizer } from "../module-host/static-route-authorization.js";
4
+ import type { RouteApp } from "../system/routes.js";
5
+ import {
6
+ AgentAdminOperationCatalog,
7
+ type AgentAdminOperationCatalogInput,
8
+ } from "./operation-catalog.js";
9
+ import type { GovernedToolRegistry } from "./tool-registry.js";
10
+ import { projectAdminToolCatalog } from "./tool-catalog.js";
11
+ import { auditAgentAdminCatalogRead } from "./tool-catalog-audit.js";
12
+
13
+ export interface AgentAdminCatalogRouteInput extends AgentAdminOperationCatalogInput {
14
+ readonly modules: readonly ModuleManifest[];
15
+ readonly registry: GovernedToolRegistry;
16
+ readonly authorizer: StaticRouteAuthorizer;
17
+ }
18
+
19
+ export function registerAgentAdminCatalogRoutes(
20
+ app: RouteApp,
21
+ input: AgentAdminCatalogRouteInput,
22
+ ): void {
23
+ const operations = new AgentAdminOperationCatalog(input);
24
+ app.get("/agent/admin/tools", async (raw) => {
25
+ const { guardContext } = raw as { guardContext: RequestGuardContext };
26
+ const denied = await input.authorizer.authorize(
27
+ guardContext,
28
+ "GET",
29
+ "/api/agent/admin/tools",
30
+ );
31
+ if (denied) return denied;
32
+ const items = await projectAdminToolCatalog(
33
+ input.registry,
34
+ input.modules,
35
+ guardContext,
36
+ );
37
+ auditAgentAdminCatalogRead(
38
+ input.runtime,
39
+ guardContext,
40
+ "tools",
41
+ items.length,
42
+ );
43
+ return items;
44
+ });
45
+ app.get("/agent/admin/operations", async (raw) => {
46
+ const { guardContext } = raw as { guardContext: RequestGuardContext };
47
+ const denied = await input.authorizer.authorize(
48
+ guardContext,
49
+ "GET",
50
+ "/api/agent/admin/operations",
51
+ );
52
+ if (denied) return denied;
53
+ const items = await operations.list(guardContext);
54
+ auditAgentAdminCatalogRead(
55
+ input.runtime,
56
+ guardContext,
57
+ "operations",
58
+ items.length,
59
+ );
60
+ return items;
61
+ });
62
+ }
@@ -0,0 +1,122 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createServerRuntime } from "../runtime.js";
3
+ import { guardContext, runFacadeManifest } from "./run-facade.test-support.js";
4
+ import { projectAdminToolCatalog } from "./tool-catalog.js";
5
+ import {
6
+ GovernedToolRegistry,
7
+ type ModuleToolHandlers,
8
+ } from "./tool-registry.js";
9
+
10
+ function fixture(options: { enabled?: boolean; sandbox?: boolean } = {}) {
11
+ const module = runFacadeManifest();
12
+ module.features = module.features.map((feature) => ({
13
+ ...feature,
14
+ enabled: options.enabled ?? true,
15
+ }));
16
+ const runtime = createServerRuntime({}, { featureCatalog: module.features });
17
+ const handlers: ModuleToolHandlers = new Map([
18
+ [
19
+ "business.records.list",
20
+ {
21
+ scopeMode: "request",
22
+ executionMode: options.sandbox ? "sandbox" : "host",
23
+ execute: () => ({ items: [] }),
24
+ },
25
+ ],
26
+ ]);
27
+ return {
28
+ module,
29
+ registry: new GovernedToolRegistry([module], handlers, runtime),
30
+ };
31
+ }
32
+
33
+ describe("Admin dynamic Tool catalogue", () => {
34
+ test("Business-owned declaration, Schema and installed execution mode match actual registry", async () => {
35
+ const { module, registry } = fixture({ sandbox: true });
36
+ const [tool] = await projectAdminToolCatalog(
37
+ registry,
38
+ [module],
39
+ guardContext(["business.records.read"]),
40
+ );
41
+ const definition = registry.definition("business.records.list")!;
42
+ expect(tool).toMatchObject({
43
+ key: definition.key,
44
+ module: "business",
45
+ label: definition.label,
46
+ description: definition.description,
47
+ featureKey: definition.featureKey,
48
+ permissionKey: definition.permissionKey,
49
+ scopeMode: definition.scopeMode,
50
+ executionMode: "sandbox",
51
+ approval: definition.approval,
52
+ idempotency: definition.idempotency,
53
+ limits: definition.limits,
54
+ inputSchemaKey: definition.inputSchemaKey,
55
+ outputSchemaKey: definition.outputSchemaKey,
56
+ inputSchema: definition.inputSchema,
57
+ outputSchema: definition.outputSchema,
58
+ allowedOperationKeys: ["business.answer"],
59
+ allowed: true,
60
+ availabilityReason: "allowed",
61
+ });
62
+ expect(tool.inputSchema).not.toBe(definition.inputSchema);
63
+ });
64
+
65
+ test("Catalogued but unauthorized tools do not gain execution rights", async () => {
66
+ const { module, registry } = fixture();
67
+ const context = guardContext(["system.agent-tools.read"]);
68
+ const tools = await projectAdminToolCatalog(registry, [module], context);
69
+ expect(tools).toHaveLength(1);
70
+ expect(tools[0]).toMatchObject({
71
+ allowed: false,
72
+ availabilityReason: "permission-denied",
73
+ });
74
+ await expect(
75
+ registry.execute("business.records.list", {}, context),
76
+ ).rejects.toMatchObject({ code: "TOOL_FORBIDDEN" });
77
+ });
78
+
79
+ test("Current runtime feature and server-created scope determine callability", async () => {
80
+ const disabled = fixture({ enabled: false });
81
+ const context = guardContext(["business.records.read"]);
82
+ const [off] = await projectAdminToolCatalog(
83
+ disabled.registry,
84
+ [disabled.module],
85
+ context,
86
+ );
87
+ expect(off).toMatchObject({
88
+ allowed: false,
89
+ availabilityReason: "feature-unavailable",
90
+ });
91
+ const { registry, module } = fixture();
92
+ delete context.actor.scopeContext;
93
+ const [noScope] = await projectAdminToolCatalog(
94
+ registry,
95
+ [module],
96
+ context,
97
+ );
98
+ expect(noScope).toMatchObject({
99
+ allowed: false,
100
+ availabilityReason: "scope-unavailable",
101
+ });
102
+ expect(await registry.allowed("business.records.list", context)).toBe(
103
+ false,
104
+ );
105
+ await expect(
106
+ registry.execute("business.records.list", {}, context),
107
+ ).rejects.toMatchObject({ code: "TOOL_FORBIDDEN" });
108
+ });
109
+
110
+ test("License refusal is bounded and contains no raw guard event", async () => {
111
+ const { registry, module } = fixture();
112
+ const context = guardContext(["business.records.read"]);
113
+ context.license = { status: "expired", restricted: true };
114
+ context.licenseEnforced = true;
115
+ const [tool] = await projectAdminToolCatalog(registry, [module], context);
116
+ expect(tool).toMatchObject({
117
+ allowed: false,
118
+ availabilityReason: "license-unavailable",
119
+ });
120
+ expect(tool).not.toHaveProperty("auditEvent");
121
+ });
122
+ });
@@ -0,0 +1,44 @@
1
+ import type { ModuleManifest } from "@southwind-ai/modules";
2
+ import type { AgentAdminToolCatalogItem } from "@southwind-ai/shared";
3
+ import type { RequestGuardContext } from "../guards.js";
4
+ import type { GovernedToolRegistry } from "./tool-registry.js";
5
+
6
+ export async function projectAdminToolCatalog(
7
+ registry: GovernedToolRegistry,
8
+ modules: readonly ModuleManifest[],
9
+ context: RequestGuardContext,
10
+ ): Promise<readonly AgentAdminToolCatalogItem[]> {
11
+ const operations = modules.flatMap(({ aiOperations }) => aiOperations ?? []);
12
+ return Promise.all(
13
+ registry.definitions().map(async (tool) => ({
14
+ key: tool.key,
15
+ label: tool.label,
16
+ description: tool.description,
17
+ module: tool.module,
18
+ operation: tool.operation,
19
+ featureKey: tool.featureKey,
20
+ permissionKey: tool.permissionKey,
21
+ auditAction: tool.auditAction,
22
+ scopeMode: tool.scopeMode,
23
+ executionMode: registry.executionMode(tool.key)!,
24
+ dataClassification: tool.dataClassification,
25
+ approval: tool.approval,
26
+ ...(tool.approvalRoles
27
+ ? { approvalRoles: structuredClone(tool.approvalRoles) }
28
+ : {}),
29
+ ...(tool.approvalTimeoutSeconds
30
+ ? { approvalTimeoutSeconds: tool.approvalTimeoutSeconds }
31
+ : {}),
32
+ idempotency: tool.idempotency,
33
+ limits: { ...tool.limits },
34
+ inputSchemaKey: tool.inputSchemaKey,
35
+ outputSchemaKey: tool.outputSchemaKey,
36
+ inputSchema: structuredClone(tool.inputSchema),
37
+ outputSchema: structuredClone(tool.outputSchema),
38
+ allowedOperationKeys: operations
39
+ .filter(({ allowedToolKeys }) => allowedToolKeys.includes(tool.key))
40
+ .map(({ key }) => key),
41
+ ...(await registry.availability(tool.key, context)),
42
+ })),
43
+ );
44
+ }
@@ -7,13 +7,17 @@ import type {
7
7
  ModuleToolInput,
8
8
  ModuleToolResult,
9
9
  } from "@southwind-ai/server-sdk";
10
- import type { FeatureFlagDefinition } from "@southwind-ai/shared";
10
+ import type {
11
+ AgentAdminAvailabilityReason,
12
+ FeatureFlagDefinition,
13
+ } from "@southwind-ai/shared";
11
14
  import Ajv, { type ValidateFunction } from "ajv";
12
15
  import { evaluateAccessGuards } from "../access-guards.js";
13
16
  import { isServerScopeContext } from "../data-access/context.js";
14
17
  import type { RequestGuardContext } from "../guards.js";
15
18
  import type { createServerRuntime } from "../runtime.js";
16
19
  import { sandboxFailureEvent, toolEvent, toolJsonBytes } from "./tool-audit.js";
20
+ import { catalogGuardReason } from "./tool-catalog-availability.js";
17
21
  import type { GovernedToolExecutionWithAuditReceipt } from "./tool-execution-receipt.js";
18
22
  import {
19
23
  collectRegisteredTools,
@@ -108,20 +112,47 @@ export class GovernedToolRegistry {
108
112
  return this.#tools.get(key)?.definition;
109
113
  }
110
114
 
111
- async allowed(key: string, context: RequestGuardContext): Promise<boolean> {
115
+ executionMode(key: string): "host" | "sandbox" | undefined {
112
116
  const tool = this.#tools.get(key);
113
- if (!tool) return false;
114
- return (
115
- await evaluateAccessGuards(
116
- this.runtime,
117
- context,
118
- {
119
- permission: { permissionKey: tool.definition.permissionKey },
120
- feature: tool.definition.feature,
121
- },
122
- { record: false },
123
- )
124
- ).decision.allowed;
117
+ return tool ? (tool.handler.executionMode ?? "host") : undefined;
118
+ }
119
+
120
+ async availability(
121
+ key: string,
122
+ context: RequestGuardContext,
123
+ ): Promise<{
124
+ readonly allowed: boolean;
125
+ readonly availabilityReason: AgentAdminAvailabilityReason;
126
+ }> {
127
+ const tool = this.#tools.get(key);
128
+ if (!tool)
129
+ return { allowed: false, availabilityReason: "handler-unavailable" };
130
+ const access = await evaluateAccessGuards(
131
+ this.runtime,
132
+ context,
133
+ {
134
+ permission: { permissionKey: tool.definition.permissionKey },
135
+ feature: tool.definition.feature,
136
+ },
137
+ { record: false },
138
+ );
139
+ if (!access.decision.allowed) {
140
+ return {
141
+ allowed: false,
142
+ availabilityReason: catalogGuardReason(access.decision.reason),
143
+ };
144
+ }
145
+ if (
146
+ tool.handler.scopeMode === "request" &&
147
+ !isServerScopeContext(context.actor.scopeContext)
148
+ ) {
149
+ return { allowed: false, availabilityReason: "scope-unavailable" };
150
+ }
151
+ return { allowed: true, availabilityReason: "allowed" };
152
+ }
153
+
154
+ async allowed(key: string, context: RequestGuardContext): Promise<boolean> {
155
+ return (await this.availability(key, context)).allowed;
125
156
  }
126
157
 
127
158
  async execute(
@@ -0,0 +1,39 @@
1
+ import type { AgentPlatformRuntimeInput } from "./runtime-bootstrap-input.js";
2
+ import { AgentWorkbenchFacade } from "./workbench-facade.js";
3
+ import { registerAgentWorkbenchRoutes } from "./workbench-routes.js";
4
+ import { AgentWorkbenchRunReader } from "./workbench-run-reader.js";
5
+ import type { RouteApp } from "../system/routes.js";
6
+ import type { AgentRunFacade } from "./run-facade.js";
7
+ import type { AgentRuntimePort } from "./run-contracts.js";
8
+ import type { AgentSessionFacadeV1 } from "./agent-session-facade.js";
9
+ import type { AgentSessionProjectionFacade } from "./session-projection-facade.js";
10
+ import type { AgentWorkflowQueryService } from "../agent-workflow/query-service.js";
11
+
12
+ export function registerAgentWorkbenchRuntime(
13
+ input: AgentPlatformRuntimeInput,
14
+ app: RouteApp,
15
+ ports: {
16
+ readonly agentFacade?: AgentRunFacade;
17
+ readonly remote?: AgentRuntimePort;
18
+ readonly business?: AgentSessionFacadeV1;
19
+ readonly sessions?: AgentSessionProjectionFacade;
20
+ readonly workflows?: AgentWorkflowQueryService;
21
+ } = {},
22
+ ) {
23
+ registerAgentWorkbenchRoutes(
24
+ app,
25
+ new AgentWorkbenchFacade({
26
+ modules: input.modules,
27
+ runtime: input.runtime,
28
+ sessions: ports.sessions,
29
+ business: input.hosts.agentSessionProviders.length
30
+ ? ports.business
31
+ : undefined,
32
+ workflows: ports.workflows,
33
+ runs:
34
+ ports.agentFacade && ports.remote
35
+ ? new AgentWorkbenchRunReader(ports.agentFacade, ports.remote)
36
+ : undefined,
37
+ }),
38
+ );
39
+ }
@@ -0,0 +1,115 @@
1
+ import type { AgentWorkbenchEventV1, JsonValue } from "@southwind-ai/shared";
2
+
3
+ const labels: Readonly<Record<string, string>> = {
4
+ "run.started": "运行开始",
5
+ "run.resumed": "运行恢复",
6
+ "input.prepared": "输入已准备",
7
+ "output.delta": "模型生成回答",
8
+ "tool.requested": "请求调用工具",
9
+ "tool.completed": "工具调用完成",
10
+ "approval.required": "等待审批",
11
+ "approval.resolved": "审批已处理",
12
+ "usage.reported": "模型用量已记录",
13
+ "run.completed": "运行完成",
14
+ "run.failed": "运行失败",
15
+ "run.cancelled": "运行已取消",
16
+ };
17
+
18
+ export function projectWorkbenchRunEvent(
19
+ value: unknown,
20
+ ): AgentWorkbenchEventV1 | undefined {
21
+ if (
22
+ !isRecord(value) ||
23
+ !Number.isSafeInteger(value.sequence) ||
24
+ Number(value.sequence) < 1
25
+ )
26
+ return;
27
+ const type = safeCoordinate(value.type);
28
+ if (!type || !Object.hasOwn(labels, type)) return;
29
+ const projected: Record<string, JsonValue> = {
30
+ sequence: Number(value.sequence),
31
+ type,
32
+ summary: labels[type]!,
33
+ };
34
+ for (const key of [
35
+ "runId",
36
+ "turnId",
37
+ "stepId",
38
+ "modelAttemptId",
39
+ "toolCallId",
40
+ "toolKey",
41
+ "workItemId",
42
+ "artifactId",
43
+ "outcome",
44
+ ] as const) {
45
+ const text = safeCoordinate(value[key]);
46
+ if (text) projected[key] = text;
47
+ }
48
+ if (
49
+ typeof value.occurredAt === "string" &&
50
+ Number.isFinite(Date.parse(value.occurredAt))
51
+ )
52
+ projected.occurredAt = value.occurredAt;
53
+ if (
54
+ Number.isSafeInteger(value.attemptSequence) &&
55
+ Number(value.attemptSequence) > 0
56
+ )
57
+ projected.attemptSequence = Number(value.attemptSequence);
58
+ if (isRecord(value.usage)) {
59
+ const usage: Record<string, number> = {};
60
+ for (const key of [
61
+ "inputTokens",
62
+ "outputTokens",
63
+ "totalTokens",
64
+ "costMicros",
65
+ ] as const) {
66
+ const count = value.usage[key];
67
+ if (Number.isSafeInteger(count) && Number(count) >= 0)
68
+ usage[key] = Number(count);
69
+ }
70
+ projected.usage = usage;
71
+ }
72
+ return projected as unknown as AgentWorkbenchEventV1;
73
+ }
74
+
75
+ /** SSE keeps canonical answer deltas, but never forwards arbitrary Provider/Tool fields. */
76
+ export function projectPublicRunEvent(
77
+ value: unknown,
78
+ ): Record<string, JsonValue> | undefined {
79
+ const projected = projectWorkbenchRunEvent(value);
80
+ if (!projected || !isRecord(value)) return;
81
+ const result = { ...projected } as Record<string, JsonValue>;
82
+ if (value.type === "output.delta" && typeof value.text === "string")
83
+ result.text = value.text.slice(0, 64_000);
84
+ if (value.type === "input.prepared") {
85
+ if (
86
+ Number.isSafeInteger(value.artifactCount) &&
87
+ Number(value.artifactCount) >= 0
88
+ )
89
+ result.artifactCount = Number(value.artifactCount);
90
+ if (Array.isArray(value.inputModalities))
91
+ result.inputModalities = value.inputModalities.filter(
92
+ (item): item is string =>
93
+ typeof item === "string" && /^[a-z-]{1,32}$/.test(item),
94
+ );
95
+ }
96
+ if (value.type === "run.failed" && isRecord(value.error)) {
97
+ result.error = {
98
+ code: safeCoordinate(value.error.code) ?? "RUN_FAILED",
99
+ message: "运行失败,请查看受控错误代码",
100
+ retryable: value.error.retryable === true,
101
+ requestId: safeCoordinate(value.error.requestId) ?? "unavailable",
102
+ };
103
+ }
104
+ return result;
105
+ }
106
+
107
+ export function safeCoordinate(value: unknown): string | undefined {
108
+ return typeof value === "string" && /^[A-Za-z0-9_.:/-]{1,200}$/.test(value)
109
+ ? value
110
+ : undefined;
111
+ }
112
+
113
+ export function isRecord(value: unknown): value is Record<string, unknown> {
114
+ return !!value && typeof value === "object" && !Array.isArray(value);
115
+ }