@noetic-tools/core 5.3.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 (82) 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 +12 -1
  4. package/dist/adapters/openrouter.js +79 -5
  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 +54 -40
  28. package/dist/harness/agent-harness.js +44 -33
  29. package/dist/harness/deps/types.d.ts +1 -1
  30. package/dist/harness/model-call.d.ts +1 -1
  31. package/dist/harness/model-call.js +23 -18
  32. package/dist/index.d.ts +21 -33
  33. package/dist/index.js +10 -26
  34. package/dist/interpreter/action-types.d.ts +1 -1
  35. package/dist/interpreter/collect-tools.js +7 -7
  36. package/dist/interpreter/execute-action.d.ts +5 -5
  37. package/dist/interpreter/execute-action.js +284 -218
  38. package/dist/interpreter/execute-control.d.ts +9 -9
  39. package/dist/interpreter/execute-control.js +27 -27
  40. package/dist/interpreter/execute-sub-harness.d.ts +1 -1
  41. package/dist/interpreter/execute-sub-harness.js +7 -26
  42. package/dist/interpreter/execute.d.ts +1 -1
  43. package/dist/interpreter/execute.js +26 -25
  44. package/dist/interpreter/step-dispatcher.d.ts +1 -1
  45. package/dist/interpreter/step-dispatcher.js +3 -3
  46. package/dist/interpreter/structured-output.d.ts +14 -0
  47. package/dist/interpreter/structured-output.js +40 -0
  48. package/dist/interpreter/typeguards.d.ts +1 -1
  49. package/dist/interpreter/typeguards.js +1 -1
  50. package/dist/portable/builders.d.ts +3 -3
  51. package/dist/portable/builders.js +6 -5
  52. package/dist/portable/context-layers.d.ts +1 -1
  53. package/dist/portable/context-layers.js +1 -1
  54. package/dist/runtime/broadcaster-utils.d.ts +1 -1
  55. package/dist/runtime/broadcaster-utils.js +1 -1
  56. package/dist/runtime/context-impl.d.ts +8 -10
  57. package/dist/runtime/context-impl.js +8 -12
  58. package/dist/runtime/durable/step-ledger.d.ts +2 -2
  59. package/dist/runtime/durable/step-ledger.js +3 -3
  60. package/dist/runtime/session-runner.d.ts +12 -1
  61. package/dist/runtime/session-runner.js +33 -1
  62. package/dist/runtime/tool-context.js +0 -2
  63. package/dist/schemas/workflow.d.ts +3 -3
  64. package/dist/schemas/workflow.js +2 -2
  65. package/dist/types/checkpoint.d.ts +11 -2
  66. package/dist/types/checkpoint.js +10 -1
  67. package/dist/{runtime → util}/detached-handle.js +1 -1
  68. package/package.json +6 -4
  69. package/schema/noetic-workflow.schema.json +24 -11
  70. package/dist/builders/context-option.d.ts +0 -23
  71. package/dist/builders/context-option.js +0 -22
  72. package/dist/deprecated.d.ts +0 -30
  73. package/dist/deprecated.js +0 -28
  74. package/dist/patterns/interview.d.ts +0 -53
  75. package/dist/patterns/interview.js +0 -113
  76. package/dist/patterns/plans.d.ts +0 -44
  77. package/dist/patterns/plans.js +0 -125
  78. package/dist/patterns/ralph-wiggum.d.ts +0 -19
  79. package/dist/patterns/ralph-wiggum.js +0 -37
  80. package/dist/patterns/react.d.ts +0 -19
  81. package/dist/patterns/react.js +0 -41
  82. /package/dist/{runtime → util}/detached-handle.d.ts +0 -0
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * Action step handlers: run, llm, spawn, provide, tool.
3
3
  */
4
- import { createMessage, estimateTokens, extractAssistantText, frameworkCast, isNoeticError, isOutputCodec, NoeticConfigError, NoeticErrorImpl, } from '@noetic-tools/types';
5
- import { ZodError } from 'zod';
4
+ import { createMessage, estimateTokens, extractAssistantText, frameworkCast, isNoeticError, isOutputCodec, NoeticConfigError, NoeticErrorImpl, validateSchema, } from '@noetic-tools/types';
6
5
  import { emitToolUi } from '../runtime/tool-ui.js';
7
6
  import { allocateBudgets, assembleView, buildToolExecutionContext, ContextImpl, commitLayerUsage, computeLayerUsage, contextToExecCtx, createContextCacheStore, DEFAULT_PROJECTION, defaultItemSchemaRegistry, emitFrameworkEvent, getBroadcaster, lineageKey, noteCacheOutcome, resolveCacheConfig, resolveLayerTools, returnLayers, shouldEmit, snapshotCwdState, spawnLayers, } from './action-deps.js';
