@blockrun/franklin 3.8.8 → 3.8.9
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 +61 -2
- package/dist/agent/permissions.js +2 -2
- package/dist/agent/types.d.ts +7 -0
- package/dist/commands/start.d.ts +4 -0
- package/dist/commands/start.js +72 -2
- package/dist/index.js +9 -1
- package/dist/panel/html.js +111 -21
- package/dist/panel/server.js +15 -4
- package/dist/tools/activate.d.ts +29 -0
- package/dist/tools/activate.js +96 -0
- package/dist/tools/index.js +2 -0
- package/dist/tools/tool-categories.d.ts +22 -0
- package/dist/tools/tool-categories.js +44 -0
- package/dist/tools/trading-execute.d.ts +11 -21
- package/dist/tools/trading-execute.js +43 -130
- package/dist/tools/trading-views.d.ts +64 -0
- package/dist/tools/trading-views.js +115 -0
- package/dist/tools/trading.js +86 -7
- package/dist/tools/webhook.d.ts +18 -0
- package/dist/tools/webhook.js +185 -0
- package/dist/trading/data.d.ts +24 -1
- package/dist/trading/data.js +67 -102
- package/dist/trading/providers/blockrun/client.d.ts +48 -0
- package/dist/trading/providers/blockrun/client.js +253 -0
- package/dist/trading/providers/blockrun/price.d.ts +24 -0
- package/dist/trading/providers/blockrun/price.js +110 -0
- package/dist/trading/providers/coingecko/client.d.ts +20 -0
- package/dist/trading/providers/coingecko/client.js +87 -0
- package/dist/trading/providers/coingecko/markets.d.ts +3 -0
- package/dist/trading/providers/coingecko/markets.js +25 -0
- package/dist/trading/providers/coingecko/ohlcv.d.ts +3 -0
- package/dist/trading/providers/coingecko/ohlcv.js +29 -0
- package/dist/trading/providers/coingecko/price.d.ts +11 -0
- package/dist/trading/providers/coingecko/price.js +41 -0
- package/dist/trading/providers/coingecko/trending.d.ts +3 -0
- package/dist/trading/providers/coingecko/trending.js +22 -0
- package/dist/trading/providers/fetcher.d.ts +43 -0
- package/dist/trading/providers/fetcher.js +45 -0
- package/dist/trading/providers/registry.d.ts +45 -0
- package/dist/trading/providers/registry.js +82 -0
- package/dist/trading/providers/standard-models.d.ts +94 -0
- package/dist/trading/providers/standard-models.js +21 -0
- package/dist/trading/providers/telemetry.d.ts +51 -0
- package/dist/trading/providers/telemetry.js +115 -0
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -13,6 +13,8 @@ import { optimizeHistory, CAPPED_MAX_TOKENS, ESCALATED_MAX_TOKENS, getMaxOutputT
|
|
|
13
13
|
import { classifyAgentError } from './error-classifier.js';
|
|
14
14
|
import { SessionToolGuard } from './tool-guard.js';
|
|
15
15
|
import { resetToolSessionState } from '../tools/index.js';
|
|
16
|
+
import { CORE_TOOL_NAMES, dynamicToolsEnabled } from '../tools/tool-categories.js';
|
|
17
|
+
import { createActivateToolCapability } from '../tools/activate.js';
|
|
16
18
|
import { recordUsage } from '../stats/tracker.js';
|
|
17
19
|
import { recordSessionUsage } from '../stats/session-tracker.js';
|
|
18
20
|
import { appendAudit, extractLastUserPrompt } from '../stats/audit.js';
|
|
@@ -317,11 +319,33 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
317
319
|
chain: config.chain,
|
|
318
320
|
debug: config.debug,
|
|
319
321
|
});
|
|
322
|
+
// ── Dynamic tool visibility ──
|
|
323
|
+
// Register ActivateTool before building the capability map so the agent
|
|
324
|
+
// can always reach the meta-tool. When FRANKLIN_DYNAMIC_TOOLS=0 is set,
|
|
325
|
+
// `activeTools` is seeded with every registered name — behaves as the
|
|
326
|
+
// pre-3.8.9 static registry.
|
|
320
327
|
const capabilityMap = new Map();
|
|
321
328
|
for (const cap of config.capabilities) {
|
|
322
329
|
capabilityMap.set(cap.spec.name, cap);
|
|
323
330
|
}
|
|
324
|
-
const
|
|
331
|
+
const activeTools = new Set();
|
|
332
|
+
const dynamicTools = dynamicToolsEnabled();
|
|
333
|
+
if (dynamicTools) {
|
|
334
|
+
for (const name of CORE_TOOL_NAMES) {
|
|
335
|
+
if (capabilityMap.has(name))
|
|
336
|
+
activeTools.add(name);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
for (const cap of config.capabilities)
|
|
341
|
+
activeTools.add(cap.spec.name);
|
|
342
|
+
}
|
|
343
|
+
const activateToolCap = createActivateToolCapability({ activeTools, allTools: capabilityMap });
|
|
344
|
+
capabilityMap.set(activateToolCap.spec.name, activateToolCap);
|
|
345
|
+
if (dynamicTools)
|
|
346
|
+
activeTools.add(activateToolCap.spec.name);
|
|
347
|
+
const allToolDefs = [...capabilityMap.values()].map(c => c.spec);
|
|
348
|
+
const buildCallToolDefs = () => dynamicTools ? allToolDefs.filter(t => activeTools.has(t.name)) : allToolDefs;
|
|
325
349
|
const maxTurns = config.maxTurns ?? 15;
|
|
326
350
|
const workDir = config.workingDir ?? process.cwd();
|
|
327
351
|
const permissions = new PermissionManager(config.permissionMode ?? 'default', config.permissionPromptFn);
|
|
@@ -576,6 +600,20 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
576
600
|
'4. Think step by step — show your reasoning explicitly when it adds value\n' +
|
|
577
601
|
'Prioritize correctness and thoroughness over speed.');
|
|
578
602
|
}
|
|
603
|
+
// ── Dynamic tool visibility hint ──
|
|
604
|
+
// When the core/on-demand split is active, tell every model up front
|
|
605
|
+
// that its tool list is intentionally small and that extras can be
|
|
606
|
+
// pulled via ActivateTool. Kept byte-stable across turns (no tool
|
|
607
|
+
// names inlined) so the prompt cache still holds.
|
|
608
|
+
if (dynamicTools && allToolDefs.length > activeTools.size) {
|
|
609
|
+
systemParts.push('# Tool Inventory\n' +
|
|
610
|
+
'Your current tool list is intentionally minimal. Additional tools ' +
|
|
611
|
+
'(web search, image/video/music generation, trading, content, brain ' +
|
|
612
|
+
'recall, etc.) are available on demand. Call `ActivateTool()` with ' +
|
|
613
|
+
'no arguments to see what is available, then call `ActivateTool({ ' +
|
|
614
|
+
'"names": ["<name>"] })` to enable the ones you need. Activated ' +
|
|
615
|
+
'tools become visible on the next turn.');
|
|
616
|
+
}
|
|
579
617
|
// ── Context awareness injection ──
|
|
580
618
|
// Tell the model how full its context window is so it can self-regulate.
|
|
581
619
|
// At high usage, nudge it to be concise and avoid unnecessary tool calls.
|
|
@@ -669,7 +707,10 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
669
707
|
}
|
|
670
708
|
// Build per-call tool defs, max_tokens, and system prompt
|
|
671
709
|
// (planning calls get no tools + short output + planning prompt)
|
|
672
|
-
|
|
710
|
+
// Dynamic visibility: `buildCallToolDefs()` returns only the active set
|
|
711
|
+
// (core + any the agent pulled via ActivateTool). Re-evaluated every
|
|
712
|
+
// turn so newly activated tools take effect immediately.
|
|
713
|
+
let callToolDefs = buildCallToolDefs();
|
|
673
714
|
let callMaxTokens = maxTokens;
|
|
674
715
|
let callSystemPrompt = systemPrompt;
|
|
675
716
|
if (planActive && loopCount === 1) {
|
|
@@ -918,6 +959,24 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
918
959
|
const opusCost = (inputTokens / 1_000_000) * OPUS_PRICING.input
|
|
919
960
|
+ (usage.outputTokens / 1_000_000) * OPUS_PRICING.output;
|
|
920
961
|
sessionSavedVsOpus += Math.max(0, opusCost - costEstimate);
|
|
962
|
+
// ── Max-spend guard ──
|
|
963
|
+
// Session-level cost ceiling. Cron/daily drivers pass this to bound a
|
|
964
|
+
// single run ("spend at most $0.50 for today's digest"); interactive
|
|
965
|
+
// users can pass it to feel safe walking away. Hits as soon as accumulated
|
|
966
|
+
// cost crosses the cap — the last call that tipped us over still runs,
|
|
967
|
+
// but no further API calls are made.
|
|
968
|
+
const maxSpend = config.maxSpendUsd;
|
|
969
|
+
if (typeof maxSpend === 'number' && Number.isFinite(maxSpend) && maxSpend > 0 &&
|
|
970
|
+
sessionCostUsd >= maxSpend) {
|
|
971
|
+
onEvent({
|
|
972
|
+
kind: 'text_delta',
|
|
973
|
+
text: `\n\n_Max-spend reached: $${sessionCostUsd.toFixed(4)} ≥ cap $${maxSpend.toFixed(2)}. ` +
|
|
974
|
+
`Stopping session — further calls would exceed the budget._\n`,
|
|
975
|
+
});
|
|
976
|
+
persistSessionMeta();
|
|
977
|
+
onEvent({ kind: 'turn_done', reason: 'budget' });
|
|
978
|
+
return history;
|
|
979
|
+
}
|
|
921
980
|
// ── Max output tokens recovery ──
|
|
922
981
|
if (stopReason === 'max_tokens' && recoveryAttempts < MAX_RECOVERY_ATTEMPTS) {
|
|
923
982
|
recoveryAttempts++;
|
|
@@ -33,10 +33,10 @@ function isCommonDevCommand(cmd) {
|
|
|
33
33
|
return COMMON_DEV_PATTERNS.some(p => p.test(trimmed));
|
|
34
34
|
}
|
|
35
35
|
// ─── Default Rules ─────────────────────────────────────────────────────────
|
|
36
|
-
const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX']);
|
|
36
|
+
const READ_ONLY_TOOLS = new Set(['Read', 'Glob', 'Grep', 'WebSearch', 'Task', 'AskUser', 'ActivateTool', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX']);
|
|
37
37
|
const DESTRUCTIVE_TOOLS = new Set(['Write', 'Edit', 'Bash']);
|
|
38
38
|
const DEFAULT_RULES = {
|
|
39
|
-
allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX'],
|
|
39
|
+
allow: ['Read', 'Glob', 'Grep', 'WebSearch', 'WebFetch', 'Task', 'AskUser', 'ActivateTool', 'ImageGen', 'TradingSignal', 'TradingMarket', 'SearchX'],
|
|
40
40
|
deny: [],
|
|
41
41
|
ask: ['Write', 'Edit', 'Bash', 'Agent', 'PostToX'],
|
|
42
42
|
};
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -155,4 +155,11 @@ export interface AgentConfig {
|
|
|
155
155
|
* unset. Format: "<driver>:<owner-or-chat-id>", e.g. "telegram:12345".
|
|
156
156
|
*/
|
|
157
157
|
sessionChannel?: string;
|
|
158
|
+
/**
|
|
159
|
+
* Hard cap on total USD spend for this session. When accumulated API cost
|
|
160
|
+
* crosses the cap, the loop stops with `reason: 'budget'`. Zero/negative
|
|
161
|
+
* values disable the cap. Primary use case: cron/daily drivers that must
|
|
162
|
+
* bound a single run to keep autonomous execution inside a known envelope.
|
|
163
|
+
*/
|
|
164
|
+
maxSpendUsd?: number;
|
|
158
165
|
}
|
package/dist/commands/start.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ interface StartOptions {
|
|
|
7
7
|
resume?: string | boolean | 'picker';
|
|
8
8
|
/** Continue: resume most recent session matching the current working directory */
|
|
9
9
|
continue?: boolean;
|
|
10
|
+
/** Hard USD cap on total session spend. Stops the loop when exceeded. */
|
|
11
|
+
maxSpend?: string | number;
|
|
12
|
+
/** Run a single prompt non-interactively, then exit. For cron/scripted use. */
|
|
13
|
+
prompt?: string;
|
|
10
14
|
}
|
|
11
15
|
export declare function startCommand(options: StartOptions): Promise<void>;
|
|
12
16
|
export {};
|
package/dist/commands/start.js
CHANGED
|
@@ -74,6 +74,49 @@ export async function startCommand(options) {
|
|
|
74
74
|
// Default: free NVIDIA model — zero wallet charges until user explicitly switches
|
|
75
75
|
model = 'nvidia/nemotron-ultra-253b';
|
|
76
76
|
}
|
|
77
|
+
const workDir = process.cwd();
|
|
78
|
+
// --prompt batch mode: skip all interactive startup UI/side effects so
|
|
79
|
+
// stdout stays clean for cron/scripts. Keep the capability surface to the
|
|
80
|
+
// built-ins only — no panel, no MCP autoconnect, no wallet/banner chatter.
|
|
81
|
+
if (options.prompt) {
|
|
82
|
+
if (options.resume === true || options.resume === 'picker') {
|
|
83
|
+
console.error(chalk.red('`--prompt` requires `--resume` to include an explicit session id.'));
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const systemInstructions = assembleInstructions(workDir, model);
|
|
88
|
+
const subAgent = createSubAgentCapability(apiUrl, chain, allCapabilities, model);
|
|
89
|
+
const { registerMoAConfig } = await import('../tools/moa.js');
|
|
90
|
+
registerMoAConfig(apiUrl, chain, model);
|
|
91
|
+
const capabilities = [...allCapabilities, subAgent];
|
|
92
|
+
if (options.debug) {
|
|
93
|
+
const issues = validateToolDescriptions(capabilities);
|
|
94
|
+
for (const issue of issues) {
|
|
95
|
+
console.error(`[validate] ${issue.severity}: ${issue.toolName} — ${issue.issue}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const agentConfig = {
|
|
99
|
+
model,
|
|
100
|
+
apiUrl,
|
|
101
|
+
chain,
|
|
102
|
+
systemInstructions,
|
|
103
|
+
capabilities,
|
|
104
|
+
maxTurns: 100,
|
|
105
|
+
workingDir: workDir,
|
|
106
|
+
permissionMode: 'trust',
|
|
107
|
+
debug: options.debug,
|
|
108
|
+
resumeSessionId: (typeof options.resume === 'string' && options.resume !== 'picker')
|
|
109
|
+
? options.resume
|
|
110
|
+
: continueResolvedId,
|
|
111
|
+
...(options.maxSpend != null
|
|
112
|
+
? { maxSpendUsd: Number(options.maxSpend) }
|
|
113
|
+
: {}),
|
|
114
|
+
};
|
|
115
|
+
const exitCode = await runOneShot(agentConfig, options.prompt);
|
|
116
|
+
flushStats();
|
|
117
|
+
process.exitCode = exitCode;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
77
120
|
// Warn when a paid model is active so users know they'll be charged
|
|
78
121
|
const FREE_MODELS = new Set([
|
|
79
122
|
'nvidia/nemotron-ultra-253b',
|
|
@@ -114,7 +157,6 @@ export async function startCommand(options) {
|
|
|
114
157
|
catch { /* migration is optional */ }
|
|
115
158
|
}
|
|
116
159
|
printBanner(version);
|
|
117
|
-
const workDir = process.cwd();
|
|
118
160
|
// Auto-start panel in background unless explicitly disabled.
|
|
119
161
|
// Binds loopback-only (wallet secrets on /api/wallet/secret — never expose on LAN).
|
|
120
162
|
let panelUrl;
|
|
@@ -240,9 +282,13 @@ export async function startCommand(options) {
|
|
|
240
282
|
workingDir: workDir,
|
|
241
283
|
// Non-TTY (piped) input = scripted mode → trust all tools automatically.
|
|
242
284
|
// Interactive TTY = default mode (prompts for Bash/Write/Edit).
|
|
243
|
-
|
|
285
|
+
// --prompt is also scripted; the cron driver never sees a TTY.
|
|
286
|
+
permissionMode: (options.trust || options.prompt || !process.stdin.isTTY) ? 'trust' : 'default',
|
|
244
287
|
debug: options.debug,
|
|
245
288
|
resumeSessionId,
|
|
289
|
+
...(options.maxSpend != null
|
|
290
|
+
? { maxSpendUsd: Number(options.maxSpend) }
|
|
291
|
+
: {}),
|
|
246
292
|
};
|
|
247
293
|
// Bootstrap learnings from existing CLAUDE.md on first run (async, non-blocking)
|
|
248
294
|
Promise.all([
|
|
@@ -262,6 +308,30 @@ export async function startCommand(options) {
|
|
|
262
308
|
await runWithBasicUI(agentConfig, model, workDir);
|
|
263
309
|
}
|
|
264
310
|
}
|
|
311
|
+
// ─── One-shot mode (franklin --prompt "...") ──────────────────────────────
|
|
312
|
+
// Used by cron drivers. Non-interactive, prints text deltas to stdout as
|
|
313
|
+
// they stream, honors --max-spend, exits 0 on completion / 1 on error.
|
|
314
|
+
async function runOneShot(agentConfig, prompt) {
|
|
315
|
+
let delivered = false;
|
|
316
|
+
let exitCode = 0;
|
|
317
|
+
const getInput = async () => {
|
|
318
|
+
if (delivered)
|
|
319
|
+
return null;
|
|
320
|
+
delivered = true;
|
|
321
|
+
return prompt;
|
|
322
|
+
};
|
|
323
|
+
await interactiveSession(agentConfig, getInput, (event) => {
|
|
324
|
+
if (event.kind === 'text_delta') {
|
|
325
|
+
process.stdout.write(event.text);
|
|
326
|
+
}
|
|
327
|
+
else if (event.kind === 'turn_done') {
|
|
328
|
+
if (event.reason === 'error')
|
|
329
|
+
exitCode = 1;
|
|
330
|
+
process.stdout.write('\n');
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
return exitCode;
|
|
334
|
+
}
|
|
265
335
|
// ─── Ink UI (interactive terminal) ─────────────────────────────────────────
|
|
266
336
|
async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, onBalanceReady, fetchBalance) {
|
|
267
337
|
const startSnapshot = snapshotStats();
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,8 @@ program
|
|
|
43
43
|
.option('--trust', 'Trust mode — skip permission prompts for all tools')
|
|
44
44
|
.option('-r, --resume [sessionId]', 'Resume a session by ID (or show picker if omitted)')
|
|
45
45
|
.option('-c, --continue', 'Continue the most recent session in this directory')
|
|
46
|
+
.option('--max-spend <usd>', 'Hard USD cap on total session API spend — session stops when exceeded')
|
|
47
|
+
.option('-p, --prompt <text>', 'Run a single prompt non-interactively (for cron/scripted use)')
|
|
46
48
|
.action((options) => startCommand({ ...options, version }));
|
|
47
49
|
program
|
|
48
50
|
.command('resume [sessionId]')
|
|
@@ -209,7 +211,7 @@ const args = process.argv.slice(2);
|
|
|
209
211
|
const firstArg = args[0];
|
|
210
212
|
const HELP_FLAGS = new Set(['-h', '--help']);
|
|
211
213
|
const VERSION_FLAGS = new Set(['-V', '--version']);
|
|
212
|
-
const START_ONLY_FLAGS = new Set(['--trust', '--debug', '-m', '--model', '-r', '--resume', '-c', '--continue']);
|
|
214
|
+
const START_ONLY_FLAGS = new Set(['--trust', '--debug', '-m', '--model', '-r', '--resume', '-c', '--continue', '-p', '--prompt', '--max-spend']);
|
|
213
215
|
function hasAnyFlag(argv, flags) {
|
|
214
216
|
return argv.some(arg => flags.has(arg));
|
|
215
217
|
}
|
|
@@ -227,6 +229,12 @@ function parseStartFlags(argv, startIdx = 0) {
|
|
|
227
229
|
else if ((arg === '-m' || arg === '--model') && argv[i + 1]) {
|
|
228
230
|
opts.model = argv[++i];
|
|
229
231
|
}
|
|
232
|
+
else if ((arg === '-p' || arg === '--prompt') && argv[i + 1]) {
|
|
233
|
+
opts.prompt = argv[++i];
|
|
234
|
+
}
|
|
235
|
+
else if (arg === '--max-spend' && argv[i + 1]) {
|
|
236
|
+
opts.maxSpend = argv[++i];
|
|
237
|
+
}
|
|
230
238
|
else if (arg === '-c' || arg === '--continue') {
|
|
231
239
|
opts.continue = true;
|
|
232
240
|
}
|
package/dist/panel/html.js
CHANGED
|
@@ -371,14 +371,14 @@ a:hover { text-decoration:underline; }
|
|
|
371
371
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/><path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/><path d="M18 12a2 2 0 0 0 0 4h4v-4z"/></svg>
|
|
372
372
|
Wallet
|
|
373
373
|
</button>
|
|
374
|
+
<button class="nav-item" data-tab="markets">
|
|
375
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 14l4-4 4 4 5-5"/></svg>
|
|
376
|
+
Markets
|
|
377
|
+
</button>
|
|
374
378
|
<button class="nav-item" data-tab="sessions">
|
|
375
379
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
376
380
|
Sessions
|
|
377
381
|
</button>
|
|
378
|
-
<button class="nav-item" data-tab="social">
|
|
379
|
-
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4l11.733 16h4.267l-11.733-16z"/><path d="M4 20l6.768-6.768M15.232 11.232L20 4"/></svg>
|
|
380
|
-
Social
|
|
381
|
-
</button>
|
|
382
382
|
<button class="nav-item" data-tab="learnings">
|
|
383
383
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
|
|
384
384
|
Learnings
|
|
@@ -542,16 +542,34 @@ a:hover { text-decoration:underline; }
|
|
|
542
542
|
<div class="session-detail" id="session-detail" style="display:none"></div>
|
|
543
543
|
</div>
|
|
544
544
|
|
|
545
|
-
<!--
|
|
546
|
-
<div class="tab" id="tab-
|
|
545
|
+
<!-- Markets -->
|
|
546
|
+
<div class="tab" id="tab-markets">
|
|
547
547
|
<div class="content-header">
|
|
548
|
-
<h2>
|
|
549
|
-
<p>
|
|
548
|
+
<h2>Markets</h2>
|
|
549
|
+
<p>How Franklin gets trading data — and what it costs.</p>
|
|
550
550
|
</div>
|
|
551
|
-
|
|
552
|
-
<div class="
|
|
553
|
-
<h3>
|
|
554
|
-
<div
|
|
551
|
+
|
|
552
|
+
<div class="grid grid-4">
|
|
553
|
+
<div class="card"><h3>Calls today</h3><div class="metric" id="mk-calls">—</div></div>
|
|
554
|
+
<div class="card"><h3>Spend today</h3><div class="metric gold" id="mk-spend">—</div></div>
|
|
555
|
+
<div class="card"><h3>p50 latency</h3><div class="metric" id="mk-p50">—</div></div>
|
|
556
|
+
<div class="card"><h3>Payment chain</h3><div class="metric" id="mk-chain">—</div></div>
|
|
557
|
+
</div>
|
|
558
|
+
|
|
559
|
+
<div style="display:grid;grid-template-columns:1.1fr 1fr;gap:14px;margin-top:14px">
|
|
560
|
+
<div class="card">
|
|
561
|
+
<h3>Data pipeline</h3>
|
|
562
|
+
<p style="color:var(--text-dim);font-size:12px;margin:4px 0 14px">
|
|
563
|
+
Each asset class routes through the provider registry to the active upstream.
|
|
564
|
+
</p>
|
|
565
|
+
<div id="mk-pipeline" style="font-family:var(--mono);font-size:12px;line-height:1.75"></div>
|
|
566
|
+
</div>
|
|
567
|
+
<div class="card">
|
|
568
|
+
<h3>Providers</h3>
|
|
569
|
+
<div id="mk-providers" style="margin-top:6px"></div>
|
|
570
|
+
<h3 style="margin-top:18px">Recent paid calls</h3>
|
|
571
|
+
<div id="mk-paid" class="empty" style="margin-top:6px">No paid calls yet</div>
|
|
572
|
+
</div>
|
|
555
573
|
</div>
|
|
556
574
|
</div>
|
|
557
575
|
|
|
@@ -588,6 +606,7 @@ a:hover { text-decoration:underline; }
|
|
|
588
606
|
</div>
|
|
589
607
|
<div id="audit-list" style="font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;"></div>
|
|
590
608
|
</div>
|
|
609
|
+
|
|
591
610
|
</div>
|
|
592
611
|
|
|
593
612
|
<script>
|
|
@@ -716,14 +735,84 @@ document.getElementById('session-search').addEventListener('input', (e) => {
|
|
|
716
735
|
}, 300);
|
|
717
736
|
});
|
|
718
737
|
|
|
719
|
-
async function
|
|
720
|
-
const
|
|
721
|
-
if (!
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
738
|
+
async function loadMarkets() {
|
|
739
|
+
const data = await api('markets');
|
|
740
|
+
if (!data) return;
|
|
741
|
+
|
|
742
|
+
const calls = (data.totals && data.totals.callsToday) || 0;
|
|
743
|
+
const spend = (data.totals && data.totals.spendUsdToday) || 0;
|
|
744
|
+
const p50 = data.totals && data.totals.p50LatencyMs;
|
|
745
|
+
document.getElementById('mk-calls').textContent = String(calls);
|
|
746
|
+
document.getElementById('mk-spend').textContent = usd(spend);
|
|
747
|
+
document.getElementById('mk-p50').textContent = (p50 == null) ? '—' : (p50 + ' ms');
|
|
748
|
+
document.getElementById('mk-chain').textContent = (data.chain || 'base').toUpperCase();
|
|
749
|
+
|
|
750
|
+
// Pipeline: Franklin → registry → per-asset-class provider → endpoint
|
|
751
|
+
const rows = (data.wiring || []).filter(function(r){ return r.kind === 'price'; });
|
|
752
|
+
const singletonRows = (data.wiring || []).filter(function(r){ return r.kind !== 'price'; });
|
|
753
|
+
const providerLabel = function(name) {
|
|
754
|
+
if (name === 'coingecko') return '<span style="color:var(--success)">CoinGecko</span>';
|
|
755
|
+
if (name === 'blockrun') return '<span style="color:var(--gold)">BlockRun Gateway</span>';
|
|
756
|
+
return esc(name);
|
|
757
|
+
};
|
|
758
|
+
const pipeLines = [
|
|
759
|
+
'<div>Franklin agent</div>',
|
|
760
|
+
'<div style="color:var(--text-dim);padding-left:8px">↓</div>',
|
|
761
|
+
'<div>Provider registry</div>',
|
|
762
|
+
'<div style="color:var(--text-dim);padding-left:8px">↓</div>',
|
|
763
|
+
];
|
|
764
|
+
rows.forEach(function(r, i){
|
|
765
|
+
const last = i === rows.length - 1;
|
|
766
|
+
const branch = last ? '└' : '├';
|
|
767
|
+
const paid = r.paid ? ' <span style="color:var(--gold);font-size:10px">◆ x402</span>' : '';
|
|
768
|
+
pipeLines.push(
|
|
769
|
+
'<div> ' + branch + '─ ' + esc(r.assetClass).padEnd(9, ' ') +
|
|
770
|
+
' → ' + providerLabel(r.provider) + paid + '</div>'
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
pipeLines.push('<div style="margin-top:10px;color:var(--text-dim);font-size:11px">Other singleton kinds:</div>');
|
|
774
|
+
singletonRows.forEach(function(r){
|
|
775
|
+
pipeLines.push(
|
|
776
|
+
'<div style="color:var(--text-dim);font-size:11px"> ' +
|
|
777
|
+
esc(r.kind) + ' → ' + providerLabel(r.provider) + '</div>'
|
|
778
|
+
);
|
|
779
|
+
});
|
|
780
|
+
document.getElementById('mk-pipeline').innerHTML = pipeLines.join('');
|
|
781
|
+
|
|
782
|
+
// Providers health
|
|
783
|
+
const statusChip = function(s){
|
|
784
|
+
if (s === 'ok') return '<span class="dot on"></span> <span style="color:var(--success)">OK</span>';
|
|
785
|
+
if (s === 'degraded') return '<span class="dot off"></span> <span style="color:var(--danger)">degraded</span>';
|
|
786
|
+
return '<span class="dot" style="background:var(--text-dim)"></span> <span style="color:var(--text-dim)">cold</span>';
|
|
787
|
+
};
|
|
788
|
+
const providers = data.providers || [];
|
|
789
|
+
document.getElementById('mk-providers').innerHTML = providers.length === 0 ? '<div class="empty">No calls recorded yet.</div>' : providers.map(function(p){
|
|
790
|
+
const since = p.lastOkAt ? Math.round((Date.now() - p.lastOkAt) / 1000) + 's ago' : '—';
|
|
791
|
+
return '<div style="display:flex;justify-content:space-between;align-items:center;padding:7px 0;border-bottom:1px solid var(--border);font-size:12px">' +
|
|
792
|
+
'<span>' + statusChip(p.status) + ' <strong>' + esc(p.name) + '</strong></span>' +
|
|
793
|
+
'<span style="color:var(--text-dim);font-family:var(--mono);font-size:11px">' +
|
|
794
|
+
p.calls + ' calls · p50 ' + (p.p50LatencyMs == null ? '—' : p.p50LatencyMs + 'ms') + ' · last ' + since +
|
|
795
|
+
'</span>' +
|
|
796
|
+
'</div>';
|
|
797
|
+
}).join('');
|
|
798
|
+
|
|
799
|
+
// Recent paid calls
|
|
800
|
+
const paid = data.recentPaidCalls || [];
|
|
801
|
+
const paidBox = document.getElementById('mk-paid');
|
|
802
|
+
if (paid.length === 0) {
|
|
803
|
+
paidBox.className = 'empty';
|
|
804
|
+
paidBox.textContent = 'No paid calls yet — stocks ship in the next release.';
|
|
805
|
+
} else {
|
|
806
|
+
paidBox.className = '';
|
|
807
|
+
paidBox.innerHTML = paid.map(function(r){
|
|
808
|
+
const age = Math.round((Date.now() - r.ts) / 1000) + 's ago';
|
|
809
|
+
return '<div style="display:flex;justify-content:space-between;padding:4px 0;font-family:var(--mono);font-size:12px">' +
|
|
810
|
+
'<span>' + esc(r.endpoint) + '</span>' +
|
|
811
|
+
'<span class="gold">' + usd(r.costUsd) + '</span>' +
|
|
812
|
+
'<span style="color:var(--text-dim)">' + age + '</span>' +
|
|
813
|
+
'</div>';
|
|
814
|
+
}).join('');
|
|
815
|
+
}
|
|
727
816
|
}
|
|
728
817
|
|
|
729
818
|
async function loadLearnings() {
|
|
@@ -909,9 +998,10 @@ document.querySelector('[data-tab="audit"]')?.addEventListener('click', loadAudi
|
|
|
909
998
|
|
|
910
999
|
loadOverview();
|
|
911
1000
|
loadSessions();
|
|
912
|
-
|
|
1001
|
+
loadMarkets();
|
|
913
1002
|
loadLearnings();
|
|
914
1003
|
loadWallet();
|
|
1004
|
+
document.querySelector('[data-tab="markets"]')?.addEventListener('click', loadMarkets);
|
|
915
1005
|
setInterval(() => api('wallet').then(w => {
|
|
916
1006
|
if (w) {
|
|
917
1007
|
document.getElementById('balance').textContent = usdBig(w.balance) + ' USDC';
|
package/dist/panel/server.js
CHANGED
|
@@ -13,7 +13,8 @@ import { listSessions, loadSessionHistory } from '../session/storage.js';
|
|
|
13
13
|
import { searchSessions } from '../session/search.js';
|
|
14
14
|
import { loadLearnings } from '../learnings/store.js';
|
|
15
15
|
import { readAudit } from '../stats/audit.js';
|
|
16
|
-
import {
|
|
16
|
+
import { snapshot as marketsSnapshot } from '../trading/providers/telemetry.js';
|
|
17
|
+
import { describeWiring } from '../trading/providers/registry.js';
|
|
17
18
|
import { getHTML } from './html.js';
|
|
18
19
|
const sseClients = new Set();
|
|
19
20
|
function json(res, data, status = 200) {
|
|
@@ -312,9 +313,19 @@ export function createPanelServer(port) {
|
|
|
312
313
|
}
|
|
313
314
|
return;
|
|
314
315
|
}
|
|
315
|
-
if (p === '/api/
|
|
316
|
-
|
|
317
|
-
|
|
316
|
+
if (p === '/api/markets') {
|
|
317
|
+
// Snapshot of every active data provider for the Markets panel:
|
|
318
|
+
// pipeline wiring (which endpoint serves which asset class), live
|
|
319
|
+
// health + latency per provider, and today's paid-call ledger.
|
|
320
|
+
const snap = marketsSnapshot();
|
|
321
|
+
const wiring = describeWiring();
|
|
322
|
+
json(res, {
|
|
323
|
+
chain: loadChain(),
|
|
324
|
+
wiring,
|
|
325
|
+
providers: snap.providers,
|
|
326
|
+
totals: snap.totals,
|
|
327
|
+
recentPaidCalls: snap.recentPaidCalls,
|
|
328
|
+
});
|
|
318
329
|
return;
|
|
319
330
|
}
|
|
320
331
|
if (p === '/api/learnings') {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivateTool — meta-capability that lets the agent pull on-demand tools
|
|
3
|
+
* into the active toolset per session.
|
|
4
|
+
*
|
|
5
|
+
* Pattern borrowed from OpenBB MCP server's per-session tool visibility:
|
|
6
|
+
* a weak model confronted with 25+ tool definitions starts inventing names
|
|
7
|
+
* or emits role-play "[TOOLCALL]" fragments. Register only the core file/
|
|
8
|
+
* shell tools by default and let the model explicitly opt in to the rest.
|
|
9
|
+
*
|
|
10
|
+
* Contract:
|
|
11
|
+
* - `ActivateTool()` with no args → lists every inactive tool with a
|
|
12
|
+
* one-line description so the model knows what's available.
|
|
13
|
+
* - `ActivateTool({ names: ["ExaSearch", "ExaReadUrls"] })` → adds the
|
|
14
|
+
* named tools to the session's active set; subsequent turns include
|
|
15
|
+
* their full schemas. Returns a concise confirmation.
|
|
16
|
+
*
|
|
17
|
+
* The factory captures the shared `activeTools` Set that the loop filters
|
|
18
|
+
* against and the full `allTools` map used for name resolution. Both live
|
|
19
|
+
* in the session — activation is not durable across restarts on purpose,
|
|
20
|
+
* since the model can always re-activate on the next turn if it needs to.
|
|
21
|
+
*/
|
|
22
|
+
import type { CapabilityHandler } from '../agent/types.js';
|
|
23
|
+
export interface ActivateToolDeps {
|
|
24
|
+
/** Mutable set of tool names currently visible to the model. */
|
|
25
|
+
activeTools: Set<string>;
|
|
26
|
+
/** Map of every registered capability, keyed by name. */
|
|
27
|
+
allTools: Map<string, CapabilityHandler>;
|
|
28
|
+
}
|
|
29
|
+
export declare function createActivateToolCapability(deps: ActivateToolDeps): CapabilityHandler;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivateTool — meta-capability that lets the agent pull on-demand tools
|
|
3
|
+
* into the active toolset per session.
|
|
4
|
+
*
|
|
5
|
+
* Pattern borrowed from OpenBB MCP server's per-session tool visibility:
|
|
6
|
+
* a weak model confronted with 25+ tool definitions starts inventing names
|
|
7
|
+
* or emits role-play "[TOOLCALL]" fragments. Register only the core file/
|
|
8
|
+
* shell tools by default and let the model explicitly opt in to the rest.
|
|
9
|
+
*
|
|
10
|
+
* Contract:
|
|
11
|
+
* - `ActivateTool()` with no args → lists every inactive tool with a
|
|
12
|
+
* one-line description so the model knows what's available.
|
|
13
|
+
* - `ActivateTool({ names: ["ExaSearch", "ExaReadUrls"] })` → adds the
|
|
14
|
+
* named tools to the session's active set; subsequent turns include
|
|
15
|
+
* their full schemas. Returns a concise confirmation.
|
|
16
|
+
*
|
|
17
|
+
* The factory captures the shared `activeTools` Set that the loop filters
|
|
18
|
+
* against and the full `allTools` map used for name resolution. Both live
|
|
19
|
+
* in the session — activation is not durable across restarts on purpose,
|
|
20
|
+
* since the model can always re-activate on the next turn if it needs to.
|
|
21
|
+
*/
|
|
22
|
+
function shortDesc(desc) {
|
|
23
|
+
// First sentence or first 120 chars, whichever is shorter.
|
|
24
|
+
const firstSentence = desc.split(/[.\n]/)[0]?.trim() ?? '';
|
|
25
|
+
if (firstSentence && firstSentence.length <= 120)
|
|
26
|
+
return firstSentence;
|
|
27
|
+
const trimmed = desc.replace(/\s+/g, ' ').trim();
|
|
28
|
+
return trimmed.length <= 120 ? trimmed : trimmed.slice(0, 117) + '...';
|
|
29
|
+
}
|
|
30
|
+
export function createActivateToolCapability(deps) {
|
|
31
|
+
const { activeTools, allTools } = deps;
|
|
32
|
+
return {
|
|
33
|
+
spec: {
|
|
34
|
+
name: 'ActivateTool',
|
|
35
|
+
description: 'Activate additional tools for this session. Most tools are hidden by default to keep your tool inventory small. ' +
|
|
36
|
+
'Call with no arguments to see what is available. Call with { "names": ["ToolA", "ToolB"] } to enable specific tools — ' +
|
|
37
|
+
'they become visible in your tool list on the next turn. Activate only what you need; extra tools crowd the inventory.',
|
|
38
|
+
input_schema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
names: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: { type: 'string' },
|
|
44
|
+
description: 'List of tool names to activate. Omit to list what is available.',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
concurrent: false,
|
|
50
|
+
async execute(input) {
|
|
51
|
+
const raw = input.names;
|
|
52
|
+
const names = Array.isArray(raw) ? raw.filter((n) => typeof n === 'string') : undefined;
|
|
53
|
+
// No args → catalog the inactive tools so the model knows what's there.
|
|
54
|
+
if (!names || names.length === 0) {
|
|
55
|
+
const inactive = [...allTools.values()]
|
|
56
|
+
.filter(t => !activeTools.has(t.spec.name))
|
|
57
|
+
.sort((a, b) => a.spec.name.localeCompare(b.spec.name));
|
|
58
|
+
if (inactive.length === 0) {
|
|
59
|
+
return { output: 'All registered tools are already active.' };
|
|
60
|
+
}
|
|
61
|
+
const lines = inactive.map(t => `- ${t.spec.name}: ${shortDesc(t.spec.description)}`);
|
|
62
|
+
return {
|
|
63
|
+
output: `Available on-demand tools (${inactive.length}). Activate with ` +
|
|
64
|
+
`ActivateTool({ "names": ["<name>", ...] }):\n` +
|
|
65
|
+
lines.join('\n'),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// Activate each named tool.
|
|
69
|
+
const activated = [];
|
|
70
|
+
const alreadyActive = [];
|
|
71
|
+
const unknown = [];
|
|
72
|
+
for (const name of names) {
|
|
73
|
+
if (!allTools.has(name)) {
|
|
74
|
+
unknown.push(name);
|
|
75
|
+
}
|
|
76
|
+
else if (activeTools.has(name)) {
|
|
77
|
+
alreadyActive.push(name);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
activeTools.add(name);
|
|
81
|
+
activated.push(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const parts = [];
|
|
85
|
+
if (activated.length)
|
|
86
|
+
parts.push(`Activated: ${activated.join(', ')}`);
|
|
87
|
+
if (alreadyActive.length)
|
|
88
|
+
parts.push(`Already active: ${alreadyActive.join(', ')}`);
|
|
89
|
+
if (unknown.length)
|
|
90
|
+
parts.push(`Unknown (not registered): ${unknown.join(', ')}`);
|
|
91
|
+
const output = parts.length ? parts.join('. ') + '.' : 'No change.';
|
|
92
|
+
const isError = activated.length === 0 && unknown.length > 0;
|
|
93
|
+
return { output, isError };
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { tradingSignalCapability, tradingMarketCapability } from './trading.js';
|
|
|
22
22
|
import { searchXCapability } from './searchx.js';
|
|
23
23
|
import { postToXCapability } from './posttox.js';
|
|
24
24
|
import { moaCapability } from './moa.js';
|
|
25
|
+
import { webhookPostCapability } from './webhook.js';
|
|
25
26
|
import { createTradingCapabilities } from './trading-execute.js';
|
|
26
27
|
import { Portfolio } from '../trading/portfolio.js';
|
|
27
28
|
import { RiskEngine } from '../trading/risk.js';
|
|
@@ -138,6 +139,7 @@ export const allCapabilities = [
|
|
|
138
139
|
searchXCapability,
|
|
139
140
|
postToXCapability,
|
|
140
141
|
moaCapability,
|
|
142
|
+
webhookPostCapability,
|
|
141
143
|
];
|
|
142
144
|
export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
|
|
143
145
|
export { createSubAgentCapability } from './subagent.js';
|