@noetic-tools/context 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +28 -0
  3. package/dist/context/budget.d.ts +39 -0
  4. package/dist/context/budget.js +183 -0
  5. package/dist/context/exec-context-factory.d.ts +24 -0
  6. package/dist/context/exec-context-factory.js +30 -0
  7. package/dist/context/function-call-utils.d.ts +6 -0
  8. package/dist/context/function-call-utils.js +27 -0
  9. package/dist/context/layer-api.d.ts +11 -0
  10. package/dist/context/layer-api.js +91 -0
  11. package/dist/context/layer-lifecycle.d.ts +239 -0
  12. package/dist/context/layer-lifecycle.js +800 -0
  13. package/dist/context/layer-provides.d.ts +32 -0
  14. package/dist/context/layer-provides.js +33 -0
  15. package/dist/context/layer-usage.d.ts +13 -0
  16. package/dist/context/layer-usage.js +54 -0
  17. package/dist/context/layers/durable-task-state.d.ts +107 -0
  18. package/dist/context/layers/durable-task-state.js +250 -0
  19. package/dist/context/layers/file-reference.d.ts +55 -0
  20. package/dist/context/layers/file-reference.js +729 -0
  21. package/dist/context/layers/history-window.d.ts +31 -0
  22. package/dist/context/layers/history-window.js +150 -0
  23. package/dist/context/layers/observational-context.d.ts +56 -0
  24. package/dist/context/layers/observational-context.js +152 -0
  25. package/dist/context/layers/plan-context.d.ts +73 -0
  26. package/dist/context/layers/plan-context.js +749 -0
  27. package/dist/context/layers/static-content.d.ts +27 -0
  28. package/dist/context/layers/static-content.js +46 -0
  29. package/dist/context/layers/steering.d.ts +50 -0
  30. package/dist/context/layers/steering.js +291 -0
  31. package/dist/context/layers/temporal-context.d.ts +91 -0
  32. package/dist/context/layers/temporal-context.js +327 -0
  33. package/dist/context/layers/tool-context-layer.d.ts +15 -0
  34. package/dist/context/layers/tool-context-layer.js +43 -0
  35. package/dist/context/layers/working-memory-context.d.ts +46 -0
  36. package/dist/context/layers/working-memory-context.js +155 -0
  37. package/dist/context/projector.d.ts +19 -0
  38. package/dist/context/projector.js +89 -0
  39. package/dist/context/scope.d.ts +3 -0
  40. package/dist/context/scope.js +42 -0
  41. package/dist/context/storage-batch.d.ts +18 -0
  42. package/dist/context/storage-batch.js +35 -0
  43. package/dist/context/strip-unresolved.d.ts +13 -0
  44. package/dist/context/strip-unresolved.js +35 -0
  45. package/dist/deprecated.d.ts +43 -0
  46. package/dist/deprecated.js +28 -0
  47. package/dist/index.d.ts +29 -0
  48. package/dist/index.js +31 -0
  49. package/package.json +63 -0
