@noetic-tools/core 5.7.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 (63) hide show
  1. package/dist/adapters/openrouter.d.ts +1 -0
  2. package/dist/adapters/openrouter.js +1 -0
  3. package/dist/builders/channel-builder.d.ts +18 -0
  4. package/dist/builders/control-flow-builders.d.ts +17 -30
  5. package/dist/builders/decide-builders.d.ts +5 -5
  6. package/dist/builders/decide-builders.js +4 -0
  7. package/dist/builders/every.d.ts +2 -2
  8. package/dist/builders/every.js +2 -0
  9. package/dist/builders/loop-builder.d.ts +1 -1
  10. package/dist/builders/loop-builder.js +1 -1
  11. package/dist/builders/provide-builder.d.ts +3 -2
  12. package/dist/builders/provide-builder.js +3 -0
  13. package/dist/builders/spawn-builder.d.ts +2 -3
  14. package/dist/builders/spawn-builder.js +2 -1
  15. package/dist/builders/step-builders.d.ts +8 -9
  16. package/dist/builders/step-builders.js +0 -1
  17. package/dist/builders/tool-builder.d.ts +2 -0
  18. package/dist/builders/tool-builder.js +2 -0
  19. package/dist/builders/workflow-hydrator.js +21 -14
  20. package/dist/builders/workflow-replay-key.d.ts +15 -0
  21. package/dist/builders/workflow-replay-key.js +46 -0
  22. package/dist/harness/agent-harness.d.ts +43 -11
  23. package/dist/harness/agent-harness.js +121 -40
  24. package/dist/harness/deps/interpreter.d.ts +1 -0
  25. package/dist/harness/deps/interpreter.js +1 -0
  26. package/dist/harness/deps/runtime.d.ts +3 -2
  27. package/dist/harness/deps/runtime.js +2 -2
  28. package/dist/harness/model-call.d.ts +3 -0
  29. package/dist/harness/model-call.js +19 -5
  30. package/dist/index.d.ts +7 -5
  31. package/dist/index.js +3 -3
  32. package/dist/interpreter/action-deps.d.ts +1 -1
  33. package/dist/interpreter/action-deps.js +1 -1
  34. package/dist/interpreter/child-span.d.ts +17 -0
  35. package/dist/interpreter/child-span.js +51 -0
  36. package/dist/interpreter/collect-tools.js +19 -54
  37. package/dist/interpreter/execute-action.js +30 -17
  38. package/dist/interpreter/execute-control.js +67 -9
  39. package/dist/interpreter/execute-decide.js +9 -27
  40. package/dist/interpreter/execute.d.ts +10 -0
  41. package/dist/interpreter/execute.js +69 -46
  42. package/dist/interpreter/model-error.d.ts +9 -0
  43. package/dist/interpreter/model-error.js +32 -0
  44. package/dist/interpreter/step-walk.d.ts +11 -0
  45. package/dist/interpreter/step-walk.js +41 -0
  46. package/dist/observability/genai-attributes.d.ts +52 -29
  47. package/dist/observability/genai-attributes.js +27 -1
  48. package/dist/runtime/channel-store.d.ts +73 -1
  49. package/dist/runtime/channel-store.js +179 -63
  50. package/dist/runtime/context-impl.d.ts +36 -32
  51. package/dist/runtime/context-impl.js +80 -45
  52. package/dist/runtime/durable/harness-checkpoints.d.ts +33 -16
  53. package/dist/runtime/durable/harness-checkpoints.js +74 -52
  54. package/dist/runtime/durable/step-ledger.d.ts +15 -5
  55. package/dist/runtime/durable/step-ledger.js +23 -6
  56. package/dist/schemas/workflow.d.ts +1 -1
  57. package/dist/types/checkpoint.d.ts +31 -0
  58. package/dist/types/checkpoint.js +29 -0
  59. package/dist/until/combinators.d.ts +10 -0
  60. package/dist/until/combinators.js +37 -13
  61. package/dist/until/predicates.js +4 -2
  62. package/package.json +3 -3
  63. package/schema/noetic-workflow.schema.json +432 -0
@@ -3,6 +3,7 @@ import { AcpSessionStore } from '../runtime/acp-session-store.js';
3
3
  import type { LayerStateStore, RecallCache } from './deps/context.js';
4
4
  import type { DetachedSpawnOverrides } from './deps/interpreter.js';
5
5
  import type { CheckpointStore, EventBroadcaster, RestoreCheckpointOptions, StepLedgerRetention, StepLedgerStore } from './deps/runtime.js';
6
+ import { ChannelStore } from './deps/runtime.js';
6
7
  import type { AcpLiveSession, AcpSessionInfo, AgentConfig, AgentHarnessContract, AgentHooks, CallModelRequest, Channel, ChannelHandle, Context, ContextCacheConfig, ContextCacheStore, ContextData, ContextLayer, CwdState, DecideRequest, DecideResponse, DecisionRecord, DeliveryMode, DetachedHandle, ExecuteInput, ExecuteOptions, ExternalChannel, FsAdapter, HarnessResponse, HarnessStatus, Item, ItemSchemaConfig, LLMResponse, LlmProviderConfig, ProjectionPolicy, RecallLayerOutput, RecallOptions, SessionScope, SessionUsage, ShellAdapter, Span, SteeringDecision, Step, StorageAdapter, StreamEvent, StreamingItem, SubprocessAdapter, Tool, TraceExporter, ZodType } from './deps/types.js';
7
8
  export { createStreamIdleWatchdog } from './model-call.js';
