@noetic-tools/core 5.2.0 → 5.4.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 (92) hide show
  1. package/README.md +8 -6
  2. package/dist/adapters/in-memory-subprocess/step-completion.js +1 -1
  3. package/dist/adapters/openrouter.d.ts +20 -2
  4. package/dist/adapters/openrouter.js +102 -6
  5. package/dist/builders/control-flow-builders.d.ts +13 -13
  6. package/dist/builders/control-flow-builders.js +11 -11
  7. package/dist/{patterns → builders}/dynamic-workflow.d.ts +1 -1
  8. package/dist/{patterns → builders}/dynamic-workflow.js +18 -12
  9. package/dist/builders/every.d.ts +14 -14
  10. package/dist/builders/every.js +16 -16
  11. package/dist/builders/layer-provides-builders.d.ts +1 -1
  12. package/dist/builders/layer-provides-builders.js +1 -1
  13. package/dist/builders/provide-builder.d.ts +5 -12
  14. package/dist/builders/provide-builder.js +9 -10
  15. package/dist/builders/spawn-builder.d.ts +0 -2
  16. package/dist/builders/spawn-builder.js +2 -3
  17. package/dist/builders/step-builders.d.ts +66 -55
  18. package/dist/builders/step-builders.js +118 -111
  19. package/dist/builders/tool-builder.d.ts +18 -22
  20. package/dist/builders/tool-builder.js +6 -4
  21. package/dist/builders/workflow-hydrator.d.ts +3 -3
  22. package/dist/builders/workflow-hydrator.js +59 -63
  23. package/dist/builders/workflow-step.d.ts +13 -25
  24. package/dist/builders/workflow-step.js +13 -18
  25. package/dist/conditions/conditions.d.ts +2 -2
  26. package/dist/conditions/conditions.js +1 -1
  27. package/dist/harness/agent-harness.d.ts +58 -40
  28. package/dist/harness/agent-harness.js +125 -51
  29. package/dist/harness/deps/context.d.ts +1 -1
  30. package/dist/harness/deps/context.js +1 -1
  31. package/dist/harness/deps/interpreter.d.ts +1 -0
  32. package/dist/harness/deps/interpreter.js +1 -0
  33. package/dist/harness/deps/types.d.ts +2 -2
  34. package/dist/harness/model-call.d.ts +1 -1
  35. package/dist/harness/model-call.js +45 -23
  36. package/dist/index.d.ts +23 -35
  37. package/dist/index.js +10 -26
  38. package/dist/interpreter/action-deps.d.ts +2 -2
  39. package/dist/interpreter/action-deps.js +1 -1
  40. package/dist/interpreter/action-types.d.ts +1 -1
  41. package/dist/interpreter/collect-tools.js +7 -7
  42. package/dist/interpreter/context-assembly.d.ts +50 -0
  43. package/dist/interpreter/context-assembly.js +547 -0
  44. package/dist/interpreter/execute-action.d.ts +5 -5
  45. package/dist/interpreter/execute-action.js +337 -208
  46. package/dist/interpreter/execute-control.d.ts +9 -9
  47. package/dist/interpreter/execute-control.js +27 -27
  48. package/dist/interpreter/execute-sub-harness.d.ts +1 -1
  49. package/dist/interpreter/execute-sub-harness.js +7 -26
  50. package/dist/interpreter/execute.d.ts +1 -1
  51. package/dist/interpreter/execute.js +26 -25
  52. package/dist/interpreter/step-dispatcher.d.ts +1 -1
  53. package/dist/interpreter/step-dispatcher.js +3 -3
  54. package/dist/interpreter/structured-output.d.ts +14 -0
  55. package/dist/interpreter/structured-output.js +40 -0
  56. package/dist/interpreter/typeguards.d.ts +1 -1
  57. package/dist/interpreter/typeguards.js +1 -1
  58. package/dist/observability/genai-attributes.d.ts +20 -0
  59. package/dist/observability/genai-attributes.js +20 -0
  60. package/dist/portable/builders.d.ts +3 -3
  61. package/dist/portable/builders.js +6 -5
  62. package/dist/portable/context-layers.d.ts +1 -1
  63. package/dist/portable/context-layers.js +1 -1
  64. package/dist/runtime/broadcaster-utils.d.ts +1 -1
  65. package/dist/runtime/broadcaster-utils.js +1 -1
  66. package/dist/runtime/context-impl.d.ts +8 -10
  67. package/dist/runtime/context-impl.js +8 -12
  68. package/dist/runtime/durable/step-ledger.d.ts +2 -2
  69. package/dist/runtime/durable/step-ledger.js +3 -3
  70. package/dist/runtime/session-runner.d.ts +12 -1
  71. package/dist/runtime/session-runner.js +33 -1
  72. package/dist/runtime/tool-context.js +0 -2
  73. package/dist/schemas/workflow.d.ts +3 -3
  74. package/dist/schemas/workflow.js +2 -2
  75. package/dist/types/checkpoint.d.ts +11 -2
  76. package/dist/types/checkpoint.js +10 -1
  77. package/dist/{runtime → util}/detached-handle.js +1 -1
  78. package/package.json +6 -4
  79. package/schema/noetic-workflow.schema.json +24 -11
  80. package/dist/builders/context-option.d.ts +0 -23
  81. package/dist/builders/context-option.js +0 -22
  82. package/dist/deprecated.d.ts +0 -30
  83. package/dist/deprecated.js +0 -28
  84. package/dist/patterns/interview.d.ts +0 -53
  85. package/dist/patterns/interview.js +0 -113
  86. package/dist/patterns/plans.d.ts +0 -44
  87. package/dist/patterns/plans.js +0 -125
  88. package/dist/patterns/ralph-wiggum.d.ts +0 -19
  89. package/dist/patterns/ralph-wiggum.js +0 -37
  90. package/dist/patterns/react.d.ts +0 -19
  91. package/dist/patterns/react.js +0 -41
  92. /package/dist/{runtime → util}/detached-handle.d.ts +0 -0
