@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 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, 6e5),
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-600s).
2641
- * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var. */
2642
- LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 6e5, default: 3e5 }),
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 { AuthStorage, ModelRegistry, getAgentDir } from "@earendil-works/pi-coding-agent";
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
- const authStorage = AuthStorage.inMemory({
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
- return ModelRegistry.inMemory(AuthStorage.inMemory());
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(true, container);
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(true, container);
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(true, container);
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 (likely blocked or slow startup): "${query}"`);
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
- if (r.title) {
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 MAX_FAILED_RESEARCHERS = 2;
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 >= MAX_FAILED_RESEARCHERS;
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
- if (initialLinks.length === 0 && historicalUrls.length === 0) {
20175
- logger.warn(`[ResearcherExecutor] Researcher ${id} has no initial search results or historical links; skipping.`);
20176
- recordResearcherFailure(sessionId, researchId, id, "no initial search results or historical links (search produced nothing usable)");
20177
- metrics.increment("researcher_skipped_total", 1, { mode: "deep", complexity: String(complexity), reason: "no_initial_links" });
20178
- observer?.onResearcherFailure?.(id, "No initial search results or historical links available");
20179
- return;
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 (initialLinks.length > 0) {
20201
+ if (effectiveInitialLinks.length > 0) {
20183
20202
  evidenceSection = `## Evidence Provided
20184
20203
  Initial search results provided the following URLs to investigate:
20185
- ${initialLinks.map((l) => `- ${l}`).join("\n")}`;
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 new Error(getResearchStopMessage(sessionId, researchId));
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
- const stopMessage = getResearchStopMessage(sessionId, researchId);
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 (globalCitations.length === 0) {
21911
+ if (reports.size > 0) {
21893
21912
  const scraped = getScrapedLinks(sessionId);
21894
21913
  if (scraped.length > 0) {
21895
- globalCitations = scraped.map((url, i) => ({
21896
- id: i + 1,
21897
- url,
21898
- description: "",
21899
- source: isTranscribedLink(sessionId, url) ? "YouTube Transcript" : "Scrape"
21900
- }));
21901
- logger.warn(
21902
- `[ResearchSynthesisService] No citations parsed from reports; rebuilt ${scraped.length} source(s) from scrape/transcript provenance.`
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
- toStdout(pretty({ ok: true, depth, report, ...reportPath ? { reportPath } : {} }));
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 isConfigError = 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");
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
  };