@you-agent-factory/factory-emulator 0.0.0 → 0.0.1

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 (69) hide show
  1. package/LICENSE.md +19 -1
  2. package/README.md +305 -286
  3. package/dist/compatibility.d.ts +19 -0
  4. package/dist/compatibility.js +156 -0
  5. package/dist/event-sink.d.ts +29 -0
  6. package/dist/event-sink.js +58 -0
  7. package/dist/generated/scenario-schema.d.ts +268 -0
  8. package/dist/generated/scenario-schema.js +319 -0
  9. package/dist/index.d.ts +8 -0
  10. package/dist/index.js +8 -0
  11. package/dist/logical-move.d.ts +10 -0
  12. package/dist/logical-move.js +18 -0
  13. package/dist/recording-sink.d.ts +22 -0
  14. package/dist/recording-sink.js +122 -0
  15. package/dist/runtime-reference-conformance.d.ts +20 -0
  16. package/dist/runtime-reference-conformance.js +138 -0
  17. package/dist/runtime-reference-evidence.d.ts +9 -0
  18. package/dist/runtime-reference-evidence.js +193 -0
  19. package/dist/runtime-reference-fixtures.d.ts +7 -0
  20. package/dist/runtime-reference-fixtures.js +308 -0
  21. package/dist/runtime-reference.d.ts +48 -0
  22. package/dist/runtime-reference.js +143 -0
  23. package/dist/scenario-contracts.d.ts +81 -0
  24. package/dist/scenario-contracts.js +11 -0
  25. package/dist/scenario.d.ts +271 -0
  26. package/dist/scenario.js +333 -0
  27. package/dist/scheduler.d.ts +24 -0
  28. package/dist/scheduler.js +104 -0
  29. package/dist/session-contracts.d.ts +262 -0
  30. package/dist/session-contracts.js +74 -0
  31. package/dist/session.d.ts +4 -0
  32. package/dist/session.js +1490 -0
  33. package/dist/submission-replay.d.ts +14 -0
  34. package/dist/submission-replay.js +96 -0
  35. package/dist/submission-validation.d.ts +3 -0
  36. package/dist/submission-validation.js +113 -0
  37. package/examples/customer-support.scenario.v1.json +45 -0
  38. package/package.json +44 -22
  39. package/schema/scenario.schema.json +171 -0
  40. package/generated/factory-emulator-scenario.schema.json +0 -191
  41. package/generated/factory.schema.json +0 -2606
  42. package/src/contracts.ts +0 -41
  43. package/src/data-error.js +0 -21
  44. package/src/data-only.js +0 -208
  45. package/src/emulator.js +0 -195
  46. package/src/emulator.ts +0 -86
  47. package/src/examples.js +0 -86
  48. package/src/examples.ts +0 -11
  49. package/src/generated/factory-schema.d.ts +0 -3
  50. package/src/generated/factory-schema.js +0 -5
  51. package/src/generated/scenario-schema.d.ts +0 -4
  52. package/src/generated/scenario-schema.js +0 -6
  53. package/src/generated/scenario.ts +0 -103
  54. package/src/identity.js +0 -55
  55. package/src/index.js +0 -34
  56. package/src/index.ts +0 -102
  57. package/src/limits.js +0 -125
  58. package/src/parser.js +0 -113
  59. package/src/parser.ts +0 -56
  60. package/src/scheduler.js +0 -374
  61. package/src/semantics.js +0 -354
  62. package/src/semantics.ts +0 -38
  63. package/src/session.js +0 -1201
  64. package/src/session.ts +0 -324
  65. package/src/sinks.js +0 -118
  66. package/src/sinks.ts +0 -56
  67. package/src/support.js +0 -334
  68. package/src/support.ts +0 -15
  69. package/src/virtual-time.js +0 -36
