@you-agent-factory/factory-emulator 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.
@@ -0,0 +1,374 @@
1
+ import { deriveFactoryEmulatorIdentity } from "./identity.js";
2
+ import {
3
+ resolveEmulatorScenarioResult,
4
+ selectEmulatorRule,
5
+ } from "./semantics.js";
6
+ import {
7
+ FactoryEmulatorDurationError,
8
+ stateAtElapsed,
9
+ virtualTimeAt,
10
+ } from "./virtual-time.js";
11
+
12
+ /** Calculates one deterministic scheduler batch without performing sink IO. */
13
+ export function calculateNextSchedulerBatch({
14
+ createEvent,
15
+ identityCoordinates,
16
+ plan,
17
+ scenario,
18
+ state,
19
+ }) {
20
+ if (plan === undefined) {
21
+ return undefined;
22
+ }
23
+ const plannedWorks = plan.workIndexes.map((index) => ({
24
+ index,
25
+ work: state.works[index],
26
+ }));
27
+ if (plan.kind === "completion") {
28
+ return calculateCompletions({
29
+ createEvent,
30
+ identityCoordinates,
31
+ scenario,
32
+ state,
33
+ completedWorks: plannedWorks,
34
+ });
35
+ }
36
+ return calculateDispatchStarts({
37
+ createEvent,
38
+ identityCoordinates,
39
+ scenario,
40
+ state,
41
+ readyWorks: plannedWorks,
42
+ workIndexBySubmissionId: plan.workIndexBySubmissionId,
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Inspects a bounded slice of retained Work before an atomic scheduler batch is
48
+ * materialized. The returned continuation is explicit plain state and can be
49
+ * resumed after the host receives a task turn.
50
+ */
51
+ export function inspectNextSchedulerBatch({
52
+ continuation,
53
+ maximumWorkItems,
54
+ state,
55
+ }) {
56
+ const inspection = continuation === undefined
57
+ ? {
58
+ index: 0,
59
+ earliestElapsedMs: undefined,
60
+ earliestWorkIndexes: [],
61
+ hasWaitingWork: false,
62
+ readyWorkIndexes: [],
63
+ workIndexBySubmissionId: {},
64
+ }
65
+ : continuation;
66
+ let examined = 0;
67
+
68
+ while (examined < maximumWorkItems && inspection.index < state.works.length) {
69
+ const workIndex = inspection.index;
70
+ const work = state.works[workIndex];
71
+ inspection.index += 1;
72
+ examined += 1;
73
+ if (!Object.hasOwn(inspection.workIndexBySubmissionId, work.submissionId)) {
74
+ Object.defineProperty(inspection.workIndexBySubmissionId, work.submissionId, {
75
+ configurable: true,
76
+ enumerable: true,
77
+ value: workIndex,
78
+ writable: true,
79
+ });
80
+ }
81
+ if (work.phase === "active") {
82
+ const deadline = work.dispatch.dueElapsedMs;
83
+ if (
84
+ inspection.earliestElapsedMs === undefined
85
+ || deadline < inspection.earliestElapsedMs
86
+ ) {
87
+ inspection.earliestElapsedMs = deadline;
88
+ inspection.earliestWorkIndexes = [workIndex];
89
+ } else if (deadline === inspection.earliestElapsedMs) {
90
+ boundedPush(
91
+ inspection.earliestWorkIndexes,
92
+ workIndex,
93
+ maximumWorkItems,
94
+ );
95
+ }
96
+ } else if (work.phase === "ready") {
97
+ boundedPush(
98
+ inspection.readyWorkIndexes,
99
+ workIndex,
100
+ maximumWorkItems,
101
+ );
102
+ } else if (work.phase === "waiting") {
103
+ inspection.hasWaitingWork = true;
104
+ }
105
+ }
106
+
107
+ if (inspection.index === state.works.length) {
108
+ const completionIsDue = inspection.earliestElapsedMs !== undefined
109
+ && inspection.earliestElapsedMs <= state.virtualElapsedMs;
110
+ if (completionIsDue) {
111
+ return completedInspection(
112
+ "completion",
113
+ inspection.earliestWorkIndexes,
114
+ inspection.workIndexBySubmissionId,
115
+ );
116
+ }
117
+ if (inspection.readyWorkIndexes.length > 0) {
118
+ return completedInspection(
119
+ "dispatch",
120
+ inspection.readyWorkIndexes,
121
+ inspection.workIndexBySubmissionId,
122
+ );
123
+ }
124
+ return inspection.earliestElapsedMs === undefined
125
+ ? { done: true, batch: undefined, hasWaitingWork: inspection.hasWaitingWork }
126
+ : completedInspection(
127
+ "completion",
128
+ inspection.earliestWorkIndexes,
129
+ inspection.workIndexBySubmissionId,
130
+ );
131
+ }
132
+ return { done: false, continuation: inspection };
133
+ }
134
+
135
+ function boundedPush(values, value, maximum) {
136
+ if (values.length <= maximum) {
137
+ values.push(value);
138
+ }
139
+ }
140
+
141
+ function completedInspection(kind, workIndexes, workIndexBySubmissionId) {
142
+ return {
143
+ done: true,
144
+ batch: { kind, workItems: workIndexes.length },
145
+ hasWaitingWork: false,
146
+ plan: { kind, workIndexes, workIndexBySubmissionId },
147
+ };
148
+ }
149
+
150
+ function calculateDispatchStarts({
151
+ createEvent,
152
+ identityCoordinates,
153
+ scenario,
154
+ state,
155
+ readyWorks,
156
+ workIndexBySubmissionId,
157
+ }) {
158
+ const ruleCursors = { ...state.ruleCursors };
159
+ const replacements = new Map();
160
+ let eventOrdinal = 0;
161
+ const events = readyWorks.flatMap(({ index, work }) => {
162
+ const submission = { ...work, id: work.submissionId };
163
+ const rule = selectEmulatorRule(scenario, submission);
164
+ const invocationIndex = rule === undefined
165
+ ? 0
166
+ : ownNumberOrZero(ruleCursors, rule.id);
167
+ const resolution = resolveEmulatorScenarioResult(
168
+ scenario,
169
+ submission,
170
+ invocationIndex,
171
+ );
172
+ if (rule !== undefined) {
173
+ defineOwnValue(ruleCursors, rule.id, invocationIndex + 1);
174
+ }
175
+ const outcome = resolutionOutcome(resolution);
176
+ if (outcome === undefined) {
177
+ replacements.set(index, { ...work, phase: "waiting" });
178
+ return [];
179
+ }
180
+ const transitionId = rule?.id ?? "emulator-unmatched";
181
+ const lineageTokenId = resolveLineageTokenId({
182
+ identityCoordinates,
183
+ outcome,
184
+ scenario,
185
+ state,
186
+ work,
187
+ workIndexBySubmissionId,
188
+ });
189
+ const dispatchCoordinates = {
190
+ ...identityCoordinates,
191
+ workId: work.workId,
192
+ tokenId: work.tokenId,
193
+ lineageTokenId,
194
+ transitionId,
195
+ invocationIndex,
196
+ };
197
+ const dispatchId = deriveFactoryEmulatorIdentity("dispatch", dispatchCoordinates);
198
+ const completionId = deriveFactoryEmulatorIdentity("completion", dispatchCoordinates);
199
+ const outputTokenId = deriveFactoryEmulatorIdentity("token", {
200
+ ...dispatchCoordinates,
201
+ dispatchId,
202
+ completionId,
203
+ });
204
+ const durationMs = outcome.durationMs ?? 0;
205
+ const dueElapsedMs = state.virtualElapsedMs + durationMs;
206
+ if (!Number.isSafeInteger(dueElapsedMs)) {
207
+ throw new FactoryEmulatorDurationError(durationMs);
208
+ }
209
+ virtualTimeAt(scenario, dueElapsedMs);
210
+ replacements.set(index, {
211
+ ...work,
212
+ phase: "active",
213
+ dispatch: {
214
+ dispatchId,
215
+ completionId,
216
+ lineageTokenId,
217
+ outputTokenId,
218
+ transitionId,
219
+ startedElapsedMs: state.virtualElapsedMs,
220
+ dueElapsedMs,
221
+ outcome,
222
+ },
223
+ });
224
+ const sequence = state.counters.events + eventOrdinal;
225
+ eventOrdinal += 1;
226
+ return [createEvent({
227
+ identityCoordinates,
228
+ sequence,
229
+ tick: state.counters.events,
230
+ sessionId: state.sessionId,
231
+ eventTime: state.virtualTime,
232
+ type: "DISPATCH_REQUEST",
233
+ context: dispatchContext(work, dispatchId),
234
+ payload: {
235
+ transitionId,
236
+ inputs: [{ workId: work.workId }],
237
+ },
238
+ })];
239
+ });
240
+ return {
241
+ elapsedMs: state.virtualElapsedMs,
242
+ batch: events.length === 0 ? undefined : { events },
243
+ state: {
244
+ ...state,
245
+ ruleCursors,
246
+ counters: {
247
+ ...state.counters,
248
+ events: state.counters.events + events.length,
249
+ dispatches: state.counters.dispatches + events.length,
250
+ },
251
+ },
252
+ workReplacements: replacements,
253
+ };
254
+ }
255
+
256
+ function calculateCompletions({
257
+ createEvent,
258
+ identityCoordinates,
259
+ scenario,
260
+ state,
261
+ completedWorks,
262
+ }) {
263
+ const elapsedMs = completedWorks[0].work.dispatch.dueElapsedMs;
264
+ const eventTime = virtualTimeAt(scenario, elapsedMs);
265
+ const replacements = new Map();
266
+ const events = completedWorks.map(({ index, work }, ordinal) => {
267
+ const { dispatch } = work;
268
+ const accepted = dispatch.outcome.kind === "complete";
269
+ replacements.set(index, {
270
+ ...work,
271
+ phase: "completed",
272
+ tokenId: dispatch.outputTokenId,
273
+ ...(accepted && dispatch.outcome.output !== undefined
274
+ ? { output: dispatch.outcome.output }
275
+ : {}),
276
+ ...(!accepted ? { rejectionReason: dispatch.outcome.reason } : {}),
277
+ });
278
+ return createEvent({
279
+ identityCoordinates,
280
+ sequence: state.counters.events + ordinal,
281
+ tick: state.counters.events,
282
+ sessionId: state.sessionId,
283
+ eventTime,
284
+ type: "DISPATCH_RESPONSE",
285
+ context: dispatchContext(work, dispatch.dispatchId),
286
+ payload: {
287
+ completionId: dispatch.completionId,
288
+ transitionId: dispatch.transitionId,
289
+ outcome: accepted ? "ACCEPTED" : "REJECTED",
290
+ ...(!accepted ? { feedback: dispatch.outcome.reason } : {}),
291
+ durationMillis: dispatch.dueElapsedMs - dispatch.startedElapsedMs,
292
+ metadata: {
293
+ emulatorLineageTokenId: dispatch.lineageTokenId,
294
+ emulatorTokenId: dispatch.outputTokenId,
295
+ },
296
+ },
297
+ });
298
+ });
299
+ return {
300
+ elapsedMs,
301
+ batch: { events },
302
+ state: {
303
+ ...stateAtElapsed(scenario, state, elapsedMs),
304
+ counters: {
305
+ ...state.counters,
306
+ events: state.counters.events + events.length,
307
+ completions: state.counters.completions + events.length,
308
+ },
309
+ },
310
+ workReplacements: replacements,
311
+ };
312
+ }
313
+
314
+ function resolutionOutcome(resolution) {
315
+ if (resolution.kind === "outcome") {
316
+ return resolution.outcome;
317
+ }
318
+ if (resolution.behavior.kind === "reject") {
319
+ return { kind: "reject", reason: resolution.behavior.reason };
320
+ }
321
+ return undefined;
322
+ }
323
+
324
+ function resolveLineageTokenId({
325
+ identityCoordinates,
326
+ outcome,
327
+ scenario,
328
+ state,
329
+ work,
330
+ workIndexBySubmissionId,
331
+ }) {
332
+ const cursor = outcome?.kind === "complete" ? outcome.lineageCursor : undefined;
333
+ if (cursor?.kind === "initialSubmission") {
334
+ const workIndex = Object.hasOwn(workIndexBySubmissionId, cursor.submissionId)
335
+ ? workIndexBySubmissionId[cursor.submissionId]
336
+ : undefined;
337
+ return state.works[workIndex]?.tokenId;
338
+ }
339
+ if (cursor?.kind === "scriptedOutcome") {
340
+ const ruleIndex = scenario.rules.findIndex((rule) => rule.id === cursor.ruleId);
341
+ return deriveFactoryEmulatorIdentity("token", {
342
+ ...identityCoordinates,
343
+ lineage: {
344
+ kind: "scriptedOutcome",
345
+ ruleIndex,
346
+ ruleId: cursor.ruleId,
347
+ outcomeIndex: cursor.outcomeIndex,
348
+ },
349
+ });
350
+ }
351
+ return work.tokenId;
352
+ }
353
+
354
+ function ownNumberOrZero(values, name) {
355
+ return Object.hasOwn(values, name) ? values[name] : 0;
356
+ }
357
+
358
+ function defineOwnValue(values, name, value) {
359
+ Object.defineProperty(values, name, {
360
+ configurable: true,
361
+ enumerable: true,
362
+ value,
363
+ writable: true,
364
+ });
365
+ }
366
+ function dispatchContext(work, dispatchId) {
367
+ return {
368
+ dispatchId,
369
+ requestId: work.requestId,
370
+ traceIds: [work.traceId],
371
+ workIds: [work.workId],
372
+ currentChainingTraceId: work.traceId,
373
+ };
374
+ }