@@ -0,0 +1,547 @@
1
+ import { createMessage, estimateTokens, hashItems, hashString } from '@noetic-tools/types';
2
+ import { NoeticAttr } from '../observability/genai-attributes.js';
3
+ import { churnFor, churnRate, lineageKey, openEpoch, pin, pinKey, reanchorReason, resolveCacheConfig, } from './action-deps.js';
4
+ /**
5
+ * Supersedes below this are never treated as delta pressure, whatever share of
6
+ * the anchor band they are. Keeps a small context from re-anchoring on the fixed
7
+ * cost of the `<context_updates>` wrapper alone.
8
+ */
9
+ const MIN_PRESSURE_TOKENS = 256;
10
+ //#endregion
11
+ //#region Public API
12
+ /**
13
+ * Split recall output into the anchor and live bands, replay pinned anchors, and
14
+ * gather everything that changed into a single supersede message.
15
+ *
16
+ * Runs once per LLM step, before the steering retry loop — a retry replays the
17
+ * same view, so re-pinning or re-counting churn there would distort both.
18
+ */
19
+ export async function prepareBandedView(params) {
20
+ const config = resolveCacheConfig(params.config);
21
+ if (!config.enabled) {
22
+ return legacyView(params.recallResults);
23
+ }
24
+ const key = lineageKey(params.execCtx);
25
+ const instructionsHash = hashString(params.instructions ?? '');
26
+ const epoch = resolveEpoch({
27
+ ...params,
28
+ config,
29
+ key,
30
+ instructionsHash,
31
+ });
32
+ const banded = bandAndPin({
33
+ ...params,
34
+ config,
35
+ epoch,
36
+ });
37
+ const anchorTokens = totalTokens(banded.anchorItems);
38
+ const rendered = await buildDeltaItems(banded.stale, epoch.id, params.execCtx);
39
+ // A change we cannot put into words must not be published as silence: the
40
+ // pinned block is already in the view and the model would read it as current.
41
+ // Re-anchor so the fresh render reaches it directly instead.
42
+ const undescribed = rendered.undescribed;
43
+ // Two ways the supersedes stop being worth their tokens: they outgrow the
44
+ // band they patch, or they no longer fit the window at all. Either way the
45
+ // answer is the same — re-anchor to fresh output and publish nothing. That
46
+ // costs one cache miss and never risks shipping content known to be stale.
47
+ //
48
+ // The ratio alone is not enough: the wrapper around a supersede costs a fixed
49
+ // few dozen tokens, which would always look enormous beside a small anchor
50
+ // band and re-anchor every turn. Pressure needs the supersedes to be both
51
+ // absolutely and proportionally large.
52
+ const deltaTokens = totalTokens(rendered.items);
53
+ const overPressure = deltaTokens > Math.max(MIN_PRESSURE_TOKENS, config.deltaBudgetFraction * anchorTokens);
54
+ if (!params.readOnly && (undescribed || overPressure)) {
55
+ return rebuildOnFreshEpoch({
56
+ ...params,
57
+ config,
58
+ key,
59
+ instructionsHash,
60
+ reason: overPressure ? 'delta-pressure' : 'delta-overflow',
61
+ });
62
+ }
63
+ if (!params.readOnly) {
64
+ epoch.anchorTokens = anchorTokens;
65
+ epoch.assemblies += 1;
66
+ epoch.deltaTokens = deltaTokens;
67
+ }
68
+ const deltaItems = rendered.items;
69
+ return {
70
+ anchorItems: banded.anchorItems,
71
+ liveItems: banded.liveItems,
72
+ deltaItems,
73
+ servedPerLayer: banded.servedPerLayer,
74
+ serveInfo: banded.serveInfo,
75
+ epoch: summarize(epoch, banded.liveTokens, deltaTokens),
76
+ };
77
+ }
78
+ /**
79
+ * Record how the view was banded on the step's span, so a slow or expensive run
80
+ * can be traced back to the layer that keeps invalidating the prefix.
81
+ */
82
+ export function stampAnchoringAttributes(span, view) {
83
+ if (!span || !view.epoch) {
84
+ return;
85
+ }
86
+ const { epoch } = view;
87
+ span.setAttribute(NoeticAttr.CONTEXT_EPOCH_ID, epoch.id);
88
+ span.setAttribute(NoeticAttr.CONTEXT_EPOCH_AGE, epoch.age);
89
+ span.setAttribute(NoeticAttr.CONTEXT_ANCHOR_TOKENS, epoch.anchorTokens);
90
+ span.setAttribute(NoeticAttr.CONTEXT_LIVE_TOKENS, epoch.liveTokens);
91
+ span.setAttribute(NoeticAttr.CONTEXT_DELTA_TOKENS, epoch.deltaTokens);
92
+ if (epoch.reanchorReason) {
93
+ span.setAttribute(NoeticAttr.CONTEXT_REANCHOR_REASON, epoch.reanchorReason);
94
+ }
95
+ if (view.serveInfo.size === 0) {
96
+ return;
97
+ }
98
+ const placements = [];
99
+ const churn = [];
100
+ for (const [id, info] of view.serveInfo) {
101
+ placements.push({
102
+ id,
103
+ placement: info.placement,
104
+ served: info.served,
105
+ changed: info.changed,
106
+ });
107
+ churn.push({
108
+ id,
109
+ rate: Number(info.churnRate.toFixed(3)),
110
+ rebillTokens: Math.round(info.rebillTokens),
111
+ });
112
+ }
113
+ // Span attributes hold only scalars, so structured values travel as JSON.
114
+ span.setAttribute(NoeticAttr.CONTEXT_LAYER_PLACEMENTS, JSON.stringify(placements));
115
+ span.setAttribute(NoeticAttr.CONTEXT_LAYER_CHURN, JSON.stringify(churn));
116
+ }
117
+ /** Tokens the prompt prefix was expected to have cached — what `noteCacheOutcome` judges against. */
118
+ export function expectedCachedTokens(systemPromptItems, view) {
119
+ return totalTokens(systemPromptItems) + (view.epoch?.anchorTokens ?? 0);
120
+ }
121
+ //#endregion
122
+ //#region Epoch resolution
123
+ function resolveEpoch(params) {
124
+ const { store, key, instructionsHash, config } = params;
125
+ const current = store.epochs.get(key);
126
+ const reason = reanchorReason({
127
+ epoch: current,
128
+ key,
129
+ instructionsHash,
130
+ store,
131
+ config,
132
+ });
133
+ // A read-only pass never opens or rebuilds an epoch — looking at a
134
+ // conversation must not change what its next turn sends. It reuses whatever
135
+ // exists, or works against a throwaway when the lineage is still cold.
136
+ if (params.readOnly) {
137
+ return current ?? emptyEpoch(key, instructionsHash);
138
+ }
139
+ if (!reason) {
140
+ // No reason fires while `current` is absent — that case is 'cold-start'.
141
+ return current ?? emptyEpoch(key, instructionsHash);
142
+ }
143
+ return openEpoch({
144
+ store,
145
+ key,
146
+ reason,
147
+ instructionsHash,
148
+ layers: params.layers,
149
+ config,
150
+ });
151
+ }
152
+ /** Re-anchor and band again, publishing no supersedes against the fresh pins. */
153
+ async function rebuildOnFreshEpoch(params) {
154
+ const epoch = openEpoch({
155
+ store: params.store,
156
+ key: params.key,
157
+ reason: params.reason,
158
+ instructionsHash: params.instructionsHash,
159
+ layers: params.layers,
160
+ config: params.config,
161
+ });
162
+ const banded = bandAndPin({
163
+ ...params,
164
+ epoch,
165
+ });
166
+ epoch.anchorTokens = totalTokens(banded.anchorItems);
167
+ epoch.assemblies += 1;
168
+ epoch.deltaTokens = 0;
169
+ return {
170
+ anchorItems: banded.anchorItems,
171
+ liveItems: banded.liveItems,
172
+ deltaItems: [],
173
+ servedPerLayer: banded.servedPerLayer,
174
+ serveInfo: banded.serveInfo,
175
+ epoch: summarize(epoch, banded.liveTokens, 0),
176
+ };
177
+ }
178
+ /**
179
+ * Place each layer's output in a band, then build the anchor band from the pins
180
+ * themselves rather than from this turn's recall.
181
+ *
182
+ * Serving the pin map — in pin insertion order, which is the slot order of the
183
+ * assembly that anchored it — is what makes the prefix byte-stable by
184
+ * construction. Nothing a later turn does (a layer falling silent, a new layer
185
+ * appearing, a layer turning non-idempotent) can reorder or shorten it; those
186
+ * all become supersedes at the tail instead.
187
+ */
188
+ function bandAndPin(params) {
189
+ const { epoch, store, layers, budgets, readOnly } = params;
190
+ const byId = new Map(layers.map((l) => [
191
+ l.id,
192
+ l,
193
+ ]));
194
+ const out = {
195
+ anchorItems: [],
196
+ liveItems: [],
197
+ liveTokens: 0,
198
+ servedPerLayer: [],
199
+ serveInfo: new Map(),
200
+ stale: [],
201
+ };
202
+ const anchored = new Map();
203
+ for (const result of params.recallResults) {
204
+ const layer = byId.get(result.layerId);
205
+ // Output from a layer that is not in this step's set can never be pinned
206
+ // against it; render it live rather than guessing.
207
+ if (!layer || bandFor(layer, result, epoch) === 'live') {
208
+ out.liveItems.push(...result.items);
209
+ out.liveTokens += result.tokenCount;
210
+ out.servedPerLayer.push(result);
211
+ if (layer) {
212
+ out.serveInfo.set(layer.id, {
213
+ placement: 'live',
214
+ served: 'fresh',
215
+ changed: false,
216
+ churnRate: churnRate(churnFor(store, pinKey(layer))),
217
+ rebillTokens: 0,
218
+ });
219
+ }
220
+ continue;
221
+ }
222
+ anchored.set(pinKey(layer), result);
223
+ }
224
+ // A fresh epoch anchors whatever it is given, in slot order. A read-only pass
225
+ // does the same but keeps the result to itself, so a preview still shows the
226
+ // view the next real turn would send without deciding it in advance.
227
+ const pins = epoch.pins.size > 0 || epoch.assemblies > 0 ? epoch.pins : new Map();
228
+ if (pins !== epoch.pins) {
229
+ for (const [key, result] of anchored) {
230
+ const layer = byId.get(result.layerId);
231
+ if (!layer) {
232
+ continue;
233
+ }
234
+ const entry = {
235
+ layerId: layer.id,
236
+ items: result.items,
237
+ tokenCount: result.tokenCount,
238
+ contentHash: hashItems(result.items),
239
+ state: undefined,
240
+ slot: layer.slot,
241
+ };
242
+ pins.set(key, entry);
243
+ if (!readOnly) {
244
+ pin(epoch, key, entry);
245
+ }
246
+ }
247
+ }
248
+ servePins({
249
+ out,
250
+ epoch,
251
+ pins,
252
+ anchored,
253
+ byId,
254
+ store,
255
+ budgets,
256
+ readOnly: readOnly === true,
257
+ });
258
+ collectAdditions({
259
+ out,
260
+ pins,
261
+ anchored,
262
+ byId,
263
+ store,
264
+ budgets,
265
+ readOnly: readOnly === true,
266
+ });
267
+ return out;
268
+ }
269
+ /**
270
+ * An explicit placement always wins. A layer whose `recall` changed state is
271
+ * forced live regardless: replaying an older render would discard the very
272
+ * thing that call committed.
273
+ */
274
+ function bandFor(layer, result, epoch) {
275
+ if (result.mutatedState) {
276
+ return 'live';
277
+ }
278
+ if (layer.placement === 'anchor' || layer.placement === 'live') {
279
+ return layer.placement;
280
+ }
281
+ return epoch.autoBand.get(pinKey(layer)) ?? 'anchor';
282
+ }
283
+ /**
284
+ * Serve every pin, in pinned order, and note which have gone stale.
285
+ *
286
+ * A pin is served even when its layer produced nothing this turn, or has since
287
+ * moved to the live band: dropping it would shorten the prefix and cost the
288
+ * cache far more than the tokens it saves. The model is told to disregard it
289
+ * instead, every turn, until the next re-anchor clears it.
290
+ */
291
+ function servePins(args) {
292
+ const { out, epoch, pins, anchored, byId, store, budgets, readOnly } = args;
293
+ for (const [key, held] of pins) {
294
+ out.anchorItems.push(...held.items);
295
+ const layer = byId.get(held.layerId);
296
+ const fresh = anchored.get(key);
297
+ const churn = churnFor(store, key);
298
+ // The pin is what the model sees, changed or not — that is the whole point.
299
+ out.servedPerLayer.push({
300
+ layerId: held.layerId,
301
+ items: held.items,
302
+ tokenCount: held.tokenCount,
303
+ });
304
+ if (!fresh || !layer) {
305
+ // Gone quiet, or moved to the live band. Either way the pinned block no
306
+ // longer reflects reality and must carry a standing retraction.
307
+ if (layer) {
308
+ out.stale.push({
309
+ layer,
310
+ action: 'retract',
311
+ prev: held.items,
312
+ next: [],
313
+ prevState: held.state,
314
+ budget: budgets.get(layer.id) ?? 0,
315
+ });
316
+ out.serveInfo.set(layer.id, {
317
+ placement: 'anchor',
318
+ served: 'pinned',
319
+ changed: true,
320
+ churnRate: churnRate(churn),
321
+ rebillTokens: churn.rebillTokens,
322
+ });
323
+ }
324
+ continue;
325
+ }
326
+ const changed = held.contentHash !== hashItems(fresh.items);
327
+ if (!readOnly) {
328
+ churn.observed += 1;
329
+ if (changed) {
330
+ churn.changed += 1;
331
+ churn.rebillTokens += epoch.anchorTokens;
332
+ }
333
+ }
334
+ if (changed) {
335
+ out.stale.push({
336
+ layer,
337
+ action: 'replace',
338
+ prev: held.items,
339
+ next: fresh.items,
340
+ prevState: held.state,
341
+ budget: budgets.get(layer.id) ?? 0,
342
+ });
343
+ }
344
+ out.serveInfo.set(layer.id, {
345
+ placement: 'anchor',
346
+ served: 'pinned',
347
+ changed,
348
+ churnRate: churnRate(churn),
349
+ rebillTokens: churn.rebillTokens,
350
+ });
351
+ }
352
+ }
353
+ /**
354
+ * Publish anchored output that has no pin — a layer first seen mid-epoch. It
355
+ * cannot be spliced into a frozen prefix, so it rides in the supersede message
356
+ * until the next re-anchor folds it into the band. Its tokens count toward
357
+ * delta pressure, so a run of additions re-anchors on its own.
358
+ */
359
+ function collectAdditions(args) {
360
+ const { out, pins, anchored, byId, store, budgets, readOnly } = args;
361
+ for (const [key, result] of anchored) {
362
+ if (pins.has(key)) {
363
+ continue;
364
+ }
365
+ const layer = byId.get(result.layerId);
366
+ if (!layer) {
367
+ continue;
368
+ }
369
+ const churn = churnFor(store, key);
370
+ if (!readOnly) {
371
+ churn.observed += 1;
372
+ churn.changed += 1;
373
+ }
374
+ out.stale.push({
375
+ layer,
376
+ action: 'add',
377
+ prev: [],
378
+ next: result.items,
379
+ prevState: undefined,
380
+ budget: budgets.get(layer.id) ?? 0,
381
+ });
382
+ out.servedPerLayer.push(result);
383
+ out.serveInfo.set(layer.id, {
384
+ placement: 'anchor',
385
+ served: 'fresh',
386
+ changed: true,
387
+ churnRate: churnRate(churn),
388
+ rebillTokens: churn.rebillTokens,
389
+ });
390
+ }
391
+ }
392
+ //#endregion
393
+ //#region Supersedes
394
+ /**
395
+ * Render every stale anchor into ONE developer message.
396
+ *
397
+ * A layer may describe its own change compactly via `renderDelta`; otherwise the
398
+ * full new content is republished, which costs more tokens but is always right.
399
+ * A hook that throws or hangs falls back to the default — a supersede is a
400
+ * correctness obligation, so it is never skipped because a hook misbehaved.
401
+ */
402
+ async function buildDeltaItems(stale, epochId, ctx) {
403
+ if (stale.length === 0) {
404
+ return {
405
+ items: [],
406
+ undescribed: false,
407
+ };
408
+ }
409
+ const blocks = [];
410
+ let undescribed = false;
411
+ for (const entry of stale) {
412
+ const body = await renderBody(entry, ctx);
413
+ if (body === null) {
414
+ // Nothing renderable — a layer emitting non-text items, say. The pinned
415
+ // block is already in the view, so staying silent would present it as
416
+ // current. Report it and let the caller re-anchor.
417
+ undescribed = true;
418
+ continue;
419
+ }
420
+ blocks.push(`<update layer="${entry.layer.id}" action="${entry.action}">\n${body}\n</update>`);
421
+ }
422
+ if (blocks.length === 0) {
423
+ return {
424
+ items: [],
425
+ undescribed,
426
+ };
427
+ }
428
+ const text = [
429
+ `<context_updates epoch="${epochId}">`,
430
+ 'These supersede the blocks with the same layer id earlier in this context.',
431
+ 'Where they disagree, these are correct.',
432
+ ...blocks,
433
+ '</context_updates>',
434
+ ].join('\n');
435
+ return {
436
+ items: [
437
+ createMessage(text, 'developer'),
438
+ ],
439
+ undescribed,
440
+ };
441
+ }
442
+ async function renderBody(entry, ctx) {
443
+ if (entry.action === 'retract') {
444
+ return 'This block no longer applies. Disregard it.';
445
+ }
446
+ const custom = await tryRenderDelta(entry, ctx);
447
+ if (custom !== null) {
448
+ return custom;
449
+ }
450
+ const full = itemsToText(entry.next);
451
+ if (full.length > 0) {
452
+ return full;
453
+ }
454
+ // A layer that rendered nothing this turn is saying its block is gone, which
455
+ // is a retraction however it was classified.
456
+ return entry.next.length === 0 ? 'This block no longer applies. Disregard it.' : null;
457
+ }
458
+ async function tryRenderDelta(entry, ctx) {
459
+ const hook = entry.layer.hooks.renderDelta;
460
+ if (!hook) {
461
+ return null;
462
+ }
463
+ try {
464
+ const timeout = entry.layer.timeouts?.recall ?? 5e3;
465
+ const rendered = await withTimeout(hook({
466
+ prev: entry.prev,
467
+ next: entry.next,
468
+ prevState: entry.prevState,
469
+ state: ctx.readLayerState(entry.layer.id),
470
+ ctx,
471
+ budget: entry.budget,
472
+ }), timeout);
473
+ return rendered && rendered.length > 0 ? rendered : null;
474
+ }
475
+ catch {
476
+ // Fall back to republishing in full — see `buildDeltaItems`.
477
+ return null;
478
+ }
479
+ }
480
+ function withTimeout(promise, ms) {
481
+ return Promise.race([
482
+ promise,
483
+ new Promise((_, reject) => setTimeout(() => reject(new Error('renderDelta timed out')), ms)),
484
+ ]);
485
+ }
486
+ //#endregion
487
+ //#region Helpers
488
+ /** Everything before history, as it was assembled before bands existed. */
489
+ function legacyView(recallResults) {
490
+ return {
491
+ anchorItems: recallResults.flatMap((r) => r.items),
492
+ liveItems: [],
493
+ deltaItems: [],
494
+ servedPerLayer: [
495
+ ...recallResults,
496
+ ],
497
+ serveInfo: new Map(),
498
+ epoch: undefined,
499
+ };
500
+ }
501
+ /** A throwaway epoch for a read-only pass on a lineage that has none yet. */
502
+ function emptyEpoch(key, instructionsHash) {
503
+ return {
504
+ id: `${key}#preview`,
505
+ pins: new Map(),
506
+ autoBand: new Map(),
507
+ instructionsHash,
508
+ assemblies: 0,
509
+ anchorTokens: 0,
510
+ deltaTokens: 0,
511
+ cacheBlind: false,
512
+ misses: 0,
513
+ lastReanchorReason: 'cold-start',
514
+ };
515
+ }
516
+ function summarize(epoch, liveTokens, deltaTokens) {
517
+ return {
518
+ id: epoch.id,
519
+ age: epoch.assemblies,
520
+ anchorTokens: epoch.anchorTokens,
521
+ liveTokens,
522
+ deltaTokens,
523
+ reanchorReason: epoch.assemblies <= 1 ? epoch.lastReanchorReason : undefined,
524
+ };
525
+ }
526
+ function itemsToText(items) {
527
+ const parts = [];
528
+ for (const item of items) {
529
+ if (item.type !== 'message' || !('content' in item)) {
530
+ continue;
531
+ }
532
+ for (const part of item.content ?? []) {
533
+ if ('text' in part && typeof part.text === 'string') {
534
+ parts.push(part.text);
535
+ }
536
+ }
537
+ }
538
+ return parts.join('\n');
539
+ }
540
+ function totalTokens(items) {
541
+ let total = 0;
542
+ for (const item of items) {
543
+ total += estimateTokens(JSON.stringify(item));
544
+ }
545
+ return total;
546
+ }
547
+ //#endregion
@@ -2,8 +2,8 @@
2
2
  * Action step handlers: run, llm, spawn, provide, tool.
