@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
@@ -1,5 +1,6 @@
1
1
  import type { LayerStateStore } from '@noetic-tools/context';
2
2
  import type { Context, Item, ItemSchemaRegistry, RestoreContextOptions } from '@noetic-tools/types';
3
+ import type { ChannelStore } from '../channel-store.js';
3
4
  import type { EventBroadcaster } from '../event-broadcaster.js';
4
5
  import type { CheckpointStore } from './checkpoint-store.js';
5
6
  import type { StepLedgerRetention, StepLedgerStore } from './step-ledger.js';
@@ -28,6 +29,7 @@ export interface CheckpointHarnessHandle {
28
29
  readonly stepLedgerStore?: StepLedgerStore;
29
30
  readonly stepLedgerRetention?: StepLedgerRetention;
30
31
  readonly layerStateStore: LayerStateStore;
32
+ readonly channelStore: Pick<ChannelStore, 'snapshot' | 'restore'>;
31
33
  readonly itemSchemas: ItemSchemaRegistry;
32
34
  createContext(opts?: RestoreCheckpointOptions & {
33
35
  items?: Item[];
@@ -47,27 +49,42 @@ export interface CheckpointHarnessHandle {
47
49
  */
48
50
  export declare function captureCheckpoint(h: CheckpointHarnessHandle, ctx: Context): Promise<void>;
49
51
  /**
50
- * Rebuild a `Context` from a previously-persisted snapshot. Returns
51
- * `null` if no snapshot is recorded for `executionId`. Layer state is
52
- * replayed into `layerStateStore` keyed by the original executionId so
53
- * the restored context observes the pre-crash state through
54
- * `readLayerState` and the context projectors.
52
+ * What `restoreFromCheckpoint` rebuilt. `fromSnapshot` is false for a ledger-only
53
+ * restore, whose layer state was never recovered — the harness must then let layer
54
+ * `init` hydrate normally instead of keeping restored state.
55
55
  *
56
- * `opts` carries the host's own context wiring (broadcaster, parent, state,
57
- * layer overrides) into the rebuilt context. A snapshot cannot round-trip live
58
- * objects, so a host that decorated the original context has to hand the same
59
- * decoration back here — otherwise the resumed run gets a bare context and
60
- * whatever depended on that wiring stops working silently. Snapshot-owned
61
- * fields always win: identity, item log, and cwd come from the persisted
62
- * record, never from `opts`.
56
+ * @internal
57
+ */
58
+ export interface RestoredExecution {
59
+ ctx: Context;
60
+ fromSnapshot: boolean;
61
+ }
62
+ /**
63
+ * Rebuild a `Context` for `executionId` from what a previous run recorded: the
64
+ * checkpoint snapshot and the step-completion ledger. Either is enough. Returns
65
+ * `null` only when neither exists, so the caller starts a fresh execution.
66
+ *
67
+ * - With a snapshot, the item log, layer state, channel state, cwd, and thread/resource
68
+ * ids come from it. Layer state is replayed into `layerStateStore` under the original
69
+ * id; channel values and queued items refill the channel store for channels with no
70
+ * live state.
71
+ * - Without one, the context starts like `createContext(opts)` would — empty item
72
+ * log, no layer or channel state, the harness's root cwd, fresh thread id — and only
73
+ * its identity and ledger are recovered. Recorded steps replay; the rest run fresh.
74
+ *
75
+ * `opts` carries the host's own context wiring (broadcaster, parent, state, layer
76
+ * overrides). A snapshot cannot round-trip live objects, so a host that decorated
77
+ * the original context has to hand the same decoration back here. Snapshot-owned
78
+ * fields always win: identity, item log, and cwd come from the persisted record,
79
+ * never from `opts`.
63
80
  *
64
- * Preserves the original executionId on the returned context via
65
- * `Object.defineProperty` — adapter correlation across crash/restart
66
- * requires a stable id.
81
+ * A snapshot whose schema version is unknown throws
82
+ * `NoeticConfigError(CHECKPOINT_SCHEMA_MISMATCH)` from the store; the ledger is not
83
+ * used as a fallback, because resuming onto it would drop state the snapshot held.
67
84
  *
68
85
  * @internal
69
86
  */
70
- export declare function restoreFromCheckpoint(h: CheckpointHarnessHandle, executionId: string, opts?: RestoreCheckpointOptions): Promise<Context | null>;
87
+ export declare function restoreFromCheckpoint(h: CheckpointHarnessHandle, executionId: string, opts?: RestoreCheckpointOptions): Promise<RestoredExecution | null>;
71
88
  /**
72
89
  * Discard every recovery record for one execution: the snapshot and the completion
73
90
  * ledger. Clearing the snapshot alone would strand the ledger's shards under
@@ -1,5 +1,5 @@
1
1
  import { CheckpointSchemaVersion } from '../../types/checkpoint.js';
2
- import { ContextImpl } from '../context-impl.js';
2
+ import { ContextImpl, rootIdOf } from '../context-impl.js';
3
3
  import { StepLedger } from './step-ledger.js';
4
4
  //#endregion
5
5
  //#region captureCheckpoint
@@ -47,6 +47,9 @@ export async function captureCheckpoint(h, ctx) {
47
47
  ...ctx.itemLog.items,
48
48
  ],
49
49
  },
50
+ // Channel state belongs to the root execution, so a child checkpoint
51
+ // carries the channels its root used.
52
+ channels: h.channelStore.snapshot(rootIdOf(ctx)),
50
53
  capturedAt: new Date().toISOString(),
51
54
  };
52
55
  try {
@@ -56,26 +59,28 @@ export async function captureCheckpoint(h, ctx) {
56
59
  console.warn(`AgentHarness.checkpoint: failed to persist snapshot for execution "${ctx.id}":`, err);
57
60
  }
58
61
  }
59
- //#endregion
60
- //#region restoreFromCheckpoint
61
62
  /**
62
- * Rebuild a `Context` from a previously-persisted snapshot. Returns
63
- * `null` if no snapshot is recorded for `executionId`. Layer state is
64
- * replayed into `layerStateStore` keyed by the original executionId so
65
- * the restored context observes the pre-crash state through
66
- * `readLayerState` and the context projectors.
63
+ * Rebuild a `Context` for `executionId` from what a previous run recorded: the
64
+ * checkpoint snapshot and the step-completion ledger. Either is enough. Returns
65
+ * `null` only when neither exists, so the caller starts a fresh execution.
66
+ *
67
+ * - With a snapshot, the item log, layer state, channel state, cwd, and thread/resource
68
+ * ids come from it. Layer state is replayed into `layerStateStore` under the original
69
+ * id; channel values and queued items refill the channel store for channels with no
70
+ * live state.
71
+ * - Without one, the context starts like `createContext(opts)` would — empty item
72
+ * log, no layer or channel state, the harness's root cwd, fresh thread id — and only
73
+ * its identity and ledger are recovered. Recorded steps replay; the rest run fresh.
67
74
  *
68
- * `opts` carries the host's own context wiring (broadcaster, parent, state,
69
- * layer overrides) into the rebuilt context. A snapshot cannot round-trip live
70
- * objects, so a host that decorated the original context has to hand the same
71
- * decoration back here — otherwise the resumed run gets a bare context and
72
- * whatever depended on that wiring stops working silently. Snapshot-owned
73
- * fields always win: identity, item log, and cwd come from the persisted
74
- * record, never from `opts`.
75
+ * `opts` carries the host's own context wiring (broadcaster, parent, state, layer
76
+ * overrides). A snapshot cannot round-trip live objects, so a host that decorated
77
+ * the original context has to hand the same decoration back here. Snapshot-owned
78
+ * fields always win: identity, item log, and cwd come from the persisted record,
79
+ * never from `opts`.
75
80
  *
76
- * Preserves the original executionId on the returned context via
77
- * `Object.defineProperty` — adapter correlation across crash/restart
78
- * requires a stable id.
81
+ * A snapshot whose schema version is unknown throws
82
+ * `NoeticConfigError(CHECKPOINT_SCHEMA_MISMATCH)` from the store; the ledger is not
83
+ * used as a fallback, because resuming onto it would drop state the snapshot held.
79
84
  *
80
85
  * @internal
81
86
  */
@@ -85,52 +90,69 @@ export async function restoreFromCheckpoint(h, executionId, opts) {
85
90
  return null;
86
91
  }
87
92
  const snapshot = await store.load(executionId);
88
- if (!snapshot) {
93
+ const recovered = await h.stepLedgerStore?.load(executionId);
94
+ if (!snapshot && (!recovered || recovered.entries.size === 0)) {
89
95
  return null;
90
96
  }
97
+ const ctx = snapshot ? contextFromSnapshot(h, snapshot, opts) : h.createContext(opts);
98
+ if (ctx instanceof ContextImpl) {
99
+ adoptExecution({
100
+ h,
101
+ ctx,
102
+ executionId,
103
+ recovered,
104
+ });
105
+ }
106
+ return {
107
+ ctx,
108
+ fromSnapshot: snapshot !== null,
109
+ };
110
+ }
111
+ /** Replay a snapshot's layer and channel state and build the context its item log and cwd describe. */
112
+ function contextFromSnapshot(h, snapshot, opts) {
91
113
  for (const [layerId, state] of Object.entries(snapshot.layers)) {
92
- h.layerStateStore.set(executionId, layerId, state);
114
+ h.layerStateStore.set(snapshot.executionId, layerId, state);
93
115
  }
94
- const items = h.itemSchemas.parseMany(snapshot.itemLog.items);
95
- const cwdInit = snapshot.cwd?.current ?? undefined;
116
+ h.channelStore.restore(snapshot.executionId, snapshot.channels ?? []);
96
117
  /* Caller wiring first, snapshot second: a host may legitimately swap the context
97
118
  * layers or hang the restored execution under a new parent, but it must never be
98
119
  * able to override the identity/history the snapshot is the record of. */
99
- const ctx = h.createContext({
120
+ return h.createContext({
100
121
  ...opts,
101
- items,
122
+ items: h.itemSchemas.parseMany(snapshot.itemLog.items),
102
123
  threadId: snapshot.threadId,
103
124
  resourceId: snapshot.resourceId,
104
- cwdInit,
125
+ cwdInit: snapshot.cwd?.current ?? undefined,
105
126
  });
106
- if (ctx instanceof ContextImpl) {
107
- Object.defineProperty(ctx, 'id', {
108
- value: executionId,
109
- configurable: false,
110
- writable: false,
111
- enumerable: true,
112
- });
113
- /* Attach the recovered completion ledger. `createContext` already built a fresh
114
- * one keyed to a throwaway id; the restored context needs the entries recorded
115
- * under THIS execution, or `execute()` would replay nothing and re-run the whole
116
- * tree. Assigned the same way `id` is, to keep `createContext`'s public options
117
- * free of resume-only fields. */
118
- if (h.stepLedgerStore) {
119
- const recovered = await h.stepLedgerStore.load(executionId);
120
- Object.defineProperty(ctx, 'ledger', {
121
- value: new StepLedger({
122
- executionId,
123
- store: h.stepLedgerStore,
124
- recovered,
125
- retention: h.stepLedgerRetention,
126
- }),
127
- configurable: false,
128
- writable: false,
129
- enumerable: true,
130
- });
131
- }
127
+ }
128
+ /**
129
+ * Give `ctx` the original execution id and the ledger recorded under it.
130
+ * `createContext` already built a fresh ledger keyed to a throwaway id; without the
131
+ * recovered one `execute()` would replay nothing and re-run the whole tree. Both are
132
+ * assigned with `defineProperty` to keep `createContext`'s public options free of
133
+ * resume-only fields — adapter correlation across a restart needs the stable id.
134
+ */
135
+ function adoptExecution({ h, ctx, executionId, recovered, }) {
136
+ Object.defineProperty(ctx, 'id', {
137
+ value: executionId,
138
+ configurable: false,
139
+ writable: false,
140
+ enumerable: true,
141
+ });
142
+ if (!h.stepLedgerStore || !recovered) {
143
+ return;
132
144
  }
133
- return ctx;
145
+ Object.defineProperty(ctx, 'ledger', {
146
+ value: new StepLedger({
147
+ executionId,
148
+ store: h.stepLedgerStore,
149
+ recovered,
150
+ retention: h.stepLedgerRetention,
151
+ }),
152
+ configurable: false,
153
+ writable: false,
154
+ enumerable: true,
155
+ });
134
156
  }
135
157
  //#endregion
136
158
  //#region clearCheckpoint
@@ -2,7 +2,7 @@
2
2
  * The step completion ledger: what a resumed execution replays instead of re-running.
3
3
  *
4
4
  * The frontier records which steps were IN FLIGHT at snapshot time; it says nothing
5
- * about which ones finished, and a finished step leaves no trace once `leaveStep` pops
5
+ * about which ones finished, and a finished step leaves no trace once `leaveStep` drops
6
6
  * it. Resume therefore needs a separate record — and it must carry each step's OUTPUT,
7
7
  * not merely a "done" flag: steps consume the previous step's value, and a `callModel` step
8
8
  * re-run to catch up produces a different one than the rest of the run already saw.
@@ -11,6 +11,12 @@
11
11
  * into the single `execution:<id>:snapshot` key would rewrite a growing blob on every
12
12
  * step — O(n²) bytes over a run.
13
13
  *
14
+ * An entry's key is one of two forms (spec 23a § Identity). By default it is the step's
15
+ * execution path (`ContextImpl.currentPath()`): positional, so a resumed run lines up
16
+ * recorded outputs with steps by where they ran. A step with a `replayKey` is keyed
17
+ * `key:<replayKey>` instead: content-addressed, so it finds its output wherever it
18
+ * runs. A `durable: false` step has no key and is never recorded.
19
+ *
14
20
  * Sharding bounds the cost of one append but not the total, so retention is bounded on
15
21
  * two axes (`StepLedgerRetention`): an oversized entry is dropped rather than written,
16
22
  * and past `maxEntries` the oldest entry is evicted. Both degradations are safe by
@@ -19,7 +25,9 @@
19
25
  */
20
26
  import type { StorageAdapter } from '@noetic-tools/context';
21
27
  import { z } from 'zod';
22
- /** @public One completed step, keyed by its execution path. */
28
+ /** Marks a ledger key as a step's `replayKey` rather than its execution path. */
29
+ export declare const REPLAY_KEY_PREFIX = "key:";
30
+ /** @public One completed step, keyed by its execution path or its `key:` replay key. */
23
31
  export declare const StepLedgerEntrySchema: z.ZodObject<{
24
32
  path: z.ZodString;
25
33
  stepId: z.ZodString;
@@ -153,9 +161,11 @@ export declare class StepLedger {
153
161
  get stats(): StepLedgerStats;
154
162
  /**
155
163
  * The recorded output for `path`, when a previous run completed the same step there.
156
- * A divergence (different step id or kind at this path) discards the entry and every
157
- * entry recorded beneath it, then returns undefined so the step runs fresh — the
158
- * subtree's recorded outputs belong to a branch that no longer exists.
164
+ * A divergence (different step id or kind at this key) returns undefined so the step
165
+ * runs fresh. At a positional path it also discards every entry recorded beneath it —
166
+ * the subtree's recorded outputs belong to a branch that no longer exists. A `key:`
167
+ * entry discards only itself: its children are keyed by their own paths or keys, not
168
+ * nested under it.
159
169
  */
160
170
  take(path: string, step: {
161
171
  id: string;
@@ -2,7 +2,7 @@
2
2
  * The step completion ledger: what a resumed execution replays instead of re-running.
3
3
  *
4
4
  * The frontier records which steps were IN FLIGHT at snapshot time; it says nothing
5
- * about which ones finished, and a finished step leaves no trace once `leaveStep` pops
5
+ * about which ones finished, and a finished step leaves no trace once `leaveStep` drops
6
6
  * it. Resume therefore needs a separate record — and it must carry each step's OUTPUT,
7
7
  * not merely a "done" flag: steps consume the previous step's value, and a `callModel` step
8
8
  * re-run to catch up produces a different one than the rest of the run already saw.
@@ -11,6 +11,12 @@
11
11
  * into the single `execution:<id>:snapshot` key would rewrite a growing blob on every
12
12
  * step — O(n²) bytes over a run.
13
13
  *
14
+ * An entry's key is one of two forms (spec 23a § Identity). By default it is the step's
15
+ * execution path (`ContextImpl.currentPath()`): positional, so a resumed run lines up
16
+ * recorded outputs with steps by where they ran. A step with a `replayKey` is keyed
17
+ * `key:<replayKey>` instead: content-addressed, so it finds its output wherever it
18
+ * runs. A `durable: false` step has no key and is never recorded.
19
+ *
14
20
  * Sharding bounds the cost of one append but not the total, so retention is bounded on
15
21
  * two axes (`StepLedgerRetention`): an oversized entry is dropped rather than written,
16
22
  * and past `maxEntries` the oldest entry is evicted. Both degradations are safe by
@@ -21,9 +27,14 @@ import { storageGetMany } from '@noetic-tools/context';
21
27
  import { NoeticConfigError } from '@noetic-tools/types';
22
28
  import { z } from 'zod';
23
29
  //#region Entry
24
- /** @public One completed step, keyed by its execution path. */
30
+ /** Marks a ledger key as a step's `replayKey` rather than its execution path. */
31
+ export const REPLAY_KEY_PREFIX = 'key:';
32
+ /** @public One completed step, keyed by its execution path or its `key:` replay key. */
25
33
  export const StepLedgerEntrySchema = z.object({
26
- /** Execution path key — see `ContextImpl.currentPath()`. Unique per dispatch. */
34
+ /**
35
+ * Ledger key: the execution path (`ContextImpl.currentPath()`, unique per dispatch)
36
+ * or `key:<replayKey>` for a step that sets `replayKey`.
37
+ */
27
38
  path: z.string().min(1),
28
39
  stepId: z.string(),
29
40
  kind: z.string(),
@@ -197,9 +208,11 @@ export class StepLedger {
197
208
  }
198
209
  /**
199
210
  * The recorded output for `path`, when a previous run completed the same step there.
200
- * A divergence (different step id or kind at this path) discards the entry and every
201
- * entry recorded beneath it, then returns undefined so the step runs fresh — the
202
- * subtree's recorded outputs belong to a branch that no longer exists.
211
+ * A divergence (different step id or kind at this key) returns undefined so the step
212
+ * runs fresh. At a positional path it also discards every entry recorded beneath it —
213
+ * the subtree's recorded outputs belong to a branch that no longer exists. A `key:`
214
+ * entry discards only itself: its children are keyed by their own paths or keys, not
215
+ * nested under it.
203
216
  */
204
217
  take(path, step) {
205
218
  const entry = this.replayable.get(path);
@@ -207,6 +220,10 @@ export class StepLedger {
207
220
  return undefined;
208
221
  }
209
222
  if (entry.stepId !== step.id || entry.kind !== step.kind) {
223
+ if (path.startsWith(REPLAY_KEY_PREFIX)) {
224
+ this.replayable.delete(path);
225
+ return undefined;
226
+ }
210
227
  this.discardSubtree(path);
211
228
  return undefined;
212
229
  }
@@ -3,5 +3,5 @@
3
3
  * consumers can validate workflow documents without importing core.
4
4
  * This barrel keeps core's public and intra-core import paths unchanged.
5
5
  */
6
- export type { AcpAgentWorkflowNode, CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, DecideWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, OutputCodecRef, RouteSwitchWorkflowNode, RunCodeWorkflowNode, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode, UntilPredicate, WithContextWorkflowNode, WorkflowDocument, WorkflowGraph, WorkflowNode, } from '@noetic-tools/types';
6
+ export type { AcpAgentWorkflowNode, CallModelWorkflowNode, ConditionalRoute, ConditionalWorkflowNode, DecideWorkflowNode, InParallelWorkflowNode, InvokeToolWorkflowNode, LoopWorkflowNode, OutputCodecRef, RouteSwitchWorkflowNode, RunCodeWorkflowNode, ScheduleWorkflowNode, SequenceWorkflowNode, SpawnWorkflowNode, SubflowWorkflowNode, UntilPredicate, WithContextWorkflowNode, WorkflowDocument, WorkflowGraph, WorkflowNode, WorkflowReplayKey, } from '@noetic-tools/types';
7
7
  export { MergeStrategy, MergeStrategySchema, UntilPredicateSchema, validateWorkflow, WorkflowDocumentSchema, WorkflowNodeSchema, walkWorkflow, workflowDepth, workflowGraph, } from '@noetic-tools/types';
@@ -72,6 +72,26 @@ export declare const ItemLogSnapshotSchema: z.ZodObject<{
72
72
  }, z.core.$strip>;
73
73
  /** @public Item log portion of a snapshot. */
74
74
  export type ItemLogSnapshot = z.infer<typeof ItemLogSnapshotSchema>;
75
+ /**
76
+ * @public
77
+ * Channel state an execution used, captured so a restored execution sees the
78
+ * same values and queued items. Only stored data is kept — parked receivers,
79
+ * parked senders, and subscribers are live promises that cannot round-trip.
80
+ * Topic channels hold no stored data, so they never appear.
81
+ */
82
+ export declare const ChannelSnapshotSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
83
+ name: z.ZodString;
84
+ mode: z.ZodLiteral<"value">;
85
+ value: z.ZodOptional<z.ZodUnknown>;
86
+ hasValue: z.ZodBoolean;
87
+ }, z.core.$strip>, z.ZodObject<{
88
+ name: z.ZodString;
89
+ mode: z.ZodLiteral<"queue">;
90
+ capacity: z.ZodNumber;
91
+ queue: z.ZodArray<z.ZodUnknown>;
92
+ }, z.core.$strip>], "mode">;
93
+ /** @public One channel's stored state inside a checkpoint snapshot. */
94
+ export type ChannelSnapshot = z.infer<typeof ChannelSnapshotSchema>;
75
95
  /**
76
96
  * @public
77
97
  * Top-level checkpoint payload persisted through a `CheckpointStore`. Carries
@@ -101,6 +121,17 @@ export declare const CheckpointSnapshotSchema: z.ZodObject<{
101
121
  itemLog: z.ZodObject<{
102
122
  items: z.ZodArray<z.ZodUnknown>;
103
123
  }, z.core.$strip>;
124
+ channels: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
125
+ name: z.ZodString;
126
+ mode: z.ZodLiteral<"value">;
127
+ value: z.ZodOptional<z.ZodUnknown>;
128
+ hasValue: z.ZodBoolean;
129
+ }, z.core.$strip>, z.ZodObject<{
130
+ name: z.ZodString;
131
+ mode: z.ZodLiteral<"queue">;
132
+ capacity: z.ZodNumber;
133
+ queue: z.ZodArray<z.ZodUnknown>;
134
+ }, z.core.$strip>], "mode">>>;
104
135
  capturedAt: z.ZodString;
105
136
  }, z.core.$strip>;
106
137
  /** @public Snapshot of an execution suitable for `CheckpointStore.save()`. */
@@ -68,6 +68,29 @@ export const PendingAskUserSnapshotSchema = z.object({
68
68
  export const ItemLogSnapshotSchema = z.object({
69
69
  items: z.array(z.unknown()),
70
70
  });
71
+ /**
72
+ * @public
73
+ * Channel state an execution used, captured so a restored execution sees the
74
+ * same values and queued items. Only stored data is kept — parked receivers,
75
+ * parked senders, and subscribers are live promises that cannot round-trip.
76
+ * Topic channels hold no stored data, so they never appear.
77
+ */
78
+ export const ChannelSnapshotSchema = z.discriminatedUnion('mode', [
79
+ z.object({
80
+ name: z.string(),
81
+ mode: z.literal('value'),
82
+ /** The last value written. Absent when the channel was never written. */
83
+ value: z.unknown().optional(),
84
+ hasValue: z.boolean(),
85
+ }),
86
+ z.object({
87
+ name: z.string(),
88
+ mode: z.literal('queue'),
89
+ capacity: z.number(),
90
+ /** Queued items, oldest first. */
91
+ queue: z.array(z.unknown()),
92
+ }),
93
+ ]);
71
94
  /**
72
95
  * @public
73
96
  * Top-level checkpoint payload persisted through a `CheckpointStore`. Carries
@@ -84,6 +107,12 @@ export const CheckpointSnapshotSchema = z.object({
84
107
  cwd: CwdSnapshotSchema.nullable(),
85
108
  askUser: z.array(PendingAskUserSnapshotSchema),
86
109
  itemLog: ItemLogSnapshotSchema,
110
+ /**
111
+ * Stored state of every channel the execution used. Optional so snapshots
112
+ * written before channels were captured still load (they restore with no
113
+ * channel state).
114
+ */
115
+ channels: z.array(ChannelSnapshotSchema).optional(),
87
116
  /** ISO-8601 timestamp of when the snapshot was taken. */
88
117
  capturedAt: z.string(),
89
118
  });
@@ -2,6 +2,11 @@ import type { Until } from '@noetic-tools/types';
2
2
  /**
3
3
  * Combines predicates with OR semantics; stops the loop when any predicate says stop.
4
4
  *
5
+ * Predicates run in order and the first stop verdict short-circuits, so it is
6
+ * returned as is. When none stops, the combined verdict carries every
7
+ * predicate's `reason` (joined with `'; '`) and `feedback` (joined with `'\n'`),
8
+ * so a failing `until.verified()` still hands its feedback to `prepareNext`.
9
+ *
5
10
  * @public
6
11
  * @param predicates - Until predicates to evaluate.
7
12
  * @returns A combined `Until` that short-circuits on the first stop verdict.
@@ -10,6 +15,11 @@ export declare function any(...predicates: Until[]): Until;
10
15
  /**
11
16
  * Combines predicates with AND semantics; stops the loop only when all predicates say stop.
12
17
  *
18
+ * Predicates run in order and the first continue verdict short-circuits, so it
19
+ * is returned as is. When all stop, the combined verdict carries every
20
+ * predicate's `reason` (joined with `'; '`, default `'All predicates
21
+ * satisfied'`) and `feedback` (joined with `'\n'`).
22
+ *
13
23
  * @public
14
24
  * @param predicates - Until predicates to evaluate.
15
25
  * @returns A combined `Until` that requires all predicates to be satisfied.
@@ -1,26 +1,36 @@
1
1
  /**
2
2
  * Combines predicates with OR semantics; stops the loop when any predicate says stop.
3
3
  *
4
+ * Predicates run in order and the first stop verdict short-circuits, so it is
5
+ * returned as is. When none stops, the combined verdict carries every
6
+ * predicate's `reason` (joined with `'; '`) and `feedback` (joined with `'\n'`),
7
+ * so a failing `until.verified()` still hands its feedback to `prepareNext`.
8
+ *
4
9
  * @public
5
10
  * @param predicates - Until predicates to evaluate.
6
11
  * @returns A combined `Until` that short-circuits on the first stop verdict.
7
12
  */
8
13
  export function any(...predicates) {
9
14
  return async (snapshot) => {
15
+ const verdicts = [];
10
16
  for (const pred of predicates) {
11
17
  const verdict = await pred(snapshot);
12
18
  if (verdict.stop) {
13
19
  return verdict;
14
20
  }
21
+ verdicts.push(verdict);
15
22
  }
16
- return {
17
- stop: false,
18
- };
23
+ return merge(false, verdicts);
19
24
  };
20
25
  }
21
26
  /**
22
27
  * Combines predicates with AND semantics; stops the loop only when all predicates say stop.
23
28
  *
29
+ * Predicates run in order and the first continue verdict short-circuits, so it
30
+ * is returned as is. When all stop, the combined verdict carries every
31
+ * predicate's `reason` (joined with `'; '`, default `'All predicates
32
+ * satisfied'`) and `feedback` (joined with `'\n'`).
33
+ *
24
34
  * @public
25
35
  * @param predicates - Until predicates to evaluate.
26
36
  * @returns A combined `Until` that requires all predicates to be satisfied.
@@ -30,20 +40,34 @@ export function all(...predicates) {
30
40
  const verdicts = [];
31
41
  for (const pred of predicates) {
32
42
  const verdict = await pred(snapshot);
33
- verdicts.push(verdict);
34
43
  if (!verdict.stop) {
35
- return {
36
- stop: false,
37
- };
44
+ return verdict;
38
45
  }
46
+ verdicts.push(verdict);
39
47
  }
40
- const reasons = verdicts
41
- .map((v) => v.reason)
42
- .filter(Boolean)
43
- .join('; ');
48
+ const merged = merge(true, verdicts);
44
49
  return {
45
- stop: true,
46
- reason: reasons || 'All predicates satisfied',
50
+ ...merged,
51
+ reason: merged.reason ?? 'All predicates satisfied',
47
52
  };
48
53
  };
49
54
  }
55
+ /** Folds the verdicts that decided a combinator into one, leaving out empty fields. */
56
+ function merge(stop, verdicts) {
57
+ const verdict = {
58
+ stop,
59
+ };
60
+ const reason = join(verdicts.map((v) => v.reason), '; ');
61
+ if (reason !== undefined) {
62
+ verdict.reason = reason;
63
+ }
64
+ const feedback = join(verdicts.map((v) => v.feedback), '\n');
65
+ if (feedback !== undefined) {
66
+ verdict.feedback = feedback;
67
+ }
68
+ return verdict;
69
+ }
70
+ function join(parts, separator) {
71
+ const present = parts.filter((p) => p !== undefined && p !== '');
72
+ return present.length === 0 ? undefined : present.join(separator);
73
+ }
@@ -18,7 +18,9 @@ export const until = {
18
18
  maxCost(usd) {
19
19
  return (snap) => ({
20
20
  stop: snap.cost >= usd,
21
- reason: snap.cost >= usd ? `Reached max cost ($${usd})` : undefined,
21
+ reason: snap.cost >= usd
22
+ ? `Cost $${snap.cost.toFixed(2)} exceeded budget $${usd.toFixed(2)}`
23
+ : undefined,
22
24
  });
23
25
  },
24
26
  /** Stops the loop when elapsed time reaches `ms` milliseconds. @public */
@@ -52,7 +54,7 @@ export const until = {
52
54
  const result = await fn(snap.lastOutput);
53
55
  return {
54
56
  stop: result.pass,
55
- reason: result.pass ? 'Verification passed' : undefined,
57
+ reason: result.pass ? 'Verification passed' : 'Verification failed',
56
58
  feedback: result.feedback,
57
59
  };
58
60
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/core",
3
- "version": "5.7.0",
3
+ "version": "5.9.0",
4
4
  "type": "module",
5
5
  "description": "Step primitives, interpreter, runtime, and context layer contract for Noetic agents.",
6
6
  "license": "Apache-2.0",
@@ -68,8 +68,8 @@
68
68
  }
69
69
  },
70
70
  "dependencies": {
71
- "@noetic-tools/context": "^1.4.0",
72
- "@noetic-tools/types": "^3.5.0",
71
+ "@noetic-tools/context": "^1.6.0",
72
+ "@noetic-tools/types": "^3.7.0",
73
73
  "@openrouter/agent": "^0.11.0",
74
74
  "@openrouter/sdk": "1.4.25",
75
75
  "zod": "^4.0.0"