@blockrun/franklin 3.8.40 → 3.8.42

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;
@@ -494,13 +497,23 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
494
497
  // negative number disables the cap; a non-numeric / unparseable value
495
498
  // is treated as a typo and falls back to the safe default rather than
496
499
  // silently removing the wallet guard.
500
+ //
501
+ // Default raised from $0.25 → $1.00 in v3.8.42 — the original ceiling
502
+ // dated from when Franklin was mostly chat. Real workloads (multi-stage
503
+ // dashboard scaffolds on sonnet, image-to-image edits, research-heavy
504
+ // turns) routinely land in the $0.20–$0.80 range on a single legit
505
+ // prompt. $1.00 is still meaningful as a runaway-protection guardrail
506
+ // (catches the kind of failure v3.8.41's retry-policy was built for)
507
+ // but doesn't impose a friction tax on every multi-stage task. Users
508
+ // who liked the old ceiling can opt back in via the config.
509
+ const TURN_SPEND_DEFAULT_USD = 1.0;
497
510
  const turnSpendCap = (() => {
498
511
  const raw = loadConfig()['max-turn-spend-usd'];
499
512
  if (raw == null)
500
- return 0.25;
513
+ return TURN_SPEND_DEFAULT_USD;
501
514
  const parsed = Number(raw);
502
515
  if (!Number.isFinite(parsed))
503
- return 0.25; // typo → keep default
516
+ return TURN_SPEND_DEFAULT_USD; // typo → keep default
504
517
  if (parsed <= 0)
505
518
  return Infinity; // explicit opt-out
506
519
  return parsed;
@@ -897,6 +910,52 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
897
910
  // ── Transient error recovery (network, rate limit, server errors) ──
898
911
  // Respect per-error maxRetries (e.g., 529/overloaded gets only 3 retries)
899
912
  const effectiveMaxRetries = classified.maxRetries ?? MAX_RECOVERY_ATTEMPTS;
913
+ if (classified.category === 'timeout' && recoveryAttempts < effectiveMaxRetries) {
914
+ const retryDecision = evaluateTimeoutRetry(history, resolvedModel);
915
+ if (!retryDecision.retry) {
916
+ // Before surfacing the timeout error, try auto-continuation:
917
+ // for tasks too big to finish in one streaming turn (multi-file
918
+ // scaffolds, dashboard builds), inject a chunking-instruction
919
+ // prompt and let the model take one narrow next step. Capped at
920
+ // MAX_AUTO_CONTINUATIONS_PER_TURN — if the chunked attempt also
921
+ // times out, fall through to the normal error path.
922
+ if (!isAutoContinuationDisabled() &&
923
+ autoContinuationCount < MAX_AUTO_CONTINUATIONS_PER_TURN) {
924
+ autoContinuationCount++;
925
+ recoveryAttempts++;
926
+ const continuationPrompt = buildContinuationPrompt();
927
+ history.push(continuationPrompt);
928
+ persistSessionMessage(continuationPrompt);
929
+ if (config.debug) {
930
+ console.error(`[franklin] Stream timeout on ${resolvedModel} — auto-continuing with chunked-task prompt`);
931
+ }
932
+ onEvent({
933
+ kind: 'text_delta',
934
+ text: '\n*Task too big for one streaming turn — auto-continuing with a smaller chunk...*\n',
935
+ });
936
+ lastSessionActivity = Date.now();
937
+ continue;
938
+ }
939
+ const tokenText = retryDecision.estimatedInputTokens.toLocaleString();
940
+ const costText = retryDecision.estimatedReplayCostUsd > 0
941
+ ? ` and at least $${retryDecision.estimatedReplayCostUsd.toFixed(4)} in input charges`
942
+ : '';
943
+ if (config.debug) {
944
+ console.error(`[franklin] Timeout retry skipped for ${resolvedModel}: ` +
945
+ `~${tokenText} input tokens, replayCost=$${retryDecision.estimatedReplayCostUsd.toFixed(4)}`);
946
+ }
947
+ onEvent({
948
+ kind: 'turn_done',
949
+ reason: 'error',
950
+ error: `[${classified.label}] ${errMsg}\n` +
951
+ `Tip: Automatic retry skipped to avoid re-sending ~${tokenText} input tokens${costText}. ` +
952
+ 'Use /retry if you want to run another full attempt.',
953
+ });
954
+ lastSessionActivity = Date.now();
955
+ persistSessionMeta();
956
+ break;
957
+ }
958
+ }
900
959
  if (classified.isTransient && recoveryAttempts < effectiveMaxRetries) {
901
960
  recoveryAttempts++;
902
961
  const backoffMs = getBackoffDelay(recoveryAttempts);
@@ -973,7 +1032,8 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
973
1032
  if (turnSpend > MAX_TURN_SPEND_USD) {
974
1033
  onEvent({
975
1034
  kind: 'text_delta',
976
- text: `\n\n⚠️ Turn spend limit reached ($${turnSpend.toFixed(3)} > $${MAX_TURN_SPEND_USD}). Stopping to protect your wallet. Try again with a clearer prompt or a different model.\n`,
1035
+ text: `\n\n⚠️ Turn spend limit reached ($${turnSpend.toFixed(3)} > $${MAX_TURN_SPEND_USD}). Stopping to protect your wallet.\n` +
1036
+ `Raise the cap with \`franklin config set max-turn-spend-usd 2.0\` (or \`0\` to disable), then \`/retry\`.\n`,
977
1037
  });
978
1038
  onEvent({ kind: 'turn_done', reason: 'budget' });
979
1039
  break;
@@ -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
+ }
@@ -7,10 +7,12 @@ export interface AppConfig {
7
7
  'permission-mode'?: string;
8
8
  'max-turns'?: string;
9
9
  /**
10
- * Hard per-turn spend ceiling in USD (default $0.25). Numeric string,
11
- * e.g. "0.5" or "2". Set to "0" to disable the cap. The agent loop
12
- * stops a turn the moment cumulative cost crosses this threshold,
13
- * preventing a runaway model + tool combo from draining the wallet.
10
+ * Hard per-turn spend ceiling in USD (default $1.00 as of v3.8.42).
11
+ * Numeric string, e.g. "0.5" or "2". Set to "0" to disable the cap.
12
+ * The agent loop stops a turn the moment cumulative cost crosses this
13
+ * threshold, preventing a runaway model + tool combo from draining the
14
+ * wallet. Earlier versions used $0.25, which routinely fired on legit
15
+ * multi-stage tasks (dashboard scaffolds, image-to-image edits).
14
16
  */
15
17
  'max-turn-spend-usd'?: string;
16
18
  'auto-compact'?: string;
@@ -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.42",
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",