@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/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;
|
|
@@ -269,7 +286,7 @@ async function _doInit(options: ResearchSDKOptions = {}): Promise<void> {
|
|
|
269
286
|
}
|
|
270
287
|
|
|
271
288
|
// Build and cache the registry (one instance for the lifetime of this init cycle).
|
|
272
|
-
globalRegistry = sharedBuildModelRegistry(globalApiKey, parsedProvider);
|
|
289
|
+
globalRegistry = await sharedBuildModelRegistry(globalApiKey, parsedProvider);
|
|
273
290
|
|
|
274
291
|
try {
|
|
275
292
|
// Resolve the model. Precedence: explicit option (a Model object is used
|
|
@@ -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
|
|
@@ -5,31 +5,21 @@
|
|
|
5
5
|
* Both files use identical ResourceLoader implementations.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
8
|
+
import { createExtensionRuntime, type ResourceLoader } from '@earendil-works/pi-coding-agent';
|
|
9
9
|
|
|
10
10
|
export function makeResourceLoader(systemPromptText: string): ResourceLoader {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
getAllTools: () => [],
|
|
24
|
-
setActiveTools: () => {},
|
|
25
|
-
refreshTools: () => {},
|
|
26
|
-
getCommands: () => [],
|
|
27
|
-
setModel: async () => true,
|
|
28
|
-
getThinkingLevel: () => 'off',
|
|
29
|
-
setThinkingLevel: () => {},
|
|
30
|
-
assertActive: () => {},
|
|
31
|
-
invalidate: () => {},
|
|
32
|
-
};
|
|
11
|
+
// Use pi's own createExtensionRuntime() instead of hand-maintaining a mock.
|
|
12
|
+
//
|
|
13
|
+
// A literal mock object drifted out of sync with the ExtensionRuntime contract:
|
|
14
|
+
// pi 0.81.0 ("Full provider extensions") added `pendingNativeProviderRegistrations`
|
|
15
|
+
// + `registerNativeProvider`, which this mock omitted. Under pi >= 0.81.0 the
|
|
16
|
+
// host's runner.bindCore() / createAgentSessionServices() iterate that field —
|
|
17
|
+
// `for (... of runtime.pendingNativeProviderRegistrations)` — and an `undefined`
|
|
18
|
+
// field threw `this.runtime.pendingNativeProviderRegistrations is not iterable`,
|
|
19
|
+
// aborting EVERY researcher session ("Research produced no report — no source
|
|
20
|
+
// material was collected"). createExtensionRuntime() always returns a runtime
|
|
21
|
+
// that satisfies the current host's full contract, so this can't drift again.
|
|
22
|
+
const mockRuntime = createExtensionRuntime();
|
|
33
23
|
|
|
34
24
|
return {
|
|
35
25
|
getExtensions: () => ({ extensions: [], errors: [], runtime: mockRuntime }),
|
|
@@ -61,10 +61,22 @@ export async function performSearch(
|
|
|
61
61
|
if (onProgress) onProgress(0);
|
|
62
62
|
|
|
63
63
|
// Hard cap per query so a single Cloudflare block or hung browser worker
|
|
64
|
-
// cannot stall the entire Promise.all burst.
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
64
|
+
// cannot stall the entire Promise.all burst.
|
|
65
|
+
//
|
|
66
|
+
// The budget is the worker's nav budget (SEARCH_TIMEOUT_MS) PLUS the
|
|
67
|
+
// queue-wait/overhead margin (BROWSER_TASK_TIMEOUT_MS), mirroring the
|
|
68
|
+
// scheduler's own task-timeout ceiling in BrowserTaskScheduler.runSearch
|
|
69
|
+
// (= SEARCH_TIMEOUT_MS + BROWSER_TASK_TIMEOUT_MS). This clock starts at
|
|
70
|
+
// ENQUEUE (before runWorkerSearch queues onto the shared browser pool), so
|
|
71
|
+
// a budget of only SEARCH_TIMEOUT_MS would let a query that sat in the
|
|
72
|
+
// saturated worker queue burn its ENTIRE nav budget waiting for a slot and
|
|
73
|
+
// abort with zero actual search work — exactly the "Query timed out after
|
|
74
|
+
// 45000ms (likely blocked or slow startup)" cascade observed under
|
|
75
|
+
// concurrent load. Including the margin lets a queued query still use its
|
|
76
|
+
// full nav budget once a worker picks it up, and keeps this layer's hard
|
|
77
|
+
// cap coherent with the scheduler's so it never preempts the worker's own
|
|
78
|
+
// (post-dequeue) deadline.
|
|
79
|
+
const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45_000) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 10_000);
|
|
68
80
|
|
|
69
81
|
const filteredQueries = queries.filter(q => q.trim());
|
|
70
82
|
let timeoutCount = 0;
|
|
@@ -133,7 +145,7 @@ export async function performSearch(
|
|
|
133
145
|
metrics.increment('browser_search_queries_total', 1, { status });
|
|
134
146
|
|
|
135
147
|
if (isTimeout) {
|
|
136
|
-
logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms
|
|
148
|
+
logger.warn(`[Search] Query timed out after ${QUERY_TIMEOUT_MS}ms — likely starved waiting for a free browser worker under concurrent load, or a slow/blocked search provider: "${query}"`);
|
|
137
149
|
} else {
|
|
138
150
|
const msg = error instanceof Error ? error.message : String(error);
|
|
139
151
|
if (msg !== 'Aborted') {
|
package/src/youtube/index.ts
CHANGED
|
@@ -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
|
-
|
|
131
|
-
|
|
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
|
|