@finchagentic/mcp 4.6.1 → 4.6.2
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/package.json +5 -6
- package/dist/_http-cache.js +0 -96
- package/dist/_text-search.js +0 -39
- package/dist/agent-loop.js +0 -301
- package/dist/annotations.js +0 -122
- package/dist/cli.js +0 -1391
- package/dist/clink-input.js +0 -15
- package/dist/config.js +0 -132
- package/dist/convex.js +0 -175
- package/dist/dex-pair.js +0 -54
- package/dist/enrichment-router.js +0 -315
- package/dist/index.js +0 -258
- package/dist/llm.js +0 -298
- package/dist/local-memory-file.js +0 -150
- package/dist/local-memory.js +0 -135
- package/dist/local-vault.js +0 -456
- package/dist/output-schemas.js +0 -605
- package/dist/project.js +0 -36
- package/dist/prompts.js +0 -111
- package/dist/public-url.js +0 -107
- package/dist/resources.js +0 -111
- package/dist/server.js +0 -322
- package/dist/signal-gate.js +0 -57
- package/dist/token-decimals.js +0 -26
- package/dist/token-gate.js +0 -88
- package/dist/tool-filter.js +0 -53
- package/dist/tools/_solidity-scan.js +0 -313
- package/dist/tools/agents.js +0 -441
- package/dist/tools/automation.js +0 -354
- package/dist/tools/base-mcp.js +0 -466
- package/dist/tools/base.js +0 -283
- package/dist/tools/chronicle.js +0 -268
- package/dist/tools/coder.js +0 -94
- package/dist/tools/deep-research.js +0 -1421
- package/dist/tools/defi.js +0 -292
- package/dist/tools/equity.js +0 -372
- package/dist/tools/events.js +0 -182
- package/dist/tools/github.js +0 -564
- package/dist/tools/insider.js +0 -264
- package/dist/tools/insight.js +0 -630
- package/dist/tools/market.js +0 -555
- package/dist/tools/memory.js +0 -1059
- package/dist/tools/miroshark.js +0 -350
- package/dist/tools/monitor.js +0 -319
- package/dist/tools/os.js +0 -236
- package/dist/tools/packets.js +0 -296
- package/dist/tools/research-chain.js +0 -226
- package/dist/tools/research-compare.js +0 -280
- package/dist/tools/research.js +0 -188
- package/dist/tools/rh-bridge.js +0 -148
- package/dist/tools/rh-mcp.js +0 -1448
- package/dist/tools/rh-orders.js +0 -556
- package/dist/tools/scanner.js +0 -564
- package/dist/tools/stake.js +0 -369
- package/dist/tools/vault.js +0 -1020
- package/dist/tools/wallet.js +0 -200
- package/dist/types.js +0 -2
- package/dist/wallet.js +0 -372
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@finchagentic/mcp",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.2",
|
|
4
4
|
"description": "The runtime layer for Agentic AI. Persistent memory, autonomous agents, and workflows that survive every session.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,9 +14,7 @@
|
|
|
14
14
|
"start": "node dist/index.js",
|
|
15
15
|
"test": "vitest run",
|
|
16
16
|
"test:watch": "vitest",
|
|
17
|
-
"test:mutation": "node scripts/mutation-check.js"
|
|
18
|
-
"prepare": "husky",
|
|
19
|
-
"prepublishOnly": "npm run build && node scripts/version-readme.js"
|
|
17
|
+
"test:mutation": "node scripts/mutation-check.js"
|
|
20
18
|
},
|
|
21
19
|
"lint-staged": {
|
|
22
20
|
"*.{ts,tsx}": "eslint --fix"
|
|
@@ -70,5 +68,6 @@
|
|
|
70
68
|
},
|
|
71
69
|
"publishConfig": {
|
|
72
70
|
"access": "public"
|
|
73
|
-
}
|
|
74
|
-
|
|
71
|
+
},
|
|
72
|
+
"mcpName": "io.github.finchagentic/mcp"
|
|
73
|
+
}
|
package/dist/_http-cache.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Shared cache + 429-backoff wrapper for external HTTP calls.
|
|
3
|
-
// Designed for read-heavy public APIs like CoinGecko (free tier: 30 req/min)
|
|
4
|
-
// and DexScreener - agent loops + parallel tool calls were tripping rate
|
|
5
|
-
// limits in production. Cache hit returns the prior body without a network
|
|
6
|
-
// round-trip; cache miss does a fetch with bounded retries on 429/503.
|
|
7
|
-
//
|
|
8
|
-
// The cache is in-process only - every MCP server process keeps its own
|
|
9
|
-
// LRU. That's intentional: tokens get fresh data on cold start, no shared
|
|
10
|
-
// state to invalidate across users.
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.cachedFetch = cachedFetch;
|
|
13
|
-
exports.clearHttpCache = clearHttpCache;
|
|
14
|
-
exports.httpCacheStats = httpCacheStats;
|
|
15
|
-
const DEFAULT_TTL_MS = 45000; // 45s - fresh enough for prices, generous enough to absorb a flurry
|
|
16
|
-
const DEFAULT_MAX_ENTRIES = 200;
|
|
17
|
-
const DEFAULT_RETRY_DELAYS_MS = [500, 1500, 4000];
|
|
18
|
-
const cache = new Map();
|
|
19
|
-
function cacheKey(url, init) {
|
|
20
|
-
// POST bodies are part of the key so two POSTs with different params don't collide.
|
|
21
|
-
if (!init || !init.body || init.method === "GET")
|
|
22
|
-
return `GET ${url}`;
|
|
23
|
-
return `${init.method ?? "POST"} ${url} ${typeof init.body === "string" ? init.body : ""}`;
|
|
24
|
-
}
|
|
25
|
-
function evictExpired() {
|
|
26
|
-
const now = Date.now();
|
|
27
|
-
for (const [k, v] of cache) {
|
|
28
|
-
if (v.expiresAt < now)
|
|
29
|
-
cache.delete(k);
|
|
30
|
-
}
|
|
31
|
-
// LRU-ish eviction - Map preserves insertion order, so the first keys are oldest.
|
|
32
|
-
while (cache.size > DEFAULT_MAX_ENTRIES) {
|
|
33
|
-
const firstKey = cache.keys().next().value;
|
|
34
|
-
if (firstKey === undefined)
|
|
35
|
-
break;
|
|
36
|
-
cache.delete(firstKey);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
async function cachedFetch(url, init = {}, opts = {}) {
|
|
40
|
-
const ttl = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
41
|
-
const retryDelays = opts.retryDelaysMs ?? DEFAULT_RETRY_DELAYS_MS;
|
|
42
|
-
const timeout = opts.timeoutMs ?? 15000;
|
|
43
|
-
evictExpired();
|
|
44
|
-
const key = cacheKey(url, init);
|
|
45
|
-
if (!opts.bypassCache) {
|
|
46
|
-
const hit = cache.get(key);
|
|
47
|
-
if (hit && hit.expiresAt > Date.now()) {
|
|
48
|
-
// Refresh recency - re-insert moves it to the end of the Map.
|
|
49
|
-
cache.delete(key);
|
|
50
|
-
cache.set(key, hit);
|
|
51
|
-
return { ok: hit.status >= 200 && hit.status < 300, status: hit.status, text: hit.body, fromCache: true };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
let lastStatus = 0;
|
|
55
|
-
let lastText = "";
|
|
56
|
-
for (let attempt = 0; attempt <= retryDelays.length; attempt++) {
|
|
57
|
-
try {
|
|
58
|
-
const res = await fetch(url, { ...init, signal: AbortSignal.timeout(timeout) });
|
|
59
|
-
const text = await res.text();
|
|
60
|
-
lastStatus = res.status;
|
|
61
|
-
lastText = text;
|
|
62
|
-
// Cache successful responses + 404s (404 is "definitively not found" - no point retrying).
|
|
63
|
-
if (res.ok || res.status === 404) {
|
|
64
|
-
cache.set(key, { body: text, status: res.status, expiresAt: Date.now() + ttl });
|
|
65
|
-
return { ok: res.ok, status: res.status, text, fromCache: false };
|
|
66
|
-
}
|
|
67
|
-
// Retry on 429 (rate limit) and 5xx (transient server errors).
|
|
68
|
-
if (res.status === 429 || res.status >= 500) {
|
|
69
|
-
const delay = retryDelays[attempt];
|
|
70
|
-
if (delay === undefined)
|
|
71
|
-
break;
|
|
72
|
-
// Honor server's Retry-After if provided (CoinGecko sends this).
|
|
73
|
-
const retryAfter = res.headers.get("retry-after");
|
|
74
|
-
const waitMs = retryAfter ? Math.min(parseInt(retryAfter) * 1000, 30000) : delay;
|
|
75
|
-
await new Promise((r) => setTimeout(r, waitMs));
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
// Non-retryable client error (400/401/403) - surface to caller without retry.
|
|
79
|
-
return { ok: false, status: res.status, text, fromCache: false };
|
|
80
|
-
}
|
|
81
|
-
catch (err) {
|
|
82
|
-
lastText = err instanceof Error ? err.message : String(err);
|
|
83
|
-
const delay = retryDelays[attempt];
|
|
84
|
-
if (delay === undefined)
|
|
85
|
-
break;
|
|
86
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return { ok: false, status: lastStatus, text: lastText, fromCache: false };
|
|
90
|
-
}
|
|
91
|
-
function clearHttpCache() {
|
|
92
|
-
cache.clear();
|
|
93
|
-
}
|
|
94
|
-
function httpCacheStats() {
|
|
95
|
-
return { size: cache.size, maxEntries: DEFAULT_MAX_ENTRIES };
|
|
96
|
-
}
|
package/dist/_text-search.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Shared substring/keyword scoring for the local-file backends
|
|
3
|
-
// (local-memory-file.ts, local-vault.ts) - no embeddings, no server, zero
|
|
4
|
-
// dependencies. Word-boundary aware, not raw substring counting - a short
|
|
5
|
-
// common term like "is"/"a" used to score a "match" purely by being a
|
|
6
|
-
// substring of an unrelated word ("is" inside "distances", "a" inside
|
|
7
|
-
// "banana"). Found via real testing of memory_add's new conflict-hint
|
|
8
|
-
// feature: a query for "the user's favorite pizza topping is pepperoni"
|
|
9
|
-
// registered as "related" to a completely unrelated stored memory about
|
|
10
|
-
// metric vs imperial units, purely because both contained "the" and "is".
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.meaningfulTerms = meaningfulTerms;
|
|
13
|
-
exports.wordOccurrences = wordOccurrences;
|
|
14
|
-
const STOP_WORDS = new Set([
|
|
15
|
-
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
16
|
-
"to", "of", "in", "on", "at", "for", "with", "by", "from", "as",
|
|
17
|
-
"and", "or", "but", "if", "so", "this", "that", "it", "its", "i",
|
|
18
|
-
"you", "your", "they", "them", "their", "he", "she", "his", "her",
|
|
19
|
-
"not", "no", "do", "does", "did", "has", "have", "had", "will", "would",
|
|
20
|
-
]);
|
|
21
|
-
/** Query terms worth scoring against - lowercased, stop-words and
|
|
22
|
-
* single-character noise dropped. An empty result means the query was
|
|
23
|
-
* entirely stop words/punctuation - callers should treat that as "no
|
|
24
|
-
* meaningful query" (return no matches) rather than matching everything. */
|
|
25
|
-
function meaningfulTerms(query) {
|
|
26
|
-
return query
|
|
27
|
-
.toLowerCase()
|
|
28
|
-
.split(/[^a-z0-9]+/)
|
|
29
|
-
.filter((t) => t.length > 1 && !STOP_WORDS.has(t));
|
|
30
|
-
}
|
|
31
|
-
function escapeRegex(s) {
|
|
32
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
33
|
-
}
|
|
34
|
-
/** Word-boundary occurrence count of `term` inside `haystack` - not a raw
|
|
35
|
-
* substring count, so "is" doesn't match inside "distances" or "this". */
|
|
36
|
-
function wordOccurrences(haystack, term) {
|
|
37
|
-
const re = new RegExp(`\\b${escapeRegex(term)}\\b`, "g");
|
|
38
|
-
return (haystack.match(re) || []).length;
|
|
39
|
-
}
|
package/dist/agent-loop.js
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.runAgent = runAgent;
|
|
4
|
-
const server_js_1 = require("./server.js");
|
|
5
|
-
const llm_js_1 = require("./llm.js");
|
|
6
|
-
const convex_js_1 = require("./convex.js");
|
|
7
|
-
const SYSTEM_PROMPT = [
|
|
8
|
-
`You are Finch, the runtime layer for Agentic AI - with ${server_js_1.ALL_TOOLS.length} tools spanning memory, vault, deep research, persistent agents, code, automations, DeFi, and GitHub.`,
|
|
9
|
-
"Be direct and concise. Pick the right tool - don't narrate the choice. Summarize tool results in plain English.",
|
|
10
|
-
"",
|
|
11
|
-
"CRITICAL - on-chain / financial actions (swap, send, transfer, buy, sell, bridge, lend, deposit, withdraw, balance):",
|
|
12
|
-
"- You MUST call the corresponding tool. NEVER write 'Sent', 'Swapped', 'Tx confirmed', or any execution claim without an actual tool call returning a result first.",
|
|
13
|
-
"- Tx hash and basescan/explorer URL from the tool output are MANDATORY in your reply - show them verbatim, do not omit or paraphrase.",
|
|
14
|
-
"- NEVER fabricate a post-transaction balance with arithmetic. If user wants the new balance, call the balance tool again - do not compute it from a prior balance + amount.",
|
|
15
|
-
"- For ALL Base chain operations, you MUST use the base_mcp_* family: base_mcp_swap (NOT swap_tokens), base_mcp_send (NOT send_token), base_mcp_balance (NOT get_portfolio), base_mcp_estimate, base_mcp_resolve, base_mcp_lend, base_mcp_status. This is non-negotiable - Base operations go through the Base MCP skill, period.",
|
|
16
|
-
"- For Robinhood Chain tokenized stocks (chainId 4663, NVDA/AAPL/etc on Uniswap V4), you MUST use rh_mcp_*: rh_mcp_status, rh_mcp_list_stocks, rh_mcp_balance, rh_mcp_estimate, rh_mcp_swap. Never use base_mcp_* or 0x for RH stocks. rh_mcp_swap requires confirm:true. Explorer = robinhoodchain.blockscout.com. This is NOT Robinhood Agentic brokerage (agent.robinhood.com).",
|
|
17
|
-
"- Do NOT claim an address belongs to the user (e.g. 'your own address') unless you have verified ownership. Resolving a basename returns whoever owns that name - usually NOT the caller.",
|
|
18
|
-
"- The Finch wallet is the SAME address on both Base and Robinhood Chain, but they are separate ledgers - a balance on one tells you nothing about the other. If the user asks a chain-unspecified question ('what's my balance', 'do I have any funds') call BOTH base_mcp_balance AND rh_mcp_balance before answering. Never report only one chain's result as 'your balance' or 'your wallet is empty' - say which chain(s) you checked, and give both figures.",
|
|
19
|
-
"- Swap/send requests are only chain-unambiguous when the token itself pins the chain (a RH catalog stock like NVDA/AAPL, or a token you already know only exists on one side). A bare request like 'swap 0.001 ETH to a stablecoin' with no chain named is NOT unambiguous just because you picked one - ETH and 'stablecoin' both exist on Base (USDC/USDT/DAI) and Robinhood Chain (USDG). Before estimating or executing an ambiguous swap, call both base_mcp_balance and rh_mcp_balance first: if only one chain actually holds enough of the source token, use that chain and say which one and why; if neither holds enough, say so plainly instead of quoting a swap the wallet can't cover; if both hold enough, ask the user which chain before proceeding.",
|
|
20
|
-
"- There is NO send/transfer tool for Robinhood Chain - base_mcp_send only moves assets on Base mainnet. If the user asks to send/transfer USDG or any RH catalog stock (NVDA, AAPL, etc.), do NOT call base_mcp_send with that token name hoping it resolves - it will either error or, worse, silently match an unrelated Base token with the same symbol. Tell the user directly that on-chain sends are not yet supported on Robinhood Chain.",
|
|
21
|
-
"- Never state a balance, price, quote, token address, or transaction result from memory or inference - every number in your answer must trace to a tool call you made THIS turn. If you are not sure which chain, token, or address a term refers to, ask or resolve it (rh_token_resolve / base_mcp_resolve) before answering - do not guess and present the guess as fact.",
|
|
22
|
-
"",
|
|
23
|
-
"For deep research: prefer deep_research (multi-stage, saves to vault). Use continueFrom when extending prior reports.",
|
|
24
|
-
"For live web info: use web_search. For market questions: use get_market_data or market_thesis.",
|
|
25
|
-
"Save substantive findings to vault; do not save thin or empty outputs.",
|
|
26
|
-
"",
|
|
27
|
-
"NEVER call ask_finch from this shell. It exists for MCP clients that have no reasoning model of their own - you already are one. Calling it mid-task means asking a second model to think for you, which produces nothing you couldn't write yourself from the data you already have, and repeating it when unsatisfied just burns turns. If a tool's result already answers the question, write the answer yourself.",
|
|
28
|
-
"Do not call the same tool with the same or near-identical arguments more than once in a single answer. If deep_research, a search, or an analysis tool already returned data, synthesize from that - do not re-run it hoping for a different result, and do not chain more tool calls than the question actually needs. Stop calling tools and answer as soon as you have enough to answer well.",
|
|
29
|
-
].join("\n");
|
|
30
|
-
async function runAgent(userMessage, history, onToolCall) {
|
|
31
|
-
const provider = process.env.FINCH_PROVIDER?.toLowerCase().trim();
|
|
32
|
-
const bankrKey = process.env.BANKR_API_KEY;
|
|
33
|
-
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
34
|
-
const openaiKey = process.env.OPENAI_API_KEY;
|
|
35
|
-
const grokKey = process.env.GROK_API_KEY;
|
|
36
|
-
// Explicit override - lets FINCH_PROVIDER=openai win even when BANKR_API_KEY
|
|
37
|
-
// is also set (e.g. as a persistent shell env var), same as llm.ts's callLLM.
|
|
38
|
-
if (provider === "bankr" && bankrKey)
|
|
39
|
-
return runBankrLoop(bankrKey, userMessage, history, onToolCall);
|
|
40
|
-
if (provider === "anthropic" && anthropicKey)
|
|
41
|
-
return runAnthropicLoop(anthropicKey, userMessage, history, onToolCall);
|
|
42
|
-
if (provider === "openai" && openaiKey)
|
|
43
|
-
return runOpenAILoop(openaiKey, userMessage, history, onToolCall);
|
|
44
|
-
if (provider === "grok" && grokKey)
|
|
45
|
-
return runGrokLoop(grokKey, userMessage, history, onToolCall);
|
|
46
|
-
// Auto-priority - matches llm.ts's callLLM() order exactly (bankr →
|
|
47
|
-
// anthropic → openai → grok → Convex proxy) so a one-shot LLM tool call
|
|
48
|
-
// (ask_finch, memory extraction, etc.) and the interactive agent loop
|
|
49
|
-
// never silently pick different providers for the same configured keys.
|
|
50
|
-
// A GROK_API_KEY-only user used to fall all the way through to the
|
|
51
|
-
// Convex-proxied Anthropic loop here despite callLLM() using Grok.
|
|
52
|
-
if (bankrKey)
|
|
53
|
-
return runBankrLoop(bankrKey, userMessage, history, onToolCall);
|
|
54
|
-
if (anthropicKey)
|
|
55
|
-
return runAnthropicLoop(anthropicKey, userMessage, history, onToolCall);
|
|
56
|
-
if (openaiKey)
|
|
57
|
-
return runOpenAILoop(openaiKey, userMessage, history, onToolCall);
|
|
58
|
-
if (grokKey)
|
|
59
|
-
return runGrokLoop(grokKey, userMessage, history, onToolCall);
|
|
60
|
-
// No direct key - proxy through Finch backend. Wallet auto-creates at ~/.finch/wallet.json
|
|
61
|
-
// on first use and signs requests transparently. No account or config needed.
|
|
62
|
-
try {
|
|
63
|
-
return await runConvexProxiedLoop(userMessage, history, onToolCall);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// Network down or backend unavailable - plain chat fallback
|
|
67
|
-
const text = await (0, llm_js_1.callLLM)(SYSTEM_PROMPT, userMessage, 1024, history);
|
|
68
|
-
return { text, toolCalls: [] };
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// ── Anthropic agent loop ─────────────────────────────────────────────────────
|
|
72
|
-
function toAnthropicTool(tool) {
|
|
73
|
-
return {
|
|
74
|
-
name: tool.name,
|
|
75
|
-
description: tool.description ?? "",
|
|
76
|
-
input_schema: tool.inputSchema ?? { type: "object", properties: {} },
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
// Shared loop body for any provider that speaks Anthropic's native
|
|
80
|
-
// content-block format (tool_use/tool_result) - runAnthropicLoop (direct
|
|
81
|
-
// api.anthropic.com) and runConvexProxiedLoop (same wire format via Finch's
|
|
82
|
-
// /llm/complete proxy) previously duplicated this ~70-line body verbatim,
|
|
83
|
-
// with only the transport (sendTurn/sendFinal) differing. Keeping one copy
|
|
84
|
-
// means a future fix (retry-on-5xx, tool-result truncation, etc.) can't be
|
|
85
|
-
// applied to one provider and silently miss the other.
|
|
86
|
-
async function runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall) {
|
|
87
|
-
const tools = server_js_1.ALL_TOOLS.map(toAnthropicTool);
|
|
88
|
-
const toolCalls = [];
|
|
89
|
-
const messages = [
|
|
90
|
-
...history.map(h => ({ role: h.role, content: h.content })),
|
|
91
|
-
{ role: "user", content: userMessage },
|
|
92
|
-
];
|
|
93
|
-
for (let turn = 0; turn < 10; turn++) {
|
|
94
|
-
const data = await sendTurn(messages, tools);
|
|
95
|
-
messages.push({ role: "assistant", content: data.content });
|
|
96
|
-
if (data.stop_reason !== "tool_use") {
|
|
97
|
-
const text = data.content
|
|
98
|
-
.filter(b => b.type === "text")
|
|
99
|
-
.map(b => b.text)
|
|
100
|
-
.join("");
|
|
101
|
-
return { text, toolCalls };
|
|
102
|
-
}
|
|
103
|
-
// Execute all tool_use blocks
|
|
104
|
-
const toolResults = [];
|
|
105
|
-
for (const block of data.content) {
|
|
106
|
-
if (block.type !== "tool_use")
|
|
107
|
-
continue;
|
|
108
|
-
let resultText;
|
|
109
|
-
try {
|
|
110
|
-
const handler = server_js_1.HANDLER_MAP.get(block.name);
|
|
111
|
-
if (!handler)
|
|
112
|
-
throw new Error(`Unknown tool: ${block.name}`);
|
|
113
|
-
// Recorded only once the handler is confirmed to exist - an "Unknown
|
|
114
|
-
// tool" miss is a model error, not a real tool execution, and callers
|
|
115
|
-
// reading AgentResult.toolCalls should be able to trust every entry
|
|
116
|
-
// actually ran.
|
|
117
|
-
onToolCall(block.name);
|
|
118
|
-
toolCalls.push({ name: block.name });
|
|
119
|
-
const result = await handler(block.name, block.input ?? {});
|
|
120
|
-
// A handler returning null (a name declared in its *_TOOLS array with
|
|
121
|
-
// no matching branch - a latent bug ruled out today but not
|
|
122
|
-
// structurally prevented) must not fall through to "Done." - that
|
|
123
|
-
// would report success to the model for a call that never actually
|
|
124
|
-
// ran, directly contradicting this loop's own system-prompt rule
|
|
125
|
-
// against claiming an unverified result.
|
|
126
|
-
if (result === null)
|
|
127
|
-
throw new Error(`Tool ${block.name} returned no result (handler bug - not executed)`);
|
|
128
|
-
resultText = result.content?.[0]?.text ?? "Done.";
|
|
129
|
-
}
|
|
130
|
-
catch (err) {
|
|
131
|
-
resultText = `Error: ${err.message}`;
|
|
132
|
-
}
|
|
133
|
-
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: resultText });
|
|
134
|
-
}
|
|
135
|
-
messages.push({ role: "user", content: toolResults });
|
|
136
|
-
}
|
|
137
|
-
// Hit the turn cap without a final answer - rather than hand back nothing,
|
|
138
|
-
// force one more call with no tools available so the model has to
|
|
139
|
-
// synthesize a real answer from whatever it already gathered.
|
|
140
|
-
return finishWithoutTools(sendFinal, messages, toolCalls);
|
|
141
|
-
}
|
|
142
|
-
function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
|
|
143
|
-
const model = process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
|
|
144
|
-
const sendTurn = async (messages, tools) => {
|
|
145
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
146
|
-
method: "POST",
|
|
147
|
-
headers: {
|
|
148
|
-
"Content-Type": "application/json",
|
|
149
|
-
"x-api-key": apiKey,
|
|
150
|
-
"anthropic-version": "2023-06-01",
|
|
151
|
-
},
|
|
152
|
-
body: JSON.stringify({ model, max_tokens: 4096, system: SYSTEM_PROMPT, tools, messages }),
|
|
153
|
-
signal: AbortSignal.timeout(90000),
|
|
154
|
-
});
|
|
155
|
-
if (!res.ok) {
|
|
156
|
-
const body = await res.text().catch(() => "");
|
|
157
|
-
throw new Error(`Anthropic ${res.status}: ${body.slice(0, 300)}`);
|
|
158
|
-
}
|
|
159
|
-
return await res.json();
|
|
160
|
-
};
|
|
161
|
-
const sendFinal = async (msgs) => {
|
|
162
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
163
|
-
method: "POST",
|
|
164
|
-
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
165
|
-
body: JSON.stringify({ model, max_tokens: 4096, system: SYSTEM_PROMPT, messages: msgs }),
|
|
166
|
-
signal: AbortSignal.timeout(90000),
|
|
167
|
-
});
|
|
168
|
-
if (!res.ok)
|
|
169
|
-
return "";
|
|
170
|
-
const data = await res.json();
|
|
171
|
-
return (data.content ?? []).filter(b => b.type === "text").map(b => b.text).join("");
|
|
172
|
-
};
|
|
173
|
-
return runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall);
|
|
174
|
-
}
|
|
175
|
-
// Shared turn-cap fallback: rather than "Reached max tool iterations." with
|
|
176
|
-
// nothing useful, ask the model to synthesize a real answer from whatever
|
|
177
|
-
// conversation history (including every tool result so far) it already has,
|
|
178
|
-
// with no tools offered so it can't keep deferring.
|
|
179
|
-
async function finishWithoutTools(call, messages, toolCalls) {
|
|
180
|
-
try {
|
|
181
|
-
const closingMessages = [
|
|
182
|
-
...messages,
|
|
183
|
-
{ role: "user", content: "Stop calling tools. Summarize what you found above into a direct answer for the user right now." },
|
|
184
|
-
];
|
|
185
|
-
const text = await call(closingMessages);
|
|
186
|
-
if (text.trim())
|
|
187
|
-
return { text, toolCalls };
|
|
188
|
-
}
|
|
189
|
-
catch { /* fall through to the honest failure message below */ }
|
|
190
|
-
return {
|
|
191
|
-
text: "I gathered some information but ran out of turns before finishing the analysis. Try a narrower question, or ask me to continue from what I found.",
|
|
192
|
-
toolCalls,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
// ── Convex-proxied Anthropic loop (session token only - platform covers LLM) ──
|
|
196
|
-
function runConvexProxiedLoop(userMessage, history, onToolCall) {
|
|
197
|
-
const model = process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
|
|
198
|
-
// callConvex handles wallet/session auth automatically; 90s timeout matches the proxy endpoint
|
|
199
|
-
const sendTurn = (messages, tools) => (0, convex_js_1.callConvex)("/llm/complete", "POST", { model, max_tokens: 4096, system: SYSTEM_PROMPT, tools, messages }, "llm_complete", 90000);
|
|
200
|
-
const sendFinal = async (msgs) => {
|
|
201
|
-
const data = await (0, convex_js_1.callConvex)("/llm/complete", "POST", {
|
|
202
|
-
model, max_tokens: 4096, system: SYSTEM_PROMPT, messages: msgs,
|
|
203
|
-
}, "llm_complete", 90000);
|
|
204
|
-
return (data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
205
|
-
};
|
|
206
|
-
return runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall);
|
|
207
|
-
}
|
|
208
|
-
// ── Bankr (OpenAI-compatible) agent loop ─────────────────────────────────────
|
|
209
|
-
function toBankrTool(tool) {
|
|
210
|
-
return {
|
|
211
|
-
type: "function",
|
|
212
|
-
function: {
|
|
213
|
-
name: tool.name,
|
|
214
|
-
description: tool.description ?? "",
|
|
215
|
-
parameters: tool.inputSchema ?? { type: "object", properties: {} },
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
// Shared tool-calling loop for any OpenAI Chat Completions-compatible
|
|
220
|
-
// endpoint (Bankr's LLM gateway and OpenAI itself both speak this format).
|
|
221
|
-
// Only the URL, auth header, and model differ per provider.
|
|
222
|
-
async function runOpenAICompatibleLoop(url, authHeaders, model, providerLabel, userMessage, history, onToolCall) {
|
|
223
|
-
const tools = server_js_1.ALL_TOOLS.map(toBankrTool);
|
|
224
|
-
const toolCalls = [];
|
|
225
|
-
const messages = [
|
|
226
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
227
|
-
...history.map(h => ({ role: h.role, content: h.content })),
|
|
228
|
-
{ role: "user", content: userMessage },
|
|
229
|
-
];
|
|
230
|
-
for (let turn = 0; turn < 10; turn++) {
|
|
231
|
-
const res = await fetch(url, {
|
|
232
|
-
method: "POST",
|
|
233
|
-
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
234
|
-
body: JSON.stringify({ model, messages, tools, max_tokens: 4096 }),
|
|
235
|
-
signal: AbortSignal.timeout(90000),
|
|
236
|
-
});
|
|
237
|
-
if (!res.ok) {
|
|
238
|
-
const body = await res.text().catch(() => "");
|
|
239
|
-
throw new Error(`${providerLabel} ${res.status}: ${body.slice(0, 300)}`);
|
|
240
|
-
}
|
|
241
|
-
const data = await res.json();
|
|
242
|
-
const choice = data.choices?.[0]?.message;
|
|
243
|
-
if (!choice)
|
|
244
|
-
throw new Error(`Empty response from ${providerLabel}`);
|
|
245
|
-
messages.push(choice);
|
|
246
|
-
if (!choice.tool_calls?.length) {
|
|
247
|
-
return { text: choice.content ?? "", toolCalls };
|
|
248
|
-
}
|
|
249
|
-
for (const call of choice.tool_calls) {
|
|
250
|
-
let resultText;
|
|
251
|
-
try {
|
|
252
|
-
const args = JSON.parse(call.function.arguments ?? "{}");
|
|
253
|
-
const handler = server_js_1.HANDLER_MAP.get(call.function.name);
|
|
254
|
-
if (!handler)
|
|
255
|
-
throw new Error(`Unknown tool: ${call.function.name}`);
|
|
256
|
-
// Recorded only once the handler is confirmed to exist - see the
|
|
257
|
-
// matching comment in runAnthropicLoop/runConvexProxiedLoop.
|
|
258
|
-
onToolCall(call.function.name);
|
|
259
|
-
toolCalls.push({ name: call.function.name });
|
|
260
|
-
const result = await handler(call.function.name, args);
|
|
261
|
-
resultText = result?.content?.[0]?.text ?? "Done.";
|
|
262
|
-
}
|
|
263
|
-
catch (err) {
|
|
264
|
-
resultText = `Error: ${err.message}`;
|
|
265
|
-
}
|
|
266
|
-
messages.push({ role: "tool", tool_call_id: call.id, content: resultText });
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return finishWithoutTools(async (msgs) => {
|
|
270
|
-
const res = await fetch(url, {
|
|
271
|
-
method: "POST",
|
|
272
|
-
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
273
|
-
body: JSON.stringify({ model, messages: msgs, max_tokens: 4096 }),
|
|
274
|
-
signal: AbortSignal.timeout(90000),
|
|
275
|
-
});
|
|
276
|
-
if (!res.ok)
|
|
277
|
-
return "";
|
|
278
|
-
const data = await res.json();
|
|
279
|
-
return data.choices?.[0]?.message?.content ?? "";
|
|
280
|
-
}, messages, toolCalls);
|
|
281
|
-
}
|
|
282
|
-
async function runBankrLoop(apiKey, userMessage, history, onToolCall) {
|
|
283
|
-
const model = process.env.FINCH_MODEL ?? process.env.BANKR_MODEL ?? "claude-haiku-4-5-20251001";
|
|
284
|
-
return runOpenAICompatibleLoop("https://llm.bankr.bot/v1/chat/completions", { "X-API-Key": apiKey }, model, "Bankr", userMessage, history, onToolCall);
|
|
285
|
-
}
|
|
286
|
-
// Same OPENAI_BASE_URL override as llm.ts's callOpenAI - lets tool-calling
|
|
287
|
-
// route to a self-hosted OpenAI-compatible gateway too.
|
|
288
|
-
function openAiChatUrl() {
|
|
289
|
-
const base = process.env.OPENAI_BASE_URL?.replace(/\/+$/, "");
|
|
290
|
-
return base ? `${base}/chat/completions` : "https://api.openai.com/v1/chat/completions";
|
|
291
|
-
}
|
|
292
|
-
async function runOpenAILoop(apiKey, userMessage, history, onToolCall) {
|
|
293
|
-
const model = process.env.FINCH_MODEL ?? process.env.OPENAI_MODEL ?? "gpt-4o-mini";
|
|
294
|
-
return runOpenAICompatibleLoop(openAiChatUrl(), { Authorization: `Bearer ${apiKey}` }, model, "OpenAI", userMessage, history, onToolCall);
|
|
295
|
-
}
|
|
296
|
-
// xAI's Chat Completions API is OpenAI-compatible (same as llm.ts's callGrok),
|
|
297
|
-
// so this reuses runOpenAICompatibleLoop rather than a bespoke loop.
|
|
298
|
-
async function runGrokLoop(apiKey, userMessage, history, onToolCall) {
|
|
299
|
-
const model = process.env.FINCH_MODEL ?? process.env.FINCH_GROK_MODEL ?? process.env.GROK_MODEL ?? "grok-4-fast-reasoning";
|
|
300
|
-
return runOpenAICompatibleLoop("https://api.x.ai/v1/chat/completions", { Authorization: `Bearer ${apiKey}` }, model, "Grok", userMessage, history, onToolCall);
|
|
301
|
-
}
|
package/dist/annotations.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MUTATING_TOOL_NAMES = void 0;
|
|
4
|
-
exports.annotationsFor = annotationsFor;
|
|
5
|
-
exports.withAnnotations = withAnnotations;
|
|
6
|
-
const output_schemas_js_1 = require("./output-schemas.js");
|
|
7
|
-
// Read-only AND no network - a purely local computation (static analysis).
|
|
8
|
-
// Read-only but open-world is the default, so this set only exists to flip
|
|
9
|
-
// openWorldHint off for the rare offline tool.
|
|
10
|
-
const READ_ONLY_LOCAL = new Set([
|
|
11
|
-
"audit_contract", // deterministic static Solidity scan, no backend call
|
|
12
|
-
]);
|
|
13
|
-
// readOnly=false, destructive=false, idempotent=true.
|
|
14
|
-
// Toggles and upserts: re-running with the same args lands in the same state.
|
|
15
|
-
const WRITE_IDEMPOTENT = new Set([
|
|
16
|
-
"agent_update",
|
|
17
|
-
"pause_automation",
|
|
18
|
-
"stake_auto_restake",
|
|
19
|
-
"vault_link",
|
|
20
|
-
"vault_pin",
|
|
21
|
-
"vault_tag",
|
|
22
|
-
"vault_unpublish",
|
|
23
|
-
]);
|
|
24
|
-
// readOnly=false, destructive=false.
|
|
25
|
-
// Additive writes / new resources: they create or append, they don't destroy.
|
|
26
|
-
const WRITE = new Set([
|
|
27
|
-
"agent_spawn",
|
|
28
|
-
"chronicle_add",
|
|
29
|
-
"code_session_save",
|
|
30
|
-
"memory_add",
|
|
31
|
-
"memory_extract",
|
|
32
|
-
"memory_consolidate",
|
|
33
|
-
"packet_create",
|
|
34
|
-
"schedule_research",
|
|
35
|
-
"vault_save",
|
|
36
|
-
"vault_store_credential",
|
|
37
|
-
"miroshark_simulate",
|
|
38
|
-
"finch_shell_chat", // orchestrator: can spawn/save/create via delegated tools
|
|
39
|
-
]);
|
|
40
|
-
// readOnly=false, destructive=true.
|
|
41
|
-
// Deletes, cancels, irreversible publishes, and anything that moves real funds
|
|
42
|
-
// (or arms an order engine that will). Clients should confirm before running.
|
|
43
|
-
const DESTRUCTIVE = new Set([
|
|
44
|
-
// removals / cancels
|
|
45
|
-
"cancel_monitor",
|
|
46
|
-
"delete_automation",
|
|
47
|
-
"memory_delete",
|
|
48
|
-
"miroshark_stop",
|
|
49
|
-
"rh_order_cancel",
|
|
50
|
-
"vault_delete",
|
|
51
|
-
// irreversible public exposure
|
|
52
|
-
"packet_share", // "copies already taken remain" per its own description
|
|
53
|
-
// money movement (Base)
|
|
54
|
-
// NOTE: base_mcp_lend deliberately excluded - per its own description it
|
|
55
|
-
// "Returns deposit INSTRUCTIONS only... Does NOT broadcast" - it's a read,
|
|
56
|
-
// not a fund move, so it falls through to the read-only default below.
|
|
57
|
-
"base_mcp_send",
|
|
58
|
-
"base_mcp_swap",
|
|
59
|
-
// off-chain signature that can itself authorise value movement (order,
|
|
60
|
-
// session login) without any on-chain tx - same risk class as a real
|
|
61
|
-
// transfer per its own tool description, so it belongs here rather than
|
|
62
|
-
// in WRITE_IDEMPOTENT ("re-sign = same sig" is true but undersells the risk)
|
|
63
|
-
"wallet_sign_message",
|
|
64
|
-
// money movement (Robinhood Chain)
|
|
65
|
-
"rh_mcp_swap",
|
|
66
|
-
"rh_dca_create", // arms recurring real buys
|
|
67
|
-
"rh_bracket_create", // arms real TP/SL sells
|
|
68
|
-
// a swap/send automation arms unattended, REPEATING real fund movement
|
|
69
|
-
// (the backend's 1-minute cron evaluator fires it) - same risk class as
|
|
70
|
-
// rh_dca_create/rh_bracket_create above, not a plain additive write. An
|
|
71
|
-
// alert-only automation doesn't move funds, but the tool can't tell which
|
|
72
|
-
// kind it's about to create until AFTER the backend parses rawInput, so
|
|
73
|
-
// it's classified by its worst case, same reasoning as rh_orders_tick.
|
|
74
|
-
"create_automation",
|
|
75
|
-
"rh_orders_tick", // preview by default, but can execute:true and move funds
|
|
76
|
-
// executors that run other (possibly fund-moving) tools
|
|
77
|
-
"run_automation",
|
|
78
|
-
"packet_run",
|
|
79
|
-
// money movement (FINCH staking, custodial wallet) - stake locks real value
|
|
80
|
-
// for a fixed period; unstake moves it (plus rewards) back
|
|
81
|
-
"stake_finch",
|
|
82
|
-
"unstake_finch",
|
|
83
|
-
"claim_vested_rewards", // treasury -> custodial wallet USDG transfer
|
|
84
|
-
]);
|
|
85
|
-
function annotationsFor(name) {
|
|
86
|
-
if (DESTRUCTIVE.has(name)) {
|
|
87
|
-
return { readOnlyHint: false, destructiveHint: true, openWorldHint: true };
|
|
88
|
-
}
|
|
89
|
-
if (WRITE_IDEMPOTENT.has(name)) {
|
|
90
|
-
return { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true };
|
|
91
|
-
}
|
|
92
|
-
if (WRITE.has(name)) {
|
|
93
|
-
return { readOnlyHint: false, destructiveHint: false, openWorldHint: true };
|
|
94
|
-
}
|
|
95
|
-
if (READ_ONLY_LOCAL.has(name)) {
|
|
96
|
-
return { readOnlyHint: true, openWorldHint: false };
|
|
97
|
-
}
|
|
98
|
-
return { readOnlyHint: true, openWorldHint: true };
|
|
99
|
-
}
|
|
100
|
-
// Decorate a tool list with MCP metadata: behavioural annotations
|
|
101
|
-
// (readOnly/destructive/etc) and, for tools registered in OUTPUT_SCHEMAS, a
|
|
102
|
-
// machine-readable `outputSchema`. A value already present on the tool is left
|
|
103
|
-
// untouched, so a module can always override either.
|
|
104
|
-
function withAnnotations(tools) {
|
|
105
|
-
return tools.map((t) => {
|
|
106
|
-
const patch = {};
|
|
107
|
-
if (!t.annotations)
|
|
108
|
-
patch.annotations = annotationsFor(t.name);
|
|
109
|
-
if (!t.outputSchema && output_schemas_js_1.OUTPUT_SCHEMAS[t.name]) {
|
|
110
|
-
patch.outputSchema = output_schemas_js_1.OUTPUT_SCHEMAS[t.name];
|
|
111
|
-
}
|
|
112
|
-
return Object.keys(patch).length ? { ...t, ...patch } : t;
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
// Exported for the test suite to assert the classification only references real
|
|
116
|
-
// tool names (catches typos / tools renamed out from under a set).
|
|
117
|
-
exports.MUTATING_TOOL_NAMES = [
|
|
118
|
-
...WRITE_IDEMPOTENT,
|
|
119
|
-
...WRITE,
|
|
120
|
-
...DESTRUCTIVE,
|
|
121
|
-
...READ_ONLY_LOCAL,
|
|
122
|
-
];
|