@blockrun/franklin 3.8.40 → 3.8.41

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 CHANGED
@@ -69,6 +69,18 @@ franklin balance # show address + USDC balance
69
69
 
70
70
  That's it. Zero signup, zero credit card, zero phone verification. Send **$5 of USDC** to the wallet and you've unlocked every frontier model and every paid tool in the BlockRun gateway.
71
71
 
72
+ ### Prefer a GUI? Try Franklin for VS Code
73
+
74
+ The same agent ships as a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=blockrun.franklin-vscode) — chat panel, model picker, wallet balance, image / video generation, inline diff cards — all driven by the wallet you already funded for the CLI.
75
+
76
+ ```
77
+ VS Code → Extensions (Cmd+Shift+X / Ctrl+Shift+X)
78
+ → search "Franklin" → Install
79
+ → click the Franklin icon in the Activity Bar
80
+ ```
81
+
82
+ Free models work immediately. Paid models, image gen, and video gen activate the moment your wallet has USDC. The CLI and the extension share the same `~/.blockrun/` config and session history, so jumping between terminal and VS Code is seamless.
83
+
72
84
  ---
73
85
 
74
86
  ## YOPO
@@ -0,0 +1,17 @@
1
+ import type { Dialogue } from './types.js';
2
+ /**
3
+ * Cap on how many times a single user turn can auto-continue after a
4
+ * stream-timeout. One is enough: if the first chunked attempt also times
5
+ * out, the model isn't going to figure it out by recursion — fall through
6
+ * to the normal error path so the user can intervene.
7
+ */
8
+ export declare const MAX_AUTO_CONTINUATIONS_PER_TURN = 1;
9
+ export declare function isAutoContinuationDisabled(): boolean;
10
+ /**
11
+ * Built when a model call times out at the streaming layer on a task
12
+ * that's too big to complete in one turn (multi-file scaffolds,
13
+ * dashboard builds, etc.). Pushed into history before re-firing so the
14
+ * model treats the next attempt as a single narrow chunk rather than
15
+ * retrying the whole job and timing out again.
16
+ */
17
+ export declare function buildContinuationPrompt(): Dialogue;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Cap on how many times a single user turn can auto-continue after a
3
+ * stream-timeout. One is enough: if the first chunked attempt also times
4
+ * out, the model isn't going to figure it out by recursion — fall through
5
+ * to the normal error path so the user can intervene.
6
+ */
7
+ export const MAX_AUTO_CONTINUATIONS_PER_TURN = 1;
8
+ export function isAutoContinuationDisabled() {
9
+ return process.env.FRANKLIN_NO_AUTO_CONTINUE === '1';
10
+ }
11
+ /**
12
+ * Built when a model call times out at the streaming layer on a task
13
+ * that's too big to complete in one turn (multi-file scaffolds,
14
+ * dashboard builds, etc.). Pushed into history before re-firing so the
15
+ * model treats the next attempt as a single narrow chunk rather than
16
+ * retrying the whole job and timing out again.
17
+ */
18
+ export function buildContinuationPrompt() {
19
+ return {
20
+ role: 'user',
21
+ content: [
22
+ 'Your previous attempt timed out at the streaming layer — the task is too large for a single streaming turn.',
23
+ '',
24
+ 'DO:',
25
+ '- Pick ONE narrowly scoped next step (one file, one component, one logical chunk).',
26
+ '- Complete just that step in this response.',
27
+ '- Save work via Write/Edit and stop. The user will continue from there.',
28
+ '',
29
+ 'DO NOT:',
30
+ '- Re-attempt the entire original task in one shot.',
31
+ '- Make more than 3-4 tool calls before producing a result.',
32
+ '- Plan the whole multi-stage job — execute one chunk now.',
33
+ ].join('\n'),
34
+ };
35
+ }
@@ -29,6 +29,8 @@ import { shouldVerify, runVerification } from './verification.js';
29
29
  import { shouldCheckGrounding, checkGrounding, renderGroundingFollowup, buildGroundingRetryInstruction, extractMissingToolNames, } from './evaluator.js';
30
30
  import { augmentUserMessage, prefetchForIntent } from './intent-prefetch.js';
31
31
  import { analyzeTurn } from './turn-analyzer.js';
32
+ import { evaluateTimeoutRetry } from './retry-policy.js';
33
+ import { MAX_AUTO_CONTINUATIONS_PER_TURN, buildContinuationPrompt, isAutoContinuationDisabled, } from './continuation.js';
32
34
  import { createSessionId, appendToSession, updateSessionMeta, pruneOldSessions, loadSessionHistory, loadSessionMeta, } from '../session/storage.js';
