@codemation/host 0.1.0 → 0.1.2

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 (70) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/{AppConfigFactory-Ciz9YKWx.js → AppConfigFactory-ByT1D8dM.js} +253 -15
  3. package/dist/{AppConfigFactory-Ciz9YKWx.js.map → AppConfigFactory-ByT1D8dM.js.map} +1 -1
  4. package/dist/{AppConfigFactory-CiBPHleh.d.ts → AppConfigFactory-_fqSok1J.d.ts} +6565 -177
  5. package/dist/{AppContainerFactory-DH88oxpg.js → AppContainerFactory-BRU02PTm.js} +838 -145
  6. package/dist/AppContainerFactory-BRU02PTm.js.map +1 -0
  7. package/dist/{CodemationConfig-DfK1KLvO.d.ts → CodemationConfig-CNfytKR6.d.ts} +2 -2
  8. package/dist/{CodemationConfigNormalizer-BKgIOeLm.d.ts → CodemationConfigNormalizer-BuKWVNEq.d.ts} +2 -2
  9. package/dist/{CodemationConsumerConfigLoader-bdhJsBKt.d.ts → CodemationConsumerConfigLoader-Mv4cywWu.d.ts} +2 -2
  10. package/dist/{CodemationPluginListMerger-BFZeO0WG.d.ts → CodemationPluginListMerger-BD5mR6gK.d.ts} +11 -5
  11. package/dist/{CredentialServices-aKIwHEhf.d.ts → CredentialServices-BQsEtctT.d.ts} +8 -7
  12. package/dist/{CredentialServices-DNb3CZwW.js → CredentialServices-xVxVA9Tq.js} +60 -114
  13. package/dist/CredentialServices-xVxVA9Tq.js.map +1 -0
  14. package/dist/{PublicFrontendBootstrapFactory-6ahaU0XM.d.ts → PublicFrontendBootstrapFactory-kTyAJdHI.d.ts} +2 -2
  15. package/dist/consumer.d.ts +4 -4
  16. package/dist/credentials.d.ts +3 -3
  17. package/dist/credentials.js +1 -1
  18. package/dist/devServerSidecar.d.ts +1 -1
  19. package/dist/{index-BYbzmUwS.d.ts → index-CX752QE9.d.ts} +68 -4
  20. package/dist/index.d.ts +10 -10
  21. package/dist/index.js +5 -5
  22. package/dist/nextServer.d.ts +20 -15
  23. package/dist/nextServer.js +35 -58
  24. package/dist/nextServer.js.map +1 -1
  25. package/dist/{persistenceServer-DL8yBGDU.d.ts → persistenceServer-CLY4qtMo.d.ts} +2 -2
  26. package/dist/{persistenceServer-CuAqL_fF.js → persistenceServer-DMvIOGW8.js} +2 -2
  27. package/dist/{persistenceServer-CuAqL_fF.js.map → persistenceServer-DMvIOGW8.js.map} +1 -1
  28. package/dist/persistenceServer.d.ts +5 -5
  29. package/dist/persistenceServer.js +2 -2
  30. package/dist/{server-EbxQft_X.js → server-ChTCEc6R.js} +4 -4
  31. package/dist/{server-EbxQft_X.js.map → server-ChTCEc6R.js.map} +1 -1
  32. package/dist/{server-BceIfIJf.d.ts → server-DwpcwzFb.d.ts} +6 -5
  33. package/dist/server.d.ts +8 -8
  34. package/dist/server.js +5 -5
  35. package/package.json +5 -5
  36. package/prisma/migrations/20260407140000_run_normalized_persistence/migration.sql +327 -0
  37. package/prisma/migrations/20260407193000_rename_run_projection_to_run_slot_projection/migration.sql +10 -0
  38. package/prisma/migrations.sqlite/20260407140000_run_normalized_persistence/migration.sql +326 -0
  39. package/prisma/migrations.sqlite/20260407193000_rename_run_projection_to_run_slot_projection/migration.sql +38 -0
  40. package/prisma/schema.postgresql.prisma +100 -1
  41. package/prisma/schema.sqlite.prisma +101 -1
  42. package/scripts/integration-database-global-setup.mjs +0 -3
  43. package/src/application/mapping/WorkflowDefinitionMapper.ts +95 -56
  44. package/src/application/mapping/WorkflowPolicyUiPresentationFactory.ts +1 -1
  45. package/src/application/queries/GetWorkflowRunDetailQuery.ts +8 -0
  46. package/src/application/queries/GetWorkflowRunDetailQueryHandler.ts +24 -0
  47. package/src/application/queries/WorkflowQueryHandlers.ts +1 -0
  48. package/src/application/runs/WorkflowRunRetentionPruneScheduler.ts +52 -27
  49. package/src/domain/credentials/WorkflowCredentialNodeResolver.ts +113 -158
  50. package/src/domain/runs/WorkflowRunRepository.ts +7 -1
  51. package/src/infrastructure/persistence/InMemoryWorkflowRunRepository.ts +123 -1
  52. package/src/infrastructure/persistence/PrismaMigrationDeployer.ts +226 -6
  53. package/src/infrastructure/persistence/PrismaWorkflowRunRepository.ts +796 -109
  54. package/src/infrastructure/persistence/generated/prisma-postgresql-client/edge.js +85 -5
  55. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index-browser.js +81 -1
  56. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.d.ts +7107 -237
  57. package/src/infrastructure/persistence/generated/prisma-postgresql-client/index.js +85 -5
  58. package/src/infrastructure/persistence/generated/prisma-postgresql-client/package.json +1 -1
  59. package/src/infrastructure/persistence/generated/prisma-postgresql-client/schema.prisma +101 -1
  60. package/src/infrastructure/persistence/generated/prisma-sqlite-client/edge.js +85 -5
  61. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index-browser.js +81 -1
  62. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.d.ts +7104 -242
  63. package/src/infrastructure/persistence/generated/prisma-sqlite-client/index.js +85 -5
  64. package/src/infrastructure/persistence/generated/prisma-sqlite-client/package.json +1 -1
  65. package/src/infrastructure/persistence/generated/prisma-sqlite-client/schema.prisma +101 -1
  66. package/src/presentation/http/ApiPaths.ts +4 -0
  67. package/src/presentation/http/hono/registrars/RunHonoApiRouteRegistrar.ts +1 -0
  68. package/src/presentation/http/routeHandlers/RunHttpRouteHandler.ts +13 -0
  69. package/dist/AppContainerFactory-DH88oxpg.js.map +0 -1
  70. package/dist/CredentialServices-DNb3CZwW.js.map +0 -1
