openrecall 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -2
- package/src/agent.ts +97 -11
- package/src/dcp/auth.ts +37 -0
- package/src/dcp/commands/context.ts +265 -0
- package/src/dcp/commands/help.ts +73 -0
- package/src/dcp/commands/manual.ts +131 -0
- package/src/dcp/commands/stats.ts +73 -0
- package/src/dcp/commands/sweep.ts +263 -0
- package/src/dcp/config.ts +981 -0
- package/src/dcp/hooks.ts +224 -0
- package/src/dcp/index.ts +123 -0
- package/src/dcp/logger.ts +211 -0
- package/src/dcp/messages/index.ts +2 -0
- package/src/dcp/messages/inject.ts +316 -0
- package/src/dcp/messages/prune.ts +217 -0
- package/src/dcp/messages/utils.ts +269 -0
- package/src/dcp/prompts/_codegen/compress-nudge.generated.ts +15 -0
- package/src/dcp/prompts/_codegen/compress.generated.ts +56 -0
- package/src/dcp/prompts/_codegen/distill.generated.ts +33 -0
- package/src/dcp/prompts/_codegen/nudge.generated.ts +17 -0
- package/src/dcp/prompts/_codegen/prune.generated.ts +23 -0
- package/src/dcp/prompts/_codegen/system.generated.ts +57 -0
- package/src/dcp/prompts/index.ts +59 -0
- package/src/dcp/protected-file-patterns.ts +113 -0
- package/src/dcp/shared-utils.ts +26 -0
- package/src/dcp/state/index.ts +3 -0
- package/src/dcp/state/persistence.ts +196 -0
- package/src/dcp/state/state.ts +143 -0
- package/src/dcp/state/tool-cache.ts +112 -0
- package/src/dcp/state/types.ts +55 -0
- package/src/dcp/state/utils.ts +55 -0
- package/src/dcp/strategies/deduplication.ts +123 -0
- package/src/dcp/strategies/index.ts +4 -0
- package/src/dcp/strategies/purge-errors.ts +84 -0
- package/src/dcp/strategies/supersede-writes.ts +115 -0
- package/src/dcp/strategies/utils.ts +135 -0
- package/src/dcp/tools/compress.ts +218 -0
- package/src/dcp/tools/distill.ts +60 -0
- package/src/dcp/tools/index.ts +4 -0
- package/src/dcp/tools/prune-shared.ts +174 -0
- package/src/dcp/tools/prune.ts +36 -0
- package/src/dcp/tools/types.ts +11 -0
- package/src/dcp/tools/utils.ts +244 -0
- package/src/dcp/ui/notification.ts +273 -0
- package/src/dcp/ui/utils.ts +133 -0
- package/src/index.ts +101 -49
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { ulid } from "ulid"
|
|
2
|
+
import { isMessageCompacted } from "../shared-utils"
|
|
3
|
+
import { Logger } from "../logger"
|
|
4
|
+
import type { SessionState, WithParts } from "../state"
|
|
5
|
+
import type { UserMessage } from "@opencode-ai/sdk/v2"
|
|
6
|
+
|
|
7
|
+
export const COMPRESS_SUMMARY_PREFIX = "[Compressed conversation block]\n\n"
|
|
8
|
+
|
|
9
|
+
const generateUniqueId = (prefix: string): string => `${prefix}_${ulid()}`
|
|
10
|
+
|
|
11
|
+
const isGeminiModel = (modelID: string): boolean => {
|
|
12
|
+
const lowerModelID = modelID.toLowerCase()
|
|
13
|
+
return lowerModelID.includes("gemini")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const createSyntheticUserMessage = (
|
|
17
|
+
baseMessage: WithParts,
|
|
18
|
+
content: string,
|
|
19
|
+
variant?: string,
|
|
20
|
+
): WithParts => {
|
|
21
|
+
const userInfo = baseMessage.info as UserMessage
|
|
22
|
+
const now = Date.now()
|
|
23
|
+
const messageId = generateUniqueId("msg")
|
|
24
|
+
const partId = generateUniqueId("prt")
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
info: {
|
|
28
|
+
id: messageId,
|
|
29
|
+
sessionID: userInfo.sessionID,
|
|
30
|
+
role: "user" as const,
|
|
31
|
+
agent: userInfo.agent,
|
|
32
|
+
model: userInfo.model,
|
|
33
|
+
time: { created: now },
|
|
34
|
+
...(variant !== undefined && { variant }),
|
|
35
|
+
},
|
|
36
|
+
parts: [
|
|
37
|
+
{
|
|
38
|
+
id: partId,
|
|
39
|
+
sessionID: userInfo.sessionID,
|
|
40
|
+
messageID: messageId,
|
|
41
|
+
type: "text" as const,
|
|
42
|
+
text: content,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const createSyntheticTextPart = (baseMessage: WithParts, content: string) => {
|
|
49
|
+
const userInfo = baseMessage.info as UserMessage
|
|
50
|
+
const partId = generateUniqueId("prt")
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
id: partId,
|
|
54
|
+
sessionID: userInfo.sessionID,
|
|
55
|
+
messageID: userInfo.id,
|
|
56
|
+
type: "text" as const,
|
|
57
|
+
text: content,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const createSyntheticToolPart = (
|
|
62
|
+
baseMessage: WithParts,
|
|
63
|
+
content: string,
|
|
64
|
+
modelID: string,
|
|
65
|
+
) => {
|
|
66
|
+
const userInfo = baseMessage.info as UserMessage
|
|
67
|
+
const now = Date.now()
|
|
68
|
+
|
|
69
|
+
const partId = generateUniqueId("prt")
|
|
70
|
+
const callId = generateUniqueId("call")
|
|
71
|
+
|
|
72
|
+
// Gemini requires thoughtSignature bypass to accept synthetic tool parts
|
|
73
|
+
const toolPartMetadata = isGeminiModel(modelID)
|
|
74
|
+
? { google: { thoughtSignature: "skip_thought_signature_validator" } }
|
|
75
|
+
: {}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
id: partId,
|
|
79
|
+
sessionID: userInfo.sessionID,
|
|
80
|
+
messageID: userInfo.id,
|
|
81
|
+
type: "tool" as const,
|
|
82
|
+
callID: callId,
|
|
83
|
+
tool: "context_info",
|
|
84
|
+
state: {
|
|
85
|
+
status: "completed" as const,
|
|
86
|
+
input: {},
|
|
87
|
+
output: content,
|
|
88
|
+
title: "Context Info",
|
|
89
|
+
metadata: toolPartMetadata,
|
|
90
|
+
time: { start: now, end: now },
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extracts a human-readable key from tool metadata for display purposes.
|
|
97
|
+
*/
|
|
98
|
+
export const extractParameterKey = (tool: string, parameters: any): string => {
|
|
99
|
+
if (!parameters) return ""
|
|
100
|
+
|
|
101
|
+
if (tool === "read" && parameters.filePath) {
|
|
102
|
+
const offset = parameters.offset
|
|
103
|
+
const limit = parameters.limit
|
|
104
|
+
if (offset !== undefined && limit !== undefined) {
|
|
105
|
+
return `${parameters.filePath} (lines ${offset}-${offset + limit})`
|
|
106
|
+
}
|
|
107
|
+
if (offset !== undefined) {
|
|
108
|
+
return `${parameters.filePath} (lines ${offset}+)`
|
|
109
|
+
}
|
|
110
|
+
if (limit !== undefined) {
|
|
111
|
+
return `${parameters.filePath} (lines 0-${limit})`
|
|
112
|
+
}
|
|
113
|
+
return parameters.filePath
|
|
114
|
+
}
|
|
115
|
+
if ((tool === "write" || tool === "edit" || tool === "multiedit") && parameters.filePath) {
|
|
116
|
+
return parameters.filePath
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (tool === "apply_patch" && typeof parameters.patchText === "string") {
|
|
120
|
+
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g
|
|
121
|
+
const paths: string[] = []
|
|
122
|
+
let match
|
|
123
|
+
while ((match = pathRegex.exec(parameters.patchText)) !== null) {
|
|
124
|
+
paths.push(match[1]!.trim())
|
|
125
|
+
}
|
|
126
|
+
if (paths.length > 0) {
|
|
127
|
+
const uniquePaths = [...new Set(paths)]
|
|
128
|
+
const count = uniquePaths.length
|
|
129
|
+
const plural = count > 1 ? "s" : ""
|
|
130
|
+
if (count === 1) return uniquePaths[0]!
|
|
131
|
+
if (count === 2) return uniquePaths.join(", ")
|
|
132
|
+
return `${count} file${plural}: ${uniquePaths[0]}, ${uniquePaths[1]}...`
|
|
133
|
+
}
|
|
134
|
+
return "patch"
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (tool === "list") {
|
|
138
|
+
return parameters.path || "(current directory)"
|
|
139
|
+
}
|
|
140
|
+
if (tool === "glob") {
|
|
141
|
+
if (parameters.pattern) {
|
|
142
|
+
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
|
|
143
|
+
return `"${parameters.pattern}"${pathInfo}`
|
|
144
|
+
}
|
|
145
|
+
return "(unknown pattern)"
|
|
146
|
+
}
|
|
147
|
+
if (tool === "grep") {
|
|
148
|
+
if (parameters.pattern) {
|
|
149
|
+
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
|
|
150
|
+
return `"${parameters.pattern}"${pathInfo}`
|
|
151
|
+
}
|
|
152
|
+
return "(unknown pattern)"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (tool === "bash") {
|
|
156
|
+
if (parameters.description) return parameters.description
|
|
157
|
+
if (parameters.command) {
|
|
158
|
+
return parameters.command.length > 50
|
|
159
|
+
? parameters.command.substring(0, 50) + "..."
|
|
160
|
+
: parameters.command
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (tool === "webfetch" && parameters.url) {
|
|
165
|
+
return parameters.url
|
|
166
|
+
}
|
|
167
|
+
if (tool === "websearch" && parameters.query) {
|
|
168
|
+
return `"${parameters.query}"`
|
|
169
|
+
}
|
|
170
|
+
if (tool === "codesearch" && parameters.query) {
|
|
171
|
+
return `"${parameters.query}"`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (tool === "todowrite") {
|
|
175
|
+
return `${parameters.todos?.length || 0} todos`
|
|
176
|
+
}
|
|
177
|
+
if (tool === "todoread") {
|
|
178
|
+
return "read todo list"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (tool === "task" && parameters.description) {
|
|
182
|
+
return parameters.description
|
|
183
|
+
}
|
|
184
|
+
if (tool === "skill" && parameters.name) {
|
|
185
|
+
return parameters.name
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (tool === "lsp") {
|
|
189
|
+
const op = parameters.operation || "lsp"
|
|
190
|
+
const path = parameters.filePath || ""
|
|
191
|
+
const line = parameters.line
|
|
192
|
+
const char = parameters.character
|
|
193
|
+
if (path && line !== undefined && char !== undefined) {
|
|
194
|
+
return `${op} ${path}:${line}:${char}`
|
|
195
|
+
}
|
|
196
|
+
if (path) {
|
|
197
|
+
return `${op} ${path}`
|
|
198
|
+
}
|
|
199
|
+
return op
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (tool === "question") {
|
|
203
|
+
const questions = parameters.questions
|
|
204
|
+
if (Array.isArray(questions) && questions.length > 0) {
|
|
205
|
+
const headers = questions
|
|
206
|
+
.map((q: any) => q.header || "")
|
|
207
|
+
.filter(Boolean)
|
|
208
|
+
.slice(0, 3)
|
|
209
|
+
|
|
210
|
+
const count = questions.length
|
|
211
|
+
const plural = count > 1 ? "s" : ""
|
|
212
|
+
|
|
213
|
+
if (headers.length > 0) {
|
|
214
|
+
const suffix = count > 3 ? ` (+${count - 3} more)` : ""
|
|
215
|
+
return `${count} question${plural}: ${headers.join(", ")}${suffix}`
|
|
216
|
+
}
|
|
217
|
+
return `${count} question${plural}`
|
|
218
|
+
}
|
|
219
|
+
return "question"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const paramStr = JSON.stringify(parameters)
|
|
223
|
+
if (paramStr === "{}" || paramStr === "[]" || paramStr === "null") {
|
|
224
|
+
return ""
|
|
225
|
+
}
|
|
226
|
+
return paramStr.substring(0, 50)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function buildToolIdList(
|
|
230
|
+
state: SessionState,
|
|
231
|
+
messages: WithParts[],
|
|
232
|
+
logger: Logger,
|
|
233
|
+
): string[] {
|
|
234
|
+
const toolIds: string[] = []
|
|
235
|
+
for (const msg of messages) {
|
|
236
|
+
if (isMessageCompacted(state, msg)) {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
const parts = Array.isArray(msg.parts) ? msg.parts : []
|
|
240
|
+
if (parts.length > 0) {
|
|
241
|
+
for (const part of parts) {
|
|
242
|
+
if (part.type === "tool" && part.callID && part.tool) {
|
|
243
|
+
toolIds.push(part.callID)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
state.toolIdList = toolIds
|
|
249
|
+
return toolIds
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const isIgnoredUserMessage = (message: WithParts): boolean => {
|
|
253
|
+
const parts = Array.isArray(message.parts) ? message.parts : []
|
|
254
|
+
if (parts.length === 0) {
|
|
255
|
+
return true
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
for (const part of parts) {
|
|
259
|
+
if (!(part as any).ignored) {
|
|
260
|
+
return false
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return true
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const findMessageIndex = (messages: WithParts[], messageId: string): number => {
|
|
268
|
+
return messages.findIndex((msg) => msg.info.id === messageId)
|
|
269
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// AUTO-GENERATED FILE - DO NOT EDIT
|
|
2
|
+
// Generated from compress-nudge.md by scripts/generate-prompts.ts
|
|
3
|
+
// To modify, edit compress-nudge.md and run `npm run generate:prompts`
|
|
4
|
+
|
|
5
|
+
export const COMPRESS_NUDGE = `<instruction name=context_limit_reached>
|
|
6
|
+
CRITICAL CONTEXT LIMIT
|
|
7
|
+
Your session context has exceeded the configured limit. Strict adherence to context compression is required.
|
|
8
|
+
|
|
9
|
+
PROTOCOL
|
|
10
|
+
You should prioritize context management, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management.
|
|
11
|
+
|
|
12
|
+
IMMEDIATE ACTION REQUIRED
|
|
13
|
+
PHASE COMPLETION: If a phase is complete, use the \`compress\` tool to condense the entire sequence into a detailed summary
|
|
14
|
+
</instruction>
|
|
15
|
+
`
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// AUTO-GENERATED FILE - DO NOT EDIT
|
|
2
|
+
// Generated from compress.md by scripts/generate-prompts.ts
|
|
3
|
+
// To modify, edit compress.md and run `npm run generate:prompts`
|
|
4
|
+
|
|
5
|
+
export const COMPRESS = `Use this tool to collapse a contiguous range of conversation into a preserved summary.
|
|
6
|
+
|
|
7
|
+
THE PHILOSOPHY OF COMPRESS
|
|
8
|
+
\`compress\` transforms verbose conversation sequences into dense, high-fidelity summaries. This is not cleanup - it is crystallization. Your summary becomes the authoritative record of what transpired.
|
|
9
|
+
|
|
10
|
+
Think of compression as phase transitions: raw exploration becomes refined understanding. The original context served its purpose; your summary now carries that understanding forward.
|
|
11
|
+
|
|
12
|
+
THE SUMMARY
|
|
13
|
+
Your summary must be EXHAUSTIVE. Capture file paths, function signatures, decisions made, constraints discovered, key findings... EVERYTHING that maintains context integrity. This is not a brief note - it is an authoritative record so faithful that the original conversation adds no value.
|
|
14
|
+
|
|
15
|
+
Yet be LEAN. Strip away the noise: failed attempts that led nowhere, verbose tool outputs, back-and-forth exploration. What remains should be pure signal - golden nuggets of detail that preserve full understanding with zero ambiguity.
|
|
16
|
+
|
|
17
|
+
THE WAYS OF COMPRESS
|
|
18
|
+
\`compress\` when a chapter closes - when a phase of work is truly complete and the raw conversation has served its purpose:
|
|
19
|
+
|
|
20
|
+
Research concluded and findings are clear
|
|
21
|
+
Implementation finished and verified
|
|
22
|
+
Exploration exhausted and patterns understood
|
|
23
|
+
|
|
24
|
+
Do NOT compress when:
|
|
25
|
+
You may need exact code, error messages, or file contents from the range
|
|
26
|
+
Work in that area is still active or may resume
|
|
27
|
+
You're mid-sprint on related functionality
|
|
28
|
+
|
|
29
|
+
Before compressing, ask: _"Is this chapter closed?"_ Compression is irreversible. The summary replaces everything in the range.
|
|
30
|
+
|
|
31
|
+
BOUNDARY MATCHING
|
|
32
|
+
You specify boundaries by matching unique text strings in the conversation. CRITICAL: In code-centric conversations, strings repeat often. Provide sufficiently unique text to match exactly once. If a match fails (not found or found multiple times), the tool will error - extend your boundary string with more surrounding context in order to make SURE the tool does NOT error.
|
|
33
|
+
|
|
34
|
+
WHERE TO PICK STRINGS FROM (important for reliable matching):
|
|
35
|
+
|
|
36
|
+
- Your own assistant text responses (MOST RELIABLE - always stored verbatim)
|
|
37
|
+
- The user's own words in their messages
|
|
38
|
+
- Tool result output text (distinctive substrings within the output)
|
|
39
|
+
- Previous compress summaries
|
|
40
|
+
- Tool input string values (individual values, not whole serialized objects)
|
|
41
|
+
|
|
42
|
+
WHERE TO NEVER PICK STRINGS FROM:
|
|
43
|
+
|
|
44
|
+
- \`<system-reminder>\` tags or any XML wrapper/meta-commentary around messages
|
|
45
|
+
- Injected system instructions (plan mode text, max-steps warnings, mode-switch text, environment info)
|
|
46
|
+
- File/directory listing framing text (e.g. "Called the Read tool with the following input...")
|
|
47
|
+
- Strings that span across message or part boundaries
|
|
48
|
+
- Entire serialized JSON objects (key ordering may differ - pick a distinctive substring within instead)
|
|
49
|
+
|
|
50
|
+
THE FORMAT OF COMPRESS
|
|
51
|
+
\`topic\`: Short label (3-5 words) for display - e.g., "Auth System Exploration"
|
|
52
|
+
\`content\`: Object containing:
|
|
53
|
+
\`startString\`: Unique text string marking the beginning of the range
|
|
54
|
+
\`endString\`: Unique text string marking the end of the range
|
|
55
|
+
\`summary\`: Complete technical summary replacing all content in the range
|
|
56
|
+
`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// AUTO-GENERATED FILE - DO NOT EDIT
|
|
2
|
+
// Generated from distill.md by scripts/generate-prompts.ts
|
|
3
|
+
// To modify, edit distill.md and run `npm run generate:prompts`
|
|
4
|
+
|
|
5
|
+
export const DISTILL = `Use this tool to distill relevant findings from a selection of raw tool outputs into preserved knowledge, in order to denoise key bits and parts of context.
|
|
6
|
+
|
|
7
|
+
THE PRUNABLE TOOLS LIST
|
|
8
|
+
A <prunable-tools> will show in context when outputs are available for distillation (you don't need to look for it). Each entry follows the format \`ID: tool, parameter (~token usage)\` (e.g., \`20: read, /path/to/file.ts (~1500 tokens)\`). You MUST select outputs by their numeric ID. THESE ARE YOUR ONLY VALID TARGETS.
|
|
9
|
+
|
|
10
|
+
THE PHILOSOPHY OF DISTILLATION
|
|
11
|
+
\`distill\` is your favored instrument for transforming raw tool outputs into preserved knowledge. This is not mere summarization; it is high-fidelity extraction that makes the original output obsolete.
|
|
12
|
+
|
|
13
|
+
Your distillation must be COMPLETE. Capture function signatures, type definitions, business logic, constraints, configuration values... EVERYTHING essential. Think of it as creating a high signal technical substitute so faithful that re-fetching the original would yield no additional value. Be thorough; be comprehensive; leave no ambiguity, ensure that your distillation stands alone, and is designed for easy retrieval and comprehension.
|
|
14
|
+
|
|
15
|
+
AIM FOR IMPACT. Distillation is most powerful when applied to outputs that contain signal buried in noise. A single line requires no distillation; a hundred lines of API documentation do. Make sure the distillation is meaningful.
|
|
16
|
+
|
|
17
|
+
THE WAYS OF DISTILL
|
|
18
|
+
\`distill\` when you have extracted the essence from tool outputs and the raw form has served its purpose.
|
|
19
|
+
Here are some examples:
|
|
20
|
+
EXPLORATION: You've read extensively and grasp the architecture. The original file contents are no longer needed; your understanding, synthesized, is sufficient.
|
|
21
|
+
PRESERVATION: Valuable technical details (signatures, logic, constraints) coexist with noise. Preserve the former; discard the latter.
|
|
22
|
+
|
|
23
|
+
Not everything should be distilled. Prefer keeping raw outputs when:
|
|
24
|
+
PRECISION MATTERS: You will edit the file, grep for exact strings, or need line-accurate references. Distillation sacrifices precision for essence.
|
|
25
|
+
UNCERTAINTY REMAINS: If you might need to re-examine the original, defer. Distillation is irreversible; be certain before you commit.
|
|
26
|
+
|
|
27
|
+
Before distilling, ask yourself: _"Will I need the raw output for upcoming work?"_ If you plan to edit a file you just read, keep it intact. Distillation is for completed exploration, not active work.
|
|
28
|
+
|
|
29
|
+
THE FORMAT OF DISTILL
|
|
30
|
+
\`targets\`: Array of objects, each containing:
|
|
31
|
+
\`id\`: Numeric ID (as string) from the \`<prunable-tools>\` list
|
|
32
|
+
\`distillation\`: Complete technical substitute for that tool output
|
|
33
|
+
`
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// AUTO-GENERATED FILE - DO NOT EDIT
|
|
2
|
+
// Generated from nudge.md by scripts/generate-prompts.ts
|
|
3
|
+
// To modify, edit nudge.md and run `npm run generate:prompts`
|
|
4
|
+
|
|
5
|
+
export const NUDGE = `<instruction name=context_management_required>
|
|
6
|
+
CRITICAL CONTEXT WARNING
|
|
7
|
+
Your context window is filling with tool. Strict adherence to context hygiene is required.
|
|
8
|
+
|
|
9
|
+
PROTOCOL
|
|
10
|
+
You should prioritize context management, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management.
|
|
11
|
+
|
|
12
|
+
IMMEDIATE ACTION REQUIRED
|
|
13
|
+
<distill>KNOWLEDGE PRESERVATION: If holding valuable raw data you POTENTIALLY will need in your task, use the \`distill\` tool. Produce a high-fidelity distillation to preserve insights - be thorough</distill>
|
|
14
|
+
<compress>PHASE COMPLETION: If a phase is complete, use the \`compress\` tool to condense the entire sequence into a detailed summary</compress>
|
|
15
|
+
<prune>NOISE REMOVAL: If you read files or ran commands that yielded no value, use the \`prune\` tool to remove them. If newer tools supersedes older ones, prune the old</prune>
|
|
16
|
+
</instruction>
|
|
17
|
+
`
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// AUTO-GENERATED FILE - DO NOT EDIT
|
|
2
|
+
// Generated from prune.md by scripts/generate-prompts.ts
|
|
3
|
+
// To modify, edit prune.md and run `npm run generate:prompts`
|
|
4
|
+
|
|
5
|
+
export const PRUNE = `Use this tool to remove tool outputs from context entirely. No preservation - pure deletion.
|
|
6
|
+
|
|
7
|
+
THE PRUNABLE TOOLS LIST
|
|
8
|
+
A \`<prunable-tools>\` section surfaces in context showing outputs eligible for removal. Each line reads \`ID: tool, parameter (~token usage)\` (e.g., \`20: read, /path/to/file.ts (~1500 tokens)\`). Reference outputs by their numeric ID - these are your ONLY valid targets for pruning.
|
|
9
|
+
|
|
10
|
+
THE WAYS OF PRUNE
|
|
11
|
+
\`prune\` is surgical deletion - eliminating noise (irrelevant or unhelpful outputs), superseded information (older outputs replaced by newer data), or wrong targets (you accessed something that turned out to be irrelevant). Use it to keep your context lean and focused.
|
|
12
|
+
|
|
13
|
+
BATCH WISELY! Pruning is most effective when consolidated. Don't prune a single tiny output - accumulate several candidates before acting.
|
|
14
|
+
|
|
15
|
+
Do NOT prune when:
|
|
16
|
+
NEEDED LATER: You plan to edit the file or reference this context for implementation.
|
|
17
|
+
UNCERTAINTY: If you might need to re-examine the original, keep it.
|
|
18
|
+
|
|
19
|
+
Before pruning, ask: _"Is this noise, or will it serve me?"_ If the latter, keep it. Pruning that forces re-fetching is a net loss.
|
|
20
|
+
|
|
21
|
+
THE FORMAT OF PRUNE
|
|
22
|
+
\`ids\`: Array of numeric IDs (as strings) from the \`<prunable-tools>\` list
|
|
23
|
+
`
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// AUTO-GENERATED FILE - DO NOT EDIT
|
|
2
|
+
// Generated from system.md by scripts/generate-prompts.ts
|
|
3
|
+
// To modify, edit system.md and run `npm run generate:prompts`
|
|
4
|
+
|
|
5
|
+
export const SYSTEM = `<system-reminder>
|
|
6
|
+
<instruction name=context_management_protocol policy_level=critical>
|
|
7
|
+
You operate a context-constrained environment and MUST PROACTIVELY MANAGE IT TO AVOID CONTEXT ROT. Efficient context management is CRITICAL to maintaining performance and ensuring successful task completion.
|
|
8
|
+
|
|
9
|
+
AVAILABLE TOOLS FOR CONTEXT MANAGEMENT
|
|
10
|
+
<distill>\`distill\`: condense key findings from tool calls into high-fidelity distillation to preserve gained insights. Use to extract valuable knowledge to the user's request. BE THOROUGH, your distillation MUST be high-signal, low noise and complete</distill>
|
|
11
|
+
<compress>\`compress\`: squash contiguous portion of the conversation and replace it with a low level technical summary. Use to filter noise from the conversation and retain purified understanding. Compress conversation phases ORGANICALLY as they get completed, think meso, not micro nor macro. Do not be cheap with that low level technical summary and BE MINDFUL of specifics that must be crystallized to retain UNAMBIGUOUS full picture.</compress>
|
|
12
|
+
<prune>\`prune\`: remove individual tool calls that are noise, irrelevant, or superseded. No preservation of content. DO NOT let irrelevant tool calls accumulate. DO NOT PRUNE TOOL OUTPUTS THAT YOU MAY NEED LATER</prune>
|
|
13
|
+
|
|
14
|
+
<distill>THE DISTILL TOOL
|
|
15
|
+
\`distill\` is the favored way to target specific tools and crystalize their value into high-signal low-noise knowledge nuggets. Your distillation must be comprehensive, capturing technical details (symbols, signatures, logic, constraints) such that the raw output is no longer needed. THINK complete technical substitute. \`distill\` is typically best used when you are certain the raw information is not needed anymore, but the knowledge it contains is valuable to retain so you maintain context authenticity and understanding. Be conservative in your approach to distilling, but do NOT hesitate to distill when appropriate.
|
|
16
|
+
</distill>
|
|
17
|
+
|
|
18
|
+
<compress>THE COMPRESS TOOL
|
|
19
|
+
\`compress\` is a sledgehammer and should be used accordingly. It's purpose is to reduce whole part of the conversation to its essence and technical details in order to leave room for newer context. Your summary MUST be technical and specific enough to preserve FULL understanding of WHAT TRANSPIRED, such that NO AMBIGUITY remains about what was done, found, or decided. Your compress summary must be thorough and precise. \`compress\` will replace everything in the range you match, user and assistant messages, tool inputs and outputs. It is preferred to not compress preemptively, but rather wait for natural breakpoints in the conversation. Those breakpoints are to be infered from user messages. You WILL NOT compress based on thinking that you are done with the task, wait for conversation queues that the user has moved on from current phase.
|
|
20
|
+
|
|
21
|
+
This tool will typically be used at the end of a phase of work, when conversation starts to accumulate noise that would better served summarized, or when you've done significant exploration and can FULLY synthesize your findings and understanding into a technical summary.
|
|
22
|
+
|
|
23
|
+
Make sure to match enough of the context with start and end strings so you're not faced with an error calling the tool. Be VERY CAREFUL AND CONSERVATIVE when using \`compress\`.
|
|
24
|
+
</compress>
|
|
25
|
+
|
|
26
|
+
<prune>THE PRUNE TOOL
|
|
27
|
+
\`prune\` is your last resort for context management. It is a blunt instrument that removes tool outputs entirely, without ANY preservation. It is best used to eliminate noise, irrelevant information, or superseded outputs that no longer add value to the conversation. You MUST NOT prune tool outputs that you may need later. Prune is a targeted nuke, not a general cleanup tool.
|
|
28
|
+
|
|
29
|
+
Contemplate only pruning when you are certain that the tool output is irrelevant to the current task or has been superseded by more recent information. If in doubt, defer for when you are definitive. Evaluate WHAT SHOULD be pruned before jumping the gun.
|
|
30
|
+
</prune>
|
|
31
|
+
|
|
32
|
+
TIMING
|
|
33
|
+
Prefer managing context at the START of a new agentic loop (after receiving a user message) rather than at the END of your previous turn. At turn start, you have fresh signal about what the user needs next - you can better judge what's still relevant versus noise from prior work. Managing at turn end means making retention decisions before knowing what comes next.
|
|
34
|
+
|
|
35
|
+
EVALUATE YOUR CONTEXT AND MANAGE REGULARLY TO AVOID CONTEXT ROT. AVOID USING MANAGEMENT TOOLS AS THE ONLY TOOL CALLS IN YOUR RESPONSE, PARALLELIZE WITH OTHER RELEVANT TOOLS TO TASK CONTINUATION (read, edit, bash...). It is imperative you understand the value or lack thereof of the context you manage and make informed decisions to maintain a decluttered, high-quality and relevant context.
|
|
36
|
+
|
|
37
|
+
The session is your responsibility, and effective context management is CRITICAL to your success. Be PROACTIVE, DELIBERATE, and STRATEGIC in your approach to context management. The session is your oyster - keep it clean, relevant, and high-quality to ensure optimal performance and successful task completion.
|
|
38
|
+
|
|
39
|
+
Be respectful of the user's API usage, manage context methodically as you work through the task and avoid calling ONLY context management tools in your responses.
|
|
40
|
+
</instruction>
|
|
41
|
+
|
|
42
|
+
<manual><instruction name=manual_mode policy_level=critical>
|
|
43
|
+
Manual mode is enabled. Do NOT use distill, compress, or prune unless the user has explicitly triggered it through a manual marker.
|
|
44
|
+
|
|
45
|
+
<prune>Only use the prune tool after seeing \`<prune triggered manually>\` in the current user instruction context.</prune>
|
|
46
|
+
<distill>Only use the distill tool after seeing \`<distill triggered manually>\` in the current user instruction context.</distill>
|
|
47
|
+
<compress>Only use the compress tool after seeing \`<compress triggered manually>\` in the current user instruction context.</compress>
|
|
48
|
+
|
|
49
|
+
After completing a manually triggered context-management action, STOP IMMEDIATELY. Do NOT continue with any task execution. End your response right after the tool use completes and wait for the next user input.
|
|
50
|
+
</instruction></manual>
|
|
51
|
+
|
|
52
|
+
<instruction name=injected_context_handling policy_level=critical>
|
|
53
|
+
This chat environment injects context information on your behalf in the form of a <prunable-tools> list to help you manage context effectively. Carefully read the list and use it to inform your management decisions. The list is automatically updated after each turn to reflect the current state of manageable tools and context usage. If no list is present, do NOT attempt to prune anything.
|
|
54
|
+
There may be tools in session context that do not appear in the <prunable-tools> list, this is expected, remember that you can ONLY prune what you see in list.
|
|
55
|
+
</instruction>
|
|
56
|
+
</system-reminder>
|
|
57
|
+
`
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Generated prompts (from .md files via scripts/generate-prompts.ts)
|
|
2
|
+
import { SYSTEM as SYSTEM_PROMPT } from "./_codegen/system.generated"
|
|
3
|
+
import { NUDGE } from "./_codegen/nudge.generated"
|
|
4
|
+
import { COMPRESS_NUDGE } from "./_codegen/compress-nudge.generated"
|
|
5
|
+
import { PRUNE as PRUNE_TOOL_SPEC } from "./_codegen/prune.generated"
|
|
6
|
+
import { DISTILL as DISTILL_TOOL_SPEC } from "./_codegen/distill.generated"
|
|
7
|
+
import { COMPRESS as COMPRESS_TOOL_SPEC } from "./_codegen/compress.generated"
|
|
8
|
+
|
|
9
|
+
export interface ToolFlags {
|
|
10
|
+
distill: boolean
|
|
11
|
+
compress: boolean
|
|
12
|
+
prune: boolean
|
|
13
|
+
manual: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function processConditionals(template: string, flags: ToolFlags): string {
|
|
17
|
+
const tools = ["distill", "compress", "prune", "manual"] as const
|
|
18
|
+
let result = template
|
|
19
|
+
// Strip comments: // ... //
|
|
20
|
+
result = result.replace(/\/\/.*?\/\//g, "")
|
|
21
|
+
// Process tool conditionals
|
|
22
|
+
for (const tool of tools) {
|
|
23
|
+
const regex = new RegExp(`<${tool}>([\\s\\S]*?)</${tool}>`, "g")
|
|
24
|
+
result = result.replace(regex, (_, content) => (flags[tool] ? content : ""))
|
|
25
|
+
}
|
|
26
|
+
// Collapse multiple blank/whitespace-only lines to single blank line
|
|
27
|
+
return result.replace(/\n([ \t]*\n)+/g, "\n\n").trim()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function renderSystemPrompt(flags: ToolFlags): string {
|
|
31
|
+
return processConditionals(SYSTEM_PROMPT, flags)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function renderNudge(flags: ToolFlags): string {
|
|
35
|
+
return processConditionals(NUDGE, flags)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderCompressNudge(): string {
|
|
39
|
+
return COMPRESS_NUDGE
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const PROMPTS: Record<string, string> = {
|
|
43
|
+
"prune-tool-spec": PRUNE_TOOL_SPEC,
|
|
44
|
+
"distill-tool-spec": DISTILL_TOOL_SPEC,
|
|
45
|
+
"compress-tool-spec": COMPRESS_TOOL_SPEC,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadPrompt(name: string, vars?: Record<string, string>): string {
|
|
49
|
+
let content = PROMPTS[name]
|
|
50
|
+
if (!content) {
|
|
51
|
+
throw new Error(`Prompt not found: ${name}`)
|
|
52
|
+
}
|
|
53
|
+
if (vars) {
|
|
54
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
55
|
+
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return content
|
|
59
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
function normalizePath(input: string): string {
|
|
2
|
+
return input.replaceAll("\\\\", "/")
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function escapeRegExpChar(ch: string): string {
|
|
6
|
+
return /[\\.^$+{}()|\[\]]/.test(ch) ? `\\${ch}` : ch
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Basic glob matching with support for `**`, `*`, and `?`.
|
|
11
|
+
*
|
|
12
|
+
* Notes:
|
|
13
|
+
* - Matching is performed against the full (normalized) string.
|
|
14
|
+
* - `*` and `?` do not match `/`.
|
|
15
|
+
* - `**` matches across `/`.
|
|
16
|
+
*/
|
|
17
|
+
export function matchesGlob(inputPath: string, pattern: string): boolean {
|
|
18
|
+
if (!pattern) return false
|
|
19
|
+
|
|
20
|
+
const input = normalizePath(inputPath)
|
|
21
|
+
const pat = normalizePath(pattern)
|
|
22
|
+
|
|
23
|
+
let regex = "^"
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < pat.length; i++) {
|
|
26
|
+
const ch = pat[i]!
|
|
27
|
+
|
|
28
|
+
if (ch === "*") {
|
|
29
|
+
const next = pat[i + 1]
|
|
30
|
+
if (next === "*") {
|
|
31
|
+
const after = pat[i + 2]
|
|
32
|
+
if (after === "/") {
|
|
33
|
+
// **/ (zero or more directories)
|
|
34
|
+
regex += "(?:.*/)?"
|
|
35
|
+
i += 2
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// **
|
|
40
|
+
regex += ".*"
|
|
41
|
+
i++
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// *
|
|
46
|
+
regex += "[^/]*"
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (ch === "?") {
|
|
51
|
+
regex += "[^/]"
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (ch === "/") {
|
|
56
|
+
regex += "/"
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
regex += escapeRegExpChar(ch)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
regex += "$"
|
|
64
|
+
|
|
65
|
+
return new RegExp(regex).test(input)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function getFilePathsFromParameters(tool: string, parameters: unknown): string[] {
|
|
69
|
+
if (typeof parameters !== "object" || parameters === null) {
|
|
70
|
+
return []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const paths: string[] = []
|
|
74
|
+
const params = parameters as Record<string, any>
|
|
75
|
+
|
|
76
|
+
// 1. apply_patch uses patchText with embedded paths
|
|
77
|
+
if (tool === "apply_patch" && typeof params.patchText === "string") {
|
|
78
|
+
const pathRegex = /\*\*\* (?:Add|Delete|Update) File: ([^\n\r]+)/g
|
|
79
|
+
let match
|
|
80
|
+
while ((match = pathRegex.exec(params.patchText)) !== null) {
|
|
81
|
+
paths.push(match[1]!.trim())
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 2. multiedit uses top-level filePath and nested edits array
|
|
86
|
+
if (tool === "multiedit") {
|
|
87
|
+
if (typeof params.filePath === "string") {
|
|
88
|
+
paths.push(params.filePath)
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(params.edits)) {
|
|
91
|
+
for (const edit of params.edits) {
|
|
92
|
+
if (edit && typeof edit.filePath === "string") {
|
|
93
|
+
paths.push(edit.filePath)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 3. Default check for common filePath parameter (read, write, edit, etc)
|
|
100
|
+
if (typeof params.filePath === "string") {
|
|
101
|
+
paths.push(params.filePath)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Return unique non-empty paths
|
|
105
|
+
return [...new Set(paths)].filter((p) => p.length > 0)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function isProtected(filePaths: string[], patterns: string[]): boolean {
|
|
109
|
+
if (!filePaths || filePaths.length === 0) return false
|
|
110
|
+
if (!patterns || patterns.length === 0) return false
|
|
111
|
+
|
|
112
|
+
return filePaths.some((path) => patterns.some((pattern) => matchesGlob(path, pattern)))
|
|
113
|
+
}
|