@blockrun/franklin 3.15.11 → 3.15.13

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.
@@ -326,6 +326,12 @@ Your training data is frozen in the past. Live-world questions MUST be answered
326
326
 
327
327
  If you find yourself about to emit one of these, stop and call the tool instead. If you don't know which ticker the user means, call ExaSearch or AskUser — never deflect.
328
328
 
329
+ **Trading verdicts (TradingSignal).** When the user asks "how does $TICKER look" / "should I buy X" / "is BTC overbought":
330
+ - Run **TradingSignal** with default lookback (90d). Lower values leave MACD undefined.
331
+ - The tool returns a **Verdict** section with \`Direction\`, \`Bull signals\`, \`Bear signals\`. Echo it directly. Do not soften "bullish" to "leaning slightly positive" — say what the data says.
332
+ - If \`Data Notes\` lists an indicator as "insufficient data", state that explicitly to the user and suggest re-running with more days. Do NOT pretend that indicator is "neutral".
333
+ - **Forbidden default**: "持有观望", "wait and see", "hold for clearer signals" — these are bugs when ≥2 indicators voted in a clear direction. Bail out to those phrases ONLY when (a) the Verdict says \`neutral\` AND (b) the bull/bear signal lists are both genuinely empty or one of each. Otherwise commit to a direction with the reasoning the tool already gave you.
334
+
329
335
  **Media generation (ImageGen / VideoGen).** Pass just the user's descriptive prompt and the output path — do NOT pass \`model\`. The harness picks the right model for the requested style + budget, refines loose prompts using a 5-slot template (scene / subject / details / use case / constraints), and surfaces both the refinement and a cost proposal through AskUser before spending. If the user wants their prompt left exactly as written, prefix it with \`///\` to skip refinement. Only pass \`model\` explicitly if the user named one specifically.`;
330
336
  }
