@blockrun/franklin 3.15.22 → 3.15.24
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/loop.js +28 -7
- package/dist/agent/types.d.ts +1 -1
- package/dist/mcp/client.js +11 -10
- package/dist/proxy/server.js +10 -2
- package/dist/stats/test-fixture.js +14 -0
- package/dist/tools/defillama.js +2 -1
- package/dist/tools/exa.js +2 -1
- package/dist/tools/imagegen.js +2 -1
- package/dist/tools/modal.js +2 -1
- package/dist/tools/musicgen.js +2 -1
- package/dist/tools/videogen.js +2 -1
- package/dist/tools/zerox-gasless.js +2 -1
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -606,6 +606,14 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
606
606
|
const turnToolCounts = new Map(); // Per-tool-name counts this turn
|
|
607
607
|
const readFileCache = new Set(); // Files already read (dedup)
|
|
608
608
|
const MAX_TOOL_CALLS_PER_TURN = 25; // Hard cap per user turn
|
|
609
|
+
// Hard break threshold for runaways. The cap above is soft — we
|
|
610
|
+
// inject a "limit reached" tool_result once and let the model
|
|
611
|
+
// close out. If it ignores that signal and keeps calling tools,
|
|
612
|
+
// we force end the turn to prevent unbounded billing. Verified
|
|
613
|
+
// on a real user log: one turn went 25 → 100 tool calls before
|
|
614
|
+
// the loop ended via maxTurns (much later, much more expensive).
|
|
615
|
+
const HARD_TOOL_CAP = MAX_TOOL_CALLS_PER_TURN * 2;
|
|
616
|
+
let toolCapWarned = false; // Log + inject only once per turn
|
|
609
617
|
const SAME_TOOL_WARN_THRESHOLD = 3; // Warn after N calls to same tool (lowered from 5 — search loops were wasting turns)
|
|
610
618
|
// ── No-progress guardrail: kill infinite tiny-response loops ──
|
|
611
619
|
let consecutiveTinyResponses = 0; // Count of consecutive calls with <10 output tokens
|
|
@@ -1527,8 +1535,10 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1527
1535
|
});
|
|
1528
1536
|
}
|
|
1529
1537
|
}
|
|
1530
|
-
// Hard cap:
|
|
1531
|
-
|
|
1538
|
+
// Hard cap: nudge the model to stop. Inject once per turn —
|
|
1539
|
+
// re-injecting on every iteration past the cap is just noise
|
|
1540
|
+
// and clutters the model's context with repeated stop signals.
|
|
1541
|
+
if (turnToolCalls >= MAX_TOOL_CALLS_PER_TURN && !toolCapWarned) {
|
|
1532
1542
|
outcomeContent.push({
|
|
1533
1543
|
type: 'tool_result',
|
|
1534
1544
|
tool_use_id: 'guardrail-cap',
|
|
@@ -1569,11 +1579,22 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1569
1579
|
}
|
|
1570
1580
|
}
|
|
1571
1581
|
}
|
|
1572
|
-
//
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1582
|
+
// Cap signaling: warn once per turn (was firing every iteration
|
|
1583
|
+
// past the cap — verified on a real user log, one turn produced
|
|
1584
|
+
// 76 sequential warnings 25→100). Hard break at 2× cap stops a
|
|
1585
|
+
// runaway model that ignores the soft stop signal above.
|
|
1586
|
+
if (turnToolCalls >= MAX_TOOL_CALLS_PER_TURN && !toolCapWarned) {
|
|
1587
|
+
toolCapWarned = true;
|
|
1588
|
+
logger.warn(`[franklin] Tool call cap hit: ${turnToolCalls} calls this turn (soft cap ${MAX_TOOL_CALLS_PER_TURN}, hard cap ${HARD_TOOL_CAP})`);
|
|
1589
|
+
}
|
|
1590
|
+
if (turnToolCalls >= HARD_TOOL_CAP) {
|
|
1591
|
+
logger.error(`[franklin] Hard tool cap exceeded (${turnToolCalls}) — ending turn to prevent runaway`);
|
|
1592
|
+
onEvent({
|
|
1593
|
+
kind: 'text_delta',
|
|
1594
|
+
text: `\n\n⚠️ Tool call limit exceeded (${turnToolCalls}/${HARD_TOOL_CAP}). Ending turn to prevent runaway loop. Try rephrasing or use \`/model\` to switch.\n`,
|
|
1595
|
+
});
|
|
1596
|
+
onEvent({ kind: 'turn_done', reason: 'cap_exceeded' });
|
|
1597
|
+
break;
|
|
1577
1598
|
}
|
|
1578
1599
|
}
|
|
1579
1600
|
if (loopCount >= maxTurns) {
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -125,7 +125,7 @@ export interface StreamCapabilityDone {
|
|
|
125
125
|
}
|
|
126
126
|
export interface StreamTurnDone {
|
|
127
127
|
kind: 'turn_done';
|
|
128
|
-
reason: 'completed' | 'max_turns' | 'aborted' | 'error' | 'budget' | 'no_progress';
|
|
128
|
+
reason: 'completed' | 'max_turns' | 'aborted' | 'error' | 'budget' | 'no_progress' | 'cap_exceeded';
|
|
129
129
|
error?: string;
|
|
130
130
|
}
|
|
131
131
|
export interface StreamUsageInfo {
|
package/dist/mcp/client.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
7
7
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
8
|
+
import { logger } from '../logger.js';
|
|
8
9
|
// ─── Connection Management ────────────────────────────────────────────────
|
|
9
10
|
const connections = new Map();
|
|
10
11
|
/**
|
|
@@ -168,13 +169,9 @@ export async function connectMcpServers(config, debug) {
|
|
|
168
169
|
if (serverConfig.disabled)
|
|
169
170
|
continue;
|
|
170
171
|
try {
|
|
171
|
-
|
|
172
|
-
console.error(`[franklin] Connecting to MCP server: ${name}...`);
|
|
173
|
-
}
|
|
172
|
+
logger.debug(`[franklin] Connecting to MCP server: ${name}...`);
|
|
174
173
|
if (serverConfig.transport !== 'stdio') {
|
|
175
|
-
|
|
176
|
-
console.error(`[franklin] MCP HTTP transport not yet supported for ${name}`);
|
|
177
|
-
}
|
|
174
|
+
logger.debug(`[franklin] MCP HTTP transport not yet supported for ${name}`);
|
|
178
175
|
continue;
|
|
179
176
|
}
|
|
180
177
|
// Timeout: don't let a slow server block startup
|
|
@@ -182,14 +179,18 @@ export async function connectMcpServers(config, debug) {
|
|
|
182
179
|
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('connection timeout (5s)')), MCP_CONNECT_TIMEOUT));
|
|
183
180
|
const connected = await Promise.race([connectPromise, timeoutPromise]);
|
|
184
181
|
allTools.push(...connected.tools);
|
|
185
|
-
|
|
186
|
-
console.error(`[franklin] MCP ${name}: ${connected.tools.length} tools discovered`);
|
|
187
|
-
}
|
|
182
|
+
logger.info(`[franklin] MCP ${name}: ${connected.tools.length} tools discovered`);
|
|
188
183
|
}
|
|
189
184
|
catch (err) {
|
|
190
185
|
// Graceful degradation — one-line warning, continue without this server.
|
|
191
|
-
// Always
|
|
186
|
+
// Always written to franklin-debug.log so the user can post-mortem
|
|
187
|
+
// why tools went missing; ALSO printed to stderr at session boot
|
|
188
|
+
// so the user sees it in real time before the agent eats the
|
|
189
|
+
// terminal. Two separate writes (logger + stderr) is fine here —
|
|
190
|
+
// the user-visible "tool missing" notice has different timing
|
|
191
|
+
// requirements than the persistent log entry.
|
|
192
192
|
const shortMsg = err.message?.split('\n')[0]?.slice(0, 100) || 'unknown error';
|
|
193
|
+
logger.warn(`[franklin] MCP ${name}: ${shortMsg}`);
|
|
193
194
|
console.error(` ${name}: ${shortMsg} ${debug ? '' : '(--debug for details)'}`);
|
|
194
195
|
}
|
|
195
196
|
}
|
package/dist/proxy/server.js
CHANGED
|
@@ -16,6 +16,7 @@ const X_FRANKLIN_VERSION = VERSION;
|
|
|
16
16
|
// pattern!), and timestamp format — bug fixes never propagated. They
|
|
17
17
|
// were the last holdouts after the agent loop was migrated.
|
|
18
18
|
import { logger, setDebugMode } from '../logger.js';
|
|
19
|
+
import { isTestFixtureModel } from '../stats/test-fixture.js';
|
|
19
20
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
20
21
|
// 180s budget for *time-to-headers* — reasoning-class models (zai/glm-*,
|
|
21
22
|
// nemotron *-reasoning, deepseek-r*, gpt-5-codex, anthropic extended-thinking)
|
|
@@ -439,6 +440,11 @@ export function createProxy(options) {
|
|
|
439
440
|
solanaWallet,
|
|
440
441
|
timeoutMs: requestTimeoutMs,
|
|
441
442
|
}, (failedModel, status, nextModel) => {
|
|
443
|
+
// Skip test-fixture model names (slow/, mock/, test/, local/test*)
|
|
444
|
+
// — these come from in-process proxy tests with mock servers and
|
|
445
|
+
// would otherwise pollute the user's real franklin-debug.log.
|
|
446
|
+
if (isTestFixtureModel(failedModel) || isTestFixtureModel(nextModel))
|
|
447
|
+
return;
|
|
442
448
|
logger.warn(`[franklin] ⚠️ ${failedModel} returned ${status}, falling back to ${nextModel}`);
|
|
443
449
|
});
|
|
444
450
|
response = result.response;
|
|
@@ -446,7 +452,7 @@ export function createProxy(options) {
|
|
|
446
452
|
// Use the body with the correct fallback model for payment
|
|
447
453
|
body = result.bodyUsed;
|
|
448
454
|
usedFallback = result.fallbackUsed;
|
|
449
|
-
if (usedFallback) {
|
|
455
|
+
if (usedFallback && !isTestFixtureModel(finalModel)) {
|
|
450
456
|
logger.info(`[franklin] ↺ Fallback successful: using ${finalModel}`);
|
|
451
457
|
}
|
|
452
458
|
}
|
|
@@ -678,7 +684,9 @@ async function fetchWithPaymentFallback(url, init, originalBody, config, payment
|
|
|
678
684
|
if (nextModel && onFallback) {
|
|
679
685
|
onFallback(model, 0, nextModel);
|
|
680
686
|
}
|
|
681
|
-
|
|
687
|
+
if (!isTestFixtureModel(model)) {
|
|
688
|
+
logger.warn(`[franklin] [fallback] ${model} request error: ${err instanceof Error ? err.message : String(err)}`);
|
|
689
|
+
}
|
|
682
690
|
if (i < config.chain.length - 1) {
|
|
683
691
|
await sleep(config.retryDelayMs);
|
|
684
692
|
}
|
|
@@ -17,12 +17,26 @@
|
|
|
17
17
|
* `local/lmstudio`, etc.) are intentionally NOT filtered — only the
|
|
18
18
|
* `local/test` prefix.
|
|
19
19
|
*/
|
|
20
|
+
// Prefixes test files use to mark "this isn't a real model name". The
|
|
21
|
+
// list grew by inspection of real franklin-debug.log pollution after
|
|
22
|
+
// 3.15.16 — each new convention surfaced as a writes-to-user-home leak:
|
|
23
|
+
// `local/test*` — agent loop in-process tests (test/local.mjs:567 etc.)
|
|
24
|
+
// `slow/` — proxy timeout test (test/local.mjs:380)
|
|
25
|
+
// `mock/` — generic mock-server fixtures (defensive)
|
|
26
|
+
// `test/` — e.g. `test/model` used in some test paths
|
|
20
27
|
const TEST_FIXTURE_PREFIXES = [
|
|
21
28
|
'local/test',
|
|
29
|
+
'slow/',
|
|
30
|
+
'mock/',
|
|
31
|
+
'test/',
|
|
22
32
|
];
|
|
33
|
+
// Exact-match fixtures (model is literally "test" without a slash).
|
|
34
|
+
const TEST_FIXTURE_EXACT = new Set(['test']);
|
|
23
35
|
export function isTestFixtureModel(model) {
|
|
24
36
|
if (!model)
|
|
25
37
|
return false;
|
|
38
|
+
if (TEST_FIXTURE_EXACT.has(model))
|
|
39
|
+
return true;
|
|
26
40
|
for (const prefix of TEST_FIXTURE_PREFIXES) {
|
|
27
41
|
if (model.startsWith(prefix))
|
|
28
42
|
return true;
|
package/dist/tools/defillama.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
19
19
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
20
|
+
import { logger } from '../logger.js';
|
|
20
21
|
const TIMEOUT_MS = 30_000;
|
|
21
22
|
// ─── Shared GET-with-x402 flow ────────────────────────────────────────────
|
|
22
23
|
async function getWithPayment(path, ctx) {
|
|
@@ -90,7 +91,7 @@ async function signPayment(response, chain, endpoint) {
|
|
|
90
91
|
return { 'PAYMENT-SIGNATURE': payload };
|
|
91
92
|
}
|
|
92
93
|
catch (err) {
|
|
93
|
-
|
|
94
|
+
logger.warn(`[franklin] DefiLlama payment error: ${err.message}`);
|
|
94
95
|
return null;
|
|
95
96
|
}
|
|
96
97
|
}
|
package/dist/tools/exa.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
21
21
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
22
|
+
import { logger } from '../logger.js';
|
|
22
23
|
const GEN_TIMEOUT_MS = 30_000;
|
|
23
24
|
// ─── Shared payment flow ─────────────────────────────────────────────
|
|
24
25
|
async function postWithPayment(path, body, ctx) {
|
|
@@ -95,7 +96,7 @@ async function signPayment(response, chain, endpoint) {
|
|
|
95
96
|
return { 'PAYMENT-SIGNATURE': payload };
|
|
96
97
|
}
|
|
97
98
|
catch (err) {
|
|
98
|
-
|
|
99
|
+
logger.warn(`[franklin] Exa payment error: ${err.message}`);
|
|
99
100
|
return null;
|
|
100
101
|
}
|
|
101
102
|
}
|
package/dist/tools/imagegen.js
CHANGED
|
@@ -11,6 +11,7 @@ import { ModelClient } from '../agent/llm.js';
|
|
|
11
11
|
import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
|
|
12
12
|
import { recordUsage } from '../stats/tracker.js';
|
|
13
13
|
import { findModel, estimateCostUsd } from '../gateway-models.js';
|
|
14
|
+
import { logger } from '../logger.js';
|
|
14
15
|
/**
|
|
15
16
|
* Models that accept a reference image via /v1/images/image2image. Currently
|
|
16
17
|
* limited to OpenAI's edit endpoint — Gemini Nano Banana Pro and Grok Imagine
|
|
@@ -391,7 +392,7 @@ async function signPayment(response, chain, endpoint) {
|
|
|
391
392
|
}
|
|
392
393
|
}
|
|
393
394
|
catch (err) {
|
|
394
|
-
|
|
395
|
+
logger.warn(`[franklin] Image payment error: ${err.message}`);
|
|
395
396
|
return null;
|
|
396
397
|
}
|
|
397
398
|
}
|
package/dist/tools/modal.js
CHANGED
|
@@ -29,6 +29,7 @@ import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, creat
|
|
|
29
29
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
30
30
|
import { walletReservation } from '../wallet/reservation.js';
|
|
31
31
|
import { recordUsage } from '../stats/tracker.js';
|
|
32
|
+
import { logger } from '../logger.js';
|
|
32
33
|
// ─── Pricing table (probed from /.well-known/x402 + 402 responses) ─────────
|
|
33
34
|
const CREATE_PRICE_USD = {
|
|
34
35
|
cpu: 0.01,
|
|
@@ -95,7 +96,7 @@ async function signPayment(response, chain, endpoint, resourceDescription) {
|
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
catch (err) {
|
|
98
|
-
|
|
99
|
+
logger.warn(`[franklin] Modal payment error: ${err.message}`);
|
|
99
100
|
return null;
|
|
100
101
|
}
|
|
101
102
|
}
|
package/dist/tools/musicgen.js
CHANGED
|
@@ -17,6 +17,7 @@ import fs from 'node:fs';
|
|
|
17
17
|
import path from 'node:path';
|
|
18
18
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
19
19
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
20
|
+
import { logger } from '../logger.js';
|
|
20
21
|
const DEFAULT_MODEL = 'minimax/music-2.5+';
|
|
21
22
|
const PRICE_USD = 0.1575;
|
|
22
23
|
// MiniMax generation is 1-3 minutes + small buffer for payment + download.
|
|
@@ -199,7 +200,7 @@ async function signPayment(response, chain, endpoint) {
|
|
|
199
200
|
return { 'PAYMENT-SIGNATURE': payload };
|
|
200
201
|
}
|
|
201
202
|
catch (err) {
|
|
202
|
-
|
|
203
|
+
logger.warn(`[franklin] Music payment error: ${err.message}`);
|
|
203
204
|
return null;
|
|
204
205
|
}
|
|
205
206
|
}
|
package/dist/tools/videogen.js
CHANGED
|
@@ -22,6 +22,7 @@ import fs from 'node:fs';
|
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
24
24
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
25
|
+
import { logger } from '../logger.js';
|
|
25
26
|
import { ModelClient } from '../agent/llm.js';
|
|
26
27
|
import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
|
|
27
28
|
import { recordUsage } from '../stats/tracker.js';
|
|
@@ -385,7 +386,7 @@ async function signPayment(response, chain, endpoint) {
|
|
|
385
386
|
return { 'PAYMENT-SIGNATURE': payload };
|
|
386
387
|
}
|
|
387
388
|
catch (err) {
|
|
388
|
-
|
|
389
|
+
logger.warn(`[franklin] Video payment error: ${err.message}`);
|
|
389
390
|
return null;
|
|
390
391
|
}
|
|
391
392
|
}
|
|
@@ -24,6 +24,7 @@ import { createWalletClient, http, publicActions } from 'viem';
|
|
|
24
24
|
import { getOrCreateWallet } from '@blockrun/llm';
|
|
25
25
|
import { loadConfig } from '../commands/config.js';
|
|
26
26
|
import { loadChain, API_URLS, VERSION } from '../config.js';
|
|
27
|
+
import { logger } from '../logger.js';
|
|
27
28
|
// ─── Constants ────────────────────────────────────────────────────────────
|
|
28
29
|
const ZEROX_GATEWAY_PATH = '/v1/zerox/gasless';
|
|
29
30
|
const QUOTE_TIMEOUT_MS = 30_000;
|
|
@@ -235,7 +236,7 @@ async function pollUntilDone(tradeHash, ctx) {
|
|
|
235
236
|
}
|
|
236
237
|
catch (err) {
|
|
237
238
|
// Surface a transient failure but keep polling — relayer might be backlogged.
|
|
238
|
-
|
|
239
|
+
logger.warn(`[franklin] gasless status poll error: ${err.message}`);
|
|
239
240
|
}
|
|
240
241
|
if (last.status === 'confirmed' ||
|
|
241
242
|
last.status === 'succeeded' ||
|
package/package.json
CHANGED