@finchagentic/mcp 4.7.2 → 4.7.4
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/README.md +74 -39
- package/dist/_zod-helpers.js +10 -0
- package/dist/cli-doctor.js +300 -0
- package/dist/cli-env.js +91 -0
- package/dist/cli-install.js +236 -0
- package/dist/cli-login.js +101 -0
- package/dist/cli-orders.js +175 -0
- package/dist/cli-setup.js +270 -0
- package/dist/cli-ui.js +168 -0
- package/dist/cli-vault.js +75 -0
- package/dist/cli.js +61 -1152
- package/dist/convex.js +16 -5
- package/dist/enrichment-router.js +5 -18
- package/dist/finch-output.js +30 -29
- package/dist/finch-status.js +126 -44
- package/dist/local-vault.js +5 -12
- package/dist/output-schemas.js +3 -12
- package/dist/project.js +4 -13
- package/dist/server.js +13 -35
- package/dist/tool-filter.js +4 -13
- package/dist/tools/_solidity-scan.js +5 -14
- package/dist/tools/agents.js +14 -26
- package/dist/tools/deep-research-constants.js +33 -0
- package/dist/tools/deep-research-firecrawl.js +88 -0
- package/dist/tools/deep-research-planning.js +249 -0
- package/dist/tools/deep-research-synthesis.js +343 -0
- package/dist/tools/deep-research-text.js +158 -0
- package/dist/tools/deep-research-tools.js +90 -0
- package/dist/tools/deep-research.js +47 -938
- package/dist/tools/defi.js +4 -3
- package/dist/tools/insider.js +4 -14
- package/dist/tools/insight.js +7 -6
- package/dist/tools/market.js +16 -15
- package/dist/tools/memory.js +43 -61
- package/dist/tools/monitor.js +7 -6
- package/dist/tools/os.js +7 -85
- package/dist/tools/research.js +7 -6
- package/dist/tools/rh-mcp-constants.js +65 -0
- package/dist/tools/rh-mcp-dex.js +107 -0
- package/dist/tools/rh-mcp-provider.js +127 -0
- package/dist/tools/rh-mcp-resolve.js +137 -0
- package/dist/tools/rh-mcp-risk.js +230 -0
- package/dist/tools/rh-mcp-safety.js +234 -0
- package/dist/tools/rh-mcp-swap.js +181 -0
- package/dist/tools/rh-mcp-tools.js +137 -0
- package/dist/tools/rh-mcp.js +75 -1191
- package/dist/tools/scanner.js +10 -9
- package/dist/tools/stake.js +7 -20
- package/dist/tools/vault.js +52 -59
- package/package.json +13 -10
package/dist/tools/agents.js
CHANGED
|
@@ -9,6 +9,7 @@ const llm_js_1 = require("../llm.js");
|
|
|
9
9
|
const local_vault_js_1 = require("../local-vault.js");
|
|
10
10
|
const memory_js_1 = require("./memory.js");
|
|
11
11
|
const project_js_1 = require("../project.js");
|
|
12
|
+
const _zod_helpers_js_1 = require("../_zod-helpers.js");
|
|
12
13
|
// ─── Agent Learning Memory (v3.25) ──────────────────────────────────────────
|
|
13
14
|
// After every agent_update, an LLM reviews the new progress in context of the
|
|
14
15
|
// agent's goal + prior learnings to extract a single repeatable insight. The
|
|
@@ -104,23 +105,10 @@ function withAgentLock(agentName, fn) {
|
|
|
104
105
|
});
|
|
105
106
|
return next;
|
|
106
107
|
}
|
|
107
|
-
// list_agents/hire_agent
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
// always 404'd. Re-add them (and the matching handler blocks below) only
|
|
112
|
-
// alongside building those two routes for real.
|
|
113
|
-
//
|
|
114
|
-
// Same story, same fix, applied again here: agent_identity, agent_schedule,
|
|
115
|
-
// agent_unschedule, agent_pause, agent_resume and agent_runs called
|
|
116
|
-
// /agents/identity, /agents/schedule, /agents/unschedule, /agents/pause and
|
|
117
|
-
// /agents/runs - none of which exist in http.ts, and none of which have any
|
|
118
|
-
// backing data model in schema.ts (no agent-identity table, no scheduler,
|
|
119
|
-
// no run-history table; there isn't even a cron for it in crons.ts). This
|
|
120
|
-
// isn't a missing route, it's an unbuilt subsystem, so the tools were
|
|
121
|
-
// removed rather than stubbed. Re-add only alongside building that
|
|
122
|
-
// autonomous-scheduling backend for real: identity/schedule storage, a cron
|
|
123
|
-
// that actually wakes agents up, and run-history writes.
|
|
108
|
+
// list_agents/hire_agent/agent_identity/agent_schedule/agent_unschedule/
|
|
109
|
+
// agent_pause/agent_resume/agent_runs deleted - their backend routes never
|
|
110
|
+
// existed (dangling comments in http.ts, no actual route/data model). Not a
|
|
111
|
+
// missing route, an unbuilt subsystem. Re-add only alongside building it.
|
|
124
112
|
exports.AGENT_TOOLS = [
|
|
125
113
|
{
|
|
126
114
|
name: "agent_spawn",
|
|
@@ -226,9 +214,9 @@ function buildAgentLedger(name, versions) {
|
|
|
226
214
|
}
|
|
227
215
|
async function handleAgentTool(name, args) {
|
|
228
216
|
if (name === "agent_spawn") {
|
|
229
|
-
const parsed =
|
|
230
|
-
if (!parsed.
|
|
231
|
-
return
|
|
217
|
+
const parsed = (0, _zod_helpers_js_1.parseOrError)(SpawnAgentSchema, args);
|
|
218
|
+
if (!parsed.ok)
|
|
219
|
+
return parsed.error;
|
|
232
220
|
const { name: agentName, goal, context, workspaceProject } = parsed.data;
|
|
233
221
|
const content = JSON.stringify({
|
|
234
222
|
goal,
|
|
@@ -269,9 +257,9 @@ async function handleAgentTool(name, args) {
|
|
|
269
257
|
};
|
|
270
258
|
}
|
|
271
259
|
if (name === "agent_recall") {
|
|
272
|
-
const parsed =
|
|
273
|
-
if (!parsed.
|
|
274
|
-
return
|
|
260
|
+
const parsed = (0, _zod_helpers_js_1.parseOrError)(RecallAgentSchema, args);
|
|
261
|
+
if (!parsed.ok)
|
|
262
|
+
return parsed.error;
|
|
275
263
|
const localVault = (0, local_vault_js_1.getLocalVaultConfig)();
|
|
276
264
|
let data;
|
|
277
265
|
try {
|
|
@@ -334,9 +322,9 @@ async function handleAgentTool(name, args) {
|
|
|
334
322
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
335
323
|
}
|
|
336
324
|
if (name === "agent_update") {
|
|
337
|
-
const parsed =
|
|
338
|
-
if (!parsed.
|
|
339
|
-
return
|
|
325
|
+
const parsed = (0, _zod_helpers_js_1.parseOrError)(UpdateAgentSchema, args);
|
|
326
|
+
if (!parsed.ok)
|
|
327
|
+
return parsed.error;
|
|
340
328
|
const { name: agentName, progress, findings, status = "active", nextStep } = parsed.data;
|
|
341
329
|
return withAgentLock(agentName, async () => {
|
|
342
330
|
const localVault = (0, local_vault_js_1.getLocalVaultConfig)();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared constants for the deep_research pipeline (deep-research-*.ts).
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.STOPWORDS = exports.FRESH_TRIGGER_RE = exports.NEWS_DOMAIN_BOOST_RE = exports.DOMAIN_TIER_BONUS = exports.EXCERPT_TOP_CHUNKS = exports.EXCERPT_CHARS_PER_CHUNK = exports.MAX_PER_DOMAIN = exports.FC_BASE = void 0;
|
|
5
|
+
exports.FC_BASE = "https://api.firecrawl.dev/v1";
|
|
6
|
+
exports.MAX_PER_DOMAIN = 2; // source diversity - cap hits per domain
|
|
7
|
+
exports.EXCERPT_CHARS_PER_CHUNK = 600;
|
|
8
|
+
exports.EXCERPT_TOP_CHUNKS = 4; // pick top N relevant chunks per source
|
|
9
|
+
// Quality scoring - higher = more trustworthy primary source
|
|
10
|
+
exports.DOMAIN_TIER_BONUS = [
|
|
11
|
+
[/\.gov(\b|\/|$)/i, 4],
|
|
12
|
+
[/\.edu(\b|\/|$)/i, 3],
|
|
13
|
+
[/(?:nature|science|nih|arxiv|acm|ieee|sciencedirect)\.(?:org|com)/i, 3],
|
|
14
|
+
// Crypto primary-source boost - these are the authoritative data sources
|
|
15
|
+
// for protocol TVL, yields, prices, and on-chain analytics. Rank above
|
|
16
|
+
// generic news for crypto queries.
|
|
17
|
+
[/(?:defillama|tokenterminal|coingecko|coinmarketcap|dune|messari|artemis)\.(?:com|fi)/i, 3],
|
|
18
|
+
[/(?:etherscan|basescan|arbiscan|solscan|polygonscan|optimistic\.etherscan)\.(?:io|com)/i, 2],
|
|
19
|
+
[/(?:reuters|apnews|bbc|economist|ft|wsj|bloomberg)\.com/i, 2],
|
|
20
|
+
[/(?:coindesk|theblock|cointelegraph|decrypt|theinformation)\.(?:co|com|io)/i, 1],
|
|
21
|
+
[/(?:wikipedia|github|stackoverflow)\.(?:org|com)/i, 1],
|
|
22
|
+
[/(?:medium|substack|reddit|twitter|x)\.com/i, -1],
|
|
23
|
+
];
|
|
24
|
+
// News-domain bonus - applied additionally in fresh mode
|
|
25
|
+
exports.NEWS_DOMAIN_BOOST_RE = /(?:reuters|apnews|bbc|economist|ft|wsj|bloomberg|cnbc|theverge|techcrunch|axios|coindesk|theinformation|nytimes|guardian|aljazeera)\.com/i;
|
|
26
|
+
// Keywords that signal a time-sensitive query - trigger fresh mode auto.
|
|
27
|
+
exports.FRESH_TRIGGER_RE = /\b(today|tonight|tomorrow|yesterday|this week|last week|past week|latest|breaking|just now|recent|currently|now|live|happening|this month|last month|past month|past \d+ days?|last \d+ days?|q[1-4]|h[12]|202[6-9])\b/i;
|
|
28
|
+
exports.STOPWORDS = new Set([
|
|
29
|
+
"the", "and", "for", "with", "that", "this", "what", "when", "where", "how", "why",
|
|
30
|
+
"have", "has", "had", "are", "was", "were", "will", "would", "could", "should", "does",
|
|
31
|
+
"did", "being", "been", "from", "into", "over", "under", "about", "into", "than", "then",
|
|
32
|
+
"your", "yours", "their", "they", "them", "there", "here", "just", "also", "more", "most",
|
|
33
|
+
]);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Firecrawl search/scrape - BYOK direct call with a Finch backend-proxy
|
|
3
|
+
// fallback for signed-in users without their own key.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.normalizeSearchHits = normalizeSearchHits;
|
|
6
|
+
exports.fcSearch = fcSearch;
|
|
7
|
+
exports.fcScrape = fcScrape;
|
|
8
|
+
const convex_js_1 = require("../convex.js");
|
|
9
|
+
const deep_research_constants_js_1 = require("./deep-research-constants.js");
|
|
10
|
+
/**
|
|
11
|
+
* Firecrawl returns either a flat array or a keyed object of result groups
|
|
12
|
+
* (`{ web: [...], news: [...] }`). The proxy path assumed the flat shape and
|
|
13
|
+
* handed an object to `.forEach`, which threw — invisible until now because
|
|
14
|
+
* without an API key the run always died at the planner first.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeSearchHits(raw) {
|
|
17
|
+
if (Array.isArray(raw))
|
|
18
|
+
return raw;
|
|
19
|
+
if (raw && typeof raw === "object") {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const group of Object.values(raw)) {
|
|
22
|
+
if (Array.isArray(group))
|
|
23
|
+
out.push(...group);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
async function fcSearch(query, limit) {
|
|
30
|
+
// BYOK path - direct call to Firecrawl with user's key. Fastest, no proxy hop.
|
|
31
|
+
const key = process.env.FIRECRAWL_API_KEY;
|
|
32
|
+
if (key) {
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`${deep_research_constants_js_1.FC_BASE}/search`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
37
|
+
body: JSON.stringify({ query, limit }),
|
|
38
|
+
signal: AbortSignal.timeout(15000),
|
|
39
|
+
});
|
|
40
|
+
if (res.ok) {
|
|
41
|
+
const data = (await res.json());
|
|
42
|
+
return normalizeSearchHits(data.data);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch { /* fall through */ }
|
|
46
|
+
}
|
|
47
|
+
// Backend-proxy path - session-authed; Finch covers Firecrawl cost.
|
|
48
|
+
try {
|
|
49
|
+
const data = await (0, convex_js_1.callConvex)("/research/firecrawl-search", "POST", { query, limit }, "web_search", 20000);
|
|
50
|
+
return normalizeSearchHits(data?.results);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function fcScrape(url) {
|
|
57
|
+
const key = process.env.FIRECRAWL_API_KEY;
|
|
58
|
+
if (key) {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(`${deep_research_constants_js_1.FC_BASE}/scrape`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
|
|
63
|
+
body: JSON.stringify({ url, formats: ["markdown"], onlyMainContent: true }),
|
|
64
|
+
signal: AbortSignal.timeout(20000),
|
|
65
|
+
});
|
|
66
|
+
if (res.ok) {
|
|
67
|
+
const data = (await res.json());
|
|
68
|
+
const md = data.data?.markdown;
|
|
69
|
+
if (md) {
|
|
70
|
+
const meta = data.data?.metadata;
|
|
71
|
+
const publishedAt = meta?.publishedAt ?? meta?.ogPublishedTime ?? meta?.["article:published_time"];
|
|
72
|
+
return { markdown: md, publishedAt };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch { /* fall through */ }
|
|
77
|
+
}
|
|
78
|
+
// Backend-proxy path. Returns markdown only - no metadata extraction yet,
|
|
79
|
+
// which means continueFrom can't auto-date proxied scrapes. Acceptable
|
|
80
|
+
// tradeoff for now; markdown is the primary signal.
|
|
81
|
+
try {
|
|
82
|
+
const data = await (0, convex_js_1.callConvex)("/research/firecrawl-scrape", "POST", { url }, "web_scrape", 25000);
|
|
83
|
+
if (data?.markdown)
|
|
84
|
+
return { markdown: data.markdown };
|
|
85
|
+
}
|
|
86
|
+
catch { /* swallow */ }
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Query planning: sub-query derivation, LLM planners (flat + multi-angle),
|
|
3
|
+
// the deep-mode adversarial critic, and gap-finding reflection.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.deriveSubQueries = deriveSubQueries;
|
|
6
|
+
exports.planQueries = planQueries;
|
|
7
|
+
exports.planAngles = planAngles;
|
|
8
|
+
exports.runCritic = runCritic;
|
|
9
|
+
exports.reflectAndExtend = reflectAndExtend;
|
|
10
|
+
const llm_js_1 = require("../llm.js");
|
|
11
|
+
const deep_research_text_js_1 = require("./deep-research-text.js");
|
|
12
|
+
/**
|
|
13
|
+
* Sub-queries without an LLM.
|
|
14
|
+
*
|
|
15
|
+
* The caller should normally pass `queries` — it is a model and plans better
|
|
16
|
+
* than any heuristic. This exists so the evidence-pack path still works with
|
|
17
|
+
* zero configuration, rather than failing when no API key is set.
|
|
18
|
+
*/
|
|
19
|
+
function deriveSubQueries(query, n, focus, freshMode) {
|
|
20
|
+
const base = query.trim().replace(/\?+$/, "");
|
|
21
|
+
const recency = freshMode ? ` ${new Date().getFullYear()} latest` : "";
|
|
22
|
+
const lenses = focus
|
|
23
|
+
? [focus, `${focus} risks`, `${focus} data`]
|
|
24
|
+
: ["overview", "latest developments", "risks and criticism", "data and numbers", "expert analysis"];
|
|
25
|
+
const out = [`${base}${recency}`];
|
|
26
|
+
for (const lens of lenses) {
|
|
27
|
+
if (out.length >= Math.max(2, n))
|
|
28
|
+
break;
|
|
29
|
+
out.push(`${base} ${lens}${recency}`);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
async function planQueries(query, n, focus, priorContext, freshMode) {
|
|
34
|
+
const focusNote = focus ? ` Focus angle: ${focus}.` : "";
|
|
35
|
+
const sys = "You are a research planner. Output strict JSON only - no preamble, no markdown.";
|
|
36
|
+
const freshNote = freshMode
|
|
37
|
+
? `
|
|
38
|
+
|
|
39
|
+
⏱ FRESH MODE - last ${freshMode.days} days:
|
|
40
|
+
- Add a recency hint to most sub-questions: "in ${(0, deep_research_text_js_1.currentYearMonth)()}", "last ${freshMode.days} days", "this week", "as of ${(0, deep_research_text_js_1.todayISO)()}", etc.
|
|
41
|
+
- Prefer queries that surface news / press releases / X posts over evergreen background.
|
|
42
|
+
- Skip generic background - the user wants what's CURRENT, not historical.
|
|
43
|
+
- At least 70% of sub-questions must include a date or recency token.`
|
|
44
|
+
: "";
|
|
45
|
+
const continuationNote = priorContext
|
|
46
|
+
? `
|
|
47
|
+
|
|
48
|
+
⚠️ CONTINUATION MODE - there is a PRIOR research report on this topic:
|
|
49
|
+
|
|
50
|
+
"""
|
|
51
|
+
${priorContext.slice(0, 2500)}
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
Your sub-questions must focus on:
|
|
55
|
+
1. UPDATES - what has changed since the prior report (new releases, news, data revisions)
|
|
56
|
+
2. GAPS - angles the prior report explicitly listed as open questions or follow-ups
|
|
57
|
+
3. NEW developments - entities/events the prior report doesn't mention
|
|
58
|
+
4. VERIFICATION - claims the prior report flagged as low-confidence or single-source
|
|
59
|
+
|
|
60
|
+
DO NOT re-tread material already well-covered in the prior report. The user already has those answers.`
|
|
61
|
+
: "";
|
|
62
|
+
const user = `Decompose this research question into ${n} sub-questions that together cover the topic from different angles.${focusNote}${continuationNote}${freshNote}
|
|
63
|
+
|
|
64
|
+
Rules:
|
|
65
|
+
- Each sub-question must be a standalone web search query, under 90 chars.
|
|
66
|
+
- Cover different facets: definition, current state, key actors, comparisons, counterarguments, recent news, forward outlook.
|
|
67
|
+
- No duplicates, no near-paraphrases.
|
|
68
|
+
|
|
69
|
+
ENTITY-HUNTING - at least HALF of your sub-questions must target queries likely to surface:
|
|
70
|
+
- Specific company / product / framework names (e.g., "LangGraph adoption stats", "Manus orchestration funding")
|
|
71
|
+
- Dollar amounts (acquisitions, funding rounds, revenue, ARR, market size)
|
|
72
|
+
- Benchmark numbers (% adoption, latency ms, accuracy scores, MMLU/HumanEval/SWE-bench results)
|
|
73
|
+
- Specific dates and timeline events (when X launched, when Y reached scale)
|
|
74
|
+
- Named studies / surveys / reports (e.g., "Anthropic Economic Index 2026", "a16z AI infrastructure report")
|
|
75
|
+
|
|
76
|
+
Bad: "what is X" → too generic, returns Wikipedia
|
|
77
|
+
Good: "X adoption rate enterprise 2026 survey" → returns concrete stats
|
|
78
|
+
|
|
79
|
+
Question: "${query}"
|
|
80
|
+
|
|
81
|
+
Return: {"queries": ["...", "..."]} - exactly ${n} items.`;
|
|
82
|
+
let raw = "";
|
|
83
|
+
try {
|
|
84
|
+
raw = await (0, llm_js_1.callLLM)(sys, user, 500, [], 30000);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return [query];
|
|
88
|
+
}
|
|
89
|
+
const parsed = (0, deep_research_text_js_1.safeParseJson)(raw, {});
|
|
90
|
+
if (!parsed.queries || !Array.isArray(parsed.queries))
|
|
91
|
+
return [query];
|
|
92
|
+
const queries = parsed.queries
|
|
93
|
+
.filter((s) => typeof s === "string")
|
|
94
|
+
.map((s) => s.trim())
|
|
95
|
+
.filter((s) => s.length > 0 && s.length <= 200)
|
|
96
|
+
.slice(0, n);
|
|
97
|
+
return queries.length > 0 ? queries : [query];
|
|
98
|
+
}
|
|
99
|
+
async function planAngles(query, n, focus, priorContext, freshMode) {
|
|
100
|
+
const focusNote = focus ? ` Focus angle: ${focus}.` : "";
|
|
101
|
+
const freshNote = freshMode
|
|
102
|
+
? `\n\n⏱ FRESH MODE - last ${freshMode.days} days: bias each angle's queries toward news/recent press/dated reports.`
|
|
103
|
+
: "";
|
|
104
|
+
const continuationNote = priorContext
|
|
105
|
+
? `\n\n⚠️ CONTINUATION - prior report exists. Angles must focus on UPDATES, GAPS, NEW developments since:\n"""\n${priorContext.slice(0, 1500)}\n"""`
|
|
106
|
+
: "";
|
|
107
|
+
const sys = "You are a research planner that decomposes topics into specialist angles. Output strict JSON only.";
|
|
108
|
+
const user = `Break this research topic into ${n} DIFFERENT specialist angles. Each angle gets its own 2-3 search queries.${focusNote}${continuationNote}${freshNote}
|
|
109
|
+
|
|
110
|
+
Topic: "${query}"
|
|
111
|
+
|
|
112
|
+
Pick angles that are GENUINELY different - not paraphrases of the same question. Good angle diversity examples:
|
|
113
|
+
- data / quantitative metrics
|
|
114
|
+
- competitive landscape
|
|
115
|
+
- team / governance / actors
|
|
116
|
+
- recent news / catalysts
|
|
117
|
+
- counterarguments / risks / criticism
|
|
118
|
+
- forward outlook / projections
|
|
119
|
+
- historical context / origins
|
|
120
|
+
- regulatory / policy angle
|
|
121
|
+
- technical / mechanism
|
|
122
|
+
- ecosystem partners
|
|
123
|
+
|
|
124
|
+
Pick the ${n} angles that most fit THIS specific topic. Each angle should have a label (3-6 words) and rationale (1 sentence). Queries must be standalone, ≤90 chars, entity-rich (specific names, dollar amounts, dates, benchmark numbers - NOT generic background).
|
|
125
|
+
|
|
126
|
+
Return strict JSON:
|
|
127
|
+
{
|
|
128
|
+
"angles": [
|
|
129
|
+
{
|
|
130
|
+
"label": "...",
|
|
131
|
+
"rationale": "...",
|
|
132
|
+
"queries": ["...", "..."]
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Exactly ${n} angles. 2-3 queries per angle. No duplicate queries across angles.`;
|
|
138
|
+
let raw = "";
|
|
139
|
+
try {
|
|
140
|
+
raw = await (0, llm_js_1.callLLM)(sys, user, 1200, [], 30000);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const parsed = (0, deep_research_text_js_1.safeParseJson)(raw, {});
|
|
146
|
+
if (!parsed.angles || !Array.isArray(parsed.angles))
|
|
147
|
+
return [];
|
|
148
|
+
const angles = [];
|
|
149
|
+
for (const a of parsed.angles) {
|
|
150
|
+
if (typeof a !== "object" || a === null)
|
|
151
|
+
continue;
|
|
152
|
+
const o = a;
|
|
153
|
+
if (typeof o.label !== "string" || typeof o.rationale !== "string")
|
|
154
|
+
continue;
|
|
155
|
+
if (!Array.isArray(o.queries))
|
|
156
|
+
continue;
|
|
157
|
+
const queries = o.queries
|
|
158
|
+
.filter((q) => typeof q === "string")
|
|
159
|
+
.map((q) => q.trim())
|
|
160
|
+
.filter((q) => q.length > 0 && q.length <= 200)
|
|
161
|
+
.slice(0, 3);
|
|
162
|
+
if (queries.length === 0)
|
|
163
|
+
continue;
|
|
164
|
+
angles.push({
|
|
165
|
+
label: o.label.trim().slice(0, 80),
|
|
166
|
+
rationale: o.rationale.trim().slice(0, 200),
|
|
167
|
+
queries,
|
|
168
|
+
});
|
|
169
|
+
if (angles.length >= n)
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
return angles;
|
|
173
|
+
}
|
|
174
|
+
// Adversarial critic - only runs for depth=deep. Reads the draft + sources
|
|
175
|
+
// already gathered and produces a structured challenge block: single-source
|
|
176
|
+
// claims, contradictions across angles, speculation framed as fact. The
|
|
177
|
+
// final synthesizer is told to incorporate or refute these challenges
|
|
178
|
+
// explicitly, lifting the quality floor.
|
|
179
|
+
async function runCritic(query, draft, angles, sourceCount) {
|
|
180
|
+
const sys = "You are an adversarial research critic. Be terse, specific, and unsparing. Output markdown.";
|
|
181
|
+
const angleLabels = angles.map((a, i) => `${i + 1}. ${a.label}`).join("\n");
|
|
182
|
+
const user = `Original question: "${query}"
|
|
183
|
+
|
|
184
|
+
Specialist angles investigated:
|
|
185
|
+
${angleLabels}
|
|
186
|
+
|
|
187
|
+
Total sources gathered: ${sourceCount}
|
|
188
|
+
|
|
189
|
+
Draft report:
|
|
190
|
+
"""
|
|
191
|
+
${draft.slice(0, 5000)}
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
Audit the draft for quality problems. Be specific and quote spans where possible.
|
|
195
|
+
|
|
196
|
+
Identify:
|
|
197
|
+
1. **Single-source claims** - major assertions resting on one [N] citation that aren't widely corroborated
|
|
198
|
+
2. **Contradictions** - places where different sources disagree but the draft doesn't surface the disagreement
|
|
199
|
+
3. **Speculation framed as fact** - confident statements about future/intent/cause without evidence
|
|
200
|
+
4. **Coverage gaps** - angles from the list above that got shallow treatment
|
|
201
|
+
5. **Stale or weak sources** - dated material treated as current, blog posts cited as primary data
|
|
202
|
+
|
|
203
|
+
Output format:
|
|
204
|
+
## Critic notes
|
|
205
|
+
- **[type]**: specific issue + relevant quote or claim. (1-2 sentences each)
|
|
206
|
+
- Skip categories with no findings - don't pad.
|
|
207
|
+
|
|
208
|
+
End with one line: "Net recommendation: [accept|revise|reject]"
|
|
209
|
+
|
|
210
|
+
If the draft is solid, say so plainly - don't manufacture issues.`;
|
|
211
|
+
try {
|
|
212
|
+
const notes = await (0, llm_js_1.callLLM)(sys, user, 1500, [], 60000);
|
|
213
|
+
return notes.trim();
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return ""; // critic failure shouldn't block final synth
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function reflectAndExtend(query, draft, existingQueries, n) {
|
|
220
|
+
const sys = "You are a research auditor. Find gaps in a draft report and propose follow-up search queries. Output strict JSON.";
|
|
221
|
+
const user = `Original question: "${query}"
|
|
222
|
+
|
|
223
|
+
Queries already run:
|
|
224
|
+
${existingQueries.map((q, i) => `${i + 1}. ${q}`).join("\n")}
|
|
225
|
+
|
|
226
|
+
Draft report:
|
|
227
|
+
"""
|
|
228
|
+
${draft.slice(0, 4000)}
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
Identify ${n} GAPS in the draft - angles missing, claims that need verification, counter-perspectives not represented, or recent developments not covered. For each gap, give ONE web search query (≤90 chars) that would fill it.
|
|
232
|
+
|
|
233
|
+
Return: {"gap_queries": ["...", "..."]} - exactly ${n} items, no duplicates of existing queries.`;
|
|
234
|
+
let raw = "";
|
|
235
|
+
try {
|
|
236
|
+
raw = await (0, llm_js_1.callLLM)(sys, user, 500, [], 30000);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
const parsed = (0, deep_research_text_js_1.safeParseJson)(raw, {});
|
|
242
|
+
if (!parsed.gap_queries || !Array.isArray(parsed.gap_queries))
|
|
243
|
+
return [];
|
|
244
|
+
return parsed.gap_queries
|
|
245
|
+
.filter((s) => typeof s === "string")
|
|
246
|
+
.map((s) => s.trim())
|
|
247
|
+
.filter((s) => s.length > 0 && s.length <= 200)
|
|
248
|
+
.slice(0, n);
|
|
249
|
+
}
|