@maintainabilityai/research-runner 0.1.3 → 0.1.5
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/llm/llm-router.js +12 -3
- package/dist/runner/archeologist.js +25 -0
- package/package.json +1 -1
package/dist/llm/llm-router.js
CHANGED
|
@@ -10,7 +10,16 @@ const MODEL_BY_TIER = {
|
|
|
10
10
|
};
|
|
11
11
|
async function callLlm(opts) {
|
|
12
12
|
const tierModels = MODEL_BY_TIER[opts.tier];
|
|
13
|
-
|
|
13
|
+
// Hybrid routing: GitHub Models free tier caps requests at ~8K input
|
|
14
|
+
// tokens — too small for the synthesis step (full brief + every search
|
|
15
|
+
// result + mesh context routinely exceeds that). When the brief asks
|
|
16
|
+
// for github-models AND an Anthropic key is available, route synth →
|
|
17
|
+
// Anthropic and keep plan (small prompt) on github-models. Caller can
|
|
18
|
+
// force pure github-models by not setting anthropicApiKey.
|
|
19
|
+
const effectiveProvider = opts.provider === 'github-models' && opts.tier === 'synth' && opts.anthropicApiKey
|
|
20
|
+
? 'anthropic'
|
|
21
|
+
: opts.provider;
|
|
22
|
+
if (effectiveProvider === 'anthropic') {
|
|
14
23
|
if (!opts.anthropicApiKey) {
|
|
15
24
|
throw new Error(`callLlm: provider=anthropic requires anthropicApiKey (set ANTHROPIC_API_KEY).`);
|
|
16
25
|
}
|
|
@@ -33,7 +42,7 @@ async function callLlm(opts) {
|
|
|
33
42
|
httpStatus: r.httpStatus,
|
|
34
43
|
};
|
|
35
44
|
}
|
|
36
|
-
if (
|
|
45
|
+
if (effectiveProvider === 'github-models') {
|
|
37
46
|
if (!opts.githubToken) {
|
|
38
47
|
throw new Error(`callLlm: provider=github-models requires githubToken (set GITHUB_TOKEN; workflow needs \`permissions: models: read\`).`);
|
|
39
48
|
}
|
|
@@ -56,5 +65,5 @@ async function callLlm(opts) {
|
|
|
56
65
|
httpStatus: r.httpStatus,
|
|
57
66
|
};
|
|
58
67
|
}
|
|
59
|
-
throw new Error(`callLlm: provider "${
|
|
68
|
+
throw new Error(`callLlm: provider "${effectiveProvider}" not yet implemented (phase 2c.1 ships anthropic + github-models; openai + azure-openai land later).`);
|
|
60
69
|
}
|
|
@@ -76,6 +76,18 @@ const synthesize_report_1 = require("./nodes/synthesize-report");
|
|
|
76
76
|
const clone_and_index_1 = require("./nodes/clone-and-index");
|
|
77
77
|
const analyze_architecture_1 = require("./nodes/analyze-architecture");
|
|
78
78
|
const identify_gaps_1 = require("./nodes/identify-gaps");
|
|
79
|
+
/**
|
|
80
|
+
* Progress log → stderr. Goes to GitHub Actions job output without
|
|
81
|
+
* polluting stdout (which carries the JSON result the workflow parses).
|
|
82
|
+
* Disabled when RESEARCH_RUNNER_QUIET=1 so unit tests stay clean.
|
|
83
|
+
*/
|
|
84
|
+
function progress(msg) {
|
|
85
|
+
if (process.env.RESEARCH_RUNNER_QUIET === '1') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const ts = new Date().toISOString().slice(11, 19); // HH:MM:SS
|
|
89
|
+
process.stderr.write(`[research-runner ${ts}] ${msg}\n`);
|
|
90
|
+
}
|
|
79
91
|
async function runArcheologist(opts) {
|
|
80
92
|
// ----- validate_brief (pure) -----
|
|
81
93
|
const briefParsed = schemas_1.ResearchBrief.safeParse(opts.brief);
|
|
@@ -89,6 +101,7 @@ async function runArcheologist(opts) {
|
|
|
89
101
|
const githubToken = opts.githubToken ?? process.env.GITHUB_TOKEN ?? '';
|
|
90
102
|
const tavilyApiKey = opts.tavilyApiKey ?? process.env.TAVILY_API_KEY ?? '';
|
|
91
103
|
const usptoApiKey = opts.usptoApiKey ?? process.env.USPTO_API_KEY ?? '';
|
|
104
|
+
progress(`▶ run ${runId} | scope=${brief.scope.level}(${brief.scope.id}) | path=${brief.path} | llm_provider=${brief.llm_provider ?? 'anthropic'} | keys: anthropic=${!!anthropicApiKey} github=${!!githubToken} tavily=${!!tavilyApiKey} uspto=${!!usptoApiKey}`);
|
|
92
105
|
const absoluteAuditDir = path.resolve(opts.meshDir, opts.auditDir);
|
|
93
106
|
const absoluteOutputDir = path.resolve(opts.meshDir, opts.outputDir);
|
|
94
107
|
fs.mkdirSync(absoluteOutputDir, { recursive: true });
|
|
@@ -240,6 +253,7 @@ async function runArcheologist(opts) {
|
|
|
240
253
|
// ============================================================================
|
|
241
254
|
// RESEARCH PATH (existing): plan_queries → 4 providers → dedupe → gap-analysis
|
|
242
255
|
// ============================================================================
|
|
256
|
+
progress(`◐ plan_queries — calling LLM to generate query plan…`);
|
|
243
257
|
const planStart = Date.now();
|
|
244
258
|
const plan = await (0, plan_queries_1.planQueries)({
|
|
245
259
|
meshDir: opts.meshDir,
|
|
@@ -251,6 +265,7 @@ async function runArcheologist(opts) {
|
|
|
251
265
|
fetchImpl: opts.fetchImpl,
|
|
252
266
|
});
|
|
253
267
|
researchQueryPlan = plan.queryPlan;
|
|
268
|
+
progress(`✓ plan_queries (${plan.llm.provider} ${plan.llm.model}) in ${Date.now() - planStart}ms — ${plan.llm.inputTokens} in / ${plan.llm.outputTokens} out tokens, ${plan.llm.attempts} attempt${plan.llm.attempts !== 1 ? 's' : ''} → web=${plan.queryPlan.web.length} arxiv=${plan.queryPlan.arxiv.length} patent=${plan.queryPlan.patent.length} community=${plan.queryPlan.community.length}`);
|
|
254
269
|
totalInputTokens += plan.llm.inputTokens;
|
|
255
270
|
totalOutputTokens += plan.llm.outputTokens;
|
|
256
271
|
totalCostUsd += plan.llm.costUsd;
|
|
@@ -271,6 +286,7 @@ async function runArcheologist(opts) {
|
|
|
271
286
|
// ----- four-provider search (pure_api each, parallel across providers) -----
|
|
272
287
|
// We run all four providers concurrently with Promise.allSettled so a
|
|
273
288
|
// provider-level failure (e.g. PatentsView outage) doesn't block the rest.
|
|
289
|
+
progress(`◐ search — tavily(${plan.queryPlan.web.length}) + arxiv(${plan.queryPlan.arxiv.length}) + hackernews(${plan.queryPlan.community.length}) + uspto(${usptoApiKey ? plan.queryPlan.patent.length : 'skipped'}) in parallel…`);
|
|
274
290
|
const searchStart = Date.now();
|
|
275
291
|
const [tavily, arxiv, hn, uspto] = await Promise.allSettled([
|
|
276
292
|
(0, tavily_search_1.runTavilySearch)({ apiKey: tavilyApiKey, queries: plan.queryPlan.web, fetchImpl: opts.fetchImpl }),
|
|
@@ -330,9 +346,12 @@ async function runArcheologist(opts) {
|
|
|
330
346
|
handleProvider(arxiv, 'arxiv_search', 'arxiv', 'GET /api/query');
|
|
331
347
|
handleProvider(hn, 'hackernews_search', 'hackernews', 'GET /api/v1/search');
|
|
332
348
|
handleProvider(uspto, 'uspto_search', 'uspto', 'POST /api/v1/patent/');
|
|
349
|
+
const fmtSettled = (s) => s.status === 'fulfilled' ? 'OK' : `FAIL(${s.reason instanceof Error ? s.reason.message.slice(0, 60) : String(s.reason).slice(0, 60)})`;
|
|
350
|
+
progress(`✓ search done in ${searchDuration}ms — tavily=${providerResultCounts.tavily}/${fmtSettled(tavily)} arxiv=${providerResultCounts.arxiv}/${fmtSettled(arxiv)} hn=${providerResultCounts.hackernews}/${fmtSettled(hn)} uspto=${providerResultCounts.uspto}/${fmtSettled(uspto)} (raw=${allProviderResults.length})`);
|
|
333
351
|
// ----- dedupe_and_rank (pure) — first pass -----
|
|
334
352
|
let dedupeStart = Date.now();
|
|
335
353
|
rankedSources = (0, dedupe_and_rank_1.dedupeAndRank)({ results: allProviderResults, topN: 20 });
|
|
354
|
+
progress(`✓ dedupe_and_rank — ${rankedSources.length} ranked sources (top score=${rankedSources[0]?.salience_score?.toFixed(2) ?? 'n/a'})`);
|
|
336
355
|
emitter.emit({
|
|
337
356
|
node_kind: 'pure',
|
|
338
357
|
node_name: 'dedupe_and_rank',
|
|
@@ -354,6 +373,7 @@ async function runArcheologist(opts) {
|
|
|
354
373
|
outputs_summary: `signals=${gapSignals.map(s => s.kind).join(',')}`,
|
|
355
374
|
},
|
|
356
375
|
});
|
|
376
|
+
progress(`◐ gap_analysis — ${gapSignals.length} signal(s): ${gapSignals.map(s => s.kind).join(',')}`);
|
|
357
377
|
const gapStart = Date.now();
|
|
358
378
|
const gap = await (0, gap_analysis_1.runGapAnalysis)({
|
|
359
379
|
meshDir: opts.meshDir,
|
|
@@ -365,6 +385,7 @@ async function runArcheologist(opts) {
|
|
|
365
385
|
githubToken,
|
|
366
386
|
fetchImpl: opts.fetchImpl,
|
|
367
387
|
});
|
|
388
|
+
progress(`✓ gap_analysis (${gap.llm.provider} ${gap.llm.model}) in ${Date.now() - gapStart}ms — ${gap.llm.inputTokens} in / ${gap.llm.outputTokens} out tokens → ${gap.followUpQueries.length} follow-up queries`);
|
|
368
389
|
totalInputTokens += gap.llm.inputTokens;
|
|
369
390
|
totalOutputTokens += gap.llm.outputTokens;
|
|
370
391
|
totalCostUsd += gap.llm.costUsd;
|
|
@@ -436,6 +457,7 @@ async function runArcheologist(opts) {
|
|
|
436
457
|
}
|
|
437
458
|
} // end research-path else branch
|
|
438
459
|
// ----- synthesize_report (LLM) -----
|
|
460
|
+
progress(`◐ synthesize_report — calling LLM (provider hint=${brief.llm_provider ?? 'anthropic'}, sources=${rankedSources.length}); hybrid routing will pick anthropic for synth if anthropic key is set…`);
|
|
439
461
|
const synthStart = Date.now();
|
|
440
462
|
const synthesis = await (0, synthesize_report_1.synthesizeReport)({
|
|
441
463
|
meshDir: opts.meshDir,
|
|
@@ -454,6 +476,7 @@ async function runArcheologist(opts) {
|
|
|
454
476
|
totalInputTokens += synthesis.llm.inputTokens;
|
|
455
477
|
totalOutputTokens += synthesis.llm.outputTokens;
|
|
456
478
|
totalCostUsd += synthesis.llm.costUsd;
|
|
479
|
+
progress(`✓ synthesize_report (${synthesis.llm.provider} ${synthesis.llm.model}) in ${Date.now() - synthStart}ms — ${synthesis.llm.inputTokens} in / ${synthesis.llm.outputTokens} out tokens, ${synthesis.llm.attempts} attempt${synthesis.llm.attempts !== 1 ? 's' : ''}`);
|
|
457
480
|
emitter.emit({
|
|
458
481
|
node_kind: 'llm',
|
|
459
482
|
node_name: 'synthesize_report',
|
|
@@ -554,6 +577,8 @@ async function runArcheologist(opts) {
|
|
|
554
577
|
}
|
|
555
578
|
catch { /* leave on disk — non-fatal, just a tmpdir entry */ }
|
|
556
579
|
}
|
|
580
|
+
const totalDurationMs = Date.now() - startedAt.getTime();
|
|
581
|
+
progress(`◆ done ${runId} in ${(totalDurationMs / 1000).toFixed(1)}s — ${totalInputTokens} in / ${totalOutputTokens} out tokens, $${roundUsd(totalCostUsd)} | sources=${rankedSources.length} conclusions=${synthesis.citation_stats.conclusion_count} recs=${synthesis.citation_stats.recommendation_count} | artifact=${artifactPath}`);
|
|
557
582
|
return {
|
|
558
583
|
run_id: runId,
|
|
559
584
|
topic: brief.topic,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maintainabilityai/research-runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Research + PRD agent runner — orchestrates the Archeologist and PRD pipelines for the MaintainabilityAI governance mesh",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "MaintainabilityAI",
|