@hunterzhu/pulse-runtime 0.1.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.
Files changed (93) hide show
  1. package/dist/context/builder.d.ts +68 -0
  2. package/dist/context/builder.js +127 -0
  3. package/dist/context/index.d.ts +2 -0
  4. package/dist/context/index.js +2 -0
  5. package/dist/context/merger.d.ts +25 -0
  6. package/dist/context/merger.js +125 -0
  7. package/dist/core/actions.d.ts +1 -0
  8. package/dist/core/actions.js +1 -0
  9. package/dist/core/errors.d.ts +8 -0
  10. package/dist/core/errors.js +36 -0
  11. package/dist/core/events.d.ts +10 -0
  12. package/dist/core/events.js +24 -0
  13. package/dist/core/factory.d.ts +35 -0
  14. package/dist/core/factory.js +27 -0
  15. package/dist/core/inbox.d.ts +119 -0
  16. package/dist/core/inbox.js +217 -0
  17. package/dist/core/mutations.d.ts +80 -0
  18. package/dist/core/mutations.js +127 -0
  19. package/dist/core/records.d.ts +1 -0
  20. package/dist/core/records.js +1 -0
  21. package/dist/core/types.d.ts +615 -0
  22. package/dist/core/types.js +109 -0
  23. package/dist/dependencies/graph.d.ts +25 -0
  24. package/dist/dependencies/graph.js +92 -0
  25. package/dist/dependencies/index.d.ts +1 -0
  26. package/dist/dependencies/index.js +1 -0
  27. package/dist/dsl/context-proxy.d.ts +20 -0
  28. package/dist/dsl/context-proxy.js +64 -0
  29. package/dist/dsl/index.d.ts +4 -0
  30. package/dist/dsl/index.js +4 -0
  31. package/dist/dsl/program.d.ts +314 -0
  32. package/dist/dsl/program.js +756 -0
  33. package/dist/dsl/session.d.ts +45 -0
  34. package/dist/dsl/session.js +93 -0
  35. package/dist/dsl/templates-index.d.ts +1 -0
  36. package/dist/dsl/templates-index.js +1 -0
  37. package/dist/dsl/templates.d.ts +85 -0
  38. package/dist/dsl/templates.js +110 -0
  39. package/dist/index.d.ts +15 -0
  40. package/dist/index.js +15 -0
  41. package/dist/lifecycle/index.d.ts +2 -0
  42. package/dist/lifecycle/index.js +2 -0
  43. package/dist/lifecycle/scopes.d.ts +38 -0
  44. package/dist/lifecycle/scopes.js +50 -0
  45. package/dist/lifecycle/watchdog.d.ts +16 -0
  46. package/dist/lifecycle/watchdog.js +66 -0
  47. package/dist/models/actions.d.ts +10 -0
  48. package/dist/models/actions.js +68 -0
  49. package/dist/models/index.d.ts +2 -0
  50. package/dist/models/index.js +2 -0
  51. package/dist/models/router.d.ts +187 -0
  52. package/dist/models/router.js +353 -0
  53. package/dist/scheduler/clock.d.ts +45 -0
  54. package/dist/scheduler/clock.js +92 -0
  55. package/dist/scheduler/decision.d.ts +72 -0
  56. package/dist/scheduler/decision.js +63 -0
  57. package/dist/scheduler/index.d.ts +6 -0
  58. package/dist/scheduler/index.js +6 -0
  59. package/dist/scheduler/locks.d.ts +18 -0
  60. package/dist/scheduler/locks.js +106 -0
  61. package/dist/scheduler/ready-queue.d.ts +32 -0
  62. package/dist/scheduler/ready-queue.js +40 -0
  63. package/dist/scheduler/runtime.d.ts +486 -0
  64. package/dist/scheduler/runtime.js +3445 -0
  65. package/dist/scheduler/telemetry.d.ts +111 -0
  66. package/dist/scheduler/telemetry.js +177 -0
  67. package/dist/scheduler/worker.d.ts +158 -0
  68. package/dist/scheduler/worker.js +744 -0
  69. package/dist/storage/artifacts.d.ts +17 -0
  70. package/dist/storage/artifacts.js +90 -0
  71. package/dist/storage/findings.d.ts +12 -0
  72. package/dist/storage/findings.js +70 -0
  73. package/dist/storage/index.d.ts +8 -0
  74. package/dist/storage/index.js +8 -0
  75. package/dist/storage/memory.d.ts +11 -0
  76. package/dist/storage/memory.js +21 -0
  77. package/dist/storage/mutation-log.d.ts +41 -0
  78. package/dist/storage/mutation-log.js +140 -0
  79. package/dist/storage/outbox.d.ts +30 -0
  80. package/dist/storage/outbox.js +59 -0
  81. package/dist/storage/persistence.d.ts +183 -0
  82. package/dist/storage/persistence.js +999 -0
  83. package/dist/storage/policy.d.ts +80 -0
  84. package/dist/storage/policy.js +268 -0
  85. package/dist/storage/session.d.ts +140 -0
  86. package/dist/storage/session.js +447 -0
  87. package/dist/tools/registry.d.ts +125 -0
  88. package/dist/tools/registry.js +308 -0
  89. package/dist/transitions/index.d.ts +2 -0
  90. package/dist/transitions/index.js +1 -0
  91. package/dist/transitions/validate.d.ts +4 -0
  92. package/dist/transitions/validate.js +1118 -0
  93. package/package.json +21 -0
