@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.
Files changed (109) hide show
  1. package/README.md +65 -25
  2. package/dist/agent/commands.d.ts +1 -1
  3. package/dist/agent/commands.js +128 -17
  4. package/dist/agent/compact.d.ts +2 -2
  5. package/dist/agent/compact.js +148 -22
  6. package/dist/agent/context.d.ts +8 -3
  7. package/dist/agent/context.js +301 -108
  8. package/dist/agent/error-classifier.d.ts +11 -2
  9. package/dist/agent/error-classifier.js +64 -10
  10. package/dist/agent/llm.d.ts +8 -1
  11. package/dist/agent/llm.js +114 -19
  12. package/dist/agent/loop.d.ts +1 -2
  13. package/dist/agent/loop.js +509 -61
  14. package/dist/agent/optimize.d.ts +2 -2
  15. package/dist/agent/optimize.js +9 -7
  16. package/dist/agent/permissions.d.ts +1 -1
  17. package/dist/agent/permissions.js +1 -1
  18. package/dist/agent/planner.d.ts +42 -0
  19. package/dist/agent/planner.js +110 -0
  20. package/dist/agent/reduce.d.ts +7 -1
  21. package/dist/agent/reduce.js +85 -3
  22. package/dist/agent/streaming-executor.d.ts +6 -1
  23. package/dist/agent/streaming-executor.js +83 -5
  24. package/dist/agent/tokens.d.ts +11 -2
  25. package/dist/agent/tokens.js +38 -5
  26. package/dist/agent/tool-guard.d.ts +27 -0
  27. package/dist/agent/tool-guard.js +324 -0
  28. package/dist/agent/types.d.ts +7 -1
  29. package/dist/agent/types.js +1 -1
  30. package/dist/brain/extract.d.ts +11 -0
  31. package/dist/brain/extract.js +154 -0
  32. package/dist/brain/index.d.ts +3 -0
  33. package/dist/brain/index.js +2 -0
  34. package/dist/brain/store.d.ts +42 -0
  35. package/dist/brain/store.js +225 -0
  36. package/dist/brain/types.d.ts +45 -0
  37. package/dist/brain/types.js +5 -0
  38. package/dist/commands/daemon.js +2 -1
  39. package/dist/commands/start.js +19 -7
  40. package/dist/config.js +1 -1
  41. package/dist/index.js +27 -2
  42. package/dist/learnings/extractor.d.ts +13 -0
  43. package/dist/learnings/extractor.js +69 -8
  44. package/dist/learnings/index.d.ts +1 -1
  45. package/dist/learnings/index.js +1 -1
  46. package/dist/learnings/store.js +42 -13
  47. package/dist/learnings/types.d.ts +1 -1
  48. package/dist/mcp/client.d.ts +1 -1
  49. package/dist/mcp/client.js +5 -5
  50. package/dist/mcp/config.d.ts +1 -1
  51. package/dist/mcp/config.js +1 -1
  52. package/dist/panel/html.d.ts +2 -0
  53. package/dist/panel/html.js +409 -146
  54. package/dist/panel/server.js +19 -0
  55. package/dist/pricing.js +3 -2
  56. package/dist/proxy/fallback.d.ts +3 -1
  57. package/dist/proxy/fallback.js +4 -4
  58. package/dist/proxy/server.js +29 -11
  59. package/dist/proxy/sse-translator.js +1 -1
  60. package/dist/router/categories.d.ts +21 -0
  61. package/dist/router/categories.js +96 -0
  62. package/dist/router/index.d.ts +9 -2
  63. package/dist/router/index.js +106 -27
  64. package/dist/router/local-elo.d.ts +32 -0
  65. package/dist/router/local-elo.js +107 -0
  66. package/dist/router/selector.d.ts +46 -0
  67. package/dist/router/selector.js +106 -0
  68. package/dist/session/storage.d.ts +5 -1
  69. package/dist/session/storage.js +24 -2
  70. package/dist/social/a11y.d.ts +1 -1
  71. package/dist/social/a11y.js +5 -1
  72. package/dist/social/browser.d.ts +5 -0
  73. package/dist/social/browser.js +22 -0
  74. package/dist/social/preflight.d.ts +4 -0
  75. package/dist/social/preflight.js +42 -3
  76. package/dist/stats/failures.d.ts +20 -0
  77. package/dist/stats/failures.js +63 -0
  78. package/dist/stats/format.d.ts +6 -0
  79. package/dist/stats/format.js +23 -0
  80. package/dist/stats/insights.js +1 -21
  81. package/dist/stats/session-tracker.d.ts +21 -0
  82. package/dist/stats/session-tracker.js +28 -0
  83. package/dist/stats/tracker.d.ts +1 -1
  84. package/dist/stats/tracker.js +1 -1
  85. package/dist/tools/bash.d.ts +14 -1
  86. package/dist/tools/bash.js +132 -7
  87. package/dist/tools/edit.js +77 -14
  88. package/dist/tools/glob.js +13 -3
  89. package/dist/tools/grep.js +30 -12
  90. package/dist/tools/imagegen.js +5 -5
  91. package/dist/tools/index.d.ts +1 -1
  92. package/dist/tools/index.js +5 -1
  93. package/dist/tools/read.d.ts +16 -2
  94. package/dist/tools/read.js +36 -8
  95. package/dist/tools/searchx.d.ts +6 -2
  96. package/dist/tools/searchx.js +221 -44
  97. package/dist/tools/subagent.js +37 -3
  98. package/dist/tools/task.js +43 -7
  99. package/dist/tools/validate.d.ts +11 -0
  100. package/dist/tools/validate.js +42 -0
  101. package/dist/tools/webfetch.js +18 -7
  102. package/dist/tools/websearch.js +41 -7
  103. package/dist/tools/write.js +26 -6
  104. package/dist/ui/app.js +31 -6
  105. package/dist/ui/model-picker.d.ts +1 -1
  106. package/dist/ui/model-picker.js +1 -1
  107. package/dist/ui/terminal.d.ts +1 -1
  108. package/dist/ui/terminal.js +1 -1
  109. package/package.json +2 -2
@@ -1,48 +1,64 @@
1
1
  /**
2
- * Context compaction for runcode.
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 in this conversation were compacted to save context space. The summary below describes work that was already completed, and the current session state may still reflect that work (for example, files may already be changed). Use the summary and the current state to continue from where things left off, and avoid repeating work:`;
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
- REQUIRED output format (use these exact section headers):
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 Preferences & Corrections
42
- [Anything the user explicitly asked for or corrected — these are load-bearing]
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(`[runcode] Auto-compacting: ~${currentTokens} tokens, threshold=${threshold}`);
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(`[runcode] Auto-compaction grew history (${beforeTokens} → ${afterTokens}) — skipping`);
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(`[runcode] Compaction failed: ${err.message}`);
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(`[runcode] Compaction produced larger history (${beforeTokens} → ${afterTokens}) — reverting`);
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(`[runcode] Force compaction failed: ${err.message}`);
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(`[runcode] Summarizing ${toSummarize.length} messages, keeping ${toKeep.length}`);
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 summaryText = '';
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
- summaryText += part.text;
163
+ rawSummary += part.text;
148
164
  }
149
165
  }
150
- if (!summaryText) {
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(`[runcode] Compacted: ${estimateHistoryTokens(history)} → ${newTokens} tokens`);
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
  */
@@ -1,11 +1,16 @@
1
1
  /**
2
- * Context Manager for runcode
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: string): void;
16
+ export declare function invalidateInstructionCache(workingDir?: string): void;