@blockrun/franklin 3.10.6 → 3.12.0

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.
@@ -205,6 +205,13 @@ You run on the BlockRun AI Gateway. When the user asks you to "test the BlockRun
205
205
  - \`GET /v1/models\` — full model catalog (id, owner, context window, pricing).
206
206
  - \`GET /v1/health/overview\` · \`/v1/health/regions\` · \`/v1/health/chain\` · \`/v1/health/models\` — gateway status.
207
207
 
208
+ **Trading & DeFi (mixed methods, x402-paid; new in v3.12.0)**
209
+ - \`GET /v1/jupiter/quote?inputMint=...&outputMint=...&amount=...&slippageBps=50\` — Solana DEX-aggregator price quote across every DEX Jupiter knows. \$0.001/call.
210
+ - \`POST /v1/jupiter/swap\` — body \`{ userPublicKey, quoteResponse }\`. Returns a base64-encoded **unsigned** Solana transaction. Caller signs locally; gateway never custodies keys. \$0.001/call.
211
+ - \`GET /v1/defillama/protocols\` · \`/v1/defillama/protocol/{slug}\` · \`/v1/defillama/chains\` · \`/v1/defillama/yields\` — TVL / yield-pool data, Apache-2.0 source. \$0.005/call.
212
+ - \`GET /v1/defillama/prices/{coins}\` — token price lookup (coingecko:bitcoin, ethereum:0x..., solana:mint, comma-separated). \$0.001/call.
213
+ - \`POST /v1/solana/rpc\` — JSON-RPC passthrough to public mainnet-beta (getAccountInfo, getTokenSupply, sendTransaction, etc.). \$0.0005 per call (per element of a batch). Use this instead of running your own RPC infra.
214
+
208
215
  **Sandbox (POST, x402-paid)**
209
216
  - \`/v1/modal/{...path}\` — Modal GPU sandbox passthrough (create/exec/etc.).
210
217
  - \`/v1/pm/{...path}\` — prediction-market data passthrough.
@@ -17,7 +17,6 @@ import { resetToolSessionState } from '../tools/index.js';
17
17
  import { CORE_TOOL_NAMES, dynamicToolsEnabled } from '../tools/tool-categories.js';
18
18
  import { createActivateToolCapability } from '../tools/activate.js';
19
19
  import { recordUsage } from '../stats/tracker.js';
20
- import { loadConfig } from '../commands/config.js';
21
20
  import { recordSessionUsage } from '../stats/session-tracker.js';
22
21
  import { appendAudit, extractLastUserPrompt } from '../stats/audit.js';
23
22
  import { estimateCost, OPUS_PRICING } from '../pricing.js';
@@ -436,18 +435,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
436
435
  const cmdResult = await handleSlashCommand(input, {
437
436
  history, config, client, sessionId, onEvent,
438
437
  skillRegistry,
439
- skillVars: getSkillVars({
440
- chain: config.chain,
441
- perTurnCapUsd: (() => {
442
- const raw = loadConfig()['max-turn-spend-usd'];
443
- if (raw == null)
444
- return 1.0;
445
- const n = Number(raw);
446
- if (!Number.isFinite(n))
447
- return 1.0;
448
- return n <= 0 ? Infinity : n;
449
- })(),
450
- }),
438
+ skillVars: getSkillVars({ chain: config.chain }),
451
439
  });
452
440
  if (cmdResult.handled)
453
441
  continue;
@@ -551,37 +539,6 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
551
539
  // ── No-progress guardrail: kill infinite tiny-response loops ──
552
540
  let consecutiveTinyResponses = 0; // Count of consecutive calls with <10 output tokens
553
541
  const MAX_TINY_RESPONSES = 2; // Break after N tiny responses — if 2 calls return near-empty, something is wrong
554
- let turnSpend = 0; // Cost spent this user turn (USD)
555
- // Hard circuit breaker per user message — defends user wallets against
556
- // a runaway model+tool combo on a single prompt. User-overridable via
557
- // `franklin config set max-turn-spend-usd <number>`. Explicit "0" or a
558
- // negative number disables the cap; a non-numeric / unparseable value
559
- // is treated as a typo and falls back to the safe default rather than
560
- // silently removing the wallet guard.
561
- //
562
- // Default lineage: $0.25 (≤ v3.8.41) → $1.00 (v3.8.42) → $2.00 (v3.9.1).
563
- // v3.8.42's $1.00 was tuned for "multi-stage dashboard scaffold" workloads
564
- // landing in the $0.20–$0.80 range on a single prompt. Real coding turns
565
- // since — full BTC-style dashboards, multi-file refactors that pull in
566
- // sonnet/opus on a COMPLEX-tier route — routinely cross $1.00 in their
567
- // first planning pass alone, leaving no headroom for the execution call
568
- // and tripping the cap mid-task. $2.00 keeps the runaway-protection
569
- // promise (catches the buggy-loop drain v3.8.41's retry-policy targets)
570
- // while letting a legitimate complex coding task finish in one turn.
571
- // Users who liked the old ceiling can pin it via the config.
572
- const TURN_SPEND_DEFAULT_USD = 2.0;
573
- const turnSpendCap = (() => {
574
- const raw = loadConfig()['max-turn-spend-usd'];
575
- if (raw == null)
576
- return TURN_SPEND_DEFAULT_USD;
577
- const parsed = Number(raw);
578
- if (!Number.isFinite(parsed))
579
- return TURN_SPEND_DEFAULT_USD; // typo → keep default
580
- if (parsed <= 0)
581
- return Infinity; // explicit opt-out
582
- return parsed;
583
- })();
584
- const MAX_TURN_SPEND_USD = turnSpendCap;
585
542
  // ── Turn analysis (one classifier call, drives routing + prefetch) ──
586
543
  // Single LLM pass that answers every routing-adjacent question the
587
544
  // harness needs BEFORE the main model runs: tier, ticker intent,
@@ -1120,20 +1077,12 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1120
1077
  const costEstimate = estimateCost(resolvedModel, inputTokens, usage.outputTokens, 1);
1121
1078
  recordUsage(resolvedModel, inputTokens, usage.outputTokens, costEstimate, 0);
1122
1079
  // ── Circuit breakers: prevent infinite-loop wallet drain ──
1123
- turnSpend += costEstimate;
1124
- if (turnSpend > MAX_TURN_SPEND_USD) {
1125
- const capDisplay = MAX_TURN_SPEND_USD === Infinity ? '∞' : `$${MAX_TURN_SPEND_USD.toFixed(2)}`;
1126
- onEvent({
1127
- kind: 'text_delta',
1128
- text: `\n\n⚠️ Turn spend limit reached ($${turnSpend.toFixed(3)} > ${capDisplay}). Stopping to protect your wallet.\n\n` +
1129
- `What to do next — pick ONE (do NOT just type a number, that becomes a new prompt):\n` +
1130
- ` • Continue this turn: /retry\n` +
1131
- ` • Raise cap to $4: franklin config set max-turn-spend-usd 4\n` +
1132
- ` • Disable cap entirely: franklin config set max-turn-spend-usd 0 (then /retry)\n`,
1133
- });
1134
- onEvent({ kind: 'turn_done', reason: 'budget' });
1135
- break;
1136
- }
1080
+ // Per-turn $-cap was removed in v3.11.0 — runaway loops are caught by
1081
+ // MAX_TOOL_CALLS_PER_TURN (25) and MAX_TINY_RESPONSES (2) above; the
1082
+ // wallet balance itself is the ultimate ceiling. Batch callers that
1083
+ // need a hard $ ceiling can still pass `config.maxSpendUsd` (handled
1084
+ // a few lines below).
1085
+ //
1137
1086
  // Count a response as "no progress" only if it made no meaningful output:
1138
1087
  // no tool call, and no text content longer than a few chars. A short but
1139
1088
  // legitimate response (e.g. "done" or a compact tool_use) resets the counter.
@@ -6,15 +6,6 @@ export interface AppConfig {
6
6
  'smart-routing'?: string;
7
7
  'permission-mode'?: string;
8
8
  'max-turns'?: string;
9
- /**
10
- * Hard per-turn spend ceiling in USD (default $1.00 as of v3.8.42).
11
- * Numeric string, e.g. "0.5" or "2". Set to "0" to disable the cap.
12
- * The agent loop stops a turn the moment cumulative cost crosses this
13
- * threshold, preventing a runaway model + tool combo from draining the
14
- * wallet. Earlier versions used $0.25, which routinely fired on legit
15
- * multi-stage tasks (dashboard scaffolds, image-to-image edits).
16
- */
17
- 'max-turn-spend-usd'?: string;
18
9
  'auto-compact'?: string;
19
10
  'session-save'?: string;
20
11
  'debug'?: string;
@@ -12,7 +12,6 @@ const VALID_KEYS = [
12
12
  'smart-routing',
13
13
  'permission-mode',
14
14
  'max-turns',
15
- 'max-turn-spend-usd',
16
15
  'auto-compact',
17
16
  'session-save',
18
17
  'debug',
@@ -21,7 +21,5 @@ export interface BundledLoad {
21
21
  export declare function loadBundledSkills(): BundledLoad;
22
22
  export interface SkillVarSource {
23
23
  chain?: 'base' | 'solana';
24
- /** Per-turn spend cap in USD; mirrors the `max-turn-spend-usd` config key. */
25
- perTurnCapUsd?: number;
26
24
  }
27
25
  export declare function getSkillVars(src: SkillVarSource): Record<string, string>;
@@ -28,13 +28,5 @@ export function getSkillVars(src) {
28
28
  const out = {};
29
29
  if (src.chain)
30
30
  out.wallet_chain = src.chain;
31
- if (typeof src.perTurnCapUsd === 'number' && Number.isFinite(src.perTurnCapUsd)) {
32
- const cap = src.perTurnCapUsd.toFixed(2);
33
- out.per_turn_cap = cap;
34
- // Skills run at turn boundary, so spent-this-turn is always zero at
35
- // substitution time and remaining equals the cap.
36
- out.spent_this_turn = '0.00';
37
- out.turn_budget_remaining = cap;
38
- }
39
31
  return out;
40
32
  }
@@ -43,6 +43,9 @@ export function parseSkill(content) {
43
43
  if (typeof fields['cost-receipt'] === 'boolean') {
44
44
  skill.costReceipt = fields['cost-receipt'];
45
45
  }
46
+ if (Array.isArray(fields.triggers)) {
47
+ skill.triggers = fields.triggers.filter((t) => typeof t === 'string');
48
+ }
46
49
  return { skill, warnings };
47
50
  }
48
51
  function extractFrontmatter(content) {
@@ -57,6 +60,7 @@ function extractFrontmatter(content) {
57
60
  const body = rest.slice(closeIdx + 1 + FRONTMATTER_FENCE.length + 1);
58
61
  return { frontmatter, body };
59
62
  }
63
+ const LIST_ITEM_RE = /^\s+-\s+(.+)$/;
60
64
  function parseFrontmatter(text) {
61
65
  const fields = {};
62
66
  const warnings = [];
@@ -65,6 +69,9 @@ function parseFrontmatter(text) {
65
69
  const line = lines[i];
66
70
  if (line.trim() === '' || line.trim().startsWith('#'))
67
71
  continue;
72
+ // YAML list continuation: skip — handled by the key-line that opened it.
73
+ if (LIST_ITEM_RE.test(line))
74
+ continue;
68
75
  const colon = line.indexOf(':');
69
76
  if (colon < 0) {
70
77
  return { error: `frontmatter line ${i + 1} is not key: value — got "${line}"` };
@@ -74,6 +81,30 @@ function parseFrontmatter(text) {
74
81
  if (key.length === 0) {
75
82
  return { error: `frontmatter line ${i + 1} has empty key` };
76
83
  }
84
+ // Empty value followed by indented `- item` lines = YAML list.
85
+ if (rawValue === '') {
86
+ const items = [];
87
+ let j = i + 1;
88
+ while (j < lines.length) {
89
+ const next = lines[j];
90
+ if (next.trim() === '') {
91
+ j++;
92
+ continue;
93
+ }
94
+ const m = LIST_ITEM_RE.exec(next);
95
+ if (!m)
96
+ break;
97
+ items.push(parseScalarString(m[1].trim()));
98
+ j++;
99
+ }
100
+ if (items.length > 0) {
101
+ fields[key] = items;
102
+ continue;
103
+ }
104
+ // No list followed; treat as empty string.
105
+ fields[key] = '';
106
+ continue;
107
+ }
77
108
  fields[key] = parseScalar(rawValue);
78
109
  }
79
110
  return { fields, warnings };
@@ -140,7 +171,10 @@ function parseScalar(raw) {
140
171
  return Number.parseInt(raw, 10);
141
172
  if (/^-?\d+\.\d+$/.test(raw))
142
173
  return Number.parseFloat(raw);
143
- // Strip surrounding quotes if present.
174
+ return parseScalarString(raw);
175
+ }
176
+ /** Strip surrounding quotes from a string-shaped value, leaving everything else as-is. */
177
+ function parseScalarString(raw) {
144
178
  if ((raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) ||
145
179
  (raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2)) {
146
180
  return raw.slice(1, -1);
@@ -21,6 +21,8 @@ export interface ParsedSkill {
21
21
  budgetCapUsd?: number;
22
22
  /** Franklin extension: append a paid-call receipt under the agent reply. */
23
23
  costReceipt?: boolean;
24
+ /** Trigger phrases that should auto-invoke this skill when matched. */
25
+ triggers?: string[];
24
26
  }
25
27
  export type ParseResult = {
26
28
  skill: ParsedSkill;
@@ -1,11 +1,22 @@
1
1
  ---
2
2
  name: budget-grill
3
3
  description: Wallet-aware grilling — interview me about a plan one question at a time, with each branch of the decision tree framed as a USDC cost impact
4
+ triggers:
5
+ - "grill my plan"
6
+ - "interview my plan"
7
+ - "budget review"
8
+ - "cost analysis"
9
+ - "wallet drain"
10
+ - "spending review"
11
+ - "cost impact"
12
+ - "plan review"
13
+ - "challenge my idea"
14
+ - "stress test plan"
4
15
  argument-hint: <plan or topic to grill on>
5
16
  cost-receipt: true
6
17
  ---
7
18
 
8
- You are running inside Franklin, an Economic Agent powered by an x402 USDC wallet on {{wallet_chain}}. The user has set a per-turn spend cap of {{per_turn_cap}} USDC, of which {{spent_this_turn}} has been spent so far ({{turn_budget_remaining}} remaining).
19
+ You are running inside Franklin, an Economic Agent powered by an x402 USDC wallet on {{wallet_chain}}. The user funds the wallet directly; every paid call ($-priced tools, model API calls) draws against that balance, so wasteful spending shows up immediately on the receipt.
9
20
 
10
21
  Your job: interview the user relentlessly about the plan below, **one question at a time**, until you reach a shared understanding of every branch of the decision tree. For every question, also propose your recommended answer and the reasoning behind it.
11
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.10.6",
3
+ "version": "3.12.0",
4
4
  "description": "Franklin — 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": {