@lincoln504/pi-research 1.3.12 → 1.4.3
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/README.md +2 -2
- package/dist/cli.mjs +5670 -4705
- package/dist/prompts/research-tool-usage.md +1 -1
- package/dist/prompts/researcher.md +1 -0
- package/dist/prompts/system-lead-router.md +122 -0
- package/dist/prompts/system-lead-synthesizer.md +46 -0
- package/dist/thread-worker.mjs +169 -62
- package/docs/ARCHITECTURE.md +18 -10
- package/docs/CHANGELOG.md +225 -0
- package/docs/CONFIGURATION.md +106 -7
- package/package.json +2 -2
- package/scripts/build.cjs +8 -1
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cli.ts +5 -1
- package/src/config.ts +22 -12
- package/src/constants.ts +1 -1
- package/src/core/interfaces/observer-interfaces.ts +3 -3
- package/src/core/interfaces/orchestration-interfaces.ts +2 -0
- package/src/core/interfaces/planning-interfaces.ts +7 -1
- package/src/core/interfaces/scheduler-interfaces.ts +18 -2
- package/src/core/llm/agentic-repair.ts +7 -2
- package/src/core/llm/inject-date.ts +4 -1
- package/src/core/llm/llm-utils.ts +7 -0
- package/src/core/planning-service.ts +766 -91
- package/src/core/planning-utils.ts +21 -5
- package/src/core/service-initialization.ts +19 -3
- package/src/healthcheck/index.ts +11 -2
- package/src/index.ts +4 -3
- package/src/infrastructure/browser/browser-client.ts +68 -34
- package/src/infrastructure/browser/browser-task-scheduler.ts +236 -43
- package/src/infrastructure/browser/config.ts +79 -1
- package/src/infrastructure/browser/priority-task-queue.ts +55 -1
- package/src/infrastructure/browser/scheduler-factory.ts +12 -1
- package/src/infrastructure/browser/task-execution-service.ts +5 -5
- package/src/infrastructure/browser/thread-worker-browser.ts +87 -36
- package/src/infrastructure/browser/thread-worker-messaging.ts +99 -10
- package/src/infrastructure/browser/thread-worker.mjs +169 -62
- package/src/infrastructure/browser/thread-worker.ts +13 -5
- package/src/infrastructure/browser/worker-pool-manager.ts +127 -12
- package/src/infrastructure/embedding/embedding-client.ts +14 -4
- package/src/infrastructure/embedding/embedding-factory.ts +83 -15
- package/src/infrastructure/embedding/embedding-server.ts +34 -0
- package/src/infrastructure/file-lock-service.ts +214 -19
- package/src/infrastructure/knowledge-store-service.ts +14 -0
- package/src/infrastructure/research-run-semaphore.ts +1 -1
- package/src/infrastructure/service-initialization.ts +7 -0
- package/src/knowledge/embedder-init.ts +46 -19
- package/src/knowledge/embedder.ts +11 -3
- package/src/knowledge/index.ts +36 -10
- package/src/knowledge/store.ts +29 -5
- package/src/logger.ts +11 -0
- package/src/orchestration/deep-research-orchestrator.ts +143 -87
- package/src/orchestration/quick-research-orchestrator.ts +11 -2
- package/src/orchestration/research-orchestration-service.ts +8 -0
- package/src/orchestration/research-synthesis-service.ts +53 -1
- package/src/orchestration/researcher-executor.ts +53 -2
- package/src/orchestration/researcher.ts +15 -2
- package/src/orchestration/session-state.ts +16 -0
- package/src/prompts/research-tool-usage.md +1 -1
- package/src/prompts/researcher.md +1 -0
- package/src/prompts/system-lead-router.md +122 -0
- package/src/prompts/system-lead-synthesizer.md +46 -0
- package/src/sdk.ts +10 -0
- package/src/skill-install/skill-installer.ts +5 -1
- package/src/stackexchange/index.ts +3 -0
- package/src/stackexchange/rest-client.ts +7 -12
- package/src/tools/research-knowledge-search.ts +11 -3
- package/src/tools/research-tool-definition.ts +38 -7
- package/src/tools/scrape.ts +6 -0
- package/src/tools/search.ts +7 -0
- package/src/tools/security.ts +31 -10
- package/src/tools/stackexchange.ts +13 -7
- package/src/tools/youtube-transcript.ts +26 -12
- package/src/utils/cancellation.ts +36 -0
- package/src/utils/coverage-digest.ts +204 -0
- package/src/utils/host-config.ts +12 -1
- package/src/utils/llm-usage.ts +33 -6
- package/src/utils/log-utils.ts +10 -0
- package/src/utils/metrics-summary.ts +20 -4
- package/src/utils/metrics.ts +27 -1
- package/src/web-research/browser-search.ts +102 -22
- package/src/web-research/web-scraper.ts +27 -16
- package/src/youtube/potoken.ts +8 -1
- package/src/youtube/transcript-client.ts +56 -5
- package/dist/prompts/system-lead-evaluator.md +0 -118
- package/src/prompts/system-lead-evaluator.md +0 -118
package/src/config.ts
CHANGED
|
@@ -82,13 +82,19 @@ export const ConfigSchema = Type.Object({
|
|
|
82
82
|
SEARCH_TIMEOUT_MS: Type.Number({ minimum: 5000, maximum: 120000, default: 45000 }),
|
|
83
83
|
/** TUI refresh debounce in milliseconds (default: 100ms) */
|
|
84
84
|
TUI_REFRESH_DEBOUNCE_MS: Type.Number({ minimum: 0, maximum: 1000, default: 100 }),
|
|
85
|
-
/**
|
|
86
|
-
*
|
|
87
|
-
* SEARCH_TIMEOUT_MS + this, a scrape after SCRAPE_TIMEOUT_MS + this
|
|
88
|
-
*
|
|
85
|
+
/** Overhead margin (ms) added on top of each browser operation's own nav timeout to
|
|
86
|
+
* form its scheduler task-timeout ceiling: a search task times out after
|
|
87
|
+
* SEARCH_TIMEOUT_MS + this, a scrape after SCRAPE_TIMEOUT_MS + this — covering
|
|
88
|
+
* per-task work beyond raw navigation (page setup, result parsing). Queue wait is
|
|
89
|
+
* NOT part of this margin: the deadline is armed on dispatch, not on enqueue, so
|
|
90
|
+
* time spent waiting for a free worker never counts against it (see
|
|
91
|
+
* browser-task-scheduler.ts). Cold browser-launch/context-creation time is also
|
|
92
|
+
* separate from this margin — COLD_START_ALLOWANCE_MS
|
|
93
|
+
* (infrastructure/browser/config.ts) covers that unconditionally on top of it,
|
|
94
|
+
* since it isn't something a user should need to tune.
|
|
89
95
|
* (default: 10000) */
|
|
90
96
|
BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2000, maximum: 120000, default: 10000 }),
|
|
91
|
-
/** Timeout for coordinator/
|
|
97
|
+
/** Timeout for coordinator/research-lead/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
|
|
92
98
|
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
93
99
|
* Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
|
|
94
100
|
* large-context synthesis/plan call can legitimately run >10min, and the old
|
|
@@ -96,7 +102,7 @@ export const ConfigSchema = Type.Object({
|
|
|
96
102
|
* (e.g. 900000), causing those calls to time out mid-generation. */
|
|
97
103
|
LLM_TIMEOUT_MS: Type.Number({ minimum: 60000, maximum: 1800000, default: 300000 }),
|
|
98
104
|
/** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
|
|
99
|
-
*
|
|
105
|
+
* router, synthesizer, JSON-repair, knowledge extraction) AND the researcher
|
|
100
106
|
* sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
|
|
101
107
|
* open-ended reasoning, so thinking only burns the output-token budget (often
|
|
102
108
|
* truncating before the JSON/report block) and adds latency for at most a marginal
|
|
@@ -111,22 +117,26 @@ export const ConfigSchema = Type.Object({
|
|
|
111
117
|
Type.Literal('medium'),
|
|
112
118
|
Type.Literal('high'),
|
|
113
119
|
], { default: 'off' }),
|
|
114
|
-
/** Max output tokens for the coordinator plan
|
|
115
|
-
*
|
|
120
|
+
/** Max output tokens for the coordinator's round-1 plan. Default 16384. The routing
|
|
121
|
+
* decision has its OWN, much smaller ceiling (it emits a decision, never a report,
|
|
122
|
+
* and cannot synthesize mid-round under the split protocol); the final report uses
|
|
123
|
+
* SYNTHESIS_MAX_TOKENS below. The blanket
|
|
116
124
|
* 4096 cap previously throttled every call and could be exhausted by a thinking block
|
|
117
125
|
* before any text was emitted. Capped at the model's real ceiling at call time.
|
|
118
126
|
* Not exposed in TUI — controlled via PI_RESEARCH_PLANNING_MAX_TOKENS env var. */
|
|
119
127
|
PLANNING_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 16384 }),
|
|
120
|
-
/** Max output tokens for the final synthesized research report (the
|
|
121
|
-
*
|
|
128
|
+
/** Max output tokens for the final synthesized research report (the synthesizer call,
|
|
129
|
+
* and each pass of a reduce when the corpus does not fit in one). Default 32768 so a
|
|
130
|
+
* full cited report is never truncated. It is also the reserve subtracted from the
|
|
131
|
+
* model's context window when budgeting that corpus. Capped at
|
|
122
132
|
* the model's real maxTokens at call time.
|
|
123
133
|
* Not exposed in TUI — controlled via PI_RESEARCH_SYNTHESIS_MAX_TOKENS env var. */
|
|
124
134
|
SYNTHESIS_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 32768 }),
|
|
125
135
|
/** LLM Model override for researcher sub-agents and knowledge synthesis.
|
|
126
136
|
* Format: provider/model-id (e.g. google/gemini-2.0-flash-001) or just model-id.
|
|
127
137
|
* When set, this overrides ctx.model for researcher sub-agents (both deep and quick)
|
|
128
|
-
* and the knowledge synthesis background LLM. The coordinator and
|
|
129
|
-
* use the caller's model (ctx.model).
|
|
138
|
+
* and the knowledge synthesis background LLM. The coordinator and the research lead
|
|
139
|
+
* always use the caller's model (ctx.model).
|
|
130
140
|
*/
|
|
131
141
|
RESEARCH_MODEL: Type.Optional(Type.String()),
|
|
132
142
|
/** Explicit directory for the knowledge store database (overrides default) */
|
package/src/constants.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function getUnitsPerResearcher(config?: Config): number {
|
|
|
69
69
|
return 1 + Math.min(batches, 4);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
/** Progress bar weight per lead
|
|
72
|
+
/** Progress bar weight per research-lead round */
|
|
73
73
|
export const LEAD_EVAL_UNITS = 2;
|
|
74
74
|
|
|
75
75
|
|
|
@@ -58,9 +58,9 @@ export interface ResearchObserver {
|
|
|
58
58
|
// Synthesis/Completion
|
|
59
59
|
/**
|
|
60
60
|
* Fired when the run enters FINAL synthesis / stops consuming steering:
|
|
61
|
-
* - deep mode: right before the
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* - deep mode: right before the synthesizer call, and on the path where the
|
|
62
|
+
* router chose to finish early (idempotent there — the flag is already off
|
|
63
|
+
* via onEvaluationDecision('synthesize')).
|
|
64
64
|
* - quick mode: as soon as the researcher session settles, i.e. the point
|
|
65
65
|
* the 500ms steering-consume poller stops running.
|
|
66
66
|
* TUI consequence: steeringAcceptable flips false, so a late steer takes the
|
|
@@ -59,6 +59,8 @@ export interface RunResearchersOptions {
|
|
|
59
59
|
export interface IResearchSynthesisService extends IService {
|
|
60
60
|
storeReport(researchId: string, researcherId: string, report: string): void;
|
|
61
61
|
getAllReports(researchId: string): Map<string, string>;
|
|
62
|
+
/** Coverage digest per stored report, derived when a researcher emitted none. */
|
|
63
|
+
getAllDigests(researchId: string): Map<string, string>;
|
|
62
64
|
getReport(researchId: string, id: string): string | undefined;
|
|
63
65
|
getReportsForRound(researchId: string, round: number): Map<string, string>;
|
|
64
66
|
getReportCount(researchId: string): number;
|
|
@@ -56,6 +56,12 @@ export interface GenerateQueriesOptions {
|
|
|
56
56
|
export interface UpdatePlanOptions {
|
|
57
57
|
sessionId: string;
|
|
58
58
|
reports: Map<string, string>;
|
|
59
|
+
/** Coverage digest per report id. The router reads the FRESH round's reports in full
|
|
60
|
+
* (evidence-based routing) and falls back to these digests only for PRIOR rounds — it
|
|
61
|
+
* does not read digests instead of the current round's reports. Optional: when omitted,
|
|
62
|
+
* digests are derived from the report bodies so a caller that doesn't track them still
|
|
63
|
+
* routes on coverage rather than on nothing. */
|
|
64
|
+
digests?: Map<string, string>;
|
|
59
65
|
round: number;
|
|
60
66
|
query: string;
|
|
61
67
|
complexity: 1 | 2 | 3;
|
|
@@ -98,7 +104,7 @@ export interface IPlanningService extends IService {
|
|
|
98
104
|
getTeamSize(complexity: 1 | 2 | 3): number;
|
|
99
105
|
getQueryBudget(complexity: 1 | 2 | 3): number;
|
|
100
106
|
getComplexityGuidance(complexity: 1 | 2 | 3, maxTeamSize: number, queryBudget: number): string;
|
|
101
|
-
getEvaluatorComplexityGuidance(complexity: 1 | 2 | 3): string;
|
|
107
|
+
getEvaluatorComplexityGuidance(complexity: 1 | 2 | 3, researchRounds: number): string;
|
|
102
108
|
getRoundPhaseGuidance(currentRound: number, maxRounds: number, complexity: 1 | 2 | 3, maxTeamSize: number): string;
|
|
103
109
|
capResearcherQueries(plan: ResearchPlan, complexity: 1 | 2 | 3, serviceName: string): ResearchPlan;
|
|
104
110
|
parseJsonPlan(text: string): ResearchPlan;
|
|
@@ -70,9 +70,25 @@ export interface ScrapeResult {
|
|
|
70
70
|
/**
|
|
71
71
|
* Base scheduler interface
|
|
72
72
|
*/
|
|
73
|
+
/**
|
|
74
|
+
* Reports when a task's EXECUTION starts and stops.
|
|
75
|
+
*
|
|
76
|
+
* Callers that keep their own guard over a scheduler call arm it on 'dispatched'
|
|
77
|
+
* rather than at call time, so queue wait never counts against the work's budget,
|
|
78
|
+
* and disarm it on 'settled' so the guard is not left running over a period in
|
|
79
|
+
* which nothing is executing — between a failed attempt and its retry, that
|
|
80
|
+
* period includes a backoff sleep, a re-enqueue and a fresh queue wait, and a
|
|
81
|
+
* timer left armed across it kills the retry for the predecessor's overrun.
|
|
82
|
+
*
|
|
83
|
+
* An implementation that cannot observe dispatch (BrowserClient, which only sees
|
|
84
|
+
* the leader's final reply) never calls it at all, and the caller's guard stays
|
|
85
|
+
* disarmed — that implementation bounds the call itself.
|
|
86
|
+
*/
|
|
87
|
+
export type TaskDispatchListener = (event: 'dispatched' | 'settled') => void;
|
|
88
|
+
|
|
73
89
|
export interface IScheduler {
|
|
74
|
-
runSearch(query: string, config?: Config, signal?: AbortSignal): Promise<SearchResult[]>;
|
|
75
|
-
runScrape(url: string, config?: Config, signal?: AbortSignal): Promise<ScrapeResult>;
|
|
90
|
+
runSearch(query: string, config?: Config, signal?: AbortSignal, onTaskEvent?: TaskDispatchListener): Promise<SearchResult[]>;
|
|
91
|
+
runScrape(url: string, config?: Config, signal?: AbortSignal, onTaskEvent?: TaskDispatchListener): Promise<ScrapeResult>;
|
|
76
92
|
runHealthCheck(config?: Config, signal?: AbortSignal): Promise<{ success: boolean }>;
|
|
77
93
|
shutdown(): Promise<void>;
|
|
78
94
|
resetIdleTimerOnActivity?(): void;
|
|
@@ -34,6 +34,10 @@ export interface JsonRepairOptions {
|
|
|
34
34
|
maxTokens?: number;
|
|
35
35
|
/** Thinking level for the repair pass (default 'off' — repair emits JSON, not reasoning). */
|
|
36
36
|
thinkingLevel?: ModelThinkingLevel;
|
|
37
|
+
/** Research session id, forwarded to the provider as its prompt-cache / session-affinity
|
|
38
|
+
* key (see llm-utils.buildSafeOptions). Keeps the repair pass on the same upstream
|
|
39
|
+
* replica as the call it is repairing. */
|
|
40
|
+
sessionId?: string;
|
|
37
41
|
/** Usage sink invoked once per billed LLM attempt with the raw usage object from
|
|
38
42
|
* the response. Lets callers attribute the repair pass's tokens/cost to the run
|
|
39
43
|
* (metrics + observer) exactly as they do for the primary call. Every attempt that
|
|
@@ -70,7 +74,7 @@ export async function repairJsonWithLlm<T = any>(
|
|
|
70
74
|
auth: { apiKey: string; headers?: Record<string, string | null> },
|
|
71
75
|
options: JsonRepairOptions
|
|
72
76
|
): Promise<T | null> {
|
|
73
|
-
const { model, context, schema, serviceName = 'RepairService', signal, maxTokens = 16384, thinkingLevel = 'off' } = options;
|
|
77
|
+
const { model, context, schema, serviceName = 'RepairService', signal, maxTokens = 16384, thinkingLevel = 'off', sessionId } = options;
|
|
74
78
|
|
|
75
79
|
logger.warn(`[${serviceName}] JSON parse failed; attempting agentic salvage`);
|
|
76
80
|
|
|
@@ -118,7 +122,8 @@ Return ONLY the valid JSON object. No prose before or after.`;
|
|
|
118
122
|
],
|
|
119
123
|
}, buildSafeOptions(model, {
|
|
120
124
|
...auth,
|
|
121
|
-
signal
|
|
125
|
+
signal,
|
|
126
|
+
...(sessionId ? { sessionId } : {}),
|
|
122
127
|
}, maxTokens, thinkingLevel)),
|
|
123
128
|
llmTimeout, `agentic-repair-${serviceName}`,
|
|
124
129
|
);
|
|
@@ -35,7 +35,10 @@ function getCurrentDateString(): string {
|
|
|
35
35
|
* training-cutoff defaults). The full date follows for precision. Applies to
|
|
36
36
|
* coordinator, researcher, and evaluator across all modes.
|
|
37
37
|
*/
|
|
38
|
-
export function injectCurrentDate(
|
|
38
|
+
export function injectCurrentDate(
|
|
39
|
+
prompt: string,
|
|
40
|
+
_agentType: 'coordinator' | 'researcher' | 'router' | 'synthesizer',
|
|
41
|
+
): string {
|
|
39
42
|
const monthYear = getCurrentMonthYearString();
|
|
40
43
|
const fullDate = getCurrentDateString();
|
|
41
44
|
// Derive the stale-year examples from the current year so the guidance stays correct in any
|
|
@@ -22,6 +22,13 @@ import { logger } from '../../logger.ts';
|
|
|
22
22
|
* thinking block only consumes the output-token budget (often truncating before the
|
|
23
23
|
* text block is emitted) for at most a marginal quality gain that does not justify the cost.
|
|
24
24
|
*
|
|
25
|
+
* Callers should also pass the research `sessionId` in `options`. pi-ai forwards it as the
|
|
26
|
+
* provider's prompt-cache / session-affinity key — OpenAI's `prompt_cache_key`, OpenRouter's
|
|
27
|
+
* `x-session-id` — which keeps a run's calls on the replica that warmed their cache. Without
|
|
28
|
+
* it OpenRouter falls back to hashing the first system and user messages, which every round
|
|
29
|
+
* of a research run mutates, so it re-routes and re-warms. Providers that do not implement
|
|
30
|
+
* session affinity ignore the field, so passing it is unconditionally safe.
|
|
31
|
+
*
|
|
25
32
|
* @param model - The model being called
|
|
26
33
|
* @param options - Caller-provided options (an explicit `reasoning` here wins)
|
|
27
34
|
* @param defaultCap - Default maxTokens cap (clamped to model.maxTokens)
|