@lincoln504/pi-research 1.0.5 → 1.0.8

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/src/sdk.ts CHANGED
@@ -22,11 +22,12 @@ import { getService, resetServiceContainer, getServiceContainer } from './core/s
22
22
  import type { ServiceContainer } from './core/service-registry.ts';
23
23
  import { ServiceNames } from './core/service-interfaces.ts';
24
24
  import type { IKnowledgeStoreService } from './core/interfaces/knowledge-interfaces.ts';
25
- import type {
26
- IResearchOrchestration,
25
+ import type {
26
+ IResearchOrchestration,
27
27
  IResearchSynthesisService,
28
- ResearchOptions
28
+ ResearchOptions
29
29
  } from './core/interfaces/orchestration-interfaces.ts';
30
+ import type { IPlanningService } from './core/interfaces/planning-interfaces.ts';
30
31
  import type { Model } from '@earendil-works/pi-ai';
31
32
  import { type ExtensionContext, ModelRegistry } from '@earendil-works/pi-coding-agent';
32
33
  import { logger, createLogger, setLogger } from './logger.ts';
@@ -78,6 +79,22 @@ let _lastRunSummary: RunSummary | null = null;
78
79
  // run end when errors occurred, and consumers can read the full report after the run.
79
80
  let _lastErrorReport: ErrorReport | null = null;
80
81
 
82
+ /**
83
+ * Researcher-outcome summary for the most recent runDeepResearch call: how many
84
+ * researchers were planned/launched vs. how many ultimately failed (never landed
85
+ * a report) vs. succeeded, plus the first recorded failure reason per failed
86
+ * researcher. Lets a caller distinguish "thin report, little material out there"
87
+ * from "thin report because most researchers died" without opening the log file.
88
+ */
89
+ export interface ResearcherOutcome {
90
+ planned: number;
91
+ launched: number;
92
+ succeeded: number;
93
+ failed: number;
94
+ failureReasons: Record<string, string>;
95
+ }
96
+ let _lastResearcherOutcome: ResearcherOutcome | null = null;
97
+
81
98
  // Signal handler state — registered once, removed on clean shutdown.
82
99
  let _onSignal: ((signal: string) => void) | null = null;
83
100
  let _sigintHandler: (() => void) | null = null;
@@ -512,6 +529,7 @@ export async function runDeepResearch(
512
529
  metrics.recordRunSummary(_lastRunSummary);
513
530
  _lastErrorReport = runTracker.getReport();
514
531
  logRunErrorSummary(_lastErrorReport, depthLabel, 'success');
532
+ _lastResearcherOutcome = await captureResearcherOutcome(sessionId, researchId, _lastRunSummary.snapshot);
515
533
  return result;
516
534
  } catch (err) {
517
535
  const completedAt = Date.now();
@@ -524,6 +542,7 @@ export async function runDeepResearch(
524
542
  metrics.recordRunSummary(_lastRunSummary);
525
543
  _lastErrorReport = runTracker.getReport();
526
544
  logRunErrorSummary(_lastErrorReport, depthLabel, 'error');
545
+ _lastResearcherOutcome = await captureResearcherOutcome(sessionId, researchId, _lastRunSummary.snapshot);
527
546
  throw err;
528
547
  } finally {
529
548
  // Free the per-run PiSessionState that shouldStopResearch()/getFailedResearchers()
@@ -564,6 +583,14 @@ export function getLastErrorReport(): ErrorReport | null {
564
583
  return _lastErrorReport;
565
584
  }
566
585
 
586
+ /**
587
+ * Researcher-outcome summary (planned/launched/succeeded/failed + failure reasons)
588
+ * for the most recent run. Null until the first run completes; cleared on shutdown.
589
+ */
590
+ export function getLastResearcherOutcome(): ResearcherOutcome | null {
591
+ return _lastResearcherOutcome;
592
+ }
593
+
567
594
  /**
568
595
  * Emit a single compact line summarizing a run's tracked errors, so they are visible in
569
596
  * the operator's log instead of silently accumulating. No-op when the run was clean.
@@ -576,6 +603,31 @@ function logRunErrorSummary(report: ErrorReport | null, depthLabel: string, stat
576
603
  logger.warn(`[SDK] run ${status} with ${report.totalErrors} tracked error(s) across ${report.uniquePatterns} pattern(s) [${depthLabel}]${top ? ` — top: ${top}` : ''}`);
577
604
  }
578
605
 
606
+ /**
607
+ * Capture how many researchers were planned/launched/failed for this run, BEFORE
608
+ * the caller's `finally` block frees the PiSessionState (endResearchSession) that
609
+ * getFailedResearchers()/getResearcherFailureReasons() read from. Best-effort:
610
+ * this is instrumentation only and must never affect the run's own result.
611
+ */
612
+ async function captureResearcherOutcome(sessionId: string, researchId: string, snapshot: IMetricsSnapshot): Promise<ResearcherOutcome | null> {
613
+ try {
614
+ const planningService = await getService<IPlanningService>(ServiceNames.PLANNING, undefined, globalContainer!);
615
+ const planned = planningService.getTotalResearchersPlanned(researchId);
616
+ const launched = extractRunStats(snapshot)?.researchersLaunched ?? 0;
617
+ const failed = getFailedResearchers(sessionId, researchId);
618
+ return {
619
+ planned,
620
+ launched,
621
+ succeeded: Math.max(0, launched - failed.length),
622
+ failed: failed.length,
623
+ failureReasons: getResearcherFailureReasons(sessionId, researchId),
624
+ };
625
+ } catch (err) {
626
+ logger.debug('[SDK] Failed to capture researcher outcome (non-fatal):', err);
627
+ return null;
628
+ }
629
+ }
630
+
579
631
  /**
580
632
  * Distilled, human-relevant stats from the most recent run (researchers launched,
581
633
  * searches, URLs analyzed/failed, tokens, cost, per-tool usage). Null if no run
@@ -744,7 +796,7 @@ export async function searchKnowledge(
744
796
  };
745
797
  }
746
798
 
747
- import { clearAllSessionState, endResearchSession } from './orchestration/session-state.ts';
799
+ import { clearAllSessionState, endResearchSession, getFailedResearchers, getResearcherFailureReasons } from './orchestration/session-state.ts';
748
800
  import { shutdownManager } from './utils/shutdown-manager.ts';
749
801
 
750
802
  /**
@@ -833,6 +885,7 @@ async function _doShutdown(): Promise<void> {
833
885
  _lastResearchId = null;
834
886
  _lastRunSummary = null;
835
887
  _lastErrorReport = null;
888
+ _lastResearcherOutcome = null;
836
889
  // The module-level config cache survives the globals above. Without this, a
837
890
  // shutdown → env/config-file change → re-init cycle in one process silently
838
891
  // reuses the first init's config. (The CLI/skill are unaffected — they pass
@@ -126,10 +126,11 @@ function formatResults(results: VideoTranscript[], rejected: string[]): string {
126
126
  // citation pipeline (parseCitations extracts URLs), so the transcript's
127
127
  // provenance would silently drop out of the final report's CITED LINKS.
128
128
  // The title/channel follows so the report names the real video, not a
129
- // paraphrase (the watch URL alone is not human-identifiable).
130
- if (r.title) {
131
- md += `**Cite as:** ${r.url} — '${r.title}'${r.author ? ` by ${r.author}` : ''}\n`;
132
- }
129
+ // paraphrase (the watch URL alone is not human-identifiable). Emit this line
130
+ // unconditionally — an untitled video still needs a parseable "Cite as" the
131
+ // model can copy verbatim; omitting the line entirely for untitled videos
132
+ // left them without any prompt-visible citation string to reuse.
133
+ md += `**Cite as:** ${r.url}${r.title ? ` — '${r.title}'${r.author ? ` by ${r.author}` : ''}` : ''}\n`;
133
134
  md += `\n${r.text}\n\n---\n\n`;
134
135
  }
135
136