@noetic-tools/core 5.8.0 → 5.9.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 (41) hide show
  1. package/dist/adapters/openrouter.d.ts +1 -0
  2. package/dist/adapters/openrouter.js +1 -0
  3. package/dist/builders/provide-builder.d.ts +1 -0
  4. package/dist/builders/provide-builder.js +1 -0
  5. package/dist/builders/spawn-builder.d.ts +0 -1
  6. package/dist/builders/spawn-builder.js +0 -1
  7. package/dist/builders/step-builders.d.ts +0 -1
  8. package/dist/builders/step-builders.js +0 -1
  9. package/dist/builders/tool-builder.d.ts +2 -0
  10. package/dist/builders/tool-builder.js +2 -0
  11. package/dist/builders/workflow-hydrator.js +15 -13
  12. package/dist/builders/workflow-replay-key.d.ts +15 -0
  13. package/dist/builders/workflow-replay-key.js +46 -0
  14. package/dist/harness/agent-harness.d.ts +25 -6
  15. package/dist/harness/agent-harness.js +72 -26
  16. package/dist/harness/deps/interpreter.d.ts +1 -0
  17. package/dist/harness/deps/interpreter.js +1 -0
  18. package/dist/harness/model-call.d.ts +3 -0
  19. package/dist/harness/model-call.js +9 -5
  20. package/dist/interpreter/action-deps.d.ts +1 -1
  21. package/dist/interpreter/action-deps.js +1 -1
  22. package/dist/interpreter/collect-tools.js +19 -54
  23. package/dist/interpreter/execute-action.js +10 -2
  24. package/dist/interpreter/execute-control.js +3 -1
  25. package/dist/interpreter/execute-decide.js +9 -27
  26. package/dist/interpreter/execute.js +30 -36
  27. package/dist/interpreter/model-error.d.ts +9 -0
  28. package/dist/interpreter/model-error.js +32 -0
  29. package/dist/interpreter/step-walk.d.ts +11 -0
  30. package/dist/interpreter/step-walk.js +41 -0
  31. package/dist/observability/genai-attributes.d.ts +7 -0
  32. package/dist/observability/genai-attributes.js +8 -0
  33. package/dist/runtime/context-impl.d.ts +29 -31
  34. package/dist/runtime/context-impl.js +65 -45
  35. package/dist/runtime/durable/harness-checkpoints.d.ts +31 -18
  36. package/dist/runtime/durable/harness-checkpoints.js +70 -54
  37. package/dist/runtime/durable/step-ledger.d.ts +1 -1
  38. package/dist/runtime/durable/step-ledger.js +1 -1
  39. package/dist/schemas/workflow.d.ts +1 -1
  40. package/package.json +3 -3
  41. package/schema/noetic-workflow.schema.json +375 -0
@@ -1,4 +1,5 @@
1
1
  import { frameworkCast, isServerToolSpec } from '@noetic-tools/types';
