@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.
- package/LICENSE +201 -0
- package/README.md +38 -0
- package/dist/adapters/in-memory-fs-adapter.d.ts +3 -0
- package/dist/adapters/in-memory-fs-adapter.js +245 -0
- package/dist/adapters/in-memory-shell-adapter.d.ts +9 -0
- package/dist/adapters/in-memory-shell-adapter.js +20 -0
- package/dist/adapters/in-memory-subprocess-adapter.d.ts +5 -0
- package/dist/adapters/in-memory-subprocess-adapter.js +126 -0
- package/dist/adapters/local-fs-adapter.d.ts +3 -0
- package/dist/adapters/local-fs-adapter.js +43 -0
- package/dist/adapters/local-shell-adapter.d.ts +51 -0
- package/dist/adapters/local-shell-adapter.js +167 -0
- package/dist/adapters/local-subprocess-adapter.d.ts +16 -0
- package/dist/adapters/local-subprocess-adapter.js +187 -0
- package/dist/adapters/openrouter.d.ts +57 -0
- package/dist/adapters/openrouter.js +248 -0
- package/dist/builders/channel-builder.d.ts +36 -0
- package/dist/builders/channel-builder.js +15 -0
- package/dist/builders/control-flow-builders.d.ts +79 -0
- package/dist/builders/control-flow-builders.js +53 -0
- package/dist/builders/every.d.ts +45 -0
- package/dist/builders/every.js +60 -0
- package/dist/builders/layer-provides-builders.d.ts +32 -0
- package/dist/builders/layer-provides-builders.js +33 -0
- package/dist/builders/loop-builder.d.ts +23 -0
- package/dist/builders/loop-builder.js +46 -0
- package/dist/builders/memory-builder.d.ts +11 -0
- package/dist/builders/memory-builder.js +15 -0
- package/dist/builders/provide-builder.d.ts +20 -0
- package/dist/builders/provide-builder.js +34 -0
- package/dist/builders/spawn-builder.d.ts +20 -0
- package/dist/builders/spawn-builder.js +33 -0
- package/dist/builders/step-builders.d.ts +62 -0
- package/dist/builders/step-builders.js +99 -0
- package/dist/builders/tool-builder.d.ts +52 -0
- package/dist/builders/tool-builder.js +59 -0
- package/dist/conditions/conditions.d.ts +114 -0
- package/dist/conditions/conditions.js +271 -0
- package/dist/conditions/cosine-similarity.d.ts +11 -0
- package/dist/conditions/cosine-similarity.js +28 -0
- package/dist/conditions/index.d.ts +3 -0
- package/dist/conditions/index.js +2 -0
- package/dist/errors/noetic-config-error.d.ts +41 -0
- package/dist/errors/noetic-config-error.js +44 -0
- package/dist/errors/noetic-error.d.ts +18 -0
- package/dist/errors/noetic-error.js +52 -0
- package/dist/index.d.ts +152 -0
- package/dist/index.js +112 -0
- package/dist/internal-test.d.ts +11 -0
- package/dist/internal-test.js +11 -0
- package/dist/interpreter/clone-guard.d.ts +4 -0
- package/dist/interpreter/clone-guard.js +18 -0
- package/dist/interpreter/collect-tools.d.ts +12 -0
- package/dist/interpreter/collect-tools.js +70 -0
- package/dist/interpreter/execute-branch.d.ts +3 -0
- package/dist/interpreter/execute-branch.js +10 -0
- package/dist/interpreter/execute-every.d.ts +15 -0
- package/dist/interpreter/execute-every.js +143 -0
- package/dist/interpreter/execute-fork.d.ts +3 -0
- package/dist/interpreter/execute-fork.js +215 -0
- package/dist/interpreter/execute-llm.d.ts +4 -0
- package/dist/interpreter/execute-llm.js +217 -0
- package/dist/interpreter/execute-loop.d.ts +3 -0
- package/dist/interpreter/execute-loop.js +171 -0
- package/dist/interpreter/execute-provide.d.ts +10 -0
- package/dist/interpreter/execute-provide.js +64 -0
- package/dist/interpreter/execute-run.d.ts +3 -0
- package/dist/interpreter/execute-run.js +39 -0
- package/dist/interpreter/execute-spawn.d.ts +11 -0
- package/dist/interpreter/execute-spawn.js +113 -0
- package/dist/interpreter/execute-tool.d.ts +5 -0
- package/dist/interpreter/execute-tool.js +81 -0
- package/dist/interpreter/execute.d.ts +14 -0
- package/dist/interpreter/execute.js +119 -0
- package/dist/interpreter/framework-cast.d.ts +10 -0
- package/dist/interpreter/framework-cast.js +16 -0
- package/dist/interpreter/message-helpers.d.ts +9 -0
- package/dist/interpreter/message-helpers.js +42 -0
- package/dist/interpreter/typeguards.d.ts +25 -0
- package/dist/interpreter/typeguards.js +50 -0
- package/dist/memory/budget.d.ts +23 -0
- package/dist/memory/budget.js +117 -0
- package/dist/memory/function-call-utils.d.ts +6 -0
- package/dist/memory/function-call-utils.js +25 -0
- package/dist/memory/layer-api.d.ts +14 -0
- package/dist/memory/layer-api.js +91 -0
- package/dist/memory/layer-lifecycle.d.ts +160 -0
- package/dist/memory/layer-lifecycle.js +552 -0
- package/dist/memory/layer-usage.d.ts +15 -0
- package/dist/memory/layer-usage.js +48 -0
- package/dist/memory/layers/durable-task-state.d.ts +79 -0
- package/dist/memory/layers/durable-task-state.js +118 -0
- package/dist/memory/layers/file-reference.d.ts +55 -0
- package/dist/memory/layers/file-reference.js +538 -0
- package/dist/memory/layers/history-window.d.ts +29 -0
- package/dist/memory/layers/history-window.js +114 -0
- package/dist/memory/layers/observational-memory.d.ts +53 -0
- package/dist/memory/layers/observational-memory.js +110 -0
- package/dist/memory/layers/plan.d.ts +55 -0
- package/dist/memory/layers/plan.js +369 -0
- package/dist/memory/layers/static-content.d.ts +27 -0
- package/dist/memory/layers/static-content.js +35 -0
- package/dist/memory/layers/steering.d.ts +51 -0
- package/dist/memory/layers/steering.js +258 -0
- package/dist/memory/layers/tool-memory-layer.d.ts +16 -0
- package/dist/memory/layers/tool-memory-layer.js +43 -0
- package/dist/memory/layers/working-memory.d.ts +44 -0
- package/dist/memory/layers/working-memory.js +92 -0
- package/dist/memory/projector.d.ts +10 -0
- package/dist/memory/projector.js +19 -0
- package/dist/memory/scope.d.ts +3 -0
- package/dist/memory/scope.js +31 -0
- package/dist/memory/strip-unresolved.d.ts +13 -0
- package/dist/memory/strip-unresolved.js +35 -0
- package/dist/observability/genai-attributes.d.ts +11 -0
- package/dist/observability/genai-attributes.js +12 -0
- package/dist/observability/span-impl.d.ts +20 -0
- package/dist/observability/span-impl.js +33 -0
- package/dist/observability/trace-exporter.d.ts +15 -0
- package/dist/observability/trace-exporter.js +26 -0
- package/dist/patterns/flow.d.ts +52 -0
- package/dist/patterns/flow.js +103 -0
- package/dist/patterns/interview.d.ts +53 -0
- package/dist/patterns/interview.js +113 -0
- package/dist/patterns/plans.d.ts +45 -0
- package/dist/patterns/plans.js +125 -0
- package/dist/patterns/ralph-wiggum.d.ts +20 -0
- package/dist/patterns/ralph-wiggum.js +37 -0
- package/dist/patterns/react.d.ts +18 -0
- package/dist/patterns/react.js +39 -0
- package/dist/portable.d.ts +26 -0
- package/dist/portable.js +34 -0
- package/dist/runtime/agent-harness.d.ts +206 -0
- package/dist/runtime/agent-harness.js +1170 -0
- package/dist/runtime/broadcaster-utils.d.ts +30 -0
- package/dist/runtime/broadcaster-utils.js +53 -0
- package/dist/runtime/channel-store.d.ts +18 -0
- package/dist/runtime/channel-store.js +187 -0
- package/dist/runtime/context-impl.d.ts +80 -0
- package/dist/runtime/context-impl.js +140 -0
- package/dist/runtime/cwd-helpers.d.ts +42 -0
- package/dist/runtime/cwd-helpers.js +68 -0
- package/dist/runtime/detached-handle.d.ts +14 -0
- package/dist/runtime/detached-handle.js +56 -0
- package/dist/runtime/event-broadcaster.d.ts +61 -0
- package/dist/runtime/event-broadcaster.js +187 -0
- package/dist/runtime/exec-context-factory.d.ts +25 -0
- package/dist/runtime/exec-context-factory.js +30 -0
- package/dist/runtime/in-memory-storage.d.ts +9 -0
- package/dist/runtime/in-memory-storage.js +33 -0
- package/dist/runtime/item-log-impl.d.ts +11 -0
- package/dist/runtime/item-log-impl.js +25 -0
- package/dist/runtime/message-queue.d.ts +30 -0
- package/dist/runtime/message-queue.js +46 -0
- package/dist/runtime/session-runner.d.ts +77 -0
- package/dist/runtime/session-runner.js +253 -0
- package/dist/runtime/session-streams.d.ts +11 -0
- package/dist/runtime/session-streams.js +204 -0
- package/dist/runtime/tool-memory.d.ts +6 -0
- package/dist/runtime/tool-memory.js +22 -0
- package/dist/schemas/item.d.ts +35 -0
- package/dist/schemas/item.js +155 -0
- package/dist/types/channel.d.ts +29 -0
- package/dist/types/channel.js +1 -0
- package/dist/types/common.d.ts +111 -0
- package/dist/types/common.js +1 -0
- package/dist/types/context.d.ts +93 -0
- package/dist/types/context.js +1 -0
- package/dist/types/detached.d.ts +17 -0
- package/dist/types/detached.js +7 -0
- package/dist/types/embed.d.ts +5 -0
- package/dist/types/embed.js +1 -0
- package/dist/types/error.d.ts +51 -0
- package/dist/types/error.js +1 -0
- package/dist/types/fs-adapter.d.ts +51 -0
- package/dist/types/fs-adapter.js +3 -0
- package/dist/types/harness-result.d.ts +43 -0
- package/dist/types/harness-result.js +2 -0
- package/dist/types/items.d.ts +115 -0
- package/dist/types/items.js +2 -0
- package/dist/types/memory.d.ts +336 -0
- package/dist/types/memory.js +13 -0
- package/dist/types/mutable-context.d.ts +13 -0
- package/dist/types/mutable-context.js +1 -0
- package/dist/types/observability.d.ts +30 -0
- package/dist/types/observability.js +1 -0
- package/dist/types/runtime.d.ts +255 -0
- package/dist/types/runtime.js +1 -0
- package/dist/types/shell-adapter.d.ts +34 -0
- package/dist/types/shell-adapter.js +10 -0
- package/dist/types/steering.d.ts +89 -0
- package/dist/types/steering.js +14 -0
- package/dist/types/step.d.ts +206 -0
- package/dist/types/step.js +1 -0
- package/dist/types/subprocess-adapter.d.ts +41 -0
- package/dist/types/subprocess-adapter.js +1 -0
- package/dist/types/tool-context.d.ts +34 -0
- package/dist/types/tool-context.js +1 -0
- package/dist/unstable.d.ts +28 -0
- package/dist/unstable.js +24 -0
- package/dist/until/combinators.d.ts +17 -0
- package/dist/until/combinators.js +49 -0
- package/dist/until/never.d.ts +9 -0
- package/dist/until/never.js +12 -0
- package/dist/until/predicates.d.ts +43 -0
- package/dist/until/predicates.js +126 -0
- 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
|