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,187 @@
1
+ import type { ModuleAgentSessionRunReferenceV1 } from "@southwind-ai/server-sdk";
2
+ import type {
3
+ AgentWorkbenchEventV1,
4
+ AgentWorkbenchRunV1,
5
+ } from "@southwind-ai/shared";
6
+ import type { RequestGuardContext } from "../guards.js";
7
+ import type { AgentRunFacade } from "./run-facade.js";
8
+ import type { AgentRuntimePort } from "./run-contracts.js";
9
+ import {
10
+ isRecord,
11
+ projectWorkbenchRunEvent,
12
+ } from "./workbench-event-projection.js";
13
+ import { projectWorkbenchResult } from "./workbench-safe-result.js";
14
+
15
+ export class AgentWorkbenchRunReader {
16
+ constructor(
17
+ private readonly facade: Pick<
18
+ AgentRunFacade,
19
+ "authorizeRun" | "operationPolicy"
20
+ >,
21
+ private readonly remote: AgentRuntimePort,
22
+ ) {}
23
+
24
+ async read(
25
+ ref: ModuleAgentSessionRunReferenceV1,
26
+ context: RequestGuardContext,
27
+ signal: AbortSignal,
28
+ ): Promise<AgentWorkbenchRunV1> {
29
+ const identity = {
30
+ runId: ref.runId,
31
+ operationKey: ref.operationKey,
32
+ ...(ref.sessionId ? { sessionId: ref.sessionId } : {}),
33
+ ...(ref.turnId ? { turnId: ref.turnId } : {}),
34
+ ...(ref.messageSequence !== undefined
35
+ ? { messageSequence: ref.messageSequence }
36
+ : {}),
37
+ };
38
+ try {
39
+ if (
40
+ this.facade.operationPolicy(ref.operationKey)?.visibility === "internal"
41
+ )
42
+ throw new Error("Internal operation");
43
+ const current = await this.facade.authorizeRun(
44
+ ref.operationKey,
45
+ ref.runId,
46
+ context,
47
+ );
48
+ if (ref.sessionId && current.sessionId !== ref.sessionId)
49
+ throw new Error("Run association mismatch");
50
+ } catch {
51
+ return {
52
+ ...identity,
53
+ status: "unavailable",
54
+ events: [],
55
+ correlationStatus: "missing",
56
+ missing: ["RUN_NOT_ACCESSIBLE"],
57
+ };
58
+ }
59
+ try {
60
+ const bounded = AbortSignal.any([signal, AbortSignal.timeout(10_000)]);
61
+ const status = await this.remote.status(ref.runId, bounded);
62
+ const history = await readRecordedRunEvents(
63
+ this.remote,
64
+ ref.runId,
65
+ status.lastSequence,
66
+ bounded,
67
+ );
68
+ let result;
69
+ const missing = [...history.missing];
70
+ if (
71
+ !status.lastSequence &&
72
+ ["completed", "failed", "cancelled"].includes(status.status)
73
+ )
74
+ missing.push("RUN_EVENTS_MISSING");
75
+ if (status.status === "completed") {
76
+ const response = await this.remote.result(ref.runId, bounded);
77
+ if (response.ok)
78
+ result = projectWorkbenchResult(await readBoundedJson(response));
79
+ else missing.push("RUN_RESULT_UNAVAILABLE");
80
+ }
81
+ return {
82
+ ...identity,
83
+ status: status.status,
84
+ events: history.events,
85
+ ...(result ? { result } : {}),
86
+ correlationStatus: missing.length ? "partial" : "complete",
87
+ missing,
88
+ };
89
+ } catch {
90
+ return {
91
+ ...identity,
92
+ status: "unavailable",
93
+ events: [],
94
+ correlationStatus: "missing",
95
+ missing: ["RUN_RECORDING_UNAVAILABLE"],
96
+ };
97
+ }
98
+ }
99
+ }
100
+
101
+ export async function readRecordedRunEvents(
102
+ remote: AgentRuntimePort,
103
+ runId: string,
104
+ through: number,
105
+ signal: AbortSignal,
106
+ ) {
107
+ const events: AgentWorkbenchEventV1[] = [];
108
+ if (!through) return { events, missing: [] as string[] };
109
+ const response = await remote.events(runId, 0, signal);
110
+ if (!response.ok || !response.body)
111
+ throw new Error("Run recording unavailable");
112
+ const reader = response.body.getReader();
113
+ const decoder = new TextDecoder();
114
+ let buffer = "";
115
+ let bytes = 0;
116
+ let lastSequence = 0;
117
+ let count = 0;
118
+ let missingOrder = false;
119
+ try {
120
+ while (
121
+ lastSequence < through &&
122
+ count < 10_000 &&
123
+ bytes < 2 * 1_024 * 1_024
124
+ ) {
125
+ const chunk = await reader.read();
126
+ if (chunk.done) break;
127
+ bytes += chunk.value.byteLength;
128
+ buffer += decoder.decode(chunk.value, { stream: true });
129
+ let separator: RegExpExecArray | null;
130
+ while (
131
+ (separator = /\r?\n\r?\n/.exec(buffer)) &&
132
+ lastSequence < through &&
133
+ count < 10_000
134
+ ) {
135
+ const frame = buffer.slice(0, separator.index);
136
+ if (frame.length > 256_000) throw new Error("Run frame exceeds bound");
137
+ buffer = buffer.slice(separator.index + separator[0].length);
138
+ const data = frame
139
+ .split(/\r?\n/)
140
+ .filter((line) => line.startsWith("data:"))
141
+ .map((line) => line.slice(5).trimStart())
142
+ .join("\n");
143
+ if (!data) continue;
144
+ const value: unknown = JSON.parse(data);
145
+ if (
146
+ !isRecord(value) ||
147
+ !Number.isSafeInteger(value.sequence) ||
148
+ Number(value.sequence) <= lastSequence
149
+ )
150
+ throw new Error("Invalid Run event order");
151
+ if (Number(value.sequence) !== lastSequence + 1) missingOrder = true;
152
+ lastSequence = Number(value.sequence);
153
+ count += 1;
154
+ const projected = projectWorkbenchRunEvent(value);
155
+ if (projected) events.push({ ...projected, runId });
156
+ else missingOrder = true;
157
+ }
158
+ if (buffer.length > 256_000 && lastSequence < through)
159
+ throw new Error("Run frame exceeds bound");
160
+ }
161
+ } finally {
162
+ await reader.cancel().catch(() => undefined);
163
+ }
164
+ const missing =
165
+ lastSequence < through || missingOrder ? ["RUN_EVENTS_PARTIAL"] : [];
166
+ return { events, missing };
167
+ }
168
+
169
+ async function readBoundedJson(response: Response): Promise<unknown> {
170
+ if (!response.body) throw new Error("Missing result body");
171
+ const reader = response.body.getReader();
172
+ let content = "";
173
+ let bytes = 0;
174
+ const decoder = new TextDecoder();
175
+ try {
176
+ while (true) {
177
+ const chunk = await reader.read();
178
+ if (chunk.done) break;
179
+ bytes += chunk.value.byteLength;
180
+ if (bytes > 2 * 1_024 * 1_024) throw new Error("Result bound exceeded");
181
+ content += decoder.decode(chunk.value, { stream: true });
182
+ }
183
+ return JSON.parse(content + decoder.decode()) as unknown;
184
+ } finally {
185
+ await reader.cancel().catch(() => undefined);
186
+ }
187
+ }
@@ -0,0 +1,61 @@
1
+ import type { JsonValue } from "@southwind-ai/shared";
2
+ import { isRecord } from "./workbench-event-projection.js";
3
+
4
+ const privateKeys = new Set([
5
+ "secret",
6
+ "password",
7
+ "token",
8
+ "apikey",
9
+ "authorization",
10
+ "executiongrant",
11
+ "chainofthought",
12
+ "reasoning",
13
+ "rawreasoning",
14
+ "thoughts",
15
+ "rawbody",
16
+ "providerbody",
17
+ "arguments",
18
+ "toolarguments",
19
+ ]);
20
+
21
+ /** Controlled answer projection. Tool parameters and private provider state are not answers. */
22
+ export function projectWorkbenchResult(value: unknown): {
23
+ output?: JsonValue;
24
+ status?: string;
25
+ } {
26
+ if (!isRecord(value)) return {};
27
+ const result: { output?: JsonValue; status?: string } = {};
28
+ if (typeof value.status === "string" && /^[a-z-]{1,32}$/.test(value.status))
29
+ result.status = value.status;
30
+ if (value.output !== undefined)
31
+ result.output = safeJson(value.output, 0, { remaining: 64_000 });
32
+ return result;
33
+ }
34
+
35
+ function safeJson(
36
+ value: unknown,
37
+ depth: number,
38
+ budget: { remaining: number },
39
+ ): JsonValue {
40
+ if (depth > 12 || budget.remaining <= 0) return "[受控摘要已截断]";
41
+ if (typeof value === "string") {
42
+ const size = Math.min(value.length, 16_000, budget.remaining);
43
+ budget.remaining -= size;
44
+ return value.slice(0, size);
45
+ }
46
+ if (value === null || typeof value === "boolean") return value;
47
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
48
+ if (Array.isArray(value))
49
+ return value.slice(0, 200).map((item) => safeJson(item, depth + 1, budget));
50
+ if (isRecord(value)) {
51
+ const projected: Record<string, JsonValue> = {};
52
+ for (const [key, child] of Object.entries(value).slice(0, 200)) {
53
+ if (privateKeys.has(key.replace(/[-_\s]/g, "").toLowerCase())) continue;
54
+ if (key.length > 200) continue;
55
+ budget.remaining -= key.length;
56
+ projected[key] = safeJson(child, depth + 1, budget);
57
+ }
58
+ return projected;
59
+ }
60
+ return null;
61
+ }
@@ -0,0 +1,50 @@
1
+ import { projectPublicRunEvent } from "./workbench-event-projection.js";
2
+
3
+ export function projectPublicRunEvents(response: Response): Response {
4
+ if (!response.ok || !response.body) return response;
5
+ const encoder = new TextEncoder();
6
+ const decoder = new TextDecoder();
7
+ let buffer = "";
8
+ return new Response(
9
+ response.body.pipeThrough(
10
+ new TransformStream<Uint8Array, Uint8Array>({
11
+ transform(chunk, controller) {
12
+ buffer += decoder.decode(chunk, { stream: true });
13
+ let separator: RegExpExecArray | null;
14
+ while ((separator = /\r?\n\r?\n/.exec(buffer))) {
15
+ const frame = buffer.slice(0, separator.index);
16
+ if (frame.length > 256_000)
17
+ throw new Error("Run event frame exceeds limit");
18
+ buffer = buffer.slice(separator.index + separator[0].length);
19
+ const data = frame
20
+ .split(/\r?\n/)
21
+ .filter((line) => line.startsWith("data:"))
22
+ .map((line) => line.slice(5).trimStart())
23
+ .join("\n");
24
+ if (!data) continue;
25
+ const event = projectPublicRunEvent(JSON.parse(data) as unknown);
26
+ if (event)
27
+ controller.enqueue(
28
+ encoder.encode(
29
+ `id: ${event.sequence}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`,
30
+ ),
31
+ );
32
+ }
33
+ if (buffer.length > 256_000)
34
+ throw new Error("Run event frame exceeds limit");
35
+ },
36
+ flush() {
37
+ if (buffer.trim()) throw new Error("Incomplete Run event frame");
38
+ },
39
+ }),
40
+ ),
41
+ {
42
+ status: response.status,
43
+ headers: {
44
+ "content-type": "text/event-stream",
45
+ "cache-control": "no-store",
46
+ "x-accel-buffering": "no",
47
+ },
48
+ },
49
+ );
50
+ }
@@ -9,6 +9,7 @@ import type {
9
9
  AgentWorkflowRepository,
10
10
  AgentWorkflowRunSnapshot,
11
11
  } from "./repository.js";
