@ridit/lens 0.3.5 → 0.3.7
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/CLAUDE.md +50 -0
- package/dist/index.mjs +1484 -1058
- package/package.json +2 -2
- package/src/components/chat/ChatOverlays.tsx +12 -13
- package/src/components/chat/ChatRunner.tsx +2 -2
- package/src/components/chat/TextArea.tsx +176 -0
- package/src/components/chat/hooks/useChat.ts +160 -60
- package/src/components/repo/RepoAnalysis.tsx +2 -2
- package/src/components/timeline/TimelineRunner.tsx +2 -2
- package/src/components/watch/RunRunner.tsx +2 -1
- package/src/prompts/fewshot.ts +18 -0
- package/src/prompts/system.ts +157 -22
- package/src/tools/git.ts +26 -0
- package/src/utils/chat.ts +16 -4
- package/src/utils/intentClassifier.ts +58 -0
- package/src/utils/memory.ts +103 -26
- package/src/utils/tools/builtins.ts +46 -5
- package/src/utils/tools/registry.ts +65 -9
package/src/utils/memory.ts
CHANGED
|
@@ -15,16 +15,18 @@ export type MemoryEntry = {
|
|
|
15
15
|
detail: string;
|
|
16
16
|
summary: string;
|
|
17
17
|
timestamp: string;
|
|
18
|
+
repoPath?: string;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type Memory = {
|
|
21
22
|
id: string;
|
|
22
23
|
content: string;
|
|
23
24
|
timestamp: string;
|
|
25
|
+
repoPath?: string;
|
|
26
|
+
scope: "repo" | "global";
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
export type MemoryFile = {
|
|
27
|
-
entries: MemoryEntry[];
|
|
28
30
|
memories: Memory[];
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -32,17 +34,14 @@ const LENS_DIR = path.join(os.homedir(), ".lens");
|
|
|
32
34
|
const MEMORY_PATH = path.join(LENS_DIR, "memory.json");
|
|
33
35
|
|
|
34
36
|
function loadMemoryFile(): MemoryFile {
|
|
35
|
-
if (!existsSync(MEMORY_PATH)) return {
|
|
37
|
+
if (!existsSync(MEMORY_PATH)) return { memories: [] };
|
|
36
38
|
try {
|
|
37
39
|
const data = JSON.parse(
|
|
38
40
|
readFileSync(MEMORY_PATH, "utf-8"),
|
|
39
41
|
) as Partial<MemoryFile>;
|
|
40
|
-
return {
|
|
41
|
-
entries: data.entries ?? [],
|
|
42
|
-
memories: data.memories ?? [],
|
|
43
|
-
};
|
|
42
|
+
return { memories: data.memories ?? [] };
|
|
44
43
|
} catch {
|
|
45
|
-
return {
|
|
44
|
+
return { memories: [] };
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
@@ -51,26 +50,48 @@ function saveMemoryFile(m: MemoryFile): void {
|
|
|
51
50
|
writeFileSync(MEMORY_PATH, JSON.stringify(m, null, 2), "utf-8");
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
// ──
|
|
53
|
+
// ── Session-only action entries (in-memory, never written to disk) ────────────
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
const sessionEntries: MemoryEntry[] = [];
|
|
56
|
+
|
|
57
|
+
export function appendMemory(
|
|
58
|
+
entry: Omit<MemoryEntry, "timestamp">,
|
|
59
|
+
repoPath?: string,
|
|
60
|
+
): void {
|
|
61
|
+
sessionEntries.push({
|
|
62
|
+
...entry,
|
|
63
|
+
repoPath,
|
|
64
|
+
timestamp: new Date().toISOString(),
|
|
65
|
+
});
|
|
66
|
+
if (sessionEntries.length > 200)
|
|
67
|
+
sessionEntries.splice(0, sessionEntries.length - 200);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
export function buildMemorySummary(repoPath: string): string {
|
|
64
71
|
const m = loadMemoryFile();
|
|
65
|
-
const relevant = m.entries.slice(-50);
|
|
66
72
|
|
|
67
|
-
const
|
|
73
|
+
const globalMemories = m.memories.filter((mem) => mem.scope === "global");
|
|
74
|
+
const repoMemories = m.memories.filter(
|
|
75
|
+
(mem) => mem.scope === "repo" && mem.repoPath === repoPath,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const relevant = sessionEntries
|
|
79
|
+
.filter((e) => !e.repoPath || e.repoPath === repoPath)
|
|
80
|
+
.slice(-50);
|
|
68
81
|
|
|
69
82
|
const parts: string[] = [];
|
|
70
83
|
|
|
71
|
-
if (
|
|
84
|
+
if (globalMemories.length > 0) {
|
|
85
|
+
parts.push(
|
|
86
|
+
`## GLOBAL MEMORIES (apply to all repos)\n\n${globalMemories
|
|
87
|
+
.map((mem) => `- [${mem.id}] ${mem.content}`)
|
|
88
|
+
.join("\n")}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (repoMemories.length > 0) {
|
|
72
93
|
parts.push(
|
|
73
|
-
`## MEMORIES ABOUT THIS REPO\n\n${
|
|
94
|
+
`## MEMORIES ABOUT THIS REPO\n\n${repoMemories
|
|
74
95
|
.map((mem) => `- [${mem.id}] ${mem.content}`)
|
|
75
96
|
.join("\n")}`,
|
|
76
97
|
);
|
|
@@ -82,7 +103,7 @@ export function buildMemorySummary(repoPath: string): string {
|
|
|
82
103
|
return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
|
|
83
104
|
});
|
|
84
105
|
parts.push(
|
|
85
|
-
`## WHAT YOU HAVE ALREADY DONE
|
|
106
|
+
`## WHAT YOU HAVE ALREADY DONE THIS SESSION\n\nThe following actions have already been completed. Do NOT repeat them unless the user explicitly asks:\n\n${lines.join("\n")}`,
|
|
86
107
|
);
|
|
87
108
|
}
|
|
88
109
|
|
|
@@ -90,27 +111,42 @@ export function buildMemorySummary(repoPath: string): string {
|
|
|
90
111
|
}
|
|
91
112
|
|
|
92
113
|
export function getRepoMemory(repoPath: string): MemoryEntry[] {
|
|
93
|
-
return
|
|
114
|
+
return sessionEntries.filter((e) => !e.repoPath || e.repoPath === repoPath);
|
|
94
115
|
}
|
|
95
116
|
|
|
96
117
|
export function clearRepoMemory(repoPath: string): void {
|
|
118
|
+
// clear session entries for this repo
|
|
119
|
+
const toRemove = sessionEntries
|
|
120
|
+
.map((e, i) => (e.repoPath === repoPath ? i : -1))
|
|
121
|
+
.filter((i) => i >= 0)
|
|
122
|
+
.reverse();
|
|
123
|
+
for (const i of toRemove) sessionEntries.splice(i, 1);
|
|
124
|
+
|
|
125
|
+
// clear persisted memories for this repo (keep global)
|
|
97
126
|
const m = loadMemoryFile();
|
|
98
|
-
m.
|
|
99
|
-
|
|
127
|
+
m.memories = m.memories.filter(
|
|
128
|
+
(mem) => mem.scope === "global" || mem.repoPath !== repoPath,
|
|
129
|
+
);
|
|
100
130
|
saveMemoryFile(m);
|
|
101
131
|
}
|
|
102
132
|
|
|
103
|
-
// ── User/model memories ───────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
133
|
function generateId(): string {
|
|
106
134
|
return Math.random().toString(36).slice(2, 8);
|
|
107
135
|
}
|
|
108
136
|
|
|
109
137
|
export function addMemory(content: string, repoPath: string): Memory {
|
|
110
138
|
const m = loadMemoryFile();
|
|
139
|
+
|
|
140
|
+
const isGlobal = content.startsWith("[global]");
|
|
141
|
+
const cleanContent = isGlobal
|
|
142
|
+
? content.replace("[global]", "").trim()
|
|
143
|
+
: content;
|
|
144
|
+
|
|
111
145
|
const memory: Memory = {
|
|
112
146
|
id: generateId(),
|
|
113
|
-
content,
|
|
147
|
+
content: cleanContent,
|
|
148
|
+
repoPath: isGlobal ? undefined : repoPath,
|
|
149
|
+
scope: isGlobal ? "global" : "repo",
|
|
114
150
|
timestamp: new Date().toISOString(),
|
|
115
151
|
};
|
|
116
152
|
m.memories.push(memory);
|
|
@@ -121,12 +157,53 @@ export function addMemory(content: string, repoPath: string): Memory {
|
|
|
121
157
|
export function deleteMemory(id: string, repoPath: string): boolean {
|
|
122
158
|
const m = loadMemoryFile();
|
|
123
159
|
const before = m.memories.length;
|
|
124
|
-
m.memories = m.memories.filter((mem) =>
|
|
160
|
+
m.memories = m.memories.filter((mem) => mem.id !== id);
|
|
125
161
|
if (m.memories.length === before) return false;
|
|
126
162
|
saveMemoryFile(m);
|
|
127
163
|
return true;
|
|
128
164
|
}
|
|
129
165
|
|
|
130
166
|
export function listMemories(repoPath: string): Memory[] {
|
|
131
|
-
return loadMemoryFile().memories
|
|
167
|
+
return loadMemoryFile().memories.filter(
|
|
168
|
+
(mem) => mem.scope === "global" || mem.repoPath === repoPath,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
type SessionToolLog = {
|
|
173
|
+
toolName: string;
|
|
174
|
+
input: string;
|
|
175
|
+
resultPreview: string;
|
|
176
|
+
timestamp: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const sessionToolLog: SessionToolLog[] = [];
|
|
180
|
+
|
|
181
|
+
export function logToolCall(
|
|
182
|
+
toolName: string,
|
|
183
|
+
input: string,
|
|
184
|
+
result: string,
|
|
185
|
+
repoPath?: string,
|
|
186
|
+
): void {
|
|
187
|
+
sessionToolLog.push({
|
|
188
|
+
toolName,
|
|
189
|
+
input,
|
|
190
|
+
resultPreview: result.slice(0, 120),
|
|
191
|
+
timestamp: new Date().toISOString(),
|
|
192
|
+
});
|
|
193
|
+
if (sessionToolLog.length > 100)
|
|
194
|
+
sessionToolLog.splice(0, sessionToolLog.length - 100);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function getSessionToolSummary(repoPath: string): string {
|
|
198
|
+
if (sessionToolLog.length === 0) return "";
|
|
199
|
+
const recent = sessionToolLog.slice(-30);
|
|
200
|
+
const lines = recent.map((e) => {
|
|
201
|
+
const input = e.input.length > 60 ? e.input.slice(0, 60) + "…" : e.input;
|
|
202
|
+
return `- ${e.toolName}: ${input}`;
|
|
203
|
+
});
|
|
204
|
+
return `## TOOLS ALREADY USED THIS SESSION\n\nDo NOT call these again unless the user explicitly asks:\n\n${lines.join("\n")}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function clearSessionLog(): void {
|
|
208
|
+
sessionToolLog.splice(0, sessionToolLog.length);
|
|
132
209
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Tool, ToolContext } from "@ridit/lens-sdk";
|
|
2
|
+
import { TOOL_TAGS } from "@ridit/lens-sdk";
|
|
2
3
|
import {
|
|
3
4
|
fetchUrl,
|
|
4
5
|
searchWeb,
|
|
@@ -23,6 +24,7 @@ export const fetchTool: Tool<string> = {
|
|
|
23
24
|
name: "fetch",
|
|
24
25
|
description: "load a URL",
|
|
25
26
|
safe: true,
|
|
27
|
+
tag: TOOL_TAGS.net,
|
|
26
28
|
permissionLabel: "fetch",
|
|
27
29
|
systemPromptEntry: (i) =>
|
|
28
30
|
`### ${i}. fetch — load a URL\n<fetch>https://example.com</fetch>`,
|
|
@@ -45,6 +47,7 @@ export const shellTool: Tool<string> = {
|
|
|
45
47
|
name: "shell",
|
|
46
48
|
description: "run a terminal command",
|
|
47
49
|
safe: false,
|
|
50
|
+
tag: TOOL_TAGS.shell,
|
|
48
51
|
permissionLabel: "run",
|
|
49
52
|
systemPromptEntry: (i) =>
|
|
50
53
|
`### ${i}. shell — run a terminal command\n<shell>node -v</shell>`,
|
|
@@ -60,6 +63,7 @@ export const readFileTool: Tool<string> = {
|
|
|
60
63
|
name: "read-file",
|
|
61
64
|
description: "read a file from the repo",
|
|
62
65
|
safe: true,
|
|
66
|
+
tag: TOOL_TAGS.read,
|
|
63
67
|
permissionLabel: "read",
|
|
64
68
|
systemPromptEntry: (i) =>
|
|
65
69
|
`### ${i}. read-file — read a file from the repo\n<read-file>src/foo.ts</read-file>`,
|
|
@@ -74,6 +78,7 @@ export const readFileTool: Tool<string> = {
|
|
|
74
78
|
export const readFolderTool: Tool<string> = {
|
|
75
79
|
name: "read-folder",
|
|
76
80
|
description: "list contents of a folder (files + subfolders, one level deep)",
|
|
81
|
+
tag: TOOL_TAGS.read,
|
|
77
82
|
safe: true,
|
|
78
83
|
permissionLabel: "folder",
|
|
79
84
|
systemPromptEntry: (i) =>
|
|
@@ -94,6 +99,7 @@ interface GrepInput {
|
|
|
94
99
|
export const grepTool: Tool<GrepInput> = {
|
|
95
100
|
name: "grep",
|
|
96
101
|
description: "search for a pattern across files in the repo",
|
|
102
|
+
tag: TOOL_TAGS.find,
|
|
97
103
|
safe: true,
|
|
98
104
|
permissionLabel: "grep",
|
|
99
105
|
systemPromptEntry: (i) =>
|
|
@@ -124,18 +130,45 @@ interface WriteFileInput {
|
|
|
124
130
|
export const writeFileTool: Tool<WriteFileInput> = {
|
|
125
131
|
name: "write-file",
|
|
126
132
|
description: "create or overwrite a file",
|
|
133
|
+
tag: TOOL_TAGS.write,
|
|
127
134
|
safe: false,
|
|
128
135
|
permissionLabel: "write",
|
|
129
136
|
systemPromptEntry: (i) =>
|
|
130
137
|
`### ${i}. write-file — create or overwrite a file\n<write-file>\n{"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}\n</write-file>`,
|
|
131
138
|
parseInput: (body) => {
|
|
139
|
+
const tryParse = (s: string) => {
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(s) as { path: string; content: string };
|
|
142
|
+
if (!parsed.path || parsed.content === undefined) return null;
|
|
143
|
+
return { ...parsed, path: parsed.path.replace(/\\/g, "/") };
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const first = tryParse(body.trim());
|
|
150
|
+
if (first) return first;
|
|
151
|
+
|
|
132
152
|
try {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
const sanitized = body
|
|
154
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "")
|
|
155
|
+
.replace(/\n/g, "\\n")
|
|
156
|
+
.replace(/\r/g, "\\r")
|
|
157
|
+
.replace(/\t/g, "\\t");
|
|
158
|
+
const second = tryParse(sanitized);
|
|
159
|
+
if (second) return second;
|
|
160
|
+
} catch {}
|
|
161
|
+
|
|
162
|
+
const pathMatch = body.match(/"path"\s*:\s*"([^"]+)"/);
|
|
163
|
+
const contentMatch = body.match(/"content"\s*:\s*"([\s\S]*)"\s*}?\s*$/);
|
|
164
|
+
if (pathMatch && contentMatch && contentMatch[1] !== undefined) {
|
|
165
|
+
return {
|
|
166
|
+
path: pathMatch[1]!.replace(/\\/g, "/"),
|
|
167
|
+
content: contentMatch[1]!.replace(/\\n/g, "\n").replace(/\\t/g, "\t"),
|
|
168
|
+
};
|
|
138
169
|
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
139
172
|
},
|
|
140
173
|
summariseInput: ({ path, content }) => `${path} (${content.length} bytes)`,
|
|
141
174
|
execute: ({ path: filePath, content }, ctx) => ({
|
|
@@ -147,6 +180,7 @@ export const writeFileTool: Tool<WriteFileInput> = {
|
|
|
147
180
|
export const deleteFileTool: Tool<string> = {
|
|
148
181
|
name: "delete-file",
|
|
149
182
|
description: "permanently delete a single file",
|
|
183
|
+
tag: TOOL_TAGS.delete,
|
|
150
184
|
safe: false,
|
|
151
185
|
permissionLabel: "delete",
|
|
152
186
|
systemPromptEntry: (i) =>
|
|
@@ -162,6 +196,7 @@ export const deleteFileTool: Tool<string> = {
|
|
|
162
196
|
export const deleteFolderTool: Tool<string> = {
|
|
163
197
|
name: "delete-folder",
|
|
164
198
|
description: "permanently delete a folder and all its contents",
|
|
199
|
+
tag: TOOL_TAGS.delete,
|
|
165
200
|
safe: false,
|
|
166
201
|
permissionLabel: "delete folder",
|
|
167
202
|
systemPromptEntry: (i) =>
|
|
@@ -177,6 +212,7 @@ export const deleteFolderTool: Tool<string> = {
|
|
|
177
212
|
export const openUrlTool: Tool<string> = {
|
|
178
213
|
name: "open-url",
|
|
179
214
|
description: "open a URL in the user's default browser",
|
|
215
|
+
tag: TOOL_TAGS.net,
|
|
180
216
|
safe: true,
|
|
181
217
|
permissionLabel: "open",
|
|
182
218
|
systemPromptEntry: (i) =>
|
|
@@ -194,6 +230,7 @@ interface GeneratePdfInput {
|
|
|
194
230
|
export const generatePdfTool: Tool<GeneratePdfInput> = {
|
|
195
231
|
name: "generate-pdf",
|
|
196
232
|
description: "generate a PDF file from markdown-style content",
|
|
233
|
+
tag: TOOL_TAGS.write,
|
|
197
234
|
safe: false,
|
|
198
235
|
permissionLabel: "pdf",
|
|
199
236
|
systemPromptEntry: (i) =>
|
|
@@ -222,6 +259,7 @@ export const generatePdfTool: Tool<GeneratePdfInput> = {
|
|
|
222
259
|
|
|
223
260
|
export const searchTool: Tool<string> = {
|
|
224
261
|
name: "search",
|
|
262
|
+
tag: TOOL_TAGS.net,
|
|
225
263
|
description: "search the internet for anything you are unsure about",
|
|
226
264
|
safe: true,
|
|
227
265
|
permissionLabel: "search",
|
|
@@ -245,6 +283,7 @@ export const searchTool: Tool<string> = {
|
|
|
245
283
|
export const cloneTool: Tool<string> = {
|
|
246
284
|
name: "clone",
|
|
247
285
|
description: "clone a GitHub repo so you can explore and discuss it",
|
|
286
|
+
tag: TOOL_TAGS.write,
|
|
248
287
|
safe: false,
|
|
249
288
|
permissionLabel: "clone",
|
|
250
289
|
systemPromptEntry: (i) =>
|
|
@@ -265,6 +304,7 @@ export interface ChangesInput {
|
|
|
265
304
|
export const changesTool: Tool<ChangesInput> = {
|
|
266
305
|
name: "changes",
|
|
267
306
|
description: "propose code edits (shown as a diff for user approval)",
|
|
307
|
+
tag: TOOL_TAGS.write,
|
|
268
308
|
safe: false,
|
|
269
309
|
permissionLabel: "changes",
|
|
270
310
|
systemPromptEntry: (i) =>
|
|
@@ -290,6 +330,7 @@ interface ReadFilesInput {
|
|
|
290
330
|
export const readFilesTool: Tool<ReadFilesInput> = {
|
|
291
331
|
name: "read-files",
|
|
292
332
|
description: "read multiple files from the repo at once",
|
|
333
|
+
tag: TOOL_TAGS.read,
|
|
293
334
|
safe: true,
|
|
294
335
|
permissionLabel: "read",
|
|
295
336
|
systemPromptEntry: (i) =>
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
import type { Tool } from "@ridit/lens-sdk";
|
|
1
|
+
import type { Tool, ToolTag } from "@ridit/lens-sdk";
|
|
2
|
+
import type { Intent } from "../intentClassifier";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Broad capability category for a tool.
|
|
6
|
+
* Used to filter the system prompt based on classified user intent.
|
|
7
|
+
*
|
|
8
|
+
* "read" — safe, purely observational (read-file, read-folder, grep, etc.)
|
|
9
|
+
* "net" — outbound network (fetch, search, clone, open-url)
|
|
10
|
+
* "write" — creates or overwrites file content (write-file, changes, generate-pdf)
|
|
11
|
+
* "delete" — destructive removal (delete-file, delete-folder)
|
|
12
|
+
* "shell" — arbitrary shell execution
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Tools allowed for each intent level */
|
|
16
|
+
const INTENT_ALLOWED: Record<Intent, ToolTag[]> = {
|
|
17
|
+
readonly: ["read", "net"],
|
|
18
|
+
mutating: ["read", "net", "write", "delete", "shell"],
|
|
19
|
+
any: ["read", "net", "write", "delete", "shell"],
|
|
20
|
+
};
|
|
2
21
|
|
|
3
22
|
class ToolRegistry {
|
|
4
23
|
private tools = new Map<string, Tool<unknown>>();
|
|
@@ -27,17 +46,54 @@ class ToolRegistry {
|
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
/**
|
|
30
|
-
*
|
|
49
|
+
* Returns tool names that are allowed for the given intent.
|
|
50
|
+
* Falls back to all names when a tool has no tag (legacy / addons).
|
|
51
|
+
*/
|
|
52
|
+
namesForIntent(intent: Intent): string[] {
|
|
53
|
+
const allowed = new Set(INTENT_ALLOWED[intent]);
|
|
54
|
+
return Array.from(this.tools.values())
|
|
55
|
+
.filter((t) => {
|
|
56
|
+
const tag = (t as any).tag as ToolTag | undefined;
|
|
57
|
+
// No tag = addon / unknown → always allow (conservative)
|
|
58
|
+
if (!tag) return true;
|
|
59
|
+
return allowed.has(tag);
|
|
60
|
+
})
|
|
61
|
+
.map((t) => t.name);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build the TOOLS section of the system prompt from all registered tools,
|
|
66
|
+
* optionally scoped to a specific intent.
|
|
67
|
+
*
|
|
68
|
+
* When intent is "readonly", write/delete/shell tools are omitted entirely
|
|
69
|
+
* so the LLM never sees them and can't hallucinate calls to them.
|
|
31
70
|
*/
|
|
32
|
-
buildSystemPromptSection(): string {
|
|
71
|
+
buildSystemPromptSection(intent: Intent = "any"): string {
|
|
72
|
+
const allowed = new Set(INTENT_ALLOWED[intent]);
|
|
73
|
+
|
|
74
|
+
const visible = Array.from(this.tools.values()).filter((t) => {
|
|
75
|
+
const tag = (t as any).tag as ToolTag | undefined;
|
|
76
|
+
if (!tag) return true; // addon without tag → always show
|
|
77
|
+
return allowed.has(tag);
|
|
78
|
+
});
|
|
79
|
+
|
|
33
80
|
const lines: string[] = ["## TOOLS\n"];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
|
|
82
|
+
if (intent === "readonly") {
|
|
83
|
+
lines.push(
|
|
84
|
+
`You have ${visible.length} tools available for this read-only request. ` +
|
|
85
|
+
`Do NOT attempt to write, delete, or run shell commands — ` +
|
|
86
|
+
`those tools are not available right now.\n`,
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
lines.push(
|
|
90
|
+
`You have exactly ${visible.length} tools. To use a tool you MUST wrap it ` +
|
|
91
|
+
`in the exact XML tags shown below — no other format will work.\n`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
39
95
|
let i = 1;
|
|
40
|
-
for (const tool of
|
|
96
|
+
for (const tool of visible) {
|
|
41
97
|
lines.push(tool.systemPromptEntry(i++));
|
|
42
98
|
}
|
|
43
99
|
return lines.join("\n");
|