3
3
  */
4
4
  import type { ItemSchemaRegistry, LayerStateStore } from './action-deps.js';
5
- import type { AgentHarnessContract, Context, ContextLayer, ExecuteStepFn, StepLLM, StepProvide, StepRun, StepSpawn, StepTool } from './action-types.js';
6
- export declare function executeRun<TContext, I, O>(step: StepRun<TContext, I, O>, input: I, ctx: Context<TContext>): Promise<O>;
5
+ import type { AgentHarnessContract, Context, ContextLayer, ExecuteStepFn, StepCallModel, StepInvokeTool, StepRunCode, StepSpawn, StepWithContext } from './action-types.js';
6
+ export declare function executeRunCode<TContext, I, O>(step: StepRunCode<TContext, I, O>, input: I, ctx: Context<TContext>): Promise<O>;
7
7
  /**
8
8
  * Resolves a `Lazy<T>` step field. If the value is a function, it is invoked
9
9
  * with the current context; otherwise the value is returned as-is. Supports
@@ -14,7 +14,7 @@ export declare function executeRun<TContext, I, O>(step: StepRun<TContext, I, O>
14
14
  * tools) without baking them in at build time.
15
15
  */
16
16
  export declare function resolveLazy<T, TContext>(value: T | ((ctx: Context<TContext>) => T | Promise<T>), ctx: Context<TContext>): Promise<T>;
