@blockrun/franklin 3.15.86 → 3.15.88

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.
@@ -18,7 +18,7 @@ You are an interactive agent — not a chatbot. Use the tools available to you t
18
18
 
19
19
  # Franklin has hands
20
20
  You run with live tools by default:
21
- - **Wallet** — read your own chain, address, and USDC balance. Use this for any "what's my balance / how much money / 钱包余额 / wallet status" question instead of running \`franklin balance\` via Bash. Free, one call, never costs USDC.
21
+ - **Wallet** — read your own chain, address, and USDC balance. Use this for any "what's my balance / how much money / wallet status" question instead of running \`franklin balance\` via Bash. Free, one call, never costs USDC.
22
22
  - **TradingMarket** — current stock / FX / crypto / commodity prices (BlockRun Gateway / Pyth; wallet pays automatically, $0.001/stock call, free for everything else).
23
23
  - **ExaAnswer / ExaSearch / ExaReadUrls** — cited current-events answers, semantic web search, clean URL content.
24
24
  - **WebSearch / WebFetch** — live web.
@@ -88,7 +88,7 @@ function getOutputEfficiencySection() {
88
88
  return `# Output Efficiency
89
89
  Go straight to the point. Lead with the action, not the reasoning. Do not restate what the user said.
90
90
 
91
- **No pre-tool narration.** Do NOT write things like "让我先 X...", "Let me read the file...", "I'll now search for...", "好的,让我研究一下...", "现在我来 X", "OK now I have everything I need", "完美!", "好,现在我完全明白了". These phrases are internal monologue — the user can see your tool calls directly and does not need step-by-step play-by-play. Just call the tool.
91
+ **No pre-tool narration.** Do NOT write things like "Let me read the file...", "I'll now search for...", "Let me investigate...", "Now I'm going to X", "OK now I have everything I need", "Perfect!", "Got it, now I fully understand". These phrases are internal monologue — the user can see your tool calls directly and does not need step-by-step play-by-play. Just call the tool. The same rule applies in any language — no equivalent narration in non-English replies either.
92
92
 
93
93
  The exception: a single short sentence between tool calls is fine when it tells the user something they would otherwise miss — a finding ("Build passes — moving on to tests."), a course correction ("That approach won't work — switching to X."), or a one-line status before a long-running operation. One sentence per update is enough.
94
94
 
@@ -342,7 +342,7 @@ If you find yourself about to emit one of these, stop and call the tool instead.
342
342
  - "what are the odds on Polymarket / Kalshi specifically" → \`searchPolymarket\` (\$0.001) and \`searchKalshi\` (\$0.001) **in parallel**; comparing implied probability across the two venues is the high-value answer.
343
343
  - "where do Polymarket and Kalshi disagree / arbitrage" → \`crossPlatform\` (\$0.005) returns pre-matched pairs.
344
344
  - "who's profitable / top traders / who should I follow on Polymarket" → \`leaderboard\` (\$0.001) — global top wallets by P&L.
345
- - "analyze this wallet / can I copy this trader / 复制交易 / show me their P&L AND positions" → run \`walletProfile\` + \`walletPnl\` + \`walletPositions\` IN PARALLEL with the same address. Three \$0.005 calls = full picture for \$0.015. Do NOT \`Bash\`-curl \`data-api.polymarket.com\` directly — those are paid Predexon endpoints and going around them defeats the wallet-attached architecture. If just the profile is needed: \`walletProfile\` alone (single address → /wallet/{addr}, comma-list → batch).
345
+ - "analyze this wallet / can I copy this trader / show me their P&L AND positions" → run \`walletProfile\` + \`walletPnl\` + \`walletPositions\` IN PARALLEL with the same address. Three \$0.005 calls = full picture for \$0.015. Do NOT \`Bash\`-curl \`data-api.polymarket.com\` directly — those are paid Predexon endpoints and going around them defeats the wallet-attached architecture. If just the profile is needed: \`walletProfile\` alone (single address → /wallet/{addr}, comma-list → batch).
346
346
  - "what are smart traders betting on right now / smart money flow across markets" → \`smartActivity\` (\$0.005) — markets where high-P&L wallets are positioning.
347
347
  - "show smart money on this specific Polymarket market / this condition_id" → \`smartMoney\` (\$0.005) with \`conditionId="<condition_id>"\`.
348
348
 
@@ -352,7 +352,7 @@ NEVER answer "what are the odds of X" from training-data memory — these are li
352
352
  - Run **TradingSignal** with default lookback (90d). Lower values leave MACD undefined.
353
353
  - The tool returns a **Verdict** section with \`Direction\`, \`Bull signals\`, \`Bear signals\`. Echo it directly. Do not soften "bullish" to "leaning slightly positive" — say what the data says.
354
354
  - If \`Data Notes\` lists an indicator as "insufficient data", state that explicitly to the user and suggest re-running with more days. Do NOT pretend that indicator is "neutral".
355
- - **Forbidden default**: "持有观望", "wait and see", "hold for clearer signals" — these are bugs when ≥2 indicators voted in a clear direction. Bail out to those phrases ONLY when (a) the Verdict says \`neutral\` AND (b) the bull/bear signal lists are both genuinely empty or one of each. Otherwise commit to a direction with the reasoning the tool already gave you.
355
+ - **Forbidden default**: "wait and see" / "hold for clearer signals" / equivalent hedging in any language — these are bugs when ≥2 indicators voted in a clear direction. Bail out to that posture ONLY when (a) the Verdict says \`neutral\` AND (b) the bull/bear signal lists are both genuinely empty or one of each. Otherwise commit to a direction with the reasoning the tool already gave you.
356
356
 
357
357
  **Media generation (ImageGen / VideoGen).** Pass just the user's descriptive prompt and the output path — do NOT pass \`model\`. The harness picks the right model for the requested style + budget, refines loose prompts using a 5-slot template (scene / subject / details / use case / constraints), and surfaces both the refinement and a cost proposal through AskUser before spending. If the user wants their prompt left exactly as written, prefix it with \`///\` to skip refinement. Only pass \`model\` explicitly if the user named one specifically.`;
