@lincoln504/pi-research 0.1.13 → 1.0.1

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 +55 -138
  3. package/dist/cli.mjs +23375 -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 +130 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +211 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +122 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +66 -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 +1195 -0
  30. package/src/config.ts +1032 -155
  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 +258 -11
  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 -212
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -644
  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
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Shared metrics summary formatting.
3
+ *
4
+ * Used by BOTH:
5
+ * - The final research result (appendResearchSummary in research-tool-definition.ts)
6
+ * - The session metrics view (showMetricsAction in research-config.ts)
7
+ *
8
+ * This ensures a consistent structure between the two views:
9
+ * - Plain lists, no tables
10
+ * - Counts only, no percentages
11
+ * - Human-friendly labels, never raw Prometheus-style metric names
12
+ * - Subtly impressive: emphasizes volume of work done (sources analyzed,
13
+ * links discovered, pages scraped, etc.)
14
+ */
15
+
16
+ import { formatTimeAgo, formatDuration } from './text-utils.js';
17
+ import type { IMetricsSnapshot } from './metrics.js';
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface ResearchStats {
24
+ /** Total research duration in milliseconds. */
25
+ durationMs: number;
26
+ /** Complexity level: 0 (quick), 1, 2, or 3. */
27
+ complexity: number;
28
+ /** Number of researchers that were launched. */
29
+ researchersLaunched: number;
30
+ /** Number of rounds completed. */
31
+ roundsCompleted: number;
32
+ /** Number of search queries submitted. */
33
+ searchQueries: number;
34
+ /** Number of unique URLs discovered via search. */
35
+ urlsDiscovered: number;
36
+ /** Number of URLs successfully scraped via lightweight fetch. */
37
+ fetchSuccess: number;
38
+ /** Number of URLs successfully scraped via browser (stealth fallback). */
39
+ browserSuccess: number;
40
+ /** Number of URLs that fell back from fetch to browser. */
41
+ browserFallbacks: number;
42
+ /** Number of URLs successfully scraped/analyzed (fetch + browser). */
43
+ urlsAnalyzed: number;
44
+ /** Number of URLs that failed to scrape (blocked, stub, HTTP error, timeout). */
45
+ urlsFailed: number;
46
+ /** Genuine engine faults (errored/timed-out researcher attempts). Excludes
47
+ * expected remote scrape unavailability, which is reported via urlsFailed. */
48
+ errors: number;
49
+ /** Total LLM tokens consumed. */
50
+ tokens: number;
51
+ /** Total cost in USD across all LLM calls. */
52
+ cost: number;
53
+ /** Tool usage counts. */
54
+ toolUsage: {
55
+ searches: number;
56
+ scrapes: number;
57
+ securitySearches: number;
58
+ stackexchangeQueries: number;
59
+ knowledgeLookups: number;
60
+ };
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Internal helpers
65
+ // ---------------------------------------------------------------------------
66
+
67
+ /**
68
+ * Extract a clean numeric value from a metrics snapshot counter map.
69
+ * Sums all label-variants of the given counter base name.
70
+ *
71
+ * Example: sumCounter(snapshot.counters, 'scrape_results_total')
72
+ * sums scrape_results_total{outcome="fetch_success"} + ...browser_success + ...total_failure
73
+ */
74
+ export function sumCounter(
75
+ counters: Record<string, number>,
76
+ baseName: string
77
+ ): number {
78
+ let total = 0;
79
+ for (const [key, value] of Object.entries(counters)) {
80
+ // Match exact name or name followed by `{labels}`
81
+ if (key === baseName || key.startsWith(baseName + '{')) {
82
+ total += value;
83
+ }
84
+ }
85
+ return total;
86
+ }
87
+
88
+ /**
89
+ * Extract a specific labeled counter value.
90
+ *
91
+ * Example: getLabeledCounter(counters, 'scrape_results_total', { outcome: 'fetch_success' })
92
+ */
93
+ function getLabeledCounter(
94
+ counters: Record<string, number>,
95
+ baseName: string,
96
+ labels: Record<string, string>
97
+ ): number {
98
+ // Build the label suffix to match: {key1="val1",key2="val2"}
99
+ const labelParts = Object.entries(labels).map(([k, v]) => `${k}="${v}"`);
100
+ const suffix = `{${labelParts.join(',')}}`;
101
+
102
+ // Also try without the base name prefix repetition
103
+ for (const [key, value] of Object.entries(counters)) {
104
+ if (key === `${baseName}${suffix}`) {
105
+ return value;
106
+ }
107
+ }
108
+ return 0;
109
+ }
110
+
111
+ // formatDuration and formatTimeAgo are imported from text-utils.ts
112
+
113
+ /**
114
+ * Format a token count with thousands separators (e.g., "12,345").
115
+ */
116
+ export function formatTokens(tokens: number): string {
117
+ if (tokens < 1000) return String(tokens);
118
+ return tokens.toLocaleString('en-US');
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Run stats extraction from snapshot
123
+ // ---------------------------------------------------------------------------
124
+
125
+ /**
126
+ * Extract human-relevant research stats from a run metrics snapshot.
127
+ * Returns null if the snapshot has no meaningful data.
128
+ */
129
+ export function extractRunStats(snapshot: IMetricsSnapshot): ResearchStats | null {
130
+ const counters = snapshot.counters || {};
131
+ const histograms = snapshot.histograms || {};
132
+
133
+ // Researchers launched
134
+ const researchersLaunched = sumCounter(counters, 'researchers_launched_total');
135
+
136
+ // Search queries — combine browser search and regular search
137
+ const browserSearchQueries = sumCounter(counters, 'browser_search_queries_total');
138
+ const searchQueries = sumCounter(counters, 'search_queries_total') + browserSearchQueries;
139
+
140
+ // URLs discovered via search
141
+ const urlsDiscovered = sumCounter(counters, 'browser_search_results_total') +
142
+ sumCounter(counters, 'browser_search_unique_urls_total');
143
+
144
+ // URLs scraped — outcomes from scrape_results_total
145
+ const fetchSuccess = getLabeledCounter(counters, 'scrape_results_total', { outcome: 'fetch_success' });
146
+ const browserSuccess = getLabeledCounter(counters, 'scrape_results_total', { outcome: 'browser_success' });
147
+ const totalFailure = getLabeledCounter(counters, 'scrape_results_total', { outcome: 'total_failure' });
148
+ const browserFallbacks = sumCounter(counters, 'scrape_layer_fallbacks_total');
149
+ const urlsAnalyzed = fetchSuccess + browserSuccess;
150
+ const urlsFailed = totalFailure;
151
+
152
+ // Tool usage
153
+ const toolUsage = {
154
+ searches: sumCounter(counters, 'tool_search_calls_total'),
155
+ scrapes: sumCounter(counters, 'tool_scrape_calls_total'),
156
+ securitySearches: sumCounter(counters, 'tool_security_search_calls_total'),
157
+ stackexchangeQueries: sumCounter(counters, 'stackexchange_requests_total'),
158
+ knowledgeLookups: sumCounter(counters, 'research_knowledge_search_total'),
159
+ };
160
+
161
+ // Errors — genuine engine faults only (a researcher attempt that threw or timed
162
+ // out; llm_api_errors_total is summed for forward-compat but no path emits it yet).
163
+ // Deliberately EXCLUDES scrape_errors_total: a URL blocked by bot-protection,
164
+ // returning a stub, or answering 4xx/5xx is an EXPECTED outcome of scraping the
165
+ // open web — it is already surfaced to the user as "not scraped" (urlsFailed,
166
+ // from the once-per-URL total_failure outcome). Counting scrape_errors_total here
167
+ // both mislabels remote unavailability as an engine error AND over-counts, since
168
+ // it is incremented several times along one URL's fetch→browser fallback chain.
169
+ // A researcher whose sources were all blocked does NOT increment
170
+ // researcher_errors_total (it takes the ungrounded path), so blocked runs
171
+ // correctly show 0 errors here.
172
+ const errors = sumCounter(counters, 'researcher_errors_total') +
173
+ sumCounter(counters, 'llm_api_errors_total');
174
+
175
+ // Tokens — sum all label variants
176
+ const tokens = sumCounter(counters, 'llm_tokens_total');
177
+
178
+ // Cost — sum all label variants
179
+ const cost = sumCounter(counters, 'llm_cost_total');
180
+
181
+ // Duration from histogram
182
+ const durationMs = histograms['research_session_duration_ms{mode="deep",complexity="1",status="success"}']?.max ||
183
+ histograms['research_session_duration_ms{mode="deep",complexity="2",status="success"}']?.max ||
184
+ histograms['research_session_duration_ms{mode="deep",complexity="3",status="success"}']?.max ||
185
+ histograms['research_session_duration_ms{mode="quick",complexity="0",status="success"}']?.max ||
186
+ 0;
187
+
188
+ // Rounds completed — evaluator runs give us the round count
189
+ const roundsCompleted = sumCounter(counters, 'evaluator_runs_total');
190
+
191
+ // Complexity
192
+ let complexity = 1;
193
+ for (const key of Object.keys(counters)) {
194
+ if (key.startsWith('research_sessions_total{')) {
195
+ const match = key.match(/complexity="(\d+)"/);
196
+ if (match?.[1]) complexity = parseInt(match[1], 10);
197
+ break;
198
+ }
199
+ }
200
+
201
+ // If nothing meaningful happened, return null
202
+ if (researchersLaunched === 0 && searchQueries === 0 && urlsAnalyzed === 0 && tokens === 0) {
203
+ return null;
204
+ }
205
+
206
+ return {
207
+ durationMs,
208
+ complexity,
209
+ researchersLaunched,
210
+ roundsCompleted,
211
+ searchQueries,
212
+ urlsDiscovered,
213
+ fetchSuccess,
214
+ browserSuccess,
215
+ browserFallbacks,
216
+ urlsAnalyzed,
217
+ urlsFailed,
218
+ errors,
219
+ tokens,
220
+ cost,
221
+ toolUsage,
222
+ };
223
+ }
224
+
225
+ // ---------------------------------------------------------------------------
226
+ // Final research result summary
227
+ // ---------------------------------------------------------------------------
228
+
229
+ /**
230
+ * Build a concise human-readable summary for the END of a research result.
231
+ *
232
+ * Design principles:
233
+ * - One line for work done, one line for cost/resources
234
+ * - Errors collapsed to a single sentence (details belong in the log)
235
+ * - No tables, percentages, scrape-internal breakdowns, or diagnostic dumps
236
+ * - Consistent single bolding on key numbers
237
+ *
238
+ * Returns empty string if no meaningful data.
239
+ */
240
+ export function buildResearchSummary(stats: ResearchStats): string {
241
+ const lines: string[] = [];
242
+
243
+ lines.push('### Research Summary');
244
+
245
+ // --- Work line: researchers, rounds, searches, sources ---
246
+ const workParts: string[] = [];
247
+
248
+ if (stats.researchersLaunched > 0) {
249
+ workParts.push(`**${stats.researchersLaunched}** researcher${stats.researchersLaunched > 1 ? 's' : ''}`);
250
+ }
251
+ if (stats.roundsCompleted > 1) {
252
+ workParts.push(`**${stats.roundsCompleted}** rounds`);
253
+ }
254
+ if (stats.searchQueries > 0) {
255
+ workParts.push(`**${stats.searchQueries}** searches`);
256
+ }
257
+ if (stats.urlsAnalyzed > 0) {
258
+ let sourcesLabel: string;
259
+ if (stats.urlsDiscovered > stats.urlsAnalyzed) {
260
+ sourcesLabel = `**${stats.urlsAnalyzed}** of **${stats.urlsDiscovered}** sources analyzed`;
261
+ } else {
262
+ sourcesLabel = `**${stats.urlsAnalyzed}** sources analyzed`;
263
+ }
264
+ // Show per-tier breakdown when browser fallback was used or some URLs failed
265
+ if (stats.browserSuccess > 0 || stats.urlsFailed > 0) {
266
+ const tierParts: string[] = [];
267
+ if (stats.fetchSuccess > 0) tierParts.push(`${stats.fetchSuccess} fetch`);
268
+ if (stats.browserSuccess > 0) tierParts.push(`${stats.browserSuccess} browser`);
269
+ if (stats.urlsFailed > 0) tierParts.push(`${stats.urlsFailed} not scraped`);
270
+ sourcesLabel += ` (${tierParts.join(', ')})`;
271
+ }
272
+ workParts.push(sourcesLabel);
273
+ } else if (stats.urlsDiscovered > 0) {
274
+ workParts.push(`**${stats.urlsDiscovered}** sources discovered`);
275
+ }
276
+
277
+ if (workParts.length > 0) {
278
+ lines.push(workParts.join(' · '));
279
+ }
280
+
281
+ // --- Resource line: tokens, cost, duration ---
282
+ const resourceParts: string[] = [];
283
+ if (stats.tokens > 0) {
284
+ resourceParts.push(`**${formatTokens(stats.tokens)}** tokens`);
285
+ }
286
+ if (stats.cost > 0) {
287
+ const costStr = stats.cost < 0.01 ? '<$0.01' : `$${stats.cost.toFixed(2)}`;
288
+ resourceParts.push(`**${costStr}**`);
289
+ }
290
+ if (stats.durationMs > 0) {
291
+ resourceParts.push(`**${formatDuration(stats.durationMs)}**`);
292
+ }
293
+ if (resourceParts.length > 0) {
294
+ lines.push(resourceParts.join(' · '));
295
+ }
296
+
297
+ // --- Footnote: genuine engine errors only. Blocked/unavailable sources are NOT
298
+ // errors — they appear as "N not scraped" in the tier breakdown above. ---
299
+ if (stats.errors > 0) {
300
+ lines.push(`*${stats.errors} error${stats.errors > 1 ? 's' : ''} encountered.*`);
301
+ }
302
+
303
+ return lines.join('\n');
304
+ }
305
+
306
+ // ---------------------------------------------------------------------------
307
+ // Session metrics view (config TUI)
308
+ // ---------------------------------------------------------------------------
309
+
310
+ export interface SessionStats {
311
+ sessionStartedAt: number;
312
+ totalRuns: number;
313
+ successfulRuns: number;
314
+ failedRuns: number;
315
+ totalDurationMs: number;
316
+ totalSourcesAnalyzed: number;
317
+ totalUrlsDiscovered: number;
318
+ totalSearchQueries: number;
319
+ totalTokens: number;
320
+ totalToolUsage: {
321
+ searches: number;
322
+ scrapes: number;
323
+ securitySearches: number;
324
+ stackexchangeQueries: number;
325
+ knowledgeLookups: number;
326
+ };
327
+ }
328
+
329
+ /**
330
+ * Aggregate stats across all runs in the session history.
331
+ */
332
+ export function aggregateSessionStats(
333
+ runHistory: ReadonlyArray<{ durationMs: number; status: string; snapshot: IMetricsSnapshot }>
334
+ ): SessionStats {
335
+ let successfulRuns = 0;
336
+ let failedRuns = 0;
337
+ let totalDurationMs = 0;
338
+ let totalSourcesAnalyzed = 0;
339
+ let totalUrlsDiscovered = 0;
340
+ let totalSearchQueries = 0;
341
+ let totalTokens = 0;
342
+ const totalToolUsage = {
343
+ searches: 0,
344
+ scrapes: 0,
345
+ securitySearches: 0,
346
+ stackexchangeQueries: 0,
347
+ knowledgeLookups: 0,
348
+ };
349
+
350
+ for (const run of runHistory) {
351
+ if (run.status === 'success') successfulRuns++;
352
+ else if (run.status === 'error') failedRuns++;
353
+
354
+ totalDurationMs += run.durationMs || 0;
355
+
356
+ const stats = extractRunStats(run.snapshot);
357
+ if (stats) {
358
+ totalSourcesAnalyzed += stats.urlsAnalyzed;
359
+ totalUrlsDiscovered += stats.urlsDiscovered;
360
+ totalSearchQueries += stats.searchQueries;
361
+ totalTokens += stats.tokens;
362
+ totalToolUsage.searches += stats.toolUsage.searches;
363
+ totalToolUsage.scrapes += stats.toolUsage.scrapes;
364
+ totalToolUsage.securitySearches += stats.toolUsage.securitySearches;
365
+ totalToolUsage.stackexchangeQueries += stats.toolUsage.stackexchangeQueries;
366
+ totalToolUsage.knowledgeLookups += stats.toolUsage.knowledgeLookups;
367
+ }
368
+ }
369
+
370
+ return {
371
+ sessionStartedAt: 0, // Set by caller
372
+ totalRuns: runHistory.length,
373
+ successfulRuns,
374
+ failedRuns,
375
+ totalDurationMs,
376
+ totalSourcesAnalyzed,
377
+ totalUrlsDiscovered,
378
+ totalSearchQueries,
379
+ totalTokens,
380
+ totalToolUsage,
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Build the session-level overview section.
386
+ */
387
+ export function buildSessionOverview(stats: SessionStats): string {
388
+ const lines: string[] = [];
389
+
390
+ lines.push('### Session Overview');
391
+
392
+ const overviewParts: string[] = [];
393
+ overviewParts.push(`**${stats.totalRuns}** research run${stats.totalRuns !== 1 ? 's' : ''}`);
394
+
395
+ if (stats.totalSourcesAnalyzed > 0) {
396
+ overviewParts.push(`**${stats.totalSourcesAnalyzed}** sources analyzed`);
397
+ }
398
+ if (stats.totalUrlsDiscovered > 0) {
399
+ overviewParts.push(`**${stats.totalUrlsDiscovered}** URLs discovered`);
400
+ }
401
+ if (stats.totalSearchQueries > 0) {
402
+ overviewParts.push(`**${stats.totalSearchQueries}** search queries`);
403
+ }
404
+ if (stats.totalTokens > 0) {
405
+ overviewParts.push(`**${formatTokens(stats.totalTokens)}** tokens`);
406
+ }
407
+ if (stats.totalDurationMs > 0) {
408
+ overviewParts.push(`**${formatDuration(stats.totalDurationMs)}** total research time`);
409
+ }
410
+
411
+ lines.push(overviewParts.join(' · '));
412
+
413
+ // Tool usage line
414
+ const toolParts: string[] = [];
415
+ if (stats.totalToolUsage.searches > 0) toolParts.push(`${stats.totalToolUsage.searches} searches`);
416
+ if (stats.totalToolUsage.scrapes > 0) toolParts.push(`${stats.totalToolUsage.scrapes} scrapes`);
417
+ if (stats.totalToolUsage.securitySearches > 0) toolParts.push(`${stats.totalToolUsage.securitySearches} security lookups`);
418
+ if (stats.totalToolUsage.stackexchangeQueries > 0) toolParts.push(`${stats.totalToolUsage.stackexchangeQueries} StackExchange queries`);
419
+ if (stats.totalToolUsage.knowledgeLookups > 0) toolParts.push(`${stats.totalToolUsage.knowledgeLookups} knowledge store queries`);
420
+ if (toolParts.length > 0) {
421
+ lines.push(`Tools used: ${toolParts.join(' · ')}`);
422
+ }
423
+
424
+ return lines.join('\n');
425
+ }
426
+
427
+ /**
428
+ * Build a compact one-line summary for a prior run.
429
+ */
430
+ export function buildRunCompactLine(run: {
431
+ runId: string;
432
+ durationMs: number;
433
+ status: string;
434
+ completedAt: number;
435
+ snapshot: IMetricsSnapshot;
436
+ }): string {
437
+ const icon = run.status === 'success' ? '[ok]'
438
+ : run.status === 'cancelled' ? '[cancelled]'
439
+ : '[failed]';
440
+
441
+ const stats = extractRunStats(run.snapshot);
442
+ const extraParts: string[] = [];
443
+
444
+ if (stats) {
445
+ if (stats.urlsAnalyzed > 0) {
446
+ extraParts.push(`${stats.urlsAnalyzed} source${stats.urlsAnalyzed !== 1 ? 's' : ''}`);
447
+ }
448
+ if (stats.researchersLaunched > 0) {
449
+ extraParts.push(`${stats.researchersLaunched} researcher${stats.researchersLaunched !== 1 ? 's' : ''}`);
450
+ }
451
+ }
452
+
453
+ const extra = extraParts.length > 0 ? ` · ${extraParts.join(', ')}` : '';
454
+ const ago = formatTimeAgo(new Date(run.completedAt).toISOString());
455
+
456
+ return `- ${icon} \`${run.runId.slice(0, 8)}\` ${formatDuration(run.durationMs)}${extra} — ${ago}`;
457
+ }
458
+
459
+ export { formatDuration, formatTimeAgo };