8
9
  /**
@@ -163,9 +164,13 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
163
164
  private readonly decideOverride?;
164
165
  /** Model id → whether it answers natively as a decision model. */
165
166
  private readonly decisionModels;
166
- private readonly channelStore;
167
- /** Re-entrant `run()` depth per root context id — see `executeClosingChannels`. */
168
- private readonly rootRunDepth;
167
+ /** @internal Channel state; read by the checkpoint functions. */
168
+ readonly channelStore: ChannelStore;
169
+ /**
170
+ * Root contexts with a `run()` in flight, keyed by id, with their
171
+ * re-entrant run depth — see `executeClosingChannels`.
172
+ */
173
+ private readonly liveRoots;
169
174
  private readonly callModelOverride?;
170
175
  private readonly defaultDeliveryMode;
171
176
  private readonly sessions;
@@ -218,6 +223,12 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
218
223
  private readonly initializedExecutions;
219
224
  /** Executions restored from a checkpoint: their first layer init keeps the restored state. */
220
225
  private readonly restoredExecutions;
226
+ /**
227
+ * Layer hooks still running per context. `cancel()` waits for them before
228
+ * `onComplete`/`dispose`, so teardown never races a half-finished `store`
229
+ * or `recall`. Each hook is bounded by its own timeout.
230
+ */
231
+ private readonly runningHooks;
221
232
  readonly traceExporter: TraceExporter;
222
233
  /**
223
234
  * Long-lived shared cwd state. The same reference is seeded into every
@@ -256,6 +267,13 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
256
267
  decide(request: DecideRequest): Promise<DecideResponse>;
257
268
  notifyDecision(layers: ContextLayer[], record: DecisionRecord, ctx: Context): Promise<void>;
258
269
  run<I, O>(s: Step<ContextData, I, O>, input: I, ctx: Context): Promise<O>;
270
+ /**
271
+ * Whether `step` calls a model this harness has no credentials for. Checked
272
+ * before any step runs so a missing provider fails fast instead of after
273
+ * earlier steps' side effects. Steps the walk cannot see (dynamic branches)
274
+ * still hit the same error lazily at their model call.
275
+ */
276
+ private lacksModelFor;
259
277
  /** Run all layer `init` hooks before the first step executes. Per spec 11,
260
278
  * init MUST complete before any recall fires so layer state is populated. */
261
279
  private initAndRun;
@@ -296,11 +314,11 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
296
314
  cwdInit?: string;
297
315
  _broadcaster?: EventBroadcaster;
298
316
  }): Context;
299
- send<T>(channel: Channel<T>, value: T, ctx: Context): Promise<void>;
317
+ send<T>(channel: Channel<T>, value: NoInfer<T>, ctx: Context): Promise<void>;
300
318
  recv<T>(channel: Channel<T>, ctx: Context, opts?: {
301
319
  timeout?: number;
302
320
  }): Promise<T>;
303
- tryRecv<T>(channel: Channel<T>, _ctx: Context): T | null;
321
+ tryRecv<T>(channel: Channel<T>, ctx: Context): T | null;
304
322
  getChannelHandle<T>(channel: ExternalChannel<T>, executionId: string): ChannelHandle<T>;
305
323
  getChannelStream<T>(channel: ExternalChannel<T>, executionId: string): AsyncIterable<T>;
306
324
  /** Resolves layer-provided tools and merges with step tools into ctx.unifiedTools. */
@@ -348,8 +366,11 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
348
366
  */
349
367
  checkpoint(ctx: Context): Promise<void>;
350
368
  /**
351
- * Rebuild a `Context` from a previously-persisted snapshot. Returns `null`
352
- * if no snapshot is recorded for `executionId`. Surface a typed
369
+ * Rebuild a `Context` from what a previous run of `executionId` recorded: its
370
+ * checkpoint snapshot, its step-completion ledger, or both. Without a snapshot the
371
+ * context starts empty (no item log or layer state) but keeps the execution id and
372
+ * ledger, so recorded steps replay and the rest run fresh. Returns `null` when
373
+ * neither is recorded — start a fresh execution. Surface a typed
353
374
  * `NoeticConfigError(CHECKPOINT_SCHEMA_MISMATCH)` when the snapshot's
354
375
  * schema version is unrecognised — the caller is expected to discard the
355
376
  * checkpoint via `CheckpointStore.clear()` and start a fresh execution.
@@ -396,22 +417,33 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
396
417
  * `cancelled`.
397
418
  *
398
419
  * Cancellation is cooperative: a layer hook already in flight is allowed to
399
- * settle, and step code that ignores `ctx.aborted` between `await`s runs to
420
+ * settle (up to its timeout) before that context's teardown, and step code that ignores `ctx.aborted` between `await`s runs to
400
421
  * its next boundary. Calling `cancel()` on an already-cancelled context is a
401
422
  * no-op.
402
423
  */
403
424
  cancel(ctx: Context, reason?: string): Promise<void>;
404
425
  /**
405
- * Run the layer lifecycle's abort path for one cancelled context:
406
- * `onComplete` with `outcome: 'aborted'`, then `dispose`. Both always run
407
- * under cancellation (spec 09, Cancellation item 5).
426
+ * Run the layer lifecycle's abort path for one cancelled context: wait for
427
+ * its in-flight layer hooks to settle, then `onComplete` with
428
+ * `outcome: 'aborted'`, then `dispose`. Both always run under cancellation
429
+ * (spec 09, Cancellation item 6).
408
430
  */
409
431
  private teardownCancelledContext;
