@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.
Files changed (86) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.mjs +5670 -4705
  3. package/dist/prompts/research-tool-usage.md +1 -1
  4. package/dist/prompts/researcher.md +1 -0
  5. package/dist/prompts/system-lead-router.md +122 -0
  6. package/dist/prompts/system-lead-synthesizer.md +46 -0
  7. package/dist/thread-worker.mjs +169 -62
  8. package/docs/ARCHITECTURE.md +18 -10
  9. package/docs/CHANGELOG.md +225 -0
  10. package/docs/CONFIGURATION.md +106 -7
  11. package/package.json +2 -2
  12. package/scripts/build.cjs +8 -1
  13. package/skills/pi-research/SKILL.md +1 -1
  14. package/src/cli.ts +5 -1
  15. package/src/config.ts +22 -12
  16. package/src/constants.ts +1 -1
  17. package/src/core/interfaces/observer-interfaces.ts +3 -3
  18. package/src/core/interfaces/orchestration-interfaces.ts +2 -0
  19. package/src/core/interfaces/planning-interfaces.ts +7 -1
  20. package/src/core/interfaces/scheduler-interfaces.ts +18 -2
  21. package/src/core/llm/agentic-repair.ts +7 -2
  22. package/src/core/llm/inject-date.ts +4 -1
  23. package/src/core/llm/llm-utils.ts +7 -0
  24. package/src/core/planning-service.ts +766 -91
  25. package/src/core/planning-utils.ts +21 -5
  26. package/src/core/service-initialization.ts +19 -3
  27. package/src/healthcheck/index.ts +11 -2
  28. package/src/index.ts +4 -3
  29. package/src/infrastructure/browser/browser-client.ts +68 -34
  30. package/src/infrastructure/browser/browser-task-scheduler.ts +236 -43
  31. package/src/infrastructure/browser/config.ts +79 -1
  32. package/src/infrastructure/browser/priority-task-queue.ts +55 -1
  33. package/src/infrastructure/browser/scheduler-factory.ts +12 -1
  34. package/src/infrastructure/browser/task-execution-service.ts +5 -5
  35. package/src/infrastructure/browser/thread-worker-browser.ts +87 -36
  36. package/src/infrastructure/browser/thread-worker-messaging.ts +99 -10
  37. package/src/infrastructure/browser/thread-worker.mjs +169 -62
  38. package/src/infrastructure/browser/thread-worker.ts +13 -5
  39. package/src/infrastructure/browser/worker-pool-manager.ts +127 -12
  40. package/src/infrastructure/embedding/embedding-client.ts +14 -4
  41. package/src/infrastructure/embedding/embedding-factory.ts +83 -15
  42. package/src/infrastructure/embedding/embedding-server.ts +34 -0
  43. package/src/infrastructure/file-lock-service.ts +214 -19
  44. package/src/infrastructure/knowledge-store-service.ts +14 -0
  45. package/src/infrastructure/research-run-semaphore.ts +1 -1
  46. package/src/infrastructure/service-initialization.ts +7 -0
  47. package/src/knowledge/embedder-init.ts +46 -19
  48. package/src/knowledge/embedder.ts +11 -3
  49. package/src/knowledge/index.ts +36 -10
  50. package/src/knowledge/store.ts +29 -5
  51. package/src/logger.ts +11 -0
  52. package/src/orchestration/deep-research-orchestrator.ts +143 -87
  53. package/src/orchestration/quick-research-orchestrator.ts +11 -2
  54. package/src/orchestration/research-orchestration-service.ts +8 -0
  55. package/src/orchestration/research-synthesis-service.ts +53 -1
  56. package/src/orchestration/researcher-executor.ts +53 -2
  57. package/src/orchestration/researcher.ts +15 -2
  58. package/src/orchestration/session-state.ts +16 -0
  59. package/src/prompts/research-tool-usage.md +1 -1
  60. package/src/prompts/researcher.md +1 -0
  61. package/src/prompts/system-lead-router.md +122 -0
  62. package/src/prompts/system-lead-synthesizer.md +46 -0
  63. package/src/sdk.ts +10 -0
  64. package/src/skill-install/skill-installer.ts +5 -1
  65. package/src/stackexchange/index.ts +3 -0
  66. package/src/stackexchange/rest-client.ts +7 -12
  67. package/src/tools/research-knowledge-search.ts +11 -3
  68. package/src/tools/research-tool-definition.ts +38 -7
  69. package/src/tools/scrape.ts +6 -0
  70. package/src/tools/search.ts +7 -0
  71. package/src/tools/security.ts +31 -10
  72. package/src/tools/stackexchange.ts +13 -7
  73. package/src/tools/youtube-transcript.ts +26 -12
  74. package/src/utils/cancellation.ts +36 -0
  75. package/src/utils/coverage-digest.ts +204 -0
  76. package/src/utils/host-config.ts +12 -1
  77. package/src/utils/llm-usage.ts +33 -6
  78. package/src/utils/log-utils.ts +10 -0
  79. package/src/utils/metrics-summary.ts +20 -4
  80. package/src/utils/metrics.ts +27 -1
  81. package/src/web-research/browser-search.ts +102 -22
  82. package/src/web-research/web-scraper.ts +27 -16
  83. package/src/youtube/potoken.ts +8 -1
  84. package/src/youtube/transcript-client.ts +56 -5
  85. package/dist/prompts/system-lead-evaluator.md +0 -118
  86. 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
- /** Queue-wait / overhead margin (ms) added on top of each browser operation's own nav
86
- * timeout to form its scheduler task-timeout ceiling: a search task times out after
87
- * SEARCH_TIMEOUT_MS + this, a scrape after SCRAPE_TIMEOUT_MS + this. Sized so a task is
88
- * never killed before it has used its full nav budget plus time waiting in the queue.
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/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
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
- * evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
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 + mid-round evaluator decision (and,
115
- * when the evaluator synthesizes mid-round, the report). Default 16384. The blanket
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 forced-synthesis
121
- * evaluator call). Default 32768 so a full cited report is never truncated. Capped at
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 evaluator always
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 evaluator round */
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 forced (mustSynthesize) evaluator call, and
62
- * on the evaluator-chose-synthesize path (idempotent there — the flag is
63
- * already off via onEvaluationDecision('synthesize')).
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(prompt: string, _agentType: 'coordinator' | 'researcher' | 'evaluator'): string {
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)