@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.
- package/dist/adapters/in-memory-subprocess/manifest-persistence.d.ts +1 -1
- package/dist/adapters/in-memory-subprocess/types.d.ts +1 -1
- package/dist/adapters/openrouter.d.ts +2 -2
- package/dist/adapters/openrouter.js +2 -2
- package/dist/builders/context-builder.d.ts +11 -0
- package/dist/builders/context-builder.js +15 -0
- package/dist/builders/context-option.d.ts +23 -0
- package/dist/builders/context-option.js +22 -0
- package/dist/builders/control-flow-builders.d.ts +19 -19
- package/dist/builders/every.d.ts +4 -4
- package/dist/builders/layer-provides-builders.d.ts +1 -1
- package/dist/builders/layer-provides-builders.js +1 -1
- package/dist/builders/loop-builder.d.ts +3 -3
- package/dist/builders/provide-builder.d.ts +14 -7
- package/dist/builders/provide-builder.js +15 -4
- package/dist/builders/spawn-builder.d.ts +8 -6
- package/dist/builders/spawn-builder.js +7 -2
- package/dist/builders/step-builders.d.ts +19 -20
- package/dist/builders/tool-builder.d.ts +9 -5
- package/dist/builders/tool-builder.js +3 -2
- package/dist/builders/workflow-hydrator.d.ts +17 -4
- package/dist/builders/workflow-hydrator.js +87 -8
- package/dist/builders/workflow-step.d.ts +61 -0
- package/dist/builders/workflow-step.js +126 -0
- package/dist/conditions/conditions.d.ts +17 -17
- package/dist/conditions/conditions.js +1 -1
- package/dist/deprecated.d.ts +30 -0
- package/dist/deprecated.js +28 -0
- package/dist/harness/agent-harness.d.ts +84 -27
- package/dist/harness/agent-harness.js +101 -20
- package/dist/harness/deps/context.d.ts +2 -0
- package/dist/harness/deps/context.js +1 -0
- package/dist/harness/deps/runtime.d.ts +4 -4
- package/dist/harness/deps/runtime.js +3 -3
- package/dist/harness/deps/types.d.ts +1 -1
- package/dist/harness/model-schema.d.ts +2 -2
- package/dist/index.d.ts +23 -19
- package/dist/index.js +15 -9
- package/dist/interpreter/action-deps.d.ts +3 -3
- package/dist/interpreter/action-deps.js +2 -2
- package/dist/interpreter/action-types.d.ts +1 -1
- package/dist/interpreter/collect-tools.d.ts +1 -1
- package/dist/interpreter/execute-action.d.ts +10 -10
- package/dist/interpreter/execute-action.js +33 -14
- package/dist/interpreter/execute-control.d.ts +4 -4
- package/dist/interpreter/execute-control.js +25 -16
- package/dist/interpreter/execute-sub-harness.d.ts +1 -1
- package/dist/interpreter/execute-sub-harness.js +22 -1
- package/dist/interpreter/execute.d.ts +3 -3
- package/dist/interpreter/execute.js +78 -31
- package/dist/interpreter/step-dispatcher.d.ts +3 -3
- package/dist/interpreter/sub-harness-events.d.ts +2 -2
- package/dist/interpreter/typeguards.d.ts +4 -4
- package/dist/patterns/dynamic-workflow.d.ts +7 -4
- package/dist/patterns/dynamic-workflow.js +2 -0
- package/dist/patterns/interview.d.ts +2 -2
- package/dist/patterns/plans.d.ts +5 -5
- package/dist/patterns/ralph-wiggum.d.ts +2 -2
- package/dist/patterns/react.d.ts +7 -5
- package/dist/patterns/react.js +6 -4
- package/dist/portable/builders.js +2 -0
- package/dist/portable/context-layers.d.ts +8 -0
- package/dist/portable/context-layers.js +8 -0
- package/dist/portable.d.ts +2 -2
- package/dist/portable.js +2 -2
- package/dist/runtime/context-deps.d.ts +2 -2
- package/dist/runtime/context-deps.js +1 -1
- package/dist/runtime/context-impl.d.ts +48 -9
- package/dist/runtime/context-impl.js +95 -10
- package/dist/runtime/context-type-deps.d.ts +1 -1
- package/dist/runtime/durable/checkpoint-store.d.ts +1 -1
- package/dist/runtime/durable/harness-checkpoints.d.ts +17 -4
- package/dist/runtime/durable/harness-checkpoints.js +21 -3
- package/dist/runtime/durable/index.d.ts +2 -0
- package/dist/runtime/durable/index.js +1 -0
- package/dist/runtime/durable/step-ledger.d.ts +107 -5
- package/dist/runtime/durable/step-ledger.js +179 -10
- package/dist/runtime/in-memory-storage.d.ts +1 -1
- package/dist/runtime/session-runner.d.ts +1 -1
- package/dist/runtime/step-registry.d.ts +3 -3
- package/dist/runtime/{tool-memory.d.ts → tool-context.d.ts} +1 -2
- package/dist/runtime/{tool-memory.js → tool-context.js} +5 -2
- package/dist/runtime/tool-ui.d.ts +1 -1
- package/dist/runtime/tool-ui.js +1 -1
- package/dist/schemas/workflow.d.ts +5 -279
- package/dist/schemas/workflow.js +4 -376
- package/dist/types/mutable-context.d.ts +2 -2
- package/dist/types/step-registrar.d.ts +1 -1
- package/dist/unstable.d.ts +5 -5
- package/dist/unstable.js +3 -3
- package/dist/until/predicates.d.ts +1 -1
- package/package.json +4 -4
- package/schema/noetic-workflow.schema.json +28 -0
- package/dist/builders/memory-builder.d.ts +0 -11
- package/dist/builders/memory-builder.js +0 -15
- package/dist/harness/deps/memory.d.ts +0 -2
- package/dist/harness/deps/memory.js +0 -1
- package/dist/patterns/flow.d.ts +0 -2
- package/dist/patterns/flow.js +0 -1
- package/dist/portable/memory-layers.d.ts +0 -8
- 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/
|
|
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
|
-
/**
|
|
38
|
-
|
|
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
|
|
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/
|
|
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
|
|
45
|
-
const
|
|
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
|
|
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
|
-
|
|
138
|
+
entries.set(parsed.data.path, parsed.data);
|
|
61
139
|
}
|
|
62
|
-
return
|
|
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.
|
|
90
|
-
this.
|
|
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
|
|
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
|
|
@@ -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/
|
|
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 {
|
|
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<
|
|
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<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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.
|
package/dist/runtime/tool-ui.js
CHANGED
|
@@ -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 (
|
|
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.
|