@blockrun/franklin 3.15.20 → 3.15.22
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/brain/store.js +43 -4
- package/dist/proxy/fallback.js +2 -14
- package/dist/proxy/server.js +32 -47
- package/dist/tools/prediction.js +4 -4
- package/package.json +1 -1
package/dist/brain/store.js
CHANGED
|
@@ -11,6 +11,16 @@ const ENTITIES_FILE = path.join(BRAIN_DIR, 'entities.jsonl');
|
|
|
11
11
|
const OBSERVATIONS_FILE = path.join(BRAIN_DIR, 'observations.jsonl');
|
|
12
12
|
const RELATIONS_FILE = path.join(BRAIN_DIR, 'relations.jsonl');
|
|
13
13
|
const MAX_ENTITIES = 200;
|
|
14
|
+
// Observations and relations were previously unbounded — `extract.ts`
|
|
15
|
+
// runs at every session end (commands/start.ts:515) so they grew
|
|
16
|
+
// linearly forever. Caps below give comfortable headroom for a year+
|
|
17
|
+
// of normal use without making per-entity scans pathological:
|
|
18
|
+
// - 2000 obs / 200 entities = ~10 observations per entity on average
|
|
19
|
+
// - 500 relations covers heavy cross-references between the entity set
|
|
20
|
+
// On cap breach we drop the oldest entries — younger observations are
|
|
21
|
+
// usually more relevant and more confident than an aging one.
|
|
22
|
+
const MAX_OBSERVATIONS = 2000;
|
|
23
|
+
const MAX_RELATIONS = 500;
|
|
14
24
|
function uid() { return crypto.randomBytes(8).toString('hex'); }
|
|
15
25
|
function ensureDir() {
|
|
16
26
|
fs.mkdirSync(BRAIN_DIR, { recursive: true });
|
|
@@ -110,7 +120,7 @@ export function addObservation(entityId, content, source, confidence = 0.8, tags
|
|
|
110
120
|
if (existing.some(o => o.entity_id === entityId && o.content.toLowerCase().trim() === contentLower)) {
|
|
111
121
|
return;
|
|
112
122
|
}
|
|
113
|
-
|
|
123
|
+
const next = {
|
|
114
124
|
id: uid(),
|
|
115
125
|
entity_id: entityId,
|
|
116
126
|
content,
|
|
@@ -118,7 +128,18 @@ export function addObservation(entityId, content, source, confidence = 0.8, tags
|
|
|
118
128
|
confidence,
|
|
119
129
|
tags,
|
|
120
130
|
created_at: Date.now(),
|
|
121
|
-
}
|
|
131
|
+
};
|
|
132
|
+
// Cap reached — rewrite the file with the trimmed set instead of
|
|
133
|
+
// appending. saveJsonl is atomic (tmp + rename) so a crash mid-write
|
|
134
|
+
// can't corrupt observations.
|
|
135
|
+
if (existing.length >= MAX_OBSERVATIONS) {
|
|
136
|
+
existing.sort((a, b) => b.created_at - a.created_at);
|
|
137
|
+
existing.length = MAX_OBSERVATIONS - 1;
|
|
138
|
+
existing.unshift(next);
|
|
139
|
+
saveJsonl(OBSERVATIONS_FILE, existing);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
appendJsonl(OBSERVATIONS_FILE, next);
|
|
122
143
|
}
|
|
123
144
|
// ─── Relations ────────────────────────────────────────────────────────────
|
|
124
145
|
export function loadRelations() {
|
|
@@ -140,7 +161,7 @@ export function upsertRelation(fromId, toId, type, confidence = 0.8) {
|
|
|
140
161
|
saveJsonl(RELATIONS_FILE, relations);
|
|
141
162
|
}
|
|
142
163
|
else {
|
|
143
|
-
|
|
164
|
+
const next = {
|
|
144
165
|
id: uid(),
|
|
145
166
|
from_id: fromId,
|
|
146
167
|
to_id: toId,
|
|
@@ -148,7 +169,25 @@ export function upsertRelation(fromId, toId, type, confidence = 0.8) {
|
|
|
148
169
|
confidence,
|
|
149
170
|
count: 1,
|
|
150
171
|
last_seen: Date.now(),
|
|
151
|
-
}
|
|
172
|
+
};
|
|
173
|
+
// Same cap pattern as observations. When the relation set is at
|
|
174
|
+
// its ceiling we drop the lowest-count, oldest-seen entries to
|
|
175
|
+
// make room — count + recency together approximate "this
|
|
176
|
+
// relation is still being seen", so eviction targets entries the
|
|
177
|
+
// brain extractor hasn't reinforced in a while.
|
|
178
|
+
if (relations.length >= MAX_RELATIONS) {
|
|
179
|
+
relations.sort((a, b) => {
|
|
180
|
+
if (b.count !== a.count)
|
|
181
|
+
return b.count - a.count;
|
|
182
|
+
return b.last_seen - a.last_seen;
|
|
183
|
+
});
|
|
184
|
+
relations.length = MAX_RELATIONS - 1;
|
|
185
|
+
relations.unshift(next);
|
|
186
|
+
saveJsonl(RELATIONS_FILE, relations);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
appendJsonl(RELATIONS_FILE, next);
|
|
190
|
+
}
|
|
152
191
|
}
|
|
153
192
|
}
|
|
154
193
|
// ─── Search ───────────────────────────────────────────────────────────────
|
package/dist/proxy/fallback.js
CHANGED
|
@@ -2,19 +2,7 @@
|
|
|
2
2
|
* Fallback chain for Franklin
|
|
3
3
|
* Automatically switches to backup models when primary fails (429, 5xx, etc.)
|
|
4
4
|
*/
|
|
5
|
-
import
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
import path from 'node:path';
|
|
8
|
-
const LOG_FILE = path.join(os.homedir(), '.blockrun', 'franklin-debug.log');
|
|
9
|
-
// eslint-disable-next-line no-control-regex
|
|
10
|
-
const ANSI_RE = /\x1B\[[0-9;]*[A-Za-z]|\x1B\][^\x07]*\x07|\x1B[()][A-B]|\r/g;
|
|
11
|
-
function appendLog(msg) {
|
|
12
|
-
try {
|
|
13
|
-
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
14
|
-
fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg.replace(ANSI_RE, '')}\n`);
|
|
15
|
-
}
|
|
16
|
-
catch { /* ignore */ }
|
|
17
|
-
}
|
|
5
|
+
import { logger } from '../logger.js';
|
|
18
6
|
export const DEFAULT_FALLBACK_CONFIG = {
|
|
19
7
|
chain: [
|
|
20
8
|
'deepseek/deepseek-chat', // Direct fallback — cheap & reliable
|
|
@@ -93,7 +81,7 @@ export async function fetchWithFallback(url, init, originalBody, config = DEFAUL
|
|
|
93
81
|
if (nextModel && onFallback) {
|
|
94
82
|
const errMsg = err instanceof Error ? err.message : 'Network error';
|
|
95
83
|
onFallback(model, 0, nextModel);
|
|
96
|
-
|
|
84
|
+
logger.warn(`[franklin] [fallback] ${model} network error: ${errMsg}`);
|
|
97
85
|
}
|
|
98
86
|
if (i < config.chain.length - 1) {
|
|
99
87
|
await sleep(config.retryDelayMs);
|
package/dist/proxy/server.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
2
|
import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
|
|
6
3
|
import { recordUsage } from '../stats/tracker.js';
|
|
7
4
|
import { appendAudit } from '../stats/audit.js';
|
|
@@ -12,34 +9,13 @@ import { VERSION } from '../config.js';
|
|
|
12
9
|
// User-Agent for backend requests
|
|
13
10
|
const USER_AGENT = `franklin/${VERSION}`;
|
|
14
11
|
const X_FRANKLIN_VERSION = VERSION;
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (!options.debug)
|
|
23
|
-
return;
|
|
24
|
-
const msg = `[${new Date().toISOString()}] ${stripAnsi(args.map(String).join(' '))}\n`;
|
|
25
|
-
try {
|
|
26
|
-
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
27
|
-
fs.appendFileSync(LOG_FILE, msg);
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
/* ignore */
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function log(...args) {
|
|
34
|
-
const msg = `[franklin] ${args.map(String).join(' ')}`;
|
|
35
|
-
// Do NOT print to stdout — the terminal is owned by the parent process (stdio: inherit).
|
|
36
|
-
// Use `franklin logs` to read runtime messages.
|
|
37
|
-
try {
|
|
38
|
-
fs.mkdirSync(path.dirname(LOG_FILE), { recursive: true });
|
|
39
|
-
fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${stripAnsi(msg)}\n`);
|
|
40
|
-
}
|
|
41
|
-
catch { /* ignore */ }
|
|
42
|
-
}
|
|
12
|
+
// Logging here goes through the unified logger introduced in 3.15.11
|
|
13
|
+
// (timestamp + [LEVEL] tag, self-rotating at 10 MB, optional stderr
|
|
14
|
+
// mirror in debug mode). The previous per-module debug()/log() helpers
|
|
15
|
+
// duplicated the file path, ANSI strip regex (with a slightly different
|
|
16
|
+
// pattern!), and timestamp format — bug fixes never propagated. They
|
|
17
|
+
// were the last holdouts after the agent loop was migrated.
|
|
18
|
+
import { logger, setDebugMode } from '../logger.js';
|
|
43
19
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
44
20
|
// 180s budget for *time-to-headers* — reasoning-class models (zai/glm-*,
|
|
45
21
|
// nemotron *-reasoning, deepseek-r*, gpt-5-codex, anthropic extended-thinking)
|
|
@@ -249,6 +225,10 @@ function withinRateLimit() {
|
|
|
249
225
|
return true;
|
|
250
226
|
}
|
|
251
227
|
export function createProxy(options) {
|
|
228
|
+
// Wire stderr-mirroring of unified logger output to the proxy's debug
|
|
229
|
+
// flag — same pattern as interactiveSession in agent/loop. File writes
|
|
230
|
+
// happen regardless; only the live stderr mirror is gated.
|
|
231
|
+
setDebugMode(!!options.debug);
|
|
252
232
|
const chain = options.chain || 'base';
|
|
253
233
|
let currentModel = options.modelOverride || DEFAULT_MODEL;
|
|
254
234
|
const fallbackEnabled = options.fallbackEnabled !== false; // Default true
|
|
@@ -307,19 +287,22 @@ export function createProxy(options) {
|
|
|
307
287
|
let requestModel = currentModel || options.modelOverride || 'unknown';
|
|
308
288
|
let usedFallback = false;
|
|
309
289
|
try {
|
|
310
|
-
|
|
290
|
+
if (options.debug)
|
|
291
|
+
logger.debug(`[franklin] request: ${req.method} ${req.url} currentModel=${currentModel || 'none'}`);
|
|
311
292
|
if (body) {
|
|
312
293
|
try {
|
|
313
294
|
const parsed = JSON.parse(body);
|
|
314
295
|
// Intercept "use <model>" commands for in-session model switching
|
|
315
296
|
if (parsed.messages) {
|
|
316
297
|
const last = parsed.messages[parsed.messages.length - 1];
|
|
317
|
-
|
|
298
|
+
if (options.debug)
|
|
299
|
+
logger.debug(`[franklin] last msg role=${last?.role} content-type=${typeof last?.content} content=${JSON.stringify(last?.content).slice(0, 200)}`);
|
|
318
300
|
}
|
|
319
301
|
const switchCmd = detectModelSwitch(parsed);
|
|
320
302
|
if (switchCmd) {
|
|
321
303
|
currentModel = switchCmd;
|
|
322
|
-
|
|
304
|
+
if (options.debug)
|
|
305
|
+
logger.debug(`[franklin] model switched to: ${currentModel}`);
|
|
323
306
|
const fakeResponse = {
|
|
324
307
|
id: `msg_franklin_${Date.now()}`,
|
|
325
308
|
type: 'message',
|
|
@@ -374,7 +357,7 @@ export function createProxy(options) {
|
|
|
374
357
|
const routing = routeRequest(promptText, routingProfile);
|
|
375
358
|
parsed.model = routing.model;
|
|
376
359
|
requestModel = routing.model;
|
|
377
|
-
|
|
360
|
+
logger.info(`[franklin] 🧠 Smart routing: ${routingProfile} → ${routing.tier} → ${routing.model} ` +
|
|
378
361
|
`(${(routing.savings * 100).toFixed(0)}% savings) [${routing.signals.join(', ')}]`);
|
|
379
362
|
}
|
|
380
363
|
{
|
|
@@ -392,8 +375,8 @@ export function createProxy(options) {
|
|
|
392
375
|
? Math.max(lastOut * 2, DEFAULT_MAX_TOKENS)
|
|
393
376
|
: DEFAULT_MAX_TOKENS;
|
|
394
377
|
parsed.max_tokens = Math.min(adaptive, modelCap);
|
|
395
|
-
if (original !== parsed.max_tokens) {
|
|
396
|
-
debug(
|
|
378
|
+
if (original !== parsed.max_tokens && options.debug) {
|
|
379
|
+
logger.debug(`[franklin] max_tokens: ${original || 'unset'} → ${parsed.max_tokens} (last output: ${lastOut || 'none'})`);
|
|
397
380
|
}
|
|
398
381
|
}
|
|
399
382
|
body = JSON.stringify(parsed);
|
|
@@ -430,7 +413,7 @@ export function createProxy(options) {
|
|
|
430
413
|
body = JSON.stringify(parsed);
|
|
431
414
|
}
|
|
432
415
|
catch { /* body not JSON, skip */ }
|
|
433
|
-
|
|
416
|
+
logger.warn(`[franklin] ⚠️ Safety net: resolved unrouted ${virtualName} → ${requestModel}`);
|
|
434
417
|
}
|
|
435
418
|
}
|
|
436
419
|
// Build request init
|
|
@@ -456,7 +439,7 @@ export function createProxy(options) {
|
|
|
456
439
|
solanaWallet,
|
|
457
440
|
timeoutMs: requestTimeoutMs,
|
|
458
441
|
}, (failedModel, status, nextModel) => {
|
|
459
|
-
|
|
442
|
+
logger.warn(`[franklin] ⚠️ ${failedModel} returned ${status}, falling back to ${nextModel}`);
|
|
460
443
|
});
|
|
461
444
|
response = result.response;
|
|
462
445
|
finalModel = result.modelUsed;
|
|
@@ -464,7 +447,7 @@ export function createProxy(options) {
|
|
|
464
447
|
body = result.bodyUsed;
|
|
465
448
|
usedFallback = result.fallbackUsed;
|
|
466
449
|
if (usedFallback) {
|
|
467
|
-
|
|
450
|
+
logger.info(`[franklin] ↺ Fallback successful: using ${finalModel}`);
|
|
468
451
|
}
|
|
469
452
|
}
|
|
470
453
|
else {
|
|
@@ -516,7 +499,7 @@ export function createProxy(options) {
|
|
|
516
499
|
}
|
|
517
500
|
res.writeHead(response.status, { 'Content-Type': 'application/json' });
|
|
518
501
|
res.end(errorBody);
|
|
519
|
-
|
|
502
|
+
logger.warn(`[franklin] ⚠️ ${response.status} from backend for ${finalModel}`);
|
|
520
503
|
return;
|
|
521
504
|
}
|
|
522
505
|
res.writeHead(response.status, responseHeaders);
|
|
@@ -531,7 +514,7 @@ export function createProxy(options) {
|
|
|
531
514
|
const pump = async () => {
|
|
532
515
|
while (true) {
|
|
533
516
|
if (Date.now() > streamDeadline) {
|
|
534
|
-
|
|
517
|
+
logger.warn('[franklin] ⚠️ Stream timeout after 5 minutes');
|
|
535
518
|
try {
|
|
536
519
|
reader.cancel();
|
|
537
520
|
}
|
|
@@ -576,7 +559,8 @@ export function createProxy(options) {
|
|
|
576
559
|
fallback: usedFallback,
|
|
577
560
|
source: 'proxy',
|
|
578
561
|
});
|
|
579
|
-
|
|
562
|
+
if (options.debug)
|
|
563
|
+
logger.debug(`[franklin] recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
|
|
580
564
|
}
|
|
581
565
|
}
|
|
582
566
|
res.end();
|
|
@@ -590,7 +574,7 @@ export function createProxy(options) {
|
|
|
590
574
|
}
|
|
591
575
|
};
|
|
592
576
|
pump().catch((err) => {
|
|
593
|
-
|
|
577
|
+
logger.error(`[franklin] ❌ Stream error: ${err instanceof Error ? err.message : String(err)}`);
|
|
594
578
|
res.end();
|
|
595
579
|
});
|
|
596
580
|
}
|
|
@@ -615,7 +599,8 @@ export function createProxy(options) {
|
|
|
615
599
|
fallback: usedFallback,
|
|
616
600
|
source: 'proxy',
|
|
617
601
|
});
|
|
618
|
-
|
|
602
|
+
if (options.debug)
|
|
603
|
+
logger.debug(`[franklin] recorded: model=${finalModel} in=${inputTokens} out=${outputTokens} cost=$${cost.toFixed(4)} fallback=${usedFallback}`);
|
|
619
604
|
}
|
|
620
605
|
}
|
|
621
606
|
catch {
|
|
@@ -626,7 +611,7 @@ export function createProxy(options) {
|
|
|
626
611
|
}
|
|
627
612
|
catch (error) {
|
|
628
613
|
const msg = error instanceof Error ? error.message : 'Proxy error';
|
|
629
|
-
|
|
614
|
+
logger.error(`[franklin] ❌ Error: ${msg}`);
|
|
630
615
|
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
631
616
|
res.end(JSON.stringify({
|
|
632
617
|
type: 'error',
|
|
@@ -693,7 +678,7 @@ async function fetchWithPaymentFallback(url, init, originalBody, config, payment
|
|
|
693
678
|
if (nextModel && onFallback) {
|
|
694
679
|
onFallback(model, 0, nextModel);
|
|
695
680
|
}
|
|
696
|
-
|
|
681
|
+
logger.warn(`[franklin] [fallback] ${model} request error: ${err instanceof Error ? err.message : String(err)}`);
|
|
697
682
|
if (i < config.chain.length - 1) {
|
|
698
683
|
await sleep(config.retryDelayMs);
|
|
699
684
|
}
|
package/dist/tools/prediction.js
CHANGED
|
@@ -163,7 +163,7 @@ async function execute(input, ctx) {
|
|
|
163
163
|
try {
|
|
164
164
|
switch (action) {
|
|
165
165
|
case 'searchPolymarket': {
|
|
166
|
-
const raw = await getWithPayment('/
|
|
166
|
+
const raw = await getWithPayment('/v1/pm/polymarket/markets', {
|
|
167
167
|
search,
|
|
168
168
|
status: status ?? 'active',
|
|
169
169
|
sort: sort ?? 'volume',
|
|
@@ -193,7 +193,7 @@ async function execute(input, ctx) {
|
|
|
193
193
|
return { output: lines.join('\n') };
|
|
194
194
|
}
|
|
195
195
|
case 'searchKalshi': {
|
|
196
|
-
const raw = await getWithPayment('/
|
|
196
|
+
const raw = await getWithPayment('/v1/pm/kalshi/markets', {
|
|
197
197
|
search,
|
|
198
198
|
status: status ?? 'open',
|
|
199
199
|
sort: sort ?? 'volume',
|
|
@@ -223,7 +223,7 @@ async function execute(input, ctx) {
|
|
|
223
223
|
return { output: lines.join('\n') };
|
|
224
224
|
}
|
|
225
225
|
case 'crossPlatform': {
|
|
226
|
-
const raw = await getWithPayment('/
|
|
226
|
+
const raw = await getWithPayment('/v1/pm/matching-markets/pairs', {
|
|
227
227
|
limit: cappedLimit,
|
|
228
228
|
}, ctx);
|
|
229
229
|
const pairs = unwrapList(raw);
|
|
@@ -249,7 +249,7 @@ async function execute(input, ctx) {
|
|
|
249
249
|
if (!conditionId) {
|
|
250
250
|
return { output: 'Error: conditionId is required for smartMoney (Polymarket condition_id from a prior searchPolymarket call)', isError: true };
|
|
251
251
|
}
|
|
252
|
-
const path = `/
|
|
252
|
+
const path = `/v1/pm/polymarket/market/${encodeURIComponent(conditionId)}/smart-money`;
|
|
253
253
|
const data = await getWithPayment(path, {}, ctx);
|
|
254
254
|
const buyers = (data.buyers ?? []).slice(0, 5);
|
|
255
255
|
const sellers = (data.sellers ?? []).slice(0, 5);
|
package/package.json
CHANGED