@@ -0,0 +1,1490 @@
1
+ import { inspectFactoryEmulatorCompatibility } from "./compatibility.js";
2
+ import { visitCountGuardsAllow, visitsAfterTransition, } from "./logical-move.js";
3
+ import { safeParseFactoryEmulatorScenario } from "./scenario.js";
4
+ import { compareFactorySchedulerCandidates, FACTORY_EMULATOR_SCHEDULER_CANDIDATE_LIMIT, selectFactorySchedulerCandidates, } from "./scheduler.js";
5
+ import { DEFAULT_FACTORY_EMULATOR_LIMITS, FACTORY_EMULATOR_LIMIT_HARD_CAPS, FactoryEmulatorConfigurationError, FactoryEmulatorDurationError, FactoryEmulatorExecutionPausedError, FactoryEmulatorLifecycleError, FactoryEmulatorPendingCommandError, FactoryEmulatorSubmissionError, } from "./session-contracts.js";
6
+ export * from "./session-contracts.js";
7
+ const LIMIT_NAMES = Object.keys(DEFAULT_FACTORY_EMULATOR_LIMITS);
8
+ const PRE_START_STATE = {
9
+ lifecycle: "pre-start",
10
+ virtualElapsedMs: 0,
11
+ counters: { commands: 0, events: 0, completedDispatches: 0 },
12
+ };
13
+ function isSubmissionArray(value) {
14
+ return Array.isArray(value);
15
+ }
16
+ function clone(value) {
17
+ return JSON.parse(JSON.stringify(value));
18
+ }
19
+ function canonicalJson(value) {
20
+ return JSON.stringify(value, (_key, nested) => {
21
+ if (nested === null ||
22
+ typeof nested !== "object" ||
23
+ Array.isArray(nested)) {
24
+ return nested;
25
+ }
26
+ return Object.fromEntries(Object.entries(nested).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
27
+ });
28
+ }
29
+ function canonicalClone(value) {
30
+ return JSON.parse(canonicalJson(value));
31
+ }
32
+ function resolveLimits(configured, diagnostics) {
33
+ const limits = { ...DEFAULT_FACTORY_EMULATOR_LIMITS, ...configured };
34
+ for (const name of LIMIT_NAMES) {
35
+ const value = limits[name];
36
+ const hardCap = FACTORY_EMULATOR_LIMIT_HARD_CAPS[name];
37
+ if (!Number.isSafeInteger(value) || value < 1 || value > hardCap) {
38
+ diagnostics.push({
39
+ code: "invalid_limit",
40
+ path: ["limits", name],
41
+ message: `${name} must be a positive safe integer no greater than ${hardCap}.`,
42
+ });
43
+ }
44
+ }
45
+ return Object.freeze(limits);
46
+ }
47
+ function validateConfiguration(options) {
48
+ const diagnostics = [];
49
+ const factory = options?.factory;
50
+ if (factory === null ||
51
+ typeof factory !== "object" ||
52
+ typeof factory.name !== "string" ||
53
+ factory.name.trim() === "") {
54
+ diagnostics.push({
55
+ code: "invalid_factory",
56
+ path: ["factory"],
57
+ message: "factory must be a Factory definition with a non-empty name.",
58
+ });
59
+ }
60
+ let scenario;
61
+ if (diagnostics.length === 0) {
62
+ const parsed = safeParseFactoryEmulatorScenario(options.scenario, factory);
63
+ if (parsed.success)
64
+ scenario = parsed.data;
65
+ else
66
+ diagnostics.push(...parsed.issues);
67
+ }
68
+ if (diagnostics.length === 0) {
69
+ const compatibility = inspectFactoryEmulatorCompatibility(factory);
70
+ if (!compatibility.supported) {
71
+ diagnostics.push(...compatibility.diagnostics.map((issue) => ({
72
+ code: "incompatible_factory",
73
+ path: ["factory", ...issue.path],
74
+ message: issue.message,
75
+ })));
76
+ }
77
+ }
78
+ if (options?.sink === null || typeof options?.sink?.write !== "function") {
79
+ diagnostics.push({
80
+ code: "invalid_sink",
81
+ path: ["sink"],
82
+ message: "sink must provide an asynchronous write(events) function.",
83
+ });
84
+ }
85
+ if (options?.yieldControl !== undefined &&
86
+ typeof options.yieldControl !== "function") {
87
+ diagnostics.push({
88
+ code: "invalid_yield_control",
89
+ path: ["yieldControl"],
90
+ message: "yieldControl must be a function when provided.",
91
+ });
92
+ }
93
+ const limits = resolveLimits(options?.limits, diagnostics);
94
+ if (diagnostics.length > 0 || scenario === undefined) {
95
+ throw new FactoryEmulatorConfigurationError(diagnostics);
96
+ }
97
+ return {
98
+ factory: canonicalClone(factory),
99
+ scenario: canonicalClone(scenario),
100
+ sink: options.sink,
101
+ limits,
102
+ yieldControl: options.yieldControl,
103
+ };
104
+ }
105
+ function deterministicHash(value) {
106
+ let hash = 0xcbf29ce484222325n;
107
+ for (const character of value) {
108
+ hash ^= BigInt(character.codePointAt(0) ?? 0);
109
+ hash = BigInt.asUintN(64, hash * 0x100000001b3n);
110
+ }
111
+ return hash.toString(16).padStart(16, "0");
112
+ }
113
+ function identity(kind, ...coordinates) {
114
+ return `emulator-${kind}-${deterministicHash(canonicalJson(coordinates))}`;
115
+ }
116
+ function sessionIdentity(factory, scenario) {
117
+ return identity("session", factory.id ?? factory.name, scenario);
118
+ }
119
+ function virtualTimeAt(scenario, elapsedMs) {
120
+ const timestamp = Date.parse(scenario.startAt) + elapsedMs;
121
+ const instant = new Date(timestamp);
122
+ if (!Number.isSafeInteger(elapsedMs) ||
123
+ !Number.isFinite(timestamp) ||
124
+ !Number.isFinite(instant.getTime())) {
125
+ throw new FactoryEmulatorDurationError(elapsedMs);
126
+ }
127
+ return instant.toISOString();
128
+ }
129
+ function eventContext(state, sequence, eventTime, context = {}) {
130
+ return {
131
+ sequence,
132
+ tick: state.counters.commands,
133
+ eventTime,
134
+ sessionId: state.sessionId,
135
+ sessionSequence: sequence,
136
+ orchestratorKind: "PETRI",
137
+ source: "emulator",
138
+ ...context,
139
+ };
140
+ }
141
+ function bootstrapEvents(factory, scenario, sessionId) {
142
+ const eventTime = scenario.startAt;
143
+ const context = (sequence) => ({
144
+ sequence,
145
+ tick: 0,
146
+ eventTime,
147
+ sessionId,
148
+ sessionSequence: sequence,
149
+ orchestratorKind: "PETRI",
150
+ source: "emulator",
151
+ });
152
+ return [
153
+ {
154
+ schemaVersion: "agent-factory.event.v1",
155
+ id: `${sessionId}/event/0/run-request`,
156
+ type: "RUN_REQUEST",
157
+ context: context(0),
158
+ payload: { recordedAt: eventTime, factory: clone(factory) },
159
+ },
160
+ {
161
+ schemaVersion: "agent-factory.event.v1",
162
+ id: `${sessionId}/event/1/initial-structure-request`,
163
+ type: "INITIAL_STRUCTURE_REQUEST",
164
+ context: context(1),
165
+ payload: { factory: clone(factory) },
166
+ },
167
+ {
168
+ schemaVersion: "agent-factory.event.v1",
169
+ id: `${sessionId}/event/2/session-started`,
170
+ type: "SESSION_STARTED",
171
+ context: context(2),
172
+ payload: { factoryId: factory.id ?? factory.name, startedAt: eventTime },
173
+ },
174
+ ];
175
+ }
176
+ function ruleMatches(rule, submissions, workstation) {
177
+ const selector = rule.selector;
178
+ return ((selector.workstation === undefined ||
179
+ selector.workstation === workstation.name) &&
180
+ (selector.worker === undefined || selector.worker === workstation.worker) &&
181
+ (selector.input === undefined ||
182
+ submissions.some((submission) => (selector.input?.workType === undefined ||
183
+ selector.input.workType === submission.workType) &&
184
+ (selector.input?.state === undefined ||
185
+ selector.input.state === submission.state) &&
186
+ (selector.input?.name === undefined ||
187
+ selector.input.name === submission.name))));
188
+ }
189
+ function executionFor(configuration, submissions, workstation, invocationIndex = 0) {
190
+ if (workstation.type === "LOGICAL_MOVE") {
191
+ return {
192
+ workstation,
193
+ outcome: { result: "accepted", durationMs: 0 },
194
+ };
195
+ }
196
+ const rule = configuration.scenario.rules.find((candidate) => ruleMatches(candidate, submissions, workstation));
197
+ if (rule === undefined) {
198
+ return configuration.scenario.unmatched.behavior === "outcome"
199
+ ? { workstation, outcome: configuration.scenario.unmatched.outcome }
200
+ : { workstation };
201
+ }
202
+ const outcome = rule.outcomes[invocationIndex] ??
203
+ (rule.exhaustion === "repeat-last" ? rule.outcomes.at(-1) : undefined);
204
+ return { rule, workstation, outcome };
205
+ }
206
+ function submissionForWork(work) {
207
+ return {
208
+ name: work.submissionId,
209
+ workType: work.workType,
210
+ state: work.state,
211
+ ...(work.input === undefined ? {} : { input: work.input }),
212
+ ...(work.parent === undefined ? {} : { parent: work.parent }),
213
+ };
214
+ }
215
+ function bindingIndexesFor(state, workstation, observeExpansion, visit) {
216
+ const inputs = workstation.inputs.map((input) => state.works.flatMap((work, index) => work.phase === "ready" &&
217
+ work.workType === input.workType &&
218
+ work.state === input.state
219
+ ? [index]
220
+ : []));
221
+ if (inputs.some((matches) => matches.length === 0))
222
+ return;
223
+ const append = (slot, selected) => {
224
+ if (slot === inputs.length) {
225
+ visit(selected);
226
+ return;
227
+ }
228
+ for (const index of inputs[slot] ?? []) {
229
+ if (!selected.includes(index)) {
230
+ observeExpansion();
231
+ append(slot + 1, [...selected, index]);
232
+ }
233
+ }
234
+ };
235
+ append(0, []);
236
+ }
237
+ function dependencyStatesFor(state) {
238
+ const current = new Map();
239
+ for (const work of state.works) {
240
+ if (work.phase !== "active")
241
+ current.set(work.workId, work.state);
242
+ else
243
+ current.delete(work.workId);
244
+ }
245
+ return current;
246
+ }
247
+ function dependenciesSatisfied(work, dependencyStates) {
248
+ return (work.relations?.every(({ targetWorkId, requiredState }) => dependencyStates.get(targetWorkId) === requiredState) ?? true);
249
+ }
250
+ function dependenciesAllow(works, indexes, dependencyStates, executableWork) {
251
+ if (works.every((work) => dependenciesSatisfied(work, dependencyStates))) {
252
+ return true;
253
+ }
254
+ for (const index of indexes)
255
+ executableWork.add(index);
256
+ return false;
257
+ }
258
+ function potentiallyBoundIndexes(state, workstation) {
259
+ return state.works.flatMap((work, index) => work.phase === "ready" &&
260
+ workstation.inputs.some((input) => input.workType === work.workType && input.state === work.state)
261
+ ? [index]
262
+ : []);
263
+ }
264
+ function schedulerCandidatesFor(configuration, state, cursors, assertBindingBudget) {
265
+ const candidates = [];
266
+ const executableWork = new Set();
267
+ const dependencyStates = dependencyStatesFor(state);
268
+ let observedExpansions = 0;
269
+ const retainBoundedCandidate = (candidate) => {
270
+ candidates.push(candidate);
271
+ candidates.sort(compareFactorySchedulerCandidates);
272
+ if (candidates.length > FACTORY_EMULATOR_SCHEDULER_CANDIDATE_LIMIT) {
273
+ candidates.pop();
274
+ }
275
+ };
276
+ for (const workstation of configuration.factory.workstations ?? []) {
277
+ const potentiallyBound = potentiallyBoundIndexes(state, workstation);
278
+ let foundBinding = false;
279
+ const seenBindings = new Set();
280
+ bindingIndexesFor(state, workstation, () => {
281
+ observedExpansions += 1;
282
+ assertBindingBudget(observedExpansions);
283
+ }, (indexes) => {
284
+ foundBinding = true;
285
+ const key = [...indexes]
286
+ .map((index) => state.works[index]?.tokenId ?? "")
287
+ .sort()
288
+ .join("\u0000");
289
+ if (seenBindings.has(key))
290
+ return;
291
+ seenBindings.add(key);
292
+ const works = indexes.flatMap((index) => {
293
+ const work = state.works[index];
294
+ return work === undefined ? [] : [work];
295
+ });
296
+ const primary = works[0];
297
+ if (primary === undefined || works.length !== workstation.inputs.length)
298
+ return;
299
+ if (!dependenciesAllow(works, indexes, dependencyStates, executableWork))
300
+ return;
301
+ if (workstation.type === "LOGICAL_MOVE" &&
302
+ !visitCountGuardsAllow(workstation.guards ?? [], primary.visits)) {
303
+ return;
304
+ }
305
+ const submissions = works.map(submissionForWork);
306
+ const initial = executionFor(configuration, submissions, workstation);
307
+ const lineage = [
308
+ ...new Set(works.map(({ rootWorkId }) => rootWorkId)),
309
+ ].sort();
310
+ const cursorKey = initial.rule === undefined
311
+ ? undefined
312
+ : `${initial.rule.id}:${canonicalJson(lineage)}`;
313
+ const invocation = cursorKey === undefined ? 0 : (cursors[cursorKey] ?? 0);
314
+ const execution = executionFor(configuration, submissions, workstation, invocation);
315
+ if (execution.outcome === undefined)
316
+ return;
317
+ for (const index of indexes)
318
+ executableWork.add(index);
319
+ retainBoundedCandidate({
320
+ transitionId: workstation.name,
321
+ workerId: workstation.worker,
322
+ workstationKind: workstation.type === "LOGICAL_MOVE" ? "logical" : "normal",
323
+ resources: (workstation.resources ?? []).map(({ name, capacity }) => ({
324
+ name,
325
+ capacity,
326
+ })),
327
+ tokens: works.map((work) => ({
328
+ tokenId: work.tokenId,
329
+ customerWork: true,
330
+ processing: configuration.factory.workTypes
331
+ ?.find(({ name }) => name === work.workType)
332
+ ?.states.find(({ name }) => name === work.state)?.type ===
333
+ "PROCESSING",
334
+ queuedElapsedMs: work.queuedElapsedMs,
335
+ ...(work.lastDispatchElapsedMs === undefined
336
+ ? {}
337
+ : { lastDispatchElapsedMs: work.lastDispatchElapsedMs }),
338
+ })),
339
+ value: {
340
+ indexes,
341
+ execution,
342
+ invocation,
343
+ ...(cursorKey === undefined ? {} : { cursorKey }),
344
+ },
345
+ });
346
+ });
347
+ if (!foundBinding) {
348
+ for (const index of potentiallyBound)
349
+ executableWork.add(index);
350
+ }
351
+ }
352
+ return { candidates, executableWork };
353
+ }
354
+ function availableResourcesFor(configuration, state) {
355
+ const allocated = state.works.reduce((claims, work) => {
356
+ if (work.phase !== "active" || work.dispatch === undefined)
357
+ return claims;
358
+ for (const resource of work.dispatch.resources) {
359
+ claims[resource.name] =
360
+ (claims[resource.name] ?? 0) + resource.capacity;
361
+ }
362
+ return claims;
363
+ }, {});
364
+ return Object.fromEntries((configuration.factory.resources ?? []).map((resource) => [
365
+ resource.name,
366
+ Math.max(0, resource.capacity - (allocated[resource.name] ?? 0)),
367
+ ]));
368
+ }
369
+ function eventResourcesFor(configuration, claims) {
370
+ const totals = new Map((configuration.factory.resources ?? []).map(({ name, capacity }) => [
371
+ name,
372
+ capacity,
373
+ ]));
374
+ return claims.map(({ name }) => ({ name, capacity: totals.get(name) ?? 0 }));
375
+ }
376
+ function defaultFailureRoutesFor(configuration, workstation) {
377
+ const routes = [];
378
+ const seen = new Set();
379
+ for (const input of workstation.inputs) {
380
+ const failedState = configuration.factory.workTypes
381
+ ?.find(({ name }) => name === input.workType)
382
+ ?.states.find(({ type }) => type === "FAILED")?.name;
383
+ if (failedState === undefined)
384
+ continue;
385
+ const key = `${input.workType}\u0000${failedState}`;
386
+ if (seen.has(key))
387
+ continue;
388
+ seen.add(key);
389
+ routes.push({ workType: input.workType, state: failedState });
390
+ }
391
+ return routes;
392
+ }
393
+ function outcomeRoutesFor(configuration, workstation, result) {
394
+ if (result === "accepted")
395
+ return workstation.outputs ?? [];
396
+ if (result === "continued")
397
+ return workstation.onContinue ?? [];
398
+ if (result === "failed" && (workstation.onFailure?.length ?? 0) > 0) {
399
+ return workstation.onFailure ?? [];
400
+ }
401
+ if (result === "rejected" && (workstation.onRejection?.length ?? 0) > 0) {
402
+ return workstation.onRejection ?? [];
403
+ }
404
+ if (result === "rejected" && workstation.behavior === "REPEATER") {
405
+ return workstation.inputs;
406
+ }
407
+ return defaultFailureRoutesFor(configuration, workstation);
408
+ }
409
+ function routedWorksFor(configuration, state, dispatch, inputs) {
410
+ const workstation = configuration.factory.workstations?.find(({ name }) => name === dispatch.workstation);
411
+ const first = inputs[0];
412
+ if (workstation === undefined || first === undefined)
413
+ return [];
414
+ const routes = outcomeRoutesFor(configuration, workstation, dispatch.outcome.result);
415
+ const visits = visitsAfterTransition(inputs.map((input) => input.visits), workstation.name);
416
+ return routes.map((route, ordinal) => {
417
+ const matching = inputs.find(({ workType }) => workType === route.workType);
418
+ const lineageSource = matching ?? first;
419
+ const workId = matching?.workId ??
420
+ identity("work", state.sessionId, "route", dispatch.dispatchId, ordinal, route);
421
+ const targetStateType = configuration.factory.workTypes
422
+ ?.find(({ name }) => name === route.workType)
423
+ ?.states.find(({ name }) => name === route.state)?.type;
424
+ const preserveInput = workstation.workPropagation?.mode === "PRESERVE_INPUT" ||
425
+ dispatch.outcome.result === "failed" ||
426
+ (dispatch.outcome.result === "rejected" && targetStateType === "FAILED");
427
+ const input = preserveInput
428
+ ? lineageSource.input
429
+ : (dispatch.outcome.output ?? lineageSource.input);
430
+ return {
431
+ submissionId: `${first.submissionId}/${workstation.name}/${ordinal}`,
432
+ requestId: first.requestId,
433
+ traceId: lineageSource.traceId,
434
+ tokenId: identity("token", dispatch.dispatchId, ordinal, route),
435
+ workId,
436
+ rootWorkId: lineageSource.rootWorkId,
437
+ visits,
438
+ workType: route.workType,
439
+ state: route.state,
440
+ ...(input === undefined ? {} : { input }),
441
+ ...(matching !== undefined
442
+ ? matching.parent === undefined
443
+ ? {}
444
+ : { parent: matching.parent }
445
+ : { parent: first.workId }),
446
+ ...(matching?.relations === undefined
447
+ ? {}
448
+ : { relations: matching.relations }),
449
+ queuedElapsedMs: dispatch.dueElapsedMs,
450
+ lastDispatchElapsedMs: Math.min(lineageSource.lastDispatchElapsedMs ?? dispatch.dueElapsedMs, dispatch.dueElapsedMs),
451
+ phase: targetStateType === "TERMINAL" || targetStateType === "FAILED"
452
+ ? "completed"
453
+ : "ready",
454
+ };
455
+ });
456
+ }
457
+ function eventWorkFor(configuration, work) {
458
+ return {
459
+ name: work.submissionId,
460
+ workId: work.workId,
461
+ requestId: work.requestId,
462
+ workTypeName: work.workType,
463
+ state: {
464
+ name: work.state,
465
+ type: configuration.factory.workTypes
466
+ ?.find(({ name }) => name === work.workType)
467
+ ?.states.find(({ name }) => name === work.state)?.type ??
468
+ "PROCESSING",
469
+ },
470
+ currentChainingTraceId: work.traceId,
471
+ traceId: work.traceId,
472
+ ...(work.input === undefined ? {} : { payload: work.input }),
473
+ };
474
+ }
475
+ function stateTypeFor(configuration, work) {
476
+ return configuration.factory.workTypes
477
+ ?.find(({ name }) => name === work.workType)
478
+ ?.states.find(({ name }) => name === work.state)?.type;
479
+ }
480
+ function failedStateFor(configuration, workType) {
481
+ return configuration.factory.workTypes
482
+ ?.find(({ name }) => name === workType)
483
+ ?.states.find(({ type }) => type === "FAILED")?.name;
484
+ }
485
+ function cascadeDependencyFailures(configuration, state, eventTime, sequence) {
486
+ const works = [...state.works];
487
+ const currentIndexes = new Map();
488
+ for (const [index, work] of works.entries())
489
+ currentIndexes.set(work.workId, index);
490
+ const currentWorks = [...currentIndexes.values()].flatMap((index) => {
491
+ const work = works[index];
492
+ return work === undefined ? [] : [work];
493
+ });
494
+ const dependents = new Map();
495
+ for (const work of currentWorks) {
496
+ for (const relation of work.relations ?? []) {
497
+ const existing = dependents.get(relation.targetWorkId) ?? [];
498
+ existing.push(work);
499
+ dependents.set(relation.targetWorkId, existing);
500
+ }
501
+ }
502
+ const queue = currentWorks
503
+ .filter((work) => stateTypeFor(configuration, work) === "FAILED")
504
+ .map(({ workId }) => workId);
505
+ const cascaded = new Set();
506
+ const events = [];
507
+ for (let cursor = 0; cursor < queue.length; cursor += 1) {
508
+ const triggerWorkId = queue[cursor];
509
+ if (triggerWorkId === undefined)
510
+ continue;
511
+ for (const dependent of dependents.get(triggerWorkId) ?? []) {
512
+ if (cascaded.has(dependent.workId))
513
+ continue;
514
+ const index = currentIndexes.get(dependent.workId);
515
+ const current = index === undefined ? undefined : works[index];
516
+ if (index === undefined || current === undefined)
517
+ continue;
518
+ const currentType = stateTypeFor(configuration, current);
519
+ if (currentType === "TERMINAL" || currentType === "FAILED")
520
+ continue;
521
+ const failedState = failedStateFor(configuration, current.workType);
522
+ if (failedState === undefined)
523
+ continue;
524
+ cascaded.add(current.workId);
525
+ queue.push(current.workId);
526
+ works[index] = {
527
+ ...current,
528
+ state: failedState,
529
+ phase: "completed",
530
+ dispatch: undefined,
531
+ };
532
+ const eventSequence = sequence + events.length;
533
+ const reason = `cascading failure: dependency ${triggerWorkId} failed`;
534
+ events.push({
535
+ schemaVersion: "agent-factory.event.v1",
536
+ id: identity("event", state.sessionId, eventSequence, "WORK_STATE_CHANGE", current.workId),
537
+ type: "WORK_STATE_CHANGE",
538
+ context: eventContext(state, eventSequence, eventTime, {
539
+ requestId: current.requestId,
540
+ workIds: [current.workId],
541
+ }),
542
+ payload: {
543
+ workId: current.workId,
544
+ workTypeName: current.workType,
545
+ fromState: current.state,
546
+ toState: failedState,
547
+ fromPlaceId: `${current.workType}:${current.state}`,
548
+ toPlaceId: `${current.workType}:${failedState}`,
549
+ source: "cascading-failure",
550
+ triggerWorkId,
551
+ reason,
552
+ },
553
+ });
554
+ }
555
+ }
556
+ return { events, works };
557
+ }
558
+ function workRequestEvent(configuration, state, sequence, requestId, works, relations) {
559
+ return {
560
+ schemaVersion: "agent-factory.event.v1",
561
+ id: identity("event", state.sessionId, sequence, "WORK_REQUEST"),
562
+ type: "WORK_REQUEST",
563
+ context: eventContext(state, sequence, state.virtualTime, {
564
+ requestId,
565
+ traceIds: works.map(({ traceId }) => traceId),
566
+ workIds: works.map(({ workId }) => workId),
567
+ }),
568
+ payload: {
569
+ type: "FACTORY_REQUEST_BATCH",
570
+ source: "emulator",
571
+ works: works.map((work) => ({
572
+ name: work.submissionId,
573
+ workId: work.workId,
574
+ requestId: work.requestId,
575
+ workTypeName: work.workType,
576
+ state: {
577
+ name: work.state,
578
+ type: configuration.factory.workTypes
579
+ ?.find(({ name }) => name === work.workType)
580
+ ?.states.find(({ name }) => name === work.state)?.type ??
581
+ "INITIAL",
582
+ },
583
+ currentChainingTraceId: work.traceId,
584
+ traceId: work.traceId,
585
+ ...(work.input === undefined ? {} : { payload: work.input }),
586
+ })),
587
+ ...(relations.length === 0
588
+ ? {}
589
+ : { relations: relations.map((relation) => ({ ...relation })) }),
590
+ },
591
+ };
592
+ }
593
+ function relationshipChangeEvents(state, sequence, requestId, workByName, relations) {
594
+ return relations.map((relation, ordinal) => {
595
+ const source = workByName.get(relation.sourceWorkName);
596
+ const target = workByName.get(relation.targetWorkName);
597
+ if (source === undefined || target === undefined) {
598
+ throw new Error("Validated submission relationship endpoint is missing.");
599
+ }
600
+ const relationshipSequence = sequence + ordinal + 1;
601
+ return {
602
+ schemaVersion: "agent-factory.event.v1",
603
+ id: identity("event", state.sessionId, relationshipSequence, "RELATIONSHIP_CHANGE_REQUEST", requestId, ordinal),
604
+ type: "RELATIONSHIP_CHANGE_REQUEST",
605
+ context: eventContext(state, relationshipSequence, state.virtualTime, {
606
+ requestId,
607
+ traceIds: [source.traceId, target.traceId],
608
+ workIds: [source.workId, target.workId],
609
+ }),
610
+ payload: { relation },
611
+ };
612
+ });
613
+ }
614
+ function workRequestCalculation(configuration, state, submissionBatch, command) {
615
+ const requestId = identity("request", state.sessionId, command, state.counters.commands);
616
+ const works = submissionBatch.works.map((submission, ordinal) => {
617
+ const workId = identity("work", state.sessionId, command, state.counters.commands, ordinal, submission);
618
+ const traceId = identity("trace", workId, requestId);
619
+ return {
620
+ submissionId: submission.name,
621
+ requestId,
622
+ traceId,
623
+ tokenId: identity("token", workId, submission.workType, submission.state),
624
+ workId,
625
+ rootWorkId: workId,
626
+ visits: {},
627
+ workType: submission.workType,
628
+ state: submission.state,
629
+ ...(submission.input === undefined ? {} : { input: submission.input }),
630
+ ...(submission.parent === undefined ? {} : { parent: submission.parent }),
631
+ queuedElapsedMs: state.virtualElapsedMs,
632
+ phase: "ready",
633
+ };
634
+ });
635
+ const workByName = new Map(works.map((work) => [work.submissionId, work]));
636
+ const resolvedWorkId = (name) => {
637
+ const resolved = workByName.get(name);
638
+ if (resolved === undefined) {
639
+ throw new Error(`Validated submission target ${name} is missing.`);
640
+ }
641
+ return resolved.workId;
642
+ };
643
+ const normalizedWorks = works.map((work) => {
644
+ const relations = submissionBatch.relations
645
+ .filter(({ sourceWorkName }) => sourceWorkName === work.submissionId)
646
+ .map((relation) => ({
647
+ type: "DEPENDS_ON",
648
+ sourceWorkName: relation.sourceWorkName,
649
+ targetWorkName: relation.targetWorkName,
650
+ targetWorkId: resolvedWorkId(relation.targetWorkName),
651
+ requiredState: relation.requiredState ?? "complete",
652
+ }));
653
+ return relations.length === 0 ? work : { ...work, relations };
654
+ });
655
+ const normalizedRelations = submissionBatch.relations.map((relation) => ({
656
+ type: "DEPENDS_ON",
657
+ sourceWorkName: relation.sourceWorkName,
658
+ targetWorkName: relation.targetWorkName,
659
+ targetWorkId: resolvedWorkId(relation.targetWorkName),
660
+ requiredState: relation.requiredState ?? "complete",
661
+ }));
662
+ const sequence = state.counters.events;
663
+ const request = workRequestEvent(configuration, state, sequence, requestId, normalizedWorks, normalizedRelations);
664
+ const relationshipEvents = relationshipChangeEvents(state, sequence, requestId, workByName, normalizedRelations);
665
+ return {
666
+ works: normalizedWorks,
667
+ events: [request, ...relationshipEvents],
668
+ };
669
+ }
670
+ function logicalMoveCalculation(configuration, state, workstation, outcome, inputs, resources, logicalMoveId, sequence) {
671
+ const primary = inputs[0];
672
+ if (primary === undefined)
673
+ throw new Error("Logical move has no input Work.");
674
+ const routedWorks = routedWorksFor(configuration, state, {
675
+ dispatchId: logicalMoveId,
676
+ completionId: identity("completion", logicalMoveId),
677
+ transitionId: workstation.name,
678
+ workstation: workstation.name,
679
+ worker: "",
680
+ startedElapsedMs: state.virtualElapsedMs,
681
+ dueElapsedMs: state.virtualElapsedMs,
682
+ inputTokenIds: inputs.map(({ tokenId }) => tokenId),
683
+ resources,
684
+ outcome,
685
+ }, inputs);
686
+ return {
687
+ routedWorks,
688
+ event: {
689
+ schemaVersion: "agent-factory.event.v1",
690
+ id: identity("event", state.sessionId, sequence, "DISPATCH_RESPONSE", logicalMoveId),
691
+ type: "DISPATCH_RESPONSE",
692
+ context: eventContext(state, sequence, state.virtualTime, {
693
+ requestId: primary.requestId,
694
+ traceIds: inputs.map(({ traceId }) => traceId),
695
+ workIds: inputs.map(({ workId }) => workId),
696
+ currentChainingTraceId: primary.traceId,
697
+ previousChainingTraceIds: inputs.slice(1).map(({ traceId }) => traceId),
698
+ }),
699
+ payload: {
700
+ transitionId: workstation.name,
701
+ outcome: "ACCEPTED",
702
+ durationMillis: 0,
703
+ ...(routedWorks.length === 0
704
+ ? {}
705
+ : {
706
+ outputWork: routedWorks.map((routed) => eventWorkFor(configuration, routed)),
707
+ }),
708
+ },
709
+ },
710
+ };
711
+ }
712
+ function replaceInputPhases(replacements, state, indexes, phase) {
713
+ for (const index of indexes) {
714
+ const consumed = state.works[index];
715
+ if (consumed !== undefined)
716
+ replacements[index] = { ...consumed, phase };
717
+ }
718
+ }
719
+ function worksAtIndexes(works, indexes) {
720
+ return indexes.flatMap((index) => {
721
+ const work = works[index];
722
+ return work === undefined ? [] : [work];
723
+ });
724
+ }
725
+ function workerDispatchRequestEvent(state, inputs, transitionId, dispatchId, resources, sequence) {
726
+ const primary = inputs[0];
727
+ if (primary === undefined)
728
+ throw new Error("Worker dispatch has no input Work.");
729
+ return {
730
+ schemaVersion: "agent-factory.event.v1",
731
+ id: identity("event", state.sessionId, sequence, "DISPATCH_REQUEST", dispatchId),
732
+ type: "DISPATCH_REQUEST",
733
+ context: eventContext(state, sequence, state.virtualTime, {
734
+ dispatchId,
735
+ requestId: primary.requestId,
736
+ traceIds: inputs.map(({ traceId }) => traceId),
737
+ workIds: inputs.map(({ workId }) => workId),
738
+ currentChainingTraceId: primary.traceId,
739
+ previousChainingTraceIds: inputs.slice(1).map(({ traceId }) => traceId),
740
+ }),
741
+ payload: {
742
+ transitionId,
743
+ inputs: inputs.map(({ workId }) => ({ workId })),
744
+ ...(resources.length === 0 ? {} : { resources: [...resources] }),
745
+ },
746
+ };
747
+ }
748
+ function validateSubmissions(configuration, state, value) {
749
+ const batch = isSubmissionArray(value)
750
+ ? { works: value }
751
+ : "works" in value
752
+ ? value
753
+ : { works: [value] };
754
+ const scenarioSubmissions = isSubmissionArray(value) || !("works" in value) ? batch.works : batch;
755
+ const parsed = safeParseFactoryEmulatorScenario({ ...configuration.scenario, initialSubmissions: scenarioSubmissions }, configuration.factory);
756
+ const issues = parsed.success
757
+ ? []
758
+ : parsed.issues.map((issue) => ({
759
+ ...issue,
760
+ path: ["submissions", ...issue.path.slice(1)],
761
+ }));
762
+ const known = new Set(state.works.map(({ submissionId }) => submissionId));
763
+ batch.works.forEach((submission, index) => {
764
+ if (known.has(submission?.name)) {
765
+ issues.push({
766
+ category: "semantic",
767
+ code: "duplicate_identity",
768
+ path: ["submissions", index, "name"],
769
+ message: `Work identity ${submission.name} already exists in this session.`,
770
+ });
771
+ }
772
+ });
773
+ if (issues.length > 0)
774
+ throw new FactoryEmulatorSubmissionError(issues);
775
+ return clone({ works: batch.works, relations: batch.relations ?? [] });
776
+ }
777
+ function rejectionMessage(rejection) {
778
+ if (rejection instanceof Error)
779
+ return String(rejection.message);
780
+ try {
781
+ return String(rejection);
782
+ }
783
+ catch {
784
+ return "Sink rejected with an unprintable value.";
785
+ }
786
+ }
787
+ function budgetUsage(state, limits) {
788
+ return {
789
+ completedDispatches: {
790
+ used: state.counters.completedDispatches,
791
+ limit: limits.maxCompletedDispatches,
792
+ },
793
+ events: { used: state.counters.events, limit: limits.maxEvents },
794
+ virtualElapsedMs: {
795
+ used: state.virtualElapsedMs,
796
+ limit: limits.maxVirtualElapsedMs,
797
+ },
798
+ };
799
+ }
800
+ function unfinished(state) {
801
+ return state.works.some(({ phase }) => phase !== "completed");
802
+ }
803
+ /** Creates one validated, caller-owned deterministic Factory emulator session. */
804
+ // biome-ignore lint/complexity/noExcessiveLinesPerFunction: Command closures intentionally share one explicit session state owner.
805
+ export function createFactoryEmulatorSession(options) {
806
+ const configuration = validateConfiguration(options);
807
+ const sessionId = sessionIdentity(configuration.factory, configuration.scenario);
808
+ let committedState = clone(PRE_START_STATE);
809
+ let pendingTransaction;
810
+ let pendingAdvanceContext;
811
+ let commandInFlight;
812
+ let lastError;
813
+ const pauseExecution = (command, diagnostic) => {
814
+ lastError = {
815
+ code: "execution_paused",
816
+ operation: "execute",
817
+ command,
818
+ message: `Factory emulator execution paused: ${diagnostic.kind}.`,
819
+ diagnostic: clone(diagnostic),
820
+ };
821
+ throw new FactoryEmulatorExecutionPausedError(diagnostic);
822
+ };
823
+ const assertCandidateWithinBudgets = (command, candidate) => {
824
+ const checks = [
825
+ {
826
+ limit: "completedDispatches",
827
+ configured: configuration.limits.maxCompletedDispatches,
828
+ observed: candidate.counters.completedDispatches,
829
+ },
830
+ {
831
+ limit: "events",
832
+ configured: configuration.limits.maxEvents,
833
+ observed: candidate.counters.events,
834
+ },
835
+ {
836
+ limit: "virtualElapsedMs",
837
+ configured: configuration.limits.maxVirtualElapsedMs,
838
+ observed: candidate.virtualElapsedMs,
839
+ },
840
+ ];
841
+ const exceeded = checks.find(({ configured, observed }) => observed > configured);
842
+ if (exceeded !== undefined) {
843
+ pauseExecution(command, {
844
+ kind: "budget-exceeded",
845
+ ...exceeded,
846
+ virtualTime: candidate.virtualTime,
847
+ virtualElapsedMs: candidate.virtualElapsedMs,
848
+ });
849
+ }
850
+ };
851
+ const assertWorkBound = (command, observed) => {
852
+ if (observed <= configuration.limits.maxSynchronousWorkItems)
853
+ return;
854
+ const state = committedState;
855
+ pauseExecution(command, {
856
+ kind: "bounded-work-exceeded",
857
+ limit: "synchronousWorkItems",
858
+ configured: configuration.limits.maxSynchronousWorkItems,
859
+ observed,
860
+ virtualTime: state.lifecycle === "pre-start"
861
+ ? configuration.scenario.startAt
862
+ : state.virtualTime,
863
+ virtualElapsedMs: state.virtualElapsedMs,
864
+ });
865
+ };
866
+ const status = () => {
867
+ const common = {
868
+ virtualTime: committedState.lifecycle !== "pre-start"
869
+ ? committedState.virtualTime
870
+ : configuration.scenario.startAt,
871
+ virtualElapsedMs: committedState.virtualElapsedMs,
872
+ budgetUsage: budgetUsage(committedState, configuration.limits),
873
+ };
874
+ if (lastError !== undefined) {
875
+ return clone({
876
+ ...common,
877
+ phase: "error",
878
+ reason: lastError.operation === "execute"
879
+ ? "Execution paused at a configured safety boundary."
880
+ : lastError.operation === "close"
881
+ ? "The event sink rejected the pending close."
882
+ : "The event sink rejected the pending transaction.",
883
+ ...(pendingTransaction === undefined
884
+ ? {}
885
+ : {
886
+ pendingTransaction: {
887
+ command: pendingTransaction.command,
888
+ phase: pendingTransaction.phase,
889
+ eventCount: pendingTransaction.batch.length,
890
+ },
891
+ }),
892
+ error: lastError,
893
+ });
894
+ }
895
+ if (commandInFlight !== undefined) {
896
+ return clone({
897
+ ...common,
898
+ phase: "active",
899
+ reason: `${commandInFlight} is awaiting the event sink.`,
900
+ ...(pendingTransaction === undefined
901
+ ? {}
902
+ : {
903
+ pendingTransaction: {
904
+ command: pendingTransaction.command,
905
+ phase: pendingTransaction.phase,
906
+ eventCount: pendingTransaction.batch.length,
907
+ },
908
+ }),
909
+ });
910
+ }
911
+ if (committedState.lifecycle === "pre-start") {
912
+ return clone({
913
+ ...common,
914
+ phase: "idle",
915
+ reason: "The session is ready to start.",
916
+ });
917
+ }
918
+ if (committedState.lifecycle === "closed") {
919
+ return clone({
920
+ ...common,
921
+ phase: "closed",
922
+ reason: "The session is closed.",
923
+ });
924
+ }
925
+ if (committedState.works.some(({ phase }) => phase === "active")) {
926
+ return clone({ ...common, phase: "active", reason: "Work is active." });
927
+ }
928
+ if (committedState.works.some(({ phase }) => phase === "ready")) {
929
+ return clone({
930
+ ...common,
931
+ phase: "ready",
932
+ reason: committedState.counters.commands === 1
933
+ ? "Initial Work is ready for a later execution command."
934
+ : "Work is ready.",
935
+ });
936
+ }
937
+ if (committedState.works.some(({ phase }) => phase === "waiting")) {
938
+ return clone({
939
+ ...common,
940
+ phase: "waiting",
941
+ reason: "Work is waiting for a supported outcome.",
942
+ });
943
+ }
944
+ return clone({
945
+ ...common,
946
+ phase: "idle",
947
+ reason: "The open session has no unfinished Work.",
948
+ });
949
+ };
950
+ const assertCommand = (command, retryKey) => {
951
+ if (commandInFlight !== undefined) {
952
+ throw new FactoryEmulatorPendingCommandError(command, commandInFlight);
953
+ }
954
+ if (pendingTransaction !== undefined) {
955
+ if (command === pendingTransaction.command &&
956
+ retryKey === pendingTransaction.retryKey) {
957
+ return pendingTransaction;
958
+ }
959
+ throw new FactoryEmulatorPendingCommandError(command, pendingTransaction.command);
960
+ }
961
+ if ((command === "start" && committedState.lifecycle !== "pre-start") ||
962
+ (command !== "start" && committedState.lifecycle !== "started")) {
963
+ throw new FactoryEmulatorLifecycleError(command, committedState.lifecycle);
964
+ }
965
+ return undefined;
966
+ };
967
+ const acceptPendingTransaction = async () => {
968
+ const transaction = pendingTransaction;
969
+ if (transaction === undefined)
970
+ return;
971
+ try {
972
+ if (transaction.phase === "sink-write") {
973
+ await configuration.sink.write(clone(transaction.batch));
974
+ if (transaction.command === "close") {
975
+ transaction.phase = "sink-close";
976
+ }
977
+ }
978
+ if (transaction.phase === "sink-close") {
979
+ await configuration.sink.close?.();
980
+ }
981
+ committedState = transaction.candidate;
982
+ pendingTransaction = undefined;
983
+ lastError = undefined;
984
+ }
985
+ catch (rejection) {
986
+ lastError =
987
+ transaction.phase === "sink-close"
988
+ ? {
989
+ code: "sink_close_rejected",
990
+ operation: "close",
991
+ command: transaction.command,
992
+ message: rejectionMessage(rejection),
993
+ }
994
+ : {
995
+ code: "sink_write_rejected",
996
+ operation: "write",
997
+ command: transaction.command,
998
+ message: rejectionMessage(rejection),
999
+ };
1000
+ throw rejection;
1001
+ }
1002
+ };
1003
+ const write = async (command, retryKey, batch, candidate) => {
1004
+ assertCandidateWithinBudgets(command, candidate);
1005
+ pendingTransaction = {
1006
+ command,
1007
+ retryKey,
1008
+ batch: clone(batch),
1009
+ candidate: clone(candidate),
1010
+ phase: "sink-write",
1011
+ };
1012
+ await acceptPendingTransaction();
1013
+ };
1014
+ const start = async () => {
1015
+ const retryKey = "start";
1016
+ const retry = assertCommand("start", retryKey);
1017
+ commandInFlight = "start";
1018
+ try {
1019
+ if (retry !== undefined) {
1020
+ await acceptPendingTransaction();
1021
+ const candidate = retry.candidate;
1022
+ const batches = [
1023
+ retry.batch.slice(0, 3),
1024
+ ...(retry.batch.length > 3 ? [retry.batch.slice(3)] : []),
1025
+ ];
1026
+ return clone({ status: "started", batches, state: candidate });
1027
+ }
1028
+ lastError = undefined;
1029
+ const bootstrap = bootstrapEvents(configuration.factory, configuration.scenario, sessionId);
1030
+ let candidate = {
1031
+ lifecycle: "started",
1032
+ sessionId,
1033
+ virtualTime: configuration.scenario.startAt,
1034
+ virtualElapsedMs: 0,
1035
+ works: [],
1036
+ ruleCursors: {},
1037
+ counters: {
1038
+ commands: 1,
1039
+ events: bootstrap.length,
1040
+ completedDispatches: 0,
1041
+ },
1042
+ };
1043
+ const batches = [bootstrap];
1044
+ const initialValue = configuration.scenario.initialSubmissions ?? [];
1045
+ const initial = isSubmissionArray(initialValue)
1046
+ ? { works: initialValue, relations: [] }
1047
+ : {
1048
+ works: initialValue.works,
1049
+ relations: initialValue.relations ?? [],
1050
+ };
1051
+ assertWorkBound("start", initial.works.length);
1052
+ let combined = bootstrap;
1053
+ if (initial.works.length > 0) {
1054
+ const calculation = workRequestCalculation(configuration, { ...candidate, counters: { ...candidate.counters, commands: 0 } }, initial, "start");
1055
+ const cascade = cascadeDependencyFailures(configuration, { ...candidate, works: calculation.works }, candidate.virtualTime, candidate.counters.events + calculation.events.length);
1056
+ const submissionBatch = [...calculation.events, ...cascade.events];
1057
+ batches.push(submissionBatch);
1058
+ combined = [...bootstrap, ...submissionBatch];
1059
+ candidate = {
1060
+ ...candidate,
1061
+ works: cascade.works,
1062
+ counters: { ...candidate.counters, events: combined.length },
1063
+ };
1064
+ }
1065
+ await write("start", retryKey, combined, candidate);
1066
+ return clone({ status: "started", batches, state: candidate });
1067
+ }
1068
+ finally {
1069
+ commandInFlight = undefined;
1070
+ }
1071
+ };
1072
+ const submit = async (value) => {
1073
+ const retryKey = canonicalJson(Array.isArray(value) ? value : [value]);
1074
+ const retry = assertCommand("submit", retryKey);
1075
+ if (retry !== undefined) {
1076
+ commandInFlight = "submit";
1077
+ try {
1078
+ await acceptPendingTransaction();
1079
+ return clone({
1080
+ status: "submitted",
1081
+ batch: retry.batch,
1082
+ state: retry.candidate,
1083
+ });
1084
+ }
1085
+ finally {
1086
+ commandInFlight = undefined;
1087
+ }
1088
+ }
1089
+ const state = committedState;
1090
+ assertWorkBound("submit", isSubmissionArray(value)
1091
+ ? value.length
1092
+ : "works" in value
1093
+ ? value.works.length
1094
+ : 1);
1095
+ const submissions = validateSubmissions(configuration, state, value);
1096
+ commandInFlight = "submit";
1097
+ lastError = undefined;
1098
+ try {
1099
+ const calculation = workRequestCalculation(configuration, state, submissions, "submit");
1100
+ const cascade = cascadeDependencyFailures(configuration, { ...state, works: [...state.works, ...calculation.works] }, state.virtualTime, state.counters.events + calculation.events.length);
1101
+ const batch = [...calculation.events, ...cascade.events];
1102
+ const candidate = {
1103
+ ...state,
1104
+ works: cascade.works,
1105
+ counters: {
1106
+ ...state.counters,
1107
+ commands: state.counters.commands + 1,
1108
+ events: state.counters.events + batch.length,
1109
+ },
1110
+ };
1111
+ await write("submit", retryKey, batch, candidate);
1112
+ return clone({ status: "submitted", batch, state: candidate });
1113
+ }
1114
+ finally {
1115
+ commandInFlight = undefined;
1116
+ }
1117
+ };
1118
+ const cascadeLogicalMoveFailures = (state, replacements, events) => {
1119
+ const cascade = cascadeDependencyFailures(configuration, { ...state, works: replacements }, virtualTimeAt(configuration.scenario, state.virtualElapsedMs), state.counters.events + events.length);
1120
+ replacements.splice(0, replacements.length, ...cascade.works);
1121
+ events.push(...cascade.events);
1122
+ };
1123
+ const dispatchCalculation = (command, state) => {
1124
+ const replacements = [...state.works];
1125
+ const cursors = { ...state.ruleCursors };
1126
+ const events = [];
1127
+ const { candidates, executableWork } = schedulerCandidatesFor(configuration, state, cursors, (observed) => assertWorkBound(command, observed));
1128
+ for (const { value, resources } of selectFactorySchedulerCandidates(candidates, undefined, availableResourcesFor(configuration, state))) {
1129
+ const { indexes, execution, invocation, cursorKey } = value;
1130
+ const works = worksAtIndexes(replacements, indexes);
1131
+ const work = works[0];
1132
+ if (work === undefined ||
1133
+ works.length !== indexes.length ||
1134
+ works.some(({ phase }) => phase !== "ready") ||
1135
+ execution.workstation === undefined ||
1136
+ execution.outcome === undefined)
1137
+ continue;
1138
+ if (cursorKey !== undefined)
1139
+ cursors[cursorKey] = invocation + 1;
1140
+ const logicalMove = execution.workstation.type === "LOGICAL_MOVE";
1141
+ const transitionId = execution.workstation.name;
1142
+ const dispatchId = identity(logicalMove ? "logical-move" : "dispatch", works.map(({ tokenId }) => tokenId), transitionId, invocation);
1143
+ const completionId = identity("completion", dispatchId);
1144
+ const dueElapsedMs = state.virtualElapsedMs + execution.outcome.durationMs;
1145
+ const eventResources = eventResourcesFor(configuration, resources);
1146
+ virtualTimeAt(configuration.scenario, dueElapsedMs);
1147
+ if (logicalMove) {
1148
+ const calculation = logicalMoveCalculation(configuration, state, execution.workstation, execution.outcome, works, resources, dispatchId, state.counters.events + events.length);
1149
+ replaceInputPhases(replacements, state, indexes, "completed");
1150
+ replacements.push(...calculation.routedWorks);
1151
+ events.push(calculation.event);
1152
+ cascadeLogicalMoveFailures(state, replacements, events);
1153
+ continue;
1154
+ }
1155
+ replaceInputPhases(replacements, state, indexes, "active");
1156
+ const primaryIndex = indexes[0];
1157
+ if (primaryIndex === undefined)
1158
+ continue;
1159
+ replacements[primaryIndex] = {
1160
+ ...work,
1161
+ phase: "active",
1162
+ dispatch: {
1163
+ dispatchId,
1164
+ completionId,
1165
+ transitionId,
1166
+ workstation: execution.workstation.name,
1167
+ worker: execution.workstation.worker,
1168
+ startedElapsedMs: state.virtualElapsedMs,
1169
+ dueElapsedMs,
1170
+ inputTokenIds: works.map(({ tokenId }) => tokenId),
1171
+ resources,
1172
+ outcome: execution.outcome,
1173
+ },
1174
+ };
1175
+ const sequence = state.counters.events + events.length;
1176
+ events.push(workerDispatchRequestEvent(state, works, transitionId, dispatchId, eventResources, sequence));
1177
+ }
1178
+ for (const [index, work] of state.works.entries()) {
1179
+ if (work.phase === "ready" &&
1180
+ replacements[index]?.phase === "ready" &&
1181
+ !executableWork.has(index)) {
1182
+ replacements[index] = { ...work, phase: "waiting" };
1183
+ }
1184
+ }
1185
+ return {
1186
+ batch: events,
1187
+ state: {
1188
+ ...state,
1189
+ works: replacements,
1190
+ ruleCursors: cursors,
1191
+ counters: {
1192
+ ...state.counters,
1193
+ events: state.counters.events + events.length,
1194
+ },
1195
+ },
1196
+ };
1197
+ };
1198
+ const completionCalculation = (state, dueElapsedMs) => {
1199
+ const replacements = [...state.works];
1200
+ const events = [];
1201
+ const eventTime = virtualTimeAt(configuration.scenario, dueElapsedMs);
1202
+ for (const work of state.works) {
1203
+ if (work.phase !== "active" ||
1204
+ work.dispatch?.dueElapsedMs !== dueElapsedMs)
1205
+ continue;
1206
+ const { dispatch } = work;
1207
+ const inputs = dispatch.inputTokenIds.flatMap((tokenId) => {
1208
+ const input = state.works.find((candidate) => candidate.tokenId === tokenId);
1209
+ return input === undefined ? [] : [input];
1210
+ });
1211
+ if (inputs.length !== dispatch.inputTokenIds.length)
1212
+ continue;
1213
+ const routedWorks = routedWorksFor(configuration, state, dispatch, inputs);
1214
+ const sequence = state.counters.events + events.length;
1215
+ const outcome = {
1216
+ accepted: "ACCEPTED",
1217
+ continued: "CONTINUE",
1218
+ rejected: "REJECTED",
1219
+ failed: "FAILED",
1220
+ };
1221
+ events.push({
1222
+ schemaVersion: "agent-factory.event.v1",
1223
+ id: identity("event", state.sessionId, sequence, "DISPATCH_RESPONSE", dispatch.dispatchId),
1224
+ type: "DISPATCH_RESPONSE",
1225
+ context: eventContext(state, sequence, eventTime, {
1226
+ dispatchId: dispatch.dispatchId,
1227
+ requestId: work.requestId,
1228
+ traceIds: inputs.map(({ traceId }) => traceId),
1229
+ workIds: inputs.map(({ workId }) => workId),
1230
+ currentChainingTraceId: work.traceId,
1231
+ previousChainingTraceIds: inputs
1232
+ .slice(1)
1233
+ .map(({ traceId }) => traceId),
1234
+ }),
1235
+ payload: {
1236
+ completionId: dispatch.completionId,
1237
+ transitionId: dispatch.transitionId,
1238
+ outcome: outcome[dispatch.outcome.result],
1239
+ durationMillis: dispatch.dueElapsedMs - dispatch.startedElapsedMs,
1240
+ ...(dispatch.outcome.output === undefined
1241
+ ? {}
1242
+ : { output: dispatch.outcome.output }),
1243
+ ...(dispatch.outcome.feedback === undefined
1244
+ ? {}
1245
+ : { feedback: dispatch.outcome.feedback }),
1246
+ ...(dispatch.outcome.error === undefined
1247
+ ? {}
1248
+ : { error: dispatch.outcome.error }),
1249
+ ...(routedWorks.length === 0
1250
+ ? {}
1251
+ : {
1252
+ outputWork: routedWorks.map((routed) => eventWorkFor(configuration, routed)),
1253
+ }),
1254
+ },
1255
+ });
1256
+ for (const input of inputs) {
1257
+ const inputIndex = state.works.findIndex(({ tokenId }) => tokenId === input.tokenId);
1258
+ if (inputIndex >= 0)
1259
+ replacements[inputIndex] = { ...input, phase: "completed" };
1260
+ }
1261
+ replacements.push(...routedWorks);
1262
+ }
1263
+ const completedDispatches = events.length;
1264
+ const cascade = cascadeDependencyFailures(configuration, { ...state, works: replacements }, eventTime, state.counters.events + events.length);
1265
+ events.push(...cascade.events);
1266
+ return {
1267
+ batch: events,
1268
+ state: {
1269
+ ...state,
1270
+ virtualElapsedMs: dueElapsedMs,
1271
+ virtualTime: eventTime,
1272
+ works: cascade.works,
1273
+ counters: {
1274
+ ...state.counters,
1275
+ events: state.counters.events + events.length,
1276
+ completedDispatches: state.counters.completedDispatches + completedDispatches,
1277
+ },
1278
+ },
1279
+ };
1280
+ };
1281
+ // biome-ignore lint/complexity/noExcessiveLinesPerFunction: Advancement keeps retry, budget, yield, and transaction state in one traceable command boundary.
1282
+ const runAdvance = async (command, requestedDuration) => {
1283
+ if (requestedDuration !== undefined &&
1284
+ (!Number.isSafeInteger(requestedDuration) || requestedDuration < 0)) {
1285
+ throw new FactoryEmulatorDurationError(requestedDuration);
1286
+ }
1287
+ const retryKey = canonicalJson([command, requestedDuration]);
1288
+ const retry = assertCommand(command, retryKey);
1289
+ const original = committedState;
1290
+ const target = retry === undefined
1291
+ ? requestedDuration === undefined
1292
+ ? undefined
1293
+ : original.virtualElapsedMs + requestedDuration
1294
+ : pendingAdvanceContext?.target;
1295
+ if (retry === undefined && target !== undefined) {
1296
+ virtualTimeAt(configuration.scenario, target);
1297
+ }
1298
+ const context = retry === undefined
1299
+ ? {
1300
+ command,
1301
+ retryKey,
1302
+ fromVirtualTime: original.virtualTime,
1303
+ ...(target === undefined ? {} : { target }),
1304
+ wasUnfinished: unfinished(original),
1305
+ batches: [],
1306
+ zeroDurationBatches: 0,
1307
+ }
1308
+ : pendingAdvanceContext;
1309
+ if (context === undefined) {
1310
+ throw new Error("Pending advancement context is unavailable.");
1311
+ }
1312
+ pendingAdvanceContext = context;
1313
+ commandInFlight = command;
1314
+ let synchronousBatches = 0;
1315
+ const acceptSchedulerBatch = async (calculation) => {
1316
+ const beforeElapsedMs = committedState.virtualElapsedMs;
1317
+ const nextZeroDurationBatches = calculation.state.virtualElapsedMs === beforeElapsedMs
1318
+ ? context.zeroDurationBatches + 1
1319
+ : 0;
1320
+ if (nextZeroDurationBatches > configuration.limits.maxZeroDurationBatches) {
1321
+ pauseExecution(command, {
1322
+ kind: "zero-duration-cycle",
1323
+ limit: "zeroDurationBatches",
1324
+ configured: configuration.limits.maxZeroDurationBatches,
1325
+ observed: nextZeroDurationBatches,
1326
+ virtualTime: committedState.virtualTime,
1327
+ virtualElapsedMs: beforeElapsedMs,
1328
+ });
1329
+ }
1330
+ assertWorkBound(command, calculation.batch.length);
1331
+ context.batches.push(calculation.batch);
1332
+ await write(command, retryKey, calculation.batch, calculation.state);
1333
+ context.zeroDurationBatches = nextZeroDurationBatches;
1334
+ synchronousBatches += 1;
1335
+ if (configuration.yieldControl !== undefined &&
1336
+ synchronousBatches >= configuration.limits.maxSynchronousBatches) {
1337
+ synchronousBatches = 0;
1338
+ await configuration.yieldControl();
1339
+ }
1340
+ };
1341
+ try {
1342
+ if (retry !== undefined) {
1343
+ const beforeElapsedMs = original.virtualElapsedMs;
1344
+ await acceptPendingTransaction();
1345
+ context.zeroDurationBatches =
1346
+ committedState.virtualElapsedMs === beforeElapsedMs
1347
+ ? context.zeroDurationBatches + 1
1348
+ : 0;
1349
+ synchronousBatches = 1;
1350
+ if (configuration.yieldControl !== undefined &&
1351
+ synchronousBatches >= configuration.limits.maxSynchronousBatches) {
1352
+ synchronousBatches = 0;
1353
+ await configuration.yieldControl();
1354
+ }
1355
+ }
1356
+ else
1357
+ lastError = undefined;
1358
+ const continueAdvancing = !(retry !== undefined && command === "advanceToNext");
1359
+ while (continueAdvancing) {
1360
+ const state = committedState;
1361
+ if (state.works.some(({ phase }) => phase === "ready")) {
1362
+ const calculation = dispatchCalculation(command, state);
1363
+ if (calculation.batch.length > 0) {
1364
+ await acceptSchedulerBatch(calculation);
1365
+ if (command === "advanceToNext")
1366
+ break;
1367
+ continue;
1368
+ }
1369
+ committedState = calculation.state;
1370
+ }
1371
+ const activeDeadlines = committedState.works.flatMap((work) => work.phase === "active" && work.dispatch !== undefined
1372
+ ? [work.dispatch.dueElapsedMs]
1373
+ : []);
1374
+ if (activeDeadlines.length === 0)
1375
+ break;
1376
+ const earliest = Math.min(...activeDeadlines);
1377
+ if (target !== undefined && earliest > target)
1378
+ break;
1379
+ const calculation = completionCalculation(committedState, earliest);
1380
+ await acceptSchedulerBatch(calculation);
1381
+ if (command === "advanceToNext")
1382
+ break;
1383
+ }
1384
+ let finalState = committedState;
1385
+ if (target !== undefined &&
1386
+ context.wasUnfinished &&
1387
+ finalState.virtualElapsedMs < target) {
1388
+ finalState = {
1389
+ ...finalState,
1390
+ virtualElapsedMs: target,
1391
+ virtualTime: virtualTimeAt(configuration.scenario, target),
1392
+ };
1393
+ }
1394
+ const madeProgress = context.batches.length > 0 ||
1395
+ finalState.virtualTime !== context.fromVirtualTime;
1396
+ if (madeProgress) {
1397
+ finalState = {
1398
+ ...finalState,
1399
+ counters: {
1400
+ ...finalState.counters,
1401
+ commands: finalState.counters.commands + 1,
1402
+ },
1403
+ };
1404
+ assertCandidateWithinBudgets(command, finalState);
1405
+ committedState = finalState;
1406
+ }
1407
+ const receipt = clone({
1408
+ status: madeProgress ? "advanced" : "idle",
1409
+ command,
1410
+ fromVirtualTime: context.fromVirtualTime,
1411
+ virtualTime: finalState.virtualTime,
1412
+ virtualElapsedMs: finalState.virtualElapsedMs,
1413
+ batches: context.batches,
1414
+ state: finalState,
1415
+ });
1416
+ pendingAdvanceContext = undefined;
1417
+ return receipt;
1418
+ }
1419
+ finally {
1420
+ commandInFlight = undefined;
1421
+ }
1422
+ };
1423
+ const close = async () => {
1424
+ const retryKey = "close";
1425
+ const retry = assertCommand("close", retryKey);
1426
+ commandInFlight = "close";
1427
+ try {
1428
+ if (retry !== undefined) {
1429
+ await acceptPendingTransaction();
1430
+ return clone({
1431
+ status: "closed",
1432
+ batch: retry.batch,
1433
+ state: retry.candidate,
1434
+ });
1435
+ }
1436
+ lastError = undefined;
1437
+ const state = committedState;
1438
+ const sequence = state.counters.events;
1439
+ const event = {
1440
+ schemaVersion: "agent-factory.event.v1",
1441
+ id: identity("event", state.sessionId, sequence, "SESSION_COMPLETED"),
1442
+ type: "SESSION_COMPLETED",
1443
+ context: eventContext(state, sequence, state.virtualTime),
1444
+ payload: {
1445
+ finalStatus: "TERMINATED",
1446
+ completedAt: state.virtualTime,
1447
+ durationMillis: state.virtualElapsedMs,
1448
+ },
1449
+ };
1450
+ const candidate = {
1451
+ ...state,
1452
+ lifecycle: "closed",
1453
+ counters: {
1454
+ ...state.counters,
1455
+ commands: state.counters.commands + 1,
1456
+ events: state.counters.events + 1,
1457
+ },
1458
+ };
1459
+ const batch = [event];
1460
+ await write("close", retryKey, batch, candidate);
1461
+ return clone({ status: "closed", batch, state: candidate });
1462
+ }
1463
+ finally {
1464
+ commandInFlight = undefined;
1465
+ }
1466
+ };
1467
+ const reset = () => {
1468
+ if (commandInFlight !== undefined) {
1469
+ throw new FactoryEmulatorPendingCommandError("reset", commandInFlight);
1470
+ }
1471
+ if (committedState.lifecycle === "closed") {
1472
+ throw new FactoryEmulatorLifecycleError("reset", "closed");
1473
+ }
1474
+ pendingTransaction = undefined;
1475
+ pendingAdvanceContext = undefined;
1476
+ lastError = undefined;
1477
+ committedState = clone(PRE_START_STATE);
1478
+ return clone({ status: "reset", state: committedState });
1479
+ };
1480
+ return {
1481
+ start,
1482
+ submit,
1483
+ advanceBy: (durationMs) => runAdvance("advanceBy", durationMs),
1484
+ advanceToNext: () => runAdvance("advanceToNext"),
1485
+ close,
1486
+ reset,
1487
+ state: () => clone(committedState),
1488
+ status,
1489
+ };
1490
+ }