331
337
  function getTokenEfficiencySection() {
@@ -24,7 +24,7 @@ import { logger, setDebugMode } from '../logger.js';
24
24
  import { estimateCost, OPUS_PRICING } from '../pricing.js';
25
25
  import { maybeMidSessionExtract } from '../learnings/extractor.js';
26
26
  import { extractMentions, buildEntityContext, loadEntities } from '../brain/store.js';
27
- import { routeRequestAsync, resolveTierToModel, parseRoutingProfile, getFallbackChain } from '../router/index.js';
27
+ import { routeRequestAsync, resolveTierToModel, parseRoutingProfile, getFallbackChain, pickFreeFallback } from '../router/index.js';
28
28
  import { recordOutcome } from '../router/local-elo.js';
29
29
  import { shouldPlan, getPlanningPrompt, getExecutorModel, isExecutorStuck, toolCallSignature } from './planner.js';
30
30
  import { shouldVerify, runVerification } from './verification.js';
@@ -1098,8 +1098,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1098
1098
  if (lastRoutedCategory) {
1099
1099
  recordOutcome(lastRoutedCategory, config.model, 'payment');
1100
1100
  }
1101
- const FREE_MODELS = ['nvidia/qwen3-coder-480b', 'nvidia/llama-4-maverick', 'nvidia/glm-4.7'];
1102
- const nextFree = FREE_MODELS.find(m => !turnFailedModels.has(m));
1101
+ const nextFree = pickFreeFallback(lastRoutedCategory, turnFailedModels);
1103
1102
  if (nextFree) {
1104
1103
  const oldModel = config.model;
1105
1104
  config.model = nextFree;
@@ -1120,8 +1119,7 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1120
1119
  if (lastRoutedCategory) {
1121
1120
  recordOutcome(lastRoutedCategory, config.model, 'rate_limit');
1122
1121
  }
1123
- const FREE_MODELS = ['nvidia/qwen3-coder-480b', 'nvidia/llama-4-maverick', 'nvidia/glm-4.7'];
1124
- const nextFree = FREE_MODELS.find(m => !turnFailedModels.has(m));
1122
+ const nextFree = pickFreeFallback(lastRoutedCategory, turnFailedModels);
1125
1123
  if (nextFree) {
1126
1124
  const oldModel = config.model;
1127
1125
  config.model = nextFree;
@@ -49,6 +49,12 @@ export declare function routeRequest(prompt: string, profile?: RoutingProfile):
49
49
  * Get fallback models for a tier
50
50
  */
51
51
  export declare function getFallbackChain(tier: Tier, profile?: RoutingProfile): string[];
52
+ /**
53
+ * Pick the next free model to try given the question category and which
54
+ * free models have already failed this turn. Returns undefined when every
55
+ * candidate has been exhausted (caller should surface an error to user).
56
+ */
57
+ export declare function pickFreeFallback(category: string, alreadyFailed: Set<string>): string | undefined;
52
58
  /**
53
59
  * Parse routing profile from model string
54
60
  */
@@ -483,10 +483,45 @@ function computeSavings(model) {
483
483
  */
484
484
  export function getFallbackChain(tier, profile = 'auto') {
485
485
  if (profile === 'free')
486
- return ['nvidia/qwen3-coder-480b'];
486
+ return FREE_MODELS_BY_CATEGORY.chat;
487
487
  const config = AUTO_TIERS[tier];
488
488
  return [config.primary, ...config.fallback];
489
489
  }
490
+ // ─── Free-tier fallback (used when paid models 402 / rate-limit) ───
491
+ // Free fallback chains by question category. Used when a paid model fails
492
+ // mid-turn (402 payment, rate-limit) and we need a zero-cost replacement
493
+ // to keep the user moving without waiting for funding.
494
+ //
495
+ // The lists are ordered: best-fit free model first, then degraded fallbacks.
496
+ // Coding goes to qwen3-coder; everything else (chat / trading / research /
497
+ // reasoning / creative) prefers general-purpose free models that aren't
498
+ // coder-tuned. Without this split, a BTC question that exhausted paid
499
+ // models was being handed to qwen3-coder-480b — a coder model trying to
500
+ // do technical analysis. Reported 2026-05-03 with a markets question
501
+ // routed to a coder model on Sonnet failure.
502
+ const FREE_MODELS_BY_CATEGORY = {
503
+ coding: ['nvidia/qwen3-coder-480b', 'nvidia/glm-4.7', 'nvidia/llama-4-maverick'],
504
+ trading: ['nvidia/glm-4.7', 'nvidia/llama-4-maverick', 'nvidia/qwen3-coder-480b'],
505
+ research: ['nvidia/glm-4.7', 'nvidia/llama-4-maverick', 'nvidia/qwen3-coder-480b'],
506
+ reasoning: ['nvidia/glm-4.7', 'nvidia/qwen3-coder-480b', 'nvidia/llama-4-maverick'],
507
+ chat: ['nvidia/llama-4-maverick', 'nvidia/glm-4.7', 'nvidia/qwen3-coder-480b'],
508
+ creative: ['nvidia/llama-4-maverick', 'nvidia/glm-4.7', 'nvidia/qwen3-coder-480b'],
509
+ };
510
+ const DEFAULT_FREE_CHAIN = [
511
+ 'nvidia/glm-4.7',
512
+ 'nvidia/llama-4-maverick',
513
+ 'nvidia/qwen3-coder-480b',
514
+ ];
515
+ /**
516
+ * Pick the next free model to try given the question category and which
517
+ * free models have already failed this turn. Returns undefined when every
518
+ * candidate has been exhausted (caller should surface an error to user).
519
+ */
520
+ export function pickFreeFallback(category, alreadyFailed) {
521
+ const chain = FREE_MODELS_BY_CATEGORY[category]
522
+ ?? DEFAULT_FREE_CHAIN;
523
+ return chain.find(m => !alreadyFailed.has(m));
524
+ }
490
525
  /**
491
526
  * Parse routing profile from model string
492
527
  */
@@ -16,8 +16,19 @@ function formatUsd(n) {
16
16
  return `$${(n / 1e3).toFixed(1)}K`;
17
17
  return `$${n.toFixed(2)}`;
18
18
  }
19
+ // MACD needs slow EMA (26) + signal EMA (9) = 35 closes minimum for the
20
+ // signal/histogram to be defined. Default was 30, which left signal=NaN
21
+ // and trend stuck at 'neutral' on every call — see the 2026-05-03 BTC
22
+ // report where the agent had to write "MACD signal can't be computed
23
+ // due to insufficient data". 90d gives stable MACD plus enough room for
24
+ // reasonable Bollinger bandwidth and annualized volatility readings.
25
+ const DEFAULT_LOOKBACK_DAYS = 90;
26
+ const MIN_DAYS_FOR_MACD = 35;
27
+ function fmtNumber(n, digits) {
28
+ return Number.isFinite(n) ? n.toFixed(digits) : 'n/a';
29
+ }
19
30
  async function executeSignal(input, _ctx) {
20
- const { ticker, days = 30 } = input;
31
+ const { ticker, days = DEFAULT_LOOKBACK_DAYS } = input;
21
32
  if (!ticker) {
22
33
  return { output: 'Error: ticker is required', isError: true };
23
34
  }
@@ -37,23 +48,45 @@ async function executeSignal(input, _ctx) {
37
48
  const macdResult = macd(closes);
38
49
  const bbResult = bollingerBands(closes);
39
50
  const volResult = volatility(closes);
40
- // Determine overall direction from indicators
51
+ // Per-indicator validity. Each has its own minimum sample requirement
52
+ // and we surface the gap rather than silently defaulting to 'neutral'.
53
+ const macdValid = Number.isFinite(macdResult.signal) && Number.isFinite(macdResult.histogram);
54
+ const dataNotes = [];
55
+ if (!macdValid) {
56
+ dataNotes.push(`MACD signal/histogram unavailable — need ≥${MIN_DAYS_FOR_MACD} closes, got ${closes.length}. ` +
57
+ `Re-run with days=${MIN_DAYS_FOR_MACD} or higher for full trend detection.`);
58
+ }
59
+ // Direction count — only valid indicators contribute. A NaN MACD must
60
+ // not be counted as a 'neutral' vote, otherwise the agent reads weak
61
+ // data as a reason to recommend "wait and see".
41
62
  let bullish = 0;
42
63
  let bearish = 0;
43
- if (rsiResult.interpretation === 'oversold')
44
- bullish++;
45
- if (rsiResult.interpretation === 'overbought')
46
- bearish++;
47
- if (macdResult.trend === 'bullish')
48
- bullish++;
49
- if (macdResult.trend === 'bearish')
50
- bearish++;
51
- if (bbResult.position === 'below')
52
- bullish++;
53
- if (bbResult.position === 'above')
54
- bearish++;
64
+ let votingIndicators = 0;
65
+ if (Number.isFinite(rsiResult.value)) {
66
+ votingIndicators++;
67
+ if (rsiResult.interpretation === 'oversold')
68
+ bullish++;
69
+ if (rsiResult.interpretation === 'overbought')
70
+ bearish++;
71
+ }
72
+ if (macdValid) {
73
+ votingIndicators++;
74
+ if (macdResult.trend === 'bullish')
75
+ bullish++;
76
+ if (macdResult.trend === 'bearish')
77
+ bearish++;
78
+ }
79
+ if (Number.isFinite(bbResult.middle)) {
80
+ votingIndicators++;
81
+ if (bbResult.position === 'below')
82
+ bullish++;
83
+ if (bbResult.position === 'above')
84
+ bearish++;
85
+ }
55
86
  const direction = bullish > bearish ? 'bullish' : bearish > bullish ? 'bearish' : 'neutral';
56
- const confidence = Math.max(bullish, bearish) / 3;
87
+ const confidence = votingIndicators > 0
88
+ ? Math.max(bullish, bearish) / votingIndicators
89
+ : 0;
57
90
  bus.emit(makeEvent({
58
91
  type: 'signal.detected',
59
92
  source: 'trading',
@@ -71,6 +104,28 @@ async function executeSignal(input, _ctx) {
71
104
  }));
72
105
  const { price, change24h, marketCap, volume24h } = priceResult;
73
106
  const last5 = closes.slice(-5).map(c => c.toFixed(2)).join(', ');
107
+ // MACD line: when signal/histogram are NaN, say so explicitly instead
108
+ // of rendering "1822.7300 / Signal: NaN / Histogram: NaN — neutral",
109
+ // which read as a real signal to translation models.
110
+ const macdLine = macdValid
111
+ ? `- **MACD:** ${fmtNumber(macdResult.macd, 4)} / Signal: ${fmtNumber(macdResult.signal, 4)} / Histogram: ${fmtNumber(macdResult.histogram, 4)} — ${macdResult.trend}`
112
+ : `- **MACD:** ${fmtNumber(macdResult.macd, 4)} / Signal: insufficient data / Histogram: insufficient data — *not enough closes for trend*`;
113
+ // Bull / bear breakdown so the agent can echo a real verdict instead
114
+ // of falling back to "wait and see".
115
+ const bullSignals = [];
116
+ const bearSignals = [];
117
+ if (rsiResult.interpretation === 'oversold')
118
+ bullSignals.push('RSI oversold');
119
+ if (rsiResult.interpretation === 'overbought')
120
+ bearSignals.push('RSI overbought');
121
+ if (macdValid && macdResult.trend === 'bullish')
122
+ bullSignals.push('MACD trending up');
123
+ if (macdValid && macdResult.trend === 'bearish')
124
+ bearSignals.push('MACD trending down');
125
+ if (Number.isFinite(bbResult.middle) && bbResult.position === 'below')
126
+ bullSignals.push('price below lower Bollinger');
127
+ if (Number.isFinite(bbResult.middle) && bbResult.position === 'above')
128
+ bearSignals.push('price above upper Bollinger');
74
129
  const output = [
75
130
  `## ${upper} Signal Report`,
76
131
  '',
@@ -78,11 +133,17 @@ async function executeSignal(input, _ctx) {
78
133
  `**Market Cap:** ${formatUsd(marketCap)}`,
79
134
  `**24h Volume:** ${formatUsd(volume24h)}`,
80
135
  '',
81
- `### Technical Indicators (${days}d lookback)`,
82
- `- **RSI(14):** ${rsiResult.value.toFixed(1)} — ${rsiResult.interpretation}`,
83
- `- **MACD:** ${macdResult.macd.toFixed(4)} / Signal: ${macdResult.signal.toFixed(4)} / Histogram: ${macdResult.histogram.toFixed(4)} — ${macdResult.trend}`,
84
- `- **Bollinger:** Upper ${bbResult.upper.toFixed(2)} / Middle ${bbResult.middle.toFixed(2)} / Lower ${bbResult.lower.toFixed(2)} — Price ${bbResult.position}`,
85
- `- **Volatility:** ${(volResult.annualized * 100).toFixed(1)}% annualized — ${volResult.interpretation}`,
136
+ `### Technical Indicators (${days}d lookback, ${closes.length} closes)`,
137
+ `- **RSI(14):** ${fmtNumber(rsiResult.value, 1)} — ${rsiResult.interpretation}`,
138
+ macdLine,
139
+ `- **Bollinger:** Upper ${fmtNumber(bbResult.upper, 2)} / Middle ${fmtNumber(bbResult.middle, 2)} / Lower ${fmtNumber(bbResult.lower, 2)} — Price ${bbResult.position}`,
140
+ `- **Volatility:** ${fmtNumber(volResult.annualized * 100, 1)}% annualized — ${volResult.interpretation}`,
141
+ '',
142
+ `### Verdict`,
143
+ `**Direction:** ${direction} (${votingIndicators} indicator${votingIndicators === 1 ? '' : 's'} voting, confidence ${(confidence * 100).toFixed(0)}%)`,
144
+ bullSignals.length > 0 ? `**Bull signals:** ${bullSignals.join(', ')}` : '**Bull signals:** none',
145
+ bearSignals.length > 0 ? `**Bear signals:** ${bearSignals.join(', ')}` : '**Bear signals:** none',
146
+ ...(dataNotes.length > 0 ? ['', `### Data Notes`, ...dataNotes.map(n => `- ${n}`)] : []),
86
147
  '',
87
148
  `### Raw Data`,
88
149
  `Closes (last 5): ${last5}`,
@@ -92,12 +153,12 @@ async function executeSignal(input, _ctx) {
92
153
  export const tradingSignalCapability = {
93
154
  spec: {
94
155
  name: 'TradingSignal',
95
- description: 'Get current price, technical indicators (RSI, MACD, Bollinger Bands, volatility), and a signal summary for a cryptocurrency. Returns raw data for the agent to analyze and interpret.',
156
+ description: 'Get current price, technical indicators (RSI, MACD, Bollinger Bands, volatility), and a verdict (bullish / bearish / neutral with confidence) for a cryptocurrency. Always returns a Verdict section with bull/bear signal lists — echo it directly. When MACD signal/histogram report "insufficient data", say so explicitly; do NOT default to "wait and see".',
96
157
  input_schema: {
97
158
  type: 'object',
98
159
  properties: {
99
160
  ticker: { type: 'string', description: 'Cryptocurrency ticker, e.g. "BTC", "ETH"' },
100
- days: { type: 'number', description: 'Lookback period for indicators. Default: 30' },
161
+ days: { type: 'number', description: 'Lookback period in days. Default 90 (recommended). Below 35 will leave MACD signal/histogram undefined.' },
101
162
  },
102
163
  required: ['ticker'],
103
164
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.11",
3
+ "version": "3.15.13",
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": {