@@ -1,8 +1,9 @@
1
1
  import type { CredentialRequirement, WorkflowDefinition } from "@codemation/core";
2
2
  import {
3
3
  AgentConfigInspector,
4
+ AgentConnectionNodeCollector,
5
+ type AgentConnectionNodeDescriptor,
4
6
  ConnectionNodeIdFactory,
5
- WorkflowExecutableNodeClassifierFactory,
6
7
  } from "@codemation/core";
7
8
 
8
9
  import { injectable } from "@codemation/core";
@@ -27,48 +28,18 @@ export class WorkflowCredentialNodeResolver {
27
28
  if (direct) {
28
29
  return direct.name ?? direct.config.name ?? direct.id;
29
30
  }
30
- if (ConnectionNodeIdFactory.isLanguageModelConnectionNodeId(nodeId)) {
31
- const parentId = this.parseParentForLanguageModelConnectionNodeId(nodeId);
32
- const parent = parentId ? workflow.nodes.find((n) => n.id === parentId) : undefined;
33
- const agentLabel = parent?.name ?? parentId ?? "Agent";
34
- return `${agentLabel} › Language model`;
31
+ const recursive = this.findRecursiveConnectionNode(workflow, nodeId);
32
+ if (!recursive) {
33
+ return nodeId;
35
34
  }
36
- if (ConnectionNodeIdFactory.isToolConnectionNodeId(nodeId)) {
37
- const parsed = this.parseToolConnectionNodeId(nodeId);
38
- if (!parsed) {
39
- return nodeId;
40
- }
41
- const parent = workflow.nodes.find((n) => n.id === parsed.parentNodeId);
42
- const agentLabel = parent?.name ?? parsed.parentNodeId;
43
- const toolConfig =
44
- parent && AgentConfigInspector.isAgentNodeConfig(parent.config)
45
- ? parent.config.tools?.find(
46
- (tool) => ConnectionNodeIdFactory.normalizeToolName(tool.name) === parsed.normalizedToolName,
47
- )
48
- : undefined;
49
- const toolLabel = toolConfig?.presentation?.label ?? toolConfig?.name ?? parsed.normalizedToolName;
50
- return `${agentLabel} › ${toolLabel}`;
51
- }
52
- return nodeId;
35
+ return this.buildRecursiveDisplayLabel(recursive.rootAgentLabel, recursive.entry, recursive.entriesById);
53
36
  }
54
37
 
55
38
  isCredentialNodeIdInWorkflow(workflow: WorkflowDefinition, nodeId: string): boolean {
56
39
  if (workflow.nodes.some((n) => n.id === nodeId)) {
57
40
  return true;
58
41
  }
59
- if (ConnectionNodeIdFactory.isLanguageModelConnectionNodeId(nodeId)) {
60
- const parent = this.parseParentForLanguageModelConnectionNodeId(nodeId);
61
- if (parent && workflow.nodes.some((n) => n.id === parent)) {
62
- return true;
63
- }
64
- }
65
- if (ConnectionNodeIdFactory.isToolConnectionNodeId(nodeId)) {
66
- const parsed = this.parseToolConnectionNodeId(nodeId);
67
- if (parsed && workflow.nodes.some((n) => n.id === parsed.parentNodeId)) {
68
- return true;
69
- }
70
- }
71
- return false;
42
+ return this.findRecursiveConnectionNode(workflow, nodeId) !== undefined;
72
43
  }
73
44
 
74
45
  findRequirement(
@@ -80,83 +51,33 @@ export class WorkflowCredentialNodeResolver {
80
51
  if (direct) {
81
52
  return direct;
82
53
  }
83
- if (ConnectionNodeIdFactory.isLanguageModelConnectionNodeId(nodeId)) {
84
- const parent = this.parseParentForLanguageModelConnectionNodeId(nodeId);
85
- if (parent) {
86
- const fromConn = this.findLanguageModelRequirement(workflow, parent, slotKey);
87
- if (fromConn) {
88
- return fromConn;
89
- }
90
- }
91
- }
92
- if (ConnectionNodeIdFactory.isToolConnectionNodeId(nodeId)) {
93
- const parsed = this.parseToolConnectionNodeId(nodeId);
94
- if (parsed) {
95
- const fromConn = this.findToolRequirement(workflow, parsed.parentNodeId, parsed.normalizedToolName, slotKey);
96
- if (fromConn) {
97
- return fromConn;
98
- }
99
- }
54
+ const recursive = this.findRecursiveConnectionNode(workflow, nodeId);
55
+ if (!recursive) {
56
+ return undefined;
100
57
  }
101
- return undefined;
58
+ const requirement = recursive.entry.credentialSource
59
+ .getCredentialRequirements?.()
60
+ ?.find((entry) => entry.slotKey === slotKey);
61
+ return requirement ? { nodeName: recursive.entry.name, requirement } : undefined;
102
62
  }
103
63
 
104
64
  listSlots(workflow: WorkflowDefinition): ReadonlyArray<WorkflowCredentialSlotRef> {
105
- const slots: WorkflowCredentialSlotRef[] = [];
106
- const classifier = WorkflowExecutableNodeClassifierFactory.create(workflow);
107
- const hasConnectionMetadata = (workflow.connections?.length ?? 0) > 0;
65
+ const slotsByKey = new Map<string, WorkflowCredentialSlotRef>();
108
66
 
109
67
  for (const node of workflow.nodes) {
110
- if (classifier.isConnectionOwnedNodeId(node.id)) {
111
- for (const requirement of node.config.getCredentialRequirements?.() ?? []) {
112
- slots.push({
113
- workflowId: workflow.id,
114
- nodeId: node.id,
115
- nodeName: node.name ?? node.config.name ?? node.id,
116
- requirement,
117
- });
118
- }
119
- continue;
120
- }
121
-
122
68
  if (AgentConfigInspector.isAgentNodeConfig(node.config)) {
123
- if (!hasConnectionMetadata) {
124
- const lmNodeId = ConnectionNodeIdFactory.languageModelConnectionNodeId(node.id);
125
- const lmLabel = node.config.chatModel.presentation?.label ?? node.config.chatModel.name;
126
- for (const requirement of node.config.chatModel.getCredentialRequirements?.() ?? []) {
127
- slots.push({
128
- workflowId: workflow.id,
129
- nodeId: lmNodeId,
130
- nodeName: lmLabel,
131
- requirement,
132
- });
133
- }
134
- for (const toolConfig of node.config.tools ?? []) {
135
- const toolNodeId = ConnectionNodeIdFactory.toolConnectionNodeId(node.id, toolConfig.name);
136
- const toolLabel = toolConfig.presentation?.label ?? toolConfig.name;
137
- for (const requirement of toolConfig.getCredentialRequirements?.() ?? []) {
138
- slots.push({
139
- workflowId: workflow.id,
140
- nodeId: toolNodeId,
141
- nodeName: toolLabel,
142
- requirement,
143
- });
144
- }
145
- }
146
- }
69
+ this.addRecursiveAgentSlots(workflow.id, node.id, node.config, slotsByKey);
147
70
  continue;
148
71
  }
149
-
150
- for (const requirement of node.config.getCredentialRequirements?.() ?? []) {
151
- slots.push({
152
- workflowId: workflow.id,
153
- nodeId: node.id,
154
- nodeName: node.name ?? node.config.name ?? node.id,
155
- requirement,
156
- });
157
- }
158
- }
159
- return slots;
72
+ this.addSlotsForRequirements(
73
+ workflow.id,
74
+ node.id,
75
+ node.name ?? node.config.name ?? node.id,
76
+ node.config.getCredentialRequirements?.() ?? [],
77
+ slotsByKey,
78
+ );
79
+ }
80
+ return [...slotsByKey.values()];
160
81
  }
161
82
 
162
83
  private findDirectRequirement(
@@ -175,72 +96,106 @@ export class WorkflowCredentialNodeResolver {
175
96
  return { nodeName: node.name ?? node.config.name ?? node.id, requirement };
176
97
  }
177
98
 
178
- private findLanguageModelRequirement(
179
- workflow: WorkflowDefinition,
180
- parentNodeId: string,
181
- slotKey: string,
182
- ): Readonly<{ nodeName: string; requirement: CredentialRequirement }> | undefined {
183
- const parent = workflow.nodes.find((entry) => entry.id === parentNodeId);
184
- if (!parent || !AgentConfigInspector.isAgentNodeConfig(parent.config)) {
185
- return undefined;
99
+ private addRecursiveAgentSlots(
100
+ workflowId: string,
101
+ rootAgentNodeId: string,
102
+ agentConfig: Parameters<typeof AgentConnectionNodeCollector.collect>[1],
103
+ slotsByKey: Map<string, WorkflowCredentialSlotRef>,
104
+ ): void {
105
+ for (const entry of AgentConnectionNodeCollector.collect(rootAgentNodeId, agentConfig)) {
106
+ this.addSlotsForRequirements(
107
+ workflowId,
108
+ entry.nodeId,
109
+ entry.name,
110
+ entry.credentialSource.getCredentialRequirements?.() ?? [],
111
+ slotsByKey,
112
+ );
186
113
  }
187
- const requirement = parent.config.chatModel
188
- .getCredentialRequirements?.()
189
- ?.find((entry) => entry.slotKey === slotKey);
190
- if (!requirement) {
191
- return undefined;
114
+ }
115
+
116
+ private addSlotsForRequirements(
117
+ workflowId: string,
118
+ nodeId: string,
119
+ nodeName: string,
120
+ requirements: ReadonlyArray<CredentialRequirement>,
121
+ slotsByKey: Map<string, WorkflowCredentialSlotRef>,
122
+ ): void {
123
+ for (const requirement of requirements) {
124
+ const key = `${nodeId}\0${requirement.slotKey}`;
125
+ if (slotsByKey.has(key)) {
126
+ continue;
127
+ }
128
+ slotsByKey.set(key, {
129
+ workflowId,
130
+ nodeId,
131
+ nodeName,
132
+ requirement,
133
+ });
192
134
  }
193
- const nodeName =
194
- parent.config.chatModel.presentation?.label ?? parent.config.chatModel.name ?? parent.name ?? parent.id;
195
- return { nodeName, requirement };
196
135
  }
197
136
 
198
- private findToolRequirement(
137
+ private findRecursiveConnectionNode(
199
138
  workflow: WorkflowDefinition,
200
- parentNodeId: string,
201
- normalizedToolName: string,
202
- slotKey: string,
203
- ): Readonly<{ nodeName: string; requirement: CredentialRequirement }> | undefined {
204
- const parent = workflow.nodes.find((entry) => entry.id === parentNodeId);
205
- if (!parent || !AgentConfigInspector.isAgentNodeConfig(parent.config)) {
206
- return undefined;
207
- }
208
- const toolConfig = parent.config.tools?.find(
209
- (tool) => ConnectionNodeIdFactory.normalizeToolName(tool.name) === normalizedToolName,
210
- );
211
- if (!toolConfig) {
139
+ nodeId: string,
140
+ ):
141
+ | Readonly<{
142
+ rootAgentNodeId: string;
143
+ rootAgentLabel: string;
144
+ entry: AgentConnectionNodeDescriptor;
145
+ entriesById: ReadonlyMap<string, AgentConnectionNodeDescriptor>;
146
+ }>
147
+ | undefined {
148
+ if (
149
+ !ConnectionNodeIdFactory.isLanguageModelConnectionNodeId(nodeId) &&
150
+ !ConnectionNodeIdFactory.isToolConnectionNodeId(nodeId)
151
+ ) {
212
152
  return undefined;
213
153
  }
214
- const requirement = toolConfig.getCredentialRequirements?.()?.find((entry) => entry.slotKey === slotKey);
215
- if (!requirement) {
216
- return undefined;
154
+ for (const node of workflow.nodes) {
155
+ if (!AgentConfigInspector.isAgentNodeConfig(node.config)) {
156
+ continue;
157
+ }
158
+ const entries = AgentConnectionNodeCollector.collect(node.id, node.config);
159
+ const entriesById = new Map(entries.map((entry) => [entry.nodeId, entry]));
160
+ const entry = entriesById.get(nodeId);
161
+ if (!entry) {
162
+ continue;
163
+ }
164
+ return {
165
+ rootAgentNodeId: node.id,
166
+ rootAgentLabel: node.name ?? node.config.name ?? node.id,
167
+ entry,
168
+ entriesById,
169
+ };
217
170
  }
218
- const nodeName = toolConfig.presentation?.label ?? toolConfig.name ?? parent.name ?? parent.id;
219
- return { nodeName, requirement };
171
+ return undefined;
220
172
  }
221
173
 
222
- private parseParentForLanguageModelConnectionNodeId(nodeId: string): string | undefined {
223
- if (!ConnectionNodeIdFactory.isLanguageModelConnectionNodeId(nodeId)) {
224
- return undefined;
225
- }
226
- const suffix = `${ConnectionNodeIdFactory.connectionSegment}llm`;
227
- return nodeId.slice(0, -suffix.length);
174
+ private buildRecursiveDisplayLabel(
175
+ rootAgentLabel: string,
176
+ entry: AgentConnectionNodeDescriptor,
177
+ entriesById: ReadonlyMap<string, AgentConnectionNodeDescriptor>,
178
+ ): string {
179
+ const labels = [rootAgentLabel, ...this.collectAncestorToolLabels(entry.parentNodeId, entriesById)];
180
+ labels.push(entry.role === "languageModel" ? "Language model" : entry.name);
181
+ return labels.join(" › ");
228
182
  }
229
183
 
230
- private parseToolConnectionNodeId(nodeId: string): { parentNodeId: string; normalizedToolName: string } | undefined {
231
- if (!ConnectionNodeIdFactory.isToolConnectionNodeId(nodeId)) {
232
- return undefined;
233
- }
234
- const marker = `${ConnectionNodeIdFactory.connectionSegment}tool${ConnectionNodeIdFactory.connectionSegment}`;
235
- const idx = nodeId.indexOf(marker);
236
- if (idx < 0) {
237
- return undefined;
238
- }
239
- const parentNodeId = nodeId.slice(0, idx);
240
- const normalizedToolName = nodeId.slice(idx + marker.length);
241
- if (!parentNodeId || !normalizedToolName) {
242
- return undefined;
184
+ private collectAncestorToolLabels(
185
+ parentNodeId: string,
186
+ entriesById: ReadonlyMap<string, AgentConnectionNodeDescriptor>,
187
+ ): ReadonlyArray<string> {
188
+ const labels: string[] = [];
189
+ let currentNodeId = parentNodeId;
190
+ while (true) {
191
+ const parentEntry = entriesById.get(currentNodeId);
192
+ if (!parentEntry) {
193
+ return labels.reverse();
194
+ }
195
+ if (parentEntry.role === "tool" || parentEntry.role === "nestedAgent") {
196
+ labels.push(parentEntry.name);
197
+ }
198
+ currentNodeId = parentEntry.parentNodeId;
243
199
  }
244
- return { parentNodeId, normalizedToolName };
245
200
  }
246
201
  }
@@ -1,11 +1,17 @@
1
- import type { PersistedRunState, RunId, RunSummary } from "@codemation/core";
1
+ import type { PersistedRunState, RunId, RunPruneCandidate, RunSummary, WorkflowRunDetailDto } from "@codemation/core";
2
2
 
3
3
  export interface WorkflowRunRepository {
4
4
  load(runId: string): Promise<PersistedRunState | undefined>;
5
5
 
6
+ loadRunDetail?(runId: string): Promise<WorkflowRunDetailDto | undefined>;
7
+
6
8
  save(state: PersistedRunState): Promise<void>;
7
9
 
8
10
  listRuns(args: Readonly<{ workflowId?: string; limit?: number }>): Promise<ReadonlyArray<RunSummary>>;
9
11
 
12
+ listRunsOlderThan?(args: Readonly<{ beforeIso: string; limit?: number }>): Promise<ReadonlyArray<RunPruneCandidate>>;
13
+
14
+ listBinaryStorageKeys?(runId: RunId): Promise<ReadonlyArray<string>>;
15
+
10
16
  deleteRun(runId: RunId): Promise<void>;
11
17
  }
@@ -3,10 +3,14 @@ import {
3
3
  type NodeId,
4
4
  type NodeOutputs,
5
5
  type ParentExecutionRef,
6
+ type PersistedRunSchedulingState,
6
7
  type PersistedRunState,
7
8
  type RunId,
8
9
  type RunPruneCandidate,
9
10
  type RunSummary,
11
+ type ExecutionInstanceDto,
12
+ type SlotExecutionStateDto,
13
+ type WorkflowRunDetailDto,
10
14
  type WorkflowExecutionRepository,
11
15
  type WorkflowId,
12
16
  } from "@codemation/core";
@@ -34,6 +38,7 @@ export class InMemoryWorkflowRunRepository implements WorkflowRunRepository, Wor
34
38
  runId: args.runId,
35
39
  workflowId: args.workflowId,
36
40
  startedAt: args.startedAt,
41
+ revision: 0,
37
42
  parent: args.parent,
38
43
  executionOptions: args.executionOptions,
39
44
  control: args.control,
@@ -53,8 +58,90 @@ export class InMemoryWorkflowRunRepository implements WorkflowRunRepository, Wor
53
58
  return this.runs.get(decodeURIComponent(runId) as RunId);
54
59
  }
55
60
 
61
+ async loadSchedulingState(runId: string): Promise<PersistedRunSchedulingState | undefined> {
62
+ const state = await this.load(runId);
63
+ if (!state) {
64
+ return undefined;
65
+ }
66
+ return {
67
+ pending: state.pending ? { ...state.pending } : undefined,
68
+ queue: state.queue.map((entry) => ({ ...entry })),
69
+ };
70
+ }
71
+
56
72
  async save(state: PersistedRunState): Promise<void> {
57
- this.runs.set(state.runId, state);
73
+ this.runs.set(state.runId, { ...state, revision: (state.revision ?? 0) + 1 });
74
+ }
75
+
76
+ async loadRunDetail(runId: string): Promise<WorkflowRunDetailDto | undefined> {
77
+ const state = await this.load(runId);
78
+ if (!state) {
79
+ return undefined;
80
+ }
81
+ const slotStates: SlotExecutionStateDto[] = Object.entries(state.nodeSnapshotsByNodeId).map(
82
+ ([slotNodeId, snapshot]) => ({
83
+ slotNodeId,
84
+ latestInstanceId: snapshot.activationId
85
+ ? `${state.runId}:node:${slotNodeId}:${snapshot.activationId}`
86
+ : undefined,
87
+ latestTerminalInstanceId:
88
+ snapshot.status === "completed" || snapshot.status === "failed"
89
+ ? snapshot.activationId
90
+ ? `${state.runId}:node:${slotNodeId}:${snapshot.activationId}`
91
+ : undefined
92
+ : undefined,
93
+ latestRunningInstanceId:
94
+ snapshot.status === "queued" || snapshot.status === "running"
95
+ ? snapshot.activationId
96
+ ? `${state.runId}:node:${slotNodeId}:${snapshot.activationId}`
97
+ : undefined
98
+ : undefined,
99
+ status: snapshot.status,
100
+ invocationCount: (state.connectionInvocations ?? []).filter((inv) => inv.connectionNodeId === slotNodeId)
101
+ .length,
102
+ runCount: 1,
103
+ }),
104
+ );
105
+ const executionInstances: ExecutionInstanceDto[] = Object.entries(state.nodeSnapshotsByNodeId).map(
106
+ ([slotNodeId, snapshot]) => ({
107
+ instanceId: `${state.runId}:node:${slotNodeId}:${snapshot.activationId ?? "na"}`,
108
+ slotNodeId,
109
+ workflowNodeId: slotNodeId,
110
+ kind: "workflowNodeActivation",
111
+ runIndex: 1,
112
+ batchId: state.pending?.batchId ?? "batch_1",
113
+ activationId: snapshot.activationId,
114
+ status: snapshot.status,
115
+ queuedAt: snapshot.queuedAt,
116
+ startedAt: snapshot.startedAt,
117
+ finishedAt: snapshot.finishedAt,
118
+ itemCount: Object.values(snapshot.inputsByPort ?? {}).reduce((count, items) => count + items.length, 0),
119
+ inputJson: snapshot.inputsByPort as never,
120
+ outputJson: snapshot.outputs as never,
121
+ error: snapshot.error,
122
+ }),
123
+ );
124
+ return {
125
+ runId: state.runId,
126
+ workflowId: state.workflowId,
127
+ startedAt: state.startedAt,
128
+ finishedAt: state.finishedAt,
129
+ status: state.status,
130
+ workflowSnapshot: state.workflowSnapshot,
131
+ mutableState: state.mutableState,
132
+ slotStates,
133
+ executionInstances,
134
+ };
135
+ }
136
+
137
+ async listBinaryStorageKeys(runId: RunId): Promise<ReadonlyArray<string>> {
138
+ const state = await this.load(runId);
139
+ if (!state) {
140
+ return [];
141
+ }
142
+ const keys = new Set<string>();
143
+ this.collectBinaryKeysFromRunState(state, keys);
144
+ return [...keys].sort((left, right) => left.localeCompare(right));
58
145
  }
59
146
 
60
147
  async deleteRun(runId: RunId): Promise<void> {
@@ -91,4 +178,39 @@ export class InMemoryWorkflowRunRepository implements WorkflowRunRepository, Wor
91
178
  out.sort((a, b) => a.finishedAt.localeCompare(b.finishedAt));
92
179
  return out.slice(0, limit);
93
180
  }
181
+
182
+ private collectBinaryKeysFromRunState(state: PersistedRunState, keys: Set<string>): void {
183
+ for (const outputs of Object.values(state.outputsByNode)) {
184
+ for (const items of Object.values(outputs)) {
185
+ this.collectBinaryKeysFromItems(items, keys);
186
+ }
187
+ }
188
+ for (const snapshot of Object.values(state.nodeSnapshotsByNodeId)) {
189
+ for (const items of Object.values(snapshot.inputsByPort ?? {})) {
190
+ this.collectBinaryKeysFromItems(items, keys);
191
+ }
192
+ for (const items of Object.values(snapshot.outputs ?? {})) {
193
+ this.collectBinaryKeysFromItems(items, keys);
194
+ }
195
+ }
196
+ for (const nodeState of Object.values(state.mutableState?.nodesById ?? {})) {
197
+ for (const items of Object.values(nodeState.pinnedOutputsByPort ?? {})) {
198
+ this.collectBinaryKeysFromItems(items, keys);
199
+ }
200
+ this.collectBinaryKeysFromItems(nodeState.lastDebugInput, keys);
201
+ }
202
+ }
203
+
204
+ private collectBinaryKeysFromItems(
205
+ items: PersistedRunState["outputsByNode"][string][string] | undefined,
206
+ keys: Set<string>,
207
+ ): void {
208
+ for (const item of items ?? []) {
209
+ for (const attachment of Object.values(item.binary ?? {})) {
210
+ if (attachment.storageKey.length > 0) {
211
+ keys.add(attachment.storageKey);
212
+ }
213
+ }
214
+ }
215
+ }
94
216
  }