@blockrun/franklin 3.15.97 → 3.15.99
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 +7 -6
- package/dist/agent/commands.js +19 -2
- package/dist/agent/compact.js +30 -1
- package/dist/agent/loop.js +5 -1
- package/dist/agent/reduce.js +21 -1
- package/dist/agent/tokens.js +43 -7
- package/dist/banner.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
<br>
|
|
4
4
|
|
|
5
|
-
<img src="assets/terminal-banner.png" alt="Franklin terminal" width="680">
|
|
5
|
+
<img src="assets/terminal-banner.png" alt="Franklin Agent terminal" width="680">
|
|
6
6
|
|
|
7
7
|
<br><br>
|
|
8
8
|
|
|
9
|
+
<h1>Franklin Agent</h1>
|
|
9
10
|
<h3>The AI agent with a wallet.</h3>
|
|
10
11
|
|
|
11
12
|
<p>
|
|
12
|
-
Other agents write code. Franklin writes code <em>and spends money</em> to get things done.<br>
|
|
13
|
+
Other agents write code. Franklin Agent writes code <em>and spends money</em> to get things done.<br>
|
|
13
14
|
One wallet. Every model. Every paid API. Pay only for outcomes — not subscriptions.
|
|
14
15
|
</p>
|
|
15
16
|
|
|
@@ -41,14 +42,14 @@
|
|
|
41
42
|
|
|
42
43
|
## The pitch in one paragraph
|
|
43
44
|
|
|
44
|
-
Most coding agents write code. Franklin writes code **and spends money to get the job done**. It holds a USDC wallet, picks the best model per task from 55+ providers, purchases trading data, generates images, pays for web search — all autonomously. You state an outcome and set a budget. Franklin decides what to call, what to pay for, and when to stop. Every paid action routes through the [x402](https://x402.org) micropayment protocol and settles against your own wallet. No subscriptions. No API keys. No account. The wallet is the identity.
|
|
45
|
+
Most coding agents write code. Franklin Agent writes code **and spends money to get the job done**. It holds a USDC wallet, picks the best model per task from 55+ providers, purchases trading data, generates images, pays for web search — all autonomously. You state an outcome and set a budget. Franklin Agent decides what to call, what to pay for, and when to stop. Every paid action routes through the [x402](https://x402.org) micropayment protocol and settles against your own wallet. No subscriptions. No API keys. No account. The wallet is the identity.
|
|
45
46
|
|
|
46
47
|
Built by the [BlockRun](https://blockrun.ai) team. Apache-2.0. TypeScript. Ships as one npm package.
|
|
47
48
|
|
|
48
49
|
> **YOPO — You Only Pay Outcome**
|
|
49
50
|
>
|
|
50
51
|
> Not a subscription (pay for access). Not a generic pay-per-call (pay for trying).
|
|
51
|
-
> You pay only for the work Franklin delivers. Provider cost + 5%, settled per action
|
|
52
|
+
> You pay only for the work Franklin Agent delivers. Provider cost + 5%, settled per action
|
|
52
53
|
> in USDC. No monthly fees. No rate limits. No overdraft.
|
|
53
54
|
|
|
54
55
|
---
|
|
@@ -87,13 +88,13 @@ Free models work immediately. Paid models, image gen, and video gen activate the
|
|
|
87
88
|
|
|
88
89
|
## YOPO
|
|
89
90
|
|
|
90
|
-
**You Only Pay Outcome.** This is Franklin's pricing model, and it is the opposite of almost every other AI product you use.
|
|
91
|
+
**You Only Pay Outcome.** This is Franklin Agent's pricing model, and it is the opposite of almost every other AI product you use.
|
|
91
92
|
|
|
92
93
|
| | You pay for... | Result |
|
|
93
94
|
| ----------------------- | -------------------------------------------- | ------------------------------------ |
|
|
94
95
|
| AI subscription | Access. Paid whether you use it or not. | $20–200/month, rate-limited. |
|
|
95
96
|
| Pay-per-call (OpenAI API, etc.) | Every attempt — even failed ones. | Hidden cost from retries, dead ends. |
|
|
96
|
-
| **Franklin (YOPO)** | **The outcome.** Each signed micropayment. | **Provider cost + 5%. No more.** |
|
|
97
|
+
| **Franklin Agent (YOPO)** | **The outcome.** Each signed micropayment. | **Provider cost + 5%. No more.** |
|
|
97
98
|
|
|
98
99
|
Three consequences fall out of this:
|
|
99
100
|
|
package/dist/agent/commands.js
CHANGED
|
@@ -177,8 +177,25 @@ const DIRECT_COMMANDS = {
|
|
|
177
177
|
if ('type' in part) {
|
|
178
178
|
if (part.type === 'tool_result') {
|
|
179
179
|
toolResults++;
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
// Sibling of PR #54's tokens.ts fix: image base64 must NOT
|
|
181
|
+
// count toward the displayed char total — `/context` would
|
|
182
|
+
// otherwise show ~70K chars per attached image and confuse
|
|
183
|
+
// the user about why the ring is at 1% but "total tool
|
|
184
|
+
// chars" is huge.
|
|
185
|
+
if (typeof part.content === 'string') {
|
|
186
|
+
totalToolChars += part.content.length;
|
|
187
|
+
}
|
|
188
|
+
else if (Array.isArray(part.content)) {
|
|
189
|
+
for (const block of part.content) {
|
|
190
|
+
const t = block.type;
|
|
191
|
+
if (t === 'text') {
|
|
192
|
+
totalToolChars += (block.text || '').length;
|
|
193
|
+
}
|
|
194
|
+
else if (t === 'image') {
|
|
195
|
+
totalToolChars += 6000; // ~1500 tokens × 4 chars/tok
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
182
199
|
}
|
|
183
200
|
if (part.type === 'thinking')
|
|
184
201
|
thinkingBlocks++;
|
package/dist/agent/compact.js
CHANGED
|
@@ -385,7 +385,36 @@ function formatForSummarization(messages) {
|
|
|
385
385
|
textParts.push(`[Called tool: ${part.name}(${JSON.stringify(part.input).slice(0, 200)})]`);
|
|
386
386
|
break;
|
|
387
387
|
case 'tool_result': {
|
|
388
|
-
|
|
388
|
+
// Sibling of PR #54's tokens.ts fix: when content is a
|
|
389
|
+
// [{text}, {image}] array, JSON.stringify dumps base64
|
|
390
|
+
// bytes into the summary prompt — bloats the summarizer's
|
|
391
|
+
// input and produces a useless preview ("[Tool result:
|
|
392
|
+
// [{\"type\":\"text\",\"text\":\"Image file: ...\"},{\"type\":\"image\",\"source\":{\"type\":\"base64\",\"data\":\"...").
|
|
393
|
+
// Build the preview from text blocks only; mark images
|
|
394
|
+
// explicitly so the summarizer knows they exist.
|
|
395
|
+
let content;
|
|
396
|
+
if (typeof part.content === 'string') {
|
|
397
|
+
content = part.content;
|
|
398
|
+
}
|
|
399
|
+
else if (Array.isArray(part.content)) {
|
|
400
|
+
const pieces = [];
|
|
401
|
+
let imageCount = 0;
|
|
402
|
+
for (const block of part.content) {
|
|
403
|
+
const t = block.type;
|
|
404
|
+
if (t === 'text') {
|
|
405
|
+
pieces.push(block.text || '');
|
|
406
|
+
}
|
|
407
|
+
else if (t === 'image') {
|
|
408
|
+
imageCount++;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (imageCount > 0)
|
|
412
|
+
pieces.push(`[${imageCount} image block${imageCount > 1 ? 's' : ''}]`);
|
|
413
|
+
content = pieces.join(' ');
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
content = JSON.stringify(part.content);
|
|
417
|
+
}
|
|
389
418
|
const truncated = content.length > 500 ? content.slice(0, 500) + '...' : content;
|
|
390
419
|
textParts.push(`[Tool result${part.is_error ? ' (ERROR)' : ''}: ${truncated}]`);
|
|
391
420
|
break;
|
package/dist/agent/loop.js
CHANGED
|
@@ -1585,7 +1585,11 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1585
1585
|
tier: routingTier,
|
|
1586
1586
|
confidence: routingConfidence,
|
|
1587
1587
|
savings: routingSavings,
|
|
1588
|
-
|
|
1588
|
+
// Preserve sub-1% precision: a fresh session at 0.4% would
|
|
1589
|
+
// round to 0 and freeze the renderer's context ring until the
|
|
1590
|
+
// conversation grows past ~1k tokens. Match `/context`'s
|
|
1591
|
+
// `.toFixed(1)` fidelity.
|
|
1592
|
+
contextPct: Math.round(contextUsagePct * 10) / 10,
|
|
1589
1593
|
});
|
|
1590
1594
|
// Record usage for stats tracking (franklin stats command).
|
|
1591
1595
|
// Prefer the real x402 charge from the gateway over a token-catalog
|
package/dist/agent/reduce.js
CHANGED
|
@@ -458,7 +458,27 @@ function estimateChars(history) {
|
|
|
458
458
|
if (p.type === 'text')
|
|
459
459
|
total += p.text.length;
|
|
460
460
|
else if (p.type === 'tool_result') {
|
|
461
|
-
|
|
461
|
+
// Sibling of PR #54's tokens.ts fix: JSON.stringify-ing a
|
|
462
|
+
// [{text}, {image}] array counts the base64 `data` field as
|
|
463
|
+
// text and inflates the char count by ~70K per image. That
|
|
464
|
+
// skews every reduce-pass decision (when to dedupe, when to
|
|
465
|
+
// collapse) toward "save chars by collapsing the image-
|
|
466
|
+
// bearing result" — exactly wrong. Walk blocks instead.
|
|
467
|
+
if (typeof p.content === 'string') {
|
|
468
|
+
total += p.content.length;
|
|
469
|
+
}
|
|
470
|
+
else if (Array.isArray(p.content)) {
|
|
471
|
+
for (const block of p.content) {
|
|
472
|
+
if (block.type === 'text') {
|
|
473
|
+
total += (block.text || '').length;
|
|
474
|
+
}
|
|
475
|
+
else if (block.type === 'image') {
|
|
476
|
+
// Mirror tokens.ts: image ≈ 1500 tokens ≈ ~6K chars
|
|
477
|
+
// at the 4-chars/token rule estimateTokens uses.
|
|
478
|
+
total += 6000;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
462
482
|
}
|
|
463
483
|
else if (p.type === 'tool_use') {
|
|
464
484
|
total += JSON.stringify(p.input).length;
|
package/dist/agent/tokens.js
CHANGED
|
@@ -45,6 +45,11 @@ export function updateActualTokens(inputTokens, outputTokens, messageCount) {
|
|
|
45
45
|
* More accurate than pure estimation because it's grounded in actual API counts.
|
|
46
46
|
*/
|
|
47
47
|
export function getAnchoredTokenCount(history) {
|
|
48
|
+
// The model that just billed input — used as the denominator below.
|
|
49
|
+
// _currentModel is set per-turn by setEstimationModel(), so it reflects
|
|
50
|
+
// whatever the router actually resolved (not just config.model, which
|
|
51
|
+
// may be a routing profile like blockrun/auto).
|
|
52
|
+
const contextWindow = _currentModel ? getContextWindow(_currentModel) : 200_000;
|
|
48
53
|
if (lastApiInputTokens > 0 && lastApiMessageCount > 0 && history.length >= lastApiMessageCount) {
|
|
49
54
|
// Sanity check: if history was mutated (compaction, micro-compact), anchor may be stale.
|
|
50
55
|
// Detect by checking if new messages were only appended (length grew), not if content changed.
|
|
@@ -60,17 +65,18 @@ export function getAnchoredTokenCount(history) {
|
|
|
60
65
|
return {
|
|
61
66
|
estimated: total,
|
|
62
67
|
apiAnchored: true,
|
|
63
|
-
contextUsagePct:
|
|
68
|
+
contextUsagePct: (total / contextWindow) * 100,
|
|
64
69
|
};
|
|
65
70
|
}
|
|
66
71
|
// Too much growth — anchor is unreliable, fall through to estimation
|
|
67
72
|
resetTokenAnchor();
|
|
68
73
|
}
|
|
69
74
|
// No anchor — pure estimation
|
|
75
|
+
const est = estimateHistoryTokens(history);
|
|
70
76
|
return {
|
|
71
|
-
estimated:
|
|
77
|
+
estimated: est,
|
|
72
78
|
apiAnchored: false,
|
|
73
|
-
contextUsagePct:
|
|
79
|
+
contextUsagePct: (est / contextWindow) * 100,
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
82
|
/**
|
|
@@ -115,10 +121,40 @@ function estimateContentPartTokens(part) {
|
|
|
115
121
|
// +16 tokens for tool_use framing (type, id, name fields, JSON structure)
|
|
116
122
|
return 16 + estimateTokens(part.name) + estimateTokens(JSON.stringify(part.input), 2);
|
|
117
123
|
case 'tool_result': {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
// String content: count as text directly.
|
|
125
|
+
if (typeof part.content === 'string') {
|
|
126
|
+
return estimateTokens(part.content, 2);
|
|
127
|
+
}
|
|
128
|
+
// Array content: sum block-by-block. CRITICAL: image blocks must
|
|
129
|
+
// NOT go through JSON.stringify — their base64 `data` field would
|
|
130
|
+
// be tokenized as text (a 100KB image → ~70k phantom tokens),
|
|
131
|
+
// which is what made the context ring read ~86% on a 2-image chat
|
|
132
|
+
// and triggered premature /compact loops. Anthropic actually
|
|
133
|
+
// bills (w*h)/750 per image, ≈1100-1500 for typical sizes; a flat
|
|
134
|
+
// 1500-token estimate is close enough without needing to decode
|
|
135
|
+
// the image dimensions client-side.
|
|
136
|
+
let total = 0;
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
const blocks = part.content;
|
|
139
|
+
for (const block of blocks) {
|
|
140
|
+
const blockType = block?.type;
|
|
141
|
+
if (blockType === 'text') {
|
|
142
|
+
total += estimateTokens(block?.text ?? '', 2);
|
|
143
|
+
}
|
|
144
|
+
else if (blockType === 'image') {
|
|
145
|
+
total += 1500;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// Unknown block — stringify minus any nested base64 data field
|
|
149
|
+
// to avoid the same blow-up for future block kinds.
|
|
150
|
+
const sanitized = { ...block };
|
|
151
|
+
if (sanitized?.source && typeof sanitized.source === 'object' && sanitized.source.data) {
|
|
152
|
+
sanitized.source = { ...sanitized.source, data: '<bytes>' };
|
|
153
|
+
}
|
|
154
|
+
total += estimateTokens(JSON.stringify(sanitized), 2);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return total;
|
|
122
158
|
}
|
|
123
159
|
case 'thinking':
|
|
124
160
|
return estimateTokens(part.thinking);
|
package/dist/banner.js
CHANGED
|
@@ -113,7 +113,7 @@ export function printBanner(version) {
|
|
|
113
113
|
}
|
|
114
114
|
function printCompactBanner(version) {
|
|
115
115
|
const title = chalk.bold.hex(GOLD_START)('FRANKLIN');
|
|
116
|
-
const meta = chalk.dim(` · blockrun.ai · v${version}`);
|
|
116
|
+
const meta = chalk.dim(` Agent · blockrun.ai · v${version}`);
|
|
117
117
|
console.log(`${title}${meta}`);
|
|
118
118
|
console.log(chalk.dim('The AI agent with a wallet'));
|
|
119
119
|
console.log('');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blockrun/franklin",
|
|
3
|
-
"version": "3.15.
|
|
4
|
-
"description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
|
|
3
|
+
"version": "3.15.99",
|
|
4
|
+
"description": "Franklin Agent — 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": {
|
|
7
7
|
".": {
|