@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
package/README.md CHANGED
@@ -29,6 +29,7 @@
29
29
  <a href="#quick-start">Quick&nbsp;start</a> ·
30
30
  <a href="#a-new-category">New&nbsp;category</a> ·
31
31
  <a href="#what-franklin-can-execute">What&nbsp;it&nbsp;does</a> ·
32
+ <a href="#smart-router">Smart&nbsp;Router</a> ·
32
33
  <a href="#the-comparison">Compare</a> ·
33
34
  <a href="#features">Features</a> ·
34
35
  <a href="#how-it-works">Architecture</a> ·
@@ -120,31 +121,20 @@ Franklin is **chat-first**. You do not wire a DAG, configure six API keys, or co
120
121
 
121
122
  Live data from CoinGecko. RSI, MACD, Bollinger, and volatility computed locally. No API key needed.
122
123
 
123
- ### 🎯 Social growth
124
+ ### 🎨 Image generation
124
125
 
125
126
  ```text
126
- > find X posts complaining about AI rate limits
127
+ > generate a logo for my AI startup — minimalist, dark background
127
128
 
128
- SearchX "AI rate limits"
129
-
130
- Found 8 candidates:
131
- 1. "Claude keeps throttling me in the middle of shipping..." — @buildermax (2h)
132
- 2. "I need an agent that can switch models automatically." — @indiedev (5h)
133
- ...
134
-
135
- > write a reply to #2 — mention Franklin uses a wallet instead of subscriptions
129
+ ImageGen "minimalist AI startup logo, dark background..."
130
+ Saved: generated-logo-1713052800.png (1024x1024)
131
+ ```
136
132
 
137
- Draft:
138
- "That was my pain too. Franklin routes across 55+ models,
139
- pays per action from a USDC wallet, and doesn't trap you
140
- inside a monthly seat. Better economics, better uptime."
133
+ Generates images via DALL-E / GPT Image directly from the CLI. Paid from your wallet — no OpenAI API key needed.
141
134
 
142
- > looks good, post it
135
+ ### 🎯 Social growth (with setup)
143
136
 
