@noetic-tools/core 0.0.1

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 (207) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +38 -0
  3. package/dist/adapters/in-memory-fs-adapter.d.ts +3 -0
  4. package/dist/adapters/in-memory-fs-adapter.js +245 -0
  5. package/dist/adapters/in-memory-shell-adapter.d.ts +9 -0
  6. package/dist/adapters/in-memory-shell-adapter.js +20 -0
  7. package/dist/adapters/in-memory-subprocess-adapter.d.ts +5 -0
  8. package/dist/adapters/in-memory-subprocess-adapter.js +126 -0
  9. package/dist/adapters/local-fs-adapter.d.ts +3 -0
  10. package/dist/adapters/local-fs-adapter.js +43 -0
  11. package/dist/adapters/local-shell-adapter.d.ts +51 -0
  12. package/dist/adapters/local-shell-adapter.js +167 -0
  13. package/dist/adapters/local-subprocess-adapter.d.ts +16 -0
  14. package/dist/adapters/local-subprocess-adapter.js +187 -0
  15. package/dist/adapters/openrouter.d.ts +57 -0
  16. package/dist/adapters/openrouter.js +248 -0
  17. package/dist/builders/channel-builder.d.ts +36 -0
  18. package/dist/builders/channel-builder.js +15 -0
  19. package/dist/builders/control-flow-builders.d.ts +79 -0
  20. package/dist/builders/control-flow-builders.js +53 -0
  21. package/dist/builders/every.d.ts +45 -0
  22. package/dist/builders/every.js +60 -0
  23. package/dist/builders/layer-provides-builders.d.ts +32 -0
  24. package/dist/builders/layer-provides-builders.js +33 -0
  25. package/dist/builders/loop-builder.d.ts +23 -0
  26. package/dist/builders/loop-builder.js +46 -0
  27. package/dist/builders/memory-builder.d.ts +11 -0
  28. package/dist/builders/memory-builder.js +15 -0
  29. package/dist/builders/provide-builder.d.ts +20 -0
  30. package/dist/builders/provide-builder.js +34 -0
  31. package/dist/builders/spawn-builder.d.ts +20 -0
  32. package/dist/builders/spawn-builder.js +33 -0
  33. package/dist/builders/step-builders.d.ts +62 -0
  34. package/dist/builders/step-builders.js +99 -0
  35. package/dist/builders/tool-builder.d.ts +52 -0
  36. package/dist/builders/tool-builder.js +59 -0
  37. package/dist/conditions/conditions.d.ts +114 -0
  38. package/dist/conditions/conditions.js +271 -0
  39. package/dist/conditions/cosine-similarity.d.ts +11 -0
  40. package/dist/conditions/cosine-similarity.js +28 -0
  41. package/dist/conditions/index.d.ts +3 -0
  42. package/dist/conditions/index.js +2 -0
  43. package/dist/errors/noetic-config-error.d.ts +41 -0
  44. package/dist/errors/noetic-config-error.js +44 -0
  45. package/dist/errors/noetic-error.d.ts +18 -0
  46. package/dist/errors/noetic-error.js +52 -0
  47. package/dist/index.d.ts +152 -0
  48. package/dist/index.js +112 -0
  49. package/dist/internal-test.d.ts +11 -0
  50. package/dist/internal-test.js +11 -0
  51. package/dist/interpreter/clone-guard.d.ts +4 -0
  52. package/dist/interpreter/clone-guard.js +18 -0
  53. package/dist/interpreter/collect-tools.d.ts +12 -0
  54. package/dist/interpreter/collect-tools.js +70 -0
  55. package/dist/interpreter/execute-branch.d.ts +3 -0
  56. package/dist/interpreter/execute-branch.js +10 -0
  57. package/dist/interpreter/execute-every.d.ts +15 -0
  58. package/dist/interpreter/execute-every.js +143 -0
  59. package/dist/interpreter/execute-fork.d.ts +3 -0
  60. package/dist/interpreter/execute-fork.js +215 -0
  61. package/dist/interpreter/execute-llm.d.ts +4 -0
  62. package/dist/interpreter/execute-llm.js +217 -0
  63. package/dist/interpreter/execute-loop.d.ts +3 -0
  64. package/dist/interpreter/execute-loop.js +171 -0
  65. package/dist/interpreter/execute-provide.d.ts +10 -0
  66. package/dist/interpreter/execute-provide.js +64 -0
  67. package/dist/interpreter/execute-run.d.ts +3 -0
  68. package/dist/interpreter/execute-run.js +39 -0
  69. package/dist/interpreter/execute-spawn.d.ts +11 -0
  70. package/dist/interpreter/execute-spawn.js +113 -0
  71. package/dist/interpreter/execute-tool.d.ts +5 -0
  72. package/dist/interpreter/execute-tool.js +81 -0
  73. package/dist/interpreter/execute.d.ts +14 -0
  74. package/dist/interpreter/execute.js +119 -0
  75. package/dist/interpreter/framework-cast.d.ts +10 -0
  76. package/dist/interpreter/framework-cast.js +16 -0
  77. package/dist/interpreter/message-helpers.d.ts +9 -0
  78. package/dist/interpreter/message-helpers.js +42 -0
  79. package/dist/interpreter/typeguards.d.ts +25 -0
  80. package/dist/interpreter/typeguards.js +50 -0
  81. package/dist/memory/budget.d.ts +23 -0
  82. package/dist/memory/budget.js +117 -0
  83. package/dist/memory/function-call-utils.d.ts +6 -0
  84. package/dist/memory/function-call-utils.js +25 -0
  85. package/dist/memory/layer-api.d.ts +14 -0
  86. package/dist/memory/layer-api.js +91 -0
  87. package/dist/memory/layer-lifecycle.d.ts +160 -0
  88. package/dist/memory/layer-lifecycle.js +552 -0
  89. package/dist/memory/layer-usage.d.ts +15 -0
  90. package/dist/memory/layer-usage.js +48 -0
  91. package/dist/memory/layers/durable-task-state.d.ts +79 -0
  92. package/dist/memory/layers/durable-task-state.js +118 -0
  93. package/dist/memory/layers/file-reference.d.ts +55 -0
  94. package/dist/memory/layers/file-reference.js +538 -0
  95. package/dist/memory/layers/history-window.d.ts +29 -0
  96. package/dist/memory/layers/history-window.js +114 -0
  97. package/dist/memory/layers/observational-memory.d.ts +53 -0
  98. package/dist/memory/layers/observational-memory.js +110 -0
  99. package/dist/memory/layers/plan.d.ts +55 -0
  100. package/dist/memory/layers/plan.js +369 -0
  101. package/dist/memory/layers/static-content.d.ts +27 -0
  102. package/dist/memory/layers/static-content.js +35 -0
  103. package/dist/memory/layers/steering.d.ts +51 -0
  104. package/dist/memory/layers/steering.js +258 -0
  105. package/dist/memory/layers/tool-memory-layer.d.ts +16 -0
  106. package/dist/memory/layers/tool-memory-layer.js +43 -0
  107. package/dist/memory/layers/working-memory.d.ts +44 -0
  108. package/dist/memory/layers/working-memory.js +92 -0
  109. package/dist/memory/projector.d.ts +10 -0
  110. package/dist/memory/projector.js +19 -0
  111. package/dist/memory/scope.d.ts +3 -0
  112. package/dist/memory/scope.js +31 -0
  113. package/dist/memory/strip-unresolved.d.ts +13 -0
  114. package/dist/memory/strip-unresolved.js +35 -0
  115. package/dist/observability/genai-attributes.d.ts +11 -0
  116. package/dist/observability/genai-attributes.js +12 -0
  117. package/dist/observability/span-impl.d.ts +20 -0
  118. package/dist/observability/span-impl.js +33 -0
  119. package/dist/observability/trace-exporter.d.ts +15 -0
  120. package/dist/observability/trace-exporter.js +26 -0
  121. package/dist/patterns/flow.d.ts +52 -0
  122. package/dist/patterns/flow.js +103 -0
  123. package/dist/patterns/interview.d.ts +53 -0
  124. package/dist/patterns/interview.js +113 -0
  125. package/dist/patterns/plans.d.ts +45 -0
  126. package/dist/patterns/plans.js +125 -0
  127. package/dist/patterns/ralph-wiggum.d.ts +20 -0
  128. package/dist/patterns/ralph-wiggum.js +37 -0
  129. package/dist/patterns/react.d.ts +18 -0
  130. package/dist/patterns/react.js +39 -0
  131. package/dist/portable.d.ts +26 -0
  132. package/dist/portable.js +34 -0
  133. package/dist/runtime/agent-harness.d.ts +206 -0
  134. package/dist/runtime/agent-harness.js +1170 -0
  135. package/dist/runtime/broadcaster-utils.d.ts +30 -0
  136. package/dist/runtime/broadcaster-utils.js +53 -0
  137. package/dist/runtime/channel-store.d.ts +18 -0
  138. package/dist/runtime/channel-store.js +187 -0
  139. package/dist/runtime/context-impl.d.ts +80 -0
  140. package/dist/runtime/context-impl.js +140 -0
  141. package/dist/runtime/cwd-helpers.d.ts +42 -0
  142. package/dist/runtime/cwd-helpers.js +68 -0
  143. package/dist/runtime/detached-handle.d.ts +14 -0
  144. package/dist/runtime/detached-handle.js +56 -0
  145. package/dist/runtime/event-broadcaster.d.ts +61 -0
  146. package/dist/runtime/event-broadcaster.js +187 -0
  147. package/dist/runtime/exec-context-factory.d.ts +25 -0
  148. package/dist/runtime/exec-context-factory.js +30 -0
  149. package/dist/runtime/in-memory-storage.d.ts +9 -0
  150. package/dist/runtime/in-memory-storage.js +33 -0
  151. package/dist/runtime/item-log-impl.d.ts +11 -0
  152. package/dist/runtime/item-log-impl.js +25 -0
  153. package/dist/runtime/message-queue.d.ts +30 -0
  154. package/dist/runtime/message-queue.js +46 -0
  155. package/dist/runtime/session-runner.d.ts +77 -0
  156. package/dist/runtime/session-runner.js +253 -0
  157. package/dist/runtime/session-streams.d.ts +11 -0
  158. package/dist/runtime/session-streams.js +204 -0
  159. package/dist/runtime/tool-memory.d.ts +6 -0
  160. package/dist/runtime/tool-memory.js +22 -0
  161. package/dist/schemas/item.d.ts +35 -0
  162. package/dist/schemas/item.js +155 -0
  163. package/dist/types/channel.d.ts +29 -0
  164. package/dist/types/channel.js +1 -0
  165. package/dist/types/common.d.ts +111 -0
  166. package/dist/types/common.js +1 -0
  167. package/dist/types/context.d.ts +93 -0
  168. package/dist/types/context.js +1 -0
  169. package/dist/types/detached.d.ts +17 -0
  170. package/dist/types/detached.js +7 -0
  171. package/dist/types/embed.d.ts +5 -0
  172. package/dist/types/embed.js +1 -0
  173. package/dist/types/error.d.ts +51 -0
  174. package/dist/types/error.js +1 -0
  175. package/dist/types/fs-adapter.d.ts +51 -0
  176. package/dist/types/fs-adapter.js +3 -0
  177. package/dist/types/harness-result.d.ts +43 -0
  178. package/dist/types/harness-result.js +2 -0
  179. package/dist/types/items.d.ts +115 -0
  180. package/dist/types/items.js +2 -0
  181. package/dist/types/memory.d.ts +336 -0
  182. package/dist/types/memory.js +13 -0
  183. package/dist/types/mutable-context.d.ts +13 -0
  184. package/dist/types/mutable-context.js +1 -0
  185. package/dist/types/observability.d.ts +30 -0
  186. package/dist/types/observability.js +1 -0
  187. package/dist/types/runtime.d.ts +255 -0
  188. package/dist/types/runtime.js +1 -0
  189. package/dist/types/shell-adapter.d.ts +34 -0
  190. package/dist/types/shell-adapter.js +10 -0
  191. package/dist/types/steering.d.ts +89 -0
  192. package/dist/types/steering.js +14 -0
  193. package/dist/types/step.d.ts +206 -0
  194. package/dist/types/step.js +1 -0
  195. package/dist/types/subprocess-adapter.d.ts +41 -0
  196. package/dist/types/subprocess-adapter.js +1 -0
  197. package/dist/types/tool-context.d.ts +34 -0
  198. package/dist/types/tool-context.js +1 -0
  199. package/dist/unstable.d.ts +28 -0
  200. package/dist/unstable.js +24 -0
  201. package/dist/until/combinators.d.ts +17 -0
  202. package/dist/until/combinators.js +49 -0
  203. package/dist/until/never.d.ts +9 -0
  204. package/dist/until/never.js +12 -0
  205. package/dist/until/predicates.d.ts +43 -0
  206. package/dist/until/predicates.js +126 -0
  207. package/package.json +81 -0
