@noetic-tools/context 1.2.2 → 1.4.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/budget.d.ts +7 -0
- package/dist/context/budget.js +11 -0
- package/dist/context/layer-lifecycle.d.ts +26 -4
- package/dist/context/layer-lifecycle.js +87 -6
- package/dist/context/layers/decisions-history.d.ts +50 -0
- package/dist/context/layers/decisions-history.js +111 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
package/dist/context/budget.d.ts
CHANGED
|
@@ -9,6 +9,13 @@ export interface BudgetAllocation {
|
|
|
9
9
|
* `step.projection` to match the target model's real context length.
|
|
10
10
|
*/
|
|
11
11
|
export declare const DEFAULT_PROJECTION: ProjectionPolicy;
|
|
12
|
+
/**
|
|
13
|
+
* Default view budget for `decide` steps. Decision models degrade on long
|
|
14
|
+
* state — TypeSafe Jev drops to a slower, less accurate long-context mode above
|
|
15
|
+
* about 32 KB of text — so a decision sees a tighter window than a chat turn.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export declare const DECISION_PROJECTION: ProjectionPolicy;
|
|
12
19
|
interface AllocateBudgetsOpts {
|
|
13
20
|
layers: ContextLayer[];
|
|
14
21
|
totalBudget: number;
|
package/dist/context/budget.js
CHANGED
|
@@ -9,6 +9,17 @@ export const DEFAULT_PROJECTION = {
|
|
|
9
9
|
responseReserve: 4e3,
|
|
10
10
|
overflow: 'sliding_window',
|
|
11
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Default view budget for `decide` steps. Decision models degrade on long
|
|
14
|
+
* state — TypeSafe Jev drops to a slower, less accurate long-context mode above
|
|
15
|
+
* about 32 KB of text — so a decision sees a tighter window than a chat turn.
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
export const DECISION_PROJECTION = {
|
|
19
|
+
tokenBudget: 8e3,
|
|
20
|
+
responseReserve: 0,
|
|
21
|
+
overflow: 'truncate',
|
|
22
|
+
};
|
|
12
23
|
function extractMin(config) {
|
|
13
24
|
if (config && typeof config === 'object' && 'min' in config) {
|
|
14
25
|
return config.min;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContextLayer, ExecutionContext, Item, ItemLog, ItemSchemaRegistry, LLMResponse, ScopedStorage, SteeringDecision, StorageAdapter } from '@noetic-tools/types';
|
|
1
|
+
import type { ContextLayer, DecisionRecord, ExecutionContext, Item, ItemLog, ItemSchemaRegistry, LLMResponse, ScopedStorage, SteeringDecision, StorageAdapter } from '@noetic-tools/types';
|
|
2
2
|
export interface LayerStateStore {
|
|
3
3
|
get<T>(executionId: string, layerId: string): T | undefined;
|
|
4
4
|
set<T>(executionId: string, layerId: string, state: T): void;
|
|
@@ -42,6 +42,12 @@ interface InitLayersParams {
|
|
|
42
42
|
ctx: ExecutionContext;
|
|
43
43
|
storage: StorageAdapter;
|
|
44
44
|
store: LayerStateStore;
|
|
45
|
+
/**
|
|
46
|
+
* Skip `init` for layers that already hold state for this execution. Set for
|
|
47
|
+
* an execution restored from a checkpoint, whose snapshot state `init` would
|
|
48
|
+
* otherwise overwrite with a fresh start.
|
|
49
|
+
*/
|
|
50
|
+
keep?: boolean;
|
|
45
51
|
}
|
|
46
52
|
interface RecallLayersParams {
|
|
47
53
|
layers: ContextLayer[];
|
|
@@ -51,6 +57,8 @@ interface RecallLayersParams {
|
|
|
51
57
|
budgets: Map<string, number>;
|
|
52
58
|
store: LayerStateStore;
|
|
53
59
|
itemSchemas?: ItemSchemaRegistry;
|
|
60
|
+
/** Render without committing returned state. See `RecallOptions.peek`. */
|
|
61
|
+
peek?: boolean;
|
|
54
62
|
}
|
|
55
63
|
interface StoreLayersParams {
|
|
56
64
|
layers: ContextLayer[];
|
|
@@ -104,6 +112,7 @@ interface BeforeToolCallLayersParams {
|
|
|
104
112
|
layers: ContextLayer[];
|
|
105
113
|
toolName: string;
|
|
106
114
|
toolArgs: unknown;
|
|
115
|
+
callId?: string;
|
|
107
116
|
ctx: ExecutionContext;
|
|
108
117
|
store: LayerStateStore;
|
|
109
118
|
}
|
|
@@ -171,8 +180,8 @@ export interface RecallCache {
|
|
|
171
180
|
}
|
|
172
181
|
export declare function createRecallCache(): RecallCache;
|
|
173
182
|
export declare function mostRestrictive(decisions: SteeringDecision[]): SteeringDecision;
|
|
174
|
-
export declare function initLayers({ layers, ctx, storage, store }: InitLayersParams): Promise<void>;
|
|
175
|
-
export declare function recallLayers({ layers, query, ctx, log, budgets, store, itemSchemas, }: RecallLayersParams): Promise<RecallEntry[]>;
|
|
183
|
+
export declare function initLayers({ layers, ctx, storage, store, keep, }: InitLayersParams): Promise<void>;
|
|
184
|
+
export declare function recallLayers({ layers, query, ctx, log, budgets, store, itemSchemas, peek, }: RecallLayersParams): Promise<RecallEntry[]>;
|
|
176
185
|
type RecallEntry = {
|
|
177
186
|
layerId: string;
|
|
178
187
|
items: Item[];
|
|
@@ -201,6 +210,19 @@ interface RecallLayersEventualParams extends RecallLayersModeParams {
|
|
|
201
210
|
*/
|
|
202
211
|
export declare function recallLayersEventual({ cache, forceAtomic, ...params }: RecallLayersEventualParams): Promise<RecallEntry[]>;
|
|
203
212
|
export declare function storeLayers({ layers, response, ctx, log, store, recallCache, }: StoreLayersParams): Promise<void>;
|
|
213
|
+
interface NotifyDecisionParams {
|
|
214
|
+
layers: ContextLayer[];
|
|
215
|
+
record: DecisionRecord;
|
|
216
|
+
ctx: ExecutionContext;
|
|
217
|
+
store: LayerStateStore;
|
|
218
|
+
recallCache?: RecallCache;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Run each layer's `onDecision` hook in slot order. Sequential, unlike
|
|
222
|
+
* `storeLayers`: decisions are small and ordered, and a later layer may read
|
|
223
|
+
* what an earlier one recorded.
|
|
224
|
+
*/
|
|
225
|
+
export declare function notifyDecision({ layers, record, ctx, store, recallCache, }: NotifyDecisionParams): Promise<void>;
|
|
204
226
|
export declare function disposeLayers({ layers, ctx, store }: DisposeLayersParams): Promise<void>;
|
|
205
227
|
export declare function spawnLayers({ layers, parentCtx, childCtx, store, itemSchemas, storage, }: SpawnLayersParams): Promise<{
|
|
206
228
|
layerId: string;
|
|
@@ -209,7 +231,7 @@ export declare function spawnLayers({ layers, parentCtx, childCtx, store, itemSc
|
|
|
209
231
|
}[]>;
|
|
210
232
|
export declare function returnLayers<T>({ layers, parentCtx, childCtx, childLog, result, store, }: ReturnLayersParams<T>): Promise<T>;
|
|
211
233
|
export declare function completeLayers({ layers, ctx, log, outcome, store, }: CompleteLayersParams): Promise<void>;
|
|
212
|
-
export declare function beforeToolCallLayers({ layers, toolName, toolArgs, ctx, store, }: BeforeToolCallLayersParams): Promise<SteeringDecision>;
|
|
234
|
+
export declare function beforeToolCallLayers({ layers, toolName, toolArgs, callId, ctx, store, }: BeforeToolCallLayersParams): Promise<SteeringDecision>;
|
|
213
235
|
export declare function afterModelCallLayers({ layers, response, ctx, store, }: AfterModelCallLayersParams): Promise<SteeringDecision>;
|
|
214
236
|
/**
|
|
215
237
|
* Run history items through every layer's `projectHistory` hook in slot order.
|
|
@@ -179,12 +179,15 @@ function layerItemSchemas(layer, base = defaultItemSchemaRegistry) {
|
|
|
179
179
|
}
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region Public API
|
|
182
|
-
export async function initLayers({ layers, ctx, storage, store }) {
|
|
182
|
+
export async function initLayers({ layers, ctx, storage, store, keep = false, }) {
|
|
183
183
|
// Sequential, array order
|
|
184
184
|
for (const layer of layers) {
|
|
185
185
|
if (!layer.hooks.init) {
|
|
186
186
|
continue;
|
|
187
187
|
}
|
|
188
|
+
if (keep && store.has?.(ctx.executionId, layer.id)) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
188
191
|
const scopeKey = resolveScopeKey(layer.scope, ctx);
|
|
189
192
|
const scopedStorage = createScopedStorage(storage, layer.id, scopeKey);
|
|
190
193
|
try {
|
|
@@ -236,7 +239,7 @@ function registerDurableTargets({ layers, ctx, storage, store, }) {
|
|
|
236
239
|
store.registerDurable(ctx.executionId, layer.id, createScopedStorage(storage, layer.id, scopeKey));
|
|
237
240
|
}
|
|
238
241
|
}
|
|
239
|
-
export async function recallLayers({ layers, query, ctx, log, budgets, store, itemSchemas = defaultItemSchemaRegistry, }) {
|
|
242
|
+
export async function recallLayers({ layers, query, ctx, log, budgets, store, itemSchemas = defaultItemSchemaRegistry, peek = false, }) {
|
|
240
243
|
const sorted = [
|
|
241
244
|
...layers,
|
|
242
245
|
].sort((a, b) => a.slot - b.slot);
|
|
@@ -245,15 +248,21 @@ export async function recallLayers({ layers, query, ctx, log, budgets, store, it
|
|
|
245
248
|
if (!layer.hooks.recall) {
|
|
246
249
|
continue;
|
|
247
250
|
}
|
|
248
|
-
const
|
|
251
|
+
const stored = store.get(ctx.executionId, layer.id);
|
|
249
252
|
if (isLayerDisabled({
|
|
250
253
|
store,
|
|
251
254
|
executionId: ctx.executionId,
|
|
252
255
|
layer,
|
|
253
|
-
state,
|
|
256
|
+
state: stored,
|
|
254
257
|
})) {
|
|
255
258
|
continue; // was disabled
|
|
256
259
|
}
|
|
260
|
+
// Skipping the write is not enough for a peek: a hook may mutate the
|
|
261
|
+
// stored object in place (steering splices its queue), so it gets a copy.
|
|
262
|
+
const state = peek ? copyForPeek(stored) : stored;
|
|
263
|
+
if (peek && state === PEEK_UNSAFE) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
257
266
|
const budget = budgets.get(layer.id) ?? 0;
|
|
258
267
|
try {
|
|
259
268
|
const timeout = layer.timeouts?.recall ?? 5e3;
|
|
@@ -287,7 +296,7 @@ export async function recallLayers({ layers, query, ctx, log, budgets, store, it
|
|
|
287
296
|
tokenCount: result.tokenCount,
|
|
288
297
|
mutatedState,
|
|
289
298
|
});
|
|
290
|
-
if (mutatedState) {
|
|
299
|
+
if (mutatedState && !peek) {
|
|
291
300
|
store.set(ctx.executionId, layer.id, result.state);
|
|
292
301
|
}
|
|
293
302
|
}
|
|
@@ -297,6 +306,16 @@ export async function recallLayers({ layers, query, ctx, log, budgets, store, it
|
|
|
297
306
|
}
|
|
298
307
|
return results;
|
|
299
308
|
}
|
|
309
|
+
const PEEK_UNSAFE = Symbol('peek-unsafe');
|
|
310
|
+
/** A deep copy, or `PEEK_UNSAFE` when the state cannot be copied (it holds functions). */
|
|
311
|
+
function copyForPeek(state) {
|
|
312
|
+
try {
|
|
313
|
+
return structuredClone(state);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return PEEK_UNSAFE;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
300
319
|
/**
|
|
301
320
|
* Recall the layers that must complete before the model call: those with
|
|
302
321
|
* `recallMode !== 'eventual'`, or every layer when `forceAtomic` is set.
|
|
@@ -330,6 +349,17 @@ export async function recallLayersEventual({ cache, forceAtomic, ...params }) {
|
|
|
330
349
|
const key = recallCacheKey(layer, params.ctx);
|
|
331
350
|
const cached = cache.entries.get(key);
|
|
332
351
|
const isStale = cache.stale.has(key);
|
|
352
|
+
// A peek renders a cold or stale layer but leaves the cache as it found it,
|
|
353
|
+
// so the next real recall still sees the entry as needing a render.
|
|
354
|
+
if (params.peek && (!cached || isStale)) {
|
|
355
|
+
results.push(...(await recallLayers({
|
|
356
|
+
...params,
|
|
357
|
+
layers: [
|
|
358
|
+
layer,
|
|
359
|
+
],
|
|
360
|
+
})));
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
333
363
|
if (!cached || isStale) {
|
|
334
364
|
const recalled = await recallLayers({
|
|
335
365
|
...params,
|
|
@@ -405,6 +435,47 @@ export async function storeLayers({ layers, response, ctx, log, store, recallCac
|
|
|
405
435
|
}));
|
|
406
436
|
await store.flush?.(ctx.executionId);
|
|
407
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Run each layer's `onDecision` hook in slot order. Sequential, unlike
|
|
440
|
+
* `storeLayers`: decisions are small and ordered, and a later layer may read
|
|
441
|
+
* what an earlier one recorded.
|
|
442
|
+
*/
|
|
443
|
+
export async function notifyDecision({ layers, record, ctx, store, recallCache, }) {
|
|
444
|
+
const sorted = [
|
|
445
|
+
...layers,
|
|
446
|
+
].sort((a, b) => a.slot - b.slot);
|
|
447
|
+
for (const layer of sorted) {
|
|
448
|
+
if (!layer.hooks.onDecision) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const state = store.get(ctx.executionId, layer.id);
|
|
452
|
+
if (isLayerDisabled({
|
|
453
|
+
store,
|
|
454
|
+
executionId: ctx.executionId,
|
|
455
|
+
layer,
|
|
456
|
+
state,
|
|
457
|
+
})) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const result = await withTimeout(layer.hooks.onDecision({
|
|
462
|
+
state,
|
|
463
|
+
record,
|
|
464
|
+
ctx,
|
|
465
|
+
}), layer.timeouts?.onDecision ?? 5e3);
|
|
466
|
+
if (result && 'state' in result) {
|
|
467
|
+
store.set(ctx.executionId, layer.id, result.state);
|
|
468
|
+
if (recallCache && layer.recallMode === 'eventual') {
|
|
469
|
+
recallCache.stale.add(recallCacheKey(layer, ctx));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
catch (e) {
|
|
474
|
+
store.diagnostic(layer.id, 'onDecision', e);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
await store.flush?.(ctx.executionId);
|
|
478
|
+
}
|
|
408
479
|
export async function disposeLayers({ layers, ctx, store }) {
|
|
409
480
|
// Sequential, REVERSE array order
|
|
410
481
|
const reversed = [
|
|
@@ -566,7 +637,7 @@ export async function completeLayers({ layers, ctx, log, outcome, store, }) {
|
|
|
566
637
|
}
|
|
567
638
|
await store.flush?.(ctx.executionId);
|
|
568
639
|
}
|
|
569
|
-
export async function beforeToolCallLayers({ layers, toolName, toolArgs, ctx, store, }) {
|
|
640
|
+
export async function beforeToolCallLayers({ layers, toolName, toolArgs, callId, ctx, store, }) {
|
|
570
641
|
const sorted = [
|
|
571
642
|
...layers,
|
|
572
643
|
].sort((a, b) => a.slot - b.slot);
|
|
@@ -589,6 +660,7 @@ export async function beforeToolCallLayers({ layers, toolName, toolArgs, ctx, st
|
|
|
589
660
|
const result = await withTimeout(layer.hooks.beforeToolCall({
|
|
590
661
|
toolName,
|
|
591
662
|
toolArgs,
|
|
663
|
+
callId,
|
|
592
664
|
ctx,
|
|
593
665
|
state,
|
|
594
666
|
}), timeout);
|
|
@@ -605,6 +677,15 @@ export async function beforeToolCallLayers({ layers, toolName, toolArgs, ctx, st
|
|
|
605
677
|
throw e;
|
|
606
678
|
}
|
|
607
679
|
store.diagnostic(layer.id, 'beforeToolCall', e);
|
|
680
|
+
// A permission GATE must fail closed: a hook that threw or timed out is
|
|
681
|
+
// a check that did not run, and for a gating layer that means deny, not
|
|
682
|
+
// the silent approval an abstention would collapse into.
|
|
683
|
+
if (layer.onBeforeToolCallError === 'deny') {
|
|
684
|
+
return {
|
|
685
|
+
action: SteeringAction.Deny,
|
|
686
|
+
guidance: `tool call denied: gating layer '${layer.id}' failed (${e instanceof Error ? e.message : String(e)})`,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
608
689
|
}
|
|
609
690
|
}
|
|
610
691
|
return mostRestrictive(decisions);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ContextScope, DecisionRecord, InitParams, InitResult, LayerDataDecl, OnDecisionParams, RecallParams, ReturnParams, ReturnResult, SpawnParams, SpawnResult } from '@noetic-tools/types';
|
|
2
|
+
/** @public Configuration for {@link decisionsHistoryLayer}. */
|
|
3
|
+
export interface DecisionsHistoryConfig<Id extends string = 'decisionsHistory'> {
|
|
4
|
+
/** Layer id, and so the `ctx.context` key. Default `'decisionsHistory'`. */
|
|
5
|
+
id?: Id;
|
|
6
|
+
/** Default `'execution'`. With `'thread'` or `'resource'`, history persists across runs. */
|
|
7
|
+
scope?: ContextScope;
|
|
8
|
+
/** Records kept; the oldest drop first. Default 50. */
|
|
9
|
+
maxRecords?: number;
|
|
10
|
+
/** Render a summary of the latest decisions into the view. Default true. */
|
|
11
|
+
recall?: boolean;
|
|
12
|
+
/** Default `Slot.WORKING_MEMORY + 10`. */
|
|
13
|
+
slot?: number;
|
|
14
|
+
}
|
|
15
|
+
/** @public State held by {@link decisionsHistoryLayer}: records, oldest first. */
|
|
16
|
+
export interface DecisionsHistoryState {
|
|
17
|
+
records: DecisionRecord[];
|
|
18
|
+
}
|
|
19
|
+
/** @public The layer {@link decisionsHistoryLayer} returns, with its literal id kept for `ctx.context` typing. */
|
|
20
|
+
export interface DecisionsHistoryLayer<Id extends string> {
|
|
21
|
+
readonly id: Id;
|
|
22
|
+
name: string;
|
|
23
|
+
slot: number;
|
|
24
|
+
scope: ContextScope;
|
|
25
|
+
provides: {
|
|
26
|
+
latest: LayerDataDecl<Readonly<Record<string, DecisionRecord>>, DecisionsHistoryState>;
|
|
27
|
+
all: LayerDataDecl<ReadonlyArray<DecisionRecord>, DecisionsHistoryState>;
|
|
28
|
+
};
|
|
29
|
+
hooks: {
|
|
30
|
+
init(params: InitParams): Promise<InitResult<DecisionsHistoryState>>;
|
|
31
|
+
recall(params: RecallParams<DecisionsHistoryState>): Promise<string | null>;
|
|
32
|
+
onDecision(params: OnDecisionParams<DecisionsHistoryState>): Promise<{
|
|
33
|
+
state: DecisionsHistoryState;
|
|
34
|
+
}>;
|
|
35
|
+
onSpawn(params: SpawnParams<DecisionsHistoryState>): Promise<SpawnResult<DecisionsHistoryState>>;
|
|
36
|
+
onReturn(params: ReturnParams<DecisionsHistoryState>): Promise<ReturnResult<DecisionsHistoryState>>;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A context layer that records every `decide` step's answers. Later steps read
|
|
41
|
+
* them through `ctx.context.decisionsHistory.latest[stepId]`, and later `callModel`
|
|
42
|
+
* steps see a one-line-per-decision summary in their view. inParallel paths and spawn
|
|
43
|
+
* children start with the parent's records and hand their new ones back.
|
|
44
|
+
*
|
|
45
|
+
* @public
|
|
46
|
+
*/
|
|
47
|
+
export declare function decisionsHistoryLayer(config?: Omit<DecisionsHistoryConfig, 'id'>): DecisionsHistoryLayer<'decisionsHistory'>;
|
|
48
|
+
export declare function decisionsHistoryLayer<const Id extends string>(config: DecisionsHistoryConfig<Id> & {
|
|
49
|
+
id: Id;
|
|
50
|
+
}): DecisionsHistoryLayer<Id>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { frameworkCast, Slot } from '@noetic-tools/types';
|
|
2
|
+
import { layerData } from '../layer-provides.js';
|
|
3
|
+
//#endregion
|
|
4
|
+
//#region Public API
|
|
5
|
+
const DEFAULT_MAX_RECORDS = 50;
|
|
6
|
+
export function decisionsHistoryLayer(config) {
|
|
7
|
+
const maxRecords = config?.maxRecords ?? DEFAULT_MAX_RECORDS;
|
|
8
|
+
const renders = config?.recall ?? true;
|
|
9
|
+
const keep = (records) => ({
|
|
10
|
+
records: records.slice(-maxRecords),
|
|
11
|
+
});
|
|
12
|
+
return {
|
|
13
|
+
id: config?.id ?? 'decisionsHistory',
|
|
14
|
+
name: 'Decisions History',
|
|
15
|
+
slot: config?.slot ?? Slot.WORKING_MEMORY + 10,
|
|
16
|
+
scope: config?.scope ?? 'execution',
|
|
17
|
+
provides: {
|
|
18
|
+
latest: layerData({
|
|
19
|
+
read: (state) => latestByStep(state.records),
|
|
20
|
+
}),
|
|
21
|
+
all: layerData({
|
|
22
|
+
read: (state) => state.records,
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
hooks: {
|
|
26
|
+
async init({ storage }) {
|
|
27
|
+
const saved = await storage.get('state');
|
|
28
|
+
return {
|
|
29
|
+
state: saved ?? {
|
|
30
|
+
records: [],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async recall({ state }) {
|
|
35
|
+
if (!renders || state.records.length === 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return renderSummary(latestByStep(state.records));
|
|
39
|
+
},
|
|
40
|
+
async onDecision({ state, record }) {
|
|
41
|
+
return {
|
|
42
|
+
state: keep([
|
|
43
|
+
...state.records,
|
|
44
|
+
record,
|
|
45
|
+
]),
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
async onSpawn({ parentState }) {
|
|
49
|
+
return {
|
|
50
|
+
childState: {
|
|
51
|
+
records: [
|
|
52
|
+
...parentState.records,
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
// Concurrent fork paths each return; merging only the records the parent
|
|
58
|
+
// lacks keeps every sibling's decisions.
|
|
59
|
+
async onReturn({ parentState, childState }) {
|
|
60
|
+
const known = new Set(parentState.records.map(recordKey));
|
|
61
|
+
const added = childState.records.filter((r) => !known.has(recordKey(r)));
|
|
62
|
+
return {
|
|
63
|
+
parentState: keep([
|
|
64
|
+
...parentState.records,
|
|
65
|
+
...added,
|
|
66
|
+
]),
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region Helpers
|
|
74
|
+
function recordKey(record) {
|
|
75
|
+
return `${record.stepId}\u0000${record.at}\u0000${record.modelId}`;
|
|
76
|
+
}
|
|
77
|
+
function latestByStep(records) {
|
|
78
|
+
const latest = {};
|
|
79
|
+
for (const record of records) {
|
|
80
|
+
latest[record.stepId] = record;
|
|
81
|
+
}
|
|
82
|
+
return latest;
|
|
83
|
+
}
|
|
84
|
+
function renderSummary(latest) {
|
|
85
|
+
const lines = Object.values(latest).map((record) => {
|
|
86
|
+
const answers = Object.entries(record.answers)
|
|
87
|
+
.map(([id, answer]) => `${id}=${renderAnswer(answer)}`)
|
|
88
|
+
.join(', ');
|
|
89
|
+
return `- ${record.stepId}: ${answers}`;
|
|
90
|
+
});
|
|
91
|
+
return [
|
|
92
|
+
'Decisions so far:',
|
|
93
|
+
...lines,
|
|
94
|
+
].join('\n');
|
|
95
|
+
}
|
|
96
|
+
function renderAnswer(answer) {
|
|
97
|
+
switch (answer.type) {
|
|
98
|
+
case 'boolean':
|
|
99
|
+
return `${answer.value ? 'yes' : 'no'} (${round(answer.probability)})`;
|
|
100
|
+
case 'choice':
|
|
101
|
+
return `${answer.value} (${round(frameworkCast(answer.probabilities)[answer.value] ?? 0)})`;
|
|
102
|
+
case 'score':
|
|
103
|
+
return `${answer.level} (${round(answer.value)})`;
|
|
104
|
+
case 'refusal':
|
|
105
|
+
return 'refused';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function round(n) {
|
|
109
|
+
return Math.round(n * 100) / 100;
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from './context/layer-api.js';
|
|
|
13
13
|
export * from './context/layer-lifecycle.js';
|
|
14
14
|
export * from './context/layer-provides.js';
|
|
15
15
|
export * from './context/layer-usage.js';
|
|
16
|
+
export * from './context/layers/decisions-history.js';
|
|
16
17
|
export * from './context/layers/filesystem.js';
|
|
17
18
|
export * from './context/layers/history.js';
|
|
18
19
|
export * from './context/layers/instructions.js';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export * from './context/layer-api.js';
|
|
|
15
15
|
export * from './context/layer-lifecycle.js';
|
|
16
16
|
export * from './context/layer-provides.js';
|
|
17
17
|
export * from './context/layer-usage.js';
|
|
18
|
+
export * from './context/layers/decisions-history.js';
|
|
18
19
|
export * from './context/layers/filesystem.js';
|
|
19
20
|
export * from './context/layers/history.js';
|
|
20
21
|
export * from './context/layers/instructions.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noetic-tools/context",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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.4.0",
|
|
48
48
|
"zod": "^4.0.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|