144
- PostToX Reply posted to x.com/indiedev/status/...
145
- ```
146
-
147
- Search X, generate contextual replies, and post with confirmation. Uses Playwright for browser automation, so there is no X API key, no OAuth maze, and no $100/month developer account.
137
+ After running `franklin social setup && franklin social login x`, Franklin can search X, draft replies, and post with your confirmation — no X API key or developer account needed.
148
138
 
149
139
  ### 🔎 Research, code, anything with a budget
150
140
 
@@ -179,6 +169,52 @@ Every tool call is itemized. Every token is priced. When the wallet hits zero, F
179
169
 
180
170
  ---
181
171
 
172
+ ## Smart Router
173
+
174
+ **55+ models. One decision. Zero guesswork.**
175
+
176
+ You don't pick models. Franklin picks for you.
177
+
178
+ The Smart Router classifies every request — coding, trading, reasoning, research — and selects the model with the best quality-to-cost ratio. Trained on **2M+ real requests** from the BlockRun gateway, continuously updated.
179
+
180
+ ```text
181
+ > refactor this auth module to use JWT
182
+
183
+ CODING kimi-k2.5 · 12.4K in / 2.1K out · $0.0023 saved 84%
184
+
185
+ > what's the BTC outlook for the week?
186
+
187
+ TRADING grok-4-1-fast-reasoning · 8.2K in / 1.8K out · $0.0008 saved 95%
188
+
189
+ > prove that this algorithm is O(n log n)
190
+
191
+ REASONING claude-sonnet-4.6 · 15.1K in / 3.4K out · $0.0312
192
+ ```
193
+
194
+ Every response shows which model was chosen, why, and how much you saved vs. always using the most expensive option.
195
+
196
+ **Four profiles:**
197
+
198
+ | Profile | Strategy | Use case |
199
+ |---------|----------|----------|
200
+ | `auto` | Best quality-to-cost ratio | Default — smart spend |
201
+ | `eco` | Cheapest model with decent quality | Budget-conscious |
202
+ | `premium` | Highest quality regardless of cost | Mission-critical |
203
+ | `free` | Free NVIDIA models only | Zero wallet balance |
204
+
205
+ **Per-session breakdown** — run `/cost` to see exactly where your USDC went:
206
+
207
+ ```text
208
+ Session Cost: $0.0847 (23 requests)
209
+ gemini-2.5-flash $0.0012 14 req CODING
210
+ kimi-k2.5 $0.0423 6 req CODING
211
+ claude-sonnet-4.6 $0.0412 3 req REASONING
212
+ ```
213
+
214
+ The router also learns from **your** usage. If you keep retrying a model for coding tasks, Franklin adapts and picks a better one next time. Your router gets smarter the more you use it.
215
+
216
+ ---
217
+
182
218
  ## Why Franklin
183
219
 
184
220
  <table>
@@ -215,6 +251,7 @@ Marketing, trading, research, code, and anything else you can express as tools p
215
251
  | ------------------------------------ | --------------- | ---------------- | ---------------- | ------------------------------- |
216
252
  | Main unit of value | Answers | Code changes | Fixed automations| **Budgeted outcomes** |
217
253
  | Has purchasing power | ❌ | ❌ | ❌ | ✅ **wallet-native** |
254
+ | Picks best model per task | ❌ | ❌ | ❌ | ✅ **learned router** |
218
255
  | Can choose tools/models per step | ⚠️ limited | ✅ mostly coding | ❌ usually fixed | ✅ **yes** |
219
256
  | Works across marketing/trading/code | ⚠️ | ❌ code-first | ⚠️ integration-bound | ✅ **cross-vertical** |
220
257
  | Hard spend cap | ❌ | ❌ | ⚠️ external billing | ✅ **wallet balance** |
@@ -238,8 +275,8 @@ Franklin can decide what is worth paying for, route the call, sign the micropaym
238
275
  **📈 Trading signals**
239
276
  Ask "what's BTC looking like?" — Franklin fetches live price data, computes RSI/MACD/Bollinger/volatility, and synthesizes a signal.
240
277
 
241
- **🎯 Social growth**
242
- Ask "find X posts about my category" — Franklin searches X, drafts replies, and posts with your confirmation.
278
+ **🎨 AI image generation**
279
+ Ask "generate a logo" — Franklin calls DALL-E / GPT Image, saves the result locally, paid from your wallet.
243
280
 
244
281
  **🧠 55+ models via one wallet**
245
282
  Anthropic, OpenAI, Google, xAI, DeepSeek, GLM, Kimi, Minimax, NVIDIA free tier. One wallet, one interface, automatic fallback.
@@ -247,8 +284,8 @@ Anthropic, OpenAI, Google, xAI, DeepSeek, GLM, Kimi, Minimax, NVIDIA free tier.
247
284
  **💳 x402 micropayments**
248
285
  HTTP 402 native. Every paid action is a signed micropayment against your USDC balance. No subscriptions. No refund loop. No account lock-in.
249
286
 
250
- **🚦 Smart spend routing**
251
- Free / cheap / premium per step. Franklin picks the cheapest model that can do the job, then escalates when quality matters.
287
+ **🧠 Learned model router**
288
+ Trained on 2M+ real requests. Classifies your task and picks the best model from 55+ LLMs. Four profiles (auto/eco/premium/free). Adapts to your usage over time.
252
289
 
253
290
  </td>
254
291
  <td width="50%" valign="top">
@@ -301,7 +338,10 @@ Core is workflow-agnostic. Add new verticals without touching the loop. Discover
301
338
  ```text
302
339
  ┌──────────────────────────────────────────────────────────────┐
303
340
  │ Franklin Runtime │
304
- │ Intent → Routing → Tool Use → Spend Decisions → Result
341
+ │ Intent → Smart Router → Tool Use → Spend Control → Result
342
+ ├──────────────────────────────────────────────────────────────┤
343
+ │ Learned Router │
344
+ │ 2M+ requests · 55+ models · category detection · Elo scores │
305
345
  ├──────────────────────────────────────────────────────────────┤
306
346
  │ Agent Loop │
307
347
  │ 16 tools · Sessions · Compaction · Pricing · Plugin SDK │
@@ -349,7 +389,7 @@ src/
349
389
  ├── stats/ Usage tracking + insights engine
350
390
  ├── ui/ Ink-based terminal UI
351
391
  ├── proxy/ Payment proxy for external tools
352
- ├── router/ Smart model routing (free/cheap/premium)
392
+ ├── router/ Learned model router (2M+ requests, Elo scoring)
353
393
  ├── wallet/ Wallet management (Base + Solana)
354
394
  ├── mcp/ MCP server auto-discovery
355
395
  └── commands/ CLI subcommands
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Slash command registry for runcode.
2
+ * Slash command registry for Franklin.
3
3
  * Extracted from loop.ts for maintainability.
4
4
  *
5
5
  * Two types of commands:
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Slash command registry for runcode.
2
+ * Slash command registry for Franklin.
3
3
  * Extracted from loop.ts for maintainability.
