@blockrun/franklin 3.15.77 → 3.15.79
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 +37 -0
- package/dist/commands/stats.js +65 -1
- package/dist/stats/cost-log.d.ts +66 -0
- package/dist/stats/cost-log.js +111 -0
- package/dist/tools/trading.js +41 -1
- package/package.json +1 -1
package/dist/agent/loop.js
CHANGED
|
@@ -177,6 +177,32 @@ function sanitizeHistory(history) {
|
|
|
177
177
|
* Detect media-related errors (image too large, too many images, PDF too large).
|
|
178
178
|
* These can be recovered by stripping media blocks and retrying.
|
|
179
179
|
*/
|
|
180
|
+
/**
|
|
181
|
+
* True when the assistant's last emitted text segment ends with a question
|
|
182
|
+
* mark (ASCII `?` or fullwidth `?`). Used to render an end-of-turn marker
|
|
183
|
+
* so users don't read the post-question silence as "Franklin died." Trim
|
|
184
|
+
* trailing whitespace + closing punctuation that doesn't change intent
|
|
185
|
+
* (newlines, single closing quote/paren) before checking.
|
|
186
|
+
*/
|
|
187
|
+
function endedWithQuestion(parts) {
|
|
188
|
+
if (!parts || parts.length === 0)
|
|
189
|
+
return false;
|
|
190
|
+
// Walk back to the last text segment. Skip thinking/tool_use parts.
|
|
191
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
192
|
+
const p = parts[i];
|
|
193
|
+
if (p.type !== 'text')
|
|
194
|
+
continue;
|
|
195
|
+
const text = p.text;
|
|
196
|
+
if (typeof text !== 'string')
|
|
197
|
+
return false;
|
|
198
|
+
// Strip trailing whitespace + the ~3 closing chars that commonly
|
|
199
|
+
// follow a question without changing it (")", "'", "\"", "*", ")",
|
|
200
|
+
// "*", whitespace).
|
|
201
|
+
const trimmed = text.replace(/[\s)\]'"*`)]+$/u, '');
|
|
202
|
+
return /[??]$/.test(trimmed);
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
180
206
|
function isMediaSizeError(msg) {
|
|
181
207
|
return ((msg.includes('image exceeds') && msg.includes('maximum')) ||
|
|
182
208
|
(msg.includes('image dimensions exceed')) ||
|
|
@@ -1737,6 +1763,17 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
|
|
|
1737
1763
|
if (lastRoutedCategory && lastRoutedModel) {
|
|
1738
1764
|
recordOutcome(lastRoutedCategory, lastRoutedModel, 'continued', turnToolCalls);
|
|
1739
1765
|
}
|
|
1766
|
+
// End-of-turn marker for question-shaped responses. Real-world UX
|
|
1767
|
+
// problem 2026-05-06: agent finishes a turn with "要我查一下 X 吗?"
|
|
1768
|
+
// and stops; the user reads the silence as "Franklin died" twice in
|
|
1769
|
+
// one hour. The Ink input box is already on screen but it's easy to
|
|
1770
|
+
// miss after a long output scroll. A single trailing italic line
|
|
1771
|
+
// makes the wait state explicit. Only fires when the model's last
|
|
1772
|
+
// emitted text ends with `?` or `?` so non-question turns don't
|
|
1773
|
+
// get a noisy hint.
|
|
1774
|
+
if (endedWithQuestion(responseParts)) {
|
|
1775
|
+
onEvent({ kind: 'text_delta', text: '\n*▸ awaiting your reply (or type a new message)*\n' });
|
|
1776
|
+
}
|
|
1740
1777
|
onEvent({ kind: 'turn_done', reason: 'completed' });
|
|
1741
1778
|
break;
|
|
1742
1779
|
}
|
package/dist/commands/stats.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { clearStats, getStatsSummary } from '../stats/tracker.js';
|
|
7
|
+
import { summarizeSdkSettlements } from '../stats/cost-log.js';
|
|
7
8
|
export function statsCommand(options) {
|
|
8
9
|
if (options.clear) {
|
|
9
10
|
clearStats();
|
|
@@ -11,6 +12,27 @@ export function statsCommand(options) {
|
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
const { stats, opusCost, saved, savedPct, avgCostPerRequest, period } = getStatsSummary();
|
|
15
|
+
// SDK ledger reconciliation. `franklin-stats.json` only captures requests
|
|
16
|
+
// that flowed through Franklin's `recordUsage()` paths (main agent loop +
|
|
17
|
+
// proxy). Helper LLM calls and SDK-internal probes settle x402 payments
|
|
18
|
+
// through `~/.blockrun/cost_log.jsonl` (SDK-owned) — adding it here so
|
|
19
|
+
// the user sees the wire-level total alongside Franklin's recorded one.
|
|
20
|
+
// The gap between the two = recording instrumentation that's still
|
|
21
|
+
// missing from helper paths (analyzeTurn, compaction, evaluator, etc.).
|
|
22
|
+
const sdkLedger = summarizeSdkSettlements();
|
|
23
|
+
const recordedTotal = stats.totalCostUsd;
|
|
24
|
+
const sdkTotal = sdkLedger.totalUsd;
|
|
25
|
+
const gap = sdkTotal - recordedTotal;
|
|
26
|
+
const gapPct = sdkTotal > 0 ? (gap / sdkTotal) * 100 : 0;
|
|
27
|
+
// Bidirectional check. Two distinct gap meanings:
|
|
28
|
+
// sdkTotal > recordedTotal → helper LLM calls / SDK probes settled
|
|
29
|
+
// on-chain but bypassed Franklin's recordUsage. The ledger is the
|
|
30
|
+
// wire truth; recorded total is incomplete.
|
|
31
|
+
// sdkTotal < recordedTotal → cost_log.jsonl was probably rotated /
|
|
32
|
+
// truncated since the stats started accumulating. Recorded total is
|
|
33
|
+
// more complete; the ledger is just the recent slice.
|
|
34
|
+
// Treat any gap > $0.01 OR > 5% (in either direction) as worth flagging.
|
|
35
|
+
const significantGap = sdkTotal > 0 && (Math.abs(gap) > 0.01 || Math.abs(gapPct) > 5);
|
|
14
36
|
// JSON output for programmatic access
|
|
15
37
|
if (options.json) {
|
|
16
38
|
console.log(JSON.stringify({
|
|
@@ -22,6 +44,21 @@ export function statsCommand(options) {
|
|
|
22
44
|
avgCostPerRequest,
|
|
23
45
|
period,
|
|
24
46
|
},
|
|
47
|
+
sdkLedger: {
|
|
48
|
+
path: sdkLedger.path,
|
|
49
|
+
entries: sdkLedger.count,
|
|
50
|
+
totalUsd: sdkTotal,
|
|
51
|
+
byEndpoint: sdkLedger.byEndpoint.slice(0, 10),
|
|
52
|
+
firstTs: sdkLedger.firstTs,
|
|
53
|
+
lastTs: sdkLedger.lastTs,
|
|
54
|
+
},
|
|
55
|
+
reconciliation: {
|
|
56
|
+
recordedUsd: recordedTotal,
|
|
57
|
+
sdkLedgerUsd: sdkTotal,
|
|
58
|
+
gapUsd: gap,
|
|
59
|
+
gapPct,
|
|
60
|
+
significantGap,
|
|
61
|
+
},
|
|
25
62
|
}, null, 2));
|
|
26
63
|
return;
|
|
27
64
|
}
|
|
@@ -36,7 +73,22 @@ export function statsCommand(options) {
|
|
|
36
73
|
// Overview
|
|
37
74
|
console.log(chalk.bold('\n Overview') + chalk.gray(` (${period})\n`));
|
|
38
75
|
console.log(` Requests: ${chalk.cyan(stats.totalRequests.toLocaleString())}`);
|
|
39
|
-
console.log(`
|
|
76
|
+
console.log(` Recorded Cost: ${chalk.green('$' + stats.totalCostUsd.toFixed(4))}` +
|
|
77
|
+
chalk.gray(' (franklin-stats.json — main loop + proxy + tools that call recordUsage)'));
|
|
78
|
+
if (sdkTotal > 0) {
|
|
79
|
+
const ledgerColor = significantGap ? chalk.yellow : chalk.green;
|
|
80
|
+
console.log(` SDK Ledger: ${ledgerColor('$' + sdkTotal.toFixed(4))}` +
|
|
81
|
+
chalk.gray(` (cost_log.jsonl — actual x402 settlements, ${sdkLedger.count} rows)`));
|
|
82
|
+
if (significantGap) {
|
|
83
|
+
const explanation = gap > 0
|
|
84
|
+
? 'helper LLM calls (analyzeTurn / compaction / evaluator / verification / subagent / MoA / etc.) settled on-chain but bypassed recordUsage. SDK ledger is the wire truth.'
|
|
85
|
+
: 'cost_log.jsonl looks rotated or truncated — it covers fewer rows than franklin-stats.json. Recorded total is more complete than the ledger here.';
|
|
86
|
+
console.log(chalk.yellow(` ⚠ Gap: $${Math.abs(gap).toFixed(4)} (${Math.abs(gapPct).toFixed(1)}%) ${gap > 0 ? '↑' : '↓'} — ${explanation}`));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log(chalk.gray(` Gap: $${gap.toFixed(4)} (${gapPct.toFixed(1)}%)`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
40
92
|
console.log(` Avg per Request: ${chalk.gray('$' + avgCostPerRequest.toFixed(6))}`);
|
|
41
93
|
console.log(` Input Tokens: ${stats.totalInputTokens.toLocaleString()}`);
|
|
42
94
|
console.log(` Output Tokens: ${stats.totalOutputTokens.toLocaleString()}`);
|
|
@@ -75,6 +127,18 @@ export function statsCommand(options) {
|
|
|
75
127
|
else {
|
|
76
128
|
console.log(chalk.gray(' Not enough data to calculate savings'));
|
|
77
129
|
}
|
|
130
|
+
// SDK ledger breakdown — surfaces non-chat endpoints (Modal, PM, x.com,
|
|
131
|
+
// exa, etc.) that flow through tools and may not show up in byModel.
|
|
132
|
+
// Only print when the ledger has real data.
|
|
133
|
+
if (sdkLedger.count > 0 && sdkLedger.byEndpoint.length > 0) {
|
|
134
|
+
console.log(chalk.bold('\n SDK Ledger (top endpoints)\n'));
|
|
135
|
+
for (const e of sdkLedger.byEndpoint.slice(0, 6)) {
|
|
136
|
+
const pct = sdkTotal > 0 ? ((e.costUsd / sdkTotal) * 100).toFixed(1) : '0';
|
|
137
|
+
const display = e.endpoint.length > 40 ? e.endpoint.slice(0, 37) + '...' : e.endpoint;
|
|
138
|
+
console.log(` ${chalk.cyan(display)}`);
|
|
139
|
+
console.log(chalk.gray(` ${e.count} call${e.count === 1 ? '' : 's'} · $${e.costUsd.toFixed(4)} (${pct}%)`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
78
142
|
// Recent activity (last 5 requests)
|
|
79
143
|
if (stats.history.length > 0) {
|
|
80
144
|
console.log(chalk.bold('\n Recent Activity\n'));
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for `~/.blockrun/cost_log.jsonl` — the SDK-owned ledger of every
|
|
3
|
+
* settled x402 payment.
|
|
4
|
+
*
|
|
5
|
+
* Franklin's own `franklin-stats.json` and `franklin-audit.jsonl` only
|
|
6
|
+
* capture calls that pass through specific code paths (the main agent
|
|
7
|
+
* loop and the proxy). Helper LLM calls (analyzeTurn, prefetchForIntent,
|
|
8
|
+
* compaction, evaluator, verification, MoA, subagent, learning extraction,
|
|
9
|
+
* etc.) all settle x402 payments through the SDK — those payments DO get
|
|
10
|
+
* recorded in cost_log.jsonl by `@blockrun/llm` itself, but Franklin's
|
|
11
|
+
* stats infra had been ignoring this file entirely.
|
|
12
|
+
*
|
|
13
|
+
* Verified 2026-05-06 against a real machine: cost_log.jsonl is written
|
|
14
|
+
* by the SDK with snake_case keys (`cost_usd`, `ts` in unix seconds with
|
|
15
|
+
* subsecond precision — Python convention) and Franklin's reads/writes
|
|
16
|
+
* use camelCase + ms. This module bridges the format gap so stats /
|
|
17
|
+
* insights / `franklin balance` can surface the wallet-truth total
|
|
18
|
+
* alongside the recorded total.
|
|
19
|
+
*
|
|
20
|
+
* Responsibility: read-only. We never write or trim cost_log.jsonl —
|
|
21
|
+
* the SDK owns it.
|
|
22
|
+
*/
|
|
23
|
+
export interface SettlementRow {
|
|
24
|
+
/** Endpoint path that was paid for, e.g. `/v1/chat/completions`. */
|
|
25
|
+
endpoint: string;
|
|
26
|
+
/** USD settled on-chain via x402. */
|
|
27
|
+
costUsd: number;
|
|
28
|
+
/** Unix milliseconds (normalized — SDK writes seconds). */
|
|
29
|
+
ts: number;
|
|
30
|
+
}
|
|
31
|
+
export interface SettlementSummary {
|
|
32
|
+
/** Path to cost_log.jsonl (or the fallback location). */
|
|
33
|
+
path: string;
|
|
34
|
+
/** Total entries read. */
|
|
35
|
+
count: number;
|
|
36
|
+
/** Sum of `costUsd` across all rows in window. */
|
|
37
|
+
totalUsd: number;
|
|
38
|
+
/** Per-endpoint breakdown sorted by cost descending. */
|
|
39
|
+
byEndpoint: Array<{
|
|
40
|
+
endpoint: string;
|
|
41
|
+
count: number;
|
|
42
|
+
costUsd: number;
|
|
43
|
+
}>;
|
|
44
|
+
/** First and last timestamps observed in the window (unix ms), or null. */
|
|
45
|
+
firstTs: number | null;
|
|
46
|
+
lastTs: number | null;
|
|
47
|
+
}
|
|
48
|
+
interface ReadOptions {
|
|
49
|
+
/** Override the cost_log path (for tests). Defaults to ~/.blockrun/cost_log.jsonl. */
|
|
50
|
+
path?: string;
|
|
51
|
+
sinceMs?: number;
|
|
52
|
+
untilMs?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Load + parse cost_log.jsonl. Optional time window in unix milliseconds.
|
|
56
|
+
* Skips malformed lines silently (the SDK's JSONL writer is well-behaved
|
|
57
|
+
* but we don't want a single corrupted line to nuke the whole readout).
|
|
58
|
+
*
|
|
59
|
+
* Returns an empty list if the file doesn't exist — callers should treat
|
|
60
|
+
* that as "no SDK ledger available" rather than an error, since the file
|
|
61
|
+
* is only created on the first paid call.
|
|
62
|
+
*/
|
|
63
|
+
export declare function loadSdkSettlements(opts?: ReadOptions): SettlementRow[];
|
|
64
|
+
/** Aggregate the SDK ledger into a single summary object. */
|
|
65
|
+
export declare function summarizeSdkSettlements(opts?: ReadOptions): SettlementSummary;
|
|
66
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reader for `~/.blockrun/cost_log.jsonl` — the SDK-owned ledger of every
|
|
3
|
+
* settled x402 payment.
|
|
4
|
+
*
|
|
5
|
+
* Franklin's own `franklin-stats.json` and `franklin-audit.jsonl` only
|
|
6
|
+
* capture calls that pass through specific code paths (the main agent
|
|
7
|
+
* loop and the proxy). Helper LLM calls (analyzeTurn, prefetchForIntent,
|
|
8
|
+
* compaction, evaluator, verification, MoA, subagent, learning extraction,
|
|
9
|
+
* etc.) all settle x402 payments through the SDK — those payments DO get
|
|
10
|
+
* recorded in cost_log.jsonl by `@blockrun/llm` itself, but Franklin's
|
|
11
|
+
* stats infra had been ignoring this file entirely.
|
|
12
|
+
*
|
|
13
|
+
* Verified 2026-05-06 against a real machine: cost_log.jsonl is written
|
|
14
|
+
* by the SDK with snake_case keys (`cost_usd`, `ts` in unix seconds with
|
|
15
|
+
* subsecond precision — Python convention) and Franklin's reads/writes
|
|
16
|
+
* use camelCase + ms. This module bridges the format gap so stats /
|
|
17
|
+
* insights / `franklin balance` can surface the wallet-truth total
|
|
18
|
+
* alongside the recorded total.
|
|
19
|
+
*
|
|
20
|
+
* Responsibility: read-only. We never write or trim cost_log.jsonl —
|
|
21
|
+
* the SDK owns it.
|
|
22
|
+
*/
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { BLOCKRUN_DIR } from '../config.js';
|
|
26
|
+
function getCostLogPath() {
|
|
27
|
+
return path.join(BLOCKRUN_DIR, 'cost_log.jsonl');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load + parse cost_log.jsonl. Optional time window in unix milliseconds.
|
|
31
|
+
* Skips malformed lines silently (the SDK's JSONL writer is well-behaved
|
|
32
|
+
* but we don't want a single corrupted line to nuke the whole readout).
|
|
33
|
+
*
|
|
34
|
+
* Returns an empty list if the file doesn't exist — callers should treat
|
|
35
|
+
* that as "no SDK ledger available" rather than an error, since the file
|
|
36
|
+
* is only created on the first paid call.
|
|
37
|
+
*/
|
|
38
|
+
export function loadSdkSettlements(opts) {
|
|
39
|
+
const file = opts?.path ?? getCostLogPath();
|
|
40
|
+
if (!fs.existsSync(file))
|
|
41
|
+
return [];
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = fs.readFileSync(file, 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const rows = [];
|
|
50
|
+
const sinceMs = opts?.sinceMs ?? 0;
|
|
51
|
+
const untilMs = opts?.untilMs ?? Number.POSITIVE_INFINITY;
|
|
52
|
+
for (const line of raw.split('\n')) {
|
|
53
|
+
const trimmed = line.trim();
|
|
54
|
+
if (!trimmed)
|
|
55
|
+
continue;
|
|
56
|
+
let obj;
|
|
57
|
+
try {
|
|
58
|
+
obj = JSON.parse(trimmed);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const endpoint = typeof obj.endpoint === 'string' ? obj.endpoint : '';
|
|
64
|
+
if (!endpoint)
|
|
65
|
+
continue;
|
|
66
|
+
// SDK writes `cost_usd`. Defensively also accept `costUsd` in case a
|
|
67
|
+
// future SDK release switches conventions.
|
|
68
|
+
const costRaw = obj.cost_usd ?? obj.costUsd;
|
|
69
|
+
const costUsd = typeof costRaw === 'number' && Number.isFinite(costRaw) ? costRaw : 0;
|
|
70
|
+
// SDK writes `ts` as unix SECONDS with subsecond precision (1773424791.43...).
|
|
71
|
+
// Normalize to ms so callers can compare against `Date.now()` directly.
|
|
72
|
+
const tsRaw = obj.ts;
|
|
73
|
+
if (typeof tsRaw !== 'number' || !Number.isFinite(tsRaw))
|
|
74
|
+
continue;
|
|
75
|
+
const ts = tsRaw < 1e12 ? Math.round(tsRaw * 1000) : Math.round(tsRaw);
|
|
76
|
+
if (ts < sinceMs || ts > untilMs)
|
|
77
|
+
continue;
|
|
78
|
+
rows.push({ endpoint, costUsd, ts });
|
|
79
|
+
}
|
|
80
|
+
return rows;
|
|
81
|
+
}
|
|
82
|
+
/** Aggregate the SDK ledger into a single summary object. */
|
|
83
|
+
export function summarizeSdkSettlements(opts) {
|
|
84
|
+
const rows = loadSdkSettlements(opts);
|
|
85
|
+
let totalUsd = 0;
|
|
86
|
+
let firstTs = null;
|
|
87
|
+
let lastTs = null;
|
|
88
|
+
const byEndpointMap = new Map();
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
totalUsd += r.costUsd;
|
|
91
|
+
if (firstTs === null || r.ts < firstTs)
|
|
92
|
+
firstTs = r.ts;
|
|
93
|
+
if (lastTs === null || r.ts > lastTs)
|
|
94
|
+
lastTs = r.ts;
|
|
95
|
+
const acc = byEndpointMap.get(r.endpoint) ?? { count: 0, costUsd: 0 };
|
|
96
|
+
acc.count += 1;
|
|
97
|
+
acc.costUsd += r.costUsd;
|
|
98
|
+
byEndpointMap.set(r.endpoint, acc);
|
|
99
|
+
}
|
|
100
|
+
const byEndpoint = Array.from(byEndpointMap.entries())
|
|
101
|
+
.map(([endpoint, v]) => ({ endpoint, count: v.count, costUsd: v.costUsd }))
|
|
102
|
+
.sort((a, b) => b.costUsd - a.costUsd);
|
|
103
|
+
return {
|
|
104
|
+
path: opts?.path ?? getCostLogPath(),
|
|
105
|
+
count: rows.length,
|
|
106
|
+
totalUsd,
|
|
107
|
+
byEndpoint,
|
|
108
|
+
firstTs,
|
|
109
|
+
lastTs,
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/tools/trading.js
CHANGED
|
@@ -16,6 +16,38 @@ function formatUsd(n) {
|
|
|
16
16
|
return `$${(n / 1e3).toFixed(1)}K`;
|
|
17
17
|
return `$${n.toFixed(2)}`;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* US-listed equity tickers that ALSO have meaningful tokenized listings on-chain.
|
|
21
|
+
* When TradingSignal is called with one of these, the crypto-leg data we return
|
|
22
|
+
* is the tokenized variant — not the spot equity. We surface a notice in the
|
|
23
|
+
* output so the agent knows to also pull TradingMarket stockPrice market='us'
|
|
24
|
+
* for the equity side, and can compute the basis spread (premium/discount of
|
|
25
|
+
* tokenized vs spot — that spread is real alpha for some flows).
|
|
26
|
+
*
|
|
27
|
+
* Conservative list: high-liquidity US equities that have shown up as actively
|
|
28
|
+
* traded tokenized variants. Add more as they materialize. Verified 2026-05-06
|
|
29
|
+
* via a real session where the agent asked TradingSignal for CRCL, got the
|
|
30
|
+
* tokenized $0-cap leg back, and correctly recovered to "ignore this, pull
|
|
31
|
+
* Pyth" — but the user lost an extra $0.005 + a confused turn before recovery.
|
|
32
|
+
*/
|
|
33
|
+
const KNOWN_DUAL_LISTED_EQUITIES = new Set([
|
|
34
|
+
'CRCL', // Circle Internet Group
|
|
35
|
+
'COIN', // Coinbase
|
|
36
|
+
'MSTR', // Strategy (formerly MicroStrategy)
|
|
37
|
+
'PLTR', // Palantir
|
|
38
|
+
'TSLA', // Tesla
|
|
39
|
+
'AAPL', // Apple
|
|
40
|
+
'NVDA', // NVIDIA
|
|
41
|
+
'MSFT', // Microsoft
|
|
42
|
+
'AMZN', // Amazon
|
|
43
|
+
'GOOGL', // Alphabet
|
|
44
|
+
'META', // Meta
|
|
45
|
+
'JPM', // JPMorgan Chase
|
|
46
|
+
'BRK', // Berkshire Hathaway (BRK.A / BRK.B)
|
|
47
|
+
'HOOD', // Robinhood
|
|
48
|
+
'SQ', // Block
|
|
49
|
+
'PYPL', // PayPal
|
|
50
|
+
]);
|
|
19
51
|
// MACD needs slow EMA (26) + signal EMA (9) = 35 closes minimum for the
|
|
20
52
|
// signal/histogram to be defined. Default was 30, which left signal=NaN
|
|
21
53
|
// and trend stuck at 'neutral' on every call — see the 2026-05-03 BTC
|
|
@@ -126,9 +158,17 @@ async function executeSignal(input, _ctx) {
|
|
|
126
158
|
bullSignals.push('price below lower Bollinger');
|
|
127
159
|
if (Number.isFinite(bbResult.middle) && bbResult.position === 'above')
|
|
128
160
|
bearSignals.push('price above upper Bollinger');
|
|
161
|
+
// Dual-listing notice: prepend before the body when the ticker is also a
|
|
162
|
+
// known US equity. Doesn't suppress the crypto/tokenized data — that data
|
|
163
|
+
// is its own legitimate signal — just labels it correctly so the agent
|
|
164
|
+
// knows to also fetch the spot equity for the basis spread.
|
|
165
|
+
const dualListingNote = KNOWN_DUAL_LISTED_EQUITIES.has(upper)
|
|
166
|
+
? `> ⚠ \`${upper}\` is also a US-listed equity. The data below is the **crypto / tokenized leg** (CoinGecko). For the spot equity (NYSE / NASDAQ) call \`TradingMarket\` with \`action: stockPrice, market: "us"\`. Run both in parallel to compute the basis spread (premium/discount of tokenized vs spot — that spread is the signal).\n`
|
|
167
|
+
: '';
|
|
129
168
|
const output = [
|
|
130
169
|
`## ${upper} Signal Report`,
|
|
131
170
|
'',
|
|
171
|
+
...(dualListingNote ? [dualListingNote] : []),
|
|
132
172
|
`**Price:** $${price.toLocaleString()} USD (${change24h > 0 ? '+' : ''}${change24h.toFixed(2)}% 24h)`,
|
|
133
173
|
`**Market Cap:** ${formatUsd(marketCap)}`,
|
|
134
174
|
`**24h Volume:** ${formatUsd(volume24h)}`,
|
|
@@ -153,7 +193,7 @@ async function executeSignal(input, _ctx) {
|
|
|
153
193
|
export const tradingSignalCapability = {
|
|
154
194
|
spec: {
|
|
155
195
|
name: 'TradingSignal',
|
|
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".',
|
|
196
|
+
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". For tickers that ALSO trade as US equities (CRCL, COIN, MSTR, TSLA, AAPL, NVDA, etc.) the response includes a dual-listing note: TradingSignal returns the tokenized/crypto leg, and you should fire TradingMarket stockPrice market="us" in parallel to also get the spot equity. The basis spread between the two is itself the signal.',
|
|
157
197
|
input_schema: {
|
|
158
198
|
type: 'object',
|
|
159
199
|
properties: {
|
package/package.json
CHANGED