@lincoln504/pi-research 1.0.6 → 1.0.9
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/cli.mjs +250 -104
- package/dist/prompts/research-tool-usage.md +1 -0
- package/dist/thread-worker.mjs +9 -3
- package/docs/ARCHITECTURE.md +19 -4
- package/docs/CHANGELOG.md +129 -0
- package/docs/CONFIGURATION.md +2 -1
- package/docs/SDK.md +4 -1
- package/package.json +18 -16
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cli.ts +62 -13
- package/src/config.ts +12 -4
- package/src/core/llm/model-registry-factory.ts +77 -25
- package/src/index.ts +11 -5
- package/src/infrastructure/browser/task-execution-service.ts +15 -3
- package/src/infrastructure/browser/thread-worker.mjs +9 -3
- package/src/orchestration/research-orchestration-service.ts +6 -6
- package/src/orchestration/research-synthesis-service.ts +31 -20
- package/src/orchestration/researcher-executor.ts +51 -13
- package/src/orchestration/researcher.ts +12 -1
- package/src/orchestration/session-state.ts +27 -4
- package/src/prompts/research-tool-usage.md +1 -0
- package/src/sdk.ts +58 -5
- package/src/utils/make-resource-loader.ts +13 -23
- package/src/web-research/browser-search.ts +17 -5
- package/src/youtube/index.ts +5 -4
package/dist/cli.mjs
CHANGED
|
@@ -2295,6 +2295,7 @@ function saveConfig(config, scope = "local", cwd = process.cwd(), changedKeys) {
|
|
|
2295
2295
|
PI_RESEARCH_MAX_RESEARCHERS: String(config.MAX_CONCURRENT_RESEARCHERS),
|
|
2296
2296
|
PI_RESEARCH_MAX_RETRIES: String(config.RESEARCHER_MAX_RETRIES),
|
|
2297
2297
|
PI_RESEARCH_RETRY_DELAY_MS: String(config.RESEARCHER_MAX_RETRY_DELAY_MS),
|
|
2298
|
+
PI_RESEARCH_MAX_FAILED_RESEARCHERS: String(config.MAX_FAILED_RESEARCHERS ?? DEFAULTS.MAX_FAILED_RESEARCHERS),
|
|
2298
2299
|
PI_RESEARCH_HEALTH_CHECK_TIMEOUT_MS: String(config.HEALTH_CHECK_TIMEOUT_MS ?? DEFAULTS.HEALTH_CHECK_TIMEOUT_MS),
|
|
2299
2300
|
PI_RESEARCH_SEARCH_TIMEOUT_MS: String(config.SEARCH_TIMEOUT_MS),
|
|
2300
2301
|
PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS: String(config.TUI_REFRESH_DEBOUNCE_MS),
|
|
@@ -2457,6 +2458,7 @@ function createConfig(env, processEnv) {
|
|
|
2457
2458
|
MAX_CONCURRENT_RESEARCHERS: parseEnvNumber(e, "PI_RESEARCH_MAX_RESEARCHERS", DEFAULTS.MAX_CONCURRENT_RESEARCHERS, 1, 5, true),
|
|
2458
2459
|
RESEARCHER_MAX_RETRIES: parseEnvNumber(e, "PI_RESEARCH_MAX_RETRIES", DEFAULTS.RESEARCHER_MAX_RETRIES, 0, 5, true),
|
|
2459
2460
|
RESEARCHER_MAX_RETRY_DELAY_MS: parseEnvNumber(e, "PI_RESEARCH_RETRY_DELAY_MS", DEFAULTS.RESEARCHER_MAX_RETRY_DELAY_MS, 100, 1e4),
|
|
2461
|
+
MAX_FAILED_RESEARCHERS: parseEnvNumber(e, "PI_RESEARCH_MAX_FAILED_RESEARCHERS", DEFAULTS.MAX_FAILED_RESEARCHERS, 1, 10, true),
|
|
2460
2462
|
DEFAULT_RESEARCH_DEPTH: parseEnvNumber(e, "PI_RESEARCH_DEFAULT_RESEARCH_DEPTH", DEFAULTS.DEFAULT_RESEARCH_DEPTH, 1, 3, true),
|
|
2461
2463
|
MAX_SCRAPE_BATCHES: parseEnvNumber(e, "PI_RESEARCH_MAX_SCRAPE_BATCHES", DEFAULTS.MAX_SCRAPE_BATCHES, 0, 99, true),
|
|
2462
2464
|
MAX_GATHERING_CALLS: parseEnvNumber(e, "PI_RESEARCH_MAX_GATHERING_CALLS", DEFAULTS.MAX_GATHERING_CALLS, 1, 100, true),
|
|
@@ -2483,7 +2485,7 @@ function createConfig(env, processEnv) {
|
|
|
2483
2485
|
SEARCH_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_SEARCH_TIMEOUT_MS", DEFAULTS.SEARCH_TIMEOUT_MS, 5e3, 12e4),
|
|
2484
2486
|
TUI_REFRESH_DEBOUNCE_MS: parseEnvNumber(e, "PI_RESEARCH_TUI_REFRESH_DEBOUNCE_MS", DEFAULTS.TUI_REFRESH_DEBOUNCE_MS, 0, 1e3),
|
|
2485
2487
|
BROWSER_TASK_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_BROWSER_TASK_TIMEOUT_MS", DEFAULTS.BROWSER_TASK_TIMEOUT_MS, 2e3, 12e4),
|
|
2486
|
-
LLM_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_LLM_TIMEOUT_MS", DEFAULTS.LLM_TIMEOUT_MS, 6e4,
|
|
2488
|
+
LLM_TIMEOUT_MS: parseEnvNumber(e, "PI_RESEARCH_LLM_TIMEOUT_MS", DEFAULTS.LLM_TIMEOUT_MS, 6e4, 18e5),
|
|
2487
2489
|
LLM_THINKING_LEVEL: parseEnvEnum(e, "PI_RESEARCH_LLM_THINKING_LEVEL", ["off", "minimal", "low", "medium", "high"], DEFAULTS.LLM_THINKING_LEVEL),
|
|
2488
2490
|
PLANNING_MAX_TOKENS: parseEnvNumber(e, "PI_RESEARCH_PLANNING_MAX_TOKENS", DEFAULTS.PLANNING_MAX_TOKENS, 1024, 131072),
|
|
2489
2491
|
SYNTHESIS_MAX_TOKENS: parseEnvNumber(e, "PI_RESEARCH_SYNTHESIS_MAX_TOKENS", DEFAULTS.SYNTHESIS_MAX_TOKENS, 1024, 131072),
|
|
@@ -2580,6 +2582,8 @@ var init_config = __esm({
|
|
|
2580
2582
|
RESEARCHER_MAX_RETRIES: Type.Number({ minimum: 0, maximum: 5, default: 2 }),
|
|
2581
2583
|
/** Base delay between retries in milliseconds (default: 2000, range: 100-10000) */
|
|
2582
2584
|
RESEARCHER_MAX_RETRY_DELAY_MS: Type.Number({ minimum: 100, maximum: 1e4, default: 2e3 }),
|
|
2585
|
+
/** Unique researcher failures that abort the entire run (default: 2, range: 1-10) */
|
|
2586
|
+
MAX_FAILED_RESEARCHERS: Type.Number({ minimum: 1, maximum: 10, default: 2 }),
|
|
2583
2587
|
/** Target depth for recursive research (default: 1, range: 1-3) */
|
|
2584
2588
|
DEFAULT_RESEARCH_DEPTH: Type.Number({ minimum: 1, maximum: 3, default: 1 }),
|
|
2585
2589
|
/** Number of batches to allow for a single scrape tool call (default: 2, 0=unlimited) */
|
|
@@ -2637,9 +2641,13 @@ var init_config = __esm({
|
|
|
2637
2641
|
* never killed before it has used its full nav budget plus time waiting in the queue.
|
|
2638
2642
|
* (default: 10000) */
|
|
2639
2643
|
BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
|
|
2640
|
-
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-
|
|
2641
|
-
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
2642
|
-
|
|
2644
|
+
/** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
|
|
2645
|
+
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
2646
|
+
* Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
|
|
2647
|
+
* large-context synthesis/plan call can legitimately run >10min, and the old
|
|
2648
|
+
* 600000 ceiling silently clamped an explicit PI_RESEARCH_LLM_TIMEOUT_MS
|
|
2649
|
+
* (e.g. 900000), causing those calls to time out mid-generation. */
|
|
2650
|
+
LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 18e5, default: 3e5 }),
|
|
2643
2651
|
/** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
|
|
2644
2652
|
* evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
|
|
2645
2653
|
* sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
|
|
@@ -3197,7 +3205,8 @@ var init_agentic_repair = __esm({
|
|
|
3197
3205
|
});
|
|
3198
3206
|
|
|
3199
3207
|
// src/core/llm/model-registry-factory.ts
|
|
3200
|
-
import {
|
|
3208
|
+
import { ModelRegistry, ModelRuntime, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3209
|
+
import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
|
|
3201
3210
|
import * as path6 from "node:path";
|
|
3202
3211
|
import * as fs5 from "node:fs";
|
|
3203
3212
|
function safeGetAll(registry) {
|
|
@@ -3239,27 +3248,27 @@ function buildHeaders(provider) {
|
|
|
3239
3248
|
if (provider === "openrouter") return { "HTTP-Referer": "https://pi.ai", "X-Title": "pi-research" };
|
|
3240
3249
|
return {};
|
|
3241
3250
|
}
|
|
3242
|
-
function buildModelRegistry(apiKey, provider) {
|
|
3251
|
+
async function buildModelRegistry(apiKey, provider) {
|
|
3243
3252
|
const agentDir = getAgentDir();
|
|
3244
3253
|
const modelsJsonPath = path6.join(agentDir, "models.json");
|
|
3245
3254
|
const authPath = path6.join(agentDir, "auth.json");
|
|
3255
|
+
const hasModels = fs5.existsSync(modelsJsonPath);
|
|
3256
|
+
const hasAuth = fs5.existsSync(authPath);
|
|
3257
|
+
const runtime = await ModelRuntime.create({
|
|
3258
|
+
authPath: hasAuth ? authPath : void 0,
|
|
3259
|
+
credentials: hasAuth ? void 0 : new InMemoryCredentialStore(),
|
|
3260
|
+
modelsPath: hasModels ? modelsJsonPath : null,
|
|
3261
|
+
allowModelNetwork: false
|
|
3262
|
+
});
|
|
3246
3263
|
if (apiKey && provider) {
|
|
3247
|
-
|
|
3248
|
-
[provider]: { type: "api_key", key: apiKey }
|
|
3249
|
-
});
|
|
3250
|
-
return ModelRegistry.create(
|
|
3251
|
-
authStorage,
|
|
3252
|
-
fs5.existsSync(modelsJsonPath) ? modelsJsonPath : void 0
|
|
3253
|
-
);
|
|
3254
|
-
}
|
|
3255
|
-
if (fs5.existsSync(authPath) || fs5.existsSync(modelsJsonPath)) {
|
|
3256
|
-
const authStorage = AuthStorage.create(authPath);
|
|
3257
|
-
return ModelRegistry.create(
|
|
3258
|
-
authStorage,
|
|
3259
|
-
fs5.existsSync(modelsJsonPath) ? modelsJsonPath : void 0
|
|
3260
|
-
);
|
|
3264
|
+
await runtime.setRuntimeApiKey(provider, apiKey, { allowNetwork: false });
|
|
3261
3265
|
}
|
|
3262
|
-
|
|
3266
|
+
const registry = new ModelRegistry(runtime);
|
|
3267
|
+
registryRuntimes.set(registry, runtime);
|
|
3268
|
+
return registry;
|
|
3269
|
+
}
|
|
3270
|
+
function getRuntimeForRegistry(registry) {
|
|
3271
|
+
return registry instanceof Object ? registryRuntimes.get(registry) : void 0;
|
|
3263
3272
|
}
|
|
3264
3273
|
function constructMinimalModel(provider, modelId, _apiKey) {
|
|
3265
3274
|
return {
|
|
@@ -3351,10 +3360,12 @@ function pickPreferredAvailable(available, providerOrder) {
|
|
|
3351
3360
|
}
|
|
3352
3361
|
return available[0];
|
|
3353
3362
|
}
|
|
3363
|
+
var registryRuntimes;
|
|
3354
3364
|
var init_model_registry_factory = __esm({
|
|
3355
3365
|
"src/core/llm/model-registry-factory.ts"() {
|
|
3356
3366
|
"use strict";
|
|
3357
3367
|
init_logger();
|
|
3368
|
+
registryRuntimes = /* @__PURE__ */ new WeakMap();
|
|
3358
3369
|
}
|
|
3359
3370
|
});
|
|
3360
3371
|
|
|
@@ -14650,7 +14661,7 @@ async function runBrowserTask(taskOrUrl, type = "scrape", config, signal, retrie
|
|
|
14650
14661
|
if (now - lastRestartTime > RESTART_COOLDOWN_MS) {
|
|
14651
14662
|
lastRestartTime = now;
|
|
14652
14663
|
logger.warn(`[BrowserManager] Forcing scheduler restart and retrying...`);
|
|
14653
|
-
await forceSchedulerRestart2(
|
|
14664
|
+
await forceSchedulerRestart2(false, container);
|
|
14654
14665
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
14655
14666
|
} else {
|
|
14656
14667
|
logger.warn(`[BrowserManager] Scheduler restart recently triggered, waiting for pool idle...`);
|
|
@@ -14685,7 +14696,7 @@ async function runBrowserHealthCheck(config, retries = 1, signal, container = ge
|
|
|
14685
14696
|
if (now - lastRestartTime > RESTART_COOLDOWN_MS) {
|
|
14686
14697
|
lastRestartTime = now;
|
|
14687
14698
|
logger.warn(`[BrowserManager] Forcing scheduler restart and retrying...`);
|
|
14688
|
-
await forceSchedulerRestart2(
|
|
14699
|
+
await forceSchedulerRestart2(false, container);
|
|
14689
14700
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
14690
14701
|
} else {
|
|
14691
14702
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
@@ -14724,7 +14735,7 @@ async function runWorkerSearch(query, config, signal, retries = 1, sessionId, co
|
|
|
14724
14735
|
if (now - lastRestartTime > RESTART_COOLDOWN_MS) {
|
|
14725
14736
|
lastRestartTime = now;
|
|
14726
14737
|
logger.warn(`[BrowserManager] Forcing scheduler restart and retrying...`);
|
|
14727
|
-
await forceSchedulerRestart2(
|
|
14738
|
+
await forceSchedulerRestart2(false, container);
|
|
14728
14739
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
14729
14740
|
} else {
|
|
14730
14741
|
await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
|
|
@@ -14766,7 +14777,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
|
|
|
14766
14777
|
metrics.increment("browser_search_orchestrations_total", 1);
|
|
14767
14778
|
logger.log(`[Search] Orchestrating ${queries.length} queries across ${maxWorkers} worker processes...`);
|
|
14768
14779
|
if (onProgress) onProgress(0);
|
|
14769
|
-
const QUERY_TIMEOUT_MS = config?.SEARCH_TIMEOUT_MS ?? 45e3;
|
|
14780
|
+
const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45e3) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 1e4);
|
|
14770
14781
|
const filteredQueries = queries.filter((q) => q.trim());
|
|
14771
14782
|
let timeoutCount = 0;
|
|
14772
14783
|
let errorCount = 0;
|
|
@@ -14811,7 +14822,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
|
|
|
14811
14822
|
metrics.observe("browser_search_query_duration_ms", queryDuration, { status });
|
|
14812
14823
|
metrics.increment("browser_search_queries_total", 1, { status });
|
|
14813
14824
|
if (isTimeout) {
|
|
14814
|
-
logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms
|
|
14825
|
+
logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms \u2014 likely starved waiting for a free browser worker under concurrent load, or a slow/blocked search provider: "${query}"`);
|
|
14815
14826
|
} else {
|
|
14816
14827
|
const msg2 = error instanceof Error ? error.message : String(error);
|
|
14817
14828
|
if (msg2 !== "Aborted") {
|
|
@@ -19445,10 +19456,8 @@ function formatResults(results, rejected) {
|
|
|
19445
19456
|
if (typeof r.charCount === "number") meta.push(`${r.charCount.toLocaleString()} chars`);
|
|
19446
19457
|
if (meta.length) md += `**${meta.join(" \xB7 ")}**
|
|
19447
19458
|
`;
|
|
19448
|
-
|
|
19449
|
-
md += `**Cite as:** ${r.url} \u2014 '${r.title}'${r.author ? ` by ${r.author}` : ""}
|
|
19459
|
+
md += `**Cite as:** ${r.url}${r.title ? ` \u2014 '${r.title}'${r.author ? ` by ${r.author}` : ""}` : ""}
|
|
19450
19460
|
`;
|
|
19451
|
-
}
|
|
19452
19461
|
md += `
|
|
19453
19462
|
${r.text}
|
|
19454
19463
|
|
|
@@ -19633,40 +19642,9 @@ function createResearchTools(options) {
|
|
|
19633
19642
|
}
|
|
19634
19643
|
|
|
19635
19644
|
// src/utils/make-resource-loader.ts
|
|
19645
|
+
import { createExtensionRuntime } from "@earendil-works/pi-coding-agent";
|
|
19636
19646
|
function makeResourceLoader(systemPromptText) {
|
|
19637
|
-
const mockRuntime =
|
|
19638
|
-
flagValues: /* @__PURE__ */ new Map(),
|
|
19639
|
-
pendingProviderRegistrations: [],
|
|
19640
|
-
registerProvider: () => {
|
|
19641
|
-
},
|
|
19642
|
-
unregisterProvider: () => {
|
|
19643
|
-
},
|
|
19644
|
-
sendMessage: async () => {
|
|
19645
|
-
},
|
|
19646
|
-
sendUserMessage: async () => {
|
|
19647
|
-
},
|
|
19648
|
-
appendEntry: async () => {
|
|
19649
|
-
},
|
|
19650
|
-
setSessionName: () => void 0,
|
|
19651
|
-
getSessionName: () => void 0,
|
|
19652
|
-
setLabel: async () => {
|
|
19653
|
-
},
|
|
19654
|
-
getActiveTools: () => [],
|
|
19655
|
-
getAllTools: () => [],
|
|
19656
|
-
setActiveTools: () => {
|
|
19657
|
-
},
|
|
19658
|
-
refreshTools: () => {
|
|
19659
|
-
},
|
|
19660
|
-
getCommands: () => [],
|
|
19661
|
-
setModel: async () => true,
|
|
19662
|
-
getThinkingLevel: () => "off",
|
|
19663
|
-
setThinkingLevel: () => {
|
|
19664
|
-
},
|
|
19665
|
-
assertActive: () => {
|
|
19666
|
-
},
|
|
19667
|
-
invalidate: () => {
|
|
19668
|
-
}
|
|
19669
|
-
};
|
|
19647
|
+
const mockRuntime = createExtensionRuntime();
|
|
19670
19648
|
return {
|
|
19671
19649
|
getExtensions: () => ({ extensions: [], errors: [], runtime: mockRuntime }),
|
|
19672
19650
|
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
@@ -19820,6 +19798,7 @@ function createDefaultToolLimits(config) {
|
|
|
19820
19798
|
// src/orchestration/researcher.ts
|
|
19821
19799
|
init_logger();
|
|
19822
19800
|
init_research_model_resolver();
|
|
19801
|
+
init_model_registry_factory();
|
|
19823
19802
|
async function createResearcherSession(options) {
|
|
19824
19803
|
const {
|
|
19825
19804
|
cwd,
|
|
@@ -19887,7 +19866,17 @@ async function createResearcherSession(options) {
|
|
|
19887
19866
|
// Each researcher gets its own isolated session
|
|
19888
19867
|
settingsManager: researcherSettings,
|
|
19889
19868
|
model: modelToUse,
|
|
19890
|
-
modelRegistry
|
|
19869
|
+
// pi 0.80.8 replaced the SDK's `modelRegistry`/`authStorage` session options
|
|
19870
|
+
// with the async `modelRuntime`. Pass the runtime backing OUR registry when
|
|
19871
|
+
// this run built one (standalone CLI/SDK): it carries any explicit API key
|
|
19872
|
+
// seeded via setRuntimeApiKey, which the session's own default runtime
|
|
19873
|
+
// (auto-built from ~/.pi/agent/{auth,models}.json) would never see — without
|
|
19874
|
+
// this, standalone PI_RESEARCH_API_KEY users fail auth on every researcher
|
|
19875
|
+
// LLM call despite passing pre-flight. For registries we did NOT build
|
|
19876
|
+
// (the pi host's ExtensionContext.modelRegistry), this resolves to
|
|
19877
|
+
// undefined and the host auto-builds from its own agent dir — correct
|
|
19878
|
+
// in-host, where auth lives in the host's config.
|
|
19879
|
+
modelRuntime: getRuntimeForRegistry(modelRegistry),
|
|
19891
19880
|
resourceLoader: makeResourceLoader(systemPrompt),
|
|
19892
19881
|
// Thinking/reasoning is disabled for researcher agents: they do retrieval and
|
|
19893
19882
|
// synthesis from scraped pages, not open-ended reasoning. Keeping it off reduces
|
|
@@ -20108,10 +20097,10 @@ function getFailedResearchers(piSessionId, researchId) {
|
|
|
20108
20097
|
const failures = state.failures.get(researchId) || [];
|
|
20109
20098
|
return [...new Set(failures)];
|
|
20110
20099
|
}
|
|
20111
|
-
var
|
|
20112
|
-
function shouldStopResearch(piSessionId, researchId) {
|
|
20100
|
+
var DEFAULT_MAX_FAILED_RESEARCHERS = 2;
|
|
20101
|
+
function shouldStopResearch(piSessionId, researchId, maxFailedResearchers = DEFAULT_MAX_FAILED_RESEARCHERS) {
|
|
20113
20102
|
const sid = normalizeSessionId(piSessionId);
|
|
20114
|
-
return getFailedResearchers(sid, researchId).length >=
|
|
20103
|
+
return getFailedResearchers(sid, researchId).length >= maxFailedResearchers;
|
|
20115
20104
|
}
|
|
20116
20105
|
function getResearchStopMessage(piSessionId, researchId) {
|
|
20117
20106
|
const sid = normalizeSessionId(piSessionId);
|
|
@@ -20137,6 +20126,12 @@ function getResearchStopMessage(piSessionId, researchId) {
|
|
|
20137
20126
|
"Partial results may be available below."
|
|
20138
20127
|
].join("\n");
|
|
20139
20128
|
}
|
|
20129
|
+
var RESEARCH_STOPPED_ERROR_CODE = "RESEARCH_STOPPED";
|
|
20130
|
+
function createResearchStopError(piSessionId, researchId) {
|
|
20131
|
+
const err = new Error(getResearchStopMessage(piSessionId, researchId));
|
|
20132
|
+
err.code = RESEARCH_STOPPED_ERROR_CODE;
|
|
20133
|
+
return err;
|
|
20134
|
+
}
|
|
20140
20135
|
|
|
20141
20136
|
// src/orchestration/researcher-executor.ts
|
|
20142
20137
|
async function runResearcher(options) {
|
|
@@ -20171,18 +20166,42 @@ ${currentPlan.allQueries.map((q) => `- ${q}`).join("\n")}
|
|
|
20171
20166
|
Previous summary: ${e.description}`).join("\n");
|
|
20172
20167
|
}
|
|
20173
20168
|
const researcherPromptTemplate = loadPrompt("researcher");
|
|
20174
|
-
|
|
20175
|
-
|
|
20176
|
-
|
|
20177
|
-
|
|
20178
|
-
|
|
20179
|
-
|
|
20169
|
+
let effectiveInitialLinks = initialLinks;
|
|
20170
|
+
if (effectiveInitialLinks.length === 0 && historicalUrls.length === 0) {
|
|
20171
|
+
const retryQueries = researcherConfig.queries?.length ? researcherConfig.queries : [researcherConfig.goal];
|
|
20172
|
+
const maxLinkAttempts = config.RESEARCHER_MAX_RETRIES + 1;
|
|
20173
|
+
for (let linkAttempt = 1; linkAttempt <= maxLinkAttempts && effectiveInitialLinks.length === 0; linkAttempt++) {
|
|
20174
|
+
if (linkAttempt > 1) {
|
|
20175
|
+
if (signal?.aborted || container?.isDisposing) break;
|
|
20176
|
+
const delay2 = Math.min(1e3 * Math.pow(2, linkAttempt - 2), config.RESEARCHER_MAX_RETRY_DELAY_MS);
|
|
20177
|
+
logger.warn(`[ResearcherExecutor] Researcher ${id} has no initial search results; retry ${linkAttempt - 1}/${config.RESEARCHER_MAX_RETRIES} after ${delay2}ms`);
|
|
20178
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
20179
|
+
}
|
|
20180
|
+
if (signal?.aborted || container?.isDisposing) break;
|
|
20181
|
+
try {
|
|
20182
|
+
const queryResults = await search(retryQueries, config, signal, void 0, container);
|
|
20183
|
+
const urls = /* @__PURE__ */ new Set();
|
|
20184
|
+
for (const qr of queryResults) {
|
|
20185
|
+
for (const r of qr.results || []) urls.add(r.url);
|
|
20186
|
+
}
|
|
20187
|
+
effectiveInitialLinks = [...urls];
|
|
20188
|
+
} catch (err) {
|
|
20189
|
+
logger.debug(`[ResearcherExecutor] Researcher ${id} link-retry search burst failed:`, err);
|
|
20190
|
+
}
|
|
20191
|
+
}
|
|
20192
|
+
if (effectiveInitialLinks.length === 0) {
|
|
20193
|
+
logger.warn(`[ResearcherExecutor] Researcher ${id} has no initial search results or historical links after ${maxLinkAttempts} attempt(s); skipping.`);
|
|
20194
|
+
recordResearcherFailure(sessionId, researchId, id, "no initial search results or historical links (search produced nothing usable)");
|
|
20195
|
+
metrics.increment("researcher_skipped_total", 1, { mode: "deep", complexity: String(complexity), reason: "no_initial_links" });
|
|
20196
|
+
observer?.onResearcherFailure?.(id, "No initial search results or historical links available");
|
|
20197
|
+
return;
|
|
20198
|
+
}
|
|
20180
20199
|
}
|
|
20181
20200
|
let evidenceSection = "";
|
|
20182
|
-
if (
|
|
20201
|
+
if (effectiveInitialLinks.length > 0) {
|
|
20183
20202
|
evidenceSection = `## Evidence Provided
|
|
20184
20203
|
Initial search results provided the following URLs to investigate:
|
|
20185
|
-
${
|
|
20204
|
+
${effectiveInitialLinks.map((l) => `- ${l}`).join("\n")}`;
|
|
20186
20205
|
}
|
|
20187
20206
|
const maxAttempts = config.RESEARCHER_MAX_RETRIES + 1;
|
|
20188
20207
|
let lastError;
|
|
@@ -21414,12 +21433,13 @@ var ResearchOrchestrationService = class {
|
|
|
21414
21433
|
const researchers = plan.researchers || [];
|
|
21415
21434
|
const active = /* @__PURE__ */ new Set();
|
|
21416
21435
|
const maxConcurrent = orchestratorOptions.config?.MAX_CONCURRENT_RESEARCHERS ?? DEFAULTS.MAX_CONCURRENT_RESEARCHERS;
|
|
21436
|
+
const maxFailedResearchers = orchestratorOptions.config?.MAX_FAILED_RESEARCHERS ?? DEFAULTS.MAX_FAILED_RESEARCHERS;
|
|
21417
21437
|
for (const configItem of researchers) {
|
|
21418
21438
|
if (signal?.aborted) break;
|
|
21419
|
-
if (shouldStopResearch(sessionId, researchId)) {
|
|
21439
|
+
if (shouldStopResearch(sessionId, researchId, maxFailedResearchers)) {
|
|
21420
21440
|
const sessionService = await getService(ServiceNames.RESEARCH_SESSION_SERVICE, ctx, container);
|
|
21421
21441
|
await sessionService.abortAllSessions(researchId);
|
|
21422
|
-
throw
|
|
21442
|
+
throw createResearchStopError(sessionId, researchId);
|
|
21423
21443
|
}
|
|
21424
21444
|
if (active.size >= maxConcurrent) {
|
|
21425
21445
|
if (signal?.aborted) break;
|
|
@@ -21488,11 +21508,10 @@ var ResearchOrchestrationService = class {
|
|
|
21488
21508
|
});
|
|
21489
21509
|
if (signal?.aborted) break;
|
|
21490
21510
|
}
|
|
21491
|
-
if (shouldStopResearch(sessionId, researchId)) {
|
|
21511
|
+
if (shouldStopResearch(sessionId, researchId, maxFailedResearchers)) {
|
|
21492
21512
|
const sessionService = await getService(ServiceNames.RESEARCH_SESSION_SERVICE, ctx, container);
|
|
21493
21513
|
await sessionService.abortAllSessions(researchId);
|
|
21494
|
-
|
|
21495
|
-
throw new Error(stopMessage);
|
|
21514
|
+
throw createResearchStopError(sessionId, researchId);
|
|
21496
21515
|
}
|
|
21497
21516
|
}
|
|
21498
21517
|
await Promise.all(active);
|
|
@@ -21889,18 +21908,26 @@ Research performed using ${modelId}`;
|
|
|
21889
21908
|
ensureCitedLinks(sessionId, synthesis) {
|
|
21890
21909
|
const reports = this.getSessionReports(sessionId);
|
|
21891
21910
|
let globalCitations = reports.size > 0 ? normalizeCitations(reports).globalCitations : [];
|
|
21892
|
-
if (
|
|
21911
|
+
if (reports.size > 0) {
|
|
21893
21912
|
const scraped = getScrapedLinks(sessionId);
|
|
21894
21913
|
if (scraped.length > 0) {
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
21898
|
-
|
|
21899
|
-
|
|
21900
|
-
|
|
21901
|
-
|
|
21902
|
-
|
|
21903
|
-
|
|
21914
|
+
const known = new Set(globalCitations.map((c) => normalizeUrl(c.url)));
|
|
21915
|
+
const missing = scraped.filter((url) => !known.has(normalizeUrl(url)));
|
|
21916
|
+
if (missing.length > 0) {
|
|
21917
|
+
const startId = globalCitations.reduce((max, c) => Math.max(max, c.id), 0) + 1;
|
|
21918
|
+
globalCitations = [
|
|
21919
|
+
...globalCitations,
|
|
21920
|
+
...missing.map((url, i) => ({
|
|
21921
|
+
id: startId + i,
|
|
21922
|
+
url,
|
|
21923
|
+
description: "",
|
|
21924
|
+
source: isTranscribedLink(sessionId, url) ? "YouTube Transcript" : "Scrape"
|
|
21925
|
+
}))
|
|
21926
|
+
];
|
|
21927
|
+
logger.warn(
|
|
21928
|
+
`[ResearchSynthesisService] Folded in ${missing.length} source(s) from scrape/transcript provenance not covered by parsed citations.`
|
|
21929
|
+
);
|
|
21930
|
+
}
|
|
21904
21931
|
}
|
|
21905
21932
|
}
|
|
21906
21933
|
if (globalCitations.length > 0) {
|
|
@@ -22114,6 +22141,79 @@ init_metrics();
|
|
|
22114
22141
|
|
|
22115
22142
|
// src/utils/metrics-summary.ts
|
|
22116
22143
|
init_text_utils();
|
|
22144
|
+
function sumCounter(counters, baseName) {
|
|
22145
|
+
let total = 0;
|
|
22146
|
+
for (const [key, value] of Object.entries(counters)) {
|
|
22147
|
+
if (key === baseName || key.startsWith(baseName + "{")) {
|
|
22148
|
+
total += value;
|
|
22149
|
+
}
|
|
22150
|
+
}
|
|
22151
|
+
return total;
|
|
22152
|
+
}
|
|
22153
|
+
function getLabeledCounter(counters, baseName, labels) {
|
|
22154
|
+
const labelParts = Object.entries(labels).map(([k, v]) => `${k}="${v}"`);
|
|
22155
|
+
const suffix = `{${labelParts.join(",")}}`;
|
|
22156
|
+
for (const [key, value] of Object.entries(counters)) {
|
|
22157
|
+
if (key === `${baseName}${suffix}`) {
|
|
22158
|
+
return value;
|
|
22159
|
+
}
|
|
22160
|
+
}
|
|
22161
|
+
return 0;
|
|
22162
|
+
}
|
|
22163
|
+
function extractRunStats(snapshot) {
|
|
22164
|
+
const counters = snapshot.counters || {};
|
|
22165
|
+
const histograms = snapshot.histograms || {};
|
|
22166
|
+
const researchersLaunched = sumCounter(counters, "researchers_launched_total");
|
|
22167
|
+
const browserSearchQueries = sumCounter(counters, "browser_search_queries_total");
|
|
22168
|
+
const searchQueries = sumCounter(counters, "search_queries_total") + browserSearchQueries;
|
|
22169
|
+
const urlsDiscovered = sumCounter(counters, "browser_search_results_total") + sumCounter(counters, "browser_search_unique_urls_total");
|
|
22170
|
+
const fetchSuccess = getLabeledCounter(counters, "scrape_results_total", { outcome: "fetch_success" });
|
|
22171
|
+
const browserSuccess = getLabeledCounter(counters, "scrape_results_total", { outcome: "browser_success" });
|
|
22172
|
+
const totalFailure = getLabeledCounter(counters, "scrape_results_total", { outcome: "total_failure" });
|
|
22173
|
+
const browserFallbacks = sumCounter(counters, "scrape_layer_fallbacks_total");
|
|
22174
|
+
const urlsAnalyzed = fetchSuccess + browserSuccess;
|
|
22175
|
+
const urlsFailed = totalFailure;
|
|
22176
|
+
const toolUsage = {
|
|
22177
|
+
searches: sumCounter(counters, "tool_search_calls_total"),
|
|
22178
|
+
scrapes: sumCounter(counters, "tool_scrape_calls_total"),
|
|
22179
|
+
securitySearches: sumCounter(counters, "tool_security_search_calls_total"),
|
|
22180
|
+
stackexchangeQueries: sumCounter(counters, "stackexchange_requests_total"),
|
|
22181
|
+
knowledgeLookups: sumCounter(counters, "research_knowledge_search_total")
|
|
22182
|
+
};
|
|
22183
|
+
const errors = sumCounter(counters, "researcher_errors_total") + sumCounter(counters, "llm_api_errors_total");
|
|
22184
|
+
const tokens = sumCounter(counters, "llm_tokens_total");
|
|
22185
|
+
const cost = sumCounter(counters, "llm_cost_total");
|
|
22186
|
+
const durationMs = histograms['research_session_duration_ms{mode="deep",complexity="1",status="success"}']?.max || histograms['research_session_duration_ms{mode="deep",complexity="2",status="success"}']?.max || histograms['research_session_duration_ms{mode="deep",complexity="3",status="success"}']?.max || histograms['research_session_duration_ms{mode="quick",complexity="0",status="success"}']?.max || 0;
|
|
22187
|
+
const roundsCompleted = sumCounter(counters, "evaluator_runs_total");
|
|
22188
|
+
let complexity = 1;
|
|
22189
|
+
for (const key of Object.keys(counters)) {
|
|
22190
|
+
if (key.startsWith("research_sessions_total{")) {
|
|
22191
|
+
const match = key.match(/complexity="(\d+)"/);
|
|
22192
|
+
if (match?.[1]) complexity = parseInt(match[1], 10);
|
|
22193
|
+
break;
|
|
22194
|
+
}
|
|
22195
|
+
}
|
|
22196
|
+
if (researchersLaunched === 0 && searchQueries === 0 && urlsAnalyzed === 0 && tokens === 0) {
|
|
22197
|
+
return null;
|
|
22198
|
+
}
|
|
22199
|
+
return {
|
|
22200
|
+
durationMs,
|
|
22201
|
+
complexity,
|
|
22202
|
+
researchersLaunched,
|
|
22203
|
+
roundsCompleted,
|
|
22204
|
+
searchQueries,
|
|
22205
|
+
urlsDiscovered,
|
|
22206
|
+
fetchSuccess,
|
|
22207
|
+
browserSuccess,
|
|
22208
|
+
browserFallbacks,
|
|
22209
|
+
urlsAnalyzed,
|
|
22210
|
+
urlsFailed,
|
|
22211
|
+
errors,
|
|
22212
|
+
tokens,
|
|
22213
|
+
cost,
|
|
22214
|
+
toolUsage
|
|
22215
|
+
};
|
|
22216
|
+
}
|
|
22117
22217
|
|
|
22118
22218
|
// src/sdk.ts
|
|
22119
22219
|
init_error_tracker();
|
|
@@ -22135,6 +22235,7 @@ var _isRunning = false;
|
|
|
22135
22235
|
var _lastResearchId = null;
|
|
22136
22236
|
var _lastRunSummary = null;
|
|
22137
22237
|
var _lastErrorReport = null;
|
|
22238
|
+
var _lastResearcherOutcome = null;
|
|
22138
22239
|
var _onSignal = null;
|
|
22139
22240
|
var _sigintHandler = null;
|
|
22140
22241
|
var _sigtermHandler = null;
|
|
@@ -22218,7 +22319,7 @@ async function _doInit(options = {}) {
|
|
|
22218
22319
|
if (globalApiKey && !parsedProvider) {
|
|
22219
22320
|
throw new Error("Provider must be specified when using an explicit API key (set provider option or PI_RESEARCH_PROVIDER).");
|
|
22220
22321
|
}
|
|
22221
|
-
globalRegistry = buildModelRegistry(globalApiKey, parsedProvider);
|
|
22322
|
+
globalRegistry = await buildModelRegistry(globalApiKey, parsedProvider);
|
|
22222
22323
|
try {
|
|
22223
22324
|
if (effectiveModel && typeof effectiveModel === "object" && effectiveModel.id) {
|
|
22224
22325
|
globalModel = effectiveModel;
|
|
@@ -22321,6 +22422,7 @@ async function runDeepResearch(query, options = {}, signal) {
|
|
|
22321
22422
|
metrics.recordRunSummary(_lastRunSummary);
|
|
22322
22423
|
_lastErrorReport = runTracker.getReport();
|
|
22323
22424
|
logRunErrorSummary(_lastErrorReport, depthLabel, "success");
|
|
22425
|
+
_lastResearcherOutcome = await captureResearcherOutcome(sessionId, researchId, _lastRunSummary.snapshot);
|
|
22324
22426
|
return result;
|
|
22325
22427
|
} catch (err) {
|
|
22326
22428
|
const completedAt = Date.now();
|
|
@@ -22336,6 +22438,7 @@ async function runDeepResearch(query, options = {}, signal) {
|
|
|
22336
22438
|
metrics.recordRunSummary(_lastRunSummary);
|
|
22337
22439
|
_lastErrorReport = runTracker.getReport();
|
|
22338
22440
|
logRunErrorSummary(_lastErrorReport, depthLabel, "error");
|
|
22441
|
+
_lastResearcherOutcome = await captureResearcherOutcome(sessionId, researchId, _lastRunSummary.snapshot);
|
|
22339
22442
|
throw err;
|
|
22340
22443
|
} finally {
|
|
22341
22444
|
try {
|
|
@@ -22345,11 +22448,35 @@ async function runDeepResearch(query, options = {}, signal) {
|
|
|
22345
22448
|
_isRunning = false;
|
|
22346
22449
|
}
|
|
22347
22450
|
}
|
|
22451
|
+
function getLastErrorReport() {
|
|
22452
|
+
return _lastErrorReport;
|
|
22453
|
+
}
|
|
22454
|
+
function getLastResearcherOutcome() {
|
|
22455
|
+
return _lastResearcherOutcome;
|
|
22456
|
+
}
|
|
22348
22457
|
function logRunErrorSummary(report, depthLabel, status) {
|
|
22349
22458
|
if (!report || report.totalErrors === 0) return;
|
|
22350
22459
|
const top = report.patterns.slice(0, 3).map((p) => `"${String(p.message ?? p.signature ?? "error").slice(0, 48)}" \xD7${p.count}`).join(", ");
|
|
22351
22460
|
logger.warn(`[SDK] run ${status} with ${report.totalErrors} tracked error(s) across ${report.uniquePatterns} pattern(s) [${depthLabel}]${top ? ` \u2014 top: ${top}` : ""}`);
|
|
22352
22461
|
}
|
|
22462
|
+
async function captureResearcherOutcome(sessionId, researchId, snapshot) {
|
|
22463
|
+
try {
|
|
22464
|
+
const planningService = await getService(ServiceNames.PLANNING, void 0, globalContainer);
|
|
22465
|
+
const planned = planningService.getTotalResearchersPlanned(researchId);
|
|
22466
|
+
const launched = extractRunStats(snapshot)?.researchersLaunched ?? 0;
|
|
22467
|
+
const failed = getFailedResearchers(sessionId, researchId);
|
|
22468
|
+
return {
|
|
22469
|
+
planned,
|
|
22470
|
+
launched,
|
|
22471
|
+
succeeded: Math.max(0, launched - failed.length),
|
|
22472
|
+
failed: failed.length,
|
|
22473
|
+
failureReasons: getResearcherFailureReasons(sessionId, researchId)
|
|
22474
|
+
};
|
|
22475
|
+
} catch (err) {
|
|
22476
|
+
logger.debug("[SDK] Failed to capture researcher outcome (non-fatal):", err);
|
|
22477
|
+
return null;
|
|
22478
|
+
}
|
|
22479
|
+
}
|
|
22353
22480
|
async function searchKnowledge(queries, signal) {
|
|
22354
22481
|
if (!isInitialized || !globalContainer) {
|
|
22355
22482
|
throw new Error("SDK not initialized. Call initResearchSDK() first.");
|
|
@@ -22433,6 +22560,7 @@ async function _doShutdown() {
|
|
|
22433
22560
|
_lastResearchId = null;
|
|
22434
22561
|
_lastRunSummary = null;
|
|
22435
22562
|
_lastErrorReport = null;
|
|
22563
|
+
_lastResearcherOutcome = null;
|
|
22436
22564
|
resetConfig();
|
|
22437
22565
|
_removeSignalHandlers();
|
|
22438
22566
|
if (errors.length > 0) {
|
|
@@ -22799,16 +22927,16 @@ function fileExists(p) {
|
|
|
22799
22927
|
return false;
|
|
22800
22928
|
}
|
|
22801
22929
|
}
|
|
22802
|
-
function piKeysAvailable(piAuthPresent, provider) {
|
|
22930
|
+
async function piKeysAvailable(piAuthPresent, provider) {
|
|
22803
22931
|
try {
|
|
22804
|
-
const available = safeGetAvailable(buildModelRegistry(void 0, void 0));
|
|
22932
|
+
const available = safeGetAvailable(await buildModelRegistry(void 0, void 0));
|
|
22805
22933
|
if (!provider) return available.length > 0;
|
|
22806
22934
|
return available.some((m) => m.provider === provider);
|
|
22807
22935
|
} catch {
|
|
22808
22936
|
return piAuthPresent;
|
|
22809
22937
|
}
|
|
22810
22938
|
}
|
|
22811
|
-
function detectCredentials(explicitModel) {
|
|
22939
|
+
async function detectCredentials(explicitModel) {
|
|
22812
22940
|
const paths = resolvedConfigPaths();
|
|
22813
22941
|
const piAuthPresent = fileExists(paths.piAuth);
|
|
22814
22942
|
const piModelsPresent = fileExists(paths.piModels);
|
|
@@ -22819,7 +22947,7 @@ function detectCredentials(explicitModel) {
|
|
|
22819
22947
|
const model = explicitModel ?? modelEnv ?? modelConfig;
|
|
22820
22948
|
const modelFrom = explicitModel ? "--model" : modelEnv ? "PI_RESEARCH_MODEL" : modelConfig ? "config.env" : "unset";
|
|
22821
22949
|
const provider = providerEnv ?? (model && model.includes("/") ? model.slice(0, model.indexOf("/")) : void 0);
|
|
22822
|
-
const piKeysUsable = piKeysAvailable(piAuthPresent, provider);
|
|
22950
|
+
const piKeysUsable = await piKeysAvailable(piAuthPresent, provider);
|
|
22823
22951
|
if (apiKey) {
|
|
22824
22952
|
if (!provider) {
|
|
22825
22953
|
return {
|
|
@@ -22911,7 +23039,7 @@ function configBlock(det, extraNote) {
|
|
|
22911
23039
|
return lines.join("\n");
|
|
22912
23040
|
}
|
|
22913
23041
|
async function cmdResearch(args) {
|
|
22914
|
-
const det = detectCredentials(args.model);
|
|
23042
|
+
const det = await detectCredentials(args.model);
|
|
22915
23043
|
if (det.problem) {
|
|
22916
23044
|
if (args.json) toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
|
|
22917
23045
|
else toStderr(`
|
|
@@ -22938,6 +23066,8 @@ Error: ${vmsg}
|
|
|
22938
23066
|
`);
|
|
22939
23067
|
const observer = makeProgressObserver();
|
|
22940
23068
|
let exit = EXIT.OK;
|
|
23069
|
+
const abortController = new AbortController();
|
|
23070
|
+
activeResearchAbortController = abortController;
|
|
22941
23071
|
try {
|
|
22942
23072
|
await initResearchSDK({
|
|
22943
23073
|
model: runModel,
|
|
@@ -22954,7 +23084,7 @@ Error: ${vmsg}
|
|
|
22954
23084
|
observer,
|
|
22955
23085
|
...args.excludeTools ? { excludeTools: args.excludeTools } : {},
|
|
22956
23086
|
...args.initialLinks ? { initialLinks: args.initialLinks } : {}
|
|
22957
|
-
});
|
|
23087
|
+
}, abortController.signal);
|
|
22958
23088
|
let reportPath = null;
|
|
22959
23089
|
const exportCfg = getConfig(process.cwd(), "cli");
|
|
22960
23090
|
if (exportCfg.RESEARCH_REPORT_EXPORT_ENABLED) {
|
|
@@ -22966,19 +23096,31 @@ Error: ${vmsg}
|
|
|
22966
23096
|
}
|
|
22967
23097
|
}
|
|
22968
23098
|
if (args.json) {
|
|
22969
|
-
|
|
23099
|
+
const outcome = getLastResearcherOutcome();
|
|
23100
|
+
const errorReport = getLastErrorReport();
|
|
23101
|
+
toStdout(pretty({
|
|
23102
|
+
ok: true,
|
|
23103
|
+
depth,
|
|
23104
|
+
report,
|
|
23105
|
+
...reportPath ? { reportPath } : {},
|
|
23106
|
+
...outcome ? { researchers: { planned: outcome.planned, launched: outcome.launched, succeeded: outcome.succeeded, failed: outcome.failed } } : {},
|
|
23107
|
+
...errorReport && errorReport.totalErrors > 0 ? {
|
|
23108
|
+
trackedErrors: { total: errorReport.totalErrors, uniquePatterns: errorReport.uniquePatterns }
|
|
23109
|
+
} : {}
|
|
23110
|
+
}));
|
|
22970
23111
|
} else {
|
|
22971
23112
|
toStdout(report.endsWith("\n") ? report : report + "\n");
|
|
22972
23113
|
}
|
|
22973
23114
|
} catch (err) {
|
|
22974
|
-
exit = reportError(err, "research", args.json);
|
|
23115
|
+
exit = await reportError(err, "research", args.json);
|
|
22975
23116
|
} finally {
|
|
23117
|
+
if (activeResearchAbortController === abortController) activeResearchAbortController = null;
|
|
22976
23118
|
await safeShutdown();
|
|
22977
23119
|
}
|
|
22978
23120
|
return exit;
|
|
22979
23121
|
}
|
|
22980
23122
|
async function cmdKnowledge(queries, json) {
|
|
22981
|
-
const det = detectCredentials();
|
|
23123
|
+
const det = await detectCredentials();
|
|
22982
23124
|
if (det.problem) {
|
|
22983
23125
|
if (json) {
|
|
22984
23126
|
toStdout(pretty({ ok: false, error: det.problem, exitCode: EXIT.CONFIG }));
|
|
@@ -23032,7 +23174,7 @@ Configure in:
|
|
|
23032
23174
|
return exit;
|
|
23033
23175
|
}
|
|
23034
23176
|
async function cmdStatus(json) {
|
|
23035
|
-
const det = detectCredentials();
|
|
23177
|
+
const det = await detectCredentials();
|
|
23036
23178
|
const paths = resolvedConfigPaths();
|
|
23037
23179
|
const cfg = getConfig(process.cwd(), "cli");
|
|
23038
23180
|
const summary = {
|
|
@@ -23246,10 +23388,11 @@ function describeUninstall(r) {
|
|
|
23246
23388
|
return r.status;
|
|
23247
23389
|
}
|
|
23248
23390
|
}
|
|
23249
|
-
function reportError(err, what, json) {
|
|
23391
|
+
async function reportError(err, what, json) {
|
|
23250
23392
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
23251
23393
|
const lower = msg2.toLowerCase();
|
|
23252
|
-
const
|
|
23394
|
+
const isTaggedResearchStop = err?.code === RESEARCH_STOPPED_ERROR_CODE;
|
|
23395
|
+
const isConfigError = !isTaggedResearchStop && (lower.includes("no llm model available") || lower.includes("not found in pi") || lower.includes("no api key") || lower.includes("provider must be specified") || lower.includes("invalid model string") || lower.includes("authentication") || lower.includes("unauthorized") || lower.includes("api key"));
|
|
23253
23396
|
const isRateLimit = lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests");
|
|
23254
23397
|
const exitCode = isConfigError && !isRateLimit ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
23255
23398
|
if (json) {
|
|
@@ -23271,7 +23414,7 @@ Error: pi-research ${what} halted: provider rate limit reached. Wait a moment an
|
|
|
23271
23414
|
toStderr(`
|
|
23272
23415
|
Error: pi-research ${what} failed: ${msg2}
|
|
23273
23416
|
|
|
23274
|
-
${configBlock(detectCredentials())}
|
|
23417
|
+
${configBlock(await detectCredentials())}
|
|
23275
23418
|
`);
|
|
23276
23419
|
return exitCode;
|
|
23277
23420
|
}
|
|
@@ -23557,6 +23700,7 @@ var flushAndExit = (code) => {
|
|
|
23557
23700
|
});
|
|
23558
23701
|
});
|
|
23559
23702
|
};
|
|
23703
|
+
var activeResearchAbortController = null;
|
|
23560
23704
|
async function main(argv) {
|
|
23561
23705
|
let _signalCleanupDone = false;
|
|
23562
23706
|
const onSignal = (sig) => {
|
|
@@ -23565,6 +23709,7 @@ async function main(argv) {
|
|
|
23565
23709
|
toStderr(`
|
|
23566
23710
|
[pi-research] ${sig} \u2014 cleaning up\u2026
|
|
23567
23711
|
`);
|
|
23712
|
+
activeResearchAbortController?.abort();
|
|
23568
23713
|
void safeShutdown().finally(() => flushAndExit(EXIT.SOFTWARE));
|
|
23569
23714
|
};
|
|
23570
23715
|
process.once("SIGINT", () => onSignal("SIGINT"));
|
|
@@ -23634,5 +23779,6 @@ export {
|
|
|
23634
23779
|
EXIT,
|
|
23635
23780
|
UsageError,
|
|
23636
23781
|
bridgeConfigEnv,
|
|
23637
|
-
parseArgs
|
|
23782
|
+
parseArgs,
|
|
23783
|
+
reportError
|
|
23638
23784
|
};
|