@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.
- package/dist/agent/error-classifier.js +1 -0
- package/dist/agent/llm.d.ts +7 -0
- package/dist/agent/llm.js +48 -7
- package/dist/agent/loop.js +5 -1
- package/dist/banner.js +15 -0
- package/dist/index.js +2 -2
- package/dist/ui/app.js +28 -2
- package/package.json +1 -1
package/dist/agent/llm.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 },
|
package/dist/agent/loop.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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(
|
|
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