@blockrun/franklin 3.8.44 → 3.9.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.
Files changed (41) hide show
  1. package/README.md +3 -2
  2. package/dist/agent/commands.d.ts +5 -0
  3. package/dist/agent/commands.js +28 -0
  4. package/dist/agent/compact.js +1 -1
  5. package/dist/agent/context.js +1 -0
  6. package/dist/agent/llm.js +4 -4
  7. package/dist/agent/loop.js +28 -3
  8. package/dist/agent/verification.js +2 -2
  9. package/dist/commands/balance-retry.d.ts +15 -0
  10. package/dist/commands/balance-retry.js +20 -0
  11. package/dist/commands/skills.d.ts +8 -0
  12. package/dist/commands/skills.js +93 -0
  13. package/dist/commands/social.js +1 -1
  14. package/dist/commands/start.js +17 -13
  15. package/dist/commands/telegram.js +1 -1
  16. package/dist/index.js +9 -0
  17. package/dist/learnings/extractor.js +3 -3
  18. package/dist/plugin-sdk/workflow.js +2 -2
  19. package/dist/pricing.js +1 -1
  20. package/dist/proxy/fallback.js +1 -1
  21. package/dist/proxy/server.js +10 -10
  22. package/dist/router/index.js +8 -8
  23. package/dist/skills/bootstrap.d.ts +27 -0
  24. package/dist/skills/bootstrap.js +40 -0
  25. package/dist/skills/invoke.d.ts +23 -0
  26. package/dist/skills/invoke.js +38 -0
  27. package/dist/skills/loader.d.ts +21 -0
  28. package/dist/skills/loader.js +149 -0
  29. package/dist/skills/registry.d.ts +26 -0
  30. package/dist/skills/registry.js +54 -0
  31. package/dist/skills/types.d.ts +47 -0
  32. package/dist/skills/types.js +8 -0
  33. package/dist/skills-bundled/budget-grill/SKILL.md +24 -0
  34. package/dist/tools/index.js +2 -0
  35. package/dist/tools/moa.js +4 -4
  36. package/dist/tools/subagent.js +3 -3
  37. package/dist/tools/tool-categories.js +3 -0
  38. package/dist/tools/wallet.d.ts +23 -0
  39. package/dist/tools/wallet.js +63 -0
  40. package/dist/ui/model-picker.js +13 -17
  41. package/package.json +4 -3
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Boot-time helpers that wire the skills library into the running process:
3
+ *
4
+ * - `loadBundledSkills()` discovers `dist/skills-bundled/<name>/SKILL.md`
5
+ * relative to this module's location and returns a populated Registry.
6
+ * User-global and project-local discovery are deferred to Phase 2 of the
7
+ * skills MVP plan; today we only ship the bundled set.
8
+ *
9
+ * - `getSkillVars()` returns the synchronously-known runtime variables
10
+ * that `substituteVariables` injects into a skill body before
11
+ * `$ARGUMENTS` expansion. Async values (wallet balance, on-chain reads)
12
+ * are deferred to a later phase: those vars stay literal in the rendered
13
+ * prompt and `substituteVariables` leaves unknown vars intact.
14
+ */
15
+ import { fileURLToPath } from 'node:url';
16
+ import { dirname, join } from 'node:path';
17
+ import { loadSkillsFromDir } from './loader.js';
18
+ import { Registry } from './registry.js';
19
+ const HERE = dirname(fileURLToPath(import.meta.url));
20
+ // Built form lives at dist/skills/bootstrap.js, so dist/skills-bundled/
21
+ // is one level up + sibling.
22
+ const BUNDLED_DIR = join(HERE, '..', 'skills-bundled');
23
+ export function loadBundledSkills() {
24
+ const result = loadSkillsFromDir(BUNDLED_DIR, 'bundled');
25
+ return { registry: Registry.fromLoaded(result.skills), errors: result.errors };
26
+ }
27
+ export function getSkillVars(src) {
28
+ const out = {};
29
+ if (src.chain)
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
+ return out;
40
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Skill invocation helpers.
3
+ *
4
+ * `substituteVariables` is the only piece of the invocation path that runs
5
+ * for every skill: it inlines `{{wallet_balance}}` and similar runtime
6
+ * context, then expands `$ARGUMENTS` to the trailing slash-command argument.
7
+ *
8
+ * Both substitutions use function-form replacement so that values containing
9
+ * `$` or other replacement-pattern meta-characters (like a user task that
10
+ * mentions "find $5 of value") are inserted verbatim.
11
+ */
12
+ export declare function substituteVariables(body: string, vars: Record<string, string>, args: string): string;
13
+ import type { Registry } from './registry.js';
14
+ export interface SkillMatch {
15
+ rewritten: string;
16
+ }
17
+ /**
18
+ * Pure dispatch: given a slash-command input line, look up the skill in the
19
+ * registry, render its body against runtime variables and arguments, and
20
+ * return the prompt the agent should run. Returns null when the input is
21
+ * not a slash command, the slash is bare, or no skill of that name exists.
22
+ */
23
+ export declare function matchSkill(input: string, registry: Registry, vars: Record<string, string>): SkillMatch | null;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Skill invocation helpers.
3
+ *
4
+ * `substituteVariables` is the only piece of the invocation path that runs
5
+ * for every skill: it inlines `{{wallet_balance}}` and similar runtime
6
+ * context, then expands `$ARGUMENTS` to the trailing slash-command argument.
7
+ *
8
+ * Both substitutions use function-form replacement so that values containing
9
+ * `$` or other replacement-pattern meta-characters (like a user task that
10
+ * mentions "find $5 of value") are inserted verbatim.
11
+ */
12
+ const VAR_PATTERN = /\{\{(\w+)\}\}/g;
13
+ export function substituteVariables(body, vars, args) {
14
+ const withVars = body.replace(VAR_PATTERN, (match, key) => {
15
+ return Object.prototype.hasOwnProperty.call(vars, key) ? vars[key] : match;
16
+ });
17
+ return withVars.replaceAll('$ARGUMENTS', () => args);
18
+ }
19
+ /**
20
+ * Pure dispatch: given a slash-command input line, look up the skill in the
21
+ * registry, render its body against runtime variables and arguments, and
22
+ * return the prompt the agent should run. Returns null when the input is
23
+ * not a slash command, the slash is bare, or no skill of that name exists.
24
+ */
25
+ export function matchSkill(input, registry, vars) {
26
+ if (!input.startsWith('/'))
27
+ return null;
28
+ const space = input.indexOf(' ');
29
+ const name = (space < 0 ? input : input.slice(0, space)).slice(1);
30
+ if (name.length === 0)
31
+ return null;
32
+ const skill = registry.lookup(name);
33
+ if (!skill)
34
+ return null;
35
+ const args = space < 0 ? '' : input.slice(space + 1).trim();
36
+ const rewritten = substituteVariables(skill.skill.body, vars, args);
37
+ return { rewritten };
38
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * SKILL.md loader — parses Anthropic-spec frontmatter + markdown body.
3
+ *
4
+ * The frontmatter parser is intentionally minimal: flat `key: value` lines
5
+ * with scalar values (string, boolean, number). Quoted strings, nested
6
+ * objects, and arrays are out of scope for MVP — Anthropic's SKILL.md spec
7
+ * never needs them and adding a YAML dependency would be heavier than the
8
+ * spec warrants.
9
+ */
10
+ import type { LoadResult, ParseResult, SkillSource } from './types.js';
11
+ export declare function parseSkill(content: string): ParseResult;
12
+ /**
13
+ * Discover and parse every `<dir>/<name>/SKILL.md` under `root`.
14
+ *
15
+ * Missing root → empty result, no error (callers happily union project +
16
+ * user + bundled, and absent dirs are normal).
17
+ *
18
+ * Parse failures on individual skills do not abort the load; they are
19
+ * surfaced via `LoadResult.errors` so the caller can warn the user.
20
+ */
21
+ export declare function loadSkillsFromDir(root: string, source: SkillSource): LoadResult;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * SKILL.md loader — parses Anthropic-spec frontmatter + markdown body.
3
+ *
4
+ * The frontmatter parser is intentionally minimal: flat `key: value` lines
5
+ * with scalar values (string, boolean, number). Quoted strings, nested
6
+ * objects, and arrays are out of scope for MVP — Anthropic's SKILL.md spec
7
+ * never needs them and adding a YAML dependency would be heavier than the
8
+ * spec warrants.
9
+ */
10
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ const FRONTMATTER_FENCE = '---';
13
+ export function parseSkill(content) {
14
+ const fmMatch = extractFrontmatter(content);
15
+ if (!fmMatch) {
16
+ return { error: 'missing frontmatter (file must start with --- fence)' };
17
+ }
18
+ const { frontmatter, body } = fmMatch;
19
+ const parsedFrontmatter = parseFrontmatter(frontmatter);
20
+ if ('error' in parsedFrontmatter)
21
+ return { error: parsedFrontmatter.error };
22
+ const { fields, warnings } = parsedFrontmatter;
23
+ if (typeof fields.name !== 'string' || fields.name.length === 0) {
24
+ return { error: 'frontmatter missing required field: name' };
25
+ }
26
+ if (typeof fields.description !== 'string' || fields.description.length === 0) {
27
+ return { error: 'frontmatter missing required field: description' };
28
+ }
29
+ const skill = {
30
+ name: fields.name,
31
+ description: fields.description,
32
+ body,
33
+ };
34
+ if (typeof fields['argument-hint'] === 'string') {
35
+ skill.argumentHint = fields['argument-hint'];
36
+ }
37
+ if (typeof fields['disable-model-invocation'] === 'boolean') {
38
+ skill.disableModelInvocation = fields['disable-model-invocation'];
39
+ }
40
+ if (typeof fields['budget-cap-usd'] === 'number') {
41
+ skill.budgetCapUsd = fields['budget-cap-usd'];
42
+ }
43
+ if (typeof fields['cost-receipt'] === 'boolean') {
44
+ skill.costReceipt = fields['cost-receipt'];
45
+ }
46
+ return { skill, warnings };
47
+ }
48
+ function extractFrontmatter(content) {
49
+ const normalized = content.replace(/\r\n/g, '\n');
50
+ if (!normalized.startsWith(FRONTMATTER_FENCE + '\n'))
51
+ return null;
52
+ const rest = normalized.slice(FRONTMATTER_FENCE.length + 1);
53
+ const closeIdx = rest.indexOf('\n' + FRONTMATTER_FENCE + '\n');
54
+ if (closeIdx < 0)
55
+ return null;
56
+ const frontmatter = rest.slice(0, closeIdx);
57
+ const body = rest.slice(closeIdx + 1 + FRONTMATTER_FENCE.length + 1);
58
+ return { frontmatter, body };
59
+ }
60
+ function parseFrontmatter(text) {
61
+ const fields = {};
62
+ const warnings = [];
63
+ const lines = text.split('\n');
64
+ for (let i = 0; i < lines.length; i++) {
65
+ const line = lines[i];
66
+ if (line.trim() === '' || line.trim().startsWith('#'))
67
+ continue;
68
+ const colon = line.indexOf(':');
69
+ if (colon < 0) {
70
+ return { error: `frontmatter line ${i + 1} is not key: value — got "${line}"` };
71
+ }
72
+ const key = line.slice(0, colon).trim();
73
+ const rawValue = line.slice(colon + 1).trim();
74
+ if (key.length === 0) {
75
+ return { error: `frontmatter line ${i + 1} has empty key` };
76
+ }
77
+ fields[key] = parseScalar(rawValue);
78
+ }
79
+ return { fields, warnings };
80
+ }
81
+ /**
82
+ * Discover and parse every `<dir>/<name>/SKILL.md` under `root`.
83
+ *
84
+ * Missing root → empty result, no error (callers happily union project +
85
+ * user + bundled, and absent dirs are normal).
86
+ *
87
+ * Parse failures on individual skills do not abort the load; they are
88
+ * surfaced via `LoadResult.errors` so the caller can warn the user.
89
+ */
90
+ export function loadSkillsFromDir(root, source) {
91
+ const result = { skills: [], errors: [] };
92
+ let entries;
93
+ try {
94
+ entries = readdirSync(root);
95
+ }
96
+ catch {
97
+ return result;
98
+ }
99
+ for (const entry of entries) {
100
+ const dirPath = join(root, entry);
101
+ let entryStat;
102
+ try {
103
+ entryStat = statSync(dirPath);
104
+ }
105
+ catch {
106
+ continue;
107
+ }
108
+ if (!entryStat.isDirectory())
109
+ continue;
110
+ const skillPath = join(dirPath, 'SKILL.md');
111
+ let content;
112
+ try {
113
+ content = readFileSync(skillPath, 'utf8');
114
+ }
115
+ catch {
116
+ continue;
117
+ }
118
+ const parsed = parseSkill(content);
119
+ if ('error' in parsed) {
120
+ result.errors.push({ path: skillPath, error: parsed.error });
121
+ continue;
122
+ }
123
+ const warnings = [...parsed.warnings];
124
+ let skill = parsed.skill;
125
+ if (skill.name !== entry) {
126
+ warnings.push(`frontmatter name "${skill.name}" disagrees with directory "${entry}"; using directory name`);
127
+ skill = { ...skill, name: entry };
128
+ }
129
+ const loaded = { skill, source, path: skillPath, warnings };
130
+ result.skills.push(loaded);
131
+ }
132
+ return result;
133
+ }
134
+ function parseScalar(raw) {
135
+ if (raw === 'true')
136
+ return true;
137
+ if (raw === 'false')
138
+ return false;
139
+ if (/^-?\d+$/.test(raw))
140
+ return Number.parseInt(raw, 10);
141
+ if (/^-?\d+\.\d+$/.test(raw))
142
+ return Number.parseFloat(raw);
143
+ // Strip surrounding quotes if present.
144
+ if ((raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) ||
145
+ (raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2)) {
146
+ return raw.slice(1, -1);
147
+ }
148
+ return raw;
149
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Skill registry — resolves name conflicts across sources and exposes
3
+ * lookup, list, and shadow-set queries for `/help` and
4
+ * `franklin skills list`.
5
+ *
6
+ * Precedence: project > user > bundled. Within the same source, the first
7
+ * loaded skill wins so that the order returned by the loader (which is
8
+ * filesystem-ordered) is the deterministic tiebreaker the user can rely on.
9
+ *
10
+ * Built-in slash commands (e.g. `/security`) take precedence over skills
11
+ * with the same name, but that check happens at the dispatch layer in
12
+ * `src/agent/commands.ts`. The registry never sees the built-in list.
13
+ */
14
+ import type { LoadedSkill } from './types.js';
15
+ export interface ShadowEntry {
16
+ winner: LoadedSkill;
17
+ loser: LoadedSkill;
18
+ }
19
+ export declare class Registry {
20
+ private readonly byName;
21
+ private readonly shadows;
22
+ static fromLoaded(loaded: LoadedSkill[]): Registry;
23
+ lookup(name: string): LoadedSkill | undefined;
24
+ list(): LoadedSkill[];
25
+ shadowed(): ShadowEntry[];
26
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Skill registry — resolves name conflicts across sources and exposes
3
+ * lookup, list, and shadow-set queries for `/help` and
4
+ * `franklin skills list`.
5
+ *
6
+ * Precedence: project > user > bundled. Within the same source, the first
7
+ * loaded skill wins so that the order returned by the loader (which is
8
+ * filesystem-ordered) is the deterministic tiebreaker the user can rely on.
9
+ *
10
+ * Built-in slash commands (e.g. `/security`) take precedence over skills
11
+ * with the same name, but that check happens at the dispatch layer in
12
+ * `src/agent/commands.ts`. The registry never sees the built-in list.
13
+ */
14
+ const SOURCE_PRIORITY = {
15
+ project: 3,
16
+ user: 2,
17
+ bundled: 1,
18
+ };
19
+ export class Registry {
20
+ byName = new Map();
21
+ shadows = [];
22
+ static fromLoaded(loaded) {
23
+ const reg = new Registry();
24
+ // Stable sort by source priority (desc); ties broken by original order
25
+ // so that within a single source, the first-loaded skill wins.
26
+ const indexed = loaded.map((l, i) => ({ l, i }));
27
+ indexed.sort((a, b) => {
28
+ const pa = SOURCE_PRIORITY[a.l.source];
29
+ const pb = SOURCE_PRIORITY[b.l.source];
30
+ if (pa !== pb)
31
+ return pb - pa;
32
+ return a.i - b.i;
33
+ });
34
+ for (const { l } of indexed) {
35
+ const existing = reg.byName.get(l.skill.name);
36
+ if (!existing) {
37
+ reg.byName.set(l.skill.name, l);
38
+ }
39
+ else {
40
+ reg.shadows.push({ winner: existing, loser: l });
41
+ }
42
+ }
43
+ return reg;
44
+ }
45
+ lookup(name) {
46
+ return this.byName.get(name);
47
+ }
48
+ list() {
49
+ return [...this.byName.values()].sort((a, b) => a.skill.name.localeCompare(b.skill.name));
50
+ }
51
+ shadowed() {
52
+ return [...this.shadows];
53
+ }
54
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Public types for Franklin's skills layer.
3
+ *
4
+ * A "skill" is an Anthropic-spec SKILL.md file: YAML frontmatter + a markdown
5
+ * body that becomes the prompt-rewrite for a slash command. See
6
+ * docs/plans/2026-04-29-franklin-skills-mvp-design.md.
7
+ */
8
+ export type SkillSource = 'bundled' | 'user' | 'project';
9
+ export interface ParsedSkill {
10
+ /** Kebab-case skill identifier; matches the parent directory name. */
11
+ name: string;
12
+ /** Short description shown in /help and franklin skills list. */
13
+ description: string;
14
+ /** Raw markdown body (not yet variable-substituted). */
15
+ body: string;
16
+ /** Anthropic spec: hint shown after the slash command. */
17
+ argumentHint?: string;
18
+ /** Anthropic spec: when true, the model must not auto-invoke this skill. */
19
+ disableModelInvocation?: boolean;
20
+ /** Franklin extension: hard cap (USD) for the turn this skill kicks off. */
21
+ budgetCapUsd?: number;
22
+ /** Franklin extension: append a paid-call receipt under the agent reply. */
23
+ costReceipt?: boolean;
24
+ }
25
+ export type ParseResult = {
26
+ skill: ParsedSkill;
27
+ warnings: string[];
28
+ } | {
29
+ error: string;
30
+ };
31
+ export interface LoadedSkill {
32
+ skill: ParsedSkill;
33
+ source: SkillSource;
34
+ /** Absolute path to the SKILL.md file. */
35
+ path: string;
36
+ /** Non-fatal warnings raised while loading this skill. */
37
+ warnings: string[];
38
+ }
39
+ export interface LoadError {
40
+ /** Absolute path to the file that failed. */
41
+ path: string;
42
+ error: string;
43
+ }
44
+ export interface LoadResult {
45
+ skills: LoadedSkill[];
46
+ errors: LoadError[];
47
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Public types for Franklin's skills layer.
3
+ *
4
+ * A "skill" is an Anthropic-spec SKILL.md file: YAML frontmatter + a markdown
5
+ * body that becomes the prompt-rewrite for a slash command. See
6
+ * docs/plans/2026-04-29-franklin-skills-mvp-design.md.
7
+ */
8
+ export {};
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: budget-grill
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
+ argument-hint: <plan or topic to grill on>
5
+ cost-receipt: true
6
+ ---
7
+
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).
9
+
10
+ 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
+
12
+ The thing that makes this skill different from a generic grilling session: **frame every option in cost terms**. For each branch, estimate the USDC spend per call/run/cycle, the model tier it would land on, and the worst-case wallet drain over the lifetime of the feature. If the option spends $0 because it's free-tier, say so explicitly. If it depends on a paid tool (`ExaSearch`, `ImageGen`, `VideoGen`, `MusicGen`, `TradingMarket` paid actions), name the tool and estimate the per-call cost.
13
+
14
+ Rules of engagement:
15
+
16
+ 1. **One question per response.** Do not stack questions.
17
+ 2. **Walk down the decision tree.** Resolve dependencies between decisions one by one — a question that depends on the answer to another comes later.
18
+ 3. **Recommend an answer.** Every question carries your recommendation + the cost-impact reasoning behind it.
19
+ 4. **Cross-reference the codebase.** If a question can be answered by reading the code, read the code instead of asking. Use `Read`, `Grep`, `Glob`. The user's time is more expensive than tool calls.
20
+ 5. **Stop at saturation, not exhaustion.** When the marginal next question stops uncovering new cost trade-offs or design decisions, propose the agreed plan back to the user as a numbered summary, with each step's projected cost and the running total.
21
+
22
+ The plan or topic to grill on:
23
+
24
+ $ARGUMENTS
@@ -23,6 +23,7 @@ import { searchXCapability } from './searchx.js';
23
23
  import { postToXCapability } from './posttox.js';
24
24
  import { moaCapability } from './moa.js';
25
25
  import { webhookPostCapability } from './webhook.js';
26
+ import { walletCapability } from './wallet.js';
26
27
  import { createTradingCapabilities } from './trading-execute.js';
27
28
  import { Portfolio } from '../trading/portfolio.js';
28
29
  import { RiskEngine } from '../trading/risk.js';
@@ -140,6 +141,7 @@ export const allCapabilities = [
140
141
  postToXCapability,
141
142
  moaCapability,
142
143
  webhookPostCapability,
144
+ walletCapability,
143
145
  ];
144
146
  export { readCapability, writeCapability, editCapability, bashCapability, globCapability, grepCapability, webFetchCapability, webSearchCapability, taskCapability, };
145
147
  export { createSubAgentCapability } from './subagent.js';
package/dist/tools/moa.js CHANGED
@@ -14,14 +14,14 @@ import { ModelClient } from '../agent/llm.js';
14
14
  // ─── Configuration ────────────────────────────────────────────────────────
15
15
  /** Reference models — diverse, cheap/free models for parallel queries. */
16
16
  const REFERENCE_MODELS = [
17
- 'nvidia/glm-4.7', // Free, strong reasoning + coding
18
- 'nvidia/qwen3-next-80b-a3b-thinking', // Free, explicit reasoning model
19
- 'nvidia/qwen3-coder-480b', // Free, strong coding
17
+ 'nvidia/qwen3-coder-480b', // Free, agent-tested coding
18
+ 'nvidia/llama-4-maverick', // Free, agent-tested general chat
19
+ 'nvidia/glm-4.7', // Free chat fallback
20
20
  'google/gemini-2.5-flash', // Fast, cheap
21
21
  'deepseek/deepseek-chat', // Cheap, good reasoning
22
22
  ];
23
23
  /** Aggregator model — free by default. Users explicitly pass `aggregator` to upgrade. */
24
- const AGGREGATOR_MODEL = 'nvidia/glm-4.7';
24
+ const AGGREGATOR_MODEL = 'nvidia/qwen3-coder-480b';
25
25
  /** Max tokens per reference response. */
26
26
  const REFERENCE_MAX_TOKENS = 4096;
27
27
  /** Max tokens for aggregator. */
@@ -18,7 +18,7 @@ async function execute(input, ctx) {
18
18
  return { output: 'Error: prompt is required', isError: true };
19
19
  }
20
20
  // Resolve which model the sub-agent will actually run on
21
- const subModel = model || registeredParentModel || 'nvidia/glm-4.7';
21
+ const subModel = model || registeredParentModel || 'nvidia/qwen3-coder-480b';
22
22
  // Cost gate: if parent is free but sub-agent wants paid, ask user first.
23
23
  // Prevents silent charges when the agent decides to spawn a more capable sub-agent.
24
24
  if (isFreeModel(registeredParentModel) && !isFreeModel(subModel)) {
@@ -27,14 +27,14 @@ async function execute(input, ctx) {
27
27
  // No way to prompt the user (daemon/panel/non-interactive mode).
28
28
  // Fail closed — refuse the paid spawn rather than silently charging.
29
29
  return {
30
- output: `Sub-agent declined: parent is on a free model but sub-agent requested a paid model (${shortLabel}). No interactive prompt available. Retry with model='nemotron' or run interactively to approve.`,
30
+ output: `Sub-agent declined: parent is on a free model but sub-agent requested a paid model (${shortLabel}). No interactive prompt available. Retry with model='free' or run interactively to approve.`,
31
31
  isError: true,
32
32
  };
33
33
  }
34
34
  const answer = await ctx.onAskUser(`Sub-agent wants to use ${shortLabel} (paid). Approve?`, ['y', 'n']);
35
35
  if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
36
36
  return {
37
- output: `Sub-agent skipped — user declined paid model (${shortLabel}). Retry with a free model like nemotron.`,
37
+ output: `Sub-agent skipped — user declined paid model (${shortLabel}). Retry with a free model like free.`,
38
38
  isError: true,
39
39
  };
40
40
  }
@@ -50,6 +50,9 @@ export const CORE_TOOL_NAMES = new Set([
50
50
  // enough that every model tends to pick it correctly.
51
51
  'WebFetch',
52
52
  'WebSearch',
53
+ // Wallet read — Franklin is the agent with a wallet, so balance + chain
54
+ // + address must be a one-call answer rather than a Bash shell-out.
55
+ 'Wallet',
53
56
  ]);
54
57
  /** True if this tool is always available without activation. */
55
58
  export function isCoreTool(name) {
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Wallet capability — direct read of Franklin's wallet status.
3
+ *
4
+ * Why this exists as a first-class tool: without it, "what's my balance"
5
+ * routes through Bash (`franklin balance`) plus parsing, which costs both
6
+ * extra tool turns and ~1KB of bash-tool framing in the model's input
7
+ * window every turn. With Wallet as a dedicated zero-arg tool, the agent
8
+ * can answer balance questions in one cheap call. It also preserves the
9
+ * Economic-Agent positioning: the wallet is a first-class concept, not a
10
+ * shell command.
11
+ *
12
+ * Bash is still available for non-trivial wallet operations (sending,
13
+ * signing arbitrary tx) — Wallet is read-only by design.
14
+ */
15
+ import type { CapabilityHandler } from '../agent/types.js';
16
+ export interface WalletReportInput {
17
+ chain: 'base' | 'solana';
18
+ address: string;
19
+ balanceUsd: number;
20
+ }
21
+ /** Pure formatter — small, deterministic, easy to unit-test. */
22
+ export declare function formatWalletReport(input: WalletReportInput): string;
23
+ export declare const walletCapability: CapabilityHandler;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Wallet capability — direct read of Franklin's wallet status.
3
+ *
4
+ * Why this exists as a first-class tool: without it, "what's my balance"
5
+ * routes through Bash (`franklin balance`) plus parsing, which costs both
6
+ * extra tool turns and ~1KB of bash-tool framing in the model's input
7
+ * window every turn. With Wallet as a dedicated zero-arg tool, the agent
8
+ * can answer balance questions in one cheap call. It also preserves the
9
+ * Economic-Agent positioning: the wallet is a first-class concept, not a
10
+ * shell command.
11
+ *
12
+ * Bash is still available for non-trivial wallet operations (sending,
13
+ * signing arbitrary tx) — Wallet is read-only by design.
14
+ */
15
+ import { loadChain } from '../config.js';
16
+ /** Pure formatter — small, deterministic, easy to unit-test. */
17
+ export function formatWalletReport(input) {
18
+ return [
19
+ `Chain: ${input.chain}`,
20
+ `Address: ${input.address}`,
21
+ `USDC Balance: $${input.balanceUsd.toFixed(2)}`,
22
+ ].join('\n');
23
+ }
24
+ async function execute() {
25
+ const chain = loadChain();
26
+ try {
27
+ if (chain === 'solana') {
28
+ const { setupAgentSolanaWallet } = await import('@blockrun/llm');
29
+ const c = await setupAgentSolanaWallet({ silent: true });
30
+ const address = await c.getWalletAddress();
31
+ const balance = await c.getBalance();
32
+ return { output: formatWalletReport({ chain, address, balanceUsd: balance }) };
33
+ }
34
+ const { setupAgentWallet } = await import('@blockrun/llm');
35
+ const c = setupAgentWallet({ silent: true });
36
+ const address = c.getWalletAddress();
37
+ const balance = await c.getBalance();
38
+ return { output: formatWalletReport({ chain, address, balanceUsd: balance }) };
39
+ }
40
+ catch (err) {
41
+ const msg = err instanceof Error ? err.message : String(err);
42
+ return {
43
+ output: `Wallet read failed (${msg}). The user may not have run \`franklin setup\` yet, ` +
44
+ `or the chain RPC is temporarily unreachable. Surface this to the user as-is.`,
45
+ isError: true,
46
+ };
47
+ }
48
+ }
49
+ export const walletCapability = {
50
+ spec: {
51
+ name: 'Wallet',
52
+ description: 'Read Franklin\'s wallet status — chain, address, and USDC balance. ' +
53
+ 'Use this for any "what\'s my balance / how much money / 钱包余额 / wallet status" question. ' +
54
+ 'Cheaper and more direct than running `franklin balance` via Bash, and never costs USDC.',
55
+ input_schema: {
56
+ type: 'object',
57
+ properties: {},
58
+ additionalProperties: false,
59
+ },
60
+ },
61
+ execute,
62
+ concurrent: true,
63
+ };
@@ -54,20 +54,22 @@ export const MODEL_SHORTCUTS = {
54
54
  // DeepSeek
55
55
  deepseek: 'deepseek/deepseek-chat',
56
56
  r1: 'deepseek/deepseek-reasoner',
57
- // Free (BlockRun gateway free tier — refreshed 2026-04)
58
- free: 'nvidia/glm-4.7',
59
- glm4: 'nvidia/glm-4.7',
60
- 'deepseek-free': 'nvidia/deepseek-v3.2',
57
+ // Free (agent-tested BlockRun gateway free tier — refreshed 2026-04)
58
+ free: 'nvidia/qwen3-coder-480b',
59
+ glm4: 'nvidia/qwen3-coder-480b',
60
+ 'deepseek-free': 'nvidia/qwen3-coder-480b',
61
61
  'qwen-coder': 'nvidia/qwen3-coder-480b',
62
- 'qwen-think': 'nvidia/qwen3-next-80b-a3b-thinking',
62
+ 'qwen-think': 'nvidia/qwen3-coder-480b',
63
63
  maverick: 'nvidia/llama-4-maverick',
64
- 'gpt-oss': 'nvidia/gpt-oss-120b',
65
- 'gpt-oss-small': 'nvidia/gpt-oss-20b',
66
- 'mistral-small': 'nvidia/mistral-small-4-119b',
67
- // Backward-compatibility aliases for models the gateway retired.
64
+ 'gpt-oss': 'nvidia/qwen3-coder-480b',
65
+ 'gpt-oss-small': 'nvidia/qwen3-coder-480b',
66
+ 'mistral-small': 'nvidia/llama-4-maverick',
67
+ // Backward-compatibility aliases for models the gateway retired or exposes
68
+ // unreliably on /v1/messages. Map to agent-tested free models so shortcuts
69
+ // keep working without silent paid fallback or empty tool-use turns.
68
70
  // Map to the closest current free model so old session records + user
69
71
  // muscle memory keep working.
70
- nemotron: 'nvidia/glm-4.7',
72
+ nemotron: 'nvidia/qwen3-coder-480b',
71
73
  devstral: 'nvidia/qwen3-coder-480b',
72
74
  // Others
73
75
  minimax: 'minimax/minimax-m2.7',
@@ -154,14 +156,8 @@ export const PICKER_CATEGORIES = [
154
156
  {
155
157
  category: '🆓 Free (no USDC needed)',
156
158
  models: [
157
- { id: 'nvidia/glm-4.7', shortcut: 'free', label: 'GLM-4.7', price: 'FREE' },
158
- { id: 'nvidia/qwen3-next-80b-a3b-thinking', shortcut: 'qwen-think', label: 'Qwen3-Next 80B Thinking', price: 'FREE' },
159
- { id: 'nvidia/qwen3-coder-480b', shortcut: 'qwen-coder', label: 'Qwen3 Coder 480B', price: 'FREE' },
159
+ { id: 'nvidia/qwen3-coder-480b', shortcut: 'free', label: 'Qwen3 Coder 480B', price: 'FREE' },
160
160
  { id: 'nvidia/llama-4-maverick', shortcut: 'maverick', label: 'Llama 4 Maverick', price: 'FREE' },
161
- { id: 'nvidia/deepseek-v3.2', shortcut: 'deepseek-free', label: 'DeepSeek V3.2', price: 'FREE' },
162
- { id: 'nvidia/gpt-oss-120b', shortcut: 'gpt-oss', label: 'GPT-OSS 120B', price: 'FREE' },
163
- { id: 'nvidia/gpt-oss-20b', shortcut: 'gpt-oss-small', label: 'GPT-OSS 20B', price: 'FREE' },
164
- { id: 'nvidia/mistral-small-4-119b', shortcut: 'mistral-small', label: 'Mistral Small 4 119B', price: 'FREE' },
165
161
  ],
166
162
  },
167
163
  ];