@noetic-tools/core 3.1.0 → 4.0.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 (101) hide show
  1. package/dist/adapters/in-memory-subprocess/manifest-persistence.d.ts +1 -1
  2. package/dist/adapters/in-memory-subprocess/types.d.ts +1 -1
  3. package/dist/adapters/openrouter.d.ts +2 -2
  4. package/dist/adapters/openrouter.js +2 -2
  5. package/dist/builders/context-builder.d.ts +11 -0
  6. package/dist/builders/context-builder.js +15 -0
  7. package/dist/builders/context-option.d.ts +23 -0
  8. package/dist/builders/context-option.js +22 -0
  9. package/dist/builders/control-flow-builders.d.ts +19 -19
  10. package/dist/builders/every.d.ts +4 -4
  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/loop-builder.d.ts +3 -3
  14. package/dist/builders/provide-builder.d.ts +14 -7
  15. package/dist/builders/provide-builder.js +15 -4
  16. package/dist/builders/spawn-builder.d.ts +8 -6
  17. package/dist/builders/spawn-builder.js +7 -2
  18. package/dist/builders/step-builders.d.ts +19 -20
  19. package/dist/builders/tool-builder.d.ts +9 -5
  20. package/dist/builders/tool-builder.js +3 -2
  21. package/dist/builders/workflow-hydrator.d.ts +17 -4
  22. package/dist/builders/workflow-hydrator.js +87 -8
  23. package/dist/builders/workflow-step.d.ts +61 -0
  24. package/dist/builders/workflow-step.js +126 -0
  25. package/dist/conditions/conditions.d.ts +17 -17
  26. package/dist/conditions/conditions.js +1 -1
  27. package/dist/deprecated.d.ts +30 -0
  28. package/dist/deprecated.js +28 -0
  29. package/dist/harness/agent-harness.d.ts +84 -27
  30. package/dist/harness/agent-harness.js +101 -20
  31. package/dist/harness/deps/context.d.ts +2 -0
  32. package/dist/harness/deps/context.js +1 -0
  33. package/dist/harness/deps/runtime.d.ts +4 -4
  34. package/dist/harness/deps/runtime.js +3 -3
  35. package/dist/harness/deps/types.d.ts +1 -1
  36. package/dist/harness/model-schema.d.ts +2 -2
  37. package/dist/index.d.ts +23 -19
  38. package/dist/index.js +15 -9
  39. package/dist/interpreter/action-deps.d.ts +3 -3
  40. package/dist/interpreter/action-deps.js +2 -2
  41. package/dist/interpreter/action-types.d.ts +1 -1
  42. package/dist/interpreter/collect-tools.d.ts +1 -1
  43. package/dist/interpreter/execute-action.d.ts +10 -10
  44. package/dist/interpreter/execute-action.js +33 -14
  45. package/dist/interpreter/execute-control.d.ts +4 -4
  46. package/dist/interpreter/execute-control.js +25 -16
  47. package/dist/interpreter/execute-sub-harness.d.ts +1 -1
  48. package/dist/interpreter/execute-sub-harness.js +22 -1
  49. package/dist/interpreter/execute.d.ts +3 -3
  50. package/dist/interpreter/execute.js +78 -31
  51. package/dist/interpreter/step-dispatcher.d.ts +3 -3
  52. package/dist/interpreter/sub-harness-events.d.ts +2 -2
  53. package/dist/interpreter/typeguards.d.ts +4 -4
  54. package/dist/patterns/dynamic-workflow.d.ts +7 -4
  55. package/dist/patterns/dynamic-workflow.js +2 -0
  56. package/dist/patterns/interview.d.ts +2 -2
  57. package/dist/patterns/plans.d.ts +5 -5
  58. package/dist/patterns/ralph-wiggum.d.ts +2 -2
  59. package/dist/patterns/react.d.ts +7 -5
  60. package/dist/patterns/react.js +6 -4
  61. package/dist/portable/builders.js +2 -0
  62. package/dist/portable/context-layers.d.ts +8 -0
  63. package/dist/portable/context-layers.js +8 -0
  64. package/dist/portable.d.ts +2 -2
  65. package/dist/portable.js +2 -2
  66. package/dist/runtime/context-deps.d.ts +2 -2
  67. package/dist/runtime/context-deps.js +1 -1
  68. package/dist/runtime/context-impl.d.ts +48 -9
  69. package/dist/runtime/context-impl.js +95 -10
  70. package/dist/runtime/context-type-deps.d.ts +1 -1
  71. package/dist/runtime/durable/checkpoint-store.d.ts +1 -1
  72. package/dist/runtime/durable/harness-checkpoints.d.ts +17 -4
  73. package/dist/runtime/durable/harness-checkpoints.js +21 -3
  74. package/dist/runtime/durable/index.d.ts +2 -0
  75. package/dist/runtime/durable/index.js +1 -0
  76. package/dist/runtime/durable/step-ledger.d.ts +107 -5
  77. package/dist/runtime/durable/step-ledger.js +179 -10
  78. package/dist/runtime/in-memory-storage.d.ts +1 -1
  79. package/dist/runtime/session-runner.d.ts +1 -1
  80. package/dist/runtime/step-registry.d.ts +3 -3
  81. package/dist/runtime/{tool-memory.d.ts → tool-context.d.ts} +1 -2
  82. package/dist/runtime/{tool-memory.js → tool-context.js} +5 -2
  83. package/dist/runtime/tool-ui.d.ts +1 -1
  84. package/dist/runtime/tool-ui.js +1 -1
  85. package/dist/schemas/workflow.d.ts +5 -279
  86. package/dist/schemas/workflow.js +4 -376
  87. package/dist/types/mutable-context.d.ts +2 -2
  88. package/dist/types/step-registrar.d.ts +1 -1
  89. package/dist/unstable.d.ts +5 -5
  90. package/dist/unstable.js +3 -3
  91. package/dist/until/predicates.d.ts +1 -1
  92. package/package.json +4 -4
  93. package/schema/noetic-workflow.schema.json +28 -0
  94. package/dist/builders/memory-builder.d.ts +0 -11
  95. package/dist/builders/memory-builder.js +0 -15
  96. package/dist/harness/deps/memory.d.ts +0 -2
  97. package/dist/harness/deps/memory.js +0 -1
  98. package/dist/patterns/flow.d.ts +0 -2
  99. package/dist/patterns/flow.js +0 -1
  100. package/dist/portable/memory-layers.d.ts +0 -8
  101. package/dist/portable/memory-layers.js +0 -8