33
35
  /**
34
36
  * Atomically replace all elements in a history array.
@@ -452,6 +454,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
452
454
  onAbortReady?.(() => abort.abort());
453
455
  let loopCount = 0;
454
456
  let recoveryAttempts = 0;
457
+ let autoContinuationCount = 0;
455
458
  const MAX_RECOVERY_ATTEMPTS = 5;
456
459
  let compactFailures = 0;
457
460
  let maxTokensOverride;
@@ -897,6 +900,52 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
897
900
  // ── Transient error recovery (network, rate limit, server errors) ──
898
901
  // Respect per-error maxRetries (e.g., 529/overloaded gets only 3 retries)
899
902
  const effectiveMaxRetries = classified.maxRetries ?? MAX_RECOVERY_ATTEMPTS;
903
+ if (classified.category === 'timeout' && recoveryAttempts < effectiveMaxRetries) {
904
+ const retryDecision = evaluateTimeoutRetry(history, resolvedModel);
905
+ if (!retryDecision.retry) {
906
+ // Before surfacing the timeout error, try auto-continuation:
907
+ // for tasks too big to finish in one streaming turn (multi-file
908
+ // scaffolds, dashboard builds), inject a chunking-instruction
909
+ // prompt and let the model take one narrow next step. Capped at
910
+ // MAX_AUTO_CONTINUATIONS_PER_TURN — if the chunked attempt also
911
+ // times out, fall through to the normal error path.
912
+ if (!isAutoContinuationDisabled() &&
913
+ autoContinuationCount < MAX_AUTO_CONTINUATIONS_PER_TURN) {
914
+ autoContinuationCount++;
915
+ recoveryAttempts++;
916
+ const continuationPrompt = buildContinuationPrompt();
917
+ history.push(continuationPrompt);
918
+ persistSessionMessage(continuationPrompt);
919
+ if (config.debug) {
920
+ console.error(`[franklin] Stream timeout on ${resolvedModel} — auto-continuing with chunked-task prompt`);
921
+ }
922
+ onEvent({
923
+ kind: 'text_delta',
924
+ text: '\n*Task too big for one streaming turn — auto-continuing with a smaller chunk...*\n',
925
+ });
926
+ lastSessionActivity = Date.now();
927
+ continue;
928
+ }
929
+ const tokenText = retryDecision.estimatedInputTokens.toLocaleString();
930
+ const costText = retryDecision.estimatedReplayCostUsd > 0
931
+ ? ` and at least $${retryDecision.estimatedReplayCostUsd.toFixed(4)} in input charges`
932
+ : '';
933
+ if (config.debug) {
934
+ console.error(`[franklin] Timeout retry skipped for ${resolvedModel}: ` +
935
+ `~${tokenText} input tokens, replayCost=$${retryDecision.estimatedReplayCostUsd.toFixed(4)}`);
936
+ }
937
+ onEvent({
938
+ kind: 'turn_done',
939
+ reason: 'error',
940
+ error: `[${classified.label}] ${errMsg}\n` +
941
+ `Tip: Automatic retry skipped to avoid re-sending ~${tokenText} input tokens${costText}. ` +
942
+ 'Use /retry if you want to run another full attempt.',
943
+ });
944
+ lastSessionActivity = Date.now();
945
+ persistSessionMeta();
946
+ break;
947
+ }
948
+ }
900
949
  if (classified.isTransient && recoveryAttempts < effectiveMaxRetries) {
901
950
  recoveryAttempts++;
902
951
  const backoffMs = getBackoffDelay(recoveryAttempts);
@@ -0,0 +1,19 @@
1
+ import type { Dialogue } from './types.js';
2
+ export declare const TIMEOUT_RETRY_INPUT_TOKEN_LIMIT = 20000;
3
+ export declare const TIMEOUT_RETRY_MIN_REPLAY_COST_LIMIT_USD = 0.05;
4
+ export type TimeoutRetrySkipReason = 'estimated_cost' | 'input_tokens';
5
+ export interface TimeoutRetryDecision {
6
+ retry: boolean;
7
+ estimatedInputTokens: number;
8
+ estimatedReplayCostUsd: number;
9
+ reason?: TimeoutRetrySkipReason;
10
+ }
11
+ /**
12
+ * A timeout retry re-sends the entire conversation. For long paid contexts,
13
+ * that can cost more than the original useful work and hit the turn budget
14
+ * before the model gets another chance to finish.
15
+ */
16
+ export declare function evaluateTimeoutRetry(history: Dialogue[], model: string, opts?: {
17
+ inputTokenLimit?: number;
18
+ minReplayCostLimitUsd?: number;
19
+ }): TimeoutRetryDecision;
@@ -0,0 +1,36 @@
1
+ import { estimateCost } from '../pricing.js';
2
+ import { estimateHistoryTokens } from './tokens.js';
3
+ export const TIMEOUT_RETRY_INPUT_TOKEN_LIMIT = 20_000;
4
+ export const TIMEOUT_RETRY_MIN_REPLAY_COST_LIMIT_USD = 0.05;
5
+ /**
6
+ * A timeout retry re-sends the entire conversation. For long paid contexts,
7
+ * that can cost more than the original useful work and hit the turn budget
8
+ * before the model gets another chance to finish.
9
+ */
10
+ export function evaluateTimeoutRetry(history, model, opts) {
11
+ const inputTokenLimit = opts?.inputTokenLimit ?? TIMEOUT_RETRY_INPUT_TOKEN_LIMIT;
12
+ const minReplayCostLimitUsd = opts?.minReplayCostLimitUsd ?? TIMEOUT_RETRY_MIN_REPLAY_COST_LIMIT_USD;
13
+ const estimatedInputTokens = estimateHistoryTokens(history);
14
+ const estimatedReplayCostUsd = estimateCost(model, estimatedInputTokens, 0, 1);
15
+ if (estimatedReplayCostUsd > minReplayCostLimitUsd) {
16
+ return {
17
+ retry: false,
18
+ estimatedInputTokens,
19
+ estimatedReplayCostUsd,
20
+ reason: 'estimated_cost',
21
+ };
22
+ }
23
+ if (estimatedInputTokens > inputTokenLimit) {
24
+ return {
25
+ retry: false,
26
+ estimatedInputTokens,
27
+ estimatedReplayCostUsd,
28
+ reason: 'input_tokens',
29
+ };
30
+ }
31
+ return {
32
+ retry: true,
33
+ estimatedInputTokens,
34
+ estimatedReplayCostUsd,
35
+ };
36
+ }
@@ -14,10 +14,12 @@ export const MODEL_SHORTCUTS = {
14
14
  // Anthropic
15
15
  sonnet: 'anthropic/claude-sonnet-4.6',
16
16
  claude: 'anthropic/claude-sonnet-4.6',
17
+ 'sonnet-4.6': 'anthropic/claude-sonnet-4.6',
17
18
  opus: 'anthropic/claude-opus-4.7',
18
19
  'opus-4.7': 'anthropic/claude-opus-4.7',
19
20
  'opus-4.6': 'anthropic/claude-opus-4.6',
20
21
  haiku: 'anthropic/claude-haiku-4.5-20251001',
22
+ 'haiku-4.5': 'anthropic/claude-haiku-4.5-20251001',
21
23
  // OpenAI
22
24
  // `gpt` / `gpt5` / `gpt-5` follow the gateway's flagship — currently 5.5.
23
25
  gpt: 'openai/gpt-5.5',
@@ -39,12 +41,16 @@ export const MODEL_SHORTCUTS = {
39
41
  o1: 'openai/o1',
40
42
  // Google
41
43
  gemini: 'google/gemini-2.5-pro',
44
+ 'gemini-2.5': 'google/gemini-2.5-pro',
42
45
  flash: 'google/gemini-2.5-flash',
43
46
  'gemini-3': 'google/gemini-3.1-pro',
47
+ 'gemini-3.1': 'google/gemini-3.1-pro',
44
48
  // xAI
45
49
  grok: 'xai/grok-3',
50
+ 'grok-3': 'xai/grok-3',
46
51
  'grok-4': 'xai/grok-4-0709',
47
52
  'grok-fast': 'xai/grok-4-1-fast-reasoning',
53
+ 'grok-4.1': 'xai/grok-4-1-fast-reasoning',
48
54
  // DeepSeek
49
55
  deepseek: 'deepseek/deepseek-chat',
50
56
  r1: 'deepseek/deepseek-reasoner',
@@ -65,11 +71,14 @@ export const MODEL_SHORTCUTS = {
65
71
  devstral: 'nvidia/qwen3-coder-480b',
66
72
  // Others
67
73
  minimax: 'minimax/minimax-m2.7',
74
+ 'm2.7': 'minimax/minimax-m2.7',
68
75
  glm: 'zai/glm-5.1',
69
76
  'glm-turbo': 'zai/glm-5-turbo',
70
77
  'glm5': 'zai/glm-5.1',
71
78
  kimi: 'moonshot/kimi-k2.6',
79
+ 'k2.6': 'moonshot/kimi-k2.6',
72
80
  'kimi-k2.5': 'moonshot/kimi-k2.5',
81
+ 'k2.5': 'moonshot/kimi-k2.5',
73
82
  };
74
83
  /**
75
84
  * Resolve a model name — supports shortcuts.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.8.40",
3
+ "version": "3.8.41",
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": {
@@ -78,7 +78,8 @@
78
78
  "ink-text-input": "^6.0.0",
79
79
  "playwright-core": "^1.49.1",
80
80
  "qrcode": "^1.5.4",
81
- "react": "^19.2.4"
81
+ "react": "^19.2.4",
82
+ "viem": "^2.48.1"
82
83
  },
83
84
  "devDependencies": {
84
85
  "@types/node": "^22.0.0",