4
4
  *
5
5
  * Two types of commands:
@@ -104,8 +104,13 @@ function extractText(msg) {
104
104
  // ─── Command Definitions ──────────────────────────────────────────────────
105
105
  // Direct-handled commands (don't go to agent)
106
106
  const DIRECT_COMMANDS = {
107
+ '/noplan': (ctx) => {
108
+ ctx.config.planDisabled = true;
109
+ ctx.onEvent({ kind: 'text_delta', text: 'Plan-then-execute disabled for this session. Complex tasks will use a single model.\n' });
110
+ emitDone(ctx);
111
+ },
107
112
  '/stash': (ctx) => {
108
- const r = gitCmd(ctx, 'git stash push -m "runcode auto-stash"', 10000);
113
+ const r = gitCmd(ctx, 'git stash push -m "franklin auto-stash"', 10000);
109
114
  if (r !== null)
110
115
  ctx.onEvent({ kind: 'text_delta', text: r ? `${r}\n` : 'No changes to stash.\n' });
111
116
  emitDone(ctx);
@@ -197,8 +202,8 @@ const DIRECT_COMMANDS = {
197
202
  ` **Git:** /push /pr /undo /status /diff /log /branch /stash /unstash\n` +
198
203
  ` **Analysis:** /security /lint /optimize /todo /deps /clean /migrate /doc\n` +
199
204
  ` **Session:** /plan /ultraplan /execute /compact /retry /sessions /resume /session-search /context /tasks\n` +
200
- ` **Power:** /ultrathink [query] /ultraplan /dump\n` +
201
- ` **Info:** /model /wallet /cost /tokens /learnings /mcp /doctor /version /bug /help\n` +
205
+ ` **Power:** /ultrathink [query] /ultraplan /noplan /dump\n` +
206
+ ` **Info:** /model /wallet /cost /tokens /learnings /brain /mcp /doctor /version /bug /help\n` +
202
207
  ` **UI:** /clear /exit\n` +
203
208
  (ultrathinkOn ? `\n Ultrathink: ON\n` : '')
204
209
  });
@@ -225,7 +230,7 @@ const DIRECT_COMMANDS = {
225
230
  emitDone(ctx);
226
231
  },
227
232
  '/bug': (ctx) => {
228
- ctx.onEvent({ kind: 'text_delta', text: 'Report issues at: https://github.com/BlockRunAI/runcode/issues\n' });
233
+ ctx.onEvent({ kind: 'text_delta', text: 'Report issues at: https://github.com/BlockRunAI/Franklin/issues\n' });
229
234
  emitDone(ctx);
230
235
  },
231
236
  '/version': (ctx) => {
@@ -340,34 +345,57 @@ const DIRECT_COMMANDS = {
340
345
  }
341
346
  emitDone(ctx);
342
347
  },
343
- '/sessions': (ctx) => {
348
+ '/sessions': async (ctx) => {
344
349
  const sessions = listSessions();
345
350
  if (sessions.length === 0) {
346
351
  ctx.onEvent({ kind: 'text_delta', text: 'No saved sessions.\n' });
347
352
  }
348
353
  else {
354
+ const { formatTokens, formatUsd, shortModelName } = await import('../stats/format.js');
349
355
  let text = `**${sessions.length} saved sessions:**\n\n`;
350
356
  for (const s of sessions.slice(0, 10)) {
351
357
  const date = new Date(s.updatedAt).toLocaleString();
352
- const dir = s.workDir ? ` — ${s.workDir.split('/').pop()}` : '';
353
- const current = s.id === ctx.sessionId ? ' (current)' : '';
354
- text += ` ${s.id} ${s.model} ${s.turnCount} turns ${date}${dir}${current}\n`;
358
+ const dir = s.workDir ? path.basename(s.workDir) : '';
359
+ const current = s.id === ctx.sessionId ? ' (current)' : '';
360
+ const model = shortModelName(s.model);
361
+ const tokens = (s.inputTokens || s.outputTokens)
362
+ ? ` ${formatTokens(s.inputTokens ?? 0)} in / ${formatTokens(s.outputTokens ?? 0)} out`
363
+ : '';
364
+ const cost = s.costUsd ? ` ${formatUsd(s.costUsd)}` : '';
365
+ const saved = s.savedVsOpusUsd && s.savedVsOpusUsd > 0.001
366
+ ? ` saved ${formatUsd(s.savedVsOpusUsd)}`
367
+ : '';
368
+ text += ` ${model} — ${s.messageCount} messages${tokens}${cost}${saved}\n`;
369
+ text += ` ${date} · ${dir}${current}\n\n`;
355
370
  }
356
371
  if (sessions.length > 10)
357
372
  text += ` ... and ${sessions.length - 10} more\n`;
358
- text += '\nUse /resume to restore the latest session, or /resume <session-id> for a specific one.\n';
373
+ text += 'Use /resume to restore the latest session, or /resume <session-id> for a specific one.\n';
359
374
  ctx.onEvent({ kind: 'text_delta', text });
360
375
  }
361
376
  emitDone(ctx);
362
377
  },
363
378
  '/cost': async (ctx) => {
364
379
  const { stats, saved } = getStatsSummary();
365
- ctx.onEvent({ kind: 'text_delta', text: `**Session Cost**\n` +
366
- ` Requests: ${stats.totalRequests}\n` +
367
- ` Cost: $${stats.totalCostUsd.toFixed(4)} USDC\n` +
368
- ` Saved: $${saved.toFixed(2)} vs Claude Opus\n` +
369
- ` Tokens: ${stats.totalInputTokens.toLocaleString()} in / ${stats.totalOutputTokens.toLocaleString()} out\n`
370
- });
380
+ const { getSessionModelBreakdown } = await import('../stats/session-tracker.js');
381
+ const { formatTokens, formatUsd, shortModelName } = await import('../stats/format.js');
382
+ const breakdown = getSessionModelBreakdown();
383
+ let text = `**Session Cost**\n` +
384
+ ` Requests: ${stats.totalRequests}\n` +
385
+ ` Cost: $${stats.totalCostUsd.toFixed(4)} USDC\n` +
386
+ ` Saved: $${saved.toFixed(2)} vs Claude Opus\n` +
387
+ ` Tokens: ${formatTokens(stats.totalInputTokens)} in / ${formatTokens(stats.totalOutputTokens)} out\n`;
388
+ if (breakdown.length > 0) {
389
+ text += `\n **By model:**\n`;
390
+ for (const m of breakdown) {
391
+ const name = shortModelName(m.model).padEnd(28);
392
+ const cost = formatUsd(m.costUsd).padStart(8);
393
+ const reqs = `${m.requests} req`.padStart(6);
394
+ const tier = m.lastTier ? ` ${m.lastTier}` : '';
395
+ text += ` ${name} ${cost} ${reqs}${tier}\n`;
396
+ }
397
+ }
398
+ ctx.onEvent({ kind: 'text_delta', text });
371
399
  emitDone(ctx);
372
400
  },
373
401
  '/wallet': async (ctx) => {
@@ -419,6 +447,38 @@ const DIRECT_COMMANDS = {
419
447
  ctx.onEvent({ kind: 'text_delta', text: 'Conversation history cleared.\n' });
420
448
  emitDone(ctx);
421
449
  },
450
+ '/failures': async (ctx) => {
451
+ const { getFailureStats } = await import('../stats/failures.js');
452
+ const stats = getFailureStats();
453
+ if (stats.total === 0) {
454
+ ctx.onEvent({ kind: 'text_delta', text: 'No failures recorded.\n' });
455
+ emitDone(ctx);
456
+ return;
457
+ }
458
+ let text = `**Failure Log** (${stats.total} total)\n\n`;
459
+ if (stats.byType.size > 0) {
460
+ text += ' **By type:**\n';
461
+ for (const [type, count] of [...stats.byType.entries()].sort((a, b) => b[1] - a[1])) {
462
+ text += ` ${type.padEnd(20)} ${count}\n`;
463
+ }
464
+ }
465
+ if (stats.byTool.size > 0) {
466
+ text += '\n **By tool:**\n';
467
+ for (const [tool, count] of [...stats.byTool.entries()].sort((a, b) => b[1] - a[1])) {
468
+ text += ` ${tool.padEnd(20)} ${count}\n`;
469
+ }
470
+ }
471
+ if (stats.recentFailures.length > 0) {
472
+ text += '\n **Recent:**\n';
473
+ for (const f of stats.recentFailures.slice(-5)) {
474
+ const date = new Date(f.timestamp).toLocaleDateString();
475
+ const tool = f.toolName ? ` ${f.toolName}:` : '';
476
+ text += ` [${date}]${tool} ${f.errorMessage.slice(0, 80)}\n`;
477
+ }
478
+ }
479
+ ctx.onEvent({ kind: 'text_delta', text });
480
+ emitDone(ctx);
481
+ },
422
482
  '/compact': async (ctx) => {
423
483
  const beforeTokens = estimateHistoryTokens(ctx.history);
424
484
  const { history: compacted, compacted: didCompact } = await forceCompact(ctx.history, ctx.config.model, ctx.client, ctx.config.debug);
@@ -546,6 +606,57 @@ export async function handleSlashCommand(input, ctx) {
546
606
  emitDone(ctx);
547
607
  return { handled: true };
548
608
  }
609
+ // /brain — view knowledge graph entities
610
+ if (input === '/brain' || input.startsWith('/brain ')) {
611
+ const { searchEntities, loadEntities, getEntityObservations, getEntityRelations, getBrainStats, loadObservations } = await import('../brain/store.js');
612
+ const arg = input.slice('/brain'.length).trim();
613
+ if (!arg) {
614
+ const stats = getBrainStats();
615
+ if (stats.entities === 0) {
616
+ ctx.onEvent({ kind: 'text_delta', text: 'Brain is empty. Franklin learns entities (people, projects, companies) from your conversations over time.\n' });
617
+ }
618
+ else {
619
+ const entities = loadEntities().sort((a, b) => b.reference_count - a.reference_count);
620
+ let text = `**Franklin Brain** (${stats.entities} entities, ${stats.observations} facts, ${stats.relations} relations)\n\n`;
621
+ for (const e of entities.slice(0, 20)) {
622
+ text += ` ${e.type === 'person' ? '👤' : e.type === 'company' ? '🏢' : e.type === 'project' ? '📦' : '💡'} **${e.name}** (${e.type}, ×${e.reference_count})\n`;
623
+ }
624
+ if (entities.length > 20)
625
+ text += ` ... and ${entities.length - 20} more\n`;
626
+ text += '\nSearch: `/brain <name>` for details.\n';
627
+ ctx.onEvent({ kind: 'text_delta', text });
628
+ }
629
+ }
630
+ else {
631
+ const results = searchEntities(arg, 5);
632
+ if (results.length === 0) {
633
+ ctx.onEvent({ kind: 'text_delta', text: `No entities matching "${arg}".\n` });
634
+ }
635
+ else {
636
+ let text = '';
637
+ for (const e of results) {
638
+ text += `**${e.name}** (${e.type})\n`;
639
+ if (e.aliases.length > 0)
640
+ text += ` Aliases: ${e.aliases.join(', ')}\n`;
641
+ const obs = getEntityObservations(e.id).slice(0, 5);
642
+ for (const o of obs) {
643
+ text += ` - ${o.content}\n`;
644
+ }
645
+ const rels = getEntityRelations(e.id);
646
+ const allEntities = loadEntities();
647
+ for (const r of rels.slice(0, 3)) {
648
+ const other = allEntities.find(x => x.id === (r.from_id === e.id ? r.to_id : r.from_id));
649
+ if (other)
650
+ text += ` → ${r.type} ${other.name}\n`;
651
+ }
652
+ text += '\n';
653
+ }
654
+ ctx.onEvent({ kind: 'text_delta', text });
655
+ }
656
+ }
657
+ emitDone(ctx);
658
+ return { handled: true };
659
+ }
549
660
  // /model — show current model or switch with /model <name>
550
661
  if (input === '/model' || input.startsWith('/model ')) {
551
662
  if (input === '/model') {
@@ -677,7 +788,7 @@ export async function handleSlashCommand(input, ctx) {
677
788
  ...Object.keys(DIRECT_COMMANDS),
678
789
  ...Object.keys(REWRITE_COMMANDS),
679
790
  ...ARG_COMMANDS.map(c => c.prefix.trim()),
680
- '/branch', '/resume', '/model', '/wallet', '/cost', '/help', '/clear', '/retry', '/exit', '/session-search', '/ssearch',
791
+ '/branch', '/resume', '/model', '/wallet', '/cost', '/help', '/clear', '/retry', '/exit', '/session-search', '/ssearch', '/failures',
681
792
  ];
682
793
  const cmd = input.split(/\s/)[0];
683
794
  const close = allCommands.filter(c => {
@@ -1,11 +1,11 @@
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
6
  import { ModelClient } from './llm.js';
7
7
  import type { Dialogue } from './types.js';
8
- export declare 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:";
8
+ export declare const COMPACT_HEADER = "[CONTEXT COMPACTION \u2014 REFERENCE ONLY] Earlier turns were compacted into the summary below. This is a handoff from a previous context window \u2014 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.";
9
9
  /**
10
10
  * Check if compaction is needed and perform it if so.
11
11
  * Returns the (possibly compacted) history.