@noetic-tools/context 1.5.0 → 1.7.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/context/cache-anchoring.d.ts +7 -0
- package/dist/context/cache-anchoring.js +16 -0
- package/dist/context/hook-call.d.ts +31 -0
- package/dist/context/hook-call.js +92 -0
- package/dist/context/layer-lifecycle.d.ts +32 -3
- package/dist/context/layer-lifecycle.js +179 -66
- package/dist/context/layers/history.d.ts +1 -0
- package/dist/context/layers/history.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
|
@@ -27,6 +27,13 @@ export declare function pinKey(layer: ContextLayer): string;
|
|
|
27
27
|
export declare function createContextCacheStore(): ContextCacheStore;
|
|
28
28
|
/** Drop a lineage's epoch — used when a child execution ends. */
|
|
29
29
|
export declare function dropLineage(store: ContextCacheStore, key: string): void;
|
|
30
|
+
/**
|
|
31
|
+
* Drop the pins of layers that left a lineage's layer set — a closing
|
|
32
|
+
* `withContext` boundary. A pin outlives its layer by design (dropping it
|
|
33
|
+
* shortens the cached prefix), so a scoped layer must be unpinned explicitly or
|
|
34
|
+
* its last render keeps replaying after its scope ends.
|
|
35
|
+
*/
|
|
36
|
+
export declare function unpinLayers(store: ContextCacheStore, key: string, layers: ContextLayer[]): void;
|
|
30
37
|
/** Read a layer's churn record, creating an empty one on first sight. */
|
|
31
38
|
export declare function churnFor(store: ContextCacheStore, key: string): LayerChurn;
|
|
32
39
|
/** Share of watched assemblies in which a layer's output changed, 0–1. */
|
|
@@ -66,6 +66,22 @@ export function dropLineage(store, key) {
|
|
|
66
66
|
store.epochs.delete(key);
|
|
67
67
|
store.pendingReanchor.delete(key);
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Drop the pins of layers that left a lineage's layer set — a closing
|
|
71
|
+
* `withContext` boundary. A pin outlives its layer by design (dropping it
|
|
72
|
+
* shortens the cached prefix), so a scoped layer must be unpinned explicitly or
|
|
73
|
+
* its last render keeps replaying after its scope ends.
|
|
74
|
+
*/
|
|
75
|
+
export function unpinLayers(store, key, layers) {
|
|
76
|
+
const epoch = store.epochs.get(key);
|
|
77
|
+
if (!epoch) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const layer of layers) {
|
|
81
|
+
epoch.pins.delete(pinKey(layer));
|
|
82
|
+
epoch.autoBand.delete(pinKey(layer));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
69
85
|
/** Read a layer's churn record, creating an empty one on first sight. */
|
|
70
86
|
export function churnFor(store, key) {
|
|
71
87
|
const found = store.churn.get(key);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timed, traced calls into context layer hooks. Each call of a traced hook adds
|
|
3
|
+
* one `layer.hook` event to the span of the context it runs in (`ctx.trace`),
|
|
4
|
+
* shaped as a `LayerTraceSpan`. Events rather than child spans: an
|
|
5
|
+
* `ExecutionContext` can annotate its span but not open one.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExecutionContext, LayerTraceSpan } from '@noetic-tools/types';
|
|
8
|
+
type TracedHook = LayerTraceSpan['hook'];
|
|
9
|
+
interface HookCall<T> {
|
|
10
|
+
ctx: ExecutionContext;
|
|
11
|
+
layerId: string;
|
|
12
|
+
hook: TracedHook;
|
|
13
|
+
/** Milliseconds before the call fails as a timeout; `0` or less waits forever. */
|
|
14
|
+
timeout: number;
|
|
15
|
+
run: () => Promise<T>;
|
|
16
|
+
/** Reads a recall's budget and item count from the hook's result. */
|
|
17
|
+
measure?: (result: T) => Pick<LayerTraceSpan, 'budget' | 'itemCount'>;
|
|
18
|
+
}
|
|
19
|
+
/** Span event recording one layer hook call. Attributes follow `LayerTraceSpan`. */
|
|
20
|
+
export declare const LAYER_HOOK_EVENT = "layer.hook";
|
|
21
|
+
/**
|
|
22
|
+
* Runs a layer hook under its timeout and records a `layer.hook` event for it.
|
|
23
|
+
* Rethrows whatever the hook threw (or the timeout), so callers keep their own
|
|
24
|
+
* failure handling.
|
|
25
|
+
*/
|
|
26
|
+
export declare function callHook<T>({ ctx, layerId, hook, timeout, run, measure, }: HookCall<T>): Promise<T>;
|
|
27
|
+
/** Records a hook the layer defines but the lifecycle did not call. */
|
|
28
|
+
export declare function traceSkipped(ctx: ExecutionContext, layerId: string, hook: TracedHook): void;
|
|
29
|
+
/** Runs a hook under a timeout without tracing it, for hooks outside `LayerTraceSpan`. */
|
|
30
|
+
export declare function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timed, traced calls into context layer hooks. Each call of a traced hook adds
|
|
3
|
+
* one `layer.hook` event to the span of the context it runs in (`ctx.trace`),
|
|
4
|
+
* shaped as a `LayerTraceSpan`. Events rather than child spans: an
|
|
5
|
+
* `ExecutionContext` can annotate its span but not open one.
|
|
6
|
+
*/
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region Public API
|
|
9
|
+
/** Span event recording one layer hook call. Attributes follow `LayerTraceSpan`. */
|
|
10
|
+
export const LAYER_HOOK_EVENT = 'layer.hook';
|
|
11
|
+
/**
|
|
12
|
+
* Runs a layer hook under its timeout and records a `layer.hook` event for it.
|
|
13
|
+
* Rethrows whatever the hook threw (or the timeout), so callers keep their own
|
|
14
|
+
* failure handling.
|
|
15
|
+
*/
|
|
16
|
+
export async function callHook({ ctx, layerId, hook, timeout, run, measure, }) {
|
|
17
|
+
const started = performance.now();
|
|
18
|
+
try {
|
|
19
|
+
const result = await withTimeout(run(), timeout);
|
|
20
|
+
traceHook(ctx, {
|
|
21
|
+
layerId,
|
|
22
|
+
hook,
|
|
23
|
+
duration: performance.now() - started,
|
|
24
|
+
status: 'ok',
|
|
25
|
+
...measure?.(result),
|
|
26
|
+
});
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
traceHook(ctx, {
|
|
31
|
+
layerId,
|
|
32
|
+
hook,
|
|
33
|
+
duration: performance.now() - started,
|
|
34
|
+
status: e instanceof HookTimeout ? 'timeout' : 'error',
|
|
35
|
+
error: {
|
|
36
|
+
message: e instanceof Error ? e.message : String(e),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
throw e;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Records a hook the layer defines but the lifecycle did not call. */
|
|
43
|
+
export function traceSkipped(ctx, layerId, hook) {
|
|
44
|
+
traceHook(ctx, {
|
|
45
|
+
layerId,
|
|
46
|
+
hook,
|
|
47
|
+
duration: 0,
|
|
48
|
+
status: 'skipped',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** Runs a hook under a timeout without tracing it, for hooks outside `LayerTraceSpan`. */
|
|
52
|
+
export async function withTimeout(promise, ms) {
|
|
53
|
+
if (ms <= 0) {
|
|
54
|
+
return promise;
|
|
55
|
+
}
|
|
56
|
+
let timer;
|
|
57
|
+
return Promise.race([
|
|
58
|
+
promise.finally(() => clearTimeout(timer)),
|
|
59
|
+
new Promise((_, reject) => {
|
|
60
|
+
timer = setTimeout(() => reject(new HookTimeout(ms)), ms);
|
|
61
|
+
}),
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region Helpers
|
|
66
|
+
class HookTimeout extends Error {
|
|
67
|
+
constructor(ms) {
|
|
68
|
+
super(`Timeout after ${ms}ms`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Span event attributes hold only scalars, so nested fields flatten to dotted keys. */
|
|
72
|
+
function traceHook(ctx, span) {
|
|
73
|
+
const attributes = {
|
|
74
|
+
layerId: span.layerId,
|
|
75
|
+
hook: span.hook,
|
|
76
|
+
duration: span.duration,
|
|
77
|
+
status: span.status,
|
|
78
|
+
};
|
|
79
|
+
if (span.itemCount !== undefined) {
|
|
80
|
+
attributes.itemCount = span.itemCount;
|
|
81
|
+
}
|
|
82
|
+
if (span.budget) {
|
|
83
|
+
attributes['budget.allocated'] = span.budget.allocated;
|
|
84
|
+
attributes['budget.used'] = span.budget.used;
|
|
85
|
+
attributes['budget.yielded'] = span.budget.yielded;
|
|
86
|
+
}
|
|
87
|
+
if (span.error) {
|
|
88
|
+
attributes['error.message'] = span.error.message;
|
|
89
|
+
}
|
|
90
|
+
ctx.trace.addEvent(LAYER_HOOK_EVENT, attributes);
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
@@ -13,6 +13,13 @@ export interface LayerStateStore {
|
|
|
13
13
|
*/
|
|
14
14
|
has?(executionId: string, layerId: string): boolean;
|
|
15
15
|
cleanup(executionId: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Forget one layer's state, durable target, and disabled mark for an
|
|
18
|
+
* execution, leaving the execution's other layers alone. Optional so custom
|
|
19
|
+
* stores keep compiling; without it, a scoped dispose leaves the state in
|
|
20
|
+
* place for the next `init` to overwrite.
|
|
21
|
+
*/
|
|
22
|
+
drop?(executionId: string, layerId: string): void;
|
|
16
23
|
diagnostic: (layerId: string, hook: string, error: unknown) => void;
|
|
17
24
|
/**
|
|
18
25
|
* Register a durable write-through target for a layer's state under an
|
|
@@ -28,7 +35,8 @@ export interface LayerStateStore {
|
|
|
28
35
|
flush?(executionId: string): Promise<void>;
|
|
29
36
|
/**
|
|
30
37
|
* Mark a layer disabled for an execution (its `init` failed with
|
|
31
|
-
* `onInitError: 'disable'
|
|
38
|
+
* `onInitError: 'disable'`, or its `onSpawn` returned `null` or failed for
|
|
39
|
+
* a child execution). Disabled layers are skipped by every later
|
|
32
40
|
* hook. Optional so custom stores keep compiling; without explicit
|
|
33
41
|
* tracking, the lifecycle falls back to the legacy sentinel (init-bearing
|
|
34
42
|
* layer with no state).
|
|
@@ -90,6 +98,12 @@ interface SpawnLayersParams {
|
|
|
90
98
|
}
|
|
91
99
|
interface ReturnLayersParams<T = unknown> {
|
|
92
100
|
layers: ContextLayer[];
|
|
101
|
+
/**
|
|
102
|
+
* Id of the spawn (or inParallel) step whose child is returning. Names the
|
|
103
|
+
* step in `spawn_summary_failed` diagnostics; defaults to the child's
|
|
104
|
+
* execution id.
|
|
105
|
+
*/
|
|
106
|
+
stepId?: string;
|
|
93
107
|
parentCtx: ExecutionContext;
|
|
94
108
|
childCtx: ExecutionContext;
|
|
95
109
|
childLog: ItemLog;
|
|
@@ -107,6 +121,12 @@ interface DisposeLayersParams {
|
|
|
107
121
|
layers: ContextLayer[];
|
|
108
122
|
ctx: ExecutionContext;
|
|
109
123
|
store: LayerStateStore;
|
|
124
|
+
/**
|
|
125
|
+
* The layers share their execution with layers that stay live (a
|
|
126
|
+
* `withContext` boundary closing). Drop only these layers' state instead of
|
|
127
|
+
* cleaning up the whole execution.
|
|
128
|
+
*/
|
|
129
|
+
scoped?: boolean;
|
|
110
130
|
}
|
|
111
131
|
interface BeforeToolCallLayersParams {
|
|
112
132
|
layers: ContextLayer[];
|
|
@@ -180,6 +200,8 @@ export interface RecallCache {
|
|
|
180
200
|
}
|
|
181
201
|
export declare function createRecallCache(): RecallCache;
|
|
182
202
|
export declare function mostRestrictive(decisions: SteeringDecision[]): SteeringDecision;
|
|
203
|
+
/** Span event added to the child's span when a layer's `onReturn` hook fails. */
|
|
204
|
+
export declare const SPAWN_SUMMARY_FAILED_EVENT = "spawn.summary_failed";
|
|
183
205
|
export declare function initLayers({ layers, ctx, storage, store, keep, }: InitLayersParams): Promise<void>;
|
|
184
206
|
export declare function recallLayers({ layers, query, ctx, log, budgets, store, itemSchemas, peek, }: RecallLayersParams): Promise<RecallEntry[]>;
|
|
185
207
|
type RecallEntry = {
|
|
@@ -223,13 +245,20 @@ interface NotifyDecisionParams {
|
|
|
223
245
|
* what an earlier one recorded.
|
|
224
246
|
*/
|
|
225
247
|
export declare function notifyDecision({ layers, record, ctx, store, recallCache, }: NotifyDecisionParams): Promise<void>;
|
|
226
|
-
export declare function disposeLayers({ layers, ctx, store }: DisposeLayersParams): Promise<void>;
|
|
248
|
+
export declare function disposeLayers({ layers, ctx, store, scoped, }: DisposeLayersParams): Promise<void>;
|
|
227
249
|
export declare function spawnLayers({ layers, parentCtx, childCtx, store, itemSchemas, storage, }: SpawnLayersParams): Promise<{
|
|
228
250
|
layerId: string;
|
|
229
251
|
childState: unknown;
|
|
230
252
|
items: Item[];
|
|
231
253
|
}[]>;
|
|
232
|
-
|
|
254
|
+
/**
|
|
255
|
+
* Pipes a child's result through each layer's `onReturn` hook in slot order.
|
|
256
|
+
*
|
|
257
|
+
* A failing hook never fails the spawn: the child's work already succeeded,
|
|
258
|
+
* so the result passes on unchanged and the failure is reported as a
|
|
259
|
+
* `spawn_summary_failed` diagnostic plus a span event on the child's span.
|
|
260
|
+
*/
|
|
261
|
+
export declare function returnLayers<T>({ layers, stepId, parentCtx, childCtx, childLog, result, store, }: ReturnLayersParams<T>): Promise<T>;
|
|
233
262
|
export declare function completeLayers({ layers, ctx, log, outcome, store, }: CompleteLayersParams): Promise<void>;
|
|
234
263
|
export declare function beforeToolCallLayers({ layers, toolName, toolArgs, callId, ctx, store, }: BeforeToolCallLayersParams): Promise<SteeringDecision>;
|
|
235
264
|
export declare function afterModelCallLayers({ layers, response, ctx, store, }: AfterModelCallLayersParams): Promise<SteeringDecision>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createMessage, defaultItemSchemaRegistry, estimateTokens, frameworkCast, NoeticConfigError, SteeringAction, } from '@noetic-tools/types';
|
|
1
|
+
import { createMessage, defaultItemSchemaRegistry, estimateTokens, frameworkCast, NoeticConfigError, NoeticErrorImpl, SteeringAction, } from '@noetic-tools/types';
|
|
2
|
+
import { callHook, traceSkipped, withTimeout } from './hook-call.js';
|
|
2
3
|
import { createScopedStorage, resolveScopeKey } from './scope.js';
|
|
3
4
|
/** Maximum allowed re-render depth to prevent infinite loops */
|
|
4
5
|
const MAX_RERENDER_DEPTH = 3;
|
|
@@ -91,6 +92,11 @@ export function createLayerStateStore(diagnostic) {
|
|
|
91
92
|
durables.delete(executionId);
|
|
92
93
|
disabled.delete(executionId);
|
|
93
94
|
},
|
|
95
|
+
drop(executionId, layerId) {
|
|
96
|
+
states.get(executionId)?.delete(layerId);
|
|
97
|
+
durables.get(executionId)?.delete(layerId);
|
|
98
|
+
disabled.get(executionId)?.delete(layerId);
|
|
99
|
+
},
|
|
94
100
|
diagnostic: report,
|
|
95
101
|
};
|
|
96
102
|
}
|
|
@@ -136,18 +142,6 @@ function recallCacheKey(layer, ctx) {
|
|
|
136
142
|
return `${resolveScopeKey(layer.scope, ctx)}:${layer.id}`;
|
|
137
143
|
}
|
|
138
144
|
//#endregion
|
|
139
|
-
async function withTimeout(promise, ms) {
|
|
140
|
-
if (ms <= 0) {
|
|
141
|
-
return promise;
|
|
142
|
-
}
|
|
143
|
-
let timer;
|
|
144
|
-
return Promise.race([
|
|
145
|
-
promise.finally(() => clearTimeout(timer)),
|
|
146
|
-
new Promise((_, reject) => {
|
|
147
|
-
timer = setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms);
|
|
148
|
-
}),
|
|
149
|
-
]);
|
|
150
|
-
}
|
|
151
145
|
export function mostRestrictive(decisions) {
|
|
152
146
|
const guidances = [];
|
|
153
147
|
let hasGuide = false;
|
|
@@ -177,6 +171,23 @@ export function mostRestrictive(decisions) {
|
|
|
177
171
|
function layerItemSchemas(layer, base = defaultItemSchemaRegistry) {
|
|
178
172
|
return base.extend(layer.itemSchemas);
|
|
179
173
|
}
|
|
174
|
+
/** Span event added to the child's span when a layer's `onReturn` hook fails. */
|
|
175
|
+
export const SPAWN_SUMMARY_FAILED_EVENT = 'spawn.summary_failed';
|
|
176
|
+
function reportSummaryFailure({ layerId, stepId, childOutput, cause, childCtx, store, }) {
|
|
177
|
+
const summaryCause = cause instanceof Error ? cause : new Error(String(cause));
|
|
178
|
+
const error = new NoeticErrorImpl({
|
|
179
|
+
kind: 'spawn_summary_failed',
|
|
180
|
+
stepId,
|
|
181
|
+
childOutput,
|
|
182
|
+
summaryCause,
|
|
183
|
+
});
|
|
184
|
+
childCtx.trace.addEvent(SPAWN_SUMMARY_FAILED_EVENT, {
|
|
185
|
+
stepId,
|
|
186
|
+
layerId,
|
|
187
|
+
message: summaryCause.message,
|
|
188
|
+
});
|
|
189
|
+
store.diagnostic(layerId, 'onReturn', error);
|
|
190
|
+
}
|
|
180
191
|
//#endregion
|
|
181
192
|
//#region Public API
|
|
182
193
|
export async function initLayers({ layers, ctx, storage, store, keep = false, }) {
|
|
@@ -185,18 +196,25 @@ export async function initLayers({ layers, ctx, storage, store, keep = false, })
|
|
|
185
196
|
if (!layer.hooks.init) {
|
|
186
197
|
continue;
|
|
187
198
|
}
|
|
199
|
+
const init = layer.hooks.init;
|
|
188
200
|
if (keep && store.has?.(ctx.executionId, layer.id)) {
|
|
201
|
+
traceSkipped(ctx, layer.id, 'init');
|
|
189
202
|
continue;
|
|
190
203
|
}
|
|
191
204
|
const scopeKey = resolveScopeKey(layer.scope, ctx);
|
|
192
205
|
const scopedStorage = createScopedStorage(storage, layer.id, scopeKey);
|
|
193
206
|
try {
|
|
194
|
-
const
|
|
195
|
-
const result = await withTimeout(layer.hooks.init({
|
|
196
|
-
storage: scopedStorage,
|
|
197
|
-
scopeKey,
|
|
207
|
+
const result = await callHook({
|
|
198
208
|
ctx,
|
|
199
|
-
|
|
209
|
+
layerId: layer.id,
|
|
210
|
+
hook: 'init',
|
|
211
|
+
timeout: layer.timeouts?.init ?? 1e4,
|
|
212
|
+
run: () => init({
|
|
213
|
+
storage: scopedStorage,
|
|
214
|
+
scopeKey,
|
|
215
|
+
ctx,
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
200
218
|
store.set(ctx.executionId, layer.id, result.state);
|
|
201
219
|
}
|
|
202
220
|
catch (e) {
|
|
@@ -255,6 +273,9 @@ export async function recallLayers({ layers, query, ctx, log, budgets, store, it
|
|
|
255
273
|
layer,
|
|
256
274
|
state: stored,
|
|
257
275
|
})) {
|
|
276
|
+
if (!peek) {
|
|
277
|
+
traceSkipped(ctx, layer.id, 'recall');
|
|
278
|
+
}
|
|
258
279
|
continue; // was disabled
|
|
259
280
|
}
|
|
260
281
|
// Skipping the write is not enough for a peek: a hook may mutate the
|
|
@@ -264,15 +285,27 @@ export async function recallLayers({ layers, query, ctx, log, budgets, store, it
|
|
|
264
285
|
continue;
|
|
265
286
|
}
|
|
266
287
|
const budget = budgets.get(layer.id) ?? 0;
|
|
288
|
+
const recall = layer.hooks.recall;
|
|
289
|
+
const run = () => recall({
|
|
290
|
+
log,
|
|
291
|
+
query,
|
|
292
|
+
ctx,
|
|
293
|
+
state,
|
|
294
|
+
budget,
|
|
295
|
+
});
|
|
296
|
+
const timeout = layer.timeouts?.recall ?? 5e3;
|
|
267
297
|
try {
|
|
268
|
-
|
|
269
|
-
const result =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
298
|
+
// A peek renders without leaving a trace, so it records no event either.
|
|
299
|
+
const result = peek
|
|
300
|
+
? await withTimeout(run(), timeout)
|
|
301
|
+
: await callHook({
|
|
302
|
+
ctx,
|
|
303
|
+
layerId: layer.id,
|
|
304
|
+
hook: 'recall',
|
|
305
|
+
timeout,
|
|
306
|
+
run,
|
|
307
|
+
measure: (output) => measureRecall(output, budget),
|
|
308
|
+
});
|
|
276
309
|
if (!result) {
|
|
277
310
|
continue;
|
|
278
311
|
}
|
|
@@ -307,6 +340,24 @@ export async function recallLayers({ layers, query, ctx, log, budgets, store, it
|
|
|
307
340
|
return results;
|
|
308
341
|
}
|
|
309
342
|
const PEEK_UNSAFE = Symbol('peek-unsafe');
|
|
343
|
+
/** Budget and item count for a `layer.hook` recall event. */
|
|
344
|
+
function measureRecall(output, allocated) {
|
|
345
|
+
const measured = (used, itemCount) => ({
|
|
346
|
+
budget: {
|
|
347
|
+
allocated,
|
|
348
|
+
used,
|
|
349
|
+
yielded: Math.max(0, allocated - used),
|
|
350
|
+
},
|
|
351
|
+
itemCount,
|
|
352
|
+
});
|
|
353
|
+
if (!output) {
|
|
354
|
+
return measured(0, 0);
|
|
355
|
+
}
|
|
356
|
+
if (typeof output === 'string') {
|
|
357
|
+
return measured(estimateTokens(output), 1);
|
|
358
|
+
}
|
|
359
|
+
return measured(output.tokenCount, output.items.length);
|
|
360
|
+
}
|
|
310
361
|
/** A deep copy, or `PEEK_UNSAFE` when the state cannot be copied (it holds functions). */
|
|
311
362
|
function copyForPeek(state) {
|
|
312
363
|
try {
|
|
@@ -398,6 +449,7 @@ export async function storeLayers({ layers, response, ctx, log, store, recallCac
|
|
|
398
449
|
layer,
|
|
399
450
|
state,
|
|
400
451
|
})) {
|
|
452
|
+
traceSkipped(ctx, layer.id, 'store');
|
|
401
453
|
continue;
|
|
402
454
|
}
|
|
403
455
|
snapshots.push({
|
|
@@ -407,15 +459,20 @@ export async function storeLayers({ layers, response, ctx, log, store, recallCac
|
|
|
407
459
|
});
|
|
408
460
|
}
|
|
409
461
|
await Promise.allSettled(snapshots.map(async ({ layer, state, storeFn }) => {
|
|
410
|
-
const timeout = layer.timeouts?.store ?? 3e4;
|
|
411
462
|
try {
|
|
412
|
-
const result = await
|
|
413
|
-
newItems: response.items,
|
|
414
|
-
log,
|
|
415
|
-
response,
|
|
463
|
+
const result = await callHook({
|
|
416
464
|
ctx,
|
|
417
|
-
|
|
418
|
-
|
|
465
|
+
layerId: layer.id,
|
|
466
|
+
hook: 'store',
|
|
467
|
+
timeout: layer.timeouts?.store ?? 3e4,
|
|
468
|
+
run: () => storeFn({
|
|
469
|
+
newItems: response.items,
|
|
470
|
+
log,
|
|
471
|
+
response,
|
|
472
|
+
ctx,
|
|
473
|
+
state,
|
|
474
|
+
}),
|
|
475
|
+
});
|
|
419
476
|
// `'state' in result` (not `!== undefined`) so a layer can explicitly
|
|
420
477
|
// clear its state by returning `{ state: undefined }`. The store's
|
|
421
478
|
// durable write-through mirrors the write (clearing deletes the
|
|
@@ -476,7 +533,7 @@ export async function notifyDecision({ layers, record, ctx, store, recallCache,
|
|
|
476
533
|
}
|
|
477
534
|
await store.flush?.(ctx.executionId);
|
|
478
535
|
}
|
|
479
|
-
export async function disposeLayers({ layers, ctx, store }) {
|
|
536
|
+
export async function disposeLayers({ layers, ctx, store, scoped = false, }) {
|
|
480
537
|
// Sequential, REVERSE array order
|
|
481
538
|
const reversed = [
|
|
482
539
|
...layers,
|
|
@@ -494,13 +551,20 @@ export async function disposeLayers({ layers, ctx, store }) {
|
|
|
494
551
|
layer,
|
|
495
552
|
state,
|
|
496
553
|
})) {
|
|
554
|
+
traceSkipped(ctx, layer.id, 'dispose');
|
|
497
555
|
continue;
|
|
498
556
|
}
|
|
557
|
+
const dispose = layer.hooks.dispose;
|
|
499
558
|
try {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
559
|
+
await callHook({
|
|
560
|
+
ctx,
|
|
561
|
+
layerId: layer.id,
|
|
562
|
+
hook: 'dispose',
|
|
563
|
+
timeout: layer.timeouts?.dispose ?? 5e3,
|
|
564
|
+
run: () => dispose({
|
|
565
|
+
state,
|
|
566
|
+
}),
|
|
567
|
+
});
|
|
504
568
|
}
|
|
505
569
|
catch (e) {
|
|
506
570
|
store.diagnostic(layer.id, 'dispose', e);
|
|
@@ -508,7 +572,13 @@ export async function disposeLayers({ layers, ctx, store }) {
|
|
|
508
572
|
}
|
|
509
573
|
// Settle any in-flight durable mirror writes before dropping registrations.
|
|
510
574
|
await store.flush?.(ctx.executionId);
|
|
511
|
-
|
|
575
|
+
if (!scoped) {
|
|
576
|
+
store.cleanup(ctx.executionId);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
for (const layer of layers) {
|
|
580
|
+
store.drop?.(ctx.executionId, layer.id);
|
|
581
|
+
}
|
|
512
582
|
}
|
|
513
583
|
export async function spawnLayers({ layers, parentCtx, childCtx, store, itemSchemas = defaultItemSchemaRegistry, storage, }) {
|
|
514
584
|
if (storage) {
|
|
@@ -537,30 +607,52 @@ export async function spawnLayers({ layers, parentCtx, childCtx, store, itemSche
|
|
|
537
607
|
layer,
|
|
538
608
|
state: parentState,
|
|
539
609
|
})) {
|
|
610
|
+
traceSkipped(parentCtx, layer.id, 'onSpawn');
|
|
540
611
|
continue;
|
|
541
612
|
}
|
|
613
|
+
const onSpawn = layer.hooks.onSpawn;
|
|
542
614
|
try {
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
615
|
+
const result = await callHook({
|
|
616
|
+
ctx: parentCtx,
|
|
617
|
+
layerId: layer.id,
|
|
618
|
+
hook: 'onSpawn',
|
|
619
|
+
timeout: layer.timeouts?.onSpawn ?? 1e4,
|
|
620
|
+
run: () => onSpawn({
|
|
621
|
+
parentState,
|
|
622
|
+
childCtx,
|
|
623
|
+
}),
|
|
624
|
+
});
|
|
625
|
+
// A `null` result opts the layer out of this child: every later hook
|
|
626
|
+
// skips it for the child's execution.
|
|
627
|
+
if (!result) {
|
|
628
|
+
store.disable?.(childCtx.executionId, layer.id);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
// `childState: null` still injects items; the child just gets no state.
|
|
632
|
+
if (result.childState !== null) {
|
|
549
633
|
store.set(childCtx.executionId, layer.id, result.childState);
|
|
550
|
-
results.push({
|
|
551
|
-
layerId: layer.id,
|
|
552
|
-
childState: result.childState,
|
|
553
|
-
items: layerItemSchemas(layer, itemSchemas).parseMany(result.items ?? []),
|
|
554
|
-
});
|
|
555
634
|
}
|
|
635
|
+
results.push({
|
|
636
|
+
layerId: layer.id,
|
|
637
|
+
childState: result.childState,
|
|
638
|
+
items: layerItemSchemas(layer, itemSchemas).parseMany(result.items ?? []),
|
|
639
|
+
});
|
|
556
640
|
}
|
|
557
641
|
catch (e) {
|
|
558
642
|
store.diagnostic(layer.id, 'onSpawn', e);
|
|
643
|
+
store.disable?.(childCtx.executionId, layer.id);
|
|
559
644
|
}
|
|
560
645
|
}
|
|
561
646
|
return results;
|
|
562
647
|
}
|
|
563
|
-
|
|
648
|
+
/**
|
|
649
|
+
* Pipes a child's result through each layer's `onReturn` hook in slot order.
|
|
650
|
+
*
|
|
651
|
+
* A failing hook never fails the spawn: the child's work already succeeded,
|
|
652
|
+
* so the result passes on unchanged and the failure is reported as a
|
|
653
|
+
* `spawn_summary_failed` diagnostic plus a span event on the child's span.
|
|
654
|
+
*/
|
|
655
|
+
export async function returnLayers({ layers, stepId, parentCtx, childCtx, childLog, result, store, }) {
|
|
564
656
|
const sorted = [
|
|
565
657
|
...layers,
|
|
566
658
|
].sort((a, b) => a.slot - b.slot);
|
|
@@ -575,17 +667,24 @@ export async function returnLayers({ layers, parentCtx, childCtx, childLog, resu
|
|
|
575
667
|
// initialized state can still be seeded from the child. Skip only when the
|
|
576
668
|
// child produced nothing.
|
|
577
669
|
if (childState === undefined) {
|
|
670
|
+
traceSkipped(childCtx, layer.id, 'onReturn');
|
|
578
671
|
continue;
|
|
579
672
|
}
|
|
673
|
+
const onReturn = layer.hooks.onReturn;
|
|
580
674
|
try {
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
675
|
+
const returnResult = await callHook({
|
|
676
|
+
ctx: childCtx,
|
|
677
|
+
layerId: layer.id,
|
|
678
|
+
hook: 'onReturn',
|
|
679
|
+
timeout: layer.timeouts?.onReturn ?? 1e4,
|
|
680
|
+
run: () => onReturn({
|
|
681
|
+
childState,
|
|
682
|
+
childLog,
|
|
683
|
+
parentState,
|
|
684
|
+
result: currentResult,
|
|
685
|
+
childCtx,
|
|
686
|
+
}),
|
|
687
|
+
});
|
|
589
688
|
if (returnResult?.parentState !== undefined) {
|
|
590
689
|
store.set(parentCtx.executionId, layer.id, returnResult.parentState);
|
|
591
690
|
}
|
|
@@ -594,7 +693,14 @@ export async function returnLayers({ layers, parentCtx, childCtx, childLog, resu
|
|
|
594
693
|
}
|
|
595
694
|
}
|
|
596
695
|
catch (e) {
|
|
597
|
-
|
|
696
|
+
reportSummaryFailure({
|
|
697
|
+
layerId: layer.id,
|
|
698
|
+
stepId: stepId ?? childCtx.executionId,
|
|
699
|
+
childOutput: currentResult,
|
|
700
|
+
cause: e,
|
|
701
|
+
childCtx,
|
|
702
|
+
store,
|
|
703
|
+
});
|
|
598
704
|
}
|
|
599
705
|
}
|
|
600
706
|
// The child execution's store is typically cleaned up right after onReturn —
|
|
@@ -617,16 +723,23 @@ export async function completeLayers({ layers, ctx, log, outcome, store, }) {
|
|
|
617
723
|
layer,
|
|
618
724
|
state,
|
|
619
725
|
})) {
|
|
726
|
+
traceSkipped(ctx, layer.id, 'onComplete');
|
|
620
727
|
continue;
|
|
621
728
|
}
|
|
729
|
+
const onComplete = layer.hooks.onComplete;
|
|
622
730
|
try {
|
|
623
|
-
const
|
|
624
|
-
const result = await withTimeout(layer.hooks.onComplete({
|
|
625
|
-
log,
|
|
731
|
+
const result = await callHook({
|
|
626
732
|
ctx,
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
733
|
+
layerId: layer.id,
|
|
734
|
+
hook: 'onComplete',
|
|
735
|
+
timeout: layer.timeouts?.onComplete ?? 3e4,
|
|
736
|
+
run: () => onComplete({
|
|
737
|
+
log,
|
|
738
|
+
ctx,
|
|
739
|
+
state,
|
|
740
|
+
outcome,
|
|
741
|
+
}),
|
|
742
|
+
});
|
|
630
743
|
if (result && 'state' in result) {
|
|
631
744
|
store.set(ctx.executionId, layer.id, result.state);
|
|
632
745
|
}
|
|
@@ -27,5 +27,6 @@ export interface HistoryConfig {
|
|
|
27
27
|
* @public
|
|
28
28
|
* @param config - Optional `{ maxItems }`. Defaults to 40.
|
|
29
29
|
* @returns A `ContextLayer` that contributes only a `projectHistory` hook.
|
|
30
|
+
* @throws `NoeticConfigError` with code `INVALID_HISTORY_MAX_ITEMS` if `maxItems` is not an integer in the allowed range.
|
|
30
31
|
*/
|
|
31
32
|
export declare function history(config?: HistoryConfig): ContextLayer<null>;
|
|
@@ -100,6 +100,7 @@ function findHeadAnchor(items, windowStart) {
|
|
|
100
100
|
* @public
|
|
101
101
|
* @param config - Optional `{ maxItems }`. Defaults to 40.
|
|
102
102
|
* @returns A `ContextLayer` that contributes only a `projectHistory` hook.
|
|
103
|
+
* @throws `NoeticConfigError` with code `INVALID_HISTORY_MAX_ITEMS` if `maxItems` is not an integer in the allowed range.
|
|
103
104
|
*/
|
|
104
105
|
export function history(config) {
|
|
105
106
|
const maxItems = config?.maxItems ?? DEFAULT_MAX_ITEMS;
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './context/budget.js';
|
|
|
9
9
|
export * from './context/cache-anchoring.js';
|
|
10
10
|
export * from './context/exec-context-factory.js';
|
|
11
11
|
export * from './context/function-call-utils.js';
|
|
12
|
+
export { LAYER_HOOK_EVENT } from './context/hook-call.js';
|
|
12
13
|
export * from './context/layer-api.js';
|
|
13
14
|
export * from './context/layer-lifecycle.js';
|
|
14
15
|
export * from './context/layer-provides.js';
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ export * from './context/budget.js';
|
|
|
11
11
|
export * from './context/cache-anchoring.js';
|
|
12
12
|
export * from './context/exec-context-factory.js';
|
|
13
13
|
export * from './context/function-call-utils.js';
|
|
14
|
+
export { LAYER_HOOK_EVENT } from './context/hook-call.js';
|
|
14
15
|
export * from './context/layer-api.js';
|
|
15
16
|
export * from './context/layer-lifecycle.js';
|
|
16
17
|
export * from './context/layer-provides.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noetic-tools/context",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Context layer system for Noetic agents: the ContextLayer contract, lifecycle, budget/projection machinery, and built-in layer implementations.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"zod": "^4.0.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@noetic-tools/types": "^3.
|
|
47
|
+
"@noetic-tools/types": "^3.8.0",
|
|
48
48
|
"zod": "^4.0.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|