@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,333 @@
1
+ import Ajv2020, {} from "ajv/dist/2020.js";
2
+ import addFormats from "ajv-formats";
3
+ import { generatedScenarioSchema } from "./generated/scenario-schema.js";
4
+ import { FactoryEmulatorScenarioValidationError, } from "./scenario-contracts.js";
5
+ import { validateDependencyRelationships } from "./submission-validation.js";
6
+ export * from "./scenario-contracts.js";
7
+ export const scenarioSchema = generatedScenarioSchema;
8
+ const ajv = new Ajv2020({
9
+ allErrors: true,
10
+ coerceTypes: false,
11
+ removeAdditional: false,
12
+ strict: false,
13
+ useDefaults: false,
14
+ });
15
+ addFormats(ajv);
16
+ const validateScenarioShape = ajv.compile(generatedScenarioSchema);
17
+ function pointerSegments(pointer) {
18
+ if (pointer.length === 0)
19
+ return [];
20
+ return pointer
21
+ .slice(1)
22
+ .split("/")
23
+ .map((segment) => segment.replaceAll("~1", "/").replaceAll("~0", "~"))
24
+ .map((segment) => /^(0|[1-9]\d*)$/.test(segment) ? Number(segment) : segment);
25
+ }
26
+ function errorPath(error) {
27
+ const path = pointerSegments(error.instancePath);
28
+ if (error.keyword === "required") {
29
+ return [...path, error.params.missingProperty];
30
+ }
31
+ if (error.keyword === "additionalProperties") {
32
+ return [...path, error.params.additionalProperty];
33
+ }
34
+ return path;
35
+ }
36
+ function structuralIssueCode(error, path) {
37
+ if (error.keyword === "required")
38
+ return "missing_required_field";
39
+ if (error.keyword === "additionalProperties")
40
+ return "unsupported_field";
41
+ if (error.keyword === "type")
42
+ return "invalid_type";
43
+ if (path.join(".") === "schemaVersion")
44
+ return "unsupported_schema_version";
45
+ if (path.join(".") === "startAt")
46
+ return "invalid_start_at";
47
+ if (error.keyword === "pattern" &&
48
+ ["id", "name"].includes(String(path.at(-1)))) {
49
+ return "unstable_identity";
50
+ }
51
+ if (path.includes("cursor"))
52
+ return "invalid_cursor";
53
+ if (path.at(-1) === "exhaustion")
54
+ return "invalid_exhaustion";
55
+ if (path.includes("unmatched"))
56
+ return "invalid_unmatched";
57
+ if (path.includes("outcomes") || path.at(-1) === "outcome") {
58
+ return "invalid_outcome";
59
+ }
60
+ return "invalid_value";
61
+ }
62
+ function structuralIssues(errors) {
63
+ const results = [];
64
+ const seen = new Set();
65
+ for (const error of errors ?? []) {
66
+ if (error.keyword === "oneOf")
67
+ continue;
68
+ const path = errorPath(error);
69
+ const code = structuralIssueCode(error, path);
70
+ const field = path.at(-1);
71
+ const message = error.keyword === "required"
72
+ ? `Expected required field ${String(field)}.`
73
+ : error.keyword === "additionalProperties"
74
+ ? `Unsupported field ${String(field)}.`
75
+ : `Expected ${path.join(".") || "scenario"} to satisfy the scenario contract: ${error.message ?? error.keyword}.`;
76
+ const key = `${code}:${path.join(".")}:${message}`;
77
+ if (!seen.has(key)) {
78
+ seen.add(key);
79
+ results.push({ category: "structure", code, path, message });
80
+ }
81
+ }
82
+ return results;
83
+ }
84
+ function addDuplicateIssues(values, pathForIndex, label, issues) {
85
+ const indexesByValue = new Map();
86
+ for (const [index, value] of values.entries()) {
87
+ const indexes = indexesByValue.get(value) ?? [];
88
+ indexes.push(index);
89
+ indexesByValue.set(value, indexes);
90
+ }
91
+ for (const [value, indexes] of indexesByValue) {
92
+ if (indexes.length < 2)
93
+ continue;
94
+ for (const index of indexes) {
95
+ issues.push({
96
+ category: "semantic",
97
+ code: "duplicate_identity",
98
+ path: pathForIndex(index),
99
+ message: `${label} identity ${value} is duplicated at indexes ${indexes.join(", ")}.`,
100
+ });
101
+ }
102
+ }
103
+ }
104
+ function isNormalizedUtcTimestamp(value) {
105
+ const timestamp = Date.parse(value);
106
+ return (Number.isFinite(timestamp) && new Date(timestamp).toISOString() === value);
107
+ }
108
+ function selectorCovers(earlier, later) {
109
+ const earlierValues = [
110
+ earlier.workstation,
111
+ earlier.worker,
112
+ earlier.input?.workType,
113
+ earlier.input?.state,
114
+ earlier.input?.name,
115
+ ];
116
+ const laterValues = [
117
+ later.workstation,
118
+ later.worker,
119
+ later.input?.workType,
120
+ later.input?.state,
121
+ later.input?.name,
122
+ ];
123
+ return earlierValues.every((value, index) => value === undefined || value === laterValues[index]);
124
+ }
125
+ function validateOutcome(outcome, path, issues) {
126
+ if (!Number.isFinite(outcome.durationMs) || outcome.durationMs < 0) {
127
+ issues.push({
128
+ category: "semantic",
129
+ code: "invalid_outcome",
130
+ path: [...path, "durationMs"],
131
+ message: "Outcome durationMs must be a non-negative finite number.",
132
+ });
133
+ }
134
+ const hasError = outcome.error !== undefined;
135
+ if (outcome.result === "failed" ? !hasError : hasError) {
136
+ issues.push({
137
+ category: "semantic",
138
+ code: "invalid_outcome",
139
+ path: [...path, "error"],
140
+ message: outcome.result === "failed"
141
+ ? "A failed outcome requires an error."
142
+ : `An ${outcome.result} outcome must not declare an error.`,
143
+ });
144
+ }
145
+ }
146
+ function validateSelectorReferences(scenario, factory, issues) {
147
+ const workstationByName = new Map((factory.workstations ?? []).map((workstation) => [
148
+ workstation.name,
149
+ workstation,
150
+ ]));
151
+ const workerNames = new Set((factory.workers ?? []).map((worker) => worker.name));
152
+ const workTypes = new Map((factory.workTypes ?? []).map((workType) => [
153
+ workType.name,
154
+ new Set(workType.states.map((state) => state.name)),
155
+ ]));
156
+ for (const [index, rule] of scenario.rules.entries()) {
157
+ const path = ["rules", index, "selector"];
158
+ const workstation = rule.selector.workstation;
159
+ const worker = rule.selector.worker;
160
+ const workType = rule.selector.input?.workType;
161
+ const state = rule.selector.input?.state;
162
+ if (workstation !== undefined && !workstationByName.has(workstation)) {
163
+ issues.push(referenceIssue([...path, "workstation"], "workstation", workstation));
164
+ }
165
+ if (worker !== undefined && !workerNames.has(worker)) {
166
+ issues.push(referenceIssue([...path, "worker"], "worker", worker));
167
+ }
168
+ if (workstation !== undefined &&
169
+ worker !== undefined &&
170
+ workstationByName.get(workstation)?.worker !== worker) {
171
+ issues.push({
172
+ category: "semantic",
173
+ code: "invalid_selector_reference",
174
+ path: [...path, "worker"],
175
+ message: `Worker ${worker} is not assigned to workstation ${workstation}.`,
176
+ });
177
+ }
178
+ if (workType !== undefined && !workTypes.has(workType)) {
179
+ issues.push(referenceIssue([...path, "input", "workType"], "Work type", workType));
180
+ }
181
+ if (state !== undefined && workType === undefined) {
182
+ issues.push({
183
+ category: "semantic",
184
+ code: "invalid_selector_reference",
185
+ path: [...path, "input", "state"],
186
+ message: "A selector state requires an input Work type.",
187
+ });
188
+ }
189
+ else if (state !== undefined &&
190
+ !workTypes.get(workType ?? "")?.has(state)) {
191
+ issues.push(referenceIssue([...path, "input", "state"], "Work state", state));
192
+ }
193
+ }
194
+ const submissions = submissionWorks(scenario.initialSubmissions);
195
+ const workPathPrefix = scenario.initialSubmissions !== undefined &&
196
+ isSubmissionArray(scenario.initialSubmissions)
197
+ ? ["initialSubmissions"]
198
+ : ["initialSubmissions", "works"];
199
+ for (const [index, submission] of submissions.entries()) {
200
+ const path = [...workPathPrefix, index];
201
+ if (!workTypes.has(submission.workType)) {
202
+ issues.push(referenceIssue([...path, "workType"], "Work type", submission.workType));
203
+ }
204
+ else if (!workTypes.get(submission.workType)?.has(submission.state)) {
205
+ issues.push(referenceIssue([...path, "state"], "Work state", submission.state));
206
+ }
207
+ }
208
+ }
209
+ function submissionWorks(value) {
210
+ if (value === undefined)
211
+ return [];
212
+ return isSubmissionArray(value) ? value : value.works;
213
+ }
214
+ function isSubmissionArray(value) {
215
+ return Array.isArray(value);
216
+ }
217
+ function referenceIssue(path, label, value) {
218
+ return {
219
+ category: "semantic",
220
+ code: "invalid_selector_reference",
221
+ path,
222
+ message: `${label} ${value} is not declared by the Factory.`,
223
+ };
224
+ }
225
+ function validateSubmissionRelationships(submissions, pathPrefix, issues) {
226
+ const names = new Set(submissions.map((submission) => submission.name));
227
+ const parentByName = new Map(submissions
228
+ .filter((submission) => submission.parent !== undefined)
229
+ .map((submission) => [submission.name, submission.parent]));
230
+ for (const [index, submission] of submissions.entries()) {
231
+ if (submission.parent === undefined)
232
+ continue;
233
+ if (!names.has(submission.parent)) {
234
+ issues.push(relationshipIssue(pathPrefix, index, `Parent ${submission.parent} is not in this submission batch.`));
235
+ continue;
236
+ }
237
+ const visited = new Set([submission.name]);
238
+ let parent = submission.parent;
239
+ while (parent !== undefined) {
240
+ if (visited.has(parent)) {
241
+ issues.push(relationshipIssue(pathPrefix, index, "Initial submission parent relationships must be acyclic."));
242
+ break;
243
+ }
244
+ visited.add(parent);
245
+ parent = parentByName.get(parent);
246
+ }
247
+ }
248
+ }
249
+ function relationshipIssue(pathPrefix, index, message) {
250
+ return {
251
+ category: "semantic",
252
+ code: "invalid_initial_submission_relationship",
253
+ path: [...pathPrefix, index, "parent"],
254
+ message,
255
+ };
256
+ }
257
+ function semanticIssues(scenario, factory) {
258
+ const issues = [];
259
+ if (scenario.factory.name !== factory.name) {
260
+ issues.push({
261
+ category: "semantic",
262
+ code: "invalid_factory_identity",
263
+ path: ["factory", "name"],
264
+ message: `Scenario Factory ${scenario.factory.name} does not match ${factory.name}.`,
265
+ });
266
+ }
267
+ if (!isNormalizedUtcTimestamp(scenario.startAt)) {
268
+ issues.push({
269
+ category: "semantic",
270
+ code: "invalid_start_at",
271
+ path: ["startAt"],
272
+ message: "startAt must be a normalized UTC RFC 3339 timestamp with millisecond precision.",
273
+ });
274
+ }
275
+ addDuplicateIssues(scenario.rules.map((rule) => rule.id), (index) => ["rules", index, "id"], "Rule", issues);
276
+ const submissions = submissionWorks(scenario.initialSubmissions);
277
+ const submissionPathPrefix = scenario.initialSubmissions !== undefined &&
278
+ isSubmissionArray(scenario.initialSubmissions)
279
+ ? ["initialSubmissions"]
280
+ : ["initialSubmissions", "works"];
281
+ addDuplicateIssues(submissions.map((submission) => submission.name), (index) => [...submissionPathPrefix, index, "name"], "Initial submission", issues);
282
+ for (const [index, rule] of scenario.rules.entries()) {
283
+ const shadowingIndex = scenario.rules
284
+ .slice(0, index)
285
+ .findIndex((earlier) => selectorCovers(earlier.selector, rule.selector));
286
+ if (shadowingIndex >= 0) {
287
+ issues.push({
288
+ category: "semantic",
289
+ code: "fully_shadowed_rule",
290
+ path: ["rules", index, "selector"],
291
+ message: `Rule ${rule.id} is fully shadowed by earlier first-match rule ${scenario.rules[shadowingIndex]?.id}.`,
292
+ });
293
+ }
294
+ rule.outcomes.forEach((outcome, outcomeIndex) => {
295
+ validateOutcome(outcome, ["rules", index, "outcomes", outcomeIndex], issues);
296
+ });
297
+ }
298
+ if (scenario.unmatched.behavior === "outcome") {
299
+ validateOutcome(scenario.unmatched.outcome, ["unmatched", "outcome"], issues);
300
+ }
301
+ validateSelectorReferences(scenario, factory, issues);
302
+ validateSubmissionRelationships(submissions, submissionPathPrefix, issues);
303
+ if (scenario.initialSubmissions !== undefined &&
304
+ !isSubmissionArray(scenario.initialSubmissions)) {
305
+ validateDependencyRelationships(scenario.initialSubmissions, factory, issues);
306
+ }
307
+ return issues;
308
+ }
309
+ /** Validate an authored scenario without mutating it or the Factory context. */
310
+ export function safeParseFactoryEmulatorScenario(input, factory) {
311
+ if (!validateScenarioShape(input)) {
312
+ return {
313
+ success: false,
314
+ issues: structuralIssues(validateScenarioShape.errors),
315
+ };
316
+ }
317
+ const scenario = input;
318
+ const issues = semanticIssues(scenario, factory);
319
+ if (issues.length > 0)
320
+ return { success: false, issues };
321
+ return {
322
+ success: true,
323
+ data: JSON.parse(JSON.stringify(scenario)),
324
+ };
325
+ }
326
+ /** Parse a complete scenario or throw one error containing every diagnostic. */
327
+ export function parseFactoryEmulatorScenario(input, factory) {
328
+ const result = safeParseFactoryEmulatorScenario(input, factory);
329
+ if (!result.success) {
330
+ throw new FactoryEmulatorScenarioValidationError(result.issues);
331
+ }
332
+ return result.data;
333
+ }
@@ -0,0 +1,24 @@
1
+ export declare const FACTORY_EMULATOR_SCHEDULER_CANDIDATE_LIMIT = 50;
2
+ export interface FactorySchedulerToken {
3
+ readonly tokenId: string;
4
+ readonly customerWork: boolean;
5
+ readonly processing: boolean;
6
+ readonly queuedElapsedMs: number;
7
+ readonly lastDispatchElapsedMs?: number;
8
+ }
9
+ export interface FactorySchedulerResourceClaim {
10
+ readonly name: string;
11
+ readonly capacity: number;
12
+ }
13
+ export interface FactorySchedulerCandidate<Value = unknown> {
14
+ readonly transitionId: string;
15
+ readonly workerId: string;
16
+ readonly workstationKind: "logical" | "normal";
17
+ readonly tokens: readonly FactorySchedulerToken[];
18
+ readonly resources: readonly FactorySchedulerResourceClaim[];
19
+ readonly value: Value;
20
+ }
21
+ /** Go-compatible Work-in-queue ordering for the emulator's supported subset. */
22
+ export declare function compareFactorySchedulerCandidates(left: FactorySchedulerCandidate, right: FactorySchedulerCandidate): number;
23
+ /** Selects one deterministic batch while preventing double token consumption. */
24
+ export declare function selectFactorySchedulerCandidates<Value>(candidates: readonly FactorySchedulerCandidate<Value>[], limit?: number, availableResources?: Readonly<Record<string, number>>): readonly FactorySchedulerCandidate<Value>[];
@@ -0,0 +1,104 @@
1
+ export const FACTORY_EMULATOR_SCHEDULER_CANDIDATE_LIMIT = 50;
2
+ function uniqueTokens(candidate) {
3
+ const seen = new Set();
4
+ return candidate.tokens.filter(({ tokenId }) => {
5
+ if (seen.has(tokenId))
6
+ return false;
7
+ seen.add(tokenId);
8
+ return true;
9
+ });
10
+ }
11
+ function earliest(values) {
12
+ const present = values.filter((value) => value !== undefined);
13
+ return present.length === 0 ? undefined : Math.min(...present);
14
+ }
15
+ function compareOptionalAge(left, right) {
16
+ if (left === undefined)
17
+ return right === undefined ? 0 : 1;
18
+ if (right === undefined)
19
+ return -1;
20
+ return left - right;
21
+ }
22
+ function compareText(left, right) {
23
+ return left < right ? -1 : left > right ? 1 : 0;
24
+ }
25
+ function compareTokenIds(left, right) {
26
+ if (left.length !== right.length)
27
+ return left.length - right.length;
28
+ for (const [index, token] of left.entries()) {
29
+ const comparison = compareText(token.tokenId, right[index]?.tokenId ?? "");
30
+ if (comparison !== 0)
31
+ return comparison;
32
+ }
33
+ return 0;
34
+ }
35
+ /** Go-compatible Work-in-queue ordering for the emulator's supported subset. */
36
+ export function compareFactorySchedulerCandidates(left, right) {
37
+ const leftTokens = uniqueTokens(left);
38
+ const rightTokens = uniqueTokens(right);
39
+ const processing = rightTokens.filter(({ processing: value }) => value).length -
40
+ leftTokens.filter(({ processing: value }) => value).length;
41
+ if (processing !== 0)
42
+ return processing;
43
+ const customer = Number(rightTokens.some(({ customerWork }) => customerWork)) -
44
+ Number(leftTokens.some(({ customerWork }) => customerWork));
45
+ if (customer !== 0)
46
+ return customer;
47
+ const workstation = Number(left.workstationKind === "normal") -
48
+ Number(right.workstationKind === "normal");
49
+ if (workstation !== 0)
50
+ return workstation;
51
+ const leftDispatchAge = earliest(leftTokens.map(({ lastDispatchElapsedMs }) => lastDispatchElapsedMs));
52
+ const rightDispatchAge = earliest(rightTokens.map(({ lastDispatchElapsedMs }) => lastDispatchElapsedMs));
53
+ const initialized = Number(rightDispatchAge !== undefined) -
54
+ Number(leftDispatchAge !== undefined);
55
+ if (initialized !== 0)
56
+ return initialized;
57
+ if (leftDispatchAge !== undefined && rightDispatchAge !== undefined) {
58
+ const dispatchAge = compareOptionalAge(leftDispatchAge, rightDispatchAge);
59
+ if (dispatchAge !== 0)
60
+ return dispatchAge;
61
+ }
62
+ const queueAge = compareOptionalAge(earliest(leftTokens.map(({ queuedElapsedMs }) => queuedElapsedMs)), earliest(rightTokens.map(({ queuedElapsedMs }) => queuedElapsedMs)));
63
+ if (queueAge !== 0)
64
+ return queueAge;
65
+ const transition = compareText(left.transitionId, right.transitionId);
66
+ if (transition !== 0)
67
+ return transition;
68
+ const worker = compareText(left.workerId, right.workerId);
69
+ if (worker !== 0)
70
+ return worker;
71
+ return compareTokenIds(leftTokens, rightTokens);
72
+ }
73
+ /** Selects one deterministic batch while preventing double token consumption. */
74
+ export function selectFactorySchedulerCandidates(candidates, limit = FACTORY_EMULATOR_SCHEDULER_CANDIDATE_LIMIT, availableResources = {}) {
75
+ const selected = [];
76
+ const claimed = new Set();
77
+ const claimedResources = {};
78
+ const boundedLimit = Math.max(0, Math.min(limit, FACTORY_EMULATOR_SCHEDULER_CANDIDATE_LIMIT));
79
+ for (const candidate of [...candidates].sort(compareFactorySchedulerCandidates)) {
80
+ if (selected.length >= boundedLimit)
81
+ break;
82
+ const tokenIds = uniqueTokens(candidate).map(({ tokenId }) => tokenId);
83
+ if (tokenIds.length === 0 ||
84
+ tokenIds.some((tokenId) => claimed.has(tokenId))) {
85
+ continue;
86
+ }
87
+ const resourceClaims = candidate.resources.reduce((claims, resource) => {
88
+ claims[resource.name] =
89
+ (claims[resource.name] ?? 0) + resource.capacity;
90
+ return claims;
91
+ }, {});
92
+ if (Object.entries(resourceClaims).some(([name, capacity]) => capacity + (claimedResources[name] ?? 0) >
93
+ (availableResources[name] ?? 0))) {
94
+ continue;
95
+ }
96
+ for (const tokenId of tokenIds)
97
+ claimed.add(tokenId);
98
+ for (const [name, capacity] of Object.entries(resourceClaims)) {
99
+ claimedResources[name] = (claimedResources[name] ?? 0) + capacity;
100
+ }
101
+ selected.push(candidate);
102
+ }
103
+ return selected;
104
+ }