358
358
  }
@@ -48,7 +48,7 @@ Flag as ungrounded:
48
48
  - Invented specifics — names, numbers, dates the model produced without a tool call supporting them
49
49
 
50
50
  ### B. Tool-use refusal (NEW)
51
- If the user clearly asked for live-world data — a current price, today's news, the latest state of X — and the assistant's answer contains a refusal or deflection (e.g. "I can't provide real-time prices", "我无法提供实时数据", "check Yahoo Finance yourself", "as an AI I don't have access to live data"), that is also UNGROUNDED. Franklin HAS tools for this (TradingMarket for prices, ExaAnswer for current events, WebSearch for general web, etc.). Refusing to reach for them is the failure this check was built for.
51
+ If the user clearly asked for live-world data — a current price, today's news, the latest state of X — and the assistant's answer contains a refusal or deflection (e.g. "I can't provide real-time prices", "I don't have access to live data", "check Yahoo Finance yourself", "as an AI I cannot fetch this"), that is also UNGROUNDED. The same rule applies in any language. Franklin HAS tools for this (TradingMarket for prices, ExaAnswer for current events, WebSearch for general web, etc.). Refusing to reach for them is the failure this check was built for.
52
52
 
53
53
  Flag as tool-use refusal:
54
54
  - "I can't check real-time prices"
