@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 +12 -0
- package/dist/agent/continuation.d.ts +17 -0
- package/dist/agent/continuation.js +35 -0
- package/dist/agent/loop.js +63 -3
- package/dist/agent/retry-policy.d.ts +19 -0
- package/dist/agent/retry-policy.js +36 -0
- package/dist/commands/config.d.ts +6 -4
- package/dist/ui/model-picker.js +9 -0
- package/package.json +3 -2
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
|
+
}
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
|
513
|
+
return TURN_SPEND_DEFAULT_USD;
|
|
501
514
|
const parsed = Number(raw);
|
|
502
515
|
if (!Number.isFinite(parsed))
|
|
503
|
-
return
|
|
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
|
|
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 $
|
|
11
|
-
* e.g. "0.5" or "2". Set to "0" to disable the cap.
|
|
12
|
-
* stops a turn the moment cumulative cost crosses this
|
|
13
|
-
* preventing a runaway model + tool combo from draining the
|
|
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;
|
package/dist/ui/model-picker.js
CHANGED
|
@@ -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.
|
|
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",
|