@noetic-tools/context 1.6.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 +16 -2
- package/dist/context/layer-lifecycle.js +145 -63
- 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).
|
|
@@ -113,6 +121,12 @@ interface DisposeLayersParams {
|
|
|
113
121
|
layers: ContextLayer[];
|
|
114
122
|
ctx: ExecutionContext;
|
|
115
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;
|
|
116
130
|
}
|
|
117
131
|
interface BeforeToolCallLayersParams {
|
|
118
132
|
layers: ContextLayer[];
|
|
@@ -231,7 +245,7 @@ interface NotifyDecisionParams {
|
|
|
231
245
|
* what an earlier one recorded.
|
|
232
246
|
*/
|
|
233
247
|
export declare function notifyDecision({ layers, record, ctx, store, recallCache, }: NotifyDecisionParams): Promise<void>;
|
|
234
|
-
export declare function disposeLayers({ layers, ctx, store }: DisposeLayersParams): Promise<void>;
|
|
248
|
+
export declare function disposeLayers({ layers, ctx, store, scoped, }: DisposeLayersParams): Promise<void>;
|
|
235
249
|
export declare function spawnLayers({ layers, parentCtx, childCtx, store, itemSchemas, storage, }: SpawnLayersParams): Promise<{
|
|
236
250
|
layerId: string;
|
|
237
251
|
childState: unknown;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
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;
|
|
@@ -202,18 +196,25 @@ export async function initLayers({ layers, ctx, storage, store, keep = false, })
|
|
|
202
196
|
if (!layer.hooks.init) {
|
|
203
197
|
continue;
|
|
204
198
|
}
|
|
199
|
+
const init = layer.hooks.init;
|
|
205
200
|
if (keep && store.has?.(ctx.executionId, layer.id)) {
|
|
201
|
+
traceSkipped(ctx, layer.id, 'init');
|
|
206
202
|
continue;
|
|
207
203
|
}
|
|
208
204
|
const scopeKey = resolveScopeKey(layer.scope, ctx);
|
|
209
205
|
const scopedStorage = createScopedStorage(storage, layer.id, scopeKey);
|
|
210
206
|
try {
|
|
211
|
-
const
|
|
212
|
-
const result = await withTimeout(layer.hooks.init({
|
|
213
|
-
storage: scopedStorage,
|
|
214
|
-
scopeKey,
|
|
207
|
+
const result = await callHook({
|
|
215
208
|
ctx,
|
|
216
|
-
|
|
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
|
+
});
|
|
217
218
|
store.set(ctx.executionId, layer.id, result.state);
|
|
218
219
|
}
|
|
219
220
|
catch (e) {
|
|
@@ -272,6 +273,9 @@ export async function recallLayers({ layers, query, ctx, log, budgets, store, it
|
|
|
272
273
|
layer,
|
|
273
274
|
state: stored,
|
|
274
275
|
})) {
|
|
276
|
+
if (!peek) {
|
|
277
|
+
traceSkipped(ctx, layer.id, 'recall');
|
|
278
|
+
}
|
|
275
279
|
continue; // was disabled
|
|
276
280
|
}
|
|
277
281
|
// Skipping the write is not enough for a peek: a hook may mutate the
|
|
@@ -281,15 +285,27 @@ export async function recallLayers({ layers, query, ctx, log, budgets, store, it
|
|
|
281
285
|
continue;
|
|
282
286
|
}
|
|
283
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;
|
|
284
297
|
try {
|
|
285
|
-
|
|
286
|
-
const result =
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
});
|
|
293
309
|
if (!result) {
|
|
294
310
|
continue;
|
|
295
311
|
}
|
|
@@ -324,6 +340,24 @@ export async function recallLayers({ layers, query, ctx, log, budgets, store, it
|
|
|
324
340
|
return results;
|
|
325
341
|
}
|
|
326
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
|
+
}
|
|
327
361
|
/** A deep copy, or `PEEK_UNSAFE` when the state cannot be copied (it holds functions). */
|
|
328
362
|
function copyForPeek(state) {
|
|
329
363
|
try {
|
|
@@ -415,6 +449,7 @@ export async function storeLayers({ layers, response, ctx, log, store, recallCac
|
|
|
415
449
|
layer,
|
|
416
450
|
state,
|
|
417
451
|
})) {
|
|
452
|
+
traceSkipped(ctx, layer.id, 'store');
|
|
418
453
|
continue;
|
|
419
454
|
}
|
|
420
455
|
snapshots.push({
|
|
@@ -424,15 +459,20 @@ export async function storeLayers({ layers, response, ctx, log, store, recallCac
|
|
|
424
459
|
});
|
|
425
460
|
}
|
|
426
461
|
await Promise.allSettled(snapshots.map(async ({ layer, state, storeFn }) => {
|
|
427
|
-
const timeout = layer.timeouts?.store ?? 3e4;
|
|
428
462
|
try {
|
|
429
|
-
const result = await
|
|
430
|
-
newItems: response.items,
|
|
431
|
-
log,
|
|
432
|
-
response,
|
|
463
|
+
const result = await callHook({
|
|
433
464
|
ctx,
|
|
434
|
-
|
|
435
|
-
|
|
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
|
+
});
|
|
436
476
|
// `'state' in result` (not `!== undefined`) so a layer can explicitly
|
|
437
477
|
// clear its state by returning `{ state: undefined }`. The store's
|
|
438
478
|
// durable write-through mirrors the write (clearing deletes the
|
|
@@ -493,7 +533,7 @@ export async function notifyDecision({ layers, record, ctx, store, recallCache,
|
|
|
493
533
|
}
|
|
494
534
|
await store.flush?.(ctx.executionId);
|
|
495
535
|
}
|
|
496
|
-
export async function disposeLayers({ layers, ctx, store }) {
|
|
536
|
+
export async function disposeLayers({ layers, ctx, store, scoped = false, }) {
|
|
497
537
|
// Sequential, REVERSE array order
|
|
498
538
|
const reversed = [
|
|
499
539
|
...layers,
|
|
@@ -511,13 +551,20 @@ export async function disposeLayers({ layers, ctx, store }) {
|
|
|
511
551
|
layer,
|
|
512
552
|
state,
|
|
513
553
|
})) {
|
|
554
|
+
traceSkipped(ctx, layer.id, 'dispose');
|
|
514
555
|
continue;
|
|
515
556
|
}
|
|
557
|
+
const dispose = layer.hooks.dispose;
|
|
516
558
|
try {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
+
});
|
|
521
568
|
}
|
|
522
569
|
catch (e) {
|
|
523
570
|
store.diagnostic(layer.id, 'dispose', e);
|
|
@@ -525,7 +572,13 @@ export async function disposeLayers({ layers, ctx, store }) {
|
|
|
525
572
|
}
|
|
526
573
|
// Settle any in-flight durable mirror writes before dropping registrations.
|
|
527
574
|
await store.flush?.(ctx.executionId);
|
|
528
|
-
|
|
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
|
+
}
|
|
529
582
|
}
|
|
530
583
|
export async function spawnLayers({ layers, parentCtx, childCtx, store, itemSchemas = defaultItemSchemaRegistry, storage, }) {
|
|
531
584
|
if (storage) {
|
|
@@ -554,25 +607,40 @@ export async function spawnLayers({ layers, parentCtx, childCtx, store, itemSche
|
|
|
554
607
|
layer,
|
|
555
608
|
state: parentState,
|
|
556
609
|
})) {
|
|
610
|
+
traceSkipped(parentCtx, layer.id, 'onSpawn');
|
|
557
611
|
continue;
|
|
558
612
|
}
|
|
613
|
+
const onSpawn = layer.hooks.onSpawn;
|
|
559
614
|
try {
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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) {
|
|
566
633
|
store.set(childCtx.executionId, layer.id, result.childState);
|
|
567
|
-
results.push({
|
|
568
|
-
layerId: layer.id,
|
|
569
|
-
childState: result.childState,
|
|
570
|
-
items: layerItemSchemas(layer, itemSchemas).parseMany(result.items ?? []),
|
|
571
|
-
});
|
|
572
634
|
}
|
|
635
|
+
results.push({
|
|
636
|
+
layerId: layer.id,
|
|
637
|
+
childState: result.childState,
|
|
638
|
+
items: layerItemSchemas(layer, itemSchemas).parseMany(result.items ?? []),
|
|
639
|
+
});
|
|
573
640
|
}
|
|
574
641
|
catch (e) {
|
|
575
642
|
store.diagnostic(layer.id, 'onSpawn', e);
|
|
643
|
+
store.disable?.(childCtx.executionId, layer.id);
|
|
576
644
|
}
|
|
577
645
|
}
|
|
578
646
|
return results;
|
|
@@ -599,17 +667,24 @@ export async function returnLayers({ layers, stepId, parentCtx, childCtx, childL
|
|
|
599
667
|
// initialized state can still be seeded from the child. Skip only when the
|
|
600
668
|
// child produced nothing.
|
|
601
669
|
if (childState === undefined) {
|
|
670
|
+
traceSkipped(childCtx, layer.id, 'onReturn');
|
|
602
671
|
continue;
|
|
603
672
|
}
|
|
673
|
+
const onReturn = layer.hooks.onReturn;
|
|
604
674
|
try {
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
+
});
|
|
613
688
|
if (returnResult?.parentState !== undefined) {
|
|
614
689
|
store.set(parentCtx.executionId, layer.id, returnResult.parentState);
|
|
615
690
|
}
|
|
@@ -648,16 +723,23 @@ export async function completeLayers({ layers, ctx, log, outcome, store, }) {
|
|
|
648
723
|
layer,
|
|
649
724
|
state,
|
|
650
725
|
})) {
|
|
726
|
+
traceSkipped(ctx, layer.id, 'onComplete');
|
|
651
727
|
continue;
|
|
652
728
|
}
|
|
729
|
+
const onComplete = layer.hooks.onComplete;
|
|
653
730
|
try {
|
|
654
|
-
const
|
|
655
|
-
const result = await withTimeout(layer.hooks.onComplete({
|
|
656
|
-
log,
|
|
731
|
+
const result = await callHook({
|
|
657
732
|
ctx,
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
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
|
+
});
|
|
661
743
|
if (result && 'state' in result) {
|
|
662
744
|
store.set(ctx.executionId, layer.id, result.state);
|
|
663
745
|
}
|
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": {
|