@@ -291,10 +291,10 @@ export function looksLikeGatewayErrorAsText(parts) {
291
291
  * pinned by tool_choice when the user prompt actually references that
292
292
  * tool's domain — otherwise we let the smart generator pick from any tool.
293
293
  *
294
- * The motivating bug: a real-estate question ("可以还价 20% ") had its
295
- * answer flagged as ungrounded for citing $/sqft figures. The cheap
296
- * evaluator model picked TradingMarket as the missing tool because it
297
- * was the first example in the evaluator prompt. Forcing TradingMarket
294
+ * The motivating bug: a real-estate question ("can I negotiate 20% off")
295
+ * had its answer flagged as ungrounded for citing $/sqft figures. The
296
+ * cheap evaluator model picked TradingMarket as the missing tool because
297
+ * it was the first example in the evaluator prompt. Forcing TradingMarket
298
298
  * (a crypto-only tool) on a housing question made the retry useless.
299
299
  *
300
300
  * This function returns false for specialized tools when the prompt has
@@ -304,16 +304,18 @@ export function looksLikeGatewayErrorAsText(parts) {
304
304
  */
305
305
  function isToolRelevantToPrompt(toolName, promptLower) {
306
306
  // Crypto trading tools — need a ticker, "crypto", "coin", "swap", etc.
307
+ // English-only fast path; the LLM-level classifier handles other languages
308
+ // before this domain-relevance check runs.
307
309
  if (/^(Trading|DefiLlama|Jupiter|Base0x|Base0xGasless)/i.test(toolName)) {
308
- return /\b(btc|eth|sol|xrp|doge|usdc|usdt|crypto|coin|token|defi|tvl|yield|swap|jupiter|uniswap|pump\.fun|solana|base chain|polygon|ethereum|币|代币|链上|做空|做多)\b/i.test(promptLower);
310
+ return /\b(btc|eth|sol|xrp|doge|usdc|usdt|crypto|coin|token|defi|tvl|yield|swap|jupiter|uniswap|pump\.fun|solana|base chain|polygon|ethereum)\b/i.test(promptLower);
309
311
  }
310
312
  // X.com search — need an @handle, "twitter", "tweet", "X.com"
311
313
  if (/^SearchX$/i.test(toolName) || /^PostToX$/i.test(toolName)) {
312
- return /(@\w+|twitter|x\.com|tweet|推特)/i.test(promptLower);
314
+ return /(@\w+|twitter|x\.com|tweet)/i.test(promptLower);
313
315
  }
314
316
  // Image / video / music gen — need a creative-content request
315
317
  if (/^(ImageGen|VideoGen|MusicGen)$/i.test(toolName)) {
316
- return /\b(image|picture|photo|video|clip|music|song|generate|create|render|draw|画|图|视频|音乐|歌)\b/i.test(promptLower);
318
+ return /\b(image|picture|photo|video|clip|music|song|generate|create|render|draw)\b/i.test(promptLower);
317
319
  }
318
320
  // General-purpose / file / shell tools — always relevant.
319
321
  return true;
@@ -860,7 +862,8 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
860
862
  try {
861
863
  // Anchor 1: the user's current message (already in lastUserInput).
862
864
  // Anchor 2: first chunk of the previous assistant reply — gives the
863
- // analyzer enough context to resolve deictic follow-ups like "那 AAPL 呢".
865
+ // analyzer enough context to resolve deictic follow-ups like
866
+ // "and that one?" / "what about AAPL".
864
867
  const lastAssistantText = (() => {
865
868
  const prior = [...history.slice(0, -1)].reverse()
866
869
  .find((m) => m.role === 'assistant');
@@ -1843,7 +1846,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1843
1846
  recordOutcome(lastRoutedCategory, lastRoutedModel, 'continued', turnToolCalls);
1844
1847
  }
1845
1848
  // End-of-turn marker for question-shaped responses. Real-world UX
1846
- // problem 2026-05-06: agent finishes a turn with "要我查一下 X 吗?"
1849
+ // problem 2026-05-06: agent finishes a turn with "Should I look up X?"
1847
1850
  // and stops; the user reads the silence as "Franklin died" twice in
1848
1851
  // one hour. The Ink input box is already on screen but it's easy to
1849
1852
  // miss after a long output scroll. A single trailing italic line
@@ -64,15 +64,15 @@ Anti-slop rules:
64
64
  - Wrap literal text that must appear in the image in double quotes. Spell difficult words letter-by-letter.
65
65
  - One revision per turn — do not combine conflicting asks.
66
66
  - Natural language, not keyword-tag format.
67
- - refined_prompt stays in the same language as the user input. Chinese in → Chinese out.
67
+ - refined_prompt stays in the same language as the user input.
68
68
 
69
69
  Examples:
70
70
 
71
71
  Input: "a photo of a cat on Mars, photoreal"
72
72
  Output: {"style":"photoreal","priority":"balanced","refined_prompt":"Eye-level photograph of a cat standing on the rust-colored Martian surface, late-afternoon low sun casting long shadows, distant canyon rim in the background, 50mm feel, shallow depth of field, editorial photo use, no watermark.","refinement_summary":"Added scene, lighting, lens, use case, constraint.","recommended":{"model":"google/nano-banana-pro","rationale":"Photoreal scenes — Nano Banana Pro has strong realism at moderate cost."},"cheaper":{"model":"google/nano-banana","rationale":"Same family, lower cost, slightly less detail."},"premium":{"model":"openai/gpt-image-2","rationale":"Best photoreal fidelity when budget allows."}}
73
73
 
74
- Input: "赛博朋克风格的动漫角色"
75
- Output: {"style":"anime","priority":"balanced","refined_prompt":"赛博朋克风格的动漫角色,站在霓虹灯映照的雨夜街道上,身穿合成纤维夹克与金属反光饰件,头顶全息广告牌漂浮,低角度视角,强烈青粉对比,海报用,居中构图。","refinement_summary":"补全了场景、光线、材质、用途、构图。","recommended":{"model":"zai/cogview-4","rationale":"CogView-4 specializes in stylized/anime imagery."},"cheaper":{"model":"google/nano-banana","rationale":"Cheaper but less stylized."},"premium":{"model":"xai/grok-imagine-image-pro","rationale":"Premium detail for complex scenes."}}
74
+ Input: "cyberpunk-style anime character"
75
+ Output: {"style":"anime","priority":"balanced","refined_prompt":"Cyberpunk-style anime character standing on a neon-lit rainy street at night, wearing a synthetic-fiber jacket with metallic reflective accents, holographic billboards floating overhead, low-angle view, strong cyan-and-pink contrast, poster use, centered composition.","refinement_summary":"Added scene, lighting, materials, use case, composition.","recommended":{"model":"zai/cogview-4","rationale":"CogView-4 specializes in stylized/anime imagery."},"cheaper":{"model":"google/nano-banana","rationale":"Cheaper but less stylized."},"premium":{"model":"xai/grok-imagine-image-pro","rationale":"Premium detail for complex scenes."}}
76
76
 
77
77
  Input: "a 10-second cinematic drone shot over Tokyo at night"
78
78
  Output: {"style":"concept","priority":"quality","refined_prompt":null,"refinement_summary":"Already well-specified.","recommended":{"model":"bytedance/seedance-2.0","rationale":"Seedance 2.0 delivers the best cinematic quality."},"cheaper":{"model":"bytedance/seedance-2.0-fast","rationale":"Faster + cheaper, minor quality trade-off."},"premium":{"model":null,"rationale":"2.0 is already the top tier."}}
@@ -84,10 +84,10 @@ asksForLiveData: true | false
84
84
  ## Context anchors in input
85
85
 
86
86
  [CURRENT] user's message this turn (primary signal)
87
- [PREV_REPLY] last assistant reply, first ~300 chars (for follow-up references: "那 AAPL 呢", "and that one?", "the other ticker")
87
+ [PREV_REPLY] last assistant reply, first ~300 chars (for follow-up references: "and that one?", "the other ticker", "what about AAPL")
88
88
  [GOAL] original session prompt, first ~200 chars
89
89
 
90
- If [CURRENT] uses a deictic ("it", "that", "", "这个"), resolve intent/tier from [PREV_REPLY] or [GOAL].
90
+ If [CURRENT] uses a deictic ("it", "that", "the other one", or any equivalent in the user's language), resolve intent/tier from [PREV_REPLY] or [GOAL].
91
91
 
92
92
  ## Examples
93
93
 
@@ -100,17 +100,17 @@ Input:
100
100
  Output: {"tier":"COMPLEX","intent":{"kind":"ticker","symbol":"CRCL","assetClass":"stock","market":"us","wantNews":true},"needsPlanning":false,"isPushback":false,"asksForLiveData":true}
101
101
 
102
102
  Input:
103
- [CURRENT] AAPL
104
- [PREV_REPLY] CRCL 当前价格 $96.18,最近因 Drift 诉讼下跌...
103
+ [CURRENT] what about AAPL
104
+ [PREV_REPLY] CRCL price $96.18, recently down on Drift lawsuit news...
105
105
  Output: {"tier":"COMPLEX","intent":{"kind":"ticker","symbol":"AAPL","assetClass":"stock","market":"us","wantNews":false},"needsPlanning":false,"isPushback":false,"asksForLiveData":true}
106
106
 
107
107
  Input:
108
- [CURRENT] BTC 为什么跌了
108
+ [CURRENT] why did BTC drop
109
109
  Output: {"tier":"COMPLEX","intent":{"kind":"ticker","symbol":"BTC","assetClass":"crypto","wantNews":true},"needsPlanning":false,"isPushback":false,"asksForLiveData":true}
110
110
 
111
111
  Input:
112
- [CURRENT] 不对,你应该看 NVDA 不是 AAPL
113
- [PREV_REPLY] AAPL 当前价格 $186.42
112
+ [CURRENT] no, you should be looking at NVDA, not AAPL
113
+ [PREV_REPLY] AAPL price $186.42
114
114
  Output: {"tier":"COMPLEX","intent":{"kind":"ticker","symbol":"NVDA","assetClass":"stock","market":"us","wantNews":false},"needsPlanning":false,"isPushback":true,"asksForLiveData":true}
115
115
 
116
116
  Input:
@@ -5,9 +5,9 @@
5
5
  * Tools (ContentCreate / ContentAddAsset) write the library during agent
6
6
  * sessions; before this command, there was no way to see the resulting
7
7
  * spend without scripting against the JSON file. Verified 2026-05-04 in
8
- * a live session: user asked "我花了多少钱做这个", agent ran
9
- * `franklin content list` and got "no content subcommand", fell back to
10
- * estimating from memory.
8
+ * a live session: user asked "how much did I spend making this", agent
9
+ * ran `franklin content list` and got "no content subcommand", fell
10
+ * back to estimating from memory.
11
11
  *
12
12
  * Subcommands:
13
13
  * - list : table of id, type, title, status, spent/budget, assets
@@ -5,9 +5,9 @@
5
5
  * Tools (ContentCreate / ContentAddAsset) write the library during agent
6
6
  * sessions; before this command, there was no way to see the resulting
7
7
  * spend without scripting against the JSON file. Verified 2026-05-04 in
8
- * a live session: user asked "我花了多少钱做这个", agent ran
9
- * `franklin content list` and got "no content subcommand", fell back to
10
- * estimating from memory.
8
+ * a live session: user asked "how much did I spend making this", agent
9
+ * ran `franklin content list` and got "no content subcommand", fell
10
+ * back to estimating from memory.
11
11
  *
12
12
  * Subcommands:
13
13
  * - list : table of id, type, title, status, spent/budget, assets
@@ -509,25 +509,12 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
509
509
  recordLatestSessionIfEnabled(process.cwd(), agentConfig.chain);
510
510
  }
511
511
  catch { /* telemetry is best-effort */ }
512
- // Extract learnings from the session (async, 10s timeout, never blocks exit)
513
- if (sessionHistory && sessionHistory.length >= 4) {
514
- try {
515
- const { extractLearnings } = await import('../learnings/extractor.js');
516
- const { extractBrainEntities } = await import('../brain/extract.js');
517
- const { ModelClient } = await import('../agent/llm.js');
518
- const client = new ModelClient({ apiUrl: agentConfig.apiUrl, chain: agentConfig.chain });
519
- const sid = `session-${new Date().toISOString()}`;
520
- await Promise.race([
521
- Promise.all([
522
- extractLearnings(sessionHistory, sid, client),
523
- extractBrainEntities(sessionHistory, sid, client),
524
- ]),
525
- new Promise(resolve => setTimeout(resolve, 15_000)),
526
- ]);
527
- }
528
- catch { /* extraction is best-effort */ }
512
+ // Optional post-session learning extraction. Disabled by default because any
513
+ // network-backed background promise can keep Node alive after the UI exits.
514
+ if (process.env.FRANKLIN_EXTRACT_ON_EXIT === '1') {
515
+ runExitBackgroundTasks(sessionHistory, agentConfig).catch(() => { });
529
516
  }
530
- await disconnectMcpServers();
517
+ disconnectMcpServers().catch(() => { });
531
518
  // Session summary — delta vs. snapshot at session start
532
519
  try {
533
520
  const delta = statsDelta(startSnapshot);
@@ -559,6 +546,19 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
559
546
  }
560
547
  console.log(chalk.dim('\nGoodbye.\n'));
561
548
  }
549
+ async function runExitBackgroundTasks(sessionHistory, agentConfig) {
550
+ if (!sessionHistory || sessionHistory.length < 4)
551
+ return;
552
+ const { extractLearnings } = await import('../learnings/extractor.js');
553
+ const { extractBrainEntities } = await import('../brain/extract.js');
554
+ const { ModelClient } = await import('../agent/llm.js');
555
+ const client = new ModelClient({ apiUrl: agentConfig.apiUrl, chain: agentConfig.chain });
556
+ const sid = `session-${new Date().toISOString()}`;
557
+ await Promise.all([
558
+ extractLearnings(sessionHistory, sid, client),
559
+ extractBrainEntities(sessionHistory, sid, client),
560
+ ]);
561
+ }
562
562
  // ─── Basic readline UI (piped input) ───────────────────────────────────────
563
563
  async function runWithBasicUI(agentConfig, model, workDir, initialInput) {
564
564
  const { TerminalUI } = await import('../ui/terminal.js');
@@ -4,44 +4,42 @@
4
4
  * using keyword matching from router weights or built-in defaults.
5
5
  */
6
6
  // Built-in category keywords (used when no learned weights available)
7
+ // Keyword fast-path uses English only by policy (English-only-source rule).
8
+ // Non-English user queries route through the LLM-level classifier above this
9
+ // fast-path, which is multilingual and handles intent correctly without
10
+ // needing a per-language keyword list here.
7
11
  const DEFAULT_CATEGORY_KEYWORDS = {
8
12
  coding: [
9
13
  'function', 'class', 'import', 'def', 'SELECT', 'async', 'await',
10
14
  'const', 'let', 'var', 'return', '```', 'bug', 'error', 'fix',
11
15
  'refactor', 'implement', 'test', 'npm', 'pip', 'git', 'deploy',
12
16
  'API', 'endpoint', 'database', 'query', 'migration', 'lint',
13
- '函数', '类', '导入', '修复', '调试', '部署',
14
17
  ],
15
18
  trading: [
16
19
  'BTC', 'ETH', 'SOL', 'bitcoin', 'ethereum', 'solana', 'crypto',
17
20
  'price', 'market', 'signal', 'trade', 'buy', 'sell', 'RSI',
18
21
  'MACD', 'volume', 'bullish', 'bearish', 'support', 'resistance',
19
22
  'portfolio', 'risk', 'leverage', 'DeFi', 'token', 'swap',
20
- '比特币', '以太坊', '价格', '市场', '交易', '信号',
21
23
  ],
22
24
  reasoning: [
23
25
  'prove', 'theorem', 'derive', 'step by step', 'chain of thought',
24
26
  'formally', 'mathematical', 'proof', 'logically', 'analyze',
25
27
  'compare', 'evaluate', 'trade-off', 'pros and cons', 'why',
26
28
  'explain why', 'reasoning', 'logic', 'deduce', 'infer',
27
- '证明', '定理', '推导', '分析', '比较',
28
29
  ],
29
30
  creative: [
30
31
  'write a story', 'poem', 'creative', 'brainstorm', 'imagine',
31
32
  'generate an image', 'design', 'logo', 'illustration', 'art',
32
33
  'narrative', 'fiction', 'song', 'lyrics', 'slogan', 'tagline',
33
- '写一个故事', '诗', '创意', '设计', '头脑风暴',
34
34
  ],
35
35
  research: [
36
36
  'search', 'find', 'look up', 'what is', 'who is', 'when was',
37
37
  'summarize', 'report', 'overview', 'comparison', 'review',
38
38
  'article', 'paper', 'study', 'data', 'statistics', 'trend',
39
- '搜索', '查找', '什么是', '总结', '报告',
40
39
  ],
41
40
  chat: [
42
41
  'hello', 'hi', 'thanks', 'thank you', 'how are you', 'help',
43
42
  'translate', 'yes', 'no', 'ok', 'sure', 'good',
44
- '你好', '谢谢', '帮我', '翻译',
45
43
  ],
46
44
  };
47
45
  /**
@@ -70,13 +70,18 @@ const AUTO_TIERS = {
70
70
  },
71
71
  };
72
72
  // ─── Keywords for Classification ───
73
+ //
74
+ // Keyword fast-path uses English only by policy (English-only-source rule).
75
+ // Non-English user queries route through the LLM-level classifier above this
76
+ // fast-path, which is multilingual and handles intent correctly without
77
+ // needing per-language keyword lists here.
73
78
  const CODE_KEYWORDS = [
74
79
  'function', 'class', 'import', 'def', 'SELECT', 'async', 'await',
75
- 'const', 'let', 'var', 'return', '```', '函数', '类', '导入',
80
+ 'const', 'let', 'var', 'return', '```',
76
81
  ];
77
82
  const REASONING_KEYWORDS = [
78
83
  'prove', 'theorem', 'derive', 'step by step', 'chain of thought',
79
- 'formally', 'mathematical', 'proof', 'logically', '证明', '定理', '推导',
84
+ 'formally', 'mathematical', 'proof', 'logically',
80
85
  ];
81
86
  const SIMPLE_KEYWORDS = [
82
87
  // True simple intents: greeting, definition lookup, translation. Factual
@@ -84,7 +89,7 @@ const SIMPLE_KEYWORDS = [
84
89
  // because they look easy but require external recall — sending them to
85
90
  // SIMPLE-tier models reliably produces hallucinated subscriber counts,
86
91
  // birth years, etc. that the post-hoc grounding check then has to flag.
87
- 'define', 'translate', 'hello', 'yes or no', '翻译', '你好',
92
+ 'define', 'translate', 'hello', 'yes or no',
88
93
  ];
89
94
  // Research / fact-retrieval intent: questions whose correct answer depends
90
95
  // on data the model can't reliably recall from weights — current statistics,
@@ -98,19 +103,16 @@ const RESEARCH_KEYWORDS = [
98
103
  'best', 'top ', 'most popular', 'compare', 'vs ', ' vs.',
99
104
  'latest', 'current', 'recent', 'today', 'now',
100
105
  'subscribers', 'members', 'followers', 'market cap', 'price of',
101
- '最好的', '最新', '最近', '现在', '当前', '排名', '对比',
102
106
  ];
103
107
  const TECHNICAL_KEYWORDS = [
104
108
  'algorithm', 'optimize', 'architecture', 'distributed', 'kubernetes',
105
- 'microservice', 'database', 'infrastructure', '算法', '架构', '优化',
109
+ 'microservice', 'database', 'infrastructure',
106
110
  ];
107
111
  const AGENTIC_KEYWORDS = [
108
112
  'read file', 'edit', 'modify', 'update', 'create file', 'execute',
109
113
  'deploy', 'install', 'npm', 'pip', 'fix', 'debug', 'verify',
110
114
  'commit', 'push', 'pull', 'merge', 'rename', 'replace', 'delete',
111
115
  'remove', 'add', 'change', 'move', 'refactor', 'migrate',
112
- '编辑', '修改', '部署', '安装', '修复', '调试',
113
- '更新', '替换', '删除', '添加', '提交', '改',
114
116
  ];
115
117
  // URL patterns that signal agentic/coding tasks
116
118
  const AGENTIC_URL_PATTERNS = [
@@ -223,7 +225,7 @@ function classifyRequest(prompt, tokenCount) {
223
225
  // Imperative verbs (build, create, implement, etc.)
224
226
  const imperativeMatches = countMatches(prompt, [
225
227
  'build', 'create', 'implement', 'design', 'develop', 'write', 'make',
226
- 'generate', 'construct', '构建', '创建', '实现', '设计', '开发'
228
+ 'generate', 'construct',
227
229
  ]);
228
230
  if (imperativeMatches >= 1) {
229
231
  score += 0.15;
@@ -51,4 +51,4 @@ export declare function extractArticleBlocks(tree: string): Array<{
51
51
  * This doubles as the "this is a tweet" signal in social-bot — the only link
52
52
  * inside an article block with this label shape is the permalink to the tweet.
53
53
  */
54
- export declare const X_TIME_LINK_PATTERN = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+(?:,?\\s+\\d{4})?|\\d+[smhd]|\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago|just now|now|yesterday|\\d{1,2}:\\d{2}\\s*[AaPp][Mm]|\\d{4}\u5E74\\d{1,2}\u6708\\d{1,2}\u65E5";
54
+ export declare const X_TIME_LINK_PATTERN = "(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+(?:,?\\s+\\d{4})?|\\d+[smhd]|\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago|just now|now|yesterday|\\d{1,2}:\\d{2}\\s*[AaPp][Mm]|\\d{4}\\u5e74\\d{1,2}\\u6708\\d{1,2}\\u65e5";
@@ -86,8 +86,12 @@ export function extractArticleBlocks(tree) {
86
86
  // Matches all known X time-link formats:
87
87
  // "Mar 16", "Apr 12, 2026", "5h", "5m", "2d", "30s", "just now", "now"
88
88
  // "31 seconds ago", "35 minutes ago", "4 hours ago" (full-word format)
89
- // "Yesterday", "Apr 12", "12:30 AM", "2026年4月12日" (CJK)
90
- export const X_TIME_LINK_PATTERN = '(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+(?:,?\\s+\\d{4})?|\\d+[smhd]|\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago|just now|now|yesterday|\\d{1,2}:\\d{2}\\s*[AaPp][Mm]|\\d{4}年\\d{1,2}月\\d{1,2}日';
89
+ // "Yesterday", "Apr 12", "12:30 AM"
90
+ // CJK-locale date markers (year/month/day in Chinese-locale rendering of
91
+ // tweet timestamps). Encoded via Unicode escapes to keep the source file
92
+ // ASCII-clean per the English-only-source policy:
93
+ // U+5E74 = year marker, U+6708 = month marker, U+65E5 = day marker.
94
+ export const X_TIME_LINK_PATTERN = '(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+\\d+(?:,?\\s+\\d{4})?|\\d+[smhd]|\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago|just now|now|yesterday|\\d{1,2}:\\d{2}\\s*[AaPp][Mm]|\\d{4}\\u5e74\\d{1,2}\\u6708\\d{1,2}\\u65e5';
91
95
  function escapeRegex(s) {
92
96
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
93
97
  }
@@ -825,7 +825,7 @@ export const predictionMarketCapability = {
825
825
  'Default routing: ' +
826
826
  '"is there a market on X anywhere" → searchAll. ' +
827
827
  '"top wallets / who is profitable / who should I follow on Polymarket" → leaderboard. ' +
828
- '"analyze this wallet / can I copy this trader / 复制交易 / show me their P&L AND positions" → run walletProfile + walletPnl + walletPositions IN PARALLEL with the same address — three $0.005 calls give the full picture for $0.015. Do not Bash-curl Polymarket directly; the agent has paid tools for this. ' +
828
+ '"analyze this wallet / can I copy this trader / show me their P&L AND positions" → run walletProfile + walletPnl + walletPositions IN PARALLEL with the same address — three $0.005 calls give the full picture for $0.015. Do not Bash-curl Polymarket directly; the agent has paid tools for this. ' +
829
829
  '"what are smart traders betting on right now" → smartActivity. ' +
830
830
  '"show smart money on this specific Polymarket market" → smartMoney with conditionId. ' +
831
831
  '"should I bet on X" → run searchPolymarket + searchKalshi in parallel and compare implied probabilities — divergence is the signal.',
@@ -13,14 +13,14 @@ import { detectProduct } from '../social/ai.js';
13
13
  import { loadConfig, isConfigReady } from '../social/config.js';
14
14
  import { browserPool } from '../social/browser-pool.js';
15
15
  // ─── Intent detection (code-level, not LLM-level) ──────────────────────────
16
- // When the user asks "check my @handle mentions/notifications/互动",
17
- // the tool itself routes to x.com/notifications. No LLM judgment needed.
16
+ // When the user asks "check my @handle mentions/notifications", the tool
17
+ // itself routes to x.com/notifications. English-only keyword fast-path;
18
+ // the LLM-level classifier handles non-English queries before this point.
18
19
  const NOTIFICATION_KEYWORDS = [
19
20
  'notification', 'notifications',
20
21
  'mention', 'mentions', 'mentioned',
21
22
  'reply', 'replies',
22
23
  'interact', 'interaction', 'interactions',
23
- '互动', '通知', '提及', '回复', '看看',
24
24
  'check my', 'my account', 'my x',
25
25
  'to:', 'from:', '@',
26
26
  ];
@@ -50,7 +50,7 @@ export const walletCapability = {
50
50
  spec: {
51
51
  name: 'Wallet',
52
52
  description: 'Read Franklin\'s wallet status — chain, address, and USDC balance. ' +
53
- 'Use this for any "what\'s my balance / how much money / 钱包余额 / wallet status" question. ' +
53
+ 'Use this for any "what\'s my balance / how much money / wallet status" question. ' +
54
54
  'Cheaper and more direct than running `franklin balance` via Bash, and never costs USDC.',
55
55
  input_schema: {
56
56
  type: 'object',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.86",
3
+ "version": "3.15.88",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {