@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.
- package/dist/agent/context.js +6 -0
- package/dist/agent/loop.js +3 -5
- package/dist/router/index.d.ts +6 -0
- package/dist/router/index.js +36 -1
- package/dist/tools/trading.js +83 -22
- package/package.json +1 -1
package/dist/agent/context.js
CHANGED
|
@@ -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() {
|
package/dist/agent/loop.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
package/dist/router/index.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/router/index.js
CHANGED
|
@@ -483,10 +483,45 @@ function computeSavings(model) {
|
|
|
483
483
|
*/
|
|
484
484
|
export function getFallbackChain(tier, profile = 'auto') {
|
|
485
485
|
if (profile === 'free')
|
|
486
|
-
return
|
|
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
|
*/
|
package/dist/tools/trading.js
CHANGED
|
@@ -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 =
|
|
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
|
-
//
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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 =
|
|
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
|
|
83
|
-
|
|
84
|
-
`- **Bollinger:** Upper ${bbResult.upper
|
|
85
|
-
`- **Volatility:** ${(volResult.annualized * 100
|
|
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
|
|
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
|
|
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