@@ -0,0 +1,32 @@
1
+ import type { ExecutionContext, LayerDataDecl, LayerFunctionDecl } from '@noetic-tools/types';
2
+ import type { ZodType } from 'zod';
3
+ /**
4
+ * Creates a read-only data declaration for a context layer's `provides` map.
5
+ *
6
+ * @public
7
+ * @param opts.read - Function that projects a value from the layer's current state.
8
+ * @returns A `LayerDataDecl` to include in a layer's `provides`.
9
+ */
10
+ export declare function layerData<T, TState>(opts: {
11
+ read: (state: TState) => T;
12
+ }): LayerDataDecl<T, TState>;
13
+ /**
14
+ * Creates a callable function declaration for a context layer's `provides` map.
15
+ * Functions are accessible via `ctx.context['layerId'].fn()` and automatically exposed as LLM tools.
16
+ *
17
+ * @public
18
+ * @param opts.description - Human-readable description used as tool description for LLM.
19
+ * @param opts.input - Zod schema validating input arguments.
20
+ * @param opts.output - Zod schema validating the return value.
21
+ * @param opts.execute - Async function receiving args, current state, and execution context.
22
+ * @returns A `LayerFunctionDecl` to include in a layer's `provides`.
23
+ */
24
+ export declare function layerFn<TInput, TOutput, TState>(opts: {
25
+ description: string;
26
+ input: ZodType<TInput>;
27
+ output: ZodType<TOutput>;
28
+ execute: (args: TInput, state: TState, ctx: ExecutionContext) => Promise<{
29
+ result: TOutput;
30
+ state?: TState;
31
+ }>;
32
+ }): LayerFunctionDecl<TInput, TOutput, TState>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Creates a read-only data declaration for a context layer's `provides` map.
3
+ *
4
+ * @public
5
+ * @param opts.read - Function that projects a value from the layer's current state.
6
+ * @returns A `LayerDataDecl` to include in a layer's `provides`.
7
+ */
8
+ export function layerData(opts) {
9
+ return {
10
+ kind: 'data',
11
+ read: opts.read,
12
+ };
13
+ }
14
+ /**
15
+ * Creates a callable function declaration for a context layer's `provides` map.
16
+ * Functions are accessible via `ctx.context['layerId'].fn()` and automatically exposed as LLM tools.
17
+ *
18
+ * @public
19
+ * @param opts.description - Human-readable description used as tool description for LLM.
20
+ * @param opts.input - Zod schema validating input arguments.
21
+ * @param opts.output - Zod schema validating the return value.
22
+ * @param opts.execute - Async function receiving args, current state, and execution context.
23
+ * @returns A `LayerFunctionDecl` to include in a layer's `provides`.
24
+ */
25
+ export function layerFn(opts) {
26
+ return {
27
+ kind: 'function',
28
+ description: opts.description,
29
+ input: opts.input,
30
+ output: opts.output,
31
+ execute: opts.execute,
32
+ };
33
+ }
@@ -0,0 +1,13 @@
1
+ import type { Context, LastLayerUsage, RecallLayerOutput, Tool } from '@noetic-tools/types';
2
+ interface ComputeLayerUsageParams {
3
+ ctx: Context;
4
+ modelId: string;
5
+ instructions?: string;
6
+ tools?: ReadonlyArray<Tool>;
7
+ /** Per-layer recall output assembled into the context view for this LLM call. */
8
+ recallResults: ReadonlyArray<RecallLayerOutput>;
9
+ }
10
+ export declare function computeLayerUsage({ ctx, modelId, instructions, tools, recallResults, }: ComputeLayerUsageParams): LastLayerUsage;
11
+ /** Commit computed usage to the Context, replacing any prior snapshot. */
12
+ export declare function commitLayerUsage(ctx: Context, usage: LastLayerUsage): void;
13
+ export {};
@@ -0,0 +1,54 @@
1
+ import { estimateTokens } from '@noetic-tools/types';
2
+ function canWriteLayerUsage(ctx) {
3
+ const desc = Object.getOwnPropertyDescriptor(ctx, 'lastLayerUsage');
4
+ if (desc === undefined) {
5
+ return false;
6
+ }
7
+ return desc.writable !== false;
8
+ }
9
+ //#endregion
10
+ //#region Helpers
11
+ function estimateJsonTokens(value) {
12
+ try {
13
+ return estimateTokens(JSON.stringify(value));
14
+ }
15
+ catch {
16
+ return 0;
17
+ }
18
+ }
19
+ //#endregion
20
+ //#region Public API
21
+ export function computeLayerUsage({ ctx, modelId, instructions, tools, recallResults, }) {
22
+ const layers = recallResults
23
+ .map((r) => ({
24
+ layerId: r.layerId,
25
+ tokenCount: r.tokenCount,
26
+ items: r.items,
27
+ }))
28
+ .sort((a, b) => a.layerId.localeCompare(b.layerId));
29
+ let historyTokens = 0;
30
+ for (const item of ctx.itemLog.items) {
31
+ historyTokens += estimateJsonTokens(item);
32
+ }
33
+ const systemPromptTokens = instructions ? estimateTokens(instructions) : 0;
34
+ const toolsTokens = tools && tools.length > 0 ? estimateJsonTokens(tools) : 0;
35
+ const layerTotal = layers.reduce((sum, l) => sum + l.tokenCount, 0);
36
+ const totalUsedTokens = systemPromptTokens + toolsTokens + historyTokens + layerTotal;
37
+ return {
38
+ executionId: ctx.id,
39
+ modelId,
40
+ layers,
41
+ systemPromptTokens,
42
+ toolsTokens,
43
+ historyTokens,
44
+ totalUsedTokens,
45
+ };
46
+ }
47
+ /** Commit computed usage to the Context, replacing any prior snapshot. */
48
+ export function commitLayerUsage(ctx, usage) {
49
+ if (!canWriteLayerUsage(ctx)) {
50
+ return;
51
+ }
52
+ ctx.lastLayerUsage = usage;
53
+ }
54
+ //#endregion
@@ -0,0 +1,107 @@
1
+ export interface DurableTaskState {
2
+ checkpoints: Array<{
3
+ timestamp: number;
4
+ depth: number;
5
+ }>;
6
+ files: string[];
7
+ data: Record<string, unknown>;
8
+ }
9
+ /**
10
+ * How a child's `data` map is merged into the parent's at a spawn/fork return.
11
+ *
12
+ * - `'shallow'` — `{ ...parent, ...child }`. Concurrent children that write the
13
+ * same key clobber each other (last to return wins). Fine for a single child.
14
+ * - `'namespace'` — the child's map is stored whole under its execution id
15
+ * (`parent.data[childExecutionId] = childData`), so N fan-out workers each
16
+ * keep their own result. Keys the child inherited from the parent unchanged
17
+ * are not re-written at the top level.
18
+ *
19
+ * @public
20
+ */
21
+ export type DurableTaskDataMerge = 'shallow' | 'namespace';
22
+ /** @public Options for {@link durableTaskState}. */
23
+ export interface DurableTaskStateOptions {
24
+ /**
25
+ * Merge strategy for `data` at a child boundary. Defaults to `'shallow'`.
26
+ * Use `'namespace'` for coordinator/worker fan-out, where several children
27
+ * return concurrently into one parent.
28
+ */
29
+ mergeData?: DurableTaskDataMerge;
30
+ }
31
+ /**
32
+ * Creates a context layer that persists task checkpoints, files, and arbitrary data across iterations.
33
+ *
34
+ * State is rehydrated from `ScopedStorage` on init and persisted via the
35
+ * runtime's durable write-through; `store` appends capped checkpoints
36
+ * (newest 50 kept) and `recall` trims its render to the allocated budget.
37
+ *
38
+ * The layer is writable from the model: `provides` exposes
39
+ * `durable-task-state/recordArtifact` (append a produced/modified file path)
40
+ * and `durable-task-state/setTaskData` (record a structured result under a
41
+ * key). Both survive the spawn boundary — a worker's artifacts merge back into
42
+ * its coordinator via `onReturn`.
43
+ *
44
+ * @public
45
+ * @param opts - Layer options; `mergeData` selects the `data` merge strategy at a child boundary.
46
+ * @returns A `ContextLayer` scoped to the thread with durable task state.
47
+ */
48
+ export declare function durableTaskState(opts?: DurableTaskStateOptions): {
49
+ id: 'durable-task-state';
50
+ name: string;
51
+ slot: number;
52
+ scope: "thread";
53
+ budget: {
54
+ min: number;
55
+ max: number;
56
+ };
57
+ timeouts: {
58
+ store: number;
59
+ };
60
+ provides: {
61
+ recordArtifact: import("@noetic-tools/types").LayerFunctionDecl<{
62
+ path: string;
63
+ }, string, DurableTaskState>;
64
+ setTaskData: import("@noetic-tools/types").LayerFunctionDecl<{
65
+ key: string;
66
+ value: unknown;
67
+ }, string, DurableTaskState>;
68
+ };
69
+ hooks: {
70
+ init({ storage }: import("@noetic-tools/types").InitParams): Promise<{
71
+ state: DurableTaskState;
72
+ }>;
73
+ recall({ state, budget }: import("@noetic-tools/types").RecallParams<DurableTaskState>): Promise<{
74
+ items: import("@noetic-tools/types").InputMessageItem[];
75
+ tokenCount: number;
76
+ } | null>;
77
+ store({ state, ctx }: import("@noetic-tools/types").StoreParams<DurableTaskState>): Promise<{
78
+ state: DurableTaskState;
79
+ }>;
80
+ onSpawn({ parentState }: import("@noetic-tools/types").SpawnParams<DurableTaskState>): Promise<{
81
+ childState: DurableTaskState;
82
+ items: never[];
83
+ }>;
84
+ onReturn({ childState, parentState, childCtx }: import("@noetic-tools/types").ReturnParams<DurableTaskState>): Promise<{
85
+ parentState: {
86
+ checkpoints: {
87
+ timestamp: number;
88
+ depth: number;
89
+ }[];
90
+ files: string[];
91
+ data: Record<string, unknown>;
92
+ };
93
+ }>;
94
+ onComplete({ state, outcome, ctx }: import("@noetic-tools/types").CompleteParams<DurableTaskState>): Promise<{
95
+ state: {
96
+ files: string[];
97
+ data: {
98
+ __outcome: import("@noetic-tools/types").ExecutionOutcome;
99
+ };
100
+ checkpoints: {
101
+ timestamp: number;
102
+ depth: number;
103
+ }[];
104
+ };
105
+ } | undefined>;
106
+ };
107
+ };
@@ -0,0 +1,250 @@
1
+ import { createMessage, estimateTokens, Slot } from '@noetic-tools/types';
2
+ import { z } from 'zod';
3
+ import { layerFn } from '../layer-provides.js';
4
+ /**
5
+ * Hard cap on retained checkpoints. store() appends one per model call and
6
+ * onReturn concatenates the child's list, so without a cap the thread-scoped
7
+ * (durably persisted) array grows linearly with total model calls forever.
8
+ * Newest checkpoints are kept.
9
+ */
10
+ const MAX_CHECKPOINTS = 50;
11
+ function trimCheckpoints(checkpoints) {
12
+ if (checkpoints.length <= MAX_CHECKPOINTS) {
13
+ return checkpoints;
14
+ }
15
+ return checkpoints.slice(checkpoints.length - MAX_CHECKPOINTS);
16
+ }
17
+ function renderTaskState(state) {
18
+ return `<task_state>\n${JSON.stringify(state, null, 2)}\n</task_state>`;
19
+ }
20
+ function mergeChildData({ parent, childState, childExecutionId, mergeData, }) {
21
+ if (mergeData === 'shallow') {
22
+ return {
23
+ ...parent.data,
24
+ ...childState.data,
25
+ };
26
+ }
27
+ return {
28
+ ...parent.data,
29
+ [childExecutionId]: childState.data,
30
+ };
31
+ }
32
+ /**
33
+ * Creates a context layer that persists task checkpoints, files, and arbitrary data across iterations.
34
+ *
35
+ * State is rehydrated from `ScopedStorage` on init and persisted via the
36
+ * runtime's durable write-through; `store` appends capped checkpoints
37
+ * (newest 50 kept) and `recall` trims its render to the allocated budget.
38
+ *
39
+ * The layer is writable from the model: `provides` exposes
40
+ * `durable-task-state/recordArtifact` (append a produced/modified file path)
41
+ * and `durable-task-state/setTaskData` (record a structured result under a
42
+ * key). Both survive the spawn boundary — a worker's artifacts merge back into
43
+ * its coordinator via `onReturn`.
44
+ *
45
+ * @public
46
+ * @param opts - Layer options; `mergeData` selects the `data` merge strategy at a child boundary.
47
+ * @returns A `ContextLayer` scoped to the thread with durable task state.
48
+ */
49
+ export function durableTaskState(opts = {}) {
50
+ const mergeData = opts.mergeData ?? 'shallow';
51
+ return {
52
+ id: 'durable-task-state',
53
+ name: 'Durable Task State',
54
+ slot: Slot.WORKING_MEMORY + 10, // 110
55
+ // 'thread' (not 'execution'): the layer's purpose is to persist task state
56
+ // ACROSS executions/iterations within a thread. 'execution' scope rotates
57
+ // its storage key every run, so checkpoints never survived (storeLayers also
58
+ // skips durable persistence for 'execution' scope).
59
+ scope: 'thread',
60
+ budget: {
61
+ min: 100,
62
+ max: 800,
63
+ },
64
+ timeouts: {
65
+ store: 30_000,
66
+ },
67
+ provides: {
68
+ recordArtifact: layerFn({
69
+ description: 'Record a file this task produced or modified. Recorded paths survive the task boundary and merge back into the parent task.',
70
+ input: z.object({
71
+ path: z.string().min(1),
72
+ }),
73
+ output: z.string(),
74
+ execute: async (args, state) => {
75
+ if (state.files.includes(args.path)) {
76
+ return {
77
+ result: `Already recorded: ${args.path}`,
78
+ state,
79
+ };
80
+ }
81
+ return {
82
+ result: `Recorded artifact: ${args.path}`,
83
+ state: {
84
+ ...state,
85
+ files: [
86
+ ...state.files,
87
+ args.path,
88
+ ],
89
+ },
90
+ };
91
+ },
92
+ }),
93
+ setTaskData: layerFn({
94
+ description: 'Record a structured result for this task under a key (e.g. a PR url, a verdict, a summary). Values survive the task boundary and merge back into the parent task.',
95
+ input: z.object({
96
+ key: z.string().min(1),
97
+ value: z.unknown(),
98
+ }),
99
+ output: z.string(),
100
+ execute: async (args, state) => {
101
+ // `__outcome` is written by onComplete; letting the model set it would
102
+ // make the recorded outcome unreliable.
103
+ if (args.key === '__outcome') {
104
+ return {
105
+ result: 'Cannot set reserved key "__outcome".',
106
+ state,
107
+ };
108
+ }
109
+ return {
110
+ result: `Recorded ${args.key}.`,
111
+ state: {
112
+ ...state,
113
+ data: {
114
+ ...state.data,
115
+ [args.key]: args.value,
116
+ },
117
+ },
118
+ };
119
+ },
120
+ }),
121
+ },
122
+ hooks: {
123
+ async init({ storage }) {
124
+ const saved = await storage.get('state');
125
+ return {
126
+ state: saved ?? {
127
+ checkpoints: [],
128
+ files: [],
129
+ data: {},
130
+ },
131
+ };
132
+ },
133
+ async recall({ state, budget }) {
134
+ if (!state) {
135
+ return null;
136
+ }
137
+ let view = state;
138
+ let text = renderTaskState(view);
139
+ // `budget > 0` is the fail-open convention (see staticContent): a zero
140
+ // allocation must not delete the task state from the view.
141
+ if (budget > 0) {
142
+ // Halve the OLDEST checkpoints while the render exceeds the budget —
143
+ // files/data stay, recent checkpoints stay.
144
+ while (estimateTokens(text) > budget && view.checkpoints.length > 0) {
145
+ view = {
146
+ ...view,
147
+ checkpoints: view.checkpoints.slice(Math.ceil(view.checkpoints.length / 2)),
148
+ };
149
+ text = renderTaskState(view);
150
+ }
151
+ // Final guard: still over budget with no checkpoints left — char-slice
152
+ // and keep the closing tag so the block stays well-formed.
153
+ if (estimateTokens(text) > budget) {
154
+ const closing = '\n</task_state>';
155
+ const maxChars = Math.max(0, budget * 4 - closing.length);
156
+ text = `${text.slice(0, maxChars)}${closing}`;
157
+ }
158
+ }
159
+ return {
160
+ items: [
161
+ createMessage(text, 'developer'),
162
+ ],
163
+ tokenCount: estimateTokens(text),
164
+ };
165
+ },
166
+ async store({ state, ctx }) {
167
+ const currentState = state ?? {
168
+ checkpoints: [],
169
+ files: [],
170
+ data: {},
171
+ };
172
+ // Add a checkpoint for each store call (capped, newest kept)
173
+ const newState = {
174
+ ...currentState,
175
+ checkpoints: trimCheckpoints([
176
+ ...currentState.checkpoints,
177
+ {
178
+ timestamp: Date.now(),
179
+ depth: ctx.depth,
180
+ },
181
+ ]),
182
+ };
183
+ return {
184
+ state: newState,
185
+ };
186
+ },
187
+ async onSpawn({ parentState }) {
188
+ // ALWAYS provides child state (unlike other layers)
189
+ return {
190
+ childState: structuredClone(parentState),
191
+ items: [],
192
+ };
193
+ },
194
+ async onReturn({ childState, parentState, childCtx }) {
195
+ // Merge child artifacts back to parent. The parent may have no state
196
+ // (init-less / never-initialized) — seed an empty base so the child's
197
+ // contribution is still merged rather than crashing.
198
+ const parent = parentState ?? {
199
+ checkpoints: [],
200
+ files: [],
201
+ data: {},
202
+ };
203
+ return {
204
+ parentState: {
205
+ checkpoints: trimCheckpoints([
206
+ ...parent.checkpoints,
207
+ ...childState.checkpoints,
208
+ ]),
209
+ files: [
210
+ ...new Set([
211
+ ...parent.files,
212
+ ...childState.files,
213
+ ]),
214
+ ],
215
+ data: mergeChildData({
216
+ parent,
217
+ childState,
218
+ childExecutionId: childCtx.executionId,
219
+ mergeData,
220
+ }),
221
+ },
222
+ };
223
+ },
224
+ async onComplete({ state, outcome, ctx }) {
225
+ if (!state) {
226
+ return;
227
+ }
228
+ return {
229
+ state: {
230
+ ...state,
231
+ data: {
232
+ ...state.data,
233
+ __outcome: outcome,
234
+ },
235
+ checkpoints: trimCheckpoints([
236
+ ...state.checkpoints,
237
+ {
238
+ timestamp: Date.now(),
239
+ // The completing execution's own depth — a hardcoded 0 made
240
+ // every terminal checkpoint look root-level regardless of
241
+ // where the execution actually sat in the spawn tree.
242
+ depth: ctx.depth,
243
+ },
244
+ ]),
245
+ },
246
+ };
247
+ },
248
+ },
249
+ };
250
+ }
@@ -0,0 +1,55 @@
1
+ import type { ContextLayer } from '@noetic-tools/types';
2
+ interface TrackedFile {
3
+ /** Absolute path to the file */
4
+ absolutePath: string;
5
+ /** Original reference path (as written by user) */
6
+ referencePath: string;
7
+ /** File content (null if deleted or error) */
8
+ content: string | null;
9
+ /** LLM-assigned priority score (0-100, higher = more relevant) */
10
+ priority: number;
11
+ /** Content hash for change detection */
12
+ contentHash: string;
13
+ /** Whether the file has been deleted */
14
+ deleted: boolean;
15
+ /** Estimated token count */
16
+ tokenCount: number;
17
+ /** Error message if file couldn't be read (security, size, permission) */
18
+ error?: string;
19
+ }
20
+ interface FileReferenceState {
21
+ /** Map of reference path to tracked file info */
22
+ files: Map<string, TrackedFile>;
23
+ /** Base directory for resolving relative paths */
24
+ baseDir: string;
25
+ }
26
+ interface FileReferenceOptions {
27
+ /** Base directory for resolving relative paths (defaults to cwd) */
28
+ baseDir?: string;
29
+ /** Slot position (defaults to Slot.RAG) */
30
+ slot?: number;
31
+ /** Model to use for priority scoring */
32
+ scoringModel?: string;
33
+ /** Maximum file size in bytes (defaults to 1MB) */
34
+ maxFileSize?: number;
35
+ /** Whether to follow symlinks (defaults to false for security) */
36
+ followSymlinks?: boolean;
37
+ /** Allowed file extensions (defaults to common code/text extensions) */
38
+ allowedExtensions?: string[];
39
+ }
40
+ /**
41
+ * Creates a context layer that tracks file references in user messages.
42
+ *
43
+ * Syntax: `#path/to/file` in user messages
44
+ *
45
+ * Behavior:
46
+ * - Transforms references to markdown anchor links `[#path/to/file](#path-to-file)`
47
+ * - Scores file relevance using LLM when first referenced
48
+ * - Injects file contents into context via recall(), ordered by priority
49
+ * - Detects file changes on each new message, triggers immediate re-render
50
+ * - Shows warning for deleted files
51
+ *
52
+ * @public
53
+ */
54
+ export declare function fileReference(opts?: FileReferenceOptions): ContextLayer<FileReferenceState>;
55
+ export {};