@you-agent-factory/factory-replay 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +21 -0
- package/README.md +58 -0
- package/package.json +26 -0
- package/src/activity.js +208 -0
- package/src/index.d.ts +324 -0
- package/src/index.js +477 -0
- package/src/progress.js +117 -0
- package/src/topology.js +398 -0
package/src/topology.js
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/** @type {Record<import("./index.d.ts").FactoryTopologyNodeKind, import("./index.d.ts").FactoryTopologyHandle[]>} */
|
|
2
|
+
const HANDLES_BY_KIND = {
|
|
3
|
+
resource: [
|
|
4
|
+
{ id: "worker-resource-source", role: "source" },
|
|
5
|
+
{ id: "workstation-resource-source", role: "source" },
|
|
6
|
+
],
|
|
7
|
+
worker: [
|
|
8
|
+
{ id: "worker-input-target", role: "target" },
|
|
9
|
+
{ id: "worker-assignment-source", role: "source" },
|
|
10
|
+
],
|
|
11
|
+
"work-state": [
|
|
12
|
+
{ id: "workstation-input-source", role: "source" },
|
|
13
|
+
{ id: "work-state-input-target", role: "target" },
|
|
14
|
+
{ id: "work-type-state-target", role: "target" },
|
|
15
|
+
],
|
|
16
|
+
"work-type": [{ id: "work-type-state-source", role: "source" }],
|
|
17
|
+
workstation: [
|
|
18
|
+
{ id: "workstation-input-target", role: "target" },
|
|
19
|
+
{ id: "worker-assignment-target", role: "target" },
|
|
20
|
+
{ id: "workstation-resource-target", role: "target" },
|
|
21
|
+
{ id: "workstation-output-source", role: "source" },
|
|
22
|
+
{ id: "workstation-on-continue-source", role: "source" },
|
|
23
|
+
{ id: "workstation-on-failure-source", role: "source" },
|
|
24
|
+
{ id: "workstation-on-rejection-source", role: "source" },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const CONNECTION_HANDLES = {
|
|
29
|
+
"worker-assignment": [
|
|
30
|
+
"worker-assignment-source",
|
|
31
|
+
"worker-assignment-target",
|
|
32
|
+
],
|
|
33
|
+
"worker-resource": ["worker-resource-source", "worker-input-target"],
|
|
34
|
+
"workstation-input": [
|
|
35
|
+
"workstation-input-source",
|
|
36
|
+
"workstation-input-target",
|
|
37
|
+
],
|
|
38
|
+
"workstation-on-continue": [
|
|
39
|
+
"workstation-on-continue-source",
|
|
40
|
+
"work-state-input-target",
|
|
41
|
+
],
|
|
42
|
+
"workstation-on-failure": [
|
|
43
|
+
"workstation-on-failure-source",
|
|
44
|
+
"work-state-input-target",
|
|
45
|
+
],
|
|
46
|
+
"workstation-on-rejection": [
|
|
47
|
+
"workstation-on-rejection-source",
|
|
48
|
+
"work-state-input-target",
|
|
49
|
+
],
|
|
50
|
+
"workstation-output": [
|
|
51
|
+
"workstation-output-source",
|
|
52
|
+
"work-state-input-target",
|
|
53
|
+
],
|
|
54
|
+
"workstation-resource": [
|
|
55
|
+
"workstation-resource-source",
|
|
56
|
+
"workstation-resource-target",
|
|
57
|
+
],
|
|
58
|
+
"work-type-state": ["work-type-state-source", "work-type-state-target"],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const RESOURCE_AVAILABILITY_STATE = "available";
|
|
62
|
+
|
|
63
|
+
/** @param {string | undefined} explicitID @param {string} name */
|
|
64
|
+
function entityID(explicitID, name) {
|
|
65
|
+
return explicitID?.trim() || name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {import("./index.d.ts").FactoryTopologyNodeKind} kind @param {string} id */
|
|
69
|
+
function nodeID(kind, id) {
|
|
70
|
+
return `${kind}:${id}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {import("./index.d.ts").FactoryTopologyNodeKind} kind
|
|
75
|
+
* @returns {import("./index.d.ts").FactoryTopologyHandle[]}
|
|
76
|
+
*/
|
|
77
|
+
function handlesFor(kind) {
|
|
78
|
+
return HANDLES_BY_KIND[kind].map((handle) => ({ ...handle }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @typedef {NonNullable<import("@you-agent-factory/client").FactoryDefinition["workstations"]>[number]["inputs"][number]} WorkstationIO
|
|
83
|
+
* @typedef {import("@you-agent-factory/client").FactoryDefinition} FactoryDefinition
|
|
84
|
+
* @typedef {{
|
|
85
|
+
* connections: Map<string, import("./index.d.ts").FactoryTopologyConnection>,
|
|
86
|
+
* issues: Map<string, import("./index.d.ts").FactoryTopologyProjectionIssue>,
|
|
87
|
+
* nodes: Map<string, import("./index.d.ts").FactoryTopologyNode>,
|
|
88
|
+
* resourcesByName: Map<string, string>,
|
|
89
|
+
* workersByName: Map<string, string>,
|
|
90
|
+
* workStatesByName: Map<string, Map<string, string>>,
|
|
91
|
+
* workTypesByName: Map<string, string>,
|
|
92
|
+
* }} TopologyContext
|
|
93
|
+
*/
|
|
94
|
+
|
|
95
|
+
/** @param {unknown} value @returns {WorkstationIO[]} */
|
|
96
|
+
function asRouteList(value) {
|
|
97
|
+
if (!value) return [];
|
|
98
|
+
return /** @type {WorkstationIO[]} */ (Array.isArray(value) ? value : [value]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** @returns {TopologyContext} */
|
|
102
|
+
function createTopologyContext() {
|
|
103
|
+
return {
|
|
104
|
+
connections: new Map(),
|
|
105
|
+
issues: new Map(),
|
|
106
|
+
nodes: new Map(),
|
|
107
|
+
resourcesByName: new Map(),
|
|
108
|
+
workersByName: new Map(),
|
|
109
|
+
workStatesByName: new Map(),
|
|
110
|
+
workTypesByName: new Map(),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** @param {TopologyContext} context @param {import("./index.d.ts").FactoryTopologyNode} node */
|
|
115
|
+
function addNode(context, node) {
|
|
116
|
+
if (context.nodes.has(node.id)) {
|
|
117
|
+
const id = `duplicate-entity:${node.id}`;
|
|
118
|
+
context.issues.set(id, {
|
|
119
|
+
code: "DUPLICATE_ENTITY_ID",
|
|
120
|
+
id,
|
|
121
|
+
message: `Factory topology contains duplicate node id ${node.id}.`,
|
|
122
|
+
nodeId: node.id,
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
context.nodes.set(node.id, node);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {TopologyContext} context
|
|
131
|
+
* @param {import("./index.d.ts").FactoryTopologyConnectionKind} kind
|
|
132
|
+
* @param {string | undefined} sourceId
|
|
133
|
+
* @param {string | undefined} targetId
|
|
134
|
+
* @param {string} sourceReference
|
|
135
|
+
* @param {string} targetReference
|
|
136
|
+
*/
|
|
137
|
+
function addConnection(
|
|
138
|
+
context,
|
|
139
|
+
kind,
|
|
140
|
+
sourceId,
|
|
141
|
+
targetId,
|
|
142
|
+
sourceReference,
|
|
143
|
+
targetReference,
|
|
144
|
+
) {
|
|
145
|
+
if (
|
|
146
|
+
!sourceId ||
|
|
147
|
+
!targetId ||
|
|
148
|
+
!context.nodes.has(sourceId) ||
|
|
149
|
+
!context.nodes.has(targetId)
|
|
150
|
+
) {
|
|
151
|
+
const id = `unresolved-connection:${kind}:${sourceReference}->${targetReference}`;
|
|
152
|
+
context.issues.set(id, {
|
|
153
|
+
code: "UNRESOLVED_CONNECTION",
|
|
154
|
+
connectionKind: kind,
|
|
155
|
+
id,
|
|
156
|
+
message: `Cannot resolve ${kind} connection from ${sourceReference} to ${targetReference}.`,
|
|
157
|
+
sourceReference,
|
|
158
|
+
targetReference,
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const [sourceHandle, targetHandle] = CONNECTION_HANDLES[kind];
|
|
163
|
+
const id = `${kind}:${sourceId}->${targetId}`;
|
|
164
|
+
context.connections.set(id, {
|
|
165
|
+
id,
|
|
166
|
+
kind,
|
|
167
|
+
source: { handleId: sourceHandle, nodeId: sourceId },
|
|
168
|
+
target: { handleId: targetHandle, nodeId: targetId },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @param {FactoryDefinition} factory @param {TopologyContext} context */
|
|
173
|
+
function appendResourceAndWorkerNodes(factory, context) {
|
|
174
|
+
for (const resource of factory.resources ?? []) {
|
|
175
|
+
const id = entityID(resource.id, resource.name);
|
|
176
|
+
context.resourcesByName.set(resource.name, id);
|
|
177
|
+
addNode(context, {
|
|
178
|
+
capacity: resource.capacity,
|
|
179
|
+
entityId: id,
|
|
180
|
+
handles: handlesFor("resource"),
|
|
181
|
+
id: nodeID("resource", id),
|
|
182
|
+
kind: "resource",
|
|
183
|
+
label: resource.name,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
for (const worker of factory.workers ?? []) {
|
|
187
|
+
const id = entityID(worker.id, worker.name);
|
|
188
|
+
context.workersByName.set(worker.name, id);
|
|
189
|
+
addNode(context, {
|
|
190
|
+
entityId: id,
|
|
191
|
+
handles: handlesFor("worker"),
|
|
192
|
+
id: nodeID("worker", id),
|
|
193
|
+
kind: "worker",
|
|
194
|
+
label: worker.name,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** @param {FactoryDefinition} factory @param {TopologyContext} context */
|
|
200
|
+
function appendWorkTypeNodes(factory, context) {
|
|
201
|
+
for (const workType of factory.workTypes ?? []) {
|
|
202
|
+
const id = entityID(workType.id, workType.name);
|
|
203
|
+
context.workTypesByName.set(workType.name, id);
|
|
204
|
+
addNode(context, {
|
|
205
|
+
entityId: id,
|
|
206
|
+
handles: handlesFor("work-type"),
|
|
207
|
+
id: nodeID("work-type", id),
|
|
208
|
+
kind: "work-type",
|
|
209
|
+
label: workType.name,
|
|
210
|
+
});
|
|
211
|
+
const states = new Map();
|
|
212
|
+
for (const state of workType.states ?? []) {
|
|
213
|
+
const idForState = `${id}:${entityID(state.id, state.name)}`;
|
|
214
|
+
states.set(state.name, idForState);
|
|
215
|
+
addNode(context, {
|
|
216
|
+
category: state.type,
|
|
217
|
+
entityId: idForState,
|
|
218
|
+
handles: handlesFor("work-state"),
|
|
219
|
+
id: nodeID("work-state", idForState),
|
|
220
|
+
kind: "work-state",
|
|
221
|
+
label: state.name,
|
|
222
|
+
workTypeId: id,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
context.workStatesByName.set(workType.name, states);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @param {FactoryDefinition} factory @param {TopologyContext} context */
|
|
230
|
+
function appendWorkstationNodes(factory, context) {
|
|
231
|
+
for (const workstation of factory.workstations ?? []) {
|
|
232
|
+
const id = entityID(workstation.id, workstation.name);
|
|
233
|
+
addNode(context, {
|
|
234
|
+
entityId: id,
|
|
235
|
+
handles: handlesFor("workstation"),
|
|
236
|
+
id: nodeID("workstation", id),
|
|
237
|
+
kind: "workstation",
|
|
238
|
+
label: workstation.name,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** @param {FactoryDefinition} factory @param {TopologyContext} context */
|
|
244
|
+
function appendWorkerAndWorkTypeConnections(factory, context) {
|
|
245
|
+
for (const worker of factory.workers ?? []) {
|
|
246
|
+
const workerId = context.workersByName.get(worker.name);
|
|
247
|
+
for (const resource of worker.resources ?? []) {
|
|
248
|
+
const resourceId = context.resourcesByName.get(resource.name);
|
|
249
|
+
addConnection(
|
|
250
|
+
context,
|
|
251
|
+
"worker-resource",
|
|
252
|
+
resourceId && nodeID("resource", resourceId),
|
|
253
|
+
workerId && nodeID("worker", workerId),
|
|
254
|
+
resource.name,
|
|
255
|
+
worker.name,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
for (const workType of factory.workTypes ?? []) {
|
|
260
|
+
const workTypeId = context.workTypesByName.get(workType.name);
|
|
261
|
+
for (const state of workType.states ?? []) {
|
|
262
|
+
const stateId = context.workStatesByName
|
|
263
|
+
.get(workType.name)
|
|
264
|
+
?.get(state.name);
|
|
265
|
+
addConnection(
|
|
266
|
+
context,
|
|
267
|
+
"work-type-state",
|
|
268
|
+
workTypeId && nodeID("work-type", workTypeId),
|
|
269
|
+
stateId && nodeID("work-state", stateId),
|
|
270
|
+
workType.name,
|
|
271
|
+
`${workType.name}:${state.name}`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @param {TopologyContext} context
|
|
279
|
+
* @param {NonNullable<FactoryDefinition["workstations"]>[number]} workstation
|
|
280
|
+
* @param {string} workstationId
|
|
281
|
+
*/
|
|
282
|
+
function appendWorkstationRoutes(context, workstation, workstationId) {
|
|
283
|
+
/** @type {Array<[import("./index.d.ts").FactoryTopologyConnectionKind, WorkstationIO[]]>} */
|
|
284
|
+
const routes = [
|
|
285
|
+
["workstation-input", asRouteList(workstation.inputs)],
|
|
286
|
+
["workstation-output", asRouteList(workstation.outputs)],
|
|
287
|
+
["workstation-on-continue", asRouteList(workstation.onContinue)],
|
|
288
|
+
["workstation-on-failure", asRouteList(workstation.onFailure)],
|
|
289
|
+
["workstation-on-rejection", asRouteList(workstation.onRejection)],
|
|
290
|
+
[
|
|
291
|
+
"workstation-output",
|
|
292
|
+
(workstation.classificationRoutes ?? []).flatMap(
|
|
293
|
+
(route) => route.outputs ?? [],
|
|
294
|
+
),
|
|
295
|
+
],
|
|
296
|
+
];
|
|
297
|
+
for (const [kind, ios] of routes) {
|
|
298
|
+
for (const io of ios) {
|
|
299
|
+
const stateId = context.workStatesByName.get(io.workType)?.get(io.state);
|
|
300
|
+
// Canonical backend snapshots include internal resource acquisition and
|
|
301
|
+
// release arcs in workstation IO. Resource relationships are projected
|
|
302
|
+
// separately and must not become public Work-State routes or issues.
|
|
303
|
+
if (
|
|
304
|
+
!stateId &&
|
|
305
|
+
io.state === RESOURCE_AVAILABILITY_STATE &&
|
|
306
|
+
context.resourcesByName.has(io.workType)
|
|
307
|
+
) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const workStateId = stateId && nodeID("work-state", stateId);
|
|
311
|
+
const input = kind === "workstation-input";
|
|
312
|
+
addConnection(
|
|
313
|
+
context,
|
|
314
|
+
kind,
|
|
315
|
+
input ? workStateId : workstationId,
|
|
316
|
+
input ? workstationId : workStateId,
|
|
317
|
+
input ? `${io.workType}:${io.state}` : workstation.name,
|
|
318
|
+
input ? workstation.name : `${io.workType}:${io.state}`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** @param {FactoryDefinition} factory @param {TopologyContext} context */
|
|
325
|
+
function appendWorkstationConnections(factory, context) {
|
|
326
|
+
for (const workstation of factory.workstations ?? []) {
|
|
327
|
+
const workstationId = nodeID(
|
|
328
|
+
"workstation",
|
|
329
|
+
entityID(workstation.id, workstation.name),
|
|
330
|
+
);
|
|
331
|
+
const workerId = context.workersByName.get(workstation.worker);
|
|
332
|
+
if (workstation.worker?.trim()) {
|
|
333
|
+
addConnection(
|
|
334
|
+
context,
|
|
335
|
+
"worker-assignment",
|
|
336
|
+
workerId && nodeID("worker", workerId),
|
|
337
|
+
workstationId,
|
|
338
|
+
workstation.worker,
|
|
339
|
+
workstation.name,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
for (const resource of workstation.resources ?? []) {
|
|
343
|
+
const resourceId = context.resourcesByName.get(resource.name);
|
|
344
|
+
addConnection(
|
|
345
|
+
context,
|
|
346
|
+
"workstation-resource",
|
|
347
|
+
resourceId && nodeID("resource", resourceId),
|
|
348
|
+
workstationId,
|
|
349
|
+
resource.name,
|
|
350
|
+
workstation.name,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
appendWorkstationRoutes(context, workstation, workstationId);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Project one public Factory definition without mutating caller-owned data.
|
|
359
|
+
*
|
|
360
|
+
* @param {import("./index.d.ts").FactoryTopologyProjectionInput} input
|
|
361
|
+
* @returns {import("./index.d.ts").FactoryTopologyProjection}
|
|
362
|
+
*/
|
|
363
|
+
export function projectFactoryTopology(input) {
|
|
364
|
+
const factory = input.factory;
|
|
365
|
+
if (!factory) {
|
|
366
|
+
return {
|
|
367
|
+
connections: [],
|
|
368
|
+
issues: [
|
|
369
|
+
{
|
|
370
|
+
code: "MISSING_FACTORY",
|
|
371
|
+
id: "missing-factory",
|
|
372
|
+
message: "No Factory topology is available at the selected tick.",
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
nodes: [],
|
|
376
|
+
selectedTick: input.selectedTick,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const context = createTopologyContext();
|
|
380
|
+
appendResourceAndWorkerNodes(factory, context);
|
|
381
|
+
appendWorkTypeNodes(factory, context);
|
|
382
|
+
appendWorkstationNodes(factory, context);
|
|
383
|
+
appendWorkerAndWorkTypeConnections(factory, context);
|
|
384
|
+
appendWorkstationConnections(factory, context);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
connections: [...context.connections.values()].sort((left, right) =>
|
|
388
|
+
left.id.localeCompare(right.id),
|
|
389
|
+
),
|
|
390
|
+
issues: [...context.issues.values()].sort((left, right) =>
|
|
391
|
+
left.id.localeCompare(right.id),
|
|
392
|
+
),
|
|
393
|
+
nodes: [...context.nodes.values()].sort((left, right) =>
|
|
394
|
+
left.id.localeCompare(right.id),
|
|
395
|
+
),
|
|
396
|
+
selectedTick: input.selectedTick,
|
|
397
|
+
};
|
|
398
|
+
}
|