12
+ import { projectWorkflowRecording } from "./recording-projection.js";
12
13
 
13
14
  export class AgentWorkflowQueryService {
14
15
  constructor(private readonly repository: AgentWorkflowRepository) {}
@@ -80,6 +81,41 @@ export class AgentWorkflowQueryService {
80
81
  return this.requireRelease(releaseId);
81
82
  }
82
83
 
84
+ /** Explicit read port: all retry attempts remain facts; no scheduler or result writes. */
85
+ async recording(runId: string, context: RequestGuardContext) {
86
+ const snapshot = await this.requireAuthorizedSnapshot(runId, context);
87
+ const release = await this.requireRelease(snapshot.run.releaseId);
88
+ const events = await this.repository.readEvents({
89
+ runId,
90
+ afterSequence: 0,
91
+ limit: 1_000,
92
+ });
93
+ return {
94
+ recording: projectWorkflowRecording(snapshot, release.definition, events),
95
+ truncated: (events.at(-1)?.sequence ?? 0) < snapshot.headSequence,
96
+ };
97
+ }
98
+
99
+ async recordingsForSession(sessionId: string, context: RequestGuardContext) {
100
+ const access = currentAccess(context);
101
+ const candidates = await this.repository.listRunsForActor(
102
+ access.tenantId,
103
+ access.actorId,
104
+ access.scopeDigest,
105
+ 200,
106
+ );
107
+ const matched = candidates.filter(({ run }) => run.sessionId === sessionId);
108
+ const recordings = await Promise.all(
109
+ matched.map(({ run }) => this.recording(run.runId, context)),
110
+ );
111
+ return {
112
+ items: recordings.map(({ recording }) => recording),
113
+ truncated:
114
+ candidates.length === 200 ||
115
+ recordings.some(({ truncated }) => truncated),
116
+ };
117
+ }
118
+
83
119
  async requireAuthorizedSnapshot(
84
120
  runId: string,
85
121
  context: RequestGuardContext,
@@ -0,0 +1,196 @@
1
+ import { expect, test } from "bun:test";
2
+ import type {
3
+ AgentWorkflowEventEnvelope,
4
+ AgentWorkflowNodeRun,
5
+ AgentWorkflowRun,
6
+ } from "@southwind-ai/shared";
7
+ import { projectWorkflowRecording } from "./recording-projection.js";
8
+
9
+ test("Workflow recording preserves retry attempts, model Run correlation, branch, artifacts and cancellation order", () => {
10
+ const at = "2026-10-07T00:00:00Z";
11
+ const run: AgentWorkflowRun = {
12
+ runId: "workflow_1",
13
+ releaseId: "release_1",
14
+ tenantId: "tenant",
15
+ actorId: "actor-1",
16
+ routeKey: "workflow.read",
17
+ inputDigest: "digest",
18
+ inputReceipt: {
19
+ schemaKey: "input.v1",
20
+ value: null,
21
+ valueDigest: "digest",
22
+ recordedAt: at,
23
+ },
24
+ scopeDigest: "scope",
25
+ authorizationSnapshotDigest: "auth",
26
+ toolPolicyDigest: "tools",
27
+ workspacePolicyDigest: "workspace",
28
+ executionPolicyDigest: "execution",
29
+ status: "cancelled",
30
+ sessionId: "session_1",
31
+ parentRunId: "workflow_parent",
32
+ parentNodeRunId: "node_parent",
33
+ deadlineAt: at,
34
+ createdAt: at,
35
+ updatedAt: at,
36
+ version: 1,
37
+ budget: {
38
+ maxDurationSeconds: 60,
39
+ maxModelAttempts: 3,
40
+ maxToolCalls: 3,
41
+ maxSubagents: 1,
42
+ maxParallelism: 1,
43
+ maxInputTokens: 100,
44
+ maxOutputTokens: 100,
45
+ maxCostMicros: 100,
46
+ },
47
+ usage: {
48
+ durationMs: 2,
49
+ modelAttempts: 2,
50
+ toolCalls: 1,
51
+ subagents: 0,
52
+ inputTokens: { status: "unknown", reasonCode: "pending" },
53
+ outputTokens: { status: "unknown", reasonCode: "pending" },
54
+ cost: { status: "unknown", reasonCode: "pending" },
55
+ },
56
+ };
57
+ const node: AgentWorkflowNodeRun = {
58
+ nodeRunId: "node_1",
59
+ runId: "workflow_1",
60
+ nodeId: "model",
61
+ nodeType: "model",
62
+ status: "failed",
63
+ attempt: 1,
64
+ maxAttempts: 3,
65
+ idempotencyKey: "idempotency_1",
66
+ inputArtifacts: [],
67
+ outputArtifacts: [],
68
+ createdAt: at,
69
+ updatedAt: at,
70
+ version: 1,
71
+ };
72
+ const retried: AgentWorkflowNodeRun = {
73
+ ...node,
74
+ nodeRunId: "node_2",
75
+ previousNodeRunId: "node_1",
76
+ status: "completed",
77
+ attempt: 2,
78
+ resultReceipt: {
79
+ receiptId: "receipt_2",
80
+ nodeRunId: "node_2",
81
+ resultDigest: "digest",
82
+ selectedBranch: "has-records",
83
+ artifactIds: ["artifact_1"],
84
+ recordedAt: at,
85
+ smallResult: { secret: "not disclosed" },
86
+ executionEvidence: {
87
+ source: "agent-runtime-report",
88
+ agentRunId: "agent_run_2",
89
+ modelRouteKey: "route",
90
+ model: "model",
91
+ modelDeploymentDigest: "deployment",
92
+ modelConfigurationDigest: "config",
93
+ },
94
+ },
95
+ };
96
+ const events: AgentWorkflowEventEnvelope[] = [
97
+ {
98
+ schemaVersion: 1,
99
+ eventId: "event_1",
100
+ workflowRunId: "workflow_1",
101
+ sequence: 1,
102
+ occurredAt: at,
103
+ data: {
104
+ type: "workflow.node-retry-scheduled",
105
+ nodeRunId: "node_1",
106
+ nodeId: "model",
107
+ },
108
+ },
109
+ {
110
+ schemaVersion: 1,
111
+ eventId: "event_2",
112
+ workflowRunId: "workflow_1",
113
+ sequence: 2,
114
+ occurredAt: at,
115
+ data: {
116
+ type: "workflow.node-completed",
117
+ nodeRunId: "node_2",
118
+ nodeId: "model",
119
+ },
120
+ },
121
+ {
122
+ schemaVersion: 1,
123
+ eventId: "event_3",
124
+ workflowRunId: "workflow_1",
125
+ sequence: 3,
126
+ occurredAt: at,
127
+ data: {
128
+ type: "workflow.node-waiting",
129
+ nodeRunId: "node_approval",
130
+ nodeId: "approval",
131
+ },
132
+ },
133
+ {
134
+ schemaVersion: 1,
135
+ eventId: "event_4",
136
+ workflowRunId: "workflow_1",
137
+ sequence: 4,
138
+ occurredAt: at,
139
+ data: { type: "workflow.run-cancelled" },
140
+ },
141
+ ];
142
+ const projected = projectWorkflowRecording(
143
+ {
144
+ run,
145
+ idempotencyKey: "unique",
146
+ headSequence: 4,
147
+ recoveryCount: 0,
148
+ nodeRuns: [
149
+ node,
150
+ retried,
151
+ {
152
+ ...node,
153
+ nodeRunId: "node_approval",
154
+ nodeId: "approval",
155
+ nodeType: "approval",
156
+ status: "waiting",
157
+ adapter: {
158
+ kind: "approval-work-item",
159
+ coordinate: "work_item_approved_1",
160
+ },
161
+ },
162
+ {
163
+ ...node,
164
+ nodeRunId: "node_unsafe",
165
+ nodeId: "approval",
166
+ nodeType: "approval",
167
+ status: "waiting",
168
+ adapter: {
169
+ kind: "approval-work-item",
170
+ coordinate: "not a safe coordinate",
171
+ },
172
+ },
173
+ ],
174
+ sideEffects: [],
175
+ },
176
+ { workflowKey: "business.review", label: "业务复核" },
177
+ events,
178
+ );
179
+ expect(projected.nodes.slice(0, 2).map(({ attempt }) => attempt)).toEqual([
180
+ 1, 2,
181
+ ]);
182
+ expect(projected.nodes[2]?.workItemId).toBe("work_item_approved_1");
183
+ expect(projected.events[2]?.workItemId).toBe("work_item_approved_1");
184
+ expect(projected.nodes[3]?.workItemId).toBeUndefined();
185
+ expect(projected.nodes[1]).toMatchObject({
186
+ previousNodeRunId: "node_1",
187
+ agentRunId: "agent_run_2",
188
+ selectedBranch: "has-records",
189
+ artifactIds: ["artifact_1"],
190
+ });
191
+ expect(projected.events.map(({ sequence }) => sequence)).toEqual([
192
+ 1, 2, 3, 4,
193
+ ]);
194
+ expect(projected.parentRunId).toBe("workflow_parent");
195
+ expect(JSON.stringify(projected)).not.toContain("not disclosed");
196
+ });
@@ -0,0 +1,86 @@
1
+ import type {
2
+ AgentWorkbenchEventV1,
3
+ AgentWorkbenchWorkflowV1,
4
+ AgentWorkflowEventEnvelope,
5
+ } from "@southwind-ai/shared";
6
+ import type { AgentWorkflowRunSnapshot } from "./repository.js";
7
+
8
+ export function projectWorkflowRecording(
9
+ snapshot: AgentWorkflowRunSnapshot,
10
+ label: { workflowKey: string; label: string },
11
+ events: readonly AgentWorkflowEventEnvelope[],
12
+ ): AgentWorkbenchWorkflowV1 {
13
+ const run = snapshot.run;
14
+ const workItems = new Map(
15
+ snapshot.nodeRuns.flatMap((node) =>
16
+ node.adapter?.kind === "approval-work-item" &&
17
+ /^[A-Za-z0-9_.:-]{1,200}$/.test(node.adapter.coordinate)
18
+ ? [[node.nodeRunId, node.adapter.coordinate] as const]
19
+ : [],
20
+ ),
21
+ );
22
+ return {
23
+ workflowRunId: run.runId,
24
+ workflowKey: label.workflowKey,
25
+ title: label.label,
26
+ status: run.status,
27
+ ...(run.sessionId ? { sessionId: run.sessionId } : {}),
28
+ ...(run.parentRunId ? { parentRunId: run.parentRunId } : {}),
29
+ ...(run.parentNodeRunId ? { parentNodeRunId: run.parentNodeRunId } : {}),
30
+ nodes: snapshot.nodeRuns.map((node) => ({
31
+ nodeRunId: node.nodeRunId,
32
+ nodeId: node.nodeId,
33
+ type: node.nodeType,
34
+ status: node.status,
35
+ attempt: node.attempt,
36
+ ...(workItems.has(node.nodeRunId)
37
+ ? { workItemId: workItems.get(node.nodeRunId)! }
38
+ : {}),
39
+ ...(node.previousNodeRunId
40
+ ? { previousNodeRunId: node.previousNodeRunId }
41
+ : {}),
42
+ ...(node.resultReceipt?.executionEvidence
43
+ ? { agentRunId: node.resultReceipt.executionEvidence.agentRunId }
44
+ : {}),
45
+ ...(node.resultReceipt?.selectedBranch
46
+ ? { selectedBranch: node.resultReceipt.selectedBranch }
47
+ : {}),
48
+ artifactIds: [
49
+ ...new Set([
50
+ ...node.outputArtifacts.map(({ artifactId }) => artifactId),
51
+ ...(node.resultReceipt?.artifactIds ?? []),
52
+ ]),
53
+ ],
54
+ })),
55
+ events: events.map(
56
+ (event): AgentWorkbenchEventV1 => ({
57
+ sequence: event.sequence,
58
+ type: event.data.type,
59
+ summary: workflowEventLabel(event.data.type),
60
+ occurredAt: event.occurredAt,
61
+ workflowRunId: event.workflowRunId,
62
+ ...("nodeRunId" in event.data
63
+ ? { nodeRunId: event.data.nodeRunId }
64
+ : {}),
65
+ ...("nodeRunId" in event.data && workItems.has(event.data.nodeRunId)
66
+ ? { workItemId: workItems.get(event.data.nodeRunId)! }
67
+ : {}),
68
+ ...("childAgentRunId" in event.data && event.data.childAgentRunId
69
+ ? { runId: event.data.childAgentRunId }
70
+ : {}),
71
+ ...(event.data.type === "workflow.artifact-linked"
72
+ ? { artifactId: event.data.artifact.artifactId }
73
+ : {}),
74
+ }),
75
+ ),
76
+ };
77
+ }
78
+
79
+ function workflowEventLabel(type: string) {
80
+ if (type.includes("retry")) return "节点重试已记录";
81
+ if (type.includes("cancel")) return "取消事实已记录";
82
+ if (type.includes("recover")) return "恢复事实已记录";
83
+ if (type.includes("artifact")) return "交付件已关联";
84
+ if (type.includes("delegation")) return "子运行事实已记录";
85
+ return "工作流事实已记录";
86
+ }
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ import AgentWorkbench from "./components/agent-workbench/AgentWorkbench.vue";
3
+ </script>
4
+ <template>
5
+ <div class="space-y-5">
6
+ <header>
7
+ <h1 class="text-2xl font-semibold">Agent 调试与会话回放</h1>
8
+ <p class="mt-2 text-sm text-muted-foreground">
9
+ 在同一视图查看业务会话的多轮调用、工具、审批、产物与工作流,或创建真实受治理测试。
10
+ </p>
11
+ </header>
12
+ <AgentWorkbench />
13
+ </div>
14
+ </template>