@@ -0,0 +1,756 @@
1
+ import { z } from 'zod';
2
+ import { globalContextRef, laneContextRef } from '../core/types.js';
3
+ import { createDraftProxy } from './context-proxy.js';
4
+ import { contentHash, stableSerialize } from '../context/builder.js';
5
+ function target(step, fallback) {
6
+ if (typeof step === 'string')
7
+ return { step };
8
+ if ('step' in step)
9
+ return { step: step.step };
10
+ if ('complete' in step)
11
+ return { step: fallback, action: { type: 'complete', result: step.complete.value ?? null, ...(step.complete.privacy === undefined ? {} : { privacy: step.complete.privacy }), ...(step.complete.children === undefined ? {} : { children: step.complete.children }) } };
12
+ return { step: fallback, action: { type: 'fail', error: { code: step.fail.code, message: step.fail.message, ...(step.fail.retryable === undefined ? {} : { retryable: step.fail.retryable }), ...(step.fail.details === undefined ? {} : { details: step.fail.details }) }, ...(step.fail.privacy === undefined ? {} : { privacy: step.fail.privacy }), ...(step.fail.derivedFrom === undefined ? {} : { derivedFrom: step.fail.derivedFrom }) } };
13
+ }
14
+ function clone(value) { return structuredClone(value); }
15
+ function deepFreeze(value, seen = new Set()) {
16
+ if (!value || typeof value !== 'object' || seen.has(value))
17
+ return value;
18
+ seen.add(value);
19
+ for (const child of Object.values(value))
20
+ deepFreeze(child, seen);
21
+ return Object.freeze(value);
22
+ }
23
+ function asJson(value) { return value; }
24
+ function scalarProjection(value) {
25
+ if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
26
+ return value;
27
+ if (!value || typeof value !== 'object' || Array.isArray(value))
28
+ return {};
29
+ return Object.fromEntries(Object.entries(value).filter(([, child]) => child === null || typeof child === 'string' || typeof child === 'number' || typeof child === 'boolean').map(([key, child]) => [key, child]));
30
+ }
31
+ function boundedInstruction(value) {
32
+ if (Buffer.byteLength(value, 'utf8') > 2048)
33
+ throw Object.assign(new Error('Instruction exceeds the 2 KB DSL limit.'), { code: 'INSTRUCTION_TOO_LARGE', retryable: false });
34
+ return value;
35
+ }
36
+ function programLLMInput(config, input) {
37
+ return { ...input, ...(config.system === undefined ? {} : { system: config.system }), ...(config.toolSet === undefined ? {} : { toolSetId: config.toolSet }) };
38
+ }
39
+ function affinityAdvice(ctx) {
40
+ const input = ctx.resumeInput;
41
+ if (!input || input.type !== 'control_error' || input.error.code !== 'FORK_AFFINITY_COLLAPSIBLE')
42
+ return undefined;
43
+ const details = input.error.details;
44
+ if (!details || typeof details !== 'object' || Array.isArray(details))
45
+ return undefined;
46
+ const groups = details.groups;
47
+ if (!Array.isArray(groups))
48
+ return undefined;
49
+ return groups.flatMap((group) => {
50
+ if (!group || typeof group !== 'object' || Array.isArray(group))
51
+ return [];
52
+ const value = group;
53
+ const keys = Array.isArray(value.keys) ? value.keys.filter((key) => typeof key === 'string') : [];
54
+ const signals = Array.isArray(value.signals) ? value.signals.filter((signal) => typeof signal === 'string') : [];
55
+ return keys.length > 1 ? [{ keys, signals }] : [];
56
+ });
57
+ }
58
+ function sameProgram(left, right) {
59
+ return left.program.programId === right.program.programId && left.program.programVersion === right.program.programVersion && left.program.step === right.program.step && JSON.stringify(left.program.locals ?? {}) === JSON.stringify(right.program.locals ?? {});
60
+ }
61
+ function normalizeDependsOn(dependencies) {
62
+ return dependencies?.map((dependency) => 'sibling' in dependency ? { key: dependency.sibling, target: { local: dependency.sibling }, condition: dependency.condition } : dependency);
63
+ }
64
+ function joinOptions(value, legacy) {
65
+ return { condition: value?.condition ?? legacy.condition ?? 'settled', mode: legacy.mode ?? 'all', ...(legacy.quorum === undefined ? {} : { quorum: legacy.quorum }), ...(legacy.deadlineAt === undefined ? {} : { deadlineAt: legacy.deadlineAt }), onUnsatisfied: value?.onUnsatisfied ?? 'resume_with_error', ...(value?.onCancelled === undefined ? {} : { onCancelled: value.onCancelled }) };
66
+ }
67
+ function seriesOrder(members) {
68
+ const byKey = new Map(members.map((lane) => [lane.key, lane]));
69
+ const visiting = new Set();
70
+ const visited = new Set();
71
+ const ordered = [];
72
+ const visit = (key) => {
73
+ if (visited.has(key))
74
+ return true;
75
+ if (visiting.has(key))
76
+ return false;
77
+ visiting.add(key);
78
+ const lane = byKey.get(key);
79
+ if (!lane)
80
+ return false;
81
+ for (const dependency of lane.dependsOn ?? [])
82
+ if ('local' in dependency.target && byKey.has(dependency.target.local) && !visit(dependency.target.local))
83
+ return false;
84
+ visiting.delete(key);
85
+ visited.add(key);
86
+ ordered.push(lane);
87
+ return true;
88
+ };
89
+ return members.every((lane) => visit(lane.key)) ? ordered : undefined;
90
+ }
91
+ function collapseAffinityLanes(name, lanes, groups, enabled, joinMode, condition) {
92
+ if (!enabled || joinMode !== 'all' || condition !== 'settled')
93
+ return { lanes };
94
+ const byKey = new Map(lanes.map((lane) => [lane.key, lane]));
95
+ const collapsed = new Set();
96
+ const aliases = {};
97
+ const output = [];
98
+ let groupIndex = 0;
99
+ for (const group of groups) {
100
+ const members = group.keys.map((key) => byKey.get(key)).filter((lane) => lane !== undefined);
101
+ if (members.length !== group.keys.length || members.some((lane) => (lane.dependsOn ?? []).some((dependency) => !('local' in dependency.target) || !group.keys.includes(dependency.target.local))) || members.some((lane) => !sameProgram(lane, members[0])))
102
+ continue;
103
+ const ordered = seriesOrder(members);
104
+ if (!ordered)
105
+ continue;
106
+ const key = `__series_${name}_${groupIndex++}`;
107
+ const member = ordered[0];
108
+ const internalDependencies = Object.fromEntries(ordered.flatMap((lane) => {
109
+ const dependsOn = (lane.dependsOn ?? []).filter((dependency) => 'local' in dependency.target).map((dependency) => ({ key: dependency.target.local, condition: dependency.condition }));
110
+ return dependsOn.length ? [[lane.key, { dependsOn }]] : [];
111
+ }));
112
+ output.push({ key, goal: ordered.map((lane) => `${lane.key}: ${lane.goal}`).join('\n'), program: member.program, ...(member.priority === undefined ? {} : { priority: member.priority }), ...(member.contextVersion === undefined ? {} : { contextVersion: member.contextVersion }), ...(member.resources === undefined ? {} : { resources: member.resources }), series: { member: member.program, keys: ordered.map((lane) => lane.key), goals: Object.fromEntries(ordered.map((lane) => [lane.key, lane.goal])), ...(Object.keys(internalDependencies).length ? { members: internalDependencies } : {}), onMemberFailure: 'continue' } });
113
+ for (const lane of ordered) {
114
+ collapsed.add(lane.key);
115
+ aliases[lane.key] = key;
116
+ }
117
+ }
118
+ output.push(...lanes.filter((lane) => !collapsed.has(lane.key)));
119
+ return Object.keys(aliases).length ? { lanes: output, aliases } : { lanes };
120
+ }
121
+ function joinedOutcome(ctx, dependency, key) {
122
+ if (dependency.outcome.resultRef === undefined)
123
+ return dependency.outcome;
124
+ const value = readResult(ctx, dependency.outcome.resultRef);
125
+ if (!value || typeof value !== 'object' || Array.isArray(value))
126
+ return dependency.outcome;
127
+ const member = value.results;
128
+ if (!member || typeof member !== 'object' || Array.isArray(member))
129
+ return dependency.outcome;
130
+ const result = member[key];
131
+ if (!result || typeof result !== 'object' || Array.isArray(result))
132
+ return dependency.outcome;
133
+ const record = result;
134
+ const status = record.status;
135
+ if (status !== 'succeeded' && status !== 'failed' && status !== 'cancelled')
136
+ return dependency.outcome;
137
+ return { status, resultRef: dependency.outcome.resultRef, ...(record.result === undefined ? {} : { result: record.result }), ...(record.error && typeof record.error === 'object' && !Array.isArray(record.error) ? { error: record.error } : {}), ...(typeof record.reason === 'string' ? { reason: record.reason } : {}), ...(Array.isArray(record.unresolvedEffectIds) ? { unresolvedEffectIds: record.unresolvedEffectIds.filter((value) => typeof value === 'string') } : {}) };
138
+ }
139
+ function zodJsonSchema(schema) {
140
+ const definition = schema?._def;
141
+ if (!definition)
142
+ return {};
143
+ const typeName = definition.typeName;
144
+ let result;
145
+ if (typeName === 'ZodObject') {
146
+ const shape = typeof definition.shape === 'function' ? definition.shape() : definition.shape ?? {};
147
+ const properties = {};
148
+ const required = [];
149
+ for (const [key, child] of Object.entries(shape)) {
150
+ properties[key] = zodJsonSchema(child);
151
+ if (!child.isOptional())
152
+ required.push(key);
153
+ }
154
+ result = { type: 'object', properties, ...(required.length ? { required } : {}) };
155
+ }
156
+ else if (typeName === 'ZodString')
157
+ result = { type: 'string' };
158
+ else if (typeName === 'ZodNumber')
159
+ result = { type: 'number' };
160
+ else if (typeName === 'ZodBoolean')
161
+ result = { type: 'boolean' };
162
+ else if (typeName === 'ZodNull')
163
+ result = { type: 'null' };
164
+ else if (typeName === 'ZodArray')
165
+ result = { type: 'array', items: zodJsonSchema(definition.type) };
166
+ else if (typeName === 'ZodOptional' || typeName === 'ZodDefault')
167
+ return zodJsonSchema(definition.innerType ?? definition.type);
168
+ else if (typeName === 'ZodNullable')
169
+ result = { anyOf: [zodJsonSchema(definition.innerType), { type: 'null' }] };
170
+ else if (typeName === 'ZodEnum')
171
+ result = { enum: [...(definition.values ?? [])] };
172
+ else if (typeName === 'ZodLiteral')
173
+ result = { const: definition.value ?? null };
174
+ else if (typeName === 'ZodUnion')
175
+ result = { anyOf: (definition.options ?? []).map(zodJsonSchema) };
176
+ else if (typeName === 'ZodEffects')
177
+ return zodJsonSchema(definition.schema);
178
+ else if (typeName === undefined)
179
+ result = {};
180
+ else
181
+ throw new Error(`UNSUPPORTED_OUTPUT_SCHEMA:${typeName}`);
182
+ return definition.description === undefined ? result : { ...result, description: definition.description };
183
+ }
184
+ function resultVisible(context, ref) { return context.lane.visibleResultRefs === undefined || context.lane.visibleResultRefs.has(ref); }
185
+ function findResult(context, ref) { return resultVisible(context, ref) ? context.state.results.get(ref)?.value : undefined; }
186
+ function collectResumeResultRefs(input, refs) {
187
+ if (!input)
188
+ return;
189
+ if (input.type === 'control_error') {
190
+ collectResumeResultRefs(input.original, refs);
191
+ return;
192
+ }
193
+ if (input.type !== 'wait')
194
+ return;
195
+ for (const dependency of Object.values(input.resolution.dependencies)) {
196
+ if (dependency.state !== 'settled' && dependency.state !== 'ignored')
197
+ continue;
198
+ if (dependency.outcome.resultRef)
199
+ refs.add(dependency.outcome.resultRef);
200
+ for (const ref of dependency.outcome.rejectedOutputRefs ?? [])
201
+ refs.add(ref);
202
+ }
203
+ }
204
+ function annotateAction(action, derivedFrom) {
205
+ if (!derivedFrom.length)
206
+ return action;
207
+ if (action.type === 'complete' || action.type === 'fail')
208
+ return { ...action, derivedFrom: [...new Set([...derivedFrom, ...(action.derivedFrom ?? [])])] };
209
+ if (action.type === 'submit_effects')
210
+ return { ...action, effects: action.effects.map((effect) => ({ ...effect, derivedFrom: [...new Set([...derivedFrom, ...(effect.derivedFrom ?? [])])] })) };
211
+ return action;
212
+ }
213
+ const resultReaders = new WeakMap();
214
+ function readResult(ctx, ref) { return resultReaders.get(ctx)?.(ref); }
215
+ function waitFailure(ctx) {
216
+ const resolution = ctx.resumeInput?.type === 'wait' ? ctx.resumeInput.resolution : undefined;
217
+ if (!resolution)
218
+ return undefined;
219
+ let dependencyError;
220
+ for (const dependency of Object.values(resolution.dependencies)) {
221
+ if (dependency.state !== 'pending' && dependency.outcome.status === 'failed' && dependency.outcome.error && !(dependency.outcome.rejectedOutputRefs?.length)) {
222
+ dependencyError = dependency.outcome.error;
223
+ break;
224
+ }
225
+ }
226
+ if (dependencyError)
227
+ return dependencyError;
228
+ return resolution.status === 'unsatisfied' ? resolution.error ?? { code: 'WAIT_UNSATISFIED', message: 'Wait did not reach its required outcome.' } : undefined;
229
+ }
230
+ function sdkLocals(locals) {
231
+ if (!locals || typeof locals !== 'object' || Array.isArray(locals))
232
+ return {};
233
+ const value = locals.$sdk;
234
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
235
+ }
236
+ function ordinaryLocals(locals) {
237
+ return locals && typeof locals === 'object' && !Array.isArray(locals) ? locals : {};
238
+ }
239
+ function makeContext(context, initialState) {
240
+ const draft = clone(initialState);
241
+ const readonlyState = deepFreeze(clone(initialState));
242
+ const draftProxy = draft && typeof draft === 'object' ? createDraftProxy(draft) : undefined;
243
+ let delta;
244
+ const actions = [];
245
+ const derivedRefs = new Set();
246
+ let adoptImmediately = false;
247
+ const agent = context.state.agents.get(context.lane.agentId);
248
+ const globalVersion = context.lane.contextSnapshotVersion;
249
+ const globalDraft = clone(agent?.globalVersions.get(globalVersion) ?? {});
250
+ const global = deepFreeze(clone(globalDraft));
251
+ const globalDraftProxy = globalDraft && typeof globalDraft === 'object' && !Array.isArray(globalDraft) ? createDraftProxy(globalDraft) : undefined;
252
+ if (agent)
253
+ derivedRefs.add(globalContextRef(agent.id, globalVersion));
254
+ derivedRefs.add(laneContextRef(context.lane.id, context.lane.context.version));
255
+ collectResumeResultRefs(context.resumeInput, derivedRefs);
256
+ for (const record of context.lane.context.history)
257
+ for (const ref of record.resultRefs)
258
+ derivedRefs.add(ref);
259
+ const history = context.lane.context.history.map((record) => ({
260
+ seq: record.seq,
261
+ hash: contentHash({ seq: record.seq, ...(record.effectId === undefined ? {} : { effectId: record.effectId }), instruction: record.instruction, resultRefs: record.resultRefs, ...(record.resultSelection === undefined ? {} : { resultSelection: record.resultSelection }), ...(record.result === undefined ? {} : { result: record.result }), ...(record.findings === undefined ? {} : { findings: record.findings }), output: record.output, privacy: record.privacy, ...(record.privacyTaints === undefined ? {} : { privacyTaints: record.privacyTaints }) }),
262
+ ...(record.effectId === undefined ? {} : { effectId: record.effectId }),
263
+ resultRefs: [...record.resultRefs],
264
+ ...(record.resultSelection === undefined ? {} : { resultSelection: clone(record.resultSelection) }),
265
+ ...(record.result === undefined ? {} : { result: record.result }),
266
+ ...(record.findings === undefined ? {} : { findings: [...record.findings] }),
267
+ privacy: record.privacy,
268
+ ...(record.privacyTaints === undefined ? {} : { privacyTaints: clone(record.privacyTaints) }),
269
+ }));
270
+ const resultMeta = (ref) => {
271
+ const result = resultVisible(context, ref) ? context.state.results.get(ref) : undefined;
272
+ if (!result)
273
+ return undefined;
274
+ const value = result.value ?? null;
275
+ return {
276
+ ref,
277
+ privacy: result.privacy,
278
+ derivedFrom: [...result.derivedFrom],
279
+ sizeBytes: result.sizeBytes ?? Buffer.byteLength(stableSerialize(value), 'utf8'),
280
+ hash: result.contentHash ?? contentHash(value),
281
+ producer: result.producer ?? (result.effectId === undefined ? { kind: 'lane', id: context.lane.id } : { kind: 'effect', id: result.effectId }),
282
+ ...(result.summary === undefined ? {} : { summary: clone(result.summary) }),
283
+ };
284
+ };
285
+ const globalDelta = (value) => {
286
+ const ops = typeof value.ops === 'function' ? (() => { if (!globalDraftProxy)
287
+ throw Object.assign(new Error('GLOBAL_DRAFT_REQUIRES_OBJECT'), { code: 'GLOBAL_DRAFT_REQUIRES_OBJECT', retryable: false }); value.ops(globalDraftProxy.draft); return globalDraftProxy.changes().ops; })() : value.ops;
288
+ delta = { target: 'global', baseVersion: agent?.latestGlobalVersion ?? 0, sourceLaneId: context.lane.id, ops: clone(ops), ...(value.privacy === undefined ? {} : { privacy: value.privacy }), proposal: value.proposal };
289
+ };
290
+ const ctx = {
291
+ lane: context.lane, goal: context.lane.goal, global, globalVersion, laneState: readonlyState, history, now: context.now, ...(context.lane.progressWatchdog === undefined ? {} : { watchdog: context.lane.progressWatchdog }), ...(context.resumeInput ? { resumeInput: context.resumeInput } : {}),
292
+ results: { meta: resultMeta, summary: (ref) => { if (context.state.results.has(ref) && resultVisible(context, ref))
293
+ derivedRefs.add(ref); return resultMeta(ref)?.summary; } },
294
+ mergeProposals: [...context.state.mergeProposals.values()].filter((proposal) => proposal.agentId === context.lane.agentId).map((proposal) => { for (const ref of proposal.delta.derivedFrom ?? [])
295
+ derivedRefs.add(ref); return clone(proposal); }),
296
+ mutateLane: (mutator) => { mutator((draftProxy?.draft ?? draft)); const changes = draftProxy?.changes(); delta = { target: 'lane', baseVersion: context.lane.context.version, ops: changes?.ops.map((op) => op.op === 'set' ? { op: 'set', path: op.path, value: asJson(op.value) } : op.op === 'append' ? { op: 'append', path: op.path, value: asJson(op.value) } : { op: 'remove', path: op.path }) ?? [] }; },
297
+ proposeGlobal: (value) => globalDelta({ ...value, proposal: true }),
298
+ commitGlobal: (value) => { globalDelta({ ...value, proposal: false }); adoptImmediately = value.adoptImmediately ?? false; },
299
+ adoptContext: (version) => actions.push({ type: 'adopt_context', version }),
300
+ cancelLane: (laneId, reason) => actions.push({ type: 'cancel_lane', laneId, reason }),
301
+ proposeCancel: (laneId, reason) => actions.push({ type: 'propose_cancel', laneId, reason }),
302
+ trace: (message) => { context.observe?.({ type: 'trace', data: typeof message === 'string' ? message : asJson(message) }); },
303
+ };
304
+ resultReaders.set(ctx, (ref) => { if (context.state.results.has(ref) && resultVisible(context, ref))
305
+ derivedRefs.add(ref); return findResult(context, ref); });
306
+ return { ctx, getDelta: () => delta, getActions: () => actions, getDerivedRefs: () => [...derivedRefs], getAdoptImmediately: () => adoptImmediately };
307
+ }
308
+ export class StepBuilder {
309
+ config;
310
+ handlers = new Map();
311
+ /**
312
+ * History compaction is a macro boundary concern. Internal handlers such as
313
+ * `:submit`, `:decode`, `:join`, and `:resume` must consume the pending
314
+ * Wait input before the next compaction check can run.
315
+ */
316
+ compactionBoundaries = new Set();
317
+ boundaryHandler;
318
+ constructor(config) {
319
+ this.config = config;
320
+ }
321
+ addStep(name, handler) { this.handlers.set(name, handler); this.compactionBoundaries.add(name); return this; }
322
+ onErrorBoundary(handler) { this.boundaryHandler = handler; return this; }
323
+ addStructuredLLMStep(name, options) {
324
+ this.compactionBoundaries.add(name);
325
+ const submit = `${name}:submit`;
326
+ const decode = `${name}:decode`;
327
+ const maxCorrectionRounds = options.selfCorrect?.maxRounds ?? 1;
328
+ const correctionRoundKey = `${name}CorrectRound`;
329
+ const inputKey = `${name}Inputs`;
330
+ const readSdk = (ctx) => sdkLocals(ctx.lane.resume.locals);
331
+ const writeSdk = (ctx, patch) => { const locals = ctx.lane.resume.locals; const base = locals && typeof locals === 'object' && !Array.isArray(locals) ? locals : {}; return { ...base, $sdk: { ...readSdk(ctx), ...patch } }; };
332
+ const fail = (runtimeError, ctx) => options.onError ? { next: options.onError(runtimeError, ctx) } : (() => { throw Object.assign(new Error(runtimeError.message), runtimeError); })();
333
+ const retryPolicy = options.retryPolicy;
334
+ const requirements = { ...(options.requirements ?? {}) };
335
+ this.handlers.set(name, (ctx) => {
336
+ const instruction = boundedInstruction(typeof options.instruction === 'string' ? options.instruction : options.instruction({ goal: ctx.goal, state: scalarProjection(ctx.laneState) }));
337
+ const inputs = options.inputs?.(ctx) ?? {};
338
+ const inputResultRefs = [...new Set([...(inputs.results ?? []), ...(inputs.findings ?? [])])];
339
+ const inputArtifactRefs = [...new Set(inputs.artifacts ?? [])];
340
+ const outputSchema = zodJsonSchema(options.schema);
341
+ const derivedFrom = [...inputResultRefs, ...inputArtifactRefs.map((ref) => ({ kind: 'artifact', ref }))];
342
+ return { actions: [{ type: 'submit_effects', effects: [{ key: `${name}-llm`, kind: 'llm', concurrencyClass: 'llm', input: programLLMInput(this.config, { task: options.task, instruction, inputs: asJson(inputs), outputSchema, requirements: { ...requirements, structuredOutput: { schema: outputSchema } }, ...(options.executionPolicy === undefined ? {} : { executionPolicy: options.executionPolicy }) }), ...(retryPolicy === undefined ? {} : { retryPolicy }), ...(derivedFrom.length ? { derivedFrom } : {}) }], wait: { onUnsatisfied: 'resume_with_error' } }], next: decode, locals: writeSdk(ctx, { [correctionRoundKey]: 0, [inputKey]: asJson(inputs) }) };
343
+ });
344
+ this.handlers.set(submit, (ctx) => ({ next: decode }));
345
+ this.handlers.set(decode, (ctx) => {
346
+ const dependencyError = waitFailure(ctx);
347
+ if (dependencyError)
348
+ return fail(dependencyError, ctx);
349
+ const input = ctx.resumeInput?.type === 'wait' ? Object.values(ctx.resumeInput.resolution.dependencies).find((dependency) => dependency.state !== 'pending') : undefined;
350
+ const ref = input?.state === 'settled' ? input.outcome.resultRef : undefined;
351
+ const rejectedRef = input?.state === 'settled' ? input.outcome.rejectedOutputRefs?.[0] : undefined;
352
+ const value = rejectedRef ? readResult(ctx, rejectedRef) : ref ? readResult(ctx, ref) : undefined;
353
+ const parsed = options.schema.safeParse(value);
354
+ if (!parsed.success) {
355
+ const sdk = readSdk(ctx);
356
+ const currentRound = typeof sdk[correctionRoundKey] === 'number' && Number.isInteger(sdk[correctionRoundKey]) ? sdk[correctionRoundKey] : 0;
357
+ if (currentRound >= maxCorrectionRounds)
358
+ return fail({ code: 'OUTPUT_SCHEMA_VIOLATION', message: 'Structured LLM output did not match the declared schema.', retryable: false, details: parsed.error.message }, ctx);
359
+ const originalInputs = sdk[inputKey] && typeof sdk[inputKey] === 'object' && !Array.isArray(sdk[inputKey]) ? sdk[inputKey] : {};
360
+ const inputResultRefs = [...new Set([...(Array.isArray(originalInputs.results) ? originalInputs.results.filter((item) => typeof item === 'string') : []), ...(Array.isArray(originalInputs.findings) ? originalInputs.findings.filter((item) => typeof item === 'string') : []), ...(rejectedRef ? [rejectedRef] : [])])];
361
+ const inputArtifactRefs = [...new Set(Array.isArray(originalInputs.artifacts) ? originalInputs.artifacts.filter((item) => typeof item === 'string') : [])];
362
+ const correctionDerivedFrom = [...inputResultRefs, ...inputArtifactRefs.map((ref) => ({ kind: 'artifact', ref }))];
363
+ const outputSchema = zodJsonSchema(options.schema);
364
+ const correctionInstruction = boundedInstruction(`${typeof options.instruction === 'string' ? options.instruction : 'structured'}\nValidation errors: ${parsed.error.message}`);
365
+ return { actions: [{ type: 'submit_effects', effects: [{ key: `${name}-correct-${currentRound + 1}`, kind: 'llm', concurrencyClass: 'llm', input: programLLMInput(this.config, { task: options.task, instruction: correctionInstruction, inputs: { ...originalInputs, rejectedOutputRefs: inputResultRefs }, outputSchema, requirements: { ...requirements, structuredOutput: { schema: outputSchema } }, ...(options.executionPolicy === undefined ? {} : { executionPolicy: options.executionPolicy }) }), ...(retryPolicy === undefined ? {} : { retryPolicy }), ...(correctionDerivedFrom.length ? { derivedFrom: correctionDerivedFrom } : {}) }], wait: { onUnsatisfied: 'resume_with_error' } }], next: decode, locals: writeSdk(ctx, { [correctionRoundKey]: currentRound + 1, [inputKey]: { ...originalInputs, rejectedOutputRefs: inputResultRefs } }) };
366
+ }
367
+ const next = options.onSuccess(parsed.data, ctx);
368
+ return { next };
369
+ });
370
+ return this;
371
+ }
372
+ addReActLoopStep(name, options) {
373
+ this.compactionBoundaries.add(name);
374
+ const readTurns = (ctx) => { const sdk = sdkLocals(ctx.lane.resume.locals); const turn = sdk[`${name}Turns`]; return typeof turn === 'number' && Number.isInteger(turn) && turn >= 0 ? turn : 0; };
375
+ const inputKey = `${name}Inputs`;
376
+ const readInputs = (ctx) => { const value = sdkLocals(ctx.lane.resume.locals)[inputKey]; return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; };
377
+ const writeTurns = (ctx, turns, inputs = readInputs(ctx)) => { const locals = ctx.lane.resume.locals; const base = locals && typeof locals === 'object' && !Array.isArray(locals) ? locals : {}; return { ...base, $sdk: { ...sdkLocals(locals), [`${name}Turns`]: turns, [inputKey]: asJson(inputs) } }; };
378
+ const resultRefFromWait = (ctx) => { const dependency = ctx.resumeInput?.type === 'wait' ? Object.values(ctx.resumeInput.resolution.dependencies)[0] : undefined; return dependency?.state === 'settled' ? dependency.outcome.resultRef : undefined; };
379
+ const resultRefsFromWait = (ctx) => ctx.resumeInput?.type === 'wait' ? Object.values(ctx.resumeInput.resolution.dependencies).flatMap((dependency) => dependency.state === 'settled' && dependency.outcome.resultRef ? [dependency.outcome.resultRef] : []) : [];
380
+ const instruction = (ctx) => boundedInstruction(typeof options.instruction === 'string' ? options.instruction : options.instruction({ goal: ctx.goal, state: scalarProjection(ctx.laneState) }));
381
+ const submitModel = (ctx, turn, inputs = {}) => { const resultRefs = [...new Set(inputs.results ?? [])]; const findingRefs = [...new Set(inputs.findings ?? [])]; const artifactRefs = [...new Set(inputs.artifacts ?? [])]; const dataRefs = [...resultRefs, ...findingRefs, ...artifactRefs.map((ref) => ({ kind: 'artifact', ref }))]; const requirements = { ...(options.requirements ?? {}), ...(options.toolAllow === undefined ? {} : { toolCalling: true }) }; return { actions: [{ type: 'submit_effects', effects: [{ key: `${name}-turn-${turn}`, kind: 'llm', concurrencyClass: 'llm', input: programLLMInput(this.config, { task: options.task ?? 'reason', instruction: instruction(ctx), inputs: { ...(resultRefs.length ? { results: resultRefs } : {}), ...(findingRefs.length ? { findings: findingRefs } : {}), ...(artifactRefs.length ? { artifacts: artifactRefs } : {}), ...(inputs.events?.length ? { events: [...new Set(inputs.events)] } : {}) }, turn, ...(options.outputSchema === undefined ? {} : { outputSchema: zodJsonSchema(options.outputSchema) }), ...(Object.keys(requirements).length ? { requirements } : {}) }), ...(dataRefs.length ? { derivedFrom: dataRefs } : {}) }], wait: { onUnsatisfied: 'resume_with_error' } }], next: { programId: this.config.id, programVersion: this.config.version, step: `${name}:decode`, locals: writeTurns(ctx, turn, inputs) }, locals: writeTurns(ctx, turn, inputs) }; };
382
+ this.handlers.set(name, (ctx) => { const turn = readTurns(ctx) + 1; const inputs = options.inputs?.(ctx) ?? {}; const output = submitModel(ctx, turn, inputs); return { ...output, next: `${name}:decode` }; });
383
+ this.handlers.set(`${name}:tools`, (ctx) => { const turn = readTurns(ctx); const previous = readInputs(ctx); const inputs = { ...previous, results: [...new Set([...(previous.results ?? []), ...resultRefsFromWait(ctx)])] }; return { ...submitModel(ctx, turn + 1, inputs), next: `${name}:decode` }; });
384
+ this.handlers.set(`${name}:decode`, (ctx) => {
385
+ const turns = readTurns(ctx);
386
+ const ref = resultRefFromWait(ctx);
387
+ const value = ref ? readResult(ctx, ref) ?? null : null;
388
+ const record = value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
389
+ const finishReason = record?.finishReason;
390
+ const toolCalls = Array.isArray(record?.toolCalls) ? record.toolCalls : [];
391
+ const fail = (runtimeError) => options.onError ? { next: options.onError(runtimeError, ctx) } : (() => { const error = Object.assign(new Error(runtimeError.message), runtimeError); throw error; })();
392
+ const dependencyError = waitFailure(ctx);
393
+ if (dependencyError)
394
+ return fail(dependencyError);
395
+ const maxTurns = Math.max(1, Math.floor(options.maxTurns ?? 10));
396
+ const maxTurnsReached = () => options.onMaxTurns ? { next: options.onMaxTurns(ctx) } : fail({ code: 'MAX_TURNS_REACHED', message: `ReAct loop ${name} reached its maximum of ${maxTurns} turns.`, retryable: false });
397
+ if (finishReason === 'tool_calls') {
398
+ if (turns >= maxTurns || toolCalls.length === 0)
399
+ return maxTurnsReached();
400
+ const invalidTool = toolCalls.find((call) => { const item = call && typeof call === 'object' && !Array.isArray(call) ? call : {}; const toolName = typeof item.name === 'string' ? item.name : ''; return !toolName || (options.toolAllow !== undefined && !options.toolAllow.includes(toolName)); });
401
+ if (invalidTool !== undefined)
402
+ return fail({ code: 'ACTION_TOOL_NOT_ALLOWED', message: 'Model requested a tool outside the ReAct allow-list.', retryable: false });
403
+ const sourceEffectId = ctx.resumeInput?.type === 'wait' ? Object.values(ctx.resumeInput.resolution.dependencies).find((dependency) => dependency.state === 'settled' && dependency.target.kind === 'effect')?.target.id : undefined;
404
+ const sourcePrivacy = ref === undefined ? undefined : ctx.results.meta(ref)?.privacy;
405
+ const toolDerivedFrom = ref === undefined ? [] : [ref];
406
+ const calls = toolCalls.map((call, index) => {
407
+ const item = call && typeof call === 'object' && !Array.isArray(call) ? call : {};
408
+ const originalId = typeof item.toolCallId === 'string' ? item.toolCallId : `call-${index + 1}`;
409
+ const toolName = typeof item.name === 'string' ? item.name : '';
410
+ return { originalId, toolName, toolCallId: `${name}:${turns}:${originalId}`, input: item.input ?? {} };
411
+ });
412
+ const makeToolEffects = (approvedCalls) => ({ type: 'submit_effects', effects: approvedCalls.map((call, index) => ({ key: `${name}-tool-${turns}-${index + 1}`, toolCallId: String(call.toolCallId ?? `${name}:${turns}:${String(call.originalId)}`), ...(sourceEffectId === undefined ? {} : { llmEffectId: sourceEffectId }), ...(sourcePrivacy === undefined ? {} : { privacy: sourcePrivacy }), ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}), kind: 'tool', concurrencyClass: 'tool', input: { toolCallId: String(call.toolCallId ?? `${name}:${turns}:${String(call.originalId)}`), name: String(call.toolName), arguments: call.input, ...(sourcePrivacy === undefined ? {} : { privacy: sourcePrivacy }), ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}) } })), wait: { onUnsatisfied: 'resume_with_error' } });
413
+ if (options.toolApproval) {
414
+ const approvalKey = `${name}PendingToolCalls`;
415
+ const digestKey = `${name}PendingToolDigest`;
416
+ const locals = ctx.lane.resume.locals && typeof ctx.lane.resume.locals === 'object' && !Array.isArray(ctx.lane.resume.locals) ? ctx.lane.resume.locals : {};
417
+ const digest = contentHash(calls);
418
+ const listing = calls.map((call, index) => `${index + 1}. ${call.toolName} ${call.toolCallId}`).join('\n');
419
+ const extra = typeof options.toolApproval.prompt === 'string' ? options.toolApproval.prompt : options.toolApproval.prompt(calls.map((call) => ({ name: call.toolName, toolCallId: call.originalId })), ctx);
420
+ const prompt = boundedInstruction(`Approve ${calls.length} tool call(s). Digest ${digest}.\n${listing}\n${extra}`);
421
+ this.handlers.set(`${name}:approval`, (approvalCtx) => {
422
+ const dependency = approvalCtx.resumeInput?.type === 'wait' ? Object.values(approvalCtx.resumeInput.resolution.dependencies)[0] : undefined;
423
+ const approvalError = waitFailure(approvalCtx);
424
+ if (approvalError)
425
+ return fail(approvalError);
426
+ if (dependency?.state !== 'settled')
427
+ return fail({ code: 'APPROVAL_RESPONSE_MISSING', message: 'Approval response was not received.', retryable: false });
428
+ const value = dependency.outcome.resultRef ? readResult(approvalCtx, dependency.outcome.resultRef) : undefined;
429
+ const parsed = z.object({ approved: z.boolean(), reason: z.string().optional() }).safeParse(value);
430
+ if (!parsed.success)
431
+ return fail({ code: 'APPROVAL_RESPONSE_INVALID', message: 'Approval response must contain approved=true or false.', retryable: false, details: parsed.error.message });
432
+ if (!parsed.data.approved) {
433
+ const reason = parsed.data.reason ?? 'User denied the proposed tool call.';
434
+ if (options.toolApproval?.onDenied)
435
+ return { next: options.toolApproval.onDenied(reason, approvalCtx) };
436
+ return fail({ code: 'APPROVAL_DENIED', message: reason, retryable: false });
437
+ }
438
+ const pendingLocals = sdkLocals(approvalCtx.lane.resume.locals);
439
+ const pending = pendingLocals[approvalKey];
440
+ if (!Array.isArray(pending))
441
+ return fail({ code: 'APPROVAL_CALLS_MISSING', message: 'Approved tool calls were not found in the persisted lane state.', retryable: false });
442
+ if (pendingLocals[digestKey] !== contentHash(pending))
443
+ return fail({ code: 'APPROVAL_DIGEST_MISMATCH', message: 'Persisted tool calls no longer match the approved digest.', retryable: false });
444
+ const approvedCalls = pending.flatMap((item) => item && typeof item === 'object' && !Array.isArray(item) ? [{ originalId: item.originalId ?? '', toolName: item.toolName ?? '', toolCallId: item.toolCallId ?? '', input: item.input ?? {} }] : []);
445
+ const cleared = { ...locals, $sdk: { ...pendingLocals, [approvalKey]: null, [digestKey]: null } };
446
+ return { actions: [makeToolEffects(approvedCalls)], next: `${name}:tools`, locals: cleared };
447
+ });
448
+ return { actions: [{ type: 'submit_effects', effects: [{ key: `${name}-approval-${turns}`, kind: 'human', concurrencyClass: 'none', input: { prompt, digest, tools: calls }, ...(toolDerivedFrom.length ? { derivedFrom: [...toolDerivedFrom] } : {}) }], wait: { onUnsatisfied: 'resume_with_error' } }], next: `${name}:approval`, locals: { ...locals, $sdk: { ...sdkLocals(locals), [approvalKey]: calls, [digestKey]: digest } } };
449
+ }
450
+ return { actions: [makeToolEffects(calls)], next: `${name}:tools` };
451
+ }
452
+ if (turns >= maxTurns)
453
+ return maxTurnsReached();
454
+ if (options.outputSchema) {
455
+ const outputValue = record?.structured === undefined ? value : record.structured;
456
+ const parsed = options.outputSchema.safeParse(outputValue);
457
+ if (!parsed.success)
458
+ return fail({ code: 'OUTPUT_SCHEMA_VIOLATION', message: 'ReAct result did not match outputSchema.', retryable: false, details: parsed.error.message });
459
+ }
460
+ if (!ref)
461
+ return fail({ code: 'MISSING_RESULT_REF', message: 'ReAct result did not produce a ResultRef.', retryable: false });
462
+ if (typeof options.onFinish === 'function')
463
+ return { next: options.onFinish(ref, ctx) };
464
+ if (options.onFinish.structured) {
465
+ const structuredValue = record?.structured ?? value;
466
+ const parsed = options.onFinish.structured.schema.safeParse(structuredValue);
467
+ if (!parsed.success)
468
+ return fail({ code: 'OUTPUT_SCHEMA_VIOLATION', message: 'ReAct structured result did not match schema.', retryable: false, details: parsed.error.message });
469
+ return { next: options.onFinish.structured.onParsed(parsed.data, ctx) };
470
+ }
471
+ return { next: options.onFinish.text(ref, ctx) };
472
+ });
473
+ return this;
474
+ }
475
+ addParallelStep(name, options) {
476
+ this.compactionBoundaries.add(name);
477
+ const joinStep = `${name}:join`;
478
+ this.handlers.set(name, (ctx) => {
479
+ const join = joinOptions(options.join, options);
480
+ const rawLanes = Object.entries(options.lanes).map(([key, lane]) => { const dependsOn = normalizeDependsOn(lane.dependsOn); return { key, goal: lane.goal, program: { programId: lane.program.programId, programVersion: lane.program.programVersion, step: lane.program.step ?? 'start', locals: lane.program.locals ?? {} }, ...(lane.priority === undefined ? {} : { priority: lane.priority }), ...(lane.contextVersion === undefined ? {} : { contextVersion: lane.contextVersion }), ...(lane.affinityKey === undefined ? {} : { affinityKey: lane.affinityKey }), ...(lane.resources === undefined ? {} : { resources: lane.resources }), ...(lane.inputResultRefs === undefined ? {} : { inputResultRefs: lane.inputResultRefs }), ...(dependsOn === undefined ? {} : { dependsOn }) }; });
481
+ const collapsed = collapseAffinityLanes(name, rawLanes, affinityAdvice(ctx) ?? [], options.affinity !== 'ack', join.mode, join.condition);
482
+ const action = { type: 'fork', affinityAck: options.affinity === 'ack' || affinityAdvice(ctx) !== undefined, lanes: collapsed.lanes, ...(collapsed.aliases === undefined ? {} : { joinAliases: collapsed.aliases }), join };
483
+ return { actions: [action], next: options.onJoin ? joinStep : options.next ?? joinStep };
484
+ });
485
+ if (options.onJoin)
486
+ this.handlers.set(joinStep, (ctx) => { const outcomes = {}; if (ctx.resumeInput?.type === 'wait')
487
+ for (const [key, dependency] of Object.entries(ctx.resumeInput.resolution.dependencies))
488
+ if (dependency.state !== 'pending')
489
+ outcomes[key] = joinedOutcome(ctx, dependency, key); return { next: options.onJoin(outcomes, ctx) }; });
490
+ return this;
491
+ }
492
+ addDynamicForkStep(name, options) {
493
+ this.compactionBoundaries.add(name);
494
+ const joinStep = `${name}:join`;
495
+ this.handlers.set(name, (ctx) => {
496
+ const join = joinOptions(options.join, options);
497
+ const proposal = options.proposal?.(ctx) ?? { lanes: options.lanes?.(ctx) ?? {} };
498
+ const rawLanes = Object.entries(proposal.lanes).map(([key, lane]) => { const dependsOn = normalizeDependsOn(lane.dependsOn); return { key, goal: lane.goal, program: { programId: lane.program.programId, programVersion: lane.program.programVersion, step: lane.program.step ?? 'start', locals: lane.program.locals ?? {} }, ...(lane.priority === undefined ? {} : { priority: lane.priority }), ...(lane.contextVersion === undefined ? {} : { contextVersion: lane.contextVersion }), ...(lane.affinityKey === undefined ? {} : { affinityKey: lane.affinityKey }), ...(lane.resources === undefined ? {} : { resources: lane.resources }), ...(lane.inputResultRefs === undefined ? {} : { inputResultRefs: lane.inputResultRefs }), ...(dependsOn === undefined ? {} : { dependsOn }) }; });
499
+ const advice = affinityAdvice(ctx) ?? [];
500
+ const affinity = typeof options.affinity === 'function' ? options.affinity(advice, ctx) : options.affinity;
501
+ const collapsed = collapseAffinityLanes(name, rawLanes, advice, affinity !== 'ack', join.mode, join.condition);
502
+ const action = { type: 'fork', affinityAck: affinity === 'ack' || advice.length > 0, lanes: collapsed.lanes, ...(collapsed.aliases === undefined ? {} : { joinAliases: collapsed.aliases }), join };
503
+ return { actions: [action], next: options.onJoin ? joinStep : options.next ?? joinStep };
504
+ });
505
+ if (options.onJoin)
506
+ this.handlers.set(joinStep, (ctx) => { const outcomes = new Map(); if (ctx.resumeInput?.type === 'wait')
507
+ for (const [key, dependency] of Object.entries(ctx.resumeInput.resolution.dependencies))
508
+ if (dependency.state !== 'pending')
509
+ outcomes.set(key, joinedOutcome(ctx, dependency, key)); return { next: options.onJoin(outcomes, ctx) }; });
510
+ return this;
511
+ }
512
+ addMergeStep(name, options) {
513
+ this.compactionBoundaries.add(name);
514
+ this.handlers.set(name, (ctx) => {
515
+ const dependencies = ctx.resumeInput?.type === 'wait' ? Object.values(ctx.resumeInput.resolution.dependencies) : [];
516
+ const joinedLaneIds = new Set(dependencies.filter((dependency) => dependency.target.kind === 'lane').map((dependency) => dependency.target.id));
517
+ const outcomeSource = options.sources?.outcomes;
518
+ const joined = dependencies.flatMap((dependency) => dependency.state === 'settled' && dependency.outcome.resultRef && (outcomeSource === undefined || outcomeSource === 'joined' || outcomeSource.includes(dependency.target.id)) ? [dependency.outcome.resultRef] : []);
519
+ const proposalSource = options.sources?.proposals;
520
+ const proposals = ctx.mergeProposals.filter((proposal) => proposalSource === undefined || (proposalSource === 'joined' ? joinedLaneIds.has(proposal.sourceLaneId) : proposalSource.includes(proposal.sourceLaneId)));
521
+ const instruction = options.instruction === undefined ? undefined : boundedInstruction(typeof options.instruction === 'string' ? options.instruction : options.instruction(ctx));
522
+ return { actions: [{ type: 'submit_effects', effects: [{ key: `${name}-llm`, kind: 'llm', concurrencyClass: 'llm', input: programLLMInput(this.config, { task: options.task ?? 'reason', merge: true, sources: asJson(joined), proposals: asJson(proposals.map((proposal) => ({ id: proposal.id, sourceLaneId: proposal.sourceLaneId, delta: proposal.delta }))), ...(instruction === undefined ? {} : { instruction }) }), ...(joined.length ? { derivedFrom: joined } : {}) }], wait: { onUnsatisfied: 'resume_with_error' } }], next: `${name}:decode` };
523
+ });
524
+ this.handlers.set(`${name}:decode`, (ctx) => {
525
+ const dependency = ctx.resumeInput?.type === 'wait' ? Object.values(ctx.resumeInput.resolution.dependencies)[0] : undefined;
526
+ const ref = dependency?.state === 'settled' ? dependency.outcome.resultRef : undefined;
527
+ const value = ref ? readResult(ctx, ref) : undefined;
528
+ const fail = (runtimeError) => options.onError ? { next: options.onError(runtimeError, ctx) } : (() => { throw Object.assign(new Error(runtimeError.message), runtimeError); })();
529
+ const dependencyError = waitFailure(ctx);
530
+ if (dependencyError)
531
+ return fail(dependencyError);
532
+ if (options.schema) {
533
+ const parsed = options.schema.safeParse(value);
534
+ if (!parsed.success)
535
+ return fail({ code: 'OUTPUT_SCHEMA_VIOLATION', message: 'Merge result did not match schema.', retryable: false, details: parsed.error.message });
536
+ const next = options.onSynthesized ? options.onSynthesized(parsed.data, ctx) : options.next;
537
+ if (!next)
538
+ return fail({ code: 'MERGE_TARGET_MISSING', message: 'Merge step requires onSynthesized or next.', retryable: false });
539
+ return { next };
540
+ }
541
+ const next = options.onSynthesized ? options.onSynthesized(value, ctx) : options.next;
542
+ if (!next)
543
+ return fail({ code: 'MERGE_TARGET_MISSING', message: 'Merge step requires onSynthesized or next.', retryable: false });
544
+ return { next };
545
+ });
546
+ return this;
547
+ }
548
+ addWaitStep(name, spec) {
549
+ this.compactionBoundaries.add(name);
550
+ if ('dependencies' in spec) {
551
+ this.handlers.set(name, () => ({ actions: [{ type: 'wait', spec: { ...spec, mode: spec.mode ?? 'all', ...(spec.quorum === undefined ? {} : { quorum: spec.quorum }), ...(spec.deadlineAt === undefined ? {} : { deadlineAt: spec.deadlineAt }), onUnsatisfied: 'resume_with_error', reason: 'dependency' } }], next: spec.next }));
552
+ return this;
553
+ }
554
+ const resume = `${name}:resume`;
555
+ this.handlers.set(name, (ctx) => ({ actions: [{ type: 'wait', spec: { dependencies: spec.targets(ctx), mode: spec.mode ?? 'all', ...(spec.quorum === undefined ? {} : { quorum: spec.quorum }), ...(spec.timeoutMs === undefined ? {} : { deadlineAt: ctx.now + Math.max(0, spec.timeoutMs) }), onUnsatisfied: 'resume_with_error', reason: 'dependency' } }], next: resume }));
556
+ this.handlers.set(resume, (ctx) => {
557
+ const resolution = ctx.resumeInput?.type === 'wait' ? ctx.resumeInput.resolution : undefined;
558
+ if (!resolution)
559
+ throw Object.assign(new Error('WAIT_RESOLUTION_MISSING'), { code: 'WAIT_RESOLUTION_MISSING', retryable: false });
560
+ if (resolution.status === 'satisfied')
561
+ return { next: spec.onResolved(resolution, ctx) };
562
+ if (spec.onUnsatisfied)
563
+ return { next: spec.onUnsatisfied(resolution, ctx) };
564
+ throw Object.assign(new Error(resolution.error?.message ?? 'WAIT_UNSATISFIED'), resolution.error ?? { code: 'WAIT_UNSATISFIED', retryable: false });
565
+ });
566
+ return this;
567
+ }
568
+ addHumanStep(name, options) {
569
+ this.compactionBoundaries.add(name);
570
+ const decode = `${name}:decode`;
571
+ this.handlers.set(name, (ctx) => { const prompt = boundedInstruction(typeof options.prompt === 'string' ? options.prompt : options.prompt({ goal: ctx.goal, state: scalarProjection(ctx.laneState) })); const inputs = options.inputs?.(ctx) ?? {}; const inputResultRefs = [...new Set([...(inputs.results ?? []), ...(inputs.findings ?? []), ...(inputs.artifacts ?? [])])]; return { actions: [{ type: 'submit_effects', effects: [{ key: `${name}-human`, kind: 'human', concurrencyClass: 'none', input: asJson({ prompt, inputs }), ...(inputResultRefs.length ? { derivedFrom: inputResultRefs } : {}), ...(options.timeoutMs === undefined ? {} : { attemptTimeoutMs: options.timeoutMs }) }], wait: { onUnsatisfied: 'resume_with_error' } }], next: decode }; });
572
+ this.handlers.set(decode, (ctx) => {
573
+ const dependency = ctx.resumeInput?.type === 'wait' ? Object.values(ctx.resumeInput.resolution.dependencies)[0] : undefined;
574
+ if (dependency?.state === 'settled') {
575
+ const runtimeError = dependency.outcome.error;
576
+ if (runtimeError) {
577
+ if ((runtimeError.code === 'ATTEMPT_TIMEOUT' || runtimeError.code === 'TIMEOUT') && options.onTimeout)
578
+ return { next: options.onTimeout(ctx) };
579
+ throw Object.assign(new Error(runtimeError.message), runtimeError);
580
+ }
581
+ const ref = dependency.outcome.resultRef;
582
+ const value = ref ? readResult(ctx, ref) : undefined;
583
+ const parsed = options.schema.safeParse(value);
584
+ if (parsed.success)
585
+ return { next: options.onReply(parsed.data, ctx) };
586
+ throw Object.assign(new Error('Human reply did not match schema.'), { code: 'HUMAN_RESPONSE_SCHEMA_VIOLATION', message: 'Human reply did not match schema.', retryable: false, details: parsed.error.message });
587
+ }
588
+ if (dependency?.state === 'pending')
589
+ throw Object.assign(new Error('Human response is still pending.'), { code: 'HUMAN_RESPONSE_PENDING', message: 'Human response is still pending.', retryable: false });
590
+ if (ctx.resumeInput?.type === 'wait')
591
+ throw Object.assign(new Error(ctx.resumeInput.resolution.error?.message ?? 'Human response was not received.'), ctx.resumeInput.resolution.error ?? { code: 'HUMAN_RESPONSE_UNSATISFIED', message: 'Human response was not received.', retryable: false });
592
+ throw Object.assign(new Error('Human response resolution is missing.'), { code: 'HUMAN_RESPONSE_MISSING', message: 'Human response resolution is missing.', retryable: false });
593
+ });
594
+ return this;
595
+ }
596
+ addTimerStep(name, options) {
597
+ this.compactionBoundaries.add(name);
598
+ const decode = `${name}:resume`;
599
+ this.handlers.set(name, (ctx) => ({ actions: [{ type: 'submit_effects', effects: [{ key: `${name}-timer`, kind: 'timer', concurrencyClass: 'none', input: { delayMs: typeof options.delayMs === 'number' ? options.delayMs : options.delayMs(ctx) } }], wait: { onUnsatisfied: 'resume_with_error' } }], next: decode }));
600
+ this.handlers.set(decode, (ctx) => {
601
+ const resolution = ctx.resumeInput?.type === 'wait' ? ctx.resumeInput.resolution : undefined;
602
+ const dependency = resolution ? Object.values(resolution.dependencies)[0] : undefined;
603
+ const runtimeError = dependency?.state === 'settled' ? dependency.outcome.error : undefined;
604
+ if (resolution?.status === 'satisfied' && runtimeError === undefined)
605
+ return { next: options.onFire(ctx) };
606
+ throw Object.assign(new Error(runtimeError?.message ?? resolution?.error?.message ?? 'Timer did not fire.'), runtimeError ?? resolution?.error ?? { code: 'TIMER_NOT_FIRED', message: 'Timer did not fire.', retryable: false });
607
+ });
608
+ return this;
609
+ }
610
+ build(entry = this.handlers.has('start') ? 'start' : [...this.handlers.keys()][0] ?? 'start') {
611
+ if (!this.handlers.has(entry))
612
+ this.handlers.set(entry, () => ({ actions: [{ type: 'complete', result: { ok: true } }], next: entry }));
613
+ const compaction = this.config.historyCompaction;
614
+ const compactSummarize = '$compact:summarize';
615
+ const compactApply = '$compact:apply';
616
+ if (compaction) {
617
+ const keepRecentRounds = Math.max(0, Math.floor(compaction.keepRecentRounds));
618
+ this.handlers.set(compactSummarize, (ctx) => {
619
+ const candidates = ctx.history.slice(0, Math.max(0, ctx.history.length - keepRecentRounds));
620
+ const upToSeq = candidates.at(-1)?.seq;
621
+ const locals = ordinaryLocals(ctx.lane.resume.locals);
622
+ const sdk = sdkLocals(ctx.lane.resume.locals);
623
+ const returnStep = typeof sdk.compactReturnStep === 'string' ? sdk.compactReturnStep : ctx.lane.resume.step;
624
+ if (upToSeq === undefined)
625
+ return { next: returnStep === compactSummarize ? entry : returnStep, locals: { ...locals, $sdk: sdk } };
626
+ return {
627
+ actions: [{ type: 'submit_effects', effects: [{ key: '$compact-summary', kind: 'llm', concurrencyClass: 'llm', input: programLLMInput(this.config, { task: compaction.summarizeTask, historySeqs: candidates.map((record) => record.seq), upToSeq }) }], wait: { onUnsatisfied: 'resume_with_error' } }],
628
+ next: compactApply,
629
+ locals: { ...locals, $sdk: { ...sdk, compactPending: true, compactReturnStep: returnStep, compactUpToSeq: upToSeq } },
630
+ };
631
+ });
632
+ this.handlers.set(compactApply, (ctx) => {
633
+ const sdk = sdkLocals(ctx.lane.resume.locals);
634
+ const resolution = ctx.resumeInput?.type === 'wait' ? ctx.resumeInput.resolution : undefined;
635
+ const dependency = resolution ? Object.values(resolution.dependencies).find((item) => item.state === 'settled') : undefined;
636
+ const runtimeError = dependency?.state === 'settled' ? dependency.outcome.error : undefined;
637
+ if (runtimeError || resolution?.status !== 'satisfied') {
638
+ const error = runtimeError ?? resolution?.error ?? { code: 'HISTORY_COMPACTION_FAILED', message: 'History compaction did not produce a summary.', retryable: false };
639
+ throw Object.assign(new Error(error.message), error);
640
+ }
641
+ const summaryRef = dependency?.state === 'settled' ? dependency.outcome.resultRef : undefined;
642
+ const returnStep = typeof sdk.compactReturnStep === 'string' ? sdk.compactReturnStep : entry;
643
+ const upToSeq = typeof sdk.compactUpToSeq === 'number' ? sdk.compactUpToSeq : undefined;
644
+ const base = ordinaryLocals(ctx.lane.resume.locals);
645
+ const nextSdk = { ...sdk };
646
+ delete nextSdk.compactPending;
647
+ delete nextSdk.compactReturnStep;
648
+ delete nextSdk.compactUpToSeq;
649
+ if (!summaryRef || upToSeq === undefined)
650
+ return { next: returnStep, locals: { ...base, $sdk: nextSdk } };
651
+ return { contextDelta: { target: 'lane', baseVersion: ctx.lane.context.version, ops: [{ op: 'compact_history', upToSeq, summaryRef }] }, next: returnStep, locals: { ...base, $sdk: nextSdk } };
652
+ });
653
+ }
654
+ const definition = {
655
+ id: this.config.id,
656
+ version: this.config.version,
657
+ entry,
658
+ steps: [...this.handlers.keys()],
659
+ debugSources: [...this.handlers.values()].map((handler) => handler.toString()),
660
+ ...(this.config.system === undefined ? {} : { system: this.config.system }),
661
+ ...(this.config.toolSet === undefined ? {} : { toolSet: this.config.toolSet }),
662
+ step: (context) => {
663
+ const sdk = sdkLocals(context.lane.resume.locals);
664
+ const pressure = context.lane.historyPressure;
665
+ const shouldCompact = compaction !== undefined && this.compactionBoundaries.has(context.lane.resume.step) && !context.lane.resume.step.startsWith('$compact:') && context.lane.activeWaitId === undefined && sdk.compactPending !== true && pressure !== undefined && pressure.historyTokens > pressure.softTokens && context.lane.context.history.length > Math.max(0, Math.floor(compaction.keepRecentRounds));
666
+ if (shouldCompact)
667
+ return { actions: [], next: { programId: this.config.id, programVersion: this.config.version, step: compactSummarize, locals: { ...ordinaryLocals(context.lane.resume.locals), $sdk: { ...sdk, compactPending: true, compactReturnStep: context.lane.resume.step } } } };
668
+ const requestedStep = shouldCompact ? compactSummarize : context.lane.resume.step;
669
+ const handler = this.handlers.get(requestedStep) ?? this.handlers.get(entry);
670
+ const state = this.config.state ? this.config.state.parse(context.lane.context.state) : context.lane.context.state;
671
+ const { ctx, getDelta, getActions, getDerivedRefs, getAdoptImmediately } = makeContext(context, state);
672
+ const result = handler(ctx);
673
+ const derivedFrom = getDerivedRefs();
674
+ const delta = result.contextDelta ?? getDelta();
675
+ const derivedDelta = delta && delta.derivedFrom === undefined && derivedFrom.length ? { ...delta, derivedFrom } : delta;
676
+ const mergedDelta = derivedDelta && derivedFrom.length ? { ...derivedDelta, derivedFrom: [...new Set([...derivedFrom, ...(derivedDelta.derivedFrom ?? [])])] } : derivedDelta;
677
+ const destination = target(result.next, context.lane.resume.step);
678
+ const actions = [...getActions(), ...(result.actions ?? []), ...(destination.action === undefined ? [] : [destination.action])].map((action) => annotateAction(action, derivedFrom));
679
+ return { actions, next: { programId: this.config.id, programVersion: this.config.version, step: destination.step, locals: result.locals ?? ctx.lane.resume.locals }, ...(mergedDelta ? { contextDelta: mergedDelta } : {}), ...((result.adoptCommittedContext || getAdoptImmediately()) ? { adoptCommittedContext: true } : {}) };
680
+ },
681
+ ...(this.boundaryHandler === undefined ? {} : { errorBoundary: (error, context) => { const state = this.config.state ? this.config.state.parse(context.lane.context.state) : context.lane.context.state; const { ctx, getDelta, getActions, getDerivedRefs, getAdoptImmediately } = makeContext(context, state); const result = this.boundaryHandler(error, ctx); const derivedFrom = getDerivedRefs(); const delta = getDelta(); const mergedDelta = delta && derivedFrom.length ? { ...delta, derivedFrom: [...new Set([...derivedFrom, ...(delta.derivedFrom ?? [])])] } : delta; const destination = target(result, context.lane.resume.step); const actions = [...getActions(), ...(destination.action === undefined ? [] : [destination.action])].map((action) => annotateAction(action, derivedFrom)); return { actions, next: { programId: this.config.id, programVersion: this.config.version, step: destination.step, locals: context.lane.resume.locals }, ...(mergedDelta ? { contextDelta: mergedDelta } : {}), ...(getAdoptImmediately() ? { adoptCommittedContext: true } : {}) }; } })
682
+ };
683
+ return definition;
684
+ }
685
+ }
686
+ export function defineLaneProgram(config, define) { const builder = new StepBuilder(config); define(builder); return builder.build(); }
687
+ function pureStepViolation(api) {
688
+ throw Object.assign(new Error(`Pure Step attempted to access ${api}. Use StepContext.now or ctx.trace().`), { code: 'PURE_STEP_VIOLATION' });
689
+ }
690
+ function patchGlobalValue(target, key, value) {
691
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
692
+ try {
693
+ const replacement = descriptor !== undefined && ('get' in descriptor || 'set' in descriptor)
694
+ ? { configurable: descriptor.configurable ?? false, enumerable: descriptor.enumerable ?? false, writable: true, value }
695
+ : descriptor === undefined ? { configurable: true, enumerable: true, writable: true, value } : { configurable: descriptor.configurable ?? false, enumerable: descriptor.enumerable ?? false, writable: true, value };
696
+ Object.defineProperty(target, key, replacement);
697
+ return () => {
698
+ try {
699
+ if (descriptor === undefined)
700
+ delete target[key];
701
+ else
702
+ Object.defineProperty(target, key, descriptor);
703
+ }
704
+ catch { /* best-effort restoration after a synchronous Step */ }
705
+ };
706
+ }
707
+ catch {
708
+ return () => undefined;
709
+ }
710
+ }
711
+ /** Run a synchronous Step inside the development-only impurity boundary. */
712
+ export function withPureStepGuard(callback) {
713
+ const environment = typeof process === 'undefined' ? undefined : process.env.NODE_ENV;
714
+ if (environment === 'production')
715
+ return callback();
716
+ const restores = [];
717
+ const violation = (api) => pureStepViolation(api);
718
+ const globalObject = globalThis;
719
+ const math = globalObject.Math;
720
+ if (math)
721
+ restores.push(patchGlobalValue(math, 'random', () => violation('Math.random')));
722
+ const date = globalObject.Date;
723
+ if (date)
724
+ restores.push(patchGlobalValue(date, 'now', () => violation('Date.now')));
725
+ if (typeof globalObject.fetch === 'function')
726
+ restores.push(patchGlobalValue(globalObject, 'fetch', () => violation('fetch')));
727
+ const consoleObject = globalObject.console;
728
+ if (consoleObject)
729
+ for (const method of ['debug', 'dir', 'error', 'info', 'log', 'trace', 'warn'])
730
+ if (typeof consoleObject[method] === 'function')
731
+ restores.push(patchGlobalValue(consoleObject, method, () => violation(`console.${method}`)));
732
+ const processObject = globalObject.process;
733
+ if (processObject && (typeof processObject === 'object' || typeof processObject === 'function')) {
734
+ try {
735
+ const guardedProcess = new Proxy(processObject, { get: () => violation('process'), set: () => violation('process') });
736
+ restores.push(patchGlobalValue(globalObject, 'process', guardedProcess));
737
+ }
738
+ catch { /* static purity checks still protect environments with an immutable process binding */ }
739
+ }
740
+ try {
741
+ return callback();
742
+ }
743
+ finally {
744
+ for (const restore of restores.reverse())
745
+ restore();
746
+ }
747
+ }
748
+ export function assertProgramPure(program) {
749
+ const candidate = program;
750
+ const source = [program.step.toString(), program.errorBoundary?.toString() ?? '', ...(candidate.debugSources ?? []), program.seriesMemberProgram?.step.toString() ?? '', program.seriesMemberProgram?.errorBoundary?.toString() ?? ''].join('\n');
751
+ for (const forbidden of ['Date.now(', 'Math.random(', 'fetch(', 'await '])
752
+ if (source.includes(forbidden))
753
+ throw new Error(`ASYNC_STEP_NOT_ALLOWED:${forbidden}`);
754
+ if ((candidate.steps ?? []).some((step) => step.includes('undefined')))
755
+ throw new Error('INVALID_STEP_NAME');
756
+ }