8
7
  import { isServerToolSpec, SteeringAction } from './action-types.js';
@@ -10,9 +9,10 @@ import { cloneWithGuard } from './clone-guard.js';
10
9
  import { collectAllTools, deduplicateTools } from './collect-tools.js';
11
10
  import { prepareBandedView, stampAnchoringAttributes } from './context-assembly.js';
12
11
  import { trackUsage } from './message-helpers.js';
12
+ import { parseStructuredOutput } from './structured-output.js';
13
13
  import { getContextChannelStore, isContextImpl, isFunctionCall, isMutableContext, } from './typeguards.js';
14
14
  //#region run
15
- export async function executeRun(step, input, ctx) {
15
+ export async function executeRunCode(step, input, ctx) {
16
16
  const retry = step.retry;
17
17
  const maxAttempts = retry?.maxAttempts ?? 1;
18
18
  let lastError = new Error('No attempts executed');
@@ -174,7 +174,7 @@ async function runInputPipeline({ ctx, layers, input, }) {
174
174
  * it is kept off `outputSchema` (which drives JSON-schema formatting).
175
175
  */
176
176
  function resolveOutputMode(output, baseInstructions) {
177
- // A generic type predicate does not reliably narrow a `ZodType<O> |
177
+ // A generic type predicate does not reliably narrow a `StandardSchemaV1 |
178
178
  // OutputCodec<O>` union through a free type parameter, so bridge the two
179
179
  // branches with the sanctioned framework cast — `isOutputCodec` guarantees
180
180
  // the discriminant at runtime.
@@ -223,46 +223,237 @@ function finalizeCodecOutput(codec, lastText, ctx, emit) {
223
223
  });
224
224
  return session.finish(lastText);
225
225
  }
226
- /** JSON-parse + Zod-validate the assistant text, raising `llm_parse_error`. */
227
- function parseSchemaOutput(schema, lastText, stepId) {
228
- try {
229
- return schema.parse(JSON.parse(lastText));
230
- }
231
- catch (e) {
232
- if (e instanceof SyntaxError || e instanceof ZodError) {
233
- throw new NoeticErrorImpl({
234
- kind: 'llm_parse_error',
235
- stepId,
236
- raw: lastText,
237
- schema,
238
- zodError: e instanceof ZodError
239
- ? e
240
- : new ZodError([
241
- {
242
- code: 'custom',
243
- message: `Invalid JSON: ${e.message}`,
244
- path: [],
245
- },
246
- ]),
247
- });
248
- }
249
- throw e;
250
- }
251
- }
252
226
  /**
253
227
  * Turn the finished assistant text into the step's typed output: a streaming
254
- * codec, a Zod schema, or raw text passthrough.
228
+ * codec, a Standard Schema, or raw text passthrough.
255
229
  */
256
- function finalizeStepOutput(params) {
230
+ async function finalizeStepOutput(params) {
257
231
  if (params.outputCodec) {
258
232
  return finalizeCodecOutput(params.outputCodec, params.lastText, params.ctx, params.emit);
259
233
  }
260
234
  if (params.outputSchema) {
261
- return parseSchemaOutput(params.outputSchema, params.lastText, params.stepId);
235
+ return parseStructuredOutput({
236
+ schema: params.outputSchema,
237
+ rawText: params.lastText,
238
+ stepId: params.stepId,
239
+ });
262
240
  }
263
241
  return frameworkCast(params.lastText);
264
242
  }
265
- export async function executeLLM(step, input, ctx, layers) {
243
+ /**
244
+ * `step.tools` is heterogeneous: client `Tool`s plus inline OpenRouter
245
+ * server-tool specs (web search/fetch). Partition them — client tools flow
246
+ * through the unified-tool / allowedToolNames / steering machinery as before;
247
+ * server-tool specs bypass it entirely and are stamped onto the model request
248
+ * (`_serverTools`) for the model caller to wrap via the SDK's `serverTool()`.
249
+ * `undefined` (unrestricted) is preserved as-is; only a present array is
250
+ * partitioned so the `[] = opt-out` semantics of step.tools stay intact.
251
+ */
252
+ function partitionStepTools(resolvedStepTools) {
253
+ if (resolvedStepTools === undefined) {
254
+ return {
255
+ clientStepTools: undefined,
256
+ serverToolSpecs: [],
257
+ };
258
+ }
259
+ const clientStepTools = [];
260
+ const serverToolSpecs = [];
261
+ for (const entry of resolvedStepTools) {
262
+ if (isServerToolSpec(entry)) {
263
+ serverToolSpecs.push(entry);
264
+ continue;
265
+ }
266
+ clientStepTools.push(entry);
267
+ }
268
+ return {
269
+ clientStepTools,
270
+ serverToolSpecs,
271
+ };
272
+ }
273
+ /**
274
+ * Append user input — through layer pipeline if layers exist, otherwise direct.
275
+ * The append pipeline may request a context re-render (e.g. a layer that
276
+ * expanded an input reference into new content); applied after recall.
277
+ */
278
+ async function appendUserInput(params) {
279
+ const { ctx, layers, input } = params;
280
+ if (typeof input !== 'string' || input.length === 0) {
281
+ return [];
282
+ }
283
+ if (layers !== undefined && layers.length > 0) {
284
+ return await runInputPipeline({
285
+ ctx,
286
+ layers,
287
+ input,
288
+ });
289
+ }
290
+ ctx.itemLog.append(createMessage(input, 'user'));
291
+ return [];
292
+ }
293
+ /**
294
+ * Splits the context budget across layers via `allocateBudgets`. An empty map
295
+ * when the step runs without layers.
296
+ */
297
+ function computeBudgetMap(params) {
298
+ const { layers, policy, systemPromptTokens } = params;
299
+ if (layers === undefined || layers.length === 0) {
300
+ return new Map();
301
+ }
302
+ const { allocations } = allocateBudgets({
303
+ layers,
304
+ totalBudget: policy.tokenBudget,
305
+ systemPromptTokens,
306
+ responseReserve: policy.responseReserve,
307
+ });
308
+ return new Map(allocations.map((a) => [
309
+ a.layerId,
310
+ a.allocated,
311
+ ]));
312
+ }
313
+ /**
314
+ * Recall once per LLM step: atomic layers run in the hot path; eventual layers
315
+ * are served from cache. Results drive both the assembled context window and
316
+ * the per-layer usage breakdown (ctx.lastLayerUsage). Recall fires before the
317
+ * steering retry loop because retries replay the same context. Re-render
318
+ * requests collected from the input-append pipeline are applied on top.
319
+ */
320
+ async function gatherRecallResults(params) {
321
+ const { ctx, layers, recallQuery, budgetMap, slotOf, rerenderRequests } = params;
322
+ if (layers === undefined || layers.length === 0) {
323
+ return emptyRecall;
324
+ }
325
+ const atomic = await ctx.harness.recallLayersAtomic(layers, recallQuery, ctx, budgetMap);
326
+ const eventual = await ctx.harness.recallLayersEventual(layers, recallQuery, ctx, budgetMap);
327
+ let recallResults = [
328
+ ...atomic,
329
+ ...eventual,
330
+ ].sort((a, b) => (slotOf.get(a.layerId) ?? 0) - (slotOf.get(b.layerId) ?? 0));
331
+ if (rerenderRequests.length > 0) {
332
+ const rerendered = await ctx.harness.executeRerender(rerenderRequests, layers, ctx, budgetMap, recallQuery);
333
+ recallResults = mergeRecallResults([
334
+ ...recallResults,
335
+ ], rerendered, slotOf);
336
+ }
337
+ return recallResults;
338
+ }
339
+ /**
340
+ * Builds the items the model sees for one attempt: system messages stay at the
341
+ * front, the banded anchor/live/delta output rides between them and history,
342
+ * and steering guidance from a prior retry lands at the very tail. The
343
+ * projector enforces the token budget (drops highest-slot layer output, then
344
+ * oldest history).
345
+ */
346
+ async function assembleTurnItems(params) {
347
+ const { ctx, layers, banded, steeringTail, viewPolicy } = params;
348
+ const rawHistoryItems = ctx.itemLog.items;
349
+ if (layers === undefined || layers.length === 0) {
350
+ return rawHistoryItems;
351
+ }
352
+ const projectedHistoryItems = await ctx.harness.projectHistory(layers, rawHistoryItems, ctx);
353
+ const systemItems = [];
354
+ const nonSystemHistory = [];
355
+ const tailIds = new Set(steeringTail.map((i) => i.id));
356
+ for (const item of projectedHistoryItems) {
357
+ if (item.type === 'message' && item.role === 'system') {
358
+ systemItems.push(item);
359
+ continue;
360
+ }
361
+ // Steering guidance is re-placed at the tail; skip it here so the retry
362
+ // does not see the same correction twice.
363
+ if ('id' in item && typeof item.id === 'string' && tailIds.has(item.id)) {
364
+ continue;
365
+ }
366
+ nonSystemHistory.push(item);
367
+ }
368
+ return assembleView({
369
+ systemPromptItems: systemItems,
370
+ layerOutputItems: banded.anchorItems,
371
+ historyItems: nonSystemHistory,
372
+ liveLayerItems: banded.liveItems,
373
+ deltaItems: banded.deltaItems,
374
+ tailItems: steeringTail,
375
+ policy: viewPolicy,
376
+ });
377
+ }
378
+ /**
379
+ * Builds the callModel request for one attempt. Cancellation reaches INSIDE
380
+ * the model call, not just the step boundary: the caller's abort signal stops
381
+ * the stream and the tool-round loop mid flight, so `ctx.abort()` /
382
+ * `harness.cancel()` don't wait out a long generation before taking effect
383
+ * (spec 09, Cancellation item 2).
384
+ */
385
+ function buildModelRequest(params) {
386
+ const { step, ctx, layers } = params;
387
+ const _serverTools = params.serverToolSpecs.length > 0 ? params.serverToolSpecs : undefined;
388
+ const signal = isContextImpl(ctx) ? ctx.abortSignal : undefined;
389
+ if (params.resolvedTools) {
390
+ return {
391
+ model: params.resolvedModel,
392
+ items: params.assembledItems,
393
+ instructions: params.resolvedInstructions,
394
+ tools: params.resolvedTools,
395
+ params: step.params,
396
+ outputSchema: params.outputSchema,
397
+ outputJsonSchema: params.step.outputJsonSchema,
398
+ emit: step.emit,
399
+ _serverTools,
400
+ ctx,
401
+ layers,
402
+ allowedToolNames: params.allowedToolNames,
403
+ nodeId: step.id,
404
+ parentSpan: ctx.span,
405
+ signal,
406
+ };
407
+ }
408
+ return {
409
+ model: params.resolvedModel,
410
+ items: params.assembledItems,
411
+ instructions: params.resolvedInstructions,
412
+ params: step.params,
413
+ outputSchema: params.outputSchema,
414
+ outputJsonSchema: params.step.outputJsonSchema,
415
+ emit: step.emit,
416
+ _serverTools,
417
+ nodeId: step.id,
418
+ parentSpan: ctx.span,
419
+ signal,
420
+ };
421
+ }
422
+ /**
423
+ * Records a finished model response on the context: appends response items to
424
+ * the log, publishes `lastStepMeta`, and commits the per-layer usage breakdown
425
+ * (what the model was actually shown, pinned replays included — raw recall
426
+ * output would report content it never saw).
427
+ */
428
+ function recordTurnOutcome(params) {
429
+ const { ctx, response, banded } = params;
430
+ const toolCalls = [];
431
+ for (const item of response.items) {
432
+ ctx.itemLog.append(item);
433
+ if (isFunctionCall(item)) {
434
+ toolCalls.push(item);
435
+ }
436
+ }
437
+ const meta = {
438
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
439
+ usage: response.usage,
440
+ cost: response.cost,
441
+ responseItems: response.items,
442
+ };
443
+ if (isMutableContext(ctx)) {
444
+ ctx.lastStepMeta = meta;
445
+ }
446
+ commitLayerUsage(ctx, computeLayerUsage({
447
+ ctx,
448
+ modelId: params.resolvedModel,
449
+ instructions: params.resolvedInstructions,
450
+ tools: params.resolvedTools,
451
+ recallResults: banded.servedPerLayer,
452
+ serveInfo: banded.serveInfo,
453
+ epoch: banded.epoch,
454
+ }));
455
+ }
456
+ export async function executeCallModel(step, input, ctx, layers) {
266
457
  const baseCtx = frameworkCast(ctx);
267
458
  const hasLayers = layers !== undefined && layers.length > 0;
268
459
  // Resolve lazy step fields up-front. Function-form getters run once per
@@ -272,49 +463,19 @@ export async function executeLLM(step, input, ctx, layers) {
272
463
  if (!resolvedModel || resolvedModel.trim() === '') {
273
464
  throw new NoeticConfigError({
274
465
  code: 'MISSING_MODEL',
275
- message: `step.llm(${JSON.stringify(step.id)}) resolved model to an empty string.`,
466
+ message: `callModel(${JSON.stringify(step.id)}) resolved model to an empty string.`,
276
467
  hint: "Ensure `model` (or your `(ctx) => string` getter) returns a non-empty identifier, e.g. 'anthropic/claude-sonnet-4-20250514'.",
277
468
  });
278
469
  }
279
470
  const baseInstructions = await resolveLazy(step.instructions, ctx);
280
471
  const { outputCodec, outputSchema, resolvedInstructions } = resolveOutputMode(step.output, baseInstructions);
281
472
  const resolvedStepTools = await resolveLazy(step.tools, ctx);
282
- // `step.tools` is heterogeneous: client `Tool`s plus inline OpenRouter
283
- // server-tool specs (web search/fetch). Partition them — client tools flow
284
- // through the unified-tool / allowedToolNames / steering machinery as before;
285
- // server-tool specs bypass it entirely and are stamped onto the model request
286
- // (`_serverTools`) for the model caller to wrap via the SDK's `serverTool()`.
287
- // `undefined` (unrestricted) is preserved as-is; only a present array is
288
- // partitioned so the `[] = opt-out` semantics of step.tools stay intact.
289
- let clientStepTools;
290
- const serverToolSpecs = [];
291
- if (resolvedStepTools !== undefined) {
292
- clientStepTools = [];
293
- for (const entry of resolvedStepTools) {
294
- if (isServerToolSpec(entry)) {
295
- serverToolSpecs.push(entry);
296
- }
297
- else {
298
- clientStepTools.push(entry);
299
- }
300
- }
301
- }
302
- // Append user input — through layer pipeline if layers exist, otherwise direct.
303
- // The append pipeline may request a context re-render (e.g. a layer that
304
- // expanded an input reference into new content); applied after recall below.
305
- let rerenderRequests = [];
306
- if (typeof input === 'string' && input.length > 0) {
307
- if (hasLayers) {
308
- rerenderRequests = await runInputPipeline({
309
- ctx: baseCtx,
310
- layers,
311
- input,
312
- });
313
- }
314
- else {
315
- baseCtx.itemLog.append(createMessage(input, 'user'));
316
- }
317
- }
473
+ const { clientStepTools, serverToolSpecs } = partitionStepTools(resolvedStepTools);
474
+ const rerenderRequests = await appendUserInput({
475
+ ctx: baseCtx,
476
+ layers,
477
+ input,
478
+ });
318
479
  const { tools: resolvedTools, allowedToolNames } = resolveToolsAndRestrictions(clientStepTools, layers, baseCtx);
319
480
  // Resolve the projection policy (step override > harness default > fallback)
320
481
  // and split the context budget across layers via allocateBudgets.
@@ -327,44 +488,24 @@ export async function executeLLM(step, input, ctx, layers) {
327
488
  ...policy,
328
489
  tokenBudget: Math.max(0, policy.tokenBudget - systemPromptTokens),
329
490
  };
330
- let budgetMap = new Map();
331
- if (hasLayers) {
332
- const { allocations } = allocateBudgets({
333
- layers,
334
- totalBudget: policy.tokenBudget,
335
- systemPromptTokens,
336
- responseReserve: policy.responseReserve,
337
- });
338
- budgetMap = new Map(allocations.map((a) => [
339
- a.layerId,
340
- a.allocated,
341
- ]));
342
- }
343
- // Recall once per LLM step: atomic layers run in the hot path; eventual layers
344
- // are served from cache. Results drive both the assembled context window and
345
- // the per-layer usage breakdown (ctx.lastLayerUsage). Recall fires before the
346
- // steering retry loop because retries replay the same context.
491
+ const budgetMap = computeBudgetMap({
492
+ layers,
493
+ policy,
494
+ systemPromptTokens,
495
+ });
347
496
  const recallQuery = typeof input === 'string' ? input : '';
348
497
  const slotOf = new Map((layers ?? []).map((l) => [
349
498
  l.id,
350
499
  l.slot,
351
500
  ]));
352
- let recallResults = emptyRecall;
353
- if (hasLayers) {
354
- const atomic = await baseCtx.harness.recallLayersAtomic(layers, recallQuery, baseCtx, budgetMap);
355
- const eventual = await baseCtx.harness.recallLayersEventual(layers, recallQuery, baseCtx, budgetMap);
356
- recallResults = [
357
- ...atomic,
358
- ...eventual,
359
- ].sort((a, b) => (slotOf.get(a.layerId) ?? 0) - (slotOf.get(b.layerId) ?? 0));
360
- // Apply re-render requests collected from the input-append pipeline.
361
- if (rerenderRequests.length > 0) {
362
- const rerendered = await baseCtx.harness.executeRerender(rerenderRequests, layers, baseCtx, budgetMap, recallQuery);
363
- recallResults = mergeRecallResults([
364
- ...recallResults,
365
- ], rerendered, slotOf);
366
- }
367
- }
501
+ const recallResults = await gatherRecallResults({
502
+ ctx: baseCtx,
503
+ layers,
504
+ recallQuery,
505
+ budgetMap,
506
+ slotOf,
507
+ rerenderRequests,
508
+ });
368
509
  // Split recall output into the cache-stable anchor band and the live band,
369
510
  // replaying pinned anchors and gathering any changes into one supersede
370
511
  // message. Runs once, outside the retry loop, for the same reason recall does:
@@ -391,82 +532,25 @@ export async function executeLLM(step, input, ctx, layers) {
391
532
  let steeringTail = [];
392
533
  let cacheJudged = false;
393
534
  while (retries <= MAX_STEERING_RETRIES) {
394
- const rawHistoryItems = baseCtx.itemLog.items;
395
- const projectedHistoryItems = hasLayers
396
- ? await baseCtx.harness.projectHistory(layers, rawHistoryItems, baseCtx)
397
- : rawHistoryItems;
398
- let assembledItems;
399
- if (hasLayers) {
400
- // Keep system messages at the front; the projector enforces the token
401
- // budget (drops highest-slot layer output, then oldest history).
402
- const systemItems = [];
403
- const nonSystemHistory = [];
404
- const tailIds = new Set(steeringTail.map((i) => i.id));
405
- for (const item of projectedHistoryItems) {
406
- if (item.type === 'message' && item.role === 'system') {
407
- systemItems.push(item);
408
- continue;
409
- }
410
- // Steering guidance is re-placed at the tail; skip it here so the retry
411
- // does not see the same correction twice.
412
- if ('id' in item && typeof item.id === 'string' && tailIds.has(item.id)) {
413
- continue;
414
- }
415
- nonSystemHistory.push(item);
416
- }
417
- assembledItems = assembleView({
418
- systemPromptItems: systemItems,
419
- layerOutputItems: banded.anchorItems,
420
- historyItems: nonSystemHistory,
421
- liveLayerItems: banded.liveItems,
422
- deltaItems: banded.deltaItems,
423
- tailItems: steeringTail,
424
- policy: viewPolicy,
425
- });
426
- }
427
- else {
428
- assembledItems =
429
- projectedHistoryItems === rawHistoryItems
430
- ? rawHistoryItems
431
- : [
432
- ...projectedHistoryItems,
433
- ];
434
- }
435
- const _serverTools = serverToolSpecs.length > 0 ? serverToolSpecs : undefined;
436
- // Cancellation reaches INSIDE the model call, not just the step boundary:
437
- // the caller's abort signal stops the stream and the tool-round loop mid
438
- // flight, so `ctx.abort()` / `harness.cancel()` don't wait out a long
439
- // generation before taking effect (spec 09, Cancellation item 2).
440
- const signal = isContextImpl(baseCtx) ? baseCtx.abortSignal : undefined;
441
- const request = resolvedTools
442
- ? {
443
- model: resolvedModel,
444
- items: assembledItems,
445
- instructions: resolvedInstructions,
446
- tools: resolvedTools,
447
- params: step.params,
448
- outputSchema: outputSchema,
449
- emit: step.emit,
450
- _serverTools,
451
- ctx: baseCtx,
452
- layers,
453
- allowedToolNames,
454
- nodeId: step.id,
455
- parentSpan: baseCtx.span,
456
- signal,
457
- }
458
- : {
459
- model: resolvedModel,
460
- items: assembledItems,
461
- instructions: resolvedInstructions,
462
- params: step.params,
463
- outputSchema: outputSchema,
464
- emit: step.emit,
465
- _serverTools,
466
- nodeId: step.id,
467
- parentSpan: baseCtx.span,
468
- signal,
469
- };
535
+ const assembledItems = await assembleTurnItems({
536
+ ctx: baseCtx,
537
+ layers,
538
+ banded,
539
+ steeringTail,
540
+ viewPolicy,
541
+ });
542
+ const request = buildModelRequest({
543
+ step,
544
+ ctx: baseCtx,
545
+ layers,
546
+ resolvedModel,
547
+ resolvedInstructions,
548
+ resolvedTools,
549
+ allowedToolNames,
550
+ outputSchema,
551
+ serverToolSpecs,
552
+ assembledItems,
553
+ });
470
554
  const response = await baseCtx.harness.callModel(request);
471
555
  // Track tokens/cost for EVERY model call, including responses steering
472
556
  // subsequently rejects (Deny throw / Guide retry below) — the spend is
@@ -522,33 +606,14 @@ export async function executeLLM(step, input, ctx, layers) {
522
606
  continue;
523
607
  }
524
608
  }
525
- const toolCalls = [];
526
- for (const item of response.items) {
527
- baseCtx.itemLog.append(item);
528
- if (isFunctionCall(item)) {
529
- toolCalls.push(item);
530
- }
531
- }
532
- const meta = {
533
- toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
534
- usage: response.usage,
535
- cost: response.cost,
536
- responseItems: response.items,
537
- };
538
- if (isMutableContext(baseCtx)) {
539
- baseCtx.lastStepMeta = meta;
540
- }
541
- commitLayerUsage(baseCtx, computeLayerUsage({
609
+ recordTurnOutcome({
542
610
  ctx: baseCtx,
543
- modelId: resolvedModel,
544
- instructions: resolvedInstructions,
545
- tools: resolvedTools,
546
- // What the model was actually shown, pinned replays included — raw
547
- // recall output would report content it never saw.
548
- recallResults: banded.servedPerLayer,
549
- serveInfo: banded.serveInfo,
550
- epoch: banded.epoch,
551
- }));
611
+ response,
612
+ resolvedModel,
613
+ resolvedInstructions,
614
+ resolvedTools,
615
+ banded,
616
+ });
552
617
  if (hasLayers) {
553
618
  await baseCtx.harness.storeLayers(layers, response, baseCtx);
554
619
  }
@@ -642,7 +707,7 @@ export async function executeSpawn(step, input, ctx, executeStep, opts) {
642
707
  // Build unified tool set for child from its step tree + layers, plus the
643
708
  // parent's unified tools. Sub-agents that read tools dynamically (via
644
709
  // `ctx.unifiedTools`) otherwise lose the harness tool pool at the spawn
645
- // boundary — a child whose `step.llm` resolves `tools` from context would
710
+ // boundary — a child whose `callModel` resolves `tools` from context would
646
711
  // see nothing. Inheriting the parent's tools keeps the harness toolset
647
712
  // available across spawns; the child's own step/layer tools take precedence
648
713
  // on name collision (dedup keeps the first occurrence), and children that
@@ -737,7 +802,7 @@ function mergeLayers(existing, provided) {
737
802
  * Unlike spawn, provide does not create a new itemLog or clone state.
738
803
  * Events flow through to the parent in real-time.
739
804
  */
740
- export async function executeProvide(step, input, ctx, executeStep) {
805
+ export async function executeWithContext(step, input, ctx, executeStep) {
741
806
  const baseCtx = frameworkCast(ctx);
742
807
  const previousLayers = baseCtx.layers;
743
808
  const previousUnifiedTools = baseCtx.unifiedTools;
@@ -792,7 +857,7 @@ async function consumeToolGenerator(params) {
792
857
  event: next.value,
793
858
  },
794
859
  });
795
- // A direct `step.tool` has no model call id — the step id keys its region.
860
+ // A direct `invokeTool` has no model call id — the step id keys its region.
796
861
  emitToolUi({
797
862
  ctx: params.ctx,
798
863
  tool: params.tool,
@@ -803,20 +868,21 @@ async function consumeToolGenerator(params) {
803
868
  });
804
869
  }
805
870
  }
806
- export async function executeTool(step, input, ctx, harness, layers) {
871
+ export async function executeInvokeTool(step, input, ctx, harness, layers) {
807
872
  const baseCtx = frameworkCast(ctx);
808
873
  const args = step.args ? Object.assign({}, input, step.args) : input;
809
- const parseResult = step.tool.input.safeParse(args);
874
+ const parseResult = await validateSchema(step.tool.input, args);
810
875
  if (!parseResult.success) {
811
876
  throw new NoeticErrorImpl({
812
877
  kind: 'step_failed',
813
878
  stepId: step.id,
814
- cause: new Error(`Tool input validation failed: ${parseResult.error.message}`),
879
+ cause: new Error(`Tool input validation failed: ${parseResult.zodError.message}`),
815
880
  retriesExhausted: false,
816
881
  });
817
882
  }
883
+ const parsedArgs = parseResult.value;
818
884
  if (layers && layers.length > 0) {
819
- const decision = await harness.beforeToolCall(layers, step.tool.name, parseResult.data, baseCtx);
885
+ const decision = await harness.beforeToolCall(layers, step.tool.name, parsedArgs, baseCtx);
820
886
  if (decision.action !== SteeringAction.Allow) {
821
887
  throw new NoeticErrorImpl({
822
888
  kind: 'steering_denied',
@@ -829,18 +895,18 @@ export async function executeTool(step, input, ctx, harness, layers) {
829
895
  tool: step.tool,
830
896
  callId: step.id,
831
897
  phase: 'call',
832
- args: parseResult.data,
898
+ args: parsedArgs,
833
899
  });
834
900
  try {
835
901
  const toolCtx = buildToolExecutionContext(baseCtx, harness);
836
- const execResult = step.tool.execute(parseResult.data, toolCtx);
902
+ const execResult = step.tool.execute(parsedArgs, toolCtx);
837
903
  const result = isAsyncGenerator(execResult)
838
904
  ? await consumeToolGenerator({
839
905
  generator: execResult,
840
906
  stepId: step.id,
841
907
  tool: step.tool,
842
908
  ctx: baseCtx,
843
- args: parseResult.data,
909
+ args: parsedArgs,
844
910
  })
845
911
  : await execResult;
846
912
  emitToolUi({
@@ -848,7 +914,7 @@ export async function executeTool(step, input, ctx, harness, layers) {
848
914
  tool: step.tool,
849
915
  callId: step.id,
850
916
  phase: 'result',
851
- args: parseResult.data,
917
+ args: parsedArgs,
852
918
  output: result,
853
919
  });
854
920
  return frameworkCast(result);
@@ -859,7 +925,7 @@ export async function executeTool(step, input, ctx, harness, layers) {
859
925
  tool: step.tool,
860
926
  callId: step.id,
861
927
  phase: 'error',
862
- args: parseResult.data,
928
+ args: parsedArgs,
863
929
  error: e,
864
930
  });
865
931
  if (e instanceof NoeticErrorImpl) {
@@ -1,26 +1,26 @@
1
1
  /**
2
- * Control-flow step handlers: branch, fork, loop, every.
2
+ * Control-flow step handlers: conditional, inParallel, loop, schedule.
3
3
  */
4
- import type { Context, ExecuteStepFn, StepBranch, StepEvery, StepFork, StepLoop } from '@noetic-tools/types';
4
+ import type { Context, ExecuteStepFn, StepConditional, StepInParallel, StepLoop, StepSchedule } from '@noetic-tools/types';
5
5
  import type { ItemSchemaRegistry, LayerStateStore } from './action-deps.js';
6
- export declare function executeBranch<TContext, I, O>(step: StepBranch<TContext, I, O>, input: I, ctx: Context<TContext>, executeStep: ExecuteStepFn): Promise<O>;
6
+ export declare function executeConditional<TContext, I, O>(step: StepConditional<TContext, I, O>, input: I, ctx: Context<TContext>, executeStep: ExecuteStepFn): Promise<O>;
7
7
  /** Layer-boundary wiring supplied by the interpreter (see `buildSpawnOpts`). */
8
8
  export interface ExecuteForkOpts {
9
9
  layerStore?: LayerStateStore;
10
10
  itemSchemas?: ItemSchemaRegistry;
11
11
  }
12
- export declare function executeFork<TContext, I, O>(step: StepFork<TContext, I, O>, input: I, ctx: Context<TContext>, rawExecuteStep: ExecuteStepFn, opts?: ExecuteForkOpts): Promise<O>;
12
+ export declare function executeInParallel<TContext, I, O>(step: StepInParallel<TContext, I, O>, input: I, ctx: Context<TContext>, rawExecuteStep: ExecuteStepFn, opts?: ExecuteForkOpts): Promise<O>;
13
13
  export declare function executeLoop<TContext, I, O>(step: StepLoop<TContext, I, O>, input: I, ctx: Context<TContext>, executeStep: ExecuteStepFn): Promise<O>;
14
14
  /**
15
- * Executes an `every` step: runs the body step forever, paced by `ms ± jitter`,
16
- * woken early by `wakeOn`. Throws `cancelled` when the context is aborted.
15
+ * Executes a `schedule` step: runs the body step forever, paced by `ms ± jitter`,
16
+ * woken early by `inbox`. Throws `cancelled` when the context is aborted.
17
17
  *
18
18
  * On body throw, behavior depends on `onError`:
19
- * - `'continue'` (default): emit `every.iteration.error` span event and proceed
19
+ * - `'continue'` (default): emit `schedule.iteration.error` span event and proceed
20
20
  * to the park step as if no error occurred.
21
21
  * - `'fail'`: re-throw, terminating the operator.
22
22
  *
23
- * The body's output is discarded — `every` runs forever and does not accumulate
23
+ * The body's output is discarded — `schedule` runs forever and does not accumulate
24
24
  * iteration outputs. Only ever returns by throwing on cancellation or `fail`.
25
25
  */
26
- export declare function executeEvery<TContext, I, O>(step: StepEvery<TContext, I, O>, input: I, ctx: Context<TContext>, executeStep: ExecuteStepFn): Promise<O>;
26
+ export declare function executeSchedule<TContext, I, O>(step: StepSchedule<TContext, I, O>, input: I, ctx: Context<TContext>, executeStep: ExecuteStepFn): Promise<O>;