@blockrun/franklin 3.3.3 → 3.5.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/README.md +65 -25
- package/dist/agent/commands.d.ts +1 -1
- package/dist/agent/commands.js +128 -17
- package/dist/agent/compact.d.ts +2 -2
- package/dist/agent/compact.js +148 -22
- package/dist/agent/context.d.ts +8 -3
- package/dist/agent/context.js +301 -108
- package/dist/agent/error-classifier.d.ts +11 -2
- package/dist/agent/error-classifier.js +64 -10
- package/dist/agent/llm.d.ts +8 -1
- package/dist/agent/llm.js +114 -19
- package/dist/agent/loop.d.ts +1 -2
- package/dist/agent/loop.js +509 -61
- package/dist/agent/optimize.d.ts +2 -2
- package/dist/agent/optimize.js +9 -7
- package/dist/agent/permissions.d.ts +1 -1
- package/dist/agent/permissions.js +1 -1
- package/dist/agent/planner.d.ts +42 -0
- package/dist/agent/planner.js +110 -0
- package/dist/agent/reduce.d.ts +7 -1
- package/dist/agent/reduce.js +85 -3
- package/dist/agent/streaming-executor.d.ts +6 -1
- package/dist/agent/streaming-executor.js +83 -5
- package/dist/agent/tokens.d.ts +11 -2
- package/dist/agent/tokens.js +38 -5
- package/dist/agent/tool-guard.d.ts +27 -0
- package/dist/agent/tool-guard.js +324 -0
- package/dist/agent/types.d.ts +7 -1
- package/dist/agent/types.js +1 -1
- package/dist/brain/extract.d.ts +11 -0
- package/dist/brain/extract.js +154 -0
- package/dist/brain/index.d.ts +3 -0
- package/dist/brain/index.js +2 -0
- package/dist/brain/store.d.ts +42 -0
- package/dist/brain/store.js +225 -0
- package/dist/brain/types.d.ts +45 -0
- package/dist/brain/types.js +5 -0
- package/dist/commands/daemon.js +2 -1
- package/dist/commands/start.js +19 -7
- package/dist/config.js +1 -1
- package/dist/index.js +27 -2
- package/dist/learnings/extractor.d.ts +13 -0
- package/dist/learnings/extractor.js +69 -8
- package/dist/learnings/index.d.ts +1 -1
- package/dist/learnings/index.js +1 -1
- package/dist/learnings/store.js +42 -13
- package/dist/learnings/types.d.ts +1 -1
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/client.js +5 -5
- package/dist/mcp/config.d.ts +1 -1
- package/dist/mcp/config.js +1 -1
- package/dist/panel/html.d.ts +2 -0
- package/dist/panel/html.js +409 -146
- package/dist/panel/server.js +19 -0
- package/dist/pricing.js +3 -2
- package/dist/proxy/fallback.d.ts +3 -1
- package/dist/proxy/fallback.js +4 -4
- package/dist/proxy/server.js +29 -11
- package/dist/proxy/sse-translator.js +1 -1
- package/dist/router/categories.d.ts +21 -0
- package/dist/router/categories.js +96 -0
- package/dist/router/index.d.ts +9 -2
- package/dist/router/index.js +106 -27
- package/dist/router/local-elo.d.ts +32 -0
- package/dist/router/local-elo.js +107 -0
- package/dist/router/selector.d.ts +46 -0
- package/dist/router/selector.js +106 -0
- package/dist/session/storage.d.ts +5 -1
- package/dist/session/storage.js +24 -2
- package/dist/social/a11y.d.ts +1 -1
- package/dist/social/a11y.js +5 -1
- package/dist/social/browser.d.ts +5 -0
- package/dist/social/browser.js +22 -0
- package/dist/social/preflight.d.ts +4 -0
- package/dist/social/preflight.js +42 -3
- package/dist/stats/failures.d.ts +20 -0
- package/dist/stats/failures.js +63 -0
- package/dist/stats/format.d.ts +6 -0
- package/dist/stats/format.js +23 -0
- package/dist/stats/insights.js +1 -21
- package/dist/stats/session-tracker.d.ts +21 -0
- package/dist/stats/session-tracker.js +28 -0
- package/dist/stats/tracker.d.ts +1 -1
- package/dist/stats/tracker.js +1 -1
- package/dist/tools/bash.d.ts +14 -1
- package/dist/tools/bash.js +132 -7
- package/dist/tools/edit.js +77 -14
- package/dist/tools/glob.js +13 -3
- package/dist/tools/grep.js +30 -12
- package/dist/tools/imagegen.js +5 -5
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/read.d.ts +16 -2
- package/dist/tools/read.js +36 -8
- package/dist/tools/searchx.d.ts +6 -2
- package/dist/tools/searchx.js +221 -44
- package/dist/tools/subagent.js +37 -3
- package/dist/tools/task.js +43 -7
- package/dist/tools/validate.d.ts +11 -0
- package/dist/tools/validate.js +42 -0
- package/dist/tools/webfetch.js +18 -7
- package/dist/tools/websearch.js +41 -7
- package/dist/tools/write.js +26 -6
- package/dist/ui/app.js +31 -6
- package/dist/ui/model-picker.d.ts +1 -1
- package/dist/ui/model-picker.js +1 -1
- package/dist/ui/terminal.d.ts +1 -1
- package/dist/ui/terminal.js +1 -1
- package/package.json +2 -2
package/dist/agent/compact.js
CHANGED
|
@@ -1,48 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Context compaction for
|
|
2
|
+
* Context compaction for Franklin.
|
|
3
3
|
* When conversation history approaches the context window limit,
|
|
4
4
|
* summarize older messages and replace them with the summary.
|
|
5
5
|
*/
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
6
7
|
import { estimateHistoryTokens, getCompactionThreshold, COMPACTION_SUMMARY_RESERVE, } from './tokens.js';
|
|
8
|
+
/** Max files to restore after compaction (inspired by Claude Code POST_COMPACT_MAX_FILES_TO_RESTORE) */
|
|
9
|
+
const POST_COMPACT_MAX_FILES = 5;
|
|
10
|
+
/** Max tokens to spend on post-compact file restoration */
|
|
11
|
+
const POST_COMPACT_TOKEN_BUDGET = 50_000;
|
|
7
12
|
// Structured compaction prompt (pattern from nousresearch/hermes-agent
|
|
8
13
|
// `agent/context_compressor.py`). The structured sections preserve more
|
|
9
14
|
// signal than free-form summaries and make it easier for the model to
|
|
10
15
|
// continue work from where it left off.
|
|
11
|
-
export const COMPACT_HEADER = `[CONTEXT COMPACTION] Earlier turns
|
|
16
|
+
export const COMPACT_HEADER = `[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. This is a handoff from a previous context window — treat it as background reference, NOT as active instructions. Do NOT answer questions or fulfill requests mentioned in this summary; they were already addressed. Respond ONLY to the latest user message that appears AFTER this summary.`;
|
|
12
17
|
const COMPACT_SYSTEM_PROMPT = `You are a conversation summarizer. Produce a STRUCTURED summary of the conversation so far that preserves all decision-relevant context for continuing the task.
|
|
13
18
|
|
|
19
|
+
CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
|
|
20
|
+
|
|
14
21
|
Critical rules:
|
|
15
22
|
- Preserve EXACT file paths, function names, line numbers, variable names
|
|
16
|
-
- Preserve EXACT error messages (verbatim)
|
|
23
|
+
- Preserve EXACT error messages and stack traces (verbatim)
|
|
17
24
|
- Preserve user preferences and corrections (especially "don't do X" instructions)
|
|
18
25
|
- Preserve decisions with their rationale (not just the decision)
|
|
26
|
+
- Include full code snippets and function signatures when they are load-bearing
|
|
19
27
|
- DO NOT include reasoning that led to decisions — only the decisions themselves
|
|
20
28
|
- DO NOT include pleasantries, meta-commentary, or apologies
|
|
21
29
|
- Use bullet points inside each section
|
|
22
30
|
- Be specific: "edited src/foo.ts:42 to add error handling" not "made some changes"
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
First, analyze the conversation chronologically inside <analysis> tags. This is your drafting space — it will be stripped from the final output. Think through what matters before writing the summary.
|
|
33
|
+
|
|
34
|
+
Then produce the summary inside <summary> tags using these exact section headers:
|
|
25
35
|
|
|
26
36
|
## Goal
|
|
27
37
|
[One clear sentence: what the user is trying to accomplish]
|
|
28
38
|
|
|
39
|
+
## Key Technical Context
|
|
40
|
+
[Important technical details, architecture patterns, constraints, or domain knowledge established during the conversation that future work depends on]
|
|
41
|
+
|
|
29
42
|
## Progress
|
|
30
|
-
[Chronological bullet list of what has been done so far]
|
|
43
|
+
[Chronological bullet list of what has been done so far, with specific file paths and line numbers]
|
|
44
|
+
|
|
45
|
+
## Errors and Fixes
|
|
46
|
+
[Any errors encountered, their root causes, and how they were resolved — this prevents re-investigating the same issues]
|
|
31
47
|
|
|
32
48
|
## Decisions
|
|
33
49
|
[Key decisions made, each with its rationale]
|
|
34
50
|
|
|
35
51
|
## Files Modified
|
|
36
|
-
[Each file touched, with a one-line description of what changed]
|
|
52
|
+
[Each file touched, with a one-line description of what changed and why]
|
|
37
53
|
|
|
38
54
|
## Tool Results Still Relevant
|
|
39
|
-
[Any tool output (file reads, grep matches, bash output) that later steps still depend on — include the actual content, not a reference]
|
|
55
|
+
[Any tool output (file reads, grep matches, bash output) that later steps still depend on — include the actual content, not just a reference to it]
|
|
40
56
|
|
|
41
|
-
## User
|
|
42
|
-
[
|
|
57
|
+
## User Messages and Feedback
|
|
58
|
+
[Chronological summary of what the user said, asked for, and corrected — these are load-bearing and must not be lost]
|
|
43
59
|
|
|
44
60
|
## Next Steps
|
|
45
|
-
[What comes next, in priority order]
|
|
61
|
+
[What comes next, in priority order, with enough detail to continue without re-reading the original conversation]
|
|
46
62
|
|
|
47
63
|
If there's an existing [CONTEXT COMPACTION] summary in the messages being compacted, MERGE its content into your output rather than nesting. Do not produce a summary of a summary.`;
|
|
48
64
|
/**
|
|
@@ -56,7 +72,7 @@ export async function autoCompactIfNeeded(history, model, client, debug) {
|
|
|
56
72
|
return { history, compacted: false };
|
|
57
73
|
}
|
|
58
74
|
if (debug) {
|
|
59
|
-
console.error(`[
|
|
75
|
+
console.error(`[franklin] Auto-compacting: ~${currentTokens} tokens, threshold=${threshold}`);
|
|
60
76
|
}
|
|
61
77
|
const beforeTokens = estimateHistoryTokens(history);
|
|
62
78
|
try {
|
|
@@ -64,7 +80,7 @@ export async function autoCompactIfNeeded(history, model, client, debug) {
|
|
|
64
80
|
const afterTokens = estimateHistoryTokens(compacted);
|
|
65
81
|
if (afterTokens >= beforeTokens) {
|
|
66
82
|
if (debug) {
|
|
67
|
-
console.error(`[
|
|
83
|
+
console.error(`[franklin] Auto-compaction grew history (${beforeTokens} → ${afterTokens}) — skipping`);
|
|
68
84
|
}
|
|
69
85
|
return { history, compacted: false };
|
|
70
86
|
}
|
|
@@ -72,7 +88,7 @@ export async function autoCompactIfNeeded(history, model, client, debug) {
|
|
|
72
88
|
}
|
|
73
89
|
catch (err) {
|
|
74
90
|
if (debug) {
|
|
75
|
-
console.error(`[
|
|
91
|
+
console.error(`[franklin] Compaction failed: ${err.message}`);
|
|
76
92
|
}
|
|
77
93
|
// Fallback: truncate oldest messages instead of crashing
|
|
78
94
|
const truncated = emergencyTruncate(history, threshold);
|
|
@@ -93,7 +109,7 @@ export async function forceCompact(history, model, client, debug) {
|
|
|
93
109
|
// Only accept compaction if it actually reduces tokens
|
|
94
110
|
if (afterTokens >= beforeTokens) {
|
|
95
111
|
if (debug) {
|
|
96
|
-
console.error(`[
|
|
112
|
+
console.error(`[franklin] Compaction produced larger history (${beforeTokens} → ${afterTokens}) — reverting`);
|
|
97
113
|
}
|
|
98
114
|
return { history, compacted: false };
|
|
99
115
|
}
|
|
@@ -101,7 +117,7 @@ export async function forceCompact(history, model, client, debug) {
|
|
|
101
117
|
}
|
|
102
118
|
catch (err) {
|
|
103
119
|
if (debug) {
|
|
104
|
-
console.error(`[
|
|
120
|
+
console.error(`[franklin] Force compaction failed: ${err.message}`);
|
|
105
121
|
}
|
|
106
122
|
const threshold = getCompactionThreshold(model);
|
|
107
123
|
const truncated = emergencyTruncate(history, threshold);
|
|
@@ -124,7 +140,7 @@ async function compactHistory(history, model, client, debug) {
|
|
|
124
140
|
return history;
|
|
125
141
|
}
|
|
126
142
|
if (debug) {
|
|
127
|
-
console.error(`[
|
|
143
|
+
console.error(`[franklin] Summarizing ${toSummarize.length} messages, keeping ${toKeep.length}`);
|
|
128
144
|
}
|
|
129
145
|
// Build summary request
|
|
130
146
|
const summaryMessages = [
|
|
@@ -140,16 +156,17 @@ async function compactHistory(history, model, client, debug) {
|
|
|
140
156
|
max_tokens: COMPACTION_SUMMARY_RESERVE,
|
|
141
157
|
stream: true,
|
|
142
158
|
});
|
|
143
|
-
// Extract summary text
|
|
144
|
-
let
|
|
159
|
+
// Extract summary text and strip analysis scratchpad
|
|
160
|
+
let rawSummary = '';
|
|
145
161
|
for (const part of summaryParts) {
|
|
146
162
|
if (part.type === 'text') {
|
|
147
|
-
|
|
163
|
+
rawSummary += part.text;
|
|
148
164
|
}
|
|
149
165
|
}
|
|
150
|
-
if (!
|
|
166
|
+
if (!rawSummary) {
|
|
151
167
|
throw new Error('Empty summary returned from model');
|
|
152
168
|
}
|
|
169
|
+
const summaryText = formatCompactSummary(rawSummary);
|
|
153
170
|
// Build compacted history: summary as first message, then kept messages.
|
|
154
171
|
// The COMPACT_HEADER prefix lets future compactions detect and merge rather
|
|
155
172
|
// than nest summaries.
|
|
@@ -162,14 +179,107 @@ async function compactHistory(history, model, client, debug) {
|
|
|
162
179
|
role: 'assistant',
|
|
163
180
|
content: 'Got it. I have the structured context from earlier work and will continue from where things left off.',
|
|
164
181
|
},
|
|
165
|
-
...toKeep,
|
|
166
182
|
];
|
|
183
|
+
// Post-compact file restoration (inspired by Claude Code)
|
|
184
|
+
// Re-read recently modified files to restore working context that was lost
|
|
185
|
+
// during compaction. This prevents the agent from needing to re-read files
|
|
186
|
+
// it was actively working on.
|
|
187
|
+
const restoredFiles = restoreRecentFiles(summaryText, toSummarize, debug);
|
|
188
|
+
if (restoredFiles) {
|
|
189
|
+
compacted.push({ role: 'user', content: restoredFiles.prompt }, { role: 'assistant', content: 'I have the restored file contents and will use them as context for continuing work.' });
|
|
190
|
+
}
|
|
191
|
+
compacted.push(...toKeep);
|
|
167
192
|
if (debug) {
|
|
168
193
|
const newTokens = estimateHistoryTokens(compacted);
|
|
169
|
-
console.error(`[
|
|
194
|
+
console.error(`[franklin] Compacted: ${estimateHistoryTokens(history)} → ${newTokens} tokens`);
|
|
170
195
|
}
|
|
171
196
|
return compacted;
|
|
172
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Restore recently modified files after compaction.
|
|
200
|
+
* Extracts file paths from the compaction summary and the original messages,
|
|
201
|
+
* reads the ones that still exist, and builds a context restoration prompt.
|
|
202
|
+
*
|
|
203
|
+
* Inspired by Claude Code's POST_COMPACT_MAX_FILES_TO_RESTORE mechanism.
|
|
204
|
+
*/
|
|
205
|
+
function restoreRecentFiles(summaryText, compactedMessages, debug) {
|
|
206
|
+
// Extract file paths from multiple sources:
|
|
207
|
+
// 1. "Files Modified" section in the summary
|
|
208
|
+
// 2. Edit/Write/Read tool calls in the compacted messages
|
|
209
|
+
const filePaths = new Set();
|
|
210
|
+
// Source 1: Parse "## Files Modified" section from summary
|
|
211
|
+
const filesSection = summaryText.match(/## Files Modified\n([\s\S]*?)(?=\n## |$)/);
|
|
212
|
+
if (filesSection) {
|
|
213
|
+
const pathRegex = /[`"]?([/\w.-]+\.\w{1,10})[`"]?/g;
|
|
214
|
+
let match;
|
|
215
|
+
while ((match = pathRegex.exec(filesSection[1])) !== null) {
|
|
216
|
+
const p = match[1];
|
|
217
|
+
// Filter: must look like a real file path (has directory separator or extension)
|
|
218
|
+
if (p.includes('/') || p.includes('.')) {
|
|
219
|
+
filePaths.add(p);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Source 2: Extract from Edit/Write tool_use inputs in compacted messages
|
|
224
|
+
for (const msg of compactedMessages) {
|
|
225
|
+
if (msg.role !== 'assistant' || !Array.isArray(msg.content))
|
|
226
|
+
continue;
|
|
227
|
+
for (const part of msg.content) {
|
|
228
|
+
if (part.type === 'tool_use' && (part.name === 'Edit' || part.name === 'Write')) {
|
|
229
|
+
const fp = part.input?.file_path;
|
|
230
|
+
if (typeof fp === 'string' && fp.startsWith('/')) {
|
|
231
|
+
filePaths.add(fp);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (filePaths.size === 0)
|
|
237
|
+
return null;
|
|
238
|
+
// Prioritize: most recently modified files first, limit to POST_COMPACT_MAX_FILES
|
|
239
|
+
const candidates = [...filePaths].filter(p => {
|
|
240
|
+
try {
|
|
241
|
+
return existsSync(p);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
if (candidates.length === 0)
|
|
248
|
+
return null;
|
|
249
|
+
// Read files within token budget
|
|
250
|
+
const restoredParts = [];
|
|
251
|
+
let tokenBudget = POST_COMPACT_TOKEN_BUDGET;
|
|
252
|
+
const filesToRestore = candidates.slice(0, POST_COMPACT_MAX_FILES);
|
|
253
|
+
for (const fp of filesToRestore) {
|
|
254
|
+
try {
|
|
255
|
+
const content = readFileSync(fp, 'utf-8');
|
|
256
|
+
const estimatedTokens = Math.ceil(content.length / 4 * 1.33);
|
|
257
|
+
if (estimatedTokens > tokenBudget) {
|
|
258
|
+
// File too large for remaining budget — take first chunk
|
|
259
|
+
const maxChars = Math.floor(tokenBudget * 3); // ~3 chars per token
|
|
260
|
+
if (maxChars > 500) {
|
|
261
|
+
const truncated = content.slice(0, maxChars);
|
|
262
|
+
restoredParts.push(`### ${fp}\n\`\`\`\n${truncated}\n... (truncated)\n\`\`\``);
|
|
263
|
+
tokenBudget = 0;
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
restoredParts.push(`### ${fp}\n\`\`\`\n${content}\n\`\`\``);
|
|
268
|
+
tokenBudget -= estimatedTokens;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// File unreadable — skip
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (restoredParts.length === 0)
|
|
275
|
+
return null;
|
|
276
|
+
if (debug) {
|
|
277
|
+
console.error(`[franklin] Post-compact: restored ${restoredParts.length} files`);
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
prompt: `[POST-COMPACT FILE RESTORATION] The following files were being actively worked on before context compaction. Their current contents are provided to restore working context:\n\n${restoredParts.join('\n\n')}`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
173
283
|
/**
|
|
174
284
|
* Find how many recent messages to keep (don't summarize).
|
|
175
285
|
* Keeps the most recent tool exchange + the last few user/assistant turns.
|
|
@@ -239,6 +349,22 @@ function formatForSummarization(messages) {
|
|
|
239
349
|
}
|
|
240
350
|
return parts.join('\n\n');
|
|
241
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Strip the analysis scratchpad from compaction output and extract the summary.
|
|
354
|
+
* The model drafts in <analysis> tags (for quality), then writes the final
|
|
355
|
+
* summary in <summary> tags. We keep only the summary.
|
|
356
|
+
*/
|
|
357
|
+
function formatCompactSummary(raw) {
|
|
358
|
+
// Strip <analysis>...</analysis> (the drafting scratchpad)
|
|
359
|
+
let cleaned = raw.replace(/<analysis>[\s\S]*?<\/analysis>/gi, '').trim();
|
|
360
|
+
// Extract content from <summary>...</summary> if present
|
|
361
|
+
const summaryMatch = cleaned.match(/<summary>([\s\S]*?)<\/summary>/i);
|
|
362
|
+
if (summaryMatch) {
|
|
363
|
+
cleaned = summaryMatch[1].trim();
|
|
364
|
+
}
|
|
365
|
+
// If neither tag was used, the model gave us raw output — use as-is
|
|
366
|
+
return cleaned || raw.trim();
|
|
367
|
+
}
|
|
242
368
|
/**
|
|
243
369
|
* Pick a cheaper/faster model for compaction to save cost.
|
|
244
370
|
*/
|
package/dist/agent/context.d.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Context Manager for
|
|
2
|
+
* Context Manager for Franklin
|
|
3
3
|
* Assembles system instructions, reads project config, injects environment info.
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Build the full system instructions array for a session.
|
|
7
7
|
* Result is memoized per workingDir for the process lifetime.
|
|
8
8
|
*/
|
|
9
|
-
export declare function assembleInstructions(workingDir: string): string[];
|
|
9
|
+
export declare function assembleInstructions(workingDir: string, model?: string): string[];
|
|
10
|
+
/**
|
|
11
|
+
* Model-family-specific execution guidance.
|
|
12
|
+
* Weak models get strict guardrails. Strong models get quality standards.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getModelGuidance(model: string): string;
|
|
10
15
|
/** Invalidate cache for a workingDir (call after /clear or session reset). */
|
|
11
|
-
export declare function invalidateInstructionCache(workingDir
|
|
16
|
+
export declare function invalidateInstructionCache(workingDir?: string): void;
|