@@ -0,0 +1,538 @@
1
+ import { createMessage, estimateTokens } from '../../interpreter/message-helpers';
2
+ import { Slot } from '../../types/memory';
3
+ /** Default max file size: 1MB */
4
+ const DEFAULT_MAX_FILE_SIZE = 1024 * 1024;
5
+ /** Default allowed extensions */
6
+ const DEFAULT_ALLOWED_EXTENSIONS = [
7
+ '.ts',
8
+ '.tsx',
9
+ '.js',
10
+ '.jsx',
11
+ '.mjs',
12
+ '.cjs',
13
+ '.json',
14
+ '.yaml',
15
+ '.yml',
16
+ '.toml',
17
+ '.md',
18
+ '.mdx',
19
+ '.txt',
20
+ '.csv',
21
+ '.html',
22
+ '.css',
23
+ '.scss',
24
+ '.less',
25
+ '.py',
26
+ '.rb',
27
+ '.go',
28
+ '.rs',
29
+ '.java',
30
+ '.kt',
31
+ '.swift',
32
+ '.c',
33
+ '.cpp',
34
+ '.h',
35
+ '.hpp',
36
+ '.sh',
37
+ '.bash',
38
+ '.zsh',
39
+ '.sql',
40
+ '.graphql',
41
+ '.xml',
42
+ '.svg',
43
+ '.env',
44
+ '.env.local',
45
+ '.env.example',
46
+ '.gitignore',
47
+ '.dockerignore',
48
+ 'Dockerfile',
49
+ 'Makefile',
50
+ 'Taskfile',
51
+ ];
52
+ //#endregion
53
+ //#region Helpers
54
+ // Pattern requires file-like structure: path with extension or explicit path separator
55
+ // Avoids matching #hashtag, #123, #region, etc.
56
+ const FILE_REF_PATTERN = /#((?:[a-zA-Z0-9_.-]+\/)*[a-zA-Z0-9_.-]+\.[a-zA-Z0-9]+)/g;
57
+ function sanitizeAnchor(refPath) {
58
+ return refPath.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase();
59
+ }
60
+ function transformReferencesToAnchors(text) {
61
+ return text.replace(FILE_REF_PATTERN, (_match, filePath) => {
62
+ const anchor = sanitizeAnchor(filePath);
63
+ return `[#${filePath}](#${anchor})`;
64
+ });
65
+ }
66
+ function extractFileReferences(text) {
67
+ const refs = [];
68
+ const pattern = new RegExp(FILE_REF_PATTERN);
69
+ let match = pattern.exec(text);
70
+ while (match !== null) {
71
+ refs.push(match[1]);
72
+ match = pattern.exec(text);
73
+ }
74
+ return [
75
+ ...new Set(refs),
76
+ ]; // Dedupe
77
+ }
78
+ function normalizePath(input) {
79
+ const raw = input.length > 0 ? input : '.';
80
+ const absolute = raw.startsWith('/') ? raw : `/${raw}`;
81
+ const parts = [];
82
+ for (const part of absolute.split('/')) {
83
+ if (part.length === 0 || part === '.') {
84
+ continue;
85
+ }
86
+ if (part === '..') {
87
+ parts.pop();
88
+ continue;
89
+ }
90
+ parts.push(part);
91
+ }
92
+ return `/${parts.join('/')}`;
93
+ }
94
+ function resolvePath(base, ref = '') {
95
+ return normalizePath(ref.startsWith('/') ? ref : `${base}/${ref}`);
96
+ }
97
+ function pathBasename(input) {
98
+ const normalized = normalizePath(input);
99
+ if (normalized === '/') {
100
+ return '';
101
+ }
102
+ return normalized.slice(normalized.lastIndexOf('/') + 1);
103
+ }
104
+ function pathExtname(input) {
105
+ const name = pathBasename(input);
106
+ const idx = name.lastIndexOf('.');
107
+ return idx > 0 ? name.slice(idx) : '';
108
+ }
109
+ function isAbsolutePath(input) {
110
+ return input.startsWith('/');
111
+ }
112
+ function currentWorkingDirectory() {
113
+ return typeof process !== 'undefined' ? process.cwd() : '/';
114
+ }
115
+ function simpleHash(content) {
116
+ let hash = 0;
117
+ for (let i = 0; i < content.length; i++) {
118
+ const char = content.charCodeAt(i);
119
+ hash = (hash << 5) - hash + char;
120
+ hash = hash & hash; // Convert to 32bit integer
121
+ }
122
+ // Include content length to reduce collision probability
123
+ return `${content.length}:${hash.toString(16)}`;
124
+ }
125
+ function isInputMessage(item) {
126
+ return (item.type === 'message' &&
127
+ 'role' in item &&
128
+ 'content' in item &&
129
+ Array.isArray(item.content) &&
130
+ item.content.every((c) => typeof c === 'object' && c !== null && 'type' in c && c.type === 'input_text'));
131
+ }
132
+ function extractTextFromItem(item) {
133
+ if (!isInputMessage(item)) {
134
+ return '';
135
+ }
136
+ return item.content.map((c) => c.text).join('\n');
137
+ }
138
+ async function readFileContent(absolutePath, opts, fsAdapter) {
139
+ try {
140
+ // Security: Validate path stays within baseDir (prevent path traversal)
141
+ const normalizedBase = resolvePath(opts.baseDir);
142
+ const normalizedPath = resolvePath(absolutePath);
143
+ if (!normalizedPath.startsWith(`${normalizedBase}/`) && normalizedPath !== normalizedBase) {
144
+ return {
145
+ content: null,
146
+ deleted: false,
147
+ error: 'PATH_TRAVERSAL: File outside allowed directory',
148
+ };
149
+ }
150
+ // Security: Check file extension
151
+ const ext = pathExtname(normalizedPath).toLowerCase();
152
+ const basename = pathBasename(normalizedPath);
153
+ const isAllowedExt = opts.allowedExtensions.some((allowed) => ext === allowed.toLowerCase() || basename === allowed);
154
+ if (!isAllowedExt && opts.allowedExtensions.length > 0) {
155
+ return {
156
+ content: null,
157
+ deleted: false,
158
+ error: `DISALLOWED_EXTENSION: ${ext || 'no extension'}`,
159
+ };
160
+ }
161
+ // Security: Check for symlinks if not allowed
162
+ const stats = await fsAdapter.lstat(absolutePath);
163
+ if (stats.isSymbolicLink() && !opts.followSymlinks) {
164
+ return {
165
+ content: null,
166
+ deleted: false,
167
+ error: 'SYMLINK: Symlinks not allowed',
168
+ };
169
+ }
170
+ // Security: Check file size
171
+ const fileStats = opts.followSymlinks ? await fsAdapter.stat(absolutePath) : stats;
172
+ if (fileStats.size > opts.maxFileSize) {
173
+ return {
174
+ content: null,
175
+ deleted: false,
176
+ error: `FILE_TOO_LARGE: ${fileStats.size} bytes exceeds limit of ${opts.maxFileSize}`,
177
+ };
178
+ }
179
+ const content = await fsAdapter.readFileText(absolutePath);
180
+ return {
181
+ content,
182
+ deleted: false,
183
+ };
184
+ }
185
+ catch (e) {
186
+ if (isNodeError(e) && e.code === 'ENOENT') {
187
+ return {
188
+ content: null,
189
+ deleted: true,
190
+ };
191
+ }
192
+ if (isNodeError(e) && e.code === 'EACCES') {
193
+ return {
194
+ content: null,
195
+ deleted: false,
196
+ error: 'PERMISSION_DENIED: Cannot read file',
197
+ };
198
+ }
199
+ throw e;
200
+ }
201
+ }
202
+ function isNodeError(e) {
203
+ return e instanceof Error && 'code' in e;
204
+ }
205
+ function extractResponseText(items) {
206
+ const texts = [];
207
+ for (const item of items) {
208
+ if (item.type !== 'message') {
209
+ continue;
210
+ }
211
+ if (!('content' in item) || !Array.isArray(item.content)) {
212
+ continue;
213
+ }
214
+ for (const part of item.content) {
215
+ if ('text' in part && typeof part.text === 'string') {
216
+ texts.push(part.text);
217
+ }
218
+ }
219
+ }
220
+ return texts.join('').trim();
221
+ }
222
+ async function scoreFileRelevance(params) {
223
+ const { filePath, fileContent, userQuery, ctx, model } = params;
224
+ if (!ctx.callModel) {
225
+ const queryLower = userQuery.toLowerCase();
226
+ const pathLower = filePath.toLowerCase();
227
+ if (pathLower.includes(queryLower) || queryLower.includes(pathBasename(pathLower))) {
228
+ return 80;
229
+ }
230
+ return 50;
231
+ }
232
+ const scoringPrompt = `Rate the relevance of this file to the user's query.
233
+
234
+ User Query: ${userQuery}
235
+
236
+ File Path: ${filePath}
237
+ File Content (first 2000 chars):
238
+ ${fileContent.slice(0, 2e3)}
239
+
240
+ Respond with ONLY a number from 0-100 where:
241
+ - 0-20: Not relevant at all
242
+ - 21-40: Slightly relevant
243
+ - 41-60: Moderately relevant
244
+ - 61-80: Highly relevant
245
+ - 81-100: Critical/essential
246
+
247
+ Score:`;
248
+ try {
249
+ const response = await ctx.callModel({
250
+ model,
251
+ items: [
252
+ createMessage(scoringPrompt, 'user'),
253
+ ],
254
+ instructions: 'You are a relevance scorer. Respond with only a number 0-100.',
255
+ });
256
+ const responseText = extractResponseText(response.items);
257
+ const score = Number.parseInt(responseText, 10);
258
+ if (Number.isNaN(score) || score < 0 || score > 100) {
259
+ return 50;
260
+ }
261
+ return score;
262
+ }
263
+ catch {
264
+ return 50;
265
+ }
266
+ }
267
+ //#endregion
268
+ //#region Layer Factory
269
+ /**
270
+ * Creates a memory layer that tracks file references in user messages.
271
+ *
272
+ * Syntax: `#path/to/file` in user messages
273
+ *
274
+ * Behavior:
275
+ * - Transforms references to markdown anchor links `[#path/to/file](#path-to-file)`
276
+ * - Scores file relevance using LLM when first referenced
277
+ * - Injects file contents into context via recall(), ordered by priority
278
+ * - Detects file changes on each new message, triggers immediate re-render
279
+ * - Shows warning for deleted files
280
+ *
281
+ * @public
282
+ */
283
+ export function fileReference(opts) {
284
+ const baseDir = opts?.baseDir ?? currentWorkingDirectory();
285
+ const slot = opts?.slot ?? Slot.RAG;
286
+ const scoringModel = opts?.scoringModel ?? 'anthropic/claude-haiku-4-5-20251001';
287
+ const maxFileSize = opts?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
288
+ const followSymlinks = opts?.followSymlinks ?? false;
289
+ const allowedExtensions = opts?.allowedExtensions ?? DEFAULT_ALLOWED_EXTENSIONS;
290
+ // Pre-compute read options for reuse
291
+ const readOpts = {
292
+ maxFileSize,
293
+ followSymlinks,
294
+ baseDir,
295
+ allowedExtensions,
296
+ };
297
+ return {
298
+ id: 'file-reference',
299
+ name: 'File Reference',
300
+ slot,
301
+ scope: 'thread',
302
+ budget: 'auto',
303
+ rerenderTiming: 'immediate',
304
+ hooks: {
305
+ async init() {
306
+ return {
307
+ state: {
308
+ files: new Map(),
309
+ baseDir,
310
+ },
311
+ };
312
+ },
313
+ async onItemAppend({ items, ctx, state }) {
314
+ const newFiles = new Map(state.files);
315
+ let hasChanges = false;
316
+ let userQuery = '';
317
+ // Update readOpts with current state's baseDir
318
+ const currentReadOpts = {
319
+ ...readOpts,
320
+ baseDir: state.baseDir,
321
+ };
322
+ // Process each item
323
+ const transformedItems = [];
324
+ for (const item of items) {
325
+ const text = extractTextFromItem(item);
326
+ if (!text) {
327
+ transformedItems.push(item);
328
+ continue;
329
+ }
330
+ // Extract user query for scoring
331
+ if (item.type === 'message' && 'role' in item && item.role === 'user') {
332
+ userQuery = text;
333
+ }
334
+ // Extract file references
335
+ const refs = extractFileReferences(text);
336
+ // Process new references
337
+ for (const ref of refs) {
338
+ if (!newFiles.has(ref)) {
339
+ // Security: Reject absolute paths - all paths must be relative to baseDir
340
+ if (isAbsolutePath(ref)) {
341
+ newFiles.set(ref, {
342
+ absolutePath: ref,
343
+ referencePath: ref,
344
+ content: null,
345
+ priority: 0,
346
+ contentHash: '',
347
+ deleted: false,
348
+ tokenCount: 0,
349
+ error: 'ABSOLUTE_PATH: Only relative paths are allowed',
350
+ });
351
+ hasChanges = true;
352
+ continue;
353
+ }
354
+ const absolutePath = resolvePath(state.baseDir, ref);
355
+ const result = await readFileContent(absolutePath, currentReadOpts, ctx.fs);
356
+ const priority = result.content
357
+ ? await scoreFileRelevance({
358
+ filePath: ref,
359
+ fileContent: result.content,
360
+ userQuery,
361
+ ctx,
362
+ model: scoringModel,
363
+ })
364
+ : 0;
365
+ newFiles.set(ref, {
366
+ absolutePath,
367
+ referencePath: ref,
368
+ content: result.content,
369
+ priority,
370
+ contentHash: result.content ? simpleHash(result.content) : '',
371
+ deleted: result.deleted,
372
+ tokenCount: result.content ? estimateTokens(result.content) : 0,
373
+ error: result.error,
374
+ });
375
+ hasChanges = true;
376
+ }
377
+ }
378
+ // Transform references to anchor links
379
+ if (refs.length > 0 && isInputMessage(item)) {
380
+ const transformedContent = item.content.map((c) => {
381
+ const part = {
382
+ type: 'input_text',
383
+ text: transformReferencesToAnchors(c.text),
384
+ };
385
+ return part;
386
+ });
387
+ const transformedItem = {
388
+ ...item,
389
+ content: transformedContent,
390
+ };
391
+ transformedItems.push(transformedItem);
392
+ }
393
+ else {
394
+ transformedItems.push(item);
395
+ }
396
+ }
397
+ // Check existing files for changes
398
+ for (const [ref, tracked] of newFiles) {
399
+ // Skip files with errors - they won't change
400
+ if (tracked.error) {
401
+ continue;
402
+ }
403
+ const result = await readFileContent(tracked.absolutePath, currentReadOpts, ctx.fs);
404
+ if (result.deleted && !tracked.deleted) {
405
+ // File was deleted
406
+ newFiles.set(ref, {
407
+ ...tracked,
408
+ content: null,
409
+ deleted: true,
410
+ });
411
+ hasChanges = true;
412
+ }
413
+ else if (result.error && !tracked.error) {
414
+ // File now has error (e.g., permission changed)
415
+ newFiles.set(ref, {
416
+ ...tracked,
417
+ content: null,
418
+ error: result.error,
419
+ });
420
+ hasChanges = true;
421
+ }
422
+ else if (!result.deleted && result.content) {
423
+ const newHash = simpleHash(result.content);
424
+ if (newHash !== tracked.contentHash) {
425
+ // File content changed
426
+ const priority = userQuery
427
+ ? await scoreFileRelevance({
428
+ filePath: ref,
429
+ fileContent: result.content,
430
+ userQuery,
431
+ ctx,
432
+ model: scoringModel,
433
+ })
434
+ : tracked.priority;
435
+ newFiles.set(ref, {
436
+ ...tracked,
437
+ content: result.content,
438
+ contentHash: newHash,
439
+ deleted: false,
440
+ tokenCount: estimateTokens(result.content),
441
+ priority,
442
+ error: undefined,
443
+ });
444
+ hasChanges = true;
445
+ }
446
+ }
447
+ }
448
+ return {
449
+ items: transformedItems,
450
+ state: {
451
+ ...state,
452
+ files: newFiles,
453
+ },
454
+ rerender: hasChanges,
455
+ timing: 'immediate',
456
+ scope: 'self',
457
+ };
458
+ },
459
+ async recall({ state, budget }) {
460
+ if (state.files.size === 0) {
461
+ return {
462
+ items: [],
463
+ tokenCount: 0,
464
+ };
465
+ }
466
+ // Sort files by priority (highest first)
467
+ const sortedFiles = [
468
+ ...state.files.values(),
469
+ ].sort((a, b) => b.priority - a.priority);
470
+ // Build content blocks within budget
471
+ const blocks = [];
472
+ let totalTokens = 0;
473
+ for (const file of sortedFiles) {
474
+ // Handle deleted files
475
+ if (file.deleted) {
476
+ const block = `## ${file.referencePath}\n\n[FILE DELETED: ${file.absolutePath}]`;
477
+ const tokens = estimateTokens(block);
478
+ if (totalTokens + tokens <= budget) {
479
+ blocks.push(block);
480
+ totalTokens += tokens;
481
+ }
482
+ continue;
483
+ }
484
+ // Handle files with errors (security, size, permission)
485
+ if (file.error) {
486
+ const block = `## ${file.referencePath}\n\n[ERROR: ${file.error}]`;
487
+ const tokens = estimateTokens(block);
488
+ if (totalTokens + tokens <= budget) {
489
+ blocks.push(block);
490
+ totalTokens += tokens;
491
+ }
492
+ continue;
493
+ }
494
+ if (!file.content) {
495
+ continue;
496
+ }
497
+ const header = `## ${file.referencePath}`;
498
+ const headerTokens = estimateTokens(header);
499
+ if (totalTokens + headerTokens >= budget) {
500
+ // Can't fit even the header
501
+ break;
502
+ }
503
+ const remainingBudget = budget - totalTokens - headerTokens - 10; // Buffer for formatting
504
+ let contentToInclude = file.content;
505
+ if (file.tokenCount > remainingBudget) {
506
+ // Truncate content - show head + tail
507
+ const lines = file.content.split('\n');
508
+ const headLines = Math.floor(lines.length * 0.6);
509
+ const tailLines = Math.floor(lines.length * 0.3);
510
+ const head = lines.slice(0, headLines).join('\n');
511
+ const tail = lines.slice(-tailLines).join('\n');
512
+ contentToInclude = `${head}\n\n... [truncated ${lines.length - headLines - tailLines} lines] ...\n\n${tail}`;
513
+ }
514
+ const block = `${header}\n\n\`\`\`\n${contentToInclude}\n\`\`\``;
515
+ const blockTokens = estimateTokens(block);
516
+ if (totalTokens + blockTokens <= budget) {
517
+ blocks.push(block);
518
+ totalTokens += blockTokens;
519
+ }
520
+ }
521
+ if (blocks.length === 0) {
522
+ return {
523
+ items: [],
524
+ tokenCount: 0,
525
+ };
526
+ }
527
+ const content = `# Referenced Files\n\n${blocks.join('\n\n')}`;
528
+ return {
529
+ items: [
530
+ createMessage(content, 'developer'),
531
+ ],
532
+ tokenCount: estimateTokens(content),
533
+ };
534
+ },
535
+ },
536
+ };
537
+ }
538
+ //#endregion
@@ -0,0 +1,29 @@
1
+ /**
2
+ * History-window memory layer — caps the number of items projected to the LLM
3
+ * without touching `itemLog` storage. Pure read-side projection via the
4
+ * `projectHistory` hook.
5
+ *
6
+ * Algorithm per turn:
7
+ * 1. Slice the last `maxItems`.
8
+ * 2. If the slice lacks a user `message` or an assistant `message`, expand
9
+ * backward until both are present. The cap may be exceeded — for
10
+ * tool-burst histories the projected length can grow well beyond
11
+ * `maxItems` because the most recent role-pair may be far back.
12
+ * 3. Drop any orphan `function_call` / `function_call_output` left at the
13
+ * slice boundary via `stripUnresolvedToolCalls`.
14
+ */
15
+ import type { MemoryLayer } from '../../types/memory';
16
+ /** @public Configuration for the {@link historyWindow} layer. */
17
+ export interface HistoryWindowConfig {
18
+ /** Cap on the number of trailing items projected to the LLM. Defaults to 40. */
19
+ maxItems?: number;
20
+ }
21
+ /**
22
+ * Creates a history-window memory layer that caps the trailing items projected
23
+ * to the LLM. Storage stays unbounded; only the wire payload is narrowed.
24
+ *
25
+ * @public
26
+ * @param config - Optional `{ maxItems }`. Defaults to 40.
27
+ * @returns A `MemoryLayer` that contributes only a `projectHistory` hook.
28
+ */
29
+ export declare function historyWindow(config?: HistoryWindowConfig): MemoryLayer<null>;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * History-window memory layer — caps the number of items projected to the LLM
3
+ * without touching `itemLog` storage. Pure read-side projection via the
4
+ * `projectHistory` hook.
5
+ *
6
+ * Algorithm per turn:
7
+ * 1. Slice the last `maxItems`.
8
+ * 2. If the slice lacks a user `message` or an assistant `message`, expand
9
+ * backward until both are present. The cap may be exceeded — for
10
+ * tool-burst histories the projected length can grow well beyond
11
+ * `maxItems` because the most recent role-pair may be far back.
12
+ * 3. Drop any orphan `function_call` / `function_call_output` left at the
13
+ * slice boundary via `stripUnresolvedToolCalls`.
14
+ */
15
+ import { NoeticConfigError } from '../../errors/noetic-config-error';
16
+ import { isAssistantMessage, isUserMessage } from '../../interpreter/typeguards';
17
+ import { Slot } from '../../types/memory';
18
+ import { stripUnresolvedToolCalls } from '../strip-unresolved';
19
+ //#region Constants
20
+ const DEFAULT_MAX_ITEMS = 40;
21
+ const MIN_MAX_ITEMS = 2;
22
+ const MAX_MAX_ITEMS = 1e4;
23
+ const HISTORY_WINDOW_SLOT = Slot.PROCEDURAL + 25; // 275 — runs after recall-contributing layers
24
+ //#endregion
25
+ //#region Helpers
26
+ function validateMaxItems(value) {
27
+ if (!Number.isInteger(value) || value < MIN_MAX_ITEMS || value > MAX_MAX_ITEMS) {
28
+ throw new NoeticConfigError({
29
+ code: 'INVALID_HISTORY_WINDOW_MAX_ITEMS',
30
+ message: `historyWindow: maxItems must be an integer in [${MIN_MAX_ITEMS}, ${MAX_MAX_ITEMS}], got ${value}`,
31
+ hint: 'Pass a sensible cap such as { maxItems: 40 }, or omit the layer to keep history uncapped.',
32
+ });
33
+ }
34
+ }
35
+ /**
36
+ * Walk backwards from the natural slice point until both a user message and
37
+ * an assistant message are present. Returns the index at which the projected
38
+ * window should start in the full `items` array.
39
+ */
40
+ function expandToMinimumExchange(items, sliceStart) {
41
+ let start = sliceStart;
42
+ let needsUser = true;
43
+ let needsAssistant = true;
44
+ // Pass 1: scan the natural slice for the user/assistant pair. Early-return
45
+ // at the original sliceStart when both roles are already inside.
46
+ for (let i = items.length - 1; i >= start; i--) {
47
+ const item = items[i];
48
+ if (needsUser && isUserMessage(item)) {
49
+ needsUser = false;
50
+ }
51
+ if (needsAssistant && isAssistantMessage(item)) {
52
+ needsAssistant = false;
53
+ }
54
+ if (!needsUser && !needsAssistant) {
55
+ return start;
56
+ }
57
+ }
58
+ // Pass 2: walk backward past the slice until the missing role(s) are found.
59
+ for (let i = start - 1; i >= 0 && (needsUser || needsAssistant); i--) {
60
+ const item = items[i];
61
+ if (needsUser && isUserMessage(item)) {
62
+ needsUser = false;
63
+ start = i;
64
+ continue;
65
+ }
66
+ if (needsAssistant && isAssistantMessage(item)) {
67
+ needsAssistant = false;
68
+ start = i;
69
+ }
70
+ }
71
+ return start;
72
+ }
73
+ //#endregion
74
+ //#region Public API
75
+ /**
76
+ * Creates a history-window memory layer that caps the trailing items projected
77
+ * to the LLM. Storage stays unbounded; only the wire payload is narrowed.
78
+ *
79
+ * @public
80
+ * @param config - Optional `{ maxItems }`. Defaults to 40.
81
+ * @returns A `MemoryLayer` that contributes only a `projectHistory` hook.
82
+ */
83
+ export function historyWindow(config) {
84
+ const maxItems = config?.maxItems ?? DEFAULT_MAX_ITEMS;
85
+ validateMaxItems(maxItems);
86
+ return {
87
+ id: 'history-window',
88
+ name: 'History Window',
89
+ slot: HISTORY_WINDOW_SLOT,
90
+ scope: 'execution',
91
+ hooks: {
92
+ async init() {
93
+ return {
94
+ state: null,
95
+ };
96
+ },
97
+ async projectHistory({ items }) {
98
+ if (items.length <= maxItems) {
99
+ return {
100
+ items,
101
+ };
102
+ }
103
+ const sliceStart = items.length - maxItems;
104
+ const expandedStart = expandToMinimumExchange(items, sliceStart);
105
+ const window = items.slice(expandedStart);
106
+ const cleaned = stripUnresolvedToolCalls(window);
107
+ return {
108
+ items: cleaned,
109
+ };
110
+ },
111
+ },
112
+ };
113
+ }
114
+ //#endregion