@@ -8,3 +8,5 @@ export type { SessionSeedHarness } from './session-seed.js';
8
8
  export { seedFromItems } from './session-seed.js';
9
9
  export type { CreateNudgeMessageOpts, StallNudgeOpts } from './stall-nudge.js';
10
10
  export { createNudgeMessage, createStallNudgeHook, DEFAULT_NUDGE_MESSAGE_TEXT, } from './stall-nudge.js';
11
+ export type { StepLedgerRetention, StepLedgerStats } from './step-ledger.js';
12
+ export { DEFAULT_STEP_LEDGER_RETENTION } from './step-ledger.js';
@@ -3,3 +3,4 @@ export { createDetachedSignal } from './detached-signal.js';
3
3
  export { runnableLoop } from './runnable-loop.js';
4
4
  export { seedFromItems } from './session-seed.js';
5
5
  export { createNudgeMessage, createStallNudgeHook, DEFAULT_NUDGE_MESSAGE_TEXT, } from './stall-nudge.js';
6
+ export { DEFAULT_STEP_LEDGER_RETENTION } from './step-ledger.js';
@@ -10,8 +10,14 @@
10
10
  * Entries are stored one key per step so appends are O(1). Folding the whole ledger
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
+ *
14
+ * Sharding bounds the cost of one append but not the total, so retention is bounded on
15
+ * two axes (`StepLedgerRetention`): an oversized entry is dropped rather than written,
16
+ * and past `maxEntries` the oldest entry is evicted. Both degradations are safe by
17
+ * construction — a missing entry re-runs its step, which costs work rather than
18
+ * correctness.
13
19
  */
14
- import type { StorageAdapter } from '@noetic-tools/memory';
20
+ import type { StorageAdapter } from '@noetic-tools/context';
15
21
  import { z } from 'zod';
16
22
  /** @public One completed step, keyed by its execution path. */