2
+ import { stepChildren } from './step-walk.js';
2
3
  /**
3
4
  * Recursively walks a step tree and collects all Tool instances
4
5
  * declared on LLM steps. Used to build the unified tool set
@@ -26,59 +27,23 @@ export function deduplicateTools(tools) {
26
27
  ];
27
28
  }
28
29
  function walkStep(step, out) {
29
- switch (step.kind) {
30
- case 'callModel':
31
- // Skip function-form tools: they're resolved per execution from `ctx`
32
- // and cannot contribute to the pre-computed unified set. Consumers that
33
- // need those tools in the pool should register them via
34
- // `AgentHarness.tools` instead. Inline server-tool specs (web search/
35
- // fetch) are not client `Tool`s — they never enter the unified pool.
36
- if (Array.isArray(step.tools)) {
37
- for (const entry of step.tools) {
38
- if (!isServerToolSpec(entry)) {
39
- out.push(entry);
40
- }
41
- }
42
- }
43
- return;
44
- case 'runCode':
45
- case 'invokeTool':
46
- // Decide steps ask questions; they never call tools.
47
- case 'decide':
48
- // An ACP agent runs its own tools inside the external agent process; it
49
- // contributes nothing to the host unified tool set.
50
- case 'acp-agent':
51
- return;
52
- case 'conditional':
53
- if (step._optimizable) {
54
- for (const child of step._optimizable) {
55
- walkStep(child, out);
56
- }
57
- }
58
- return;
59
- case 'inParallel':
60
- if (step._optimizable) {
61
- for (const child of step._optimizable) {
62
- walkStep(child, out);
63
- }
64
- }
65
- return;
66
- case 'withContext':
67
- walkStep(step.child, out);
68
- return;
69
- case 'spawn':
70
- walkStep(step.child, out);
71
- return;
72
- case 'loop':
73
- for (const child of step.steps) {
74
- walkStep(child, out);
75
- }
76
- return;
77
- case 'schedule':
78
- walkStep(step.step, out);
79
- return;
80
- default:
81
- step;
82
- return;
30
+ if (step.kind !== 'callModel') {
31
+ for (const child of stepChildren(step)) {
32
+ walkStep(child, out);
33
+ }
34
+ return;
35
+ }
36
+ // Skip function-form tools: they're resolved per execution from `ctx`
37
+ // and cannot contribute to the pre-computed unified set. Consumers that
38
+ // need those tools in the pool should register them via
39
+ // `AgentHarness.tools` instead. Inline server-tool specs (web search/
40
+ // fetch) are not client `Tool`s — they never enter the unified pool.
41
+ if (!Array.isArray(step.tools)) {
42
+ return;
43
+ }
44
+ for (const entry of step.tools) {
45
+ if (!isServerToolSpec(entry)) {
46
+ out.push(entry);
47
+ }
83
48
  }
84
49
  }
@@ -10,6 +10,7 @@ import { cloneWithGuard } from './clone-guard.js';
10
10
  import { collectAllTools, deduplicateTools } from './collect-tools.js';
11
11
  import { prepareBandedView, stampAnchoringAttributes } from './context-assembly.js';
12
12
  import { trackUsage } from './message-helpers.js';
13
+ import { wrapModelError } from './model-error.js';
13
14
  import { parseStructuredOutput } from './structured-output.js';
14
15
  import { getContextChannelStore, isContextImpl, isFunctionCall, isMutableContext, } from './typeguards.js';
15
16
  //#region run
@@ -548,7 +549,9 @@ export async function executeCallModel(step, input, ctx, layers) {
548
549
  serverToolSpecs,
549
550
  assembledItems,
550
551
  });
551
- const response = await baseCtx.harness.callModel(request);
552
+ const response = await baseCtx.harness.callModel(request).catch((e) => {
553
+ throw wrapModelError(e, step.id, baseCtx);
554
+ });
552
555
  // Track tokens/cost for EVERY model call, including responses steering
553
556
  // subsequently rejects (Deny throw / Guide retry below) — the spend is
554
557
  // real, and until.maxCost / HarnessResponse.usage must see it (spec 07:
@@ -738,8 +741,13 @@ export async function executeSpawn(step, input, ctx, executeStep, opts) {
738
741
  }
739
742
  return returnLayers({
740
743
  layers,
744
+ stepId: step.id,
741
745
  parentCtx: parentExecutionCtx,
742
- childCtx: childExecutionCtx,
746
+ // `childExecutionCtx` predates the spawn span; trace onto that span.
747
+ childCtx: {
748
+ ...childExecutionCtx,
749
+ trace: span,
750
+ },
743
751
  childLog: childCtx.itemLog,
744
752
  result: childOutput,
745
753
  store: layerStore,
@@ -86,7 +86,7 @@ function tracePaths(ctx, executeStep, childContexts) {
86
86
  },
87
87
  };
88
88
  }
89
- function createForkLayerBridge({ ctx, childContexts, opts, }) {
89
+ function createForkLayerBridge({ stepId, ctx, childContexts, opts, }) {
90
90
  const layers = ctx.layers;
91
91
  const layerStore = opts?.layerStore;
92
92
  if (!layers || layers.length === 0 || !layerStore) {
@@ -113,6 +113,7 @@ function createForkLayerBridge({ ctx, childContexts, opts, }) {
113
113
  settle(index, result) {
114
114
  return serialize(() => returnLayers({
115
115
  layers: layers ?? [],
116
+ stepId,
116
117
  parentCtx: parentExecCtx,
117
118
  childCtx: childExecCtxs[index],
118
119
  childLog: childContexts[index].itemLog,
@@ -173,6 +174,7 @@ export async function executeInParallel(step, input, ctx, rawExecuteStep, opts)
173
174
  const childContexts = createChildContexts(baseCtx, paths, step.id);
174
175
  const concurrency = step.concurrency ?? paths.length;
175
176
  const traced = tracePaths(baseCtx, withForkLayers(rawExecuteStep, childContexts, createForkLayerBridge({
177
+ stepId: step.id,
176
178
  ctx: baseCtx,
177
179
  childContexts,
178
180
  opts,
@@ -1,10 +1,10 @@
1
- import { choiceValues, DecideOutputError, frameworkCast, hashString, isNoeticError, NoeticConfigError, NoeticErrorImpl, scoreLevels, } from '@noetic-tools/types';
1
+ import { choiceValues, DecideOutputError, frameworkCast, hashString, NoeticConfigError, NoeticErrorImpl, scoreLevels, } from '@noetic-tools/types';
2
2
  import { z } from 'zod';
3
3
  import { GenAI, NoeticAttr } from '../observability/genai-attributes.js';
4
- import { statusOf } from '../util/status-code.js';
5
- import { allocateBudgets, assembleView, DECISION_PROJECTION } from './action-deps.js';
4
+ import { allocateBudgets, assembleView, DECISION_PROJECTION, laneBase } from './action-deps.js';
6
5
  import { resolveLazy } from './execute-action.js';
7
6
  import { trackUsage } from './message-helpers.js';
7
+ import { wrapModelError } from './model-error.js';
8
8
  import { isContextImpl, isMutableContext } from './typeguards.js';
9
9
  //#region Public API
10
10
  /**
@@ -110,10 +110,12 @@ export async function executeDecide(step, input, ctx, layers) {
110
110
  */