432
+ /**
433
+ * Channel state is shared by every root on this harness, so a channel
434
+ * warning is recorded as a `channel.warning` event on each live root's
435
+ * span (spec 10). A root span counts only if it will reach the trace
436
+ * exporter — a harness span (`SpanImpl`) with a real exporter configured.
437
+ * With no such span the warning goes to the console, so it is never lost.
438
+ */
439
+ private recordChannelWarning;
410
440
  createSpan(name: string, parent: Span | null): Span;
411
441
  getLayerState<T>(executionId: string, layerId: string): T | undefined;
412
442
  setLayerState<T>(executionId: string, layerId: string, state: T): void;
413
443
  beforeToolCall(layers: ContextLayer[], toolName: string, toolArgs: unknown, ctx: Context, callId?: string): Promise<SteeringDecision>;
414
444
  afterModelCall(layers: ContextLayer[], response: LLMResponse, ctx: Context): Promise<SteeringDecision>;
445
+ /** Record `hook` as running on `ctx` until it settles (see `runningHooks`). */
446
+ private trackHook;
415
447
  projectHistory(layers: ContextLayer[], items: ReadonlyArray<Item>, ctx: Context): Promise<ReadonlyArray<Item>>;
416
448
  runAppendPipeline(layers: ContextLayer[], items: Item[], ctx: Context): Promise<{
417
449
  items: Item[];
@@ -1,14 +1,15 @@
1
1
  import { frameworkCast, ItemSchemaRegistry, NoeticConfigError } from '@noetic-tools/types';
2
+ import { NoeticEvent } from '../observability/genai-attributes.js';
2
3
  import { SpanImpl } from '../observability/span-impl.js';
3
4
  import { NoopExporter } from '../observability/trace-exporter.js';
4
5
  import { AcpSessionStore } from '../runtime/acp-session-store.js';
5
6
  import { callDecide } from './decide-call.js';
6
7
  import { createInMemoryFsAdapter, createInMemoryShellAdapter, createInMemorySubprocessAdapter, OpenRouter, OpenRouterSdk, } from './deps/adapters.js';
7
8
  import { afterModelCallLayers, allocateBudgets, assembleView, beforeToolCallLayers, completeLayers, contextToExecCtx, createContextCacheStore, createLayerStateStore, createRecallCache, DEFAULT_PROJECTION, disposeLayers, executeRerender, initLayers, notifyDecision, projectHistoryLayers, recallLayers, recallLayersAtomic, recallLayersEventual, resolveLayerTools, runAppendPipeline, storeLayers, } from './deps/context.js';
8
- import { collectAllTools, deduplicateTools, dispatchStepThroughAdapter, execute, prepareBandedView, } from './deps/interpreter.js';
9
- import { buildItemStream, ChannelStore, ContextImpl, captureCheckpoint, clearCheckpoint, collectContextTree, createInMemoryStorage, createStepLedgerStore, filterReasoningStream, filterTextStream, resolveStepLedgerRetention, restoreFromCheckpoint, SessionRunner, StepLedger, snapshotCwdState, } from './deps/runtime.js';
9
+ import { collectAllTools, deduplicateTools, dispatchStepThroughAdapter, execute, prepareBandedView, someStep, } from './deps/interpreter.js';
10
+ import { buildItemStream, ChannelStore, ContextImpl, captureCheckpoint, clearCheckpoint, collectContextTree, createInMemoryStorage, createStepLedgerStore, filterReasoningStream, filterTextStream, resolveStepLedgerRetention, restoreFromCheckpoint, rootIdOf, SessionRunner, StepLedger, snapshotCwdState, warnToConsole, } from './deps/runtime.js';
10
11
  import { SteeringAction } from './deps/types.js';
11
- import { AgentHarnessModelCaller } from './model-call.js';
12
+ import { AgentHarnessModelCaller, noLlmProviderError } from './model-call.js';
12
13
  export { createStreamIdleWatchdog } from './model-call.js';
13
14
  import { buildItemSchemaRegistry } from './model-schema.js';
14
15
  //#endregion
@@ -202,9 +203,13 @@ export class AgentHarness {
202
203
  decideOverride;
203
204
  /** Model id → whether it answers natively as a decision model. */
204
205
  decisionModels = new Map();
206
+ /** @internal Channel state; read by the checkpoint functions. */
205
207
  channelStore;
206
- /** Re-entrant `run()` depth per root context id — see `executeClosingChannels`. */
207
- rootRunDepth = new Map();
208
+ /**
209
+ * Root contexts with a `run()` in flight, keyed by id, with their
210
+ * re-entrant run depth — see `executeClosingChannels`.
211
+ */
212
+ liveRoots = new Map();
208
213
  callModelOverride;
209
214
  defaultDeliveryMode;
210
215
  sessions = new Map();
@@ -270,6 +275,12 @@ export class AgentHarness {
270
275
  initializedExecutions = new Set();
271
276
  /** Executions restored from a checkpoint: their first layer init keeps the restored state. */
272
277
  restoredExecutions = new Set();
278
+ /**
279
+ * Layer hooks still running per context. `cancel()` waits for them before
280
+ * `onComplete`/`dispose`, so teardown never races a half-finished `store`
281
+ * or `recall`. Each hook is bounded by its own timeout.
282
+ */
283
+ runningHooks = new WeakMap();
273
284
  traceExporter;
274
285
  /**
275
286
  * Long-lived shared cwd state. The same reference is seeded into every
@@ -317,7 +328,9 @@ export class AgentHarness {
317
328
  this.decisionsClient = opts._testCallModel
318
329
  ? undefined
319
330
  : createDecisionsClient(opts.callModelDefaults);
320
- this.channelStore = new ChannelStore();
331
+ this.channelStore = new ChannelStore({
332
+ onWarning: (warning) => this.recordChannelWarning(warning),
333
+ });
321
334
  this.traceExporter = opts.traceExporter ?? new NoopExporter();
322
335
  this.layerStateStore =
323
336
  storageEnv?.layerStateStore ??
@@ -356,6 +369,9 @@ export class AgentHarness {
356
369
  hint: 'Pass `agentGraph` in constructor options, or use run() directly.',
357
370
  }));
358
371
  }
372
+ if (this.lacksModelFor(this.agentGraph)) {
373
+ return Promise.reject(noLlmProviderError());
374
+ }
359
375
  const threadId = options?.threadId ?? DEFAULT_THREAD_ID;
360
376
  const deliveryMode = options?.deliveryMode ?? this.defaultDeliveryMode;
361
377
  const session = this.getOrCreateSession(threadId);
@@ -545,16 +561,19 @@ export class AgentHarness {
545
561
  });
546
562
  }
547
563
  async notifyDecision(layers, record, ctx) {
548
- await notifyDecision({
564
+ await this.trackHook(ctx, notifyDecision({
549
565
  layers,
550
566
  record,
551
567
  ctx: this.toExecCtx(ctx),
552
568
  store: this.layerStateStore,
553
569
  recallCache: this.recallCache,
554
- });
570
+ }));
555
571
  }
556
572
  //#endregion
557
573
  async run(s, input, ctx) {
574
+ if (this.lacksModelFor(s)) {
575
+ throw noLlmProviderError();
576
+ }
558
577
  // Populate the unified tool pool when an embedder drives a step directly on
559
578
  // a bare `createContext()` context (the turn pipeline does this per turn).
560
579
  // Without it, steps that resolve tools dynamically from `ctx.unifiedTools`
@@ -571,6 +590,18 @@ export class AgentHarness {
571
590
  await this.ensureLayersInit(ctx);
572
591
  return this.executeClosingChannels(s, input, ctx);
573
592
  }
593
+ /**
594
+ * Whether `step` calls a model this harness has no credentials for. Checked
595
+ * before any step runs so a missing provider fails fast instead of after
596
+ * earlier steps' side effects. Steps the walk cannot see (dynamic branches)
597
+ * still hit the same error lazily at their model call.
598
+ */
599
+ lacksModelFor(step) {
600
+ if (this.client || this.callModelOverride) {
601
+ return false;
602
+ }
603
+ return someStep(frameworkCast(step), (s) => s.kind === 'callModel' || (s.kind === 'decide' && !this.decideOverride));
604
+ }
574
605
  /** Run all layer `init` hooks before the first step executes. Per spec 11,
575
606
  * init MUST complete before any recall fires so layer state is populated. */
576
607
  async initAndRun(s, input, ctx) {
@@ -589,23 +620,25 @@ export class AgentHarness {
589
620
  if (ctx.parent !== null) {
590
621
  return execute(s, input, ctx);
591
622
  }
592
- const depth = this.rootRunDepth.get(ctx.id) ?? 0;
593
- if (depth === 0) {
623
+ const live = this.liveRoots.get(ctx.id);
624
+ if (!live) {
594
625
  // A restored or sequentially re-run root context reuses its id — clear
595
626
  // any closure from a previous completion so its channels work again.
596
627
  this.channelStore.openExecution(ctx.id);
597
628
  }
598
- this.rootRunDepth.set(ctx.id, depth + 1);
629
+ const entry = live ?? {
630
+ ctx,
631
+ depth: 0,
632
+ };
633
+ entry.depth += 1;
634
+ this.liveRoots.set(ctx.id, entry);
599
635
  try {
600
636
  return await execute(s, input, ctx);
601
637
  }
602
638
  finally {
603
- const remaining = (this.rootRunDepth.get(ctx.id) ?? 1) - 1;
604
- if (remaining > 0) {
605
- this.rootRunDepth.set(ctx.id, remaining);
606
- }
607
- else {
608
- this.rootRunDepth.delete(ctx.id);
639
+ entry.depth -= 1;
640
+ if (entry.depth <= 0) {
641
+ this.liveRoots.delete(ctx.id);
609
642
  this.channelStore.closeExecution(ctx.id);
610
643
  await this.acpSessions.closeOwnedBy(ctx.id);
611
644
  }
@@ -688,6 +721,7 @@ export class AgentHarness {
688
721
  // Internal sender: back-pressured on full queue channels. The calling
689
722
  // context's abort signal rejects a parked send with 'cancelled'.
690
723
  const signal = ctx instanceof ContextImpl ? ctx.abortSignal : undefined;
724
+ this.channelStore.claim(channel, rootIdOf(ctx));
691
725
  return this.channelStore.send(channel, value, {
692
726
  signal,
693
727
  });
@@ -696,9 +730,11 @@ export class AgentHarness {
696
730
  // Wire the calling context's abort signal so ctx.abort() rejects a
697
731
  // pending recv with { kind: 'cancelled' } instead of hanging.
698
732
  const signal = ctx instanceof ContextImpl ? ctx.abortSignal : undefined;
733
+ this.channelStore.claim(channel, rootIdOf(ctx));
699
734
  return this.channelStore.recv(channel, opts?.timeout, signal);
700
735
  }
701
- tryRecv(channel, _ctx) {
736
+ tryRecv(channel, ctx) {
737
+ this.channelStore.claim(channel, rootIdOf(ctx));
702
738
  return this.channelStore.tryRecv(channel);
703
739
  }
704
740
  getChannelHandle(channel, executionId) {
@@ -734,7 +770,7 @@ export class AgentHarness {
734
770
  });
735
771
  }
736
772
  async recallLayers(layers, input, ctx) {
737
- return recallLayers({
773
+ return this.trackHook(ctx, recallLayers({
738
774
  layers,
739
775
  query: input,
740
776
  ctx: this.toExecCtx(ctx),
@@ -742,7 +778,7 @@ export class AgentHarness {
742
778
  budgets: this.layerBudgets(layers),
743
779
  store: this.layerStateStore,
744
780
  itemSchemas: this.itemSchemas,
745
- });
781
+ }));
746
782
  }
747
783
  /** Allocate per-layer recall budgets from the harness projection policy. */
748
784
  layerBudgets(layers) {
@@ -759,7 +795,7 @@ export class AgentHarness {
759
795
  ]));
760
796
  }
761
797
  async recallLayersAtomic(layers, input, ctx, budgets, opts) {
762
- return recallLayersAtomic({
798
+ return this.trackHook(ctx, recallLayersAtomic({
763
799
  layers,
764
800
  query: input,
765
801
  ctx: this.toExecCtx(ctx),
@@ -769,10 +805,10 @@ export class AgentHarness {
769
805
  itemSchemas: this.itemSchemas,
770
806
  forceAtomic: this.config.forceAtomicRecall,
771
807
  peek: opts?.peek,
772
- });
808
+ }));
773
809
  }
774
810
  async recallLayersEventual(layers, input, ctx, budgets, opts) {
775
- return recallLayersEventual({
811
+ return this.trackHook(ctx, recallLayersEventual({
776
812
  layers,
777
813
  query: input,
778
814
  ctx: this.toExecCtx(ctx),
@@ -783,7 +819,7 @@ export class AgentHarness {
783
819
  forceAtomic: this.config.forceAtomicRecall,
784
820
  cache: this.recallCache,
785
821
  peek: opts?.peek,
786
- });
822
+ }));
787
823
  }
788
824
  /**
789
825
  * Compute the items array that would be sent to the model on the next turn —
@@ -858,7 +894,7 @@ export class AgentHarness {
858
894
  if (!storage) {
859
895
  return;
860
896
  }
861
- await storeLayers({
897
+ await this.trackHook(ctx, storeLayers({
862
898
  layers,
863
899
  response,
864
900
  ctx: this.toExecCtx(ctx),
@@ -866,7 +902,7 @@ export class AgentHarness {
866
902
  store: this.layerStateStore,
867
903
  storage,
868
904
  recallCache: this.recallCache,
869
- });
905
+ }));
870
906
  }
871
907
  async disposeLayers(layers, ctx) {
872
908
  // Drop the init guard so a deliberate dispose→run cycle re-hydrates from
@@ -902,8 +938,11 @@ export class AgentHarness {
902
938
  return captureCheckpoint(this, ctx);
903
939
  }
904
940
  /**
905
- * Rebuild a `Context` from a previously-persisted snapshot. Returns `null`
906
- * if no snapshot is recorded for `executionId`. Surface a typed
941
+ * Rebuild a `Context` from what a previous run of `executionId` recorded: its
942
+ * checkpoint snapshot, its step-completion ledger, or both. Without a snapshot the
943
+ * context starts empty (no item log or layer state) but keeps the execution id and
944
+ * ledger, so recorded steps replay and the rest run fresh. Returns `null` when
945
+ * neither is recorded — start a fresh execution. Surface a typed
907
946
  * `NoeticConfigError(CHECKPOINT_SCHEMA_MISMATCH)` when the snapshot's
908
947
  * schema version is unrecognised — the caller is expected to discard the
909
948
  * checkpoint via `CheckpointStore.clear()` and start a fresh execution.
@@ -921,11 +960,14 @@ export class AgentHarness {
921
960
  * whose `id` is already the original `executionId`.
922
961
  */
923
962
  async restore(executionId, opts) {
924
- const ctx = await restoreFromCheckpoint(this, executionId, opts);
925
- if (ctx) {
926
- this.restoredExecutions.add(ctx.id);
963
+ const restored = await restoreFromCheckpoint(this, executionId, opts);
964
+ if (!restored) {
965
+ return null;
966
+ }
967
+ if (restored.fromSnapshot) {
968
+ this.restoredExecutions.add(executionId);
927
969
  }
928
- return ctx;
970
+ return restored.ctx;
929
971
  }
930
972
  /**
931
973
  * Discard everything `restore(executionId)` would have used — the snapshot *and* the
@@ -959,7 +1001,7 @@ export class AgentHarness {
959
1001
  * `cancelled`.
960
1002
  *
961
1003
  * Cancellation is cooperative: a layer hook already in flight is allowed to
962
- * settle, and step code that ignores `ctx.aborted` between `await`s runs to
1004
+ * settle (up to its timeout) before that context's teardown, and step code that ignores `ctx.aborted` between `await`s runs to
963
1005
  * its next boundary. Calling `cancel()` on an already-cancelled context is a
964
1006
  * no-op.
965
1007
  */
@@ -981,15 +1023,17 @@ export class AgentHarness {
981
1023
  }
982
1024
  }
983
1025
  /**
984
- * Run the layer lifecycle's abort path for one cancelled context:
985
- * `onComplete` with `outcome: 'aborted'`, then `dispose`. Both always run
986
- * under cancellation (spec 09, Cancellation item 5).
1026
+ * Run the layer lifecycle's abort path for one cancelled context: wait for
1027
+ * its in-flight layer hooks to settle, then `onComplete` with
1028
+ * `outcome: 'aborted'`, then `dispose`. Both always run under cancellation
1029
+ * (spec 09, Cancellation item 6).
987
1030
  */
988
1031
  async teardownCancelledContext(ctx) {
989
1032
  const layers = ctx.layers ?? [];
990
1033
  if (layers.length === 0) {
991
1034
  return;
992
1035
  }
1036
+ await Promise.allSettled(this.runningHooks.get(ctx) ?? []);
993
1037
  await completeLayers({
994
1038
  layers: [
995
1039
  ...layers,
@@ -1003,6 +1047,29 @@ export class AgentHarness {
1003
1047
  ...layers,
1004
1048
  ], ctx);
1005
1049
  }
1050
+ /**
1051
+ * Channel state is shared by every root on this harness, so a channel
1052
+ * warning is recorded as a `channel.warning` event on each live root's
1053
+ * span (spec 10). A root span counts only if it will reach the trace
1054
+ * exporter — a harness span (`SpanImpl`) with a real exporter configured.
1055
+ * With no such span the warning goes to the console, so it is never lost.
1056
+ */
1057
+ recordChannelWarning(warning) {
1058
+ const traced = this.traceExporter instanceof NoopExporter
1059
+ ? []
1060
+ : [
1061
+ ...this.liveRoots.values(),
1062
+ ].filter(({ ctx }) => ctx.span instanceof SpanImpl);
1063
+ if (traced.length === 0) {
1064
+ warnToConsole(warning);
1065
+ return;
1066
+ }
1067
+ for (const { ctx } of traced) {
1068
+ ctx.span.addEvent(NoeticEvent.CHANNEL_WARNING, {
1069
+ ...warning,
1070
+ });
1071
+ }
1072
+ }
1006
1073
  createSpan(name, parent) {
1007
1074
  return new SpanImpl(name, parent);
1008
1075
  }
@@ -1019,14 +1086,14 @@ export class AgentHarness {
1019
1086
  action: SteeringAction.Allow,
1020
1087
  };
1021
1088
  }
1022
- return beforeToolCallLayers({
1089
+ return this.trackHook(ctx, beforeToolCallLayers({
1023
1090
  layers,
1024
1091
  toolName,
1025
1092
  toolArgs,
1026
1093
  callId,
1027
1094
  ctx: this.toExecCtx(ctx),
1028
1095
  store: this.layerStateStore,
1029
- });
1096
+ }));
1030
1097
  }
1031
1098
  async afterModelCall(layers, response, ctx) {
1032
1099
  const hasHook = layers.some((l) => l.hooks.afterModelCall);
@@ -1035,12 +1102,26 @@ export class AgentHarness {
1035
1102
  action: SteeringAction.Allow,
1036
1103
  };
1037
1104
  }
1038
- return afterModelCallLayers({
1105
+ return this.trackHook(ctx, afterModelCallLayers({
1039
1106
  layers,
1040
1107
  response,
1041
1108
  ctx: this.toExecCtx(ctx),
1042
1109
  store: this.layerStateStore,
1043
- });
1110
+ }));
1111
+ }
1112
+ /** Record `hook` as running on `ctx` until it settles (see `runningHooks`). */
1113
+ trackHook(ctx, hook) {
1114
+ let running = this.runningHooks.get(ctx);
1115
+ if (!running) {
1116
+ running = new Set();
1117
+ this.runningHooks.set(ctx, running);
1118
+ }
1119
+ running.add(hook);
1120
+ const forget = () => {
1121
+ running.delete(hook);
1122
+ };
1123
+ hook.then(forget, forget);
1124
+ return hook;
1044
1125
  }
1045
1126
  async projectHistory(layers, items, ctx) {
1046
1127
  const hasHook = layers.some((l) => l.hooks.projectHistory);
@@ -3,4 +3,5 @@ export { prepareBandedView } from '../../interpreter/context-assembly.js';
3
3
  export { execute } from '../../interpreter/execute.js';
4
4
  export type { DetachedSpawnOverrides } from '../../interpreter/step-dispatcher.js';
5
5
  export { dispatchStepThroughAdapter } from '../../interpreter/step-dispatcher.js';
6
+ export { someStep } from '../../interpreter/step-walk.js';
6
7
  export { isFunctionCall } from '../../interpreter/typeguards.js';
@@ -2,4 +2,5 @@ export { collectAllTools, deduplicateTools } from '../../interpreter/collect-too
2
2
  export { prepareBandedView } from '../../interpreter/context-assembly.js';
3
3
  export { execute } from '../../interpreter/execute.js';
4
4
  export { dispatchStepThroughAdapter } from '../../interpreter/step-dispatcher.js';
5
+ export { someStep } from '../../interpreter/step-walk.js';
5
6
  export { isFunctionCall } from '../../interpreter/typeguards.js';
@@ -1,5 +1,6 @@
1
- export { ChannelStore } from '../../runtime/channel-store.js';
2
- export { ContextImpl, collectContextTree } from '../../runtime/context-impl.js';
1
+ export type { ChannelWarning } from '../../runtime/channel-store.js';
2
+ export { ChannelStore, warnToConsole } from '../../runtime/channel-store.js';
3
+ export { ContextImpl, collectContextTree, rootIdOf } from '../../runtime/context-impl.js';
3
4
  export { snapshotCwdState } from '../../runtime/cwd-helpers.js';
4
5
  export type { CheckpointStore } from '../../runtime/durable/index.js';
5
6
  export type { RestoreCheckpointOptions } from '../../runtime/durable/harness-checkpoints.js';
@@ -1,5 +1,5 @@
1
- export { ChannelStore } from '../../runtime/channel-store.js';
2
- export { ContextImpl, collectContextTree } from '../../runtime/context-impl.js';
1
+ export { ChannelStore, warnToConsole } from '../../runtime/channel-store.js';
2
+ export { ContextImpl, collectContextTree, rootIdOf } from '../../runtime/context-impl.js';
3
3
  export { snapshotCwdState } from '../../runtime/cwd-helpers.js';
4
4
  export { captureCheckpoint, clearCheckpoint, restoreFromCheckpoint, } from '../../runtime/durable/harness-checkpoints.js';
5
5
  export { createStepLedgerStore, resolveStepLedgerRetention, StepLedger, } from '../../runtime/durable/step-ledger.js';
@@ -1,4 +1,5 @@
1
1
  import type { AgentHarnessContract, CallModelRequest, ItemSchemaRegistry, LLMResponse, TraceExporter } from '@noetic-tools/types';
2
+ import { NoeticConfigError } from '@noetic-tools/types';
2
3
  import type { OpenRouter } from '@openrouter/agent';
3
4
  import type { MessageQueue } from '../runtime/message-queue.js';
4
5
  interface StreamIdleWatchdog {
@@ -7,6 +8,8 @@ interface StreamIdleWatchdog {
7
8
  /** Clear the pending timer. Safe to call multiple times. */
8
9
  stop: () => void;
9
10
  }
11
+ /** @internal The error for a model call on a harness with no LLM credentials. */
12
+ export declare function noLlmProviderError(): NoeticConfigError;
10
13
  /** @internal Create a watchdog that aborts `controller` when no
11
14
  * {@link StreamIdleWatchdog.reset reset} is called within `timeoutMs`. When
12
15
  * `timeoutMs <= 0`, returns an inert no-op so callers can always call `.reset()`
@@ -133,6 +133,14 @@ async function pipeStreamEventsToBroadcaster(opts) {
133
133
  throw err;
134
134
  }
135
135
  }
136
+ /** @internal The error for a model call on a harness with no LLM credentials. */
137
+ export function noLlmProviderError() {
138
+ return new NoeticConfigError({
139
+ code: 'NO_LLM_PROVIDER',
140
+ message: 'No LLM provider configured on this harness.',
141
+ hint: 'Pass `callModelDefaults: { apiKey: "..." }` (defaults to the Noetic platform) or set NOETIC_API_KEY. For direct OpenRouter, use `callModelDefaults: { provider: "openrouter", apiKey: "..." }` or set OPENROUTER_API_KEY.',
142
+ });
143
+ }
136
144
  /** @internal Create a watchdog that aborts `controller` when no
137
145
  * {@link StreamIdleWatchdog.reset reset} is called within `timeoutMs`. When
138
146
  * `timeoutMs <= 0`, returns an inert no-op so callers can always call `.reset()`
@@ -354,11 +362,7 @@ export class AgentHarnessModelCaller {
354
362
  if (this.opts.client) {
355
363
  return this.opts.client;
356
364
  }
357
- throw new NoeticConfigError({
358
- code: 'NO_LLM_PROVIDER',
359
- message: 'No LLM provider configured on this harness.',
360
- hint: 'Pass `callModelDefaults: { apiKey: "..." }` (defaults to the Noetic platform) or set NOETIC_API_KEY. For direct OpenRouter, use `callModelDefaults: { provider: "openrouter", apiKey: "..." }` or set OPENROUTER_API_KEY.',
361
- });
365
+ throw noLlmProviderError();
362
366
  }
363
367
  async callModel(request) {
364
368
  if (this.opts.callModelOverride) {
@@ -386,6 +390,7 @@ export class AgentHarnessModelCaller {
386
390
  // the assembled view was cached (see `noteCacheOutcome`).
387
391
  const rounds = [];
388
392
  let totalCost = 0;
393
+ let servedTier;
389
394
  const conversationInput = itemsToInput(prepared.remaining);
390
395
  const textFormat = request.outputSchema
391
396
  ? buildTextFormat(request.outputSchema, request.outputJsonSchema)
@@ -434,6 +439,9 @@ export class AgentHarnessModelCaller {
434
439
  temperature: request.params?.temperature,
435
440
  maxOutputTokens: request.params?.maxTokens,
436
441
  topP: request.params?.topP,
442
+ // The agent's bundled SDK types an older tier list (no `fast` or
443
+ // `ultrafast`); the field is an open enum, so newer tiers pass through.
444
+ serviceTier: frameworkCast(request.params?.serviceTier),
437
445
  ...(textFormat
438
446
  ? {
439
447
  text: textFormat,
@@ -524,6 +532,7 @@ export class AgentHarnessModelCaller {
524
532
  totalUsage.outputTokens += roundUsage.outputTokens;
525
533
  totalUsage.cachedTokens += roundUsage.cachedTokens ?? 0;
526
534
  totalCost += roundCost;
535
+ servedTier = sdkResponse.serviceTier ?? undefined;
527
536
  allItems.push(...roundItems);
528
537
  const functionCalls = roundItems.filter(isFunctionCall);
529
538
  if (functionCalls.length === 0 || !request.tools) {
@@ -563,6 +572,11 @@ export class AgentHarnessModelCaller {
563
572
  },
564
573
  rounds,
565
574
  cost: totalCost > 0 ? totalCost : undefined,
575
+ ...(servedTier
576
+ ? {
577
+ serviceTier: servedTier,
578
+ }
579
+ : {}),
566
580
  };
567
581
  }
568
582
  injectBetweenRoundMessages(ctx, conversationInput, round, emitIfAllowed) {
package/dist/index.d.ts CHANGED
@@ -15,6 +15,8 @@ export { channel } from './builders/channel-builder.js';
15
15
  /** @public */
16
16
  export { context } from './builders/context-builder.js';
17
17
  /** @public */
18
+ export type { ConditionalOpts, InParallelOpts } from './builders/control-flow-builders.js';
19
+ /** @public */
18
20
  export { conditional, inParallel } from './builders/control-flow-builders.js';
19
21
  /** @public */
20
22
  export { decide, routeSwitch } from './builders/decide-builders.js';
@@ -84,7 +86,7 @@ export { DECISION_PROJECTION, decisionsHistoryLayer, filesystem, findFunctionCal
84
86
  /** @public */
85
87
  export { createMessage, estimateTokens, recordUsage } from '@noetic-tools/types';
86
88
  /** @public */
87
- export { GenAI, NoeticAttr, ToolAttr } from './observability/genai-attributes.js';
89
+ export { GenAI, NoeticAttr, NoeticEvent, ToolAttr } from './observability/genai-attributes.js';
88
90
  /** @public */
89
91
  export { InMemoryExporter, NoopExporter } from './observability/trace-exporter.js';
90
92
  /** @public */
@@ -115,7 +117,7 @@ export type { Channel, ChannelHandle, ExternalChannel } from '@noetic-tools/type
115
117
  /** @public */
116
118
  export type { InferSchemaInput, InferSchemaOutput, InputSchemaConfig, LLMResponse, LlmProviderConfig, ModelParams, RetryPolicy, RoundUsage, SchemaValidationFailure, SchemaValidationResult, SchemaValidationSuccess, ServerToolSpec, StandardJSONSchemaV1, StandardSchemaV1, StepMeta, TokenUsage, Tool, ToolContextDeclaration, UsageCharge, } from '@noetic-tools/types';
117
119
  /** @public */
118
- export { isServerToolSpec, isStandardJsonSchema, isZodSchema, standardIssuesToZodError, validateSchema, } from '@noetic-tools/types';
120
+ export { isServerToolSpec, isStandardJsonSchema, isZodSchema, ServiceTier, standardIssuesToZodError, validateSchema, } from '@noetic-tools/types';
119
121
  /** @public */
120
122
  /** @public */
121
123
  /** @public */
@@ -129,9 +131,9 @@ export { DetachedStatus } from '@noetic-tools/types';
129
131
  /** @public */
130
132
  export type { EmbedFn } from './types/embed.js';
131
133
  /** @public */
132
- export type { CheckpointSnapshot, CwdSnapshot, FrontierFrame, ItemLogSnapshot, PendingAskUserSnapshot, } from './types/checkpoint.js';
134
+ export type { ChannelSnapshot, CheckpointSnapshot, CwdSnapshot, FrontierFrame, ItemLogSnapshot, PendingAskUserSnapshot, } from './types/checkpoint.js';
133
135
  /** @public */
134
- export { CheckpointSchemaVersion, CheckpointSnapshotSchema, CwdSnapshotSchema, FrontierFrameSchema, ItemLogSnapshotSchema, PendingAskUserSnapshotSchema, } from './types/checkpoint.js';
136
+ export { ChannelSnapshotSchema, CheckpointSchemaVersion, CheckpointSnapshotSchema, CwdSnapshotSchema, FrontierFrameSchema, ItemLogSnapshotSchema, PendingAskUserSnapshotSchema, } from './types/checkpoint.js';
135
137
  /** @public */
136
138
  export type { NoeticError } from '@noetic-tools/types';
137
139
  /** @public */
@@ -153,7 +155,7 @@ export type { AfterModelCallParams, AfterModelCallResult, BeforeToolCallParams,
153
155
  /** @public */
154
156
  export { LedgerEntryKind, SteeringAction } from '@noetic-tools/types';
155
157
  /** @public */
156
- export type { ExecuteStepFn, SettleResult, Snapshot, Step, StepAcpAgent, StepCallModel, StepConditional, StepInParallel, StepInParallelAll, StepInParallelRace, StepInParallelSettle, StepInvokeTool, StepLoop, StepRunCode, StepSpawn, StepWithContext, Until, Verdict, } from '@noetic-tools/types';
158
+ export type { ExecuteStepFn, MergeFn, SettleResult, Snapshot, Step, StepAcpAgent, StepCallModel, StepConditional, StepInParallel, StepInParallelAll, StepInParallelRace, StepInParallelSettle, StepInvokeTool, StepLoop, StepReplay, StepRunCode, StepSpawn, StepWithContext, Until, Verdict, } from '@noetic-tools/types';
157
159
  /** @public */
158
160
  export type { AnsweredAll, AnswerFor, BooleanAnswer, BooleanQuestion, CaseKeyOf, ChoiceAnswer, ChoiceQuestion, ChoiceValues, DecideAnswer, DecideAnswers, DecideQuestion, DecideQuestions, DecideRequest, DecideResponse, DecideStatePart, DecideStep, DecideView, DecisionMeta, DecisionRecord, DecisionSource, LevelLabels, OnDecisionParams, RecallOptions, ScoreAnswer, ScoreLevel, ScoreQuestion, StepDecide, } from '@noetic-tools/types';
159
161
  /** @public */