17
23
  export declare const StepLedgerEntrySchema: z.ZodObject<{
@@ -23,46 +29,128 @@ export declare const StepLedgerEntrySchema: z.ZodObject<{
23
29
  }, z.core.$strip>;
24
30
  /** @public A completed step recorded for replay. */
25
31
  export type StepLedgerEntry = z.infer<typeof StepLedgerEntrySchema>;
32
+ /**
33
+ * @public
34
+ * Bounds on what one execution's ledger retains. Both caps degrade resume rather than
35
+ * break it: a step with no entry simply runs again, so the worst case is repeated work
36
+ * (and repeated side effects — see `specs/23a-step-level-resume` § "The side-effect
37
+ * boundary"), never a replayed value that disagrees with the recorded run.
38
+ */
39
+ export interface StepLedgerRetention {
40
+ /**
41
+ * Largest output, in UTF-8 bytes of its JSON encoding, that will be recorded. A step
42
+ * whose output exceeds this is not recorded at all and re-runs on resume. Keeps one
43
+ * entry inside the per-value limit real storage backends impose (128 KiB on several)
44
+ * instead of failing the write. Pass `Infinity` to record any size.
45
+ *
46
+ * Defaults to `DEFAULT_STEP_LEDGER_RETENTION.maxEntryBytes` (128 KiB).
47
+ */
48
+ maxEntryBytes?: number;
49
+ /**
50
+ * Most entries retained for one execution. Recording past the cap evicts the oldest
51
+ * entry, so resume is best-effort over a bounded *suffix* of the run: the tail of the
52
+ * work is skipped, the head runs again. Pass `Infinity` to retain every entry.
53
+ *
54
+ * Defaults to `DEFAULT_STEP_LEDGER_RETENTION.maxEntries` (1000).
55
+ */
56
+ maxEntries?: number;
57
+ }
58
+ /** @public Retention applied when a harness does not configure `stepLedgerRetention`. */
59
+ export declare const DEFAULT_STEP_LEDGER_RETENTION: {
60
+ readonly maxEntryBytes: number;
61
+ readonly maxEntries: 1000;
62
+ };
63
+ /**
64
+ * @public
65
+ * Fill in defaults and reject nonsense. Validating loudly matters because every
66
+ * misconfiguration here fails *silently* at runtime — a negative cap would drop every
67
+ * entry and the run would look fine while resuming nothing.
68
+ *
69
+ * @throws NoeticConfigError `code: 'STEP_LEDGER_RETENTION_INVALID'`
70
+ */
71
+ export declare function resolveStepLedgerRetention(retention?: StepLedgerRetention): Required<StepLedgerRetention>;
26
72
  /** Storage key prefix owning every ledger entry for one execution. */
27
73
  export declare function stepLedgerPrefix(executionId: string): string;
74
+ /**
75
+ * @public
76
+ * Everything one execution's ledger recovered from storage: the entries available for
77
+ * replay plus the sequence bookkeeping a further append needs. `nextSeq` cannot be
78
+ * derived from `entries.size` — retention leaves gaps, and reusing a live sequence
79
+ * number would overwrite an entry the resumed run still has to replay.
80
+ */
81
+ export interface StepLedgerWindow {
82
+ /** Recovered entries, keyed by execution path. Corrupt rows are skipped. */
83
+ entries: Map<string, StepLedgerEntry>;
84
+ /** Sequence number the next append takes: highest recorded, plus one. */
85
+ nextSeq: number;
86
+ /** Lowest sequence number still in storage — where eviction resumes. */
87
+ oldestSeq: number;
88
+ }
28
89
  /**
29
90
  * @public
30
91
  * Durable append-only ledger for one harness's executions. Backed by the same
31
92
  * `StorageAdapter` as the checkpoint store; it reserves the `:ledger:` suffix that
32
93
  * `CheckpointKeys` already sets aside.
94
+ *
95
+ * The store owns the key layout only. Retention policy lives in `StepLedger`, which
96
+ * holds the per-execution sequence cursor that eviction needs.
33
97
  */
34
98
  export interface StepLedgerStore {
35
99
  /** Record a completed step. Sequence numbers order entries within an execution. */
36
100
  append: (executionId: string, seq: number, entry: StepLedgerEntry) => Promise<void>;
37
- /** Every recorded entry for an execution, keyed by path. Corrupt rows are skipped. */
38
- load: (executionId: string) => Promise<Map<string, StepLedgerEntry>>;
101
+ /** Drop one entry by sequence number. No-op when the key is absent. */
102
+ delete: (executionId: string, seq: number) => Promise<void>;
103
+ /** Everything recorded for an execution, plus its sequence bounds. */
104
+ load: (executionId: string) => Promise<StepLedgerWindow>;
39
105
  /** Drop an execution's ledger (paired with `CheckpointStore.clear`). */
40
106
  clear: (executionId: string) => Promise<void>;
41
107
  }
42
108
  export declare function createStepLedgerStore(opts: {
43
109
  storage: StorageAdapter;
44
110
  }): StepLedgerStore;
111
+ /** @public What retention did to one execution's ledger. Observable for host logging. */
112
+ export interface StepLedgerStats {
113
+ /** Entries written. */
114
+ recorded: number;
115
+ /** Entries not written because their output exceeded `maxEntryBytes`. */
116
+ droppedOversize: number;
117
+ /** Entries not written because their output does not survive JSON encoding. */
118
+ droppedUnserialisable: number;
119
+ /** Entries deleted to stay within `maxEntries`. */
120
+ evicted: number;
121
+ }
45
122
  /**
46
123
  * @public
47
124
  * The in-memory ledger a single execution carries: entries recovered from a previous
48
125
  * run (available for replay) plus the sequence counter for newly recorded ones.
49
126
  *
50
127
  * Shared by reference across fork/spawn children so one execution has one ledger —
51
- * path keys are globally unique across the step tree, so a flat map is correct.
128
+ * path keys are globally unique across the step tree, so a flat map is correct. That
129
+ * sharing is also why retention lives here rather than in the store: the sequence
130
+ * cursor eviction walks belongs to the execution, not to the harness.
52
131
  */
53
132
  export declare class StepLedger {
54
133
  /** Recovered entries, by path. Consumed as replay proceeds. */
55
134
  private readonly replayable;
135
+ /** Sequence number the next recorded entry takes. */
56
136
  private seq;
137
+ /** Lowest sequence number still in storage — advances as eviction proceeds. */
138
+ private oldestSeq;
57
139
  private readonly store?;
58
140
  private readonly executionId;
141
+ private readonly retention;
142
+ private readonly counts;
59
143
  constructor(opts: {
60
144
  executionId: string;
61
145
  store?: StepLedgerStore;
62
- recovered?: Map<string, StepLedgerEntry>;
146
+ /** What `StepLedgerStore.load` recovered. Absent on a fresh execution. */
147
+ recovered?: StepLedgerWindow;
148
+ retention?: StepLedgerRetention;
63
149
  });
64
150
  /** True when this execution has nothing recovered to replay (the common, fresh case). */
65
151
  get isEmpty(): boolean;
152
+ /** What retention has done so far. A snapshot — safe to hold. */
153
+ get stats(): StepLedgerStats;
66
154
  /**
67
155
  * The recorded output for `path`, when a previous run completed the same step there.
68
156
  * A divergence (different step id or kind at this path) discards the entry and every
@@ -77,4 +165,18 @@ export declare class StepLedger {
77
165
  private discardSubtree;
78
166
  /** Record a completed step. Best-effort: a failed write costs resumability, not the run. */
79
167
  record(entry: StepLedgerEntry): Promise<void>;
168
+ /**
169
+ * Whether this entry is small enough — and encodable at all — to be worth a write.
170
+ * An output no adapter could serialise is treated exactly like an oversized one:
171
+ * not recorded, so the step re-runs.
172
+ */
173
+ private retainable;
174
+ /**
175
+ * Delete oldest-first until the execution is back inside `maxEntries`. The bound is on
176
+ * the sequence *window* rather than an exact row count, which is what keeps this
177
+ * correct without a `list()` per append.
178
+ */
179
+ private evictOverflow;
180
+ /** One warning per reason per execution — a long run would otherwise flood the log. */
181
+ private warnOnFirstDrop;
80
182
  }
@@ -10,9 +10,17 @@
10
10
  * Entries are stored one key per step so appends are O(1). Folding the whole ledger
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
+ *
14
+ * Sharding bounds the cost of one append but not the total, so retention is bounded on
15
+ * two axes (`StepLedgerRetention`): an oversized entry is dropped rather than written,
16
+ * and past `maxEntries` the oldest entry is evicted. Both degradations are safe by
17
+ * construction — a missing entry re-runs its step, which costs work rather than
18
+ * correctness.
13
19
  */
14
- import { storageGetMany } from '@noetic-tools/memory';
20
+ import { storageGetMany } from '@noetic-tools/context';
21
+ import { NoeticConfigError } from '@noetic-tools/types';
15
22
  import { z } from 'zod';
23
+ //#region Entry
16
24
  /** @public One completed step, keyed by its execution path. */
17
25
  export const StepLedgerEntrySchema = z.object({
18
26
  /** Execution path key — see `ContextImpl.currentPath()`. Unique per dispatch. */
@@ -23,6 +31,55 @@ export const StepLedgerEntrySchema = z.object({
23
31
  output: z.unknown(),
24
32
  completedAt: z.string(),
25
33
  });
34
+ /** @public Retention applied when a harness does not configure `stepLedgerRetention`. */
35
+ export const DEFAULT_STEP_LEDGER_RETENTION = {
36
+ maxEntryBytes: 128 * 1024,
37
+ maxEntries: 1e3,
38
+ };
39
+ /**
40
+ * @public
41
+ * Fill in defaults and reject nonsense. Validating loudly matters because every
42
+ * misconfiguration here fails *silently* at runtime — a negative cap would drop every
43
+ * entry and the run would look fine while resuming nothing.
44
+ *
45
+ * @throws NoeticConfigError `code: 'STEP_LEDGER_RETENTION_INVALID'`
46
+ */
47
+ export function resolveStepLedgerRetention(retention) {
48
+ const resolved = {
49
+ maxEntryBytes: retention?.maxEntryBytes ?? DEFAULT_STEP_LEDGER_RETENTION.maxEntryBytes,
50
+ maxEntries: retention?.maxEntries ?? DEFAULT_STEP_LEDGER_RETENTION.maxEntries,
51
+ };
52
+ for (const [field, value] of Object.entries(resolved)) {
53
+ if (value === Number.POSITIVE_INFINITY) {
54
+ continue;
55
+ }
56
+ if (!Number.isFinite(value) || value < 1) {
57
+ throw new NoeticConfigError({
58
+ code: 'STEP_LEDGER_RETENTION_INVALID',
59
+ message: `stepLedgerRetention.${field} must be a positive number or Infinity (received ${String(value)}).`,
60
+ hint: 'Omit the field to accept the default, or pass Infinity to disable the cap.',
61
+ });
62
+ }
63
+ }
64
+ return resolved;
65
+ }
66
+ /**
67
+ * Whether `json` fits `cap` UTF-8 bytes. A UTF-16 code unit encodes to between 1 and 3
68
+ * UTF-8 bytes (a surrogate pair is 2 units → 4 bytes, i.e. 2 per unit), so string
69
+ * length brackets the byte count and decides most calls without encoding a megabyte
70
+ * of output just to measure it.
71
+ */
72
+ function fitsByteCap(json, cap) {
73
+ if (json.length > cap) {
74
+ return false;
75
+ }
76
+ if (json.length * 3 <= cap) {
77
+ return true;
78
+ }
79
+ return new TextEncoder().encode(json).length <= cap;
80
+ }
81
+ //#endregion
82
+ //#region Store
26
83
  /** Storage key prefix owning every ledger entry for one execution. */
27
84
  export function stepLedgerPrefix(executionId) {
28
85
  return `execution:${executionId}:ledger:`;
@@ -31,22 +88,43 @@ export function stepLedgerPrefix(executionId) {
31
88
  function seqKey(executionId, seq) {
32
89
  return `${stepLedgerPrefix(executionId)}${String(seq).padStart(8, '0')}`;
33
90
  }
91
+ /** The sequence number a ledger key carries, or null when the suffix is not one. */
92
+ function seqFromKey(executionId, key) {
93
+ const prefix = stepLedgerPrefix(executionId);
94
+ if (!key.startsWith(prefix)) {
95
+ return null;
96
+ }
97
+ const seq = Number(key.slice(prefix.length));
98
+ return Number.isInteger(seq) && seq >= 0 ? seq : null;
99
+ }
34
100
  export function createStepLedgerStore(opts) {
35
101
  const { storage } = opts;
36
102
  return {
37
103
  append: async (executionId, seq, entry) => {
38
104
  await storage.set(seqKey(executionId, seq), entry);
39
105
  },
106
+ delete: async (executionId, seq) => {
107
+ await storage.delete(seqKey(executionId, seq));
108
+ },
40
109
  load: async (executionId) => {
41
110
  const keys = await storage.list(stepLedgerPrefix(executionId));
42
111
  /* One batch read, not one round trip per completed step. Recovery is the
43
112
  * moment a network-backed adapter can least afford an N+1. */
44
- const entries = await storageGetMany(storage, keys);
45
- const byPath = new Map();
113
+ const raws = await storageGetMany(storage, keys);
114
+ const entries = new Map();
115
+ /* Sequence bounds come from the KEYS, not the parsed rows: a row that fails to
116
+ * parse still occupies its key, so it must not be handed back out as reusable. */
117
+ let highestSeq = -1;
118
+ let oldestSeq = -1;
46
119
  // Walk `keys`, not the map: `seqKey`'s zero-padding makes `list()` order
47
120
  // dispatch order, and a later entry at a path must win over an earlier one.
48
121
  for (const key of keys) {
49
- const raw = entries.get(key);
122
+ const seq = seqFromKey(executionId, key);
123
+ if (seq !== null) {
124
+ highestSeq = Math.max(highestSeq, seq);
125
+ oldestSeq = oldestSeq === -1 ? seq : Math.min(oldestSeq, seq);
126
+ }
127
+ const raw = raws.get(key);
50
128
  if (raw === undefined) {
51
129
  continue;
52
130
  }
@@ -57,9 +135,13 @@ export function createStepLedgerStore(opts) {
57
135
  console.warn(`StepLedger: discarding unreadable entry "${key}": ${parsed.error.message}`);
58
136
  continue;
59
137
  }
60
- byPath.set(parsed.data.path, parsed.data);
138
+ entries.set(parsed.data.path, parsed.data);
61
139
  }
62
- return byPath;
140
+ return {
141
+ entries,
142
+ nextSeq: highestSeq + 1,
143
+ oldestSeq: oldestSeq === -1 ? 0 : oldestSeq,
144
+ };
63
145
  },
64
146
  clear: async (executionId) => {
65
147
  const keys = await storage.list(stepLedgerPrefix(executionId));
@@ -75,24 +157,44 @@ export function createStepLedgerStore(opts) {
75
157
  * run (available for replay) plus the sequence counter for newly recorded ones.
76
158
  *
77
159
  * Shared by reference across fork/spawn children so one execution has one ledger —
78
- * path keys are globally unique across the step tree, so a flat map is correct.
160
+ * path keys are globally unique across the step tree, so a flat map is correct. That
161
+ * sharing is also why retention lives here rather than in the store: the sequence
162
+ * cursor eviction walks belongs to the execution, not to the harness.
79
163
  */
80
164
  export class StepLedger {
81
165
  /** Recovered entries, by path. Consumed as replay proceeds. */
82
166
  replayable;
167
+ /** Sequence number the next recorded entry takes. */
83
168
  seq;
169
+ /** Lowest sequence number still in storage — advances as eviction proceeds. */
170
+ oldestSeq;
84
171
  store;
85
172
  executionId;
173
+ retention;
174
+ counts = {
175
+ recorded: 0,
176
+ droppedOversize: 0,
177
+ droppedUnserialisable: 0,
178
+ evicted: 0,
179
+ };
86
180
  constructor(opts) {
87
181
  this.executionId = opts.executionId;
88
182
  this.store = opts.store;
89
- this.replayable = opts.recovered ?? new Map();
90
- this.seq = this.replayable.size;
183
+ this.retention = resolveStepLedgerRetention(opts.retention);
184
+ this.replayable = opts.recovered?.entries ?? new Map();
185
+ this.seq = opts.recovered?.nextSeq ?? 0;
186
+ this.oldestSeq = opts.recovered?.oldestSeq ?? 0;
91
187
  }
92
188
  /** True when this execution has nothing recovered to replay (the common, fresh case). */
93
189
  get isEmpty() {
94
190
  return this.replayable.size === 0;
95
191
  }
192
+ /** What retention has done so far. A snapshot — safe to hold. */
193
+ get stats() {
194
+ return {
195
+ ...this.counts,
196
+ };
197
+ }
96
198
  /**
97
199
  * The recorded output for `path`, when a previous run completed the same step there.
98
200
  * A divergence (different step id or kind at this path) discards the entry and every
@@ -125,13 +227,80 @@ export class StepLedger {
125
227
  if (!this.store) {
126
228
  return;
127
229
  }
230
+ if (!this.retainable(entry)) {
231
+ return;
232
+ }
233
+ /* Reserve the sequence number BEFORE awaiting: concurrent fork legs record through
234
+ * the one shared ledger, and two of them reading the same counter would write the
235
+ * same key, silently overwriting a sibling's entry. A write that then fails leaves a
236
+ * gap, which only makes the window below a slight over-estimate of the live count —
237
+ * eviction runs marginally early, never late. */
128
238
  const seq = this.seq;
129
- this.seq += 1;
239
+ this.seq = seq + 1;
130
240
  try {
131
241
  await this.store.append(this.executionId, seq, entry);
132
242
  }
133
243
  catch (error) {
134
244
  console.warn(`StepLedger: failed to record "${entry.path}":`, error instanceof Error ? error.message : String(error));
245
+ return;
246
+ }
247
+ this.counts.recorded += 1;
248
+ await this.evictOverflow();
249
+ }
250
+ /**
251
+ * Whether this entry is small enough — and encodable at all — to be worth a write.
252
+ * An output no adapter could serialise is treated exactly like an oversized one:
253
+ * not recorded, so the step re-runs.
254
+ */
255
+ retainable(entry) {
256
+ let json;
257
+ try {
258
+ json = JSON.stringify(entry.output);
259
+ }
260
+ catch {
261
+ this.counts.droppedUnserialisable += 1;
262
+ this.warnOnFirstDrop(this.counts.droppedUnserialisable, `StepLedger: not recording "${entry.path}" — its output does not survive JSON encoding.`);
263
+ return false;
264
+ }
265
+ // `undefined` stringifies to nothing; there is no size to bound.
266
+ if (json !== undefined && !fitsByteCap(json, this.retention.maxEntryBytes)) {
267
+ this.counts.droppedOversize += 1;
268
+ this.warnOnFirstDrop(this.counts.droppedOversize, `StepLedger: not recording "${entry.path}" — output exceeds maxEntryBytes (${this.retention.maxEntryBytes}).`);
269
+ return false;
270
+ }
271
+ return true;
272
+ }
273
+ /**
274
+ * Delete oldest-first until the execution is back inside `maxEntries`. The bound is on
275
+ * the sequence *window* rather than an exact row count, which is what keeps this
276
+ * correct without a `list()` per append.
277
+ */
278
+ async evictOverflow() {
279
+ const store = this.store;
280
+ if (!store) {
281
+ return;
282
+ }
283
+ while (this.seq - this.oldestSeq > this.retention.maxEntries) {
284
+ const victim = this.oldestSeq;
285
+ try {
286
+ await store.delete(this.executionId, victim);
287
+ }
288
+ catch (error) {
289
+ /* Leave the cursor where it is and stop: the next append retries this victim
290
+ * rather than spinning on a store that is currently refusing deletes. */
291
+ console.warn(`StepLedger: failed to evict entry ${victim} of "${this.executionId}":`, error instanceof Error ? error.message : String(error));
292
+ return;
293
+ }
294
+ this.oldestSeq = victim + 1;
295
+ this.counts.evicted += 1;
296
+ }
297
+ }
298
+ /** One warning per reason per execution — a long run would otherwise flood the log. */
299
+ warnOnFirstDrop(count, message) {
300
+ if (count > 1) {
301
+ return;
135
302
  }
303
+ console.warn(`${message} The step will re-run on resume; further drops are silent.`);
136
304
  }
137
305
  }
306
+ //#endregion
@@ -1,4 +1,4 @@
1
- import type { StorageAdapter } from '@noetic-tools/memory';
1
+ import type { StorageAdapter } from '@noetic-tools/context';
2
2
  /**
3
3
  * @public Process-local, Map-backed StorageAdapter.
4
4
  *
@@ -16,7 +16,7 @@ export interface TurnContext {
16
16
  * (so the runner is the single point of ExecuteInput → Item[] conversion),
17
17
  * plus the messages being delivered this turn so the harness can honour
18
18
  * per-turn `ExecuteOptions` (the first message's options establish
19
- * resourceId/state/memory for the turn). */
19
+ * resourceId/state/context for the turn). */
20
20
  export type CreateContextFn = (items: ReadonlyArray<Item>, turnId: string, messages: ReadonlyArray<QueuedMessage>) => Context;
21
21
  export interface SessionRunnerOpts {
22
22
  readonly threadId: string;
@@ -1,10 +1,10 @@
1
- import type { ContextMemory } from '@noetic-tools/memory';
1
+ import type { ContextData } from '@noetic-tools/context';
2
2
  import type { Step } from '@noetic-tools/types';
3
3
  /** Registry-wide step type — stored at the widest generic parameters so
4
4
  * callers can register steps with narrower input/output types without
5
5
  * fighting variance. The public `Step` default already uses `unknown` for
6
6
  * the input and output parameters, matching this shape. */
7
- type RegisteredStep = Step<ContextMemory, unknown, unknown>;
7
+ type RegisteredStep = Step<ContextData, unknown, unknown>;
8
8
  /**
9
9
  * @public
10
10
  * Registers a step so the subprocess adapter can look it up by id.
@@ -18,7 +18,7 @@ type RegisteredStep = Step<ContextMemory, unknown, unknown>;
18
18
  * `clearRegistry` wiring is deferred — see the tracked follow-up task on
19
19
  * the team's board (strict-registry-plus-test-clear).
20
20
  */
21
- export declare function registerStep<TMemory, I, O>(step: Step<TMemory, I, O>): void;
21
+ export declare function registerStep<TContext, I, O>(step: Step<TContext, I, O>): void;
22
22
  /**
23
23
  * @public
24
24
  * Looks up a registered step by id. Returns `null` when the id is not known
@@ -1,4 +1,3 @@
1
- import type { AgentHarnessContract, Context, ToolExecutionContext, ToolMemory } from '@noetic-tools/types';
1
+ import type { AgentHarnessContract, Context, ToolExecutionContext } from '@noetic-tools/types';
2
2
  import type { TurnContext } from '@openrouter/agent';
3
- export declare function buildToolMemory(harness: AgentHarnessContract, ctx: Context): ToolMemory;
4
3
  export declare function buildToolExecutionContext(ctx: Context, harness: AgentHarnessContract, turnContext?: TurnContext): ToolExecutionContext;
@@ -1,4 +1,4 @@
1
- export function buildToolMemory(harness, ctx) {
1
+ function buildToolContext(harness, ctx) {
2
2
  return {
3
3
  get(layerId) {
4
4
  return harness.getLayerState(ctx.id, layerId);
@@ -9,12 +9,15 @@ export function buildToolMemory(harness, ctx) {
9
9
  };
10
10
  }
11
11
  export function buildToolExecutionContext(ctx, harness, turnContext) {
12
+ const toolContext = buildToolContext(harness, ctx);
12
13
  return {
13
14
  ctx,
14
15
  harness,
15
16
  fs: harness.fs,
16
17
  shell: harness.shell,
17
- memory: buildToolMemory(harness, ctx),
18
+ context: toolContext,
19
+ // Deprecated alias — same accessor object, not a copy.
20
+ memory: toolContext,
18
21
  assembledView: ctx.itemLog.items,
19
22
  lastStepMeta: ctx.lastStepMeta,
20
23
  turnContext,
@@ -5,7 +5,7 @@
5
5
  * render functions at the call / progress / result / error lifecycle points and
6
6
  * forwards each returned `UiFragment` as an `openui.fragment` framework event
7
7
  * (namespaced `${agentName}:openui.fragment`). The framework never interprets
8
- * the fragment source — a UI surface (memory layer + transport) composes it.
8
+ * the fragment source — a UI surface (context layer + transport) composes it.
9
9
  *
10
10
  * The `ui` methods are declared bivariantly on `ToolUiDeclaration`, so the
11
11
  * erased `Tool` handle the runtime holds can call them with runtime values.
@@ -5,7 +5,7 @@
5
5
  * render functions at the call / progress / result / error lifecycle points and
6
6
  * forwards each returned `UiFragment` as an `openui.fragment` framework event
7
7
  * (namespaced `${agentName}:openui.fragment`). The framework never interprets
8
- * the fragment source — a UI surface (memory layer + transport) composes it.
8
+ * the fragment source — a UI surface (context layer + transport) composes it.
9
9
  *
10
10
  * The `ui` methods are declared bivariantly on `ToolUiDeclaration`, so the
11
11
  * erased `Tool` handle the runtime holds can call them with runtime values.