111
111
  const ANSWERED = new WeakMap();
112
112
  function answered(ctx) {
113
- let map = ANSWERED.get(ctx);
113
+ // Every step runs on its own lane of the context; the answers belong to the context.
114
+ const key = laneBase(ctx);
115
+ let map = ANSWERED.get(key);
114
116
  if (!map) {
115
117
  map = new Map();
116
- ANSWERED.set(ctx, map);
118
+ ANSWERED.set(key, map);
117
119
  }
118
120
  return map;
119
121
  }
@@ -150,16 +152,7 @@ async function askHarness(step, ctx, request) {
150
152
  return await ctx.harness.decide(request);
151
153
  }
152
154
  catch (e) {
153
- if (isNoeticError(e) || e instanceof NoeticConfigError) {
154
- throw e;
155
- }
156
- if (ctx.aborted) {
157
- throw new NoeticErrorImpl({
158
- kind: 'cancelled',
159
- reason: ctx.abortReason ?? 'context aborted',
160
- });
161
- }
162
- if (e instanceof DecideOutputError) {
155
+ if (e instanceof DecideOutputError && !ctx.aborted) {
163
156
  if (e.usage) {
164
157
  trackUsage(ctx, {
165
158
  items: [],
@@ -175,18 +168,7 @@ async function askHarness(step, ctx, request) {
175
168
  zodError: e.zodError,
176
169
  });
177
170
  }
178
- if (statusOf(e) === 429) {
179
- throw new NoeticErrorImpl({
180
- kind: 'model_rate_limit',
181
- stepId: step.id,
182
- });
183
- }
184
- throw new NoeticErrorImpl({
185
- kind: 'step_failed',
186
- stepId: step.id,
187
- cause: e instanceof Error ? e : new Error(String(e)),
188
- retriesExhausted: false,
189
- });
171
+ throw wrapModelError(e, step.id, ctx);
190
172
  }
191
173
  }
192
174
  //#endregion
@@ -142,14 +142,12 @@ export function resolveLedgerKey(step, input, ctx) {
142
142
  }
143
143
  /**
144
144
  * Resume path: when a previous run recorded this exact step at `ledgerKey`,
145
- * replay its output instead of re-running. Held as a helper (rather than inline
146
- * in `execute`) so the resume branch stays flat: it pops the frontier itself —
147
- * the caller returns before the normal `leaveStep` runs — and emits
148
- * `step_replayed`. Returns `{ hit: false }` when there is nothing to replay so
149
- * the caller dispatches the step normally.
145
+ * replay its output instead of re-running, and emit `step_replayed`. Returns
146
+ * `{ hit: false }` when there is nothing to replay so the caller dispatches the
147
+ * step normally.
150
148
  */
151
149
  function replayFromLedger(opts) {
152
- const { ledger, ledgerKey, impl, step, broadcaster, agentName } = opts;
150
+ const { ledger, ledgerKey, step, broadcaster, agentName } = opts;
153
151
  if (!ledger || ledgerKey === undefined || ledger.isEmpty) {
154
152
  return {
155
153
  hit: false,
@@ -164,9 +162,6 @@ function replayFromLedger(opts) {
164
162
  hit: false,
165
163
  };
166
164
  }
167
- if (impl && step.id.length > 0) {
168
- impl.leaveStep(step.id);
169
- }
170
165
  emitFrameworkEvent({
171
166
  broadcaster,
172
167
  agentName,
@@ -249,56 +244,57 @@ export async function execute(step, input, ctx) {
249
244
  data: startedData,
250
245
  });
251
246
  }
252
- // Push onto the durable frontier so checkpoints can record exactly which
253
- // step was in flight when the snapshot fired. Steps with empty ids are
254
- // skipped — they are not reattachable by id and would pollute the stack.
247
+ /* Register the step as in flight, so a checkpoint records it in the frontier, and
248
+ * run it on its own lane: a view of the context that carries this dispatch's path.
249
+ * Steps this step dispatches hang under that path, so concurrent dispatches on one
250
+ * context cannot interleave each other's paths. Steps with empty ids stay on the
251
+ * caller's lane — they are not reattachable by id and add no path segment. */
255
252
  const impl = asContextImpl(baseCtx);
256
- if (impl && step.id.length > 0) {
257
- impl.enterStep({
258
- stepId: step.id,
259
- input,
260
- });
261
- }
253
+ const frame = {
254
+ stepId: step.id,
255
+ input,
256
+ };
257
+ const lane = impl && step.id.length > 0 ? impl.enterStep(frame) : null;
258
+ const stepCtx = lane ? frameworkCast(lane) : ctx;
262
259
  /* Resume: a previous run that completed this exact step replays its recorded
263
260
  * output instead of re-running it. `ledgerKey`/`ledger` are captured before
264
- * dispatch and reused by the record below, because `leaveStep` pops the path
265
- * on the way out. */
266
- const ledgerKey = resolveLedgerKey(step, input, ctx);
261
+ * dispatch and reused by the record below. */
262
+ const ledgerKey = resolveLedgerKey(step, input, stepCtx);
267
263
  const ledger = impl?.ledger;
268
264
  const replay = replayFromLedger({
269
265
  ledger,
270
266
  ledgerKey,
271
- impl,
272
267
  step,
273
268
  broadcaster,
274
269
  agentName,
275
270
  });
276
271
  if (replay.hit) {
272
+ lane?.leaveStep(frame);
277
273
  return replay.output;
278
274
  }
279
275
  let result;
280
276
  try {
281
277
  switch (step.kind) {
282
278
  case 'runCode':
283
- result = await dispatchViaAdapter(step, input, ctx, () => executeRunCode(step, input, ctx));
279
+ result = await dispatchViaAdapter(step, input, stepCtx, () => executeRunCode(step, input, stepCtx));
284
280
  break;
285
281
  case 'callModel':
286
- result = await executeCallModel(step, input, ctx, baseCtx.layers);
282
+ result = await executeCallModel(step, input, stepCtx, baseCtx.layers);
287
283
  break;
288
284
  case 'acp-agent':
289
- result = await executeAcpAgent(step, input, ctx, baseCtx.layers);
285
+ result = await executeAcpAgent(step, input, stepCtx, baseCtx.layers);
290
286
  break;
291
287
  case 'invokeTool':
292
- result = await executeInvokeTool(step, input, ctx, baseCtx.harness);
288
+ result = await executeInvokeTool(step, input, stepCtx, baseCtx.harness);
293
289
  break;
294
290
  case 'decide':
295
- result = await executeDecide(step, input, ctx, baseCtx.layers);
291
+ result = await executeDecide(step, input, stepCtx, baseCtx.layers);
296
292
  break;
297
293
  case 'conditional':
298
- result = await executeConditional(step, input, ctx, (s, i, c) => execute(s, i, c));
294
+ result = await executeConditional(step, input, stepCtx, (s, i, c) => execute(s, i, c));
299
295
  break;
300
296
  case 'inParallel':
301
- result = await executeInParallel(step, input, ctx, (s, i, c) => execute(s, i, c), {
297
+ result = await executeInParallel(step, input, stepCtx, (s, i, c) => execute(s, i, c), {
302
298
  // Fork paths are child executions: the layer store lets their
303
299
  // `onSpawn`/`onReturn` boundary run (see `createForkLayerBridge`).
304
300
  layerStore: harnessWithLayerStore(baseCtx).layerStateStore,
@@ -306,16 +302,16 @@ export async function execute(step, input, ctx) {
306
302
  });
307
303
  break;
308
304
  case 'spawn':
309
- result = await dispatchViaAdapter(step, input, ctx, () => executeSpawn(step, input, ctx, (s, i, c) => execute(s, i, c), buildSpawnOpts(ctx)));
305
+ result = await dispatchViaAdapter(step, input, stepCtx, () => executeSpawn(step, input, stepCtx, (s, i, c) => execute(s, i, c), buildSpawnOpts(stepCtx)));
310
306
  break;
311
307
  case 'withContext':
312
- result = await executeWithContext(step, input, ctx, (s, i, c) => execute(s, i, c));
308
+ result = await executeWithContext(step, input, stepCtx, (s, i, c) => execute(s, i, c));
313
309
  break;
314
310
  case 'loop':
315
- result = await executeLoop(step, input, ctx, (s, i, c) => execute(s, i, c));
311
+ result = await executeLoop(step, input, stepCtx, (s, i, c) => execute(s, i, c));
316
312
  break;
317
313
  case 'schedule':
318
- result = await executeSchedule(step, input, ctx, (s, i, c) => execute(s, i, c));
314
+ result = await executeSchedule(step, input, stepCtx, (s, i, c) => execute(s, i, c));
319
315
  break;
320
316
  default: {
321
317
  const _exhaustive = step;
@@ -330,9 +326,7 @@ export async function execute(step, input, ctx) {
330
326
  }
331
327
  }
332
328
  finally {
333
- if (impl && step.id.length > 0) {
334
- impl.leaveStep(step.id);
335
- }
329
+ lane?.leaveStep(frame);
336
330
  }
337
331
  // Emit step_completed framework event (respects step.emit option)
338
332
  const completedData = {
@@ -0,0 +1,9 @@
1
+ import type { Context } from './action-types.js';
2
+ /**
3
+ * Give an error from a model provider call the step's id and a Noetic kind.
4
+ *
5
+ * Noetic errors pass through untouched. A failure on an aborted context is
6
+ * `cancelled` (the abort is what broke the call), a 429 is `model_rate_limit`,
7
+ * and anything else is `step_failed` with the provider error as its cause.
8
+ */
9
+ export declare function wrapModelError(e: unknown, stepId: string, ctx: Pick<Context, 'aborted' | 'abortReason'>): Error;
@@ -0,0 +1,32 @@
1
+ import { isNoeticError, NoeticConfigError, NoeticErrorImpl } from '@noetic-tools/types';
2
+ import { statusOf } from '../util/status-code.js';
3
+ /**
4
+ * Give an error from a model provider call the step's id and a Noetic kind.
5
+ *
6
+ * Noetic errors pass through untouched. A failure on an aborted context is
7
+ * `cancelled` (the abort is what broke the call), a 429 is `model_rate_limit`,
8
+ * and anything else is `step_failed` with the provider error as its cause.
9
+ */
10
+ export function wrapModelError(e, stepId, ctx) {
11
+ if (isNoeticError(e) || e instanceof NoeticConfigError) {
12
+ return e;
13
+ }
14
+ if (ctx.aborted) {
15
+ return new NoeticErrorImpl({
16
+ kind: 'cancelled',
17
+ reason: ctx.abortReason ?? 'context aborted',
18
+ });
19
+ }
20
+ if (statusOf(e) === 429) {
21
+ return new NoeticErrorImpl({
22
+ kind: 'model_rate_limit',
23
+ stepId,
24
+ });
25
+ }
26
+ return new NoeticErrorImpl({
27
+ kind: 'step_failed',
28
+ stepId,
29
+ cause: e instanceof Error ? e : new Error(String(e)),
30
+ retriesExhausted: false,
31
+ });
32
+ }
@@ -0,0 +1,11 @@
1
+ import type { Step } from '@noetic-tools/types';
2
+ /**
3
+ * Whether `step` or any step nested in it satisfies `test`.
4
+ *
5
+ * Only statically declared children are visited: a `conditional` or
6
+ * `inParallel` built from functions exposes its children through
7
+ * `_optimizable` only when the builder could enumerate them.
8
+ */
9
+ export declare function someStep(step: Step, test: (step: Step) => boolean): boolean;
10
+ /** The steps nested directly inside `step`, as far as they are statically known. */
11
+ export declare function stepChildren(step: Step): ReadonlyArray<Step>;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Whether `step` or any step nested in it satisfies `test`.
3
+ *
4
+ * Only statically declared children are visited: a `conditional` or
5
+ * `inParallel` built from functions exposes its children through
6
+ * `_optimizable` only when the builder could enumerate them.
7
+ */
8
+ export function someStep(step, test) {
9
+ if (test(step)) {
10
+ return true;
11
+ }
12
+ return stepChildren(step).some((child) => someStep(child, test));
13
+ }
14
+ /** The steps nested directly inside `step`, as far as they are statically known. */
15
+ export function stepChildren(step) {
16
+ switch (step.kind) {
17
+ case 'callModel':
18
+ case 'runCode':
19
+ case 'invokeTool':
20
+ case 'decide':
21
+ case 'acp-agent':
22
+ return [];
23
+ case 'conditional':
24
+ case 'inParallel':
25
+ return step._optimizable ?? [];
26
+ case 'withContext':
27
+ case 'spawn':
28
+ return [
29
+ step.child,
30
+ ];
31
+ case 'loop':
32
+ return step.steps;
33
+ case 'schedule':
34
+ return [
35
+ step.step,
36
+ ];
37
+ default:
38
+ step;
39
+ return [];
40
+ }
41
+ }
@@ -72,4 +72,11 @@ export declare const NoeticEvent: {
72
72
  readonly LOOP_VERDICT: "loop.verdict";
73
73
  /** A channel dropped data or is waiting with no timeout: `{ kind, channelName, message, capacity?, operation? }`. */
74
74
  readonly CHANNEL_WARNING: "channel.warning";
75
+ /**
76
+ * A layer's `onReturn` hook failed, so a spawn or inParallel path returned
77
+ * its result unchanged. Added to the child's span: `{ stepId, layerId,
78
+ * message }`. Defined in `@noetic-tools/context`, whose `returnLayers`
79
+ * emits it and cannot import core.
80
+ */
81
+ readonly SPAWN_SUMMARY_FAILED: "spawn.summary_failed";
75
82
  };
@@ -1,3 +1,4 @@
1
+ import { SPAWN_SUMMARY_FAILED_EVENT } from '@noetic-tools/context';
1
2
  // GenAI Semantic Convention attributes
2
3
  export const GenAI = {
3
4
  SYSTEM: 'gen_ai.system',
@@ -77,4 +78,11 @@ export const NoeticEvent = {
77
78
  LOOP_VERDICT: 'loop.verdict',
78
79
  /** A channel dropped data or is waiting with no timeout: `{ kind, channelName, message, capacity?, operation? }`. */
79
80
  CHANNEL_WARNING: 'channel.warning',
81
+ /**
82
+ * A layer's `onReturn` hook failed, so a spawn or inParallel path returned
83
+ * its result unchanged. Added to the child's span: `{ stepId, layerId,
84
+ * message }`. Defined in `@noetic-tools/context`, whose `returnLayers`
85
+ * emits it and cannot import core.
86
+ */
87
+ SPAWN_SUMMARY_FAILED: SPAWN_SUMMARY_FAILED_EVENT,
80
88
  };
@@ -69,30 +69,24 @@ export declare class ContextImpl implements Context<ContextData> {
69
69
  private readonly _children;
70
70
  private _contextData?;
71
71
  /**
72
- * Stack of steps currently in flight on this context, most-recent last.
73
- * Pushed by `enterStep` at the top of `execute()` and popped by
74
- * `leaveStep` when the step resolves (success or failure). The harness'
75
- * checkpoint writer serialises this stack as the execution frontier so
76
- * a restart can identify which step the context was paused inside.
72
+ * Steps in flight on this context, in dispatch order. `enterStep` adds a frame and
73
+ * `leaveStep` removes that same frame, so concurrent dispatches can settle in any
74
+ * order without disturbing each other. The checkpoint writer serialises this set as
75
+ * the execution frontier.
77
76
  * @internal
78
77
  */
79
- private readonly _frontier;
78
+ private readonly _inFlight;
80
79
  /**
81
- * Path segment for each in-flight frame, parallel to `_frontier`. Kept separate so
82
- * `serialiseFrontier()` keeps emitting the published `FrontierFrame` shape.
80
+ * Execution path of the step whose lane this is. On the context itself it is the
81
+ * prefix its steps hang under: empty on a root context, the parent's path plus a
82
+ * discriminator on an inParallel/spawn child, so a path stays unique across the whole
83
+ * step tree. A lane (see `enterStep`) reads its own step's path here instead.
83
84
  * @internal
84
85
  */
85
- private readonly _segments;
86
- /**
87
- * Path this context's steps hang under. Empty on a root context; an inParallel/spawn child
88
- * inherits its parent's current path plus a discriminator, so a path key stays unique
89
- * across the whole step tree rather than restarting per child context.
90
- * @internal
91
- */
92
- private readonly _pathPrefix;
86
+ private readonly _path;
93
87
  /**
94
88
  * Dispatch counts per `<parent path>/<step id>`, so a loop body re-entered on each
95
- * iteration gets `#0`, `#1`, … instead of colliding on one key.
89
+ * iteration gets `#0`, `#1`, … instead of colliding on one key. Shared by every lane.
96
90
  * @internal
97
91
  */
98
92
  private readonly _occurrences;
@@ -179,28 +173,26 @@ export declare class ContextImpl implements Context<ContextData> {
179
173
  get abortSignal(): AbortSignal;
180
174
  /**
181
175
  * @internal
182
- * Push a frame onto the execution frontier. Called by `execute()` at the
183
- * top of every step dispatch so that the frontier reflects exactly the
184
- * stack of steps currently in-flight on this context.
176
+ * Register `frame` as in flight and return the lane its step runs on: a view of
177
+ * this context that shares everything except its execution path. `execute()` hands
178
+ * the lane to the step, so steps the step dispatches hang under its path no matter
179
+ * how other dispatches on this context interleave. The occurrence ordinal is taken
180
+ * here, at dispatch, never at settle.
185
181
  */
186
- enterStep(frame: FrontierFrame): void;
182
+ enterStep(frame: FrontierFrame): ContextImpl;
187
183
  /**
188
184
  * @internal
189
- * The execution path of the step currently on top of the frontier — the ledger key
190
- * identifying this exact dispatch. Stable across a replay given the same control
191
- * flow, which is what lets a resumed run line recorded outputs up with the steps
192
- * that produced them.
185
+ * The execution path of the step this lane belongs to — the ledger key identifying
186
+ * this exact dispatch. Stable across a replay given the same control flow, which is
187
+ * what lets a resumed run line recorded outputs up with the steps that produced them.
193
188
  */
194
189
  currentPath(): string;
195
190
  /**
196
191
  * @internal
197
- * Pop the top frame. Called when a step resolves (success or failure)
198
- * so that the frontier unwinds cleanly. The value of `expectedStepId`
199
- * is used as a consistency check — if it does not match the top frame
200
- * the pop is still performed, but a best-effort warning is surfaced to
201
- * stderr rather than swallowed silently.
192
+ * Drop `frame` from the in-flight set. Called when its step resolves, success or
193
+ * failure.
202
194
  */
203
- leaveStep(expectedStepId: string): void;
195
+ leaveStep(frame: FrontierFrame): void;
204
196
  /**
205
197
  * @internal
206
198
  * Return a defensive copy of the current frontier. Consumed by the
@@ -208,3 +200,9 @@ export declare class ContextImpl implements Context<ContextData> {
208
200
  */
209
201
  serialiseFrontier(): FrontierFrame[];
210
202
  }
203
+ /**
204
+ * @internal
205
+ * The context a lane views, or `ctx` itself when it is not a lane. For per-execution
206
+ * caches keyed by context identity, which every lane of one context must share.
207
+ */
208
+ export declare function laneBase(ctx: Context<ContextData>): Context<ContextData>;