17
- export declare function executeLLM<TContext, I, O>(step: StepLLM<TContext, I, O>, input: I, ctx: Context<TContext>, layers?: ContextLayer[]): Promise<O>;
17
+ export declare function executeCallModel<TContext, I, O>(step: StepCallModel<TContext, I, O>, input: I, ctx: Context<TContext>, layers?: ContextLayer[]): Promise<O>;
18
18
  export interface ExecuteSpawnOpts {
19
19
  layerStore?: LayerStateStore;
20
20
  parentLayers?: ContextLayer[];
@@ -28,5 +28,5 @@ export declare function executeSpawn<TContext, I, O>(step: StepSpawn<TContext, I
28
28
  * Unlike spawn, provide does not create a new itemLog or clone state.
29
29
  * Events flow through to the parent in real-time.
30
30
  */
31
- export declare function executeProvide<TContext, I, O>(step: StepProvide<TContext, I, O>, input: I, ctx: Context<TContext>, executeStep: ExecuteStepFn): Promise<O>;
32
- export declare function executeTool<TContext, I, O>(step: StepTool<TContext, I, O>, input: I, ctx: Context<TContext>, harness: AgentHarnessContract, layers?: ContextLayer[]): Promise<O>;
31
+ export declare function executeWithContext<TContext, I, O>(step: StepWithContext<TContext, I, O>, input: I, ctx: Context<TContext>, executeStep: ExecuteStepFn): Promise<O>;
32
+ export declare function executeInvokeTool<TContext, I, O>(step: StepInvokeTool<TContext, I, O>, input: I, ctx: Context<TContext>, harness: AgentHarnessContract, layers?: ContextLayer[]): Promise<O>;