@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 +12 -0
- package/dist/agent/continuation.d.ts +17 -0
- package/dist/agent/continuation.js +35 -0
- package/dist/agent/loop.js +49 -0
- package/dist/agent/retry-policy.d.ts +19 -0
- package/dist/agent/retry-policy.js +36 -0
- 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;
|
|
@@ -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
|
+
}
|
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.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",
|