@lincoln504/pi-research 0.1.12 → 1.0.0

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 (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -115
  3. package/dist/cli.mjs +23211 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1044 -0
  30. package/src/config.ts +1036 -147
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +264 -7
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -174
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -577
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
package/src/index.ts CHANGED
@@ -1,74 +1,663 @@
1
- /**
2
- * pi-research Extension
3
- *
4
- * Orchestrates multi-agent research using a state-driven deep mode architecture.
5
- * Manages SearXNG as singleton (shared across all agents).
6
- * SearXNG is initialized on first research() call, NOT on session_start.
7
- * Extension-owned cleanup is delegated to pi's session lifecycle.
8
- */
1
+ // Set ONNX Runtime logging level early (before library load) to prevent
2
+ // "Attempt to use DefaultLogger but none has been registered" crash on exit.
3
+ // Level 3 = Error, 4 = Fatal.
4
+ process.env['ORT_LOGGING_LEVEL'] = '3';
9
5
 
10
- import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
11
- import { createResearchTool } from './tool.ts';
12
- import { logger, isVerboseFromEnv, getDefaultDebugLogPathTemplate } from './logger.ts';
13
- import { checkDockerAvailability } from './infrastructure/searxng-lifecycle.ts';
6
+ import type { ExtensionAPI, ToolDefinition, AgentToolResult, ExtensionContext, SessionShutdownEvent, SessionBeforeCompactEvent, SessionCompactEvent } from '@earendil-works/pi-coding-agent';
7
+ import type { ExtendedExtensionContext } from './types/extension-context.ts';
8
+ import { VERSION as PI_VERSION } from '@earendil-works/pi-coding-agent';
9
+ import { Key } from '@earendil-works/pi-tui';
10
+ import type { ResearchResultDetails } from './types/index.ts';
11
+ import { createResearchTool, createHealthTool } from './tool.ts';
12
+ import { createResearchKnowledgeSearchTool } from './tools/research-knowledge-search.ts';
13
+ import { logger } from './logger.ts';
14
+ import { randomUUID } from 'node:crypto';
14
15
  import { shutdownManager } from './utils/shutdown-manager.ts';
15
- import { readFileSync } from 'node:fs';
16
- import { dirname, join } from 'node:path';
17
- import { fileURLToPath } from 'node:url';
16
+ import { healthRegistry } from './healthcheck/index.ts';
17
+ import { registerBeforeExitSafetyNet } from './knowledge/embedder-utils.ts';
18
+ import { getConfig, validateConfig } from './config.ts';
19
+ import { metrics } from './utils/metrics.ts';
20
+ import { handleResearchConfigCommand } from './research-config.ts';
21
+ import { loadPrompt } from './core/llm/prompts.ts';
22
+ import { clearAllSessionState, addSteeringMessage, getSteeringMessages, normalizeSessionId, getActiveSessionCount, popQueuedMessages, requeuePoppedMessage, getAllTrackedSessions, getPiActiveSessionOrder, getPiActivePanels } from './orchestration/session-state.ts';
23
+ import { initGlobalTuiController, disposeGlobalTuiController } from './tui/tui-controller.ts';
24
+ import { registerCoreServices, initializeCoreServices, disposeCoreServices } from './core/service-initialization.ts';
25
+ import { getServiceContainer, resetServiceContainer } from './core/service-registry.ts';
26
+ import { registerInfrastructureServices } from './infrastructure/service-initialization.ts';
27
+ import { registerOrchestrationServices } from './orchestration/service-initialization.ts';
28
+
29
+ // Modular Orchestration Exports
30
+ export { ServiceNames } from './core/interfaces/service-names.ts';
31
+ export type { IResearchOrchestration, ResearchOptions } from './core/interfaces/orchestration-interfaces.ts';
32
+ export { DeepResearchOrchestrator, type DeepResearchOrchestratorOptions } from './orchestration/deep-research-orchestrator.ts';
33
+ export { QuickResearchOrchestrator, type QuickResearchOrchestratorOptions } from './orchestration/quick-research-orchestrator.ts';
34
+ export { shutdownManager } from './utils/shutdown-manager.ts';
35
+ export type { ResearchObserver } from './core/interfaces/observer-interfaces.ts';
36
+ export { normalizeUrl } from './utils/shared-links.ts';
37
+ export { resetConfig, getConfig, setConfig, validateConfig } from './config.ts';
38
+
39
+ // Programmatic SDK Exports. Explicit list (not `export *`) so getSDKContainer —
40
+ // @internal, test-only in src/sdk.ts, imported only by shutdown-perf.test.ts via a
41
+ // direct relative path — is not re-published from the package's primary entry point.
42
+ // (It remains reachable via the separate, intentional `@lincoln504/pi-research/sdk`
43
+ // raw-module subpath in package.json exports, which this does not touch.)
44
+ export {
45
+ initResearchSDK,
46
+ type ResearchSDKOptions,
47
+ repairJson,
48
+ exportKnowledge,
49
+ verifyUrl,
50
+ scrapeUrl,
51
+ runDeepResearch,
52
+ getLastRunMetrics,
53
+ getLastRunSummary,
54
+ getLastErrorReport,
55
+ getLastRunStats,
56
+ getSessionMetrics,
57
+ getResearchHealth,
58
+ runQuickResearch,
59
+ getResearchReports,
60
+ type ResearchRunResult,
61
+ runResearchDetailed,
62
+ type KnowledgeSearchResult,
63
+ searchKnowledge,
64
+ shutdownResearchSDK,
65
+ } from './sdk.ts';
66
+ export { HeadlessObserver, type HeadlessObserverOptions } from './orchestration/headless-observer.ts';
67
+
68
+ // Audit / metrics types for SDK consumers (getLastRunMetrics/getLastRunStats etc.)
69
+ export type { IMetricsSnapshot, IMetricHistogram, RunSummary } from './utils/metrics.ts';
70
+ export type { ResearchStats } from './utils/metrics-summary.ts';
71
+ export { extractRunStats } from './utils/metrics-summary.ts';
72
+
73
+ import {
74
+ MAX_TEAM_SIZE_LEVEL_1,
75
+ MAX_TEAM_SIZE_LEVEL_2,
76
+ MAX_TEAM_SIZE_LEVEL_3,
77
+ } from './constants.ts';
18
78
 
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
79
+ // Extract the text content from a research tool result
80
+ function extractResultText(result: AgentToolResult<unknown>): string {
81
+ const textBlock = result.content?.find(
82
+ (c): c is { type: 'text'; text: string } => c.type === 'text',
83
+ );
84
+ return textBlock?.text || 'Research completed, but no text content was generated.';
85
+ }
21
86
 
22
87
  /**
23
- * Load prompt from markdown file
88
+ * Pi Research Extension
24
89
  */
25
- function loadPrompt(name: string): string {
26
- const path = join(__dirname, 'prompts', `${name}.md`);
90
+ export default async function (pi: ExtensionAPI) {
91
+ // Runtime version check — must match the @earendil-works/* dependency minimum (>=0.80.2).
92
+ // 0.80.2 is the floor because the AuthStorage credential discriminator pi-research
93
+ // passes (`type: 'api_key'`, model-registry-factory.ts) only became correct in 0.80.2
94
+ // (it was `api-key` before); the pi-ai `/compat` entrypoint also exists from 0.80.0.
95
+ const versionParts = PI_VERSION.split('.').map(Number);
96
+ const major = versionParts[0] ?? 0;
97
+ const minor = versionParts[1] ?? 0;
98
+ const patch = versionParts[2] ?? 0;
99
+ if (isNaN(major) || isNaN(minor) || isNaN(patch)) {
100
+ throw new Error(
101
+ `[pi-research] Cannot parse pi-coding-agent version "${PI_VERSION}". ` +
102
+ `Please ensure pi-coding-agent is installed correctly.`,
103
+ );
104
+ }
105
+ const minMajor = 0, minMinor = 80, minPatch = 2;
106
+ const tooOld = major < minMajor
107
+ || (major === minMajor && minor < minMinor)
108
+ || (major === minMajor && minor === minMinor && patch < minPatch);
109
+ if (tooOld) {
110
+ throw new Error(
111
+ `[pi-research] pi-coding-agent v${PI_VERSION} is too old. ` +
112
+ `Requires v${minMajor}.${minMinor}.${minPatch}+. Please update pi-coding-agent.`,
113
+ );
114
+ }
115
+
116
+ // Re-register the beforeExit safety net (deactivate() strips event listeners during reload).
117
+ // This ensures the ONNX pipeline is disposed even after extension reloads.
118
+ registerBeforeExitSafetyNet();
119
+
120
+ // 1. REGISTER CRITICAL EVENT LISTENERS IMMEDIATELY
121
+ // This ensures we capture steering even if initialization takes time.
122
+ pi.on('input', async (event: any, ctx: ExtensionContext) => {
123
+ // Only intercept genuine interactive keystrokes. Programmatic sends originate
124
+ // from this extension (source 'extension') — e.g. the Alt+P pop handler forwards
125
+ // a popped message as 'steer'. Without this guard that forward would be captured
126
+ // straight back into the research queue, so the pop could never reach pi.
127
+ if (event.source === 'extension') return undefined;
128
+ if (event.streamingBehavior === 'steer' && event.text) {
129
+ try {
130
+ const eCtx = ctx as ExtendedExtensionContext;
131
+ // eslint-disable-next-line no-control-regex
132
+ const sanitized = event.text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').trim();
133
+ if (!sanitized) return undefined;
134
+
135
+ const activeCount = getActiveSessionCount();
136
+ if (activeCount === 0) return undefined;
137
+
138
+ logger.debug(`[pi-research] Captured steering input. sessionId=${eCtx.sessionId}`);
139
+
140
+ // Best-effort UI feedback. Without it, a steering message that is forwarded to
141
+ // pi's follow-up queue (because the current research step can't be interrupted)
142
+ // is delivered but invisible — it looks like nothing happened.
143
+ const notify = (msg: string, level: 'info' | 'warning' = 'info') => {
144
+ try { if (ctx.hasUI) ctx.ui.notify(msg, level); } catch { /* notify is best-effort */ }
145
+ };
146
+
147
+ let sessionIds: string[] = [];
148
+ if (eCtx.sessionId) {
149
+ sessionIds = [eCtx.sessionId];
150
+ } else {
151
+ const activeSessions = getAllTrackedSessions().filter(sid => getPiActiveSessionOrder(sid).length > 0);
152
+ if (activeSessions.length === 1) {
153
+ sessionIds = [activeSessions[0]!];
154
+ } else if (activeSessions.length > 1) {
155
+ // Ambiguous — we cannot attribute the message to one research session.
156
+ // Do NOT swallow it: fall through so pi applies its own native steering.
157
+ notify('Multiple active research sessions — steering left to pi.', 'warning');
158
+ return undefined;
159
+ }
160
+ }
161
+
162
+ if (sessionIds.length === 0) {
163
+ // No research session we can route to — let pi handle the input natively
164
+ // rather than silently dropping it (returning 'handled' would eat it).
165
+ return undefined;
166
+ }
167
+
168
+ let queued = 0;
169
+ let forwarded = 0;
170
+ for (const sid of sessionIds) {
171
+ // Check if any active research panel in this session can accept steering.
172
+ // During eval/coordinator LLM calls, steering is forwarded to follow-up
173
+ // instead of being queued, since the running LLM call cannot be interrupted
174
+ // and the message might not be consumed before the research ends.
175
+ const activePanels = getPiActivePanels(sid);
176
+ const steeringAcceptable = activePanels.some(p => p.steeringAcceptable === true);
177
+
178
+ if (steeringAcceptable) {
179
+ addSteeringMessage(sid, sanitized);
180
+ queued++;
181
+ } else {
182
+ // Steering not acceptable right now (e.g. an eval/coordinator LLM call is in
183
+ // flight). Forward to pi as a STEER, not a follow-up: steer is injected at the
184
+ // next agent step — right after the running research tool returns its report —
185
+ // so it lands as the next message instead of sitting until the agent stops
186
+ // (which, in a long autonomous turn, may never happen). The source-'extension'
187
+ // guard above prevents this from being re-captured back into the queue.
188
+ logger.debug(`[pi-research] Steering not acceptable for session ${sid}, forwarding as steer: ${sanitized}`);
189
+ try {
190
+ await pi.sendUserMessage(sanitized, { deliverAs: 'steer' });
191
+ forwarded++;
192
+ } catch (err) {
193
+ logger.warn('[pi-research] Failed to forward steering to pi:', err);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Surface the outcome so the action is never invisible.
199
+ if (queued > 0 && forwarded === 0) {
200
+ notify('Queued — will steer the next research round.', 'info');
201
+ } else if (forwarded > 0 && queued === 0) {
202
+ notify('Sent to pi to steer the agent.', 'info');
203
+ } else if (queued > 0 && forwarded > 0) {
204
+ notify('Steering received (queued for next round + steer to pi).', 'info');
205
+ } else {
206
+ // Nothing was delivered (e.g. the steer send threw) — let pi handle it
207
+ // natively rather than eating the message.
208
+ return undefined;
209
+ }
210
+
211
+ // Return handled to indicate we have fully processed this input
212
+ // and Pi should not echo it into the main chat log.
213
+ return { action: 'handled' };
214
+ } catch (err) {
215
+ logger.debug('[pi-research] Input handler error:', err);
216
+ }
217
+ }
218
+
219
+ return undefined;
220
+ });
221
+
222
+ // 2. REGISTER SHUTDOWN TASKS
223
+ shutdownManager.register(async () => {
224
+ try {
225
+ // Dispose native resources (ONNX pipeline) while C++ statics are still alive.
226
+ await disposeCoreServices();
227
+ logger.log('[pi-research] All services disposed');
228
+ // Clear service REGISTRATIONS too (not just instances). disposeCoreServices
229
+ // nulls instances but leaves the factory map intact, so without this a
230
+ // same-process re-activate() (pi reload / new session) would re-run
231
+ // registerCoreServices against an already-populated container and throw on
232
+ // the first allowOverwrite:false register — leaving isReady=true over stale
233
+ // services. Matches the SDK and CLI teardown (both reset after dispose).
234
+ await resetServiceContainer(getServiceContainer());
235
+ // Clear in-memory state after disposal.
236
+ disposeGlobalTuiController();
237
+ clearAllSessionState();
238
+ metrics.clearSession();
239
+ logger.info('[pi-research] All session state cleared');
240
+ } catch (err) {
241
+ logger.error('[pi-research] Shutdown task failed:', err);
242
+ }
243
+ });
244
+
245
+ // 3. SERVICE INITIALIZATION
246
+ logger.log(`[pi-research] Activating extension (pi v${PI_VERSION})...`);
247
+
248
+ // Register and initialize services into the global container
27
249
  try {
28
- return readFileSync(path, 'utf-8');
29
- } catch (error) {
30
- const msg = error instanceof Error ? error.message : String(error);
31
- logger.error(`[pi-research] Failed to load prompt from ${path}: ${msg}`);
32
- return '';
250
+ const container = getServiceContainer();
251
+ registerCoreServices(container);
252
+ registerInfrastructureServices(container);
253
+ registerOrchestrationServices(container);
254
+ logger.log('[pi-research] Services registered');
255
+
256
+ // Pass pi as context — includes cwd for proper config loading
257
+ const result = await initializeCoreServices(pi, container);
258
+ if (result.failed.length > 0) {
259
+ logger.error(`[pi-research] WARNING: Service initialization incomplete: ${result.failed.join(', ')}`);
260
+ } else {
261
+ logger.log('[pi-research] All critical services initialized and ready');
262
+ container.isReady = true;
263
+ }
264
+ } catch (err) {
265
+ logger.error('[pi-research] Critical failure during service setup:', err);
33
266
  }
34
- }
35
267
 
36
- export default function (pi: ExtensionAPI) {
37
- logger.log('[pi-research] Extension loading...');
38
268
 
39
- // NOTE: SearXNG is still initialized lazily on first research() call in tool.ts.
40
- // Cleanup runs on pi's session lifecycle instead of owning host process signals.
269
+ // Validate config at startup (using pi.cwd)
270
+ try {
271
+ const config = getConfig((pi as any).cwd, 'pi');
272
+ validateConfig(config);
273
+ logger.debug('[pi-research] Config validated');
274
+ } catch (err) {
275
+ logger.error(`[pi-research] WARNING: Config validation failed: ${err instanceof Error ? err.message : String(err)}`);
276
+ }
277
+
278
+ // Self-heal cross-harness skill installs: re-point links left stale by an
279
+ // update and prune ones left dangling by a prior removal. Best-effort — a
280
+ // filesystem hiccup here must never block activation. (npm does not run our
281
+ // preuninstall on uninstall, so this startup pass is the reliable GC.)
282
+ try {
283
+ const { reconcileSkillInstalls } = await import('./skill-install/skill-installer.ts');
284
+ const r = reconcileSkillInstalls();
285
+ if (r.repointed.length || r.pruned.length) {
286
+ logger.info(`[pi-research] Skill links reconciled: ${r.repointed.length} re-pointed, ${r.pruned.length} pruned`);
287
+ }
288
+ } catch (err) {
289
+ logger.debug('[pi-research] Skill reconcile skipped:', err);
290
+ }
291
+
292
+ // Primary cleanup path for pi -p (print mode) and normal session end.
293
+ // Only arm force-exit and mark PI_PROCESS_EXITING on a genuine quit — not on
294
+ // reload/new/resume/fork where the process continues under a rebuilt extension.
295
+ pi.on('session_shutdown', async (event: SessionShutdownEvent) => {
296
+ try {
297
+ if (event.reason === 'quit') {
298
+ process.env['PI_PROCESS_EXITING'] = '1';
299
+ // Watchdog: browser pool + ONNX teardown can take up to ~30s in worst case.
300
+ shutdownManager.forceExitAfter(35000);
301
+ }
302
+ await shutdownManager.runCleanup(`session_shutdown:${event.reason}`);
303
+ } catch (_err) {
304
+ logger.error('[pi-research] session_shutdown cleanup failed:', _err);
305
+ }
306
+ });
307
+
308
+ pi.on('session_before_compact', (event: SessionBeforeCompactEvent) => {
309
+ logger.debug(`[pi-research] compaction starting: reason=${event.reason} willRetry=${event.willRetry}`);
310
+ });
311
+
312
+ pi.on('session_compact', (event: SessionCompactEvent) => {
313
+ logger.debug(`[pi-research] compaction complete: reason=${event.reason} willRetry=${event.willRetry}`);
314
+ });
315
+
316
+ // Create and register the research tool
317
+ const researchTool: ToolDefinition = createResearchTool('pi');
318
+ pi.registerTool(researchTool);
319
+
320
+ // Create and register the health check tool
321
+ const healthTool: ToolDefinition = createHealthTool();
322
+ pi.registerTool(healthTool);
323
+
324
+ // Create and register the Research Knowledge Search tool. It is native-free at
325
+ // construction (the vector/ML stack loads lazily on first execute) and self-guards when
326
+ // Knowledge Mode is 'none' (its execute returns a "store disabled" miss). Registering it
327
+ // unconditionally lets a Knowledge Mode change via /research-config take effect without a
328
+ // Pi restart — availability to the agent and the /knowledge-store command below both key
329
+ // off live config, not this startup binding. The pi API has no unregisterTool, so a
330
+ // startup-gated registration could never be added back after enabling mid-session anyway.
331
+ const researchKnowledgeSearchTool: ToolDefinition = createResearchKnowledgeSearchTool('pi');
332
+ pi.registerTool(researchKnowledgeSearchTool);
41
333
 
42
- pi.on('session_shutdown', async (_event, _ctx) => {
43
- await shutdownManager.runCleanup('session_shutdown');
334
+ // Alt+P — Pop queued steering messages out of the research queue and steer pi with them.
335
+ pi.registerShortcut(Key.alt('p'), {
336
+ description: 'Pop queued research steering messages back to chat',
337
+ handler: async (ctx: ExtensionContext) => {
338
+ const eCtx = ctx as ExtendedExtensionContext;
339
+ let piSessionId = eCtx.sessionId || eCtx.sessionManager?.getSessionId();
340
+
341
+ if (!piSessionId) {
342
+ const activeSessions = getAllTrackedSessions().filter(sid => getPiActiveSessionOrder(sid).length > 0);
343
+ if (activeSessions.length === 1) {
344
+ piSessionId = activeSessions[0]!;
345
+ } else if (activeSessions.length > 1) {
346
+ if (ctx.hasUI) ctx.ui.notify('Ambiguous sessions — cannot pop steering.', 'warning');
347
+ return;
348
+ } else {
349
+ piSessionId = 'default';
350
+ }
351
+ }
352
+
353
+ const activeCount = getActiveSessionCount();
354
+ if (activeCount === 0) return;
355
+
356
+ const queuedBefore = getSteeringMessages(piSessionId).filter(m => m.status === 'queued');
357
+ const popped = popQueuedMessages(piSessionId);
358
+
359
+ if (popped.length === 0) {
360
+ if (ctx.hasUI) {
361
+ // queuedBefore>0 but nothing popped means the orchestrator consumed the
362
+ // message between the panel render and this keypress — tell the user
363
+ // rather than appearing to do nothing.
364
+ ctx.ui.notify(
365
+ queuedBefore.length === 0
366
+ ? 'No steering messages found.'
367
+ : 'Steering already consumed by research.',
368
+ 'info',
369
+ );
370
+ }
371
+ return;
372
+ }
373
+ // Forward as 'steer', NOT 'followUp'. A popped message means "redirect the agent
374
+ // now". A follow-up is only consumed once the agent would otherwise stop, so during
375
+ // a long autonomous turn — e.g. a multi-round research the model keeps extending —
376
+ // it never drains and the user sees their instruction ignored. 'steer' is injected
377
+ // at the next agent step: right after the running research tool returns its report
378
+ // into chat, the agent acts on it. (The input handler ignores source 'extension',
379
+ // so this is not re-captured into the research queue.)
380
+ let sent = 0;
381
+ for (const msg of popped) {
382
+ try {
383
+ await pi.sendUserMessage(msg.text, { deliverAs: 'steer' });
384
+ sent++;
385
+ } catch (err) {
386
+ // Never lose the user's words: restore the message to queued (still poppable).
387
+ logger.warn('[pi-research] Failed to forward popped steering to pi:', err);
388
+ requeuePoppedMessage(piSessionId, msg.id);
389
+ }
390
+ }
391
+
392
+ if (ctx.hasUI) {
393
+ if (sent > 0) {
394
+ ctx.ui.notify(`Sent ${sent} steering message(s) to pi.`, 'info');
395
+ } else {
396
+ ctx.ui.notify('Could not send steering to pi — left it queued.', 'warning');
397
+ }
398
+ }
399
+ },
400
+ });
401
+
402
+ // /research <query> — direct quick research, no LLM turn.
403
+ pi.registerCommand('research', {
404
+ description: 'Web research a query',
405
+ handler: async (args, ctx) => {
406
+ const text = args.trim();
407
+ if (!text) return;
408
+
409
+ try {
410
+ const config = getConfig(ctx.cwd, 'pi');
411
+
412
+ const result = await researchTool.execute(
413
+ randomUUID(),
414
+ { query: text, depth: config.DEFAULT_RESEARCH_DEPTH },
415
+ ctx.signal,
416
+ undefined,
417
+ ctx,
418
+ );
419
+
420
+ const output = extractResultText(result);
421
+
422
+ try {
423
+ pi.sendMessage({
424
+ customType: 'research-result',
425
+ content: output,
426
+ display: true,
427
+ details: {
428
+ totalTokens: (result.details as ResearchResultDetails)?.totalTokens ?? 0,
429
+ researchId: (result.details as any)?.researchId
430
+ },
431
+ });
432
+
433
+ if (ctx.hasUI) {
434
+ ctx.ui.notify('Research finished.', 'info');
435
+ }
436
+ } catch (deliverErr) {
437
+ // ctx went stale (the session was closed mid-run) — there's no live session to
438
+ // deliver the result to. Not a failure of the research itself.
439
+ logger.debug('[pi-research] could not deliver research result (session closed):', deliverErr);
440
+ }
441
+ } catch (error: unknown) {
442
+ const message = error instanceof Error ? error.message : String(error);
443
+ // A quit / session replacement mid-run aborts ctx.signal and invalidates ctx; the run
444
+ // was cancelled, not broken, and any sendMessage/ui call below would itself throw
445
+ // "ctx is stale after session replacement or reload". There is no live session to
446
+ // report to, so exit quietly instead of logging a failure and re-throwing.
447
+ if (ctx.signal?.aborted || /ctx is stale after session replacement/i.test(message)) {
448
+ logger.debug('[pi-research] /research command ended early (session closed mid-run); skipping result delivery');
449
+ return;
450
+ }
451
+ logger.error('[pi-research] /research command failed:', error);
452
+
453
+ try {
454
+ pi.sendMessage({
455
+ customType: 'research-result',
456
+ content: `**Research failed**\n\n${message}`,
457
+ display: true,
458
+ details: { error: message },
459
+ });
460
+
461
+ if (ctx.hasUI) {
462
+ ctx.ui.notify(`Research failed: ${message}`, 'error');
463
+ }
464
+ } catch (deliverErr) {
465
+ logger.debug('[pi-research] could not deliver failure notice (session closed):', deliverErr);
466
+ }
467
+ }
468
+ },
44
469
  });
45
470
 
46
- // Notify user of Docker status and log file location when starting a new session
47
- pi.on('session_start', async (_event, ctx) => {
48
- // Skip Docker check during install to avoid errors on fresh systems
49
- // Docker will be checked when research tool is first used
50
- if (!process.env['PI_INSTALL_MODE']) {
51
- const dockerCheck = await checkDockerAvailability();
52
- if (!dockerCheck.running) {
53
- ctx.ui.notify(`pi-research: ${dockerCheck.error}`, 'warning');
471
+ // /research-config — consolidated research configuration and management command
472
+ pi.registerCommand('research-config', {
473
+ description: 'Research configuration and management (health, errors, knowledge, settings, metrics)',
474
+ handler: async (args, ctx: ExtensionContext) => {
475
+ await handleResearchConfigCommand(args, ctx, pi);
476
+ },
477
+ });
478
+
479
+ // /knowledge-store <query> — search the local knowledge store for previously
480
+ // researched findings (synthesised answer). Query-only: the store auto-manages its
481
+ // own compaction/pruning after runs, so there is no manual maintenance subcommand.
482
+ pi.registerCommand('knowledge-store', {
483
+ description: 'Search the local research knowledge store for a query',
484
+ handler: async (args, ctx: ExtensionContext) => {
485
+ const text = args.trim();
486
+ if (!text) return;
487
+
488
+ // With KNOWLEDGE_STORE_MODE='none' the store is disabled and a search can only miss.
489
+ // Point the user at the toggle instead. Read LIVE config so a /research-config change
490
+ // applies without a Pi restart (getConfig is re-read after research-config's resetConfig).
491
+ // Key the gate off the SAME cwd the tool below resolves its config from (ctx.cwd, not
492
+ // the activation pi.cwd) so the "disabled?" check and the tool never disagree on the
493
+ // mode when the session cwd differs from the activation directory.
494
+ if (getConfig(ctx.cwd ?? (pi as any).cwd, 'pi').KNOWLEDGE_STORE_MODE === 'none') {
495
+ const msg = 'The knowledge store is disabled (Knowledge Mode = none). Enable it via /research-config.';
496
+ if (ctx.hasUI) ctx.ui.notify(msg, 'warning');
497
+ else pi.sendMessage({ customType: 'knowledge-store', content: msg, display: true });
498
+ return;
499
+ }
500
+
501
+ try {
502
+ const result = await researchKnowledgeSearchTool.execute(
503
+ randomUUID(),
504
+ { queries: [text] },
505
+ ctx.signal,
506
+ undefined,
507
+ ctx,
508
+ );
509
+
510
+ const output = extractResultText(result);
511
+ try {
512
+ pi.sendMessage({
513
+ customType: 'knowledge-store',
514
+ content: output,
515
+ display: true,
516
+ details: { researchId: (result.details as any)?.researchId },
517
+ });
518
+ if (ctx.hasUI) ctx.ui.notify('Knowledge search finished.', 'info');
519
+ } catch (deliverErr) {
520
+ logger.debug('[pi-research] could not deliver knowledge-store result (session closed):', deliverErr);
521
+ }
522
+ } catch (error: unknown) {
523
+ const message = error instanceof Error ? error.message : String(error);
524
+ // A quit/session replacement mid-run aborts ctx.signal and invalidates ctx —
525
+ // the search was cancelled, not broken. Exit quietly (mirrors /research).
526
+ if (ctx.signal?.aborted || /ctx is stale after session replacement/i.test(message)) {
527
+ logger.debug('[pi-research] /knowledge-store command ended early (session closed mid-run)');
528
+ return;
529
+ }
530
+ logger.error('[pi-research] /knowledge-store command failed:', error);
531
+ try {
532
+ pi.sendMessage({
533
+ customType: 'knowledge-store',
534
+ content: `**Knowledge store search failed**\n\n${message}`,
535
+ display: true,
536
+ details: { error: message },
537
+ });
538
+ if (ctx.hasUI) ctx.ui.notify(`Knowledge store search failed: ${message}`, 'error');
539
+ } catch (deliverErr) {
540
+ logger.debug('[pi-research] could not deliver knowledge-store failure notice (session closed):', deliverErr);
541
+ }
54
542
  }
543
+ },
544
+ });
545
+
546
+ // JIT Prompt Injection (Simplified & Non-Intrusive)
547
+ pi.on('before_agent_start', async (event: any, ctx: ExtensionContext) => {
548
+ if (event.streamingBehavior === 'steer') {
549
+ return { systemPrompt: event.systemPrompt };
550
+ }
551
+
552
+ if (ctx.mode === 'tui' && ctx.hasUI) {
553
+ initGlobalTuiController(ctx.ui, (ctx as ExtendedExtensionContext).sessionId);
554
+ }
555
+
556
+ // Researcher sub-agent sessions already carry the authoritative steering copy:
557
+ // the executor (deep) and quick orchestrator inject "ADDITIONAL USER GUIDANCE"/
558
+ // "ADDITIONAL CONSIDERATIONS" into the researcher system prompt themselves.
559
+ // Appending the same messages again here delivered every steering message
560
+ // TWICE to every researcher — return the prompt untouched. (The coordinator/
561
+ // evaluator never pass through this hook at all: they run via completeSimple,
562
+ // not an agent session, and get steering from planning-service directly.)
563
+ const isResearcher = event.systemPrompt?.includes('RESEARCHER_AGENT_MARKER');
564
+ if (isResearcher) {
565
+ return { systemPrompt: event.systemPrompt };
55
566
  }
56
567
 
57
- if (isVerboseFromEnv()) {
58
- ctx.ui.notify(`pi-research: debug log → ${getDefaultDebugLogPathTemplate()}`, 'info');
568
+ const eCtx = ctx as ExtendedExtensionContext;
569
+ const normalizedSid = normalizeSessionId(eCtx.sessionId);
570
+ const steeringMessages = getSteeringMessages(normalizedSid);
571
+ let injectedSystemPrompt = event.systemPrompt;
572
+
573
+ const activeCount = getActiveSessionCount();
574
+ const hasSteeringMessages = steeringMessages.length > 0;
575
+ const shouldInjectSteering = hasSteeringMessages && activeCount > 0;
576
+
577
+ if (shouldInjectSteering) {
578
+ injectedSystemPrompt += '\n\n### ADDITIONAL CONSIDERATIONS\n' +
579
+ 'The user has provided the following additional considerations for this task:\n' +
580
+ steeringMessages.map(m => `- ${m.text}`).join('\n');
581
+ }
582
+
583
+ const isResearchToolAvailable = !event.systemPromptOptions || event.systemPromptOptions.selectedTools?.includes(researchTool.name);
584
+ // Gate the KNOWLEDGE SEARCH prompt block on LIVE Knowledge Mode (not the startup tool
585
+ // binding, which is now always registered) so enabling/disabling via /research-config
586
+ // applies without a Pi restart. The tool must also be selected for this turn.
587
+ // Key the gate off the SAME cwd the tool and /knowledge-store command resolve config
588
+ // from (ctx.cwd, not the activation pi.cwd) — KNOWLEDGE_STORE_MODE is per-directory, so
589
+ // when the session cwd differs from the activation dir the prompt block and the tool
590
+ // must not disagree on whether the store is enabled.
591
+ const knowledgeModeEnabled = getConfig(ctx.cwd ?? (pi as any).cwd, 'pi').KNOWLEDGE_STORE_MODE !== 'none';
592
+ const isKnowledgeSearchAvailable = knowledgeModeEnabled &&
593
+ (!event.systemPromptOptions || event.systemPromptOptions.selectedTools?.includes(researchKnowledgeSearchTool.name));
594
+
595
+ if (isResearchToolAvailable || isKnowledgeSearchAvailable) {
596
+ let researchPrompt = loadPrompt('research-tool-usage')
597
+ .replace('{{max_team_size_l1}}', MAX_TEAM_SIZE_LEVEL_1.toString())
598
+ .replace('{{max_team_size_l2}}', MAX_TEAM_SIZE_LEVEL_2.toString())
599
+ .replace('{{max_team_size_l3}}', MAX_TEAM_SIZE_LEVEL_3.toString());
600
+
601
+ if (!isKnowledgeSearchAvailable) {
602
+ // Strip the whole KNOWLEDGE SEARCH block when no store is enabled. Match from
603
+ // the `**KNOWLEDGE SEARCH` header (the title may carry a suffix, e.g.
604
+ // "— MANDATORY FIRST STEP") through its terminating `---` rule.
605
+ researchPrompt = researchPrompt.replace(/\n\*\*KNOWLEDGE SEARCH[\s\S]*?\n---\n/m, '\n---\n');
606
+ }
607
+
608
+ return {
609
+ systemPrompt: injectedSystemPrompt + '\n\n' + researchPrompt
610
+ };
59
611
  }
612
+
613
+ return { systemPrompt: injectedSystemPrompt };
60
614
  });
61
615
 
62
- // Register research tool
63
- pi.registerTool(createResearchTool());
616
+ // Monitor provider responses for diagnostics
617
+ pi.on('after_provider_response', async (event: any, ctx: any) => {
618
+ const { status, headers } = event;
64
619
 
65
- // Append research tool usage instructions to the system prompt
66
- pi.on('before_agent_start', async (event) => {
67
- const researchPrompt = loadPrompt('research-tool-usage');
68
- return {
69
- systemPrompt: event.systemPrompt + '\n\n' + researchPrompt
70
- };
620
+ if (status >= 500) {
621
+ logger.warn(`[pi-research] Provider server error: ${status}`, { headers });
622
+ } else if (status === 429) {
623
+ const retryAfter = headers?.['retry-after'];
624
+ logger.warn(`[pi-research] Rate limited by provider`, { retryAfter });
625
+ if (retryAfter && ctx.hasUI) {
626
+ ctx.ui.notify(`Rate limited. Retry in ${retryAfter}s.`, 'warning');
627
+ }
628
+ } else if (status >= 400) {
629
+ logger.warn(`[pi-research] Provider error: ${status}`, { headers });
630
+ }
71
631
  });
72
632
 
633
+ // Log health status at startup (non-blocking). Track + unref the handle: unref() so a
634
+ // short `pi -p` run isn't held open up to 2s waiting on it, and clear it on shutdown so a
635
+ // fast reload/deactivate doesn't leave it pending to fire against disposed services.
636
+ const healthTimer = setTimeout(async () => {
637
+ try {
638
+ const health = await healthRegistry.runAll();
639
+ const statusIcon = health.status === 'healthy' ? '[OK]' :
640
+ health.status === 'degraded' ? '[WARN]' : '[ERROR]';
641
+ const failedComponents = health.components.filter((c: any) => !c.healthy).map((c: any) => c.component).join(', ');
642
+
643
+ if (health.status === 'healthy') {
644
+ logger.log(`[pi-research] ${statusIcon} System health check passed. All components operational.`);
645
+ } else {
646
+ logger.warn(`[pi-research] ${statusIcon} System health check: ${health.status}. Failed: ${failedComponents || 'none'}`);
647
+ }
648
+ } catch (error) {
649
+ logger.warn('[pi-research] Startup health check failed (non-fatal):', error);
650
+ }
651
+ }, 2000);
652
+ if (healthTimer.unref) healthTimer.unref();
653
+ shutdownManager.register(async () => clearTimeout(healthTimer));
654
+
73
655
  logger.log('[pi-research] Extension loaded');
74
656
  }
657
+
658
+ /**
659
+ * Extension Deactivation
660
+ */
661
+ export async function deactivate(): Promise<void> {
662
+ await shutdownManager.runCleanup('extension-deactivate');
663
+ }