@blockrun/franklin 3.8.9 → 3.8.10

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.
@@ -96,6 +96,7 @@ export function classifyAgentError(message) {
96
96
  'workers are busy',
97
97
  'all workers are busy',
98
98
  'server busy',
99
+ 'high demand',
99
100
  'capacity',
100
101
  ])) {
101
102
  return {
@@ -27,6 +27,13 @@ export interface LLMClientOptions {
27
27
  chain: Chain;
28
28
  debug?: boolean;
29
29
  }
30
+ /**
31
+ * Extract the most human-readable message from an error body.
32
+ * Some gateways wrap provider errors multiple times, e.g.
33
+ * `{"error":{"message":"{\"error\":{\"message\":\"...\"}}"}}`.
34
+ * Peel those layers so the UI doesn't show raw nested JSON.
35
+ */
36
+ export declare function extractApiErrorMessage(errorBody: string): string;
30
37
  /**
31
38
  * Apply Anthropic prompt caching using the `system_and_3` strategy.
32
39
  * Pattern from nousresearch/hermes-agent `agent/prompt_caching.py`.
package/dist/agent/llm.js CHANGED
@@ -6,6 +6,53 @@
6
6
  import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
7
7
  import { USER_AGENT } from '../config.js';
8
8
  import { ThinkTagStripper } from './think-tag-stripper.js';
9
+ /**
10
+ * Extract the most human-readable message from an error body.
11
+ * Some gateways wrap provider errors multiple times, e.g.
12
+ * `{"error":{"message":"{\"error\":{\"message\":\"...\"}}"}}`.
13
+ * Peel those layers so the UI doesn't show raw nested JSON.
14
+ */
15
+ export function extractApiErrorMessage(errorBody) {
16
+ const visited = new Set();
17
+ const walk = (value, depth = 0) => {
18
+ // Some providers wrap the real message under error.message as a JSON
19
+ // string, which adds another object/string hop. Allow a few layers of
20
+ // nesting without risking runaway recursion.
21
+ if (depth > 8 || visited.has(value))
22
+ return null;
23
+ if (value && (typeof value === 'object' || typeof value === 'string')) {
24
+ visited.add(value);
25
+ }
26
+ if (typeof value === 'string') {
27
+ const trimmed = value.trim();
28
+ if (trimmed) {
29
+ try {
30
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
31
+ const parsed = JSON.parse(trimmed);
32
+ const nested = walk(parsed, depth + 1);
33
+ if (nested)
34
+ return nested;
35
+ }
36
+ }
37
+ catch { /* plain string — use as-is below */ }
38
+ }
39
+ return trimmed || null;
40
+ }
41
+ if (!value || typeof value !== 'object')
42
+ return null;
43
+ const obj = value;
44
+ for (const key of ['error', 'message', 'detail', 'reason']) {
45
+ if (key in obj) {
46
+ const nested = walk(obj[key], depth + 1);
47
+ if (nested)
48
+ return nested;
49
+ }
50
+ }
51
+ return null;
52
+ };
53
+ const extracted = walk(errorBody) ?? errorBody;
54
+ return extracted.replace(/\s+/g, ' ').trim();
55
+ }
9
56
  // ─── Anthropic Prompt Caching ─────────────────────────────────────────────
10
57
  /**
11
58
  * Apply Anthropic prompt caching using the `system_and_3` strategy.
@@ -265,13 +312,7 @@ export class ModelClient {
265
312
  }
266
313
  if (!response.ok) {
267
314
  const errorBody = await response.text().catch(() => 'unknown error');
268
- // Extract human-readable message from JSON error bodies ({"error":{"message":"..."}})
269
- let message = errorBody;
270
- try {
271
- const parsed = JSON.parse(errorBody);
272
- message = parsed?.error?.message || parsed?.message || errorBody;
273
- }
274
- catch { /* not JSON — use raw text */ }
315
+ const message = extractApiErrorMessage(errorBody);
275
316
  yield {
276
317
  kind: 'error',
277
318
  payload: { status: response.status, message },
@@ -346,6 +346,9 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
346
346
  activeTools.add(activateToolCap.spec.name);
347
347
  const allToolDefs = [...capabilityMap.values()].map(c => c.spec);
348
348
  const buildCallToolDefs = () => dynamicTools ? allToolDefs.filter(t => activeTools.has(t.name)) : allToolDefs;
349
+ const buildActiveCapabilityMap = () => dynamicTools
350
+ ? new Map([...capabilityMap.entries()].filter(([name]) => activeTools.has(name)))
351
+ : capabilityMap;
349
352
  const maxTurns = config.maxTurns ?? 15;
350
353
  const workDir = config.workingDir ?? process.cwd();
351
354
  const permissions = new PermissionManager(config.permissionMode ?? 'default', config.permissionPromptFn);
@@ -649,8 +652,9 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
649
652
  let usage;
650
653
  let stopReason;
651
654
  // Create streaming executor for concurrent tool execution
655
+ const activeCapabilityMap = buildActiveCapabilityMap();
652
656
  const streamExec = new StreamingExecutor({
653
- handlers: capabilityMap,
657
+ handlers: activeCapabilityMap,
654
658
  scope: {
655
659
  workingDir: workDir,
656
660
  abortSignal: abort.signal,
package/dist/banner.js CHANGED
@@ -82,6 +82,21 @@ function padVisible(s, targetWidth) {
82
82
  return s + '\x1b[0m' + ' '.repeat(targetWidth - current);
83
83
  }
84
84
  export function printBanner(version) {
85
+ const style = process.env.FRANKLIN_BANNER?.toLowerCase();
86
+ if (style === 'full' || style === 'legacy') {
87
+ printLegacyBanner(version);
88
+ return;
89
+ }
90
+ printCompactBanner(version);
91
+ }
92
+ function printCompactBanner(version) {
93
+ const title = chalk.bold.hex(GOLD_START)('FRANKLIN');
94
+ const meta = chalk.dim(` · blockrun.ai · v${version}`);
95
+ console.log(`${title}${meta}`);
96
+ console.log(chalk.dim('The AI agent with a wallet'));
97
+ console.log('');
98
+ }
99
+ function printLegacyBanner(version) {
85
100
  const termWidth = process.stdout.columns ?? 80;
86
101
  const useSideBySide = termWidth >= MIN_WIDTH_FOR_PORTRAIT;
87
102
  if (useSideBySide) {
package/dist/index.js CHANGED
@@ -265,7 +265,7 @@ if (firstArg === 'solana' || firstArg === 'base') {
265
265
  saveChain(firstArg);
266
266
  const startOpts = parseStartFlags(args, 1);
267
267
  await startCommand(startOpts);
268
- process.exit(0);
268
+ process.exit(process.exitCode ?? 0);
269
269
  }
270
270
  else if (!firstArg || firstArg.startsWith('-')) {
271
271
  if (hasAnyFlag(args, HELP_FLAGS) && hasStartOnlyFlag(args)) {
@@ -281,7 +281,7 @@ else if (!firstArg || firstArg.startsWith('-')) {
281
281
  // No subcommand or only flags — treat as 'start' with flags
282
282
  const startOpts = parseStartFlags(args, 0);
283
283
  await startCommand(startOpts);
284
- process.exit(0);
284
+ process.exit(process.exitCode ?? 0);
285
285
  }
286
286
  else {
287
287
  program.parse();
package/dist/ui/app.js CHANGED
@@ -39,12 +39,16 @@ function useTerminalSize() {
39
39
  }
40
40
  function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queued, queuedCount, focused, busy, contextPct, vimMode, onVimModeChange }) {
41
41
  const { cols } = useTerminalSize();
42
+ // Avoid drawing right up to the terminal edge. Several terminals auto-wrap
43
+ // a full-width border glyph onto the next row, which leaves "ghost" top
44
+ // borders behind on re-render after errors / status changes.
45
+ const boxWidth = Math.max(20, cols - 2);
42
46
  const placeholder = busy
43
47
  ? (queued
44
48
  ? `⏎ ${queuedCount ?? 1} queued: ${queued.slice(0, 40)}`
45
49
  : 'Working...')
46
50
  : 'Type a message...';
47
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderDimColor: true, paddingX: 1, width: cols, children: [busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange })) : (_jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false })) })] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), " \u00B7 ", balance, sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (() => {
51
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { borderStyle: "round", borderDimColor: true, paddingX: 1, width: boxWidth, children: [busy && !input ? _jsxs(Text, { color: "yellow", children: [_jsx(Spinner, { type: "dots" }), " "] }) : null, _jsx(Box, { flexGrow: 1, children: vimMode ? (_jsx(VimInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false, showMode: true, onModeChange: onVimModeChange })) : (_jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: placeholder, focus: focused !== false })) })] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { dimColor: true, children: [busy ? _jsx(Text, { color: "yellow", children: _jsx(Spinner, { type: "dots" }) }) : null, busy ? ' ' : '', shortModelName(model), " \u00B7 ", balance, sessionCost > 0.00001 ? _jsxs(Text, { color: "yellow", children: [" -$", sessionCost.toFixed(4)] }) : '', contextPct !== undefined && contextPct > 0 ? (() => {
48
52
  // Visual context bar: ▓▓▓▓▓▓░░░░ 75%
49
53
  const filled = Math.round(contextPct / 10);
50
54
  const empty = 10 - filled;
@@ -52,6 +56,28 @@ function InputBox({ input, setInput, onSubmit, model, balance, sessionCost, queu
52
56
  return (_jsxs(Text, { children: [' ', _jsx(Text, { color: barColor, children: '▓'.repeat(filled) }), _jsx(Text, { dimColor: true, children: '░'.repeat(empty) }), _jsxs(Text, { color: barColor, children: [' ', contextPct, "%"] })] }));
53
57
  })() : null, (queuedCount ?? 0) > 0 ? _jsxs(Text, { color: "cyan", children: [" \u00B7 ", queuedCount, " queued"] }) : null, ' · esc'] }) })] }));
54
58
  }
59
+ function formatAgentErrorForDisplay(error) {
60
+ const lines = error.split('\n').map((line) => line.trim()).filter(Boolean);
61
+ const tipIndex = lines.findIndex((line) => /^tip:/i.test(line));
62
+ const mainLines = tipIndex >= 0 ? lines.slice(0, tipIndex) : lines;
63
+ const tipLines = tipIndex >= 0 ? lines.slice(tipIndex) : [];
64
+ let main = mainLines.join(' ').replace(/\s+/g, ' ').trim();
65
+ let tip = tipLines.join(' ').replace(/\s+/g, ' ').trim();
66
+ const labelMatch = /^\[([^\]]+)\]\s*/.exec(main);
67
+ const label = labelMatch?.[1];
68
+ if (labelMatch)
69
+ main = main.slice(labelMatch[0].length).trim();
70
+ if (tip)
71
+ tip = tip.replace(/^tip:\s*/i, '');
72
+ const out = ['**Request failed**'];
73
+ if (label)
74
+ out.push(`- Type: ${label}`);
75
+ if (main)
76
+ out.push(`- Message: ${main}`);
77
+ if (tip)
78
+ out.push(`- Tip: ${tip}`);
79
+ return out.join('\n');
80
+ }
55
81
  function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain, startWithPicker, onSubmit, onModelChange, onAbort, onExit, }) {
56
82
  const { exit } = useApp();
57
83
  const [input, setInput] = useState('');
@@ -635,7 +661,7 @@ function RunCodeApp({ initialModel, workDir, walletAddress, walletBalance, chain
635
661
  setStreamText('');
636
662
  }
637
663
  if (event.reason === 'error' && event.error) {
638
- commitResponse(`Error: ${event.error}`, turnTokensRef.current, turnCostRef.current);
664
+ commitResponse(formatAgentErrorForDisplay(event.error), turnTokensRef.current, turnCostRef.current);
639
665
  showStatus('Turn failed', 'error', 5000);
640
666
  }
641
667
  else if (event.reason === 'aborted') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.8.9",
3
+ "version": "3.8.10",
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": {