@peakinfer/cli 1.0.133

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 (367) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/.env.example +6 -0
  3. package/.github/workflows/peakinfer.yml +64 -0
  4. package/CHANGELOG.md +31 -0
  5. package/LICENSE +190 -0
  6. package/README.md +335 -0
  7. package/data/inferencemax.json +274 -0
  8. package/dist/agent-analyzer.d.ts +45 -0
  9. package/dist/agent-analyzer.d.ts.map +1 -0
  10. package/dist/agent-analyzer.js +374 -0
  11. package/dist/agent-analyzer.js.map +1 -0
  12. package/dist/agent.d.ts +76 -0
  13. package/dist/agent.d.ts.map +1 -0
  14. package/dist/agent.js +965 -0
  15. package/dist/agent.js.map +1 -0
  16. package/dist/agents/correlation-analyzer.d.ts +34 -0
  17. package/dist/agents/correlation-analyzer.d.ts.map +1 -0
  18. package/dist/agents/correlation-analyzer.js +261 -0
  19. package/dist/agents/correlation-analyzer.js.map +1 -0
  20. package/dist/agents/index.d.ts +91 -0
  21. package/dist/agents/index.d.ts.map +1 -0
  22. package/dist/agents/index.js +111 -0
  23. package/dist/agents/index.js.map +1 -0
  24. package/dist/agents/runtime-analyzer.d.ts +38 -0
  25. package/dist/agents/runtime-analyzer.d.ts.map +1 -0
  26. package/dist/agents/runtime-analyzer.js +244 -0
  27. package/dist/agents/runtime-analyzer.js.map +1 -0
  28. package/dist/analysis-types.d.ts +500 -0
  29. package/dist/analysis-types.d.ts.map +1 -0
  30. package/dist/analysis-types.js +11 -0
  31. package/dist/analysis-types.js.map +1 -0
  32. package/dist/analytics.d.ts +25 -0
  33. package/dist/analytics.d.ts.map +1 -0
  34. package/dist/analytics.js +94 -0
  35. package/dist/analytics.js.map +1 -0
  36. package/dist/analyzer.d.ts +48 -0
  37. package/dist/analyzer.d.ts.map +1 -0
  38. package/dist/analyzer.js +547 -0
  39. package/dist/analyzer.js.map +1 -0
  40. package/dist/artifacts.d.ts +44 -0
  41. package/dist/artifacts.d.ts.map +1 -0
  42. package/dist/artifacts.js +165 -0
  43. package/dist/artifacts.js.map +1 -0
  44. package/dist/benchmarks/index.d.ts +88 -0
  45. package/dist/benchmarks/index.d.ts.map +1 -0
  46. package/dist/benchmarks/index.js +205 -0
  47. package/dist/benchmarks/index.js.map +1 -0
  48. package/dist/cli.d.ts +3 -0
  49. package/dist/cli.d.ts.map +1 -0
  50. package/dist/cli.js +427 -0
  51. package/dist/cli.js.map +1 -0
  52. package/dist/commands/ci.d.ts +19 -0
  53. package/dist/commands/ci.d.ts.map +1 -0
  54. package/dist/commands/ci.js +253 -0
  55. package/dist/commands/ci.js.map +1 -0
  56. package/dist/commands/config.d.ts +16 -0
  57. package/dist/commands/config.d.ts.map +1 -0
  58. package/dist/commands/config.js +249 -0
  59. package/dist/commands/config.js.map +1 -0
  60. package/dist/commands/demo.d.ts +15 -0
  61. package/dist/commands/demo.d.ts.map +1 -0
  62. package/dist/commands/demo.js +106 -0
  63. package/dist/commands/demo.js.map +1 -0
  64. package/dist/commands/export.d.ts +14 -0
  65. package/dist/commands/export.d.ts.map +1 -0
  66. package/dist/commands/export.js +209 -0
  67. package/dist/commands/export.js.map +1 -0
  68. package/dist/commands/history.d.ts +15 -0
  69. package/dist/commands/history.d.ts.map +1 -0
  70. package/dist/commands/history.js +389 -0
  71. package/dist/commands/history.js.map +1 -0
  72. package/dist/commands/template.d.ts +14 -0
  73. package/dist/commands/template.d.ts.map +1 -0
  74. package/dist/commands/template.js +341 -0
  75. package/dist/commands/template.js.map +1 -0
  76. package/dist/commands/validate-map.d.ts +12 -0
  77. package/dist/commands/validate-map.d.ts.map +1 -0
  78. package/dist/commands/validate-map.js +274 -0
  79. package/dist/commands/validate-map.js.map +1 -0
  80. package/dist/commands/whatif.d.ts +17 -0
  81. package/dist/commands/whatif.d.ts.map +1 -0
  82. package/dist/commands/whatif.js +206 -0
  83. package/dist/commands/whatif.js.map +1 -0
  84. package/dist/comparison.d.ts +38 -0
  85. package/dist/comparison.d.ts.map +1 -0
  86. package/dist/comparison.js +223 -0
  87. package/dist/comparison.js.map +1 -0
  88. package/dist/config.d.ts +42 -0
  89. package/dist/config.d.ts.map +1 -0
  90. package/dist/config.js +158 -0
  91. package/dist/config.js.map +1 -0
  92. package/dist/connectors/helicone.d.ts +9 -0
  93. package/dist/connectors/helicone.d.ts.map +1 -0
  94. package/dist/connectors/helicone.js +106 -0
  95. package/dist/connectors/helicone.js.map +1 -0
  96. package/dist/connectors/index.d.ts +37 -0
  97. package/dist/connectors/index.d.ts.map +1 -0
  98. package/dist/connectors/index.js +65 -0
  99. package/dist/connectors/index.js.map +1 -0
  100. package/dist/connectors/langsmith.d.ts +9 -0
  101. package/dist/connectors/langsmith.d.ts.map +1 -0
  102. package/dist/connectors/langsmith.js +122 -0
  103. package/dist/connectors/langsmith.js.map +1 -0
  104. package/dist/connectors/types.d.ts +83 -0
  105. package/dist/connectors/types.d.ts.map +1 -0
  106. package/dist/connectors/types.js +98 -0
  107. package/dist/connectors/types.js.map +1 -0
  108. package/dist/cost-estimator.d.ts +46 -0
  109. package/dist/cost-estimator.d.ts.map +1 -0
  110. package/dist/cost-estimator.js +104 -0
  111. package/dist/cost-estimator.js.map +1 -0
  112. package/dist/costs.d.ts +57 -0
  113. package/dist/costs.d.ts.map +1 -0
  114. package/dist/costs.js +251 -0
  115. package/dist/costs.js.map +1 -0
  116. package/dist/counterfactuals.d.ts +29 -0
  117. package/dist/counterfactuals.d.ts.map +1 -0
  118. package/dist/counterfactuals.js +448 -0
  119. package/dist/counterfactuals.js.map +1 -0
  120. package/dist/enhancement-prompts.d.ts +41 -0
  121. package/dist/enhancement-prompts.d.ts.map +1 -0
  122. package/dist/enhancement-prompts.js +88 -0
  123. package/dist/enhancement-prompts.js.map +1 -0
  124. package/dist/envelopes.d.ts +20 -0
  125. package/dist/envelopes.d.ts.map +1 -0
  126. package/dist/envelopes.js +790 -0
  127. package/dist/envelopes.js.map +1 -0
  128. package/dist/format-normalizer.d.ts +71 -0
  129. package/dist/format-normalizer.d.ts.map +1 -0
  130. package/dist/format-normalizer.js +1331 -0
  131. package/dist/format-normalizer.js.map +1 -0
  132. package/dist/history.d.ts +79 -0
  133. package/dist/history.d.ts.map +1 -0
  134. package/dist/history.js +313 -0
  135. package/dist/history.js.map +1 -0
  136. package/dist/html.d.ts +11 -0
  137. package/dist/html.d.ts.map +1 -0
  138. package/dist/html.js +463 -0
  139. package/dist/html.js.map +1 -0
  140. package/dist/impact.d.ts +42 -0
  141. package/dist/impact.d.ts.map +1 -0
  142. package/dist/impact.js +443 -0
  143. package/dist/impact.js.map +1 -0
  144. package/dist/index.d.ts +26 -0
  145. package/dist/index.d.ts.map +1 -0
  146. package/dist/index.js +34 -0
  147. package/dist/index.js.map +1 -0
  148. package/dist/insights.d.ts +5 -0
  149. package/dist/insights.d.ts.map +1 -0
  150. package/dist/insights.js +271 -0
  151. package/dist/insights.js.map +1 -0
  152. package/dist/joiner.d.ts +9 -0
  153. package/dist/joiner.d.ts.map +1 -0
  154. package/dist/joiner.js +247 -0
  155. package/dist/joiner.js.map +1 -0
  156. package/dist/orchestrator.d.ts +34 -0
  157. package/dist/orchestrator.d.ts.map +1 -0
  158. package/dist/orchestrator.js +827 -0
  159. package/dist/orchestrator.js.map +1 -0
  160. package/dist/pdf.d.ts +26 -0
  161. package/dist/pdf.d.ts.map +1 -0
  162. package/dist/pdf.js +84 -0
  163. package/dist/pdf.js.map +1 -0
  164. package/dist/prediction.d.ts +33 -0
  165. package/dist/prediction.d.ts.map +1 -0
  166. package/dist/prediction.js +316 -0
  167. package/dist/prediction.js.map +1 -0
  168. package/dist/prompts/loader.d.ts +38 -0
  169. package/dist/prompts/loader.d.ts.map +1 -0
  170. package/dist/prompts/loader.js +60 -0
  171. package/dist/prompts/loader.js.map +1 -0
  172. package/dist/renderer.d.ts +64 -0
  173. package/dist/renderer.d.ts.map +1 -0
  174. package/dist/renderer.js +923 -0
  175. package/dist/renderer.js.map +1 -0
  176. package/dist/runid.d.ts +57 -0
  177. package/dist/runid.d.ts.map +1 -0
  178. package/dist/runid.js +199 -0
  179. package/dist/runid.js.map +1 -0
  180. package/dist/runtime.d.ts +29 -0
  181. package/dist/runtime.d.ts.map +1 -0
  182. package/dist/runtime.js +366 -0
  183. package/dist/runtime.js.map +1 -0
  184. package/dist/scanner.d.ts +11 -0
  185. package/dist/scanner.d.ts.map +1 -0
  186. package/dist/scanner.js +426 -0
  187. package/dist/scanner.js.map +1 -0
  188. package/dist/templates.d.ts +120 -0
  189. package/dist/templates.d.ts.map +1 -0
  190. package/dist/templates.js +429 -0
  191. package/dist/templates.js.map +1 -0
  192. package/dist/tools/index.d.ts +153 -0
  193. package/dist/tools/index.d.ts.map +1 -0
  194. package/dist/tools/index.js +177 -0
  195. package/dist/tools/index.js.map +1 -0
  196. package/dist/types.d.ts +3647 -0
  197. package/dist/types.d.ts.map +1 -0
  198. package/dist/types.js +703 -0
  199. package/dist/types.js.map +1 -0
  200. package/dist/version.d.ts +7 -0
  201. package/dist/version.d.ts.map +1 -0
  202. package/dist/version.js +23 -0
  203. package/dist/version.js.map +1 -0
  204. package/docs/demo-guide.md +423 -0
  205. package/docs/events-format.md +295 -0
  206. package/docs/inferencemap-spec.md +344 -0
  207. package/docs/migration-v2.md +293 -0
  208. package/fixtures/demo/precomputed.json +142 -0
  209. package/fixtures/demo-project/README.md +52 -0
  210. package/fixtures/demo-project/ai-service.ts +65 -0
  211. package/fixtures/demo-project/sample-events.jsonl +15 -0
  212. package/fixtures/demo-project/src/ai-service.ts +128 -0
  213. package/fixtures/demo-project/src/llm-client.ts +155 -0
  214. package/package.json +65 -0
  215. package/prompts/agent-analyzer.yaml +47 -0
  216. package/prompts/ci-gate.yaml +98 -0
  217. package/prompts/correlation-analyzer.yaml +178 -0
  218. package/prompts/format-normalizer.yaml +46 -0
  219. package/prompts/peak-performance.yaml +180 -0
  220. package/prompts/pr-comment.yaml +111 -0
  221. package/prompts/runtime-analyzer.yaml +189 -0
  222. package/prompts/unified-analyzer.yaml +241 -0
  223. package/schemas/inference-map.v0.1.json +215 -0
  224. package/scripts/benchmark.ts +394 -0
  225. package/scripts/demo-v1.5.sh +158 -0
  226. package/scripts/sync-from-site.sh +197 -0
  227. package/scripts/validate-sync.sh +178 -0
  228. package/src/agent-analyzer.ts +481 -0
  229. package/src/agent.ts +1232 -0
  230. package/src/agents/correlation-analyzer.ts +353 -0
  231. package/src/agents/index.ts +235 -0
  232. package/src/agents/runtime-analyzer.ts +343 -0
  233. package/src/analysis-types.ts +558 -0
  234. package/src/analytics.ts +100 -0
  235. package/src/analyzer.ts +692 -0
  236. package/src/artifacts.ts +218 -0
  237. package/src/benchmarks/index.ts +309 -0
  238. package/src/cli.ts +503 -0
  239. package/src/commands/ci.ts +336 -0
  240. package/src/commands/config.ts +288 -0
  241. package/src/commands/demo.ts +175 -0
  242. package/src/commands/export.ts +297 -0
  243. package/src/commands/history.ts +425 -0
  244. package/src/commands/template.ts +385 -0
  245. package/src/commands/validate-map.ts +324 -0
  246. package/src/commands/whatif.ts +272 -0
  247. package/src/comparison.ts +283 -0
  248. package/src/config.ts +188 -0
  249. package/src/connectors/helicone.ts +164 -0
  250. package/src/connectors/index.ts +93 -0
  251. package/src/connectors/langsmith.ts +179 -0
  252. package/src/connectors/types.ts +180 -0
  253. package/src/cost-estimator.ts +146 -0
  254. package/src/costs.ts +347 -0
  255. package/src/counterfactuals.ts +516 -0
  256. package/src/enhancement-prompts.ts +118 -0
  257. package/src/envelopes.ts +814 -0
  258. package/src/format-normalizer.ts +1486 -0
  259. package/src/history.ts +400 -0
  260. package/src/html.ts +512 -0
  261. package/src/impact.ts +522 -0
  262. package/src/index.ts +83 -0
  263. package/src/insights.ts +341 -0
  264. package/src/joiner.ts +289 -0
  265. package/src/orchestrator.ts +1015 -0
  266. package/src/pdf.ts +110 -0
  267. package/src/prediction.ts +392 -0
  268. package/src/prompts/loader.ts +88 -0
  269. package/src/renderer.ts +1045 -0
  270. package/src/runid.ts +261 -0
  271. package/src/runtime.ts +450 -0
  272. package/src/scanner.ts +508 -0
  273. package/src/templates.ts +561 -0
  274. package/src/tools/index.ts +214 -0
  275. package/src/types.ts +873 -0
  276. package/src/version.ts +24 -0
  277. package/templates/context-accumulation.yaml +23 -0
  278. package/templates/cost-concentration.yaml +20 -0
  279. package/templates/dead-code.yaml +20 -0
  280. package/templates/latency-explainer.yaml +23 -0
  281. package/templates/optimizations/ab-testing-framework.yaml +74 -0
  282. package/templates/optimizations/api-gateway-optimization.yaml +81 -0
  283. package/templates/optimizations/api-model-routing-strategy.yaml +126 -0
  284. package/templates/optimizations/auto-scaling-optimization.yaml +85 -0
  285. package/templates/optimizations/batch-utilization-diagnostic.yaml +142 -0
  286. package/templates/optimizations/comprehensive-apm.yaml +76 -0
  287. package/templates/optimizations/context-window-optimization.yaml +91 -0
  288. package/templates/optimizations/cost-sensitive-batch-processing.yaml +77 -0
  289. package/templates/optimizations/distributed-training-optimization.yaml +77 -0
  290. package/templates/optimizations/document-analysis-edge.yaml +77 -0
  291. package/templates/optimizations/document-pipeline-optimization.yaml +78 -0
  292. package/templates/optimizations/domain-specific-distillation.yaml +78 -0
  293. package/templates/optimizations/error-handling-optimization.yaml +76 -0
  294. package/templates/optimizations/gptq-4bit-quantization.yaml +96 -0
  295. package/templates/optimizations/long-context-memory-management.yaml +78 -0
  296. package/templates/optimizations/max-tokens-optimization.yaml +76 -0
  297. package/templates/optimizations/memory-bandwidth-optimization.yaml +73 -0
  298. package/templates/optimizations/multi-framework-resilience.yaml +75 -0
  299. package/templates/optimizations/multi-tenant-optimization.yaml +75 -0
  300. package/templates/optimizations/prompt-caching-optimization.yaml +143 -0
  301. package/templates/optimizations/pytorch-to-onnx-migration.yaml +109 -0
  302. package/templates/optimizations/quality-monitoring.yaml +74 -0
  303. package/templates/optimizations/realtime-budget-controls.yaml +74 -0
  304. package/templates/optimizations/realtime-latency-optimization.yaml +74 -0
  305. package/templates/optimizations/sglang-concurrency-optimization.yaml +78 -0
  306. package/templates/optimizations/smart-model-routing.yaml +96 -0
  307. package/templates/optimizations/streaming-batch-selection.yaml +167 -0
  308. package/templates/optimizations/system-prompt-optimization.yaml +75 -0
  309. package/templates/optimizations/tensorrt-llm-performance.yaml +77 -0
  310. package/templates/optimizations/vllm-high-throughput-optimization.yaml +93 -0
  311. package/templates/optimizations/vllm-migration-memory-bound.yaml +78 -0
  312. package/templates/overpowered-extraction.yaml +32 -0
  313. package/templates/overpowered-model.yaml +31 -0
  314. package/templates/prompt-bloat.yaml +24 -0
  315. package/templates/retry-explosion.yaml +28 -0
  316. package/templates/schema/insight.schema.json +113 -0
  317. package/templates/schema/optimization.schema.json +180 -0
  318. package/templates/streaming-drift.yaml +30 -0
  319. package/templates/throughput-gap.yaml +21 -0
  320. package/templates/token-underutilization.yaml +28 -0
  321. package/templates/untested-fallback.yaml +21 -0
  322. package/tests/accuracy/drift-detection.test.ts +184 -0
  323. package/tests/accuracy/false-positives.test.ts +166 -0
  324. package/tests/accuracy/templates.test.ts +205 -0
  325. package/tests/action/commands.test.ts +125 -0
  326. package/tests/action/comments.test.ts +347 -0
  327. package/tests/cli.test.ts +203 -0
  328. package/tests/comparison.test.ts +309 -0
  329. package/tests/correlation-analyzer.test.ts +534 -0
  330. package/tests/counterfactuals.test.ts +347 -0
  331. package/tests/fixtures/events/missing-id.jsonl +1 -0
  332. package/tests/fixtures/events/missing-input.jsonl +1 -0
  333. package/tests/fixtures/events/missing-latency.jsonl +1 -0
  334. package/tests/fixtures/events/missing-model.jsonl +1 -0
  335. package/tests/fixtures/events/missing-output.jsonl +1 -0
  336. package/tests/fixtures/events/missing-provider.jsonl +1 -0
  337. package/tests/fixtures/events/missing-ts.jsonl +1 -0
  338. package/tests/fixtures/events/valid.csv +3 -0
  339. package/tests/fixtures/events/valid.json +1 -0
  340. package/tests/fixtures/events/valid.jsonl +2 -0
  341. package/tests/fixtures/events/with-callsite.jsonl +1 -0
  342. package/tests/fixtures/events/with-intent.jsonl +1 -0
  343. package/tests/fixtures/events/wrong-type.jsonl +1 -0
  344. package/tests/fixtures/repos/empty/.gitkeep +0 -0
  345. package/tests/fixtures/repos/hybrid-router/router.py +35 -0
  346. package/tests/fixtures/repos/saas-anthropic/agent.ts +27 -0
  347. package/tests/fixtures/repos/saas-openai/assistant.js +33 -0
  348. package/tests/fixtures/repos/saas-openai/client.py +26 -0
  349. package/tests/fixtures/repos/self-hosted-vllm/inference.py +22 -0
  350. package/tests/github-action.test.ts +292 -0
  351. package/tests/insights.test.ts +878 -0
  352. package/tests/joiner.test.ts +168 -0
  353. package/tests/performance/action-latency.test.ts +132 -0
  354. package/tests/performance/benchmark.test.ts +189 -0
  355. package/tests/performance/cli-latency.test.ts +102 -0
  356. package/tests/pr-comment.test.ts +313 -0
  357. package/tests/prediction.test.ts +296 -0
  358. package/tests/runtime-analyzer.test.ts +375 -0
  359. package/tests/runtime.test.ts +205 -0
  360. package/tests/scanner.test.ts +122 -0
  361. package/tests/template-conformance.test.ts +526 -0
  362. package/tests/unit/cost-calculator.test.ts +303 -0
  363. package/tests/unit/credits.test.ts +180 -0
  364. package/tests/unit/inference-map.test.ts +276 -0
  365. package/tests/unit/schema.test.ts +300 -0
  366. package/tsconfig.json +20 -0
  367. package/vitest.config.ts +14 -0
package/src/agent.ts ADDED
@@ -0,0 +1,1232 @@
1
+ import { existsSync, statSync, readFileSync, writeFileSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import type { ExecutionPlan, PlannedTask, TaskResult, ScanResult, Callsite, InferenceEvent, JoinedOutput, Insight, RuntimeSummary, InferenceMap, ImpactEstimate, EnrichedCallsite } from './types.js';
4
+ import { scan } from './scanner.js';
5
+ import { analyze, type LLMInsight } from './analyzer.js';
6
+ import { parseEvents, aggregate } from './runtime.js';
7
+ import { join } from './joiner.js';
8
+ import { loadTemplates, getDefaultPrompt, type AnalysisPrompt } from './templates.js';
9
+ import { evaluate } from './insights.js';
10
+ import { ENVELOPES } from './envelopes.js';
11
+ import { loadPricing } from './costs.js';
12
+ import { saveArtifacts, checkResumable, loadArtifacts, generateRunId, type ArtifactData } from './artifacts.js';
13
+ import { generateHTML } from './html.js';
14
+ import { generatePDF } from './pdf.js';
15
+ import { VERSION } from './version.js';
16
+ import { enrichInsightsWithImpact, generateImpactSummary, type ImpactSummary } from './impact.js';
17
+ import { saveRun, getLatestRun, loadRun, type AnalysisData } from './history.js';
18
+ import { compareSnapshots, formatComparisonSummary, type AnalysisSnapshot } from './comparison.js';
19
+ import { generatePredictions } from './prediction.js';
20
+ import { generateCounterfactuals } from './counterfactuals.js';
21
+ import type { ComparisonResult, PredictionResult, CounterfactualResult } from './types.js';
22
+ // Agent SDK pattern (DESIGN.md v2.0 Section 2.1, Patterns v0.2)
23
+ import {
24
+ DiscoveryAgent,
25
+ AnalyzerAgent,
26
+ JoinerAgent,
27
+ InsightAgent,
28
+ RuntimeAnalyzerAgent,
29
+ CorrelationAnalyzerAgent,
30
+ StaticAnalysisOrchestrator,
31
+ type StaticAnalysisOutput,
32
+ type PerformanceProfile,
33
+ } from './agents/index.js';
34
+ import { getPricingContext } from './costs.js';
35
+
36
+ // =============================================================================
37
+ // HELPERS
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Create synthetic enriched inference points from runtime events for insight evaluation.
42
+ * Groups events by provider:model and computes usage statistics.
43
+ * This enables runtime-only analysis to benefit from template-based insights.
44
+ */
45
+ function createSyntheticCallsitesFromEvents(events: InferenceEvent[]): EnrichedCallsite[] {
46
+ // Group events by provider:model
47
+ const groups = new Map<string, InferenceEvent[]>();
48
+
49
+ for (const event of events) {
50
+ const key = `${event.provider}:${event.model}`;
51
+ if (!groups.has(key)) {
52
+ groups.set(key, []);
53
+ }
54
+ groups.get(key)!.push(event);
55
+ }
56
+
57
+ // Convert each group to a synthetic enriched inference point
58
+ const callsites: EnrichedCallsite[] = [];
59
+ let id = 1;
60
+
61
+ for (const [key, groupEvents] of groups) {
62
+ const [provider, model] = key.split(':');
63
+
64
+ // Compute usage stats
65
+ const calls = groupEvents.length;
66
+ const tokens_in = groupEvents.reduce((sum, e) => sum + e.input_tokens, 0);
67
+ const tokens_out = groupEvents.reduce((sum, e) => sum + e.output_tokens, 0);
68
+ const latencies = groupEvents.map(e => e.latency_ms).sort((a, b) => a - b);
69
+
70
+ const p50Index = Math.floor(latencies.length * 0.5);
71
+ const p95Index = Math.floor(latencies.length * 0.95);
72
+ const p99Index = Math.floor(latencies.length * 0.99);
73
+
74
+ callsites.push({
75
+ id: `runtime-${id++}`,
76
+ file: 'runtime', // Synthetic location
77
+ line: 0,
78
+ provider: provider as EnrichedCallsite['provider'],
79
+ model,
80
+ framework: null,
81
+ runtime: null,
82
+ patterns: {
83
+ streaming: groupEvents.some(e => e.streaming === true),
84
+ batching: groupEvents.some(e => e.batch_id !== undefined),
85
+ caching: groupEvents.some(e => e.cached === true),
86
+ retries: groupEvents.some(e => (e.retry_count || 0) > 0),
87
+ fallback: groupEvents.some(e => e.fallback_used === true),
88
+ },
89
+ confidence: 1.0,
90
+ usage: {
91
+ calls,
92
+ tokens_in,
93
+ tokens_out,
94
+ latency_p50: latencies[p50Index] || 0,
95
+ latency_p95: latencies[p95Index] || latencies[p50Index] || 0,
96
+ latency_p99: latencies[p99Index] || latencies[p95Index] || latencies[p50Index] || 0,
97
+ },
98
+ });
99
+ }
100
+
101
+ return callsites;
102
+ }
103
+
104
+ /**
105
+ * Convert StaticAnalysisOutput to LLM insights for display.
106
+ * Maps optimizations from all dimensions (cost, latency, throughput, reliability) to insights.
107
+ */
108
+ function convertPerformanceToInsights(analysis: StaticAnalysisOutput): Array<{
109
+ severity: 'critical' | 'warning' | 'info';
110
+ category: string;
111
+ headline: string;
112
+ evidence: string;
113
+ location?: string;
114
+ recommendation: string;
115
+ impact?: {
116
+ layer: string;
117
+ impactType: string;
118
+ estimatedImpactPercent: number;
119
+ effort: string;
120
+ };
121
+ }> {
122
+ const insights: Array<{
123
+ severity: 'critical' | 'warning' | 'info';
124
+ category: string;
125
+ headline: string;
126
+ evidence: string;
127
+ location?: string;
128
+ recommendation: string;
129
+ impact?: {
130
+ layer: string;
131
+ impactType: string;
132
+ estimatedImpactPercent: number;
133
+ effort: string;
134
+ };
135
+ }> = [];
136
+
137
+ // Convert all optimizations to insights
138
+ for (const opt of analysis.all_optimizations) {
139
+ const severity = opt.priority === 'critical' || opt.priority === 'high' ? 'critical' :
140
+ opt.priority === 'medium' ? 'warning' : 'info';
141
+
142
+ const categoryMap: Record<string, string> = {
143
+ cost: 'Cost Optimization',
144
+ latency: 'Latency Optimization',
145
+ throughput: 'Throughput Optimization',
146
+ reliability: 'Reliability Improvement',
147
+ };
148
+
149
+ const impactTypeMap: Record<string, string> = {
150
+ cost: 'cost',
151
+ latency: 'latency',
152
+ throughput: 'throughput',
153
+ reliability: 'improvement',
154
+ };
155
+
156
+ // Parse impact percentage from the impact string (e.g., "90% savings" or "50% improvement")
157
+ const impactMatch = opt.impact.match(/(\d+)%/);
158
+ const impactPercent = impactMatch ? parseInt(impactMatch[1]) : 20;
159
+
160
+ insights.push({
161
+ severity,
162
+ category: categoryMap[opt.dimension] || opt.dimension,
163
+ headline: opt.description,
164
+ evidence: `${opt.type}: ${opt.impact}`,
165
+ location: `${opt.file}:${opt.line}`,
166
+ recommendation: opt.description,
167
+ impact: {
168
+ layer: opt.dimension,
169
+ impactType: impactTypeMap[opt.dimension] || 'improvement',
170
+ estimatedImpactPercent: impactPercent,
171
+ effort: opt.effort,
172
+ },
173
+ });
174
+ }
175
+
176
+ // Add reliability anti-pattern insights
177
+ for (const profile of analysis.performance_profiles) {
178
+ if (profile.reliability?.anti_patterns) {
179
+ for (const antiPattern of profile.reliability.anti_patterns) {
180
+ const severity = antiPattern.severity === 'high' ? 'critical' :
181
+ antiPattern.severity === 'medium' ? 'warning' : 'info';
182
+
183
+ insights.push({
184
+ severity,
185
+ category: 'Reliability Issue',
186
+ headline: antiPattern.pattern,
187
+ evidence: antiPattern.description,
188
+ location: antiPattern.location,
189
+ recommendation: `Fix: ${antiPattern.pattern}`,
190
+ impact: {
191
+ layer: 'reliability',
192
+ impactType: 'improvement',
193
+ estimatedImpactPercent: antiPattern.severity === 'high' ? 40 : 20,
194
+ effort: 'low',
195
+ },
196
+ });
197
+ }
198
+ }
199
+ }
200
+
201
+ return insights;
202
+ }
203
+
204
+ // =============================================================================
205
+ // TYPES
206
+ // =============================================================================
207
+
208
+ export interface AgentOptions {
209
+ path: string;
210
+ events?: string;
211
+ eventsUrl?: string; // URL to fetch runtime events
212
+ html?: boolean;
213
+ pdf?: boolean;
214
+ open?: boolean;
215
+ out?: string; // Write output to file
216
+ offline?: boolean;
217
+ verbose?: boolean;
218
+ noCache?: boolean; // Force fresh analysis, ignore cached runs
219
+ // Format detection options (PRD §6.4)
220
+ formatHint?: string; // User-specified format type
221
+ fieldHints?: Record<string, string>; // User-specified field mappings
222
+ lenient?: boolean; // Accept low-confidence mappings
223
+ strict?: boolean; // Fail on missing fields
224
+ redact?: boolean; // Redact code snippets from artifacts
225
+ // History options (v1.5)
226
+ noHistory?: boolean; // Skip saving run to history
227
+ compare?: boolean; // Compare with previous run
228
+ compareRunId?: string; // Specific run ID to compare with
229
+ predict?: boolean; // Generate deploy-time predictions
230
+ targetP95?: number; // Target p95 latency for budget calculation
231
+ }
232
+
233
+ // Progress phases - Julie Zhou aligned (DD Section 6.4)
234
+ export type ProgressPhase = 'scanning' | 'analyzing' | 'profiling' | 'parsing' | 'correlating' | 'generating';
235
+
236
+ export interface ProgressData {
237
+ phase: ProgressPhase;
238
+ detail?: string; // e.g., "847 files" or "23 inference points"
239
+ percent?: number; // 0-100 for progress bar
240
+ currentFile?: string; // current file being analyzed
241
+ }
242
+
243
+ export interface AgentCallbacks {
244
+ onPlanReady?: (plan: ExecutionPlan) => void;
245
+ onTaskStart?: (task: PlannedTask) => void;
246
+ onTaskComplete?: (task: PlannedTask, result: TaskResult) => void;
247
+ onProgress?: (data: ProgressData) => void; // User-meaningful progress
248
+ onComplete?: (results: AgentResults) => void;
249
+ onError?: (error: Error) => void;
250
+ onResumed?: (runId: string) => void; // Called when resuming from cache
251
+ onPartial?: (warnings: string[]) => void; // Called for partial results
252
+ }
253
+
254
+ export interface AgentResults {
255
+ mode: 'static' | 'runtime' | 'combined';
256
+ runId: string;
257
+ resumed: boolean;
258
+ scanResult?: ScanResult;
259
+ callsites?: Callsite[];
260
+ events?: InferenceEvent[];
261
+ runtimeSummary?: RuntimeSummary;
262
+ joined?: JoinedOutput;
263
+ insights: Insight[];
264
+ impactSummary?: ImpactSummary; // Stack-ranked impact analysis
265
+ inferenceMap?: InferenceMap;
266
+ staticAnalysis?: StaticAnalysisOutput; // 6-agent performance profiling
267
+ comparison?: ComparisonResult; // v1.5: Historical comparison
268
+ prediction?: PredictionResult; // v1.5: Deploy-time predictions
269
+ counterfactuals?: CounterfactualResult; // v1.5: What-if optimization scenarios
270
+ htmlPath?: string;
271
+ pdfPath?: string;
272
+ warnings?: string[]; // Partial state warnings
273
+ }
274
+
275
+ // =============================================================================
276
+ // AGENT CONTEXT
277
+ // =============================================================================
278
+
279
+ interface AgentContext {
280
+ opts: AgentOptions;
281
+ runId: string;
282
+ resumed: boolean;
283
+ scanResult?: ScanResult;
284
+ callsites?: Callsite[];
285
+ events?: InferenceEvent[];
286
+ runtimeSummary?: RuntimeSummary;
287
+ joined?: JoinedOutput;
288
+ insights?: Insight[];
289
+ llmInsights?: LLMInsight[]; // Phase 1: LLM-generated semantic insights
290
+ impactSummary?: ImpactSummary; // Stack-ranked impact analysis
291
+ inferenceMap?: InferenceMap;
292
+ staticAnalysis?: StaticAnalysisOutput; // 6-agent performance profiling
293
+ comparison?: ComparisonResult; // v1.5: Historical comparison result
294
+ prediction?: PredictionResult; // v1.5: Deploy-time predictions
295
+ counterfactuals?: CounterfactualResult; // v1.5: What-if scenarios
296
+ htmlContent?: string;
297
+ pdfPath?: string;
298
+ warnings: string[]; // Track partial state warnings
299
+ }
300
+
301
+ // =============================================================================
302
+ // PASS 1: PLAN
303
+ // =============================================================================
304
+
305
+ function detectMode(opts: AgentOptions): 'static' | 'runtime' | 'combined' {
306
+ // Check if the main path is an events file (case-insensitive for robustness)
307
+ const pathLower = opts.path.toLowerCase();
308
+ const isEventsFile = pathLower.endsWith('.jsonl') ||
309
+ pathLower.endsWith('.ndjson') ||
310
+ pathLower.endsWith('.json') ||
311
+ pathLower.endsWith('.csv');
312
+
313
+ // Also check if it's a file (not directory) - file paths with these extensions are events
314
+ const pathIsFile = !isDirectory(opts.path);
315
+
316
+ // Runtime mode: events file path without separate --events option
317
+ if (isEventsFile && pathIsFile && !opts.events) {
318
+ return 'runtime';
319
+ }
320
+ // Combined mode: directory path with --events option
321
+ if (!isEventsFile && opts.events) {
322
+ return 'combined';
323
+ }
324
+ // Combined mode: events file path with separate --events option (rare but valid)
325
+ if (isEventsFile && opts.events) {
326
+ return 'combined';
327
+ }
328
+ // Static mode: directory path without --events option
329
+ if (!isEventsFile && !opts.events) {
330
+ return 'static';
331
+ }
332
+ return 'combined';
333
+ }
334
+
335
+ function isDirectory(path: string): boolean {
336
+ try {
337
+ return existsSync(path) && statSync(path).isDirectory();
338
+ } catch {
339
+ return false;
340
+ }
341
+ }
342
+
343
+ /**
344
+ * Fetch runtime events from a URL
345
+ */
346
+ async function fetchEventsFromUrl(url: string): Promise<string> {
347
+ const response = await fetch(url);
348
+ if (!response.ok) {
349
+ throw new Error(`Failed to fetch events from ${url}: ${response.status} ${response.statusText}`);
350
+ }
351
+ return response.text();
352
+ }
353
+
354
+ export interface PlanResult {
355
+ plan: ExecutionPlan;
356
+ runId: string;
357
+ canResume: boolean;
358
+ runDir: string;
359
+ }
360
+
361
+ export function plan(opts: AgentOptions): PlanResult {
362
+ const tasks: PlannedTask[] = [];
363
+ let id = 1;
364
+ const mode = detectMode(opts);
365
+ const pathIsDirectory = isDirectory(opts.path);
366
+
367
+ // Generate run ID and check resumability
368
+ const inputs = {
369
+ repoRoot: isDirectory(opts.path) ? opts.path : undefined,
370
+ eventsPath: opts.events || (isDirectory(opts.path) ? undefined : opts.path),
371
+ offline: opts.offline,
372
+ };
373
+
374
+ const runId = generateRunId(inputs);
375
+ const resumeCheck = checkResumable(inputs);
376
+ const shouldResume = !opts.noCache && resumeCheck.canResume;
377
+
378
+ // If we can resume, skip analysis tasks
379
+ if (!shouldResume) {
380
+ // Always load pricing first
381
+ tasks.push({
382
+ id: id++,
383
+ type: 'scan', // Reusing for pricing load
384
+ description: 'Load pricing data',
385
+ });
386
+
387
+ // Only add static analysis tasks if path is a directory AND mode requires it
388
+ if ((mode === 'static' || mode === 'combined') && pathIsDirectory) {
389
+ tasks.push({
390
+ id: id++,
391
+ type: 'scan',
392
+ description: 'Scan repository',
393
+ });
394
+ // Unified analysis: discovery + profiling in single LLM call
395
+ tasks.push({
396
+ id: id++,
397
+ type: 'analyze',
398
+ description: 'Analyze and profile inference points',
399
+ depends_on: [id - 1],
400
+ });
401
+ }
402
+
403
+ if (mode === 'runtime' || mode === 'combined') {
404
+ tasks.push({
405
+ id: id++,
406
+ type: 'parse_events',
407
+ description: 'Parse runtime events',
408
+ });
409
+
410
+ // NEW: LLM-based runtime analysis for ALL modes with runtime data (Patterns v0.2)
411
+ tasks.push({
412
+ id: id++,
413
+ type: 'analyze', // Reuse 'analyze' type for runtime LLM analysis
414
+ description: 'Analyze runtime patterns',
415
+ depends_on: [id - 1],
416
+ });
417
+ }
418
+
419
+ if (mode === 'combined') {
420
+ tasks.push({
421
+ id: id++,
422
+ type: 'join',
423
+ description: 'Correlate static + runtime',
424
+ });
425
+
426
+ // NEW: LLM-based correlation analysis (Patterns v0.2)
427
+ tasks.push({
428
+ id: id++,
429
+ type: 'join', // Reuse 'join' type for correlation analysis
430
+ description: 'Analyze code-runtime drift',
431
+ depends_on: [id - 1],
432
+ });
433
+ }
434
+
435
+ tasks.push({
436
+ id: id++,
437
+ type: 'load_templates',
438
+ description: 'Load insight templates',
439
+ });
440
+
441
+ tasks.push({
442
+ id: id++,
443
+ type: 'generate_insights',
444
+ description: 'Generate findings',
445
+ });
446
+
447
+ // v1.5: Compare with previous run if requested
448
+ if (opts.compare) {
449
+ tasks.push({
450
+ id: id++,
451
+ type: 'compare',
452
+ description: 'Compare with previous run',
453
+ });
454
+ }
455
+
456
+ // v1.5: Generate deploy-time predictions if requested
457
+ if (opts.predict) {
458
+ tasks.push({
459
+ id: id++,
460
+ type: 'predict',
461
+ description: 'Generate latency predictions',
462
+ });
463
+ }
464
+
465
+ // v1.5: Always generate counterfactual insights (show optimization opportunities)
466
+ tasks.push({
467
+ id: id++,
468
+ type: 'counterfactuals',
469
+ description: 'Identify optimization opportunities',
470
+ });
471
+
472
+ if (opts.html) {
473
+ tasks.push({
474
+ id: id++,
475
+ type: 'generate_html',
476
+ description: 'Generate HTML report',
477
+ });
478
+ }
479
+
480
+ if (opts.pdf) {
481
+ tasks.push({
482
+ id: id++,
483
+ type: 'generate_pdf',
484
+ description: 'Generate PDF report',
485
+ });
486
+ }
487
+
488
+ tasks.push({
489
+ id: id++,
490
+ type: 'save_artifacts',
491
+ description: 'Save artifacts',
492
+ });
493
+
494
+ // v1.5: Save to history for comparison/prediction (unless --no-history)
495
+ if (!opts.noHistory) {
496
+ tasks.push({
497
+ id: id++,
498
+ type: 'save_history',
499
+ description: 'Save to history',
500
+ });
501
+ }
502
+ } else {
503
+ // Resuming - just need to load cached artifacts
504
+ tasks.push({
505
+ id: id++,
506
+ type: 'scan', // Reusing for load cached
507
+ description: 'Load cached results',
508
+ });
509
+
510
+ // Always generate HTML if requested, even when resuming
511
+ if (opts.html) {
512
+ tasks.push({
513
+ id: id++,
514
+ type: 'generate_html',
515
+ description: 'Generate HTML report',
516
+ });
517
+ }
518
+
519
+ if (opts.pdf) {
520
+ tasks.push({
521
+ id: id++,
522
+ type: 'generate_pdf',
523
+ description: 'Generate PDF report',
524
+ });
525
+ }
526
+
527
+ if (opts.html || opts.pdf) {
528
+ tasks.push({
529
+ id: id++,
530
+ type: 'save_artifacts',
531
+ description: 'Save artifacts',
532
+ });
533
+ }
534
+ }
535
+
536
+ return {
537
+ plan: { mode, tasks },
538
+ runId,
539
+ canResume: shouldResume,
540
+ runDir: resumeCheck.runDir,
541
+ };
542
+ }
543
+
544
+ // =============================================================================
545
+ // PASS 2: EXECUTE
546
+ // =============================================================================
547
+
548
+ async function executeTask(
549
+ task: PlannedTask,
550
+ ctx: AgentContext,
551
+ templates: Awaited<ReturnType<typeof loadTemplates>>,
552
+ runDir?: string,
553
+ onProgress?: (data: ProgressData) => void
554
+ ): Promise<void> {
555
+ switch (task.type) {
556
+ case 'scan':
557
+ if (task.description === 'Load pricing data') {
558
+ await loadPricing();
559
+ } else if (task.description === 'Load cached results') {
560
+ // Resume from cache
561
+ if (runDir) {
562
+ const cached = loadArtifacts(runDir);
563
+ ctx.inferenceMap = cached.inferenceMap;
564
+ ctx.insights = cached.insights;
565
+ ctx.joined = cached.joined;
566
+ ctx.runtimeSummary = cached.runtime;
567
+ if (cached.inferenceMap) {
568
+ ctx.callsites = cached.inferenceMap.callsites;
569
+ }
570
+ if (cached.insights && cached.insights.length > 0) {
571
+ ctx.impactSummary = generateImpactSummary(cached.insights);
572
+ }
573
+ }
574
+ } else {
575
+ // Validate that path is a directory before attempting to scan
576
+ if (!isDirectory(ctx.opts.path)) {
577
+ const ext = ctx.opts.path.toLowerCase();
578
+ if (ext.endsWith('.jsonl') || ext.endsWith('.ndjson') || ext.endsWith('.json') || ext.endsWith('.csv')) {
579
+ throw new Error(`Cannot scan file "${ctx.opts.path}" as a codebase. This looks like an events file - try 'peakinfer analyze ${ctx.opts.path}' for runtime analysis.`);
580
+ }
581
+ throw new Error(`Expected directory for static analysis, got file: ${ctx.opts.path}`);
582
+ }
583
+ // Agent SDK pattern: DiscoveryAgent with constrained tools (Glob/Grep/Read)
584
+ const discoveryResult = await DiscoveryAgent.execute({ root: ctx.opts.path });
585
+ ctx.scanResult = discoveryResult.result.scanResult;
586
+ const fileCount = ctx.scanResult?.files.length ?? 0;
587
+ onProgress?.({ phase: 'scanning', detail: `${fileCount} files` });
588
+ }
589
+ break;
590
+
591
+ case 'analyze':
592
+ // Handle static code analysis, runtime pattern analysis, AND performance profiling
593
+ if (task.description === 'Analyze runtime patterns') {
594
+ // NEW: LLM-based runtime analysis (Patterns v0.2)
595
+ if (!ctx.events || !ctx.runtimeSummary) {
596
+ ctx.warnings.push('Runtime analysis skipped: no events parsed');
597
+ break;
598
+ }
599
+ try {
600
+ // Get pricing context for models in the data
601
+ const models = Object.keys(ctx.runtimeSummary.byModel);
602
+ const pricingContext = getPricingContext(models);
603
+
604
+ // Emit progress: starting runtime pattern analysis
605
+ onProgress?.({ phase: 'analyzing', detail: `analyzing ${ctx.events.length} runtime events...` });
606
+
607
+ const runtimeResult = await RuntimeAnalyzerAgent.execute({
608
+ events: ctx.events,
609
+ runtimeSummary: ctx.runtimeSummary,
610
+ pricingContext,
611
+ });
612
+
613
+ // Store runtime insights for later merging
614
+ const runtimeInsights = runtimeResult.result.insights.map(i => ({
615
+ ...i,
616
+ source: 'llm' as const,
617
+ }));
618
+ ctx.llmInsights = [...(ctx.llmInsights || []), ...runtimeInsights as LLMInsight[]];
619
+
620
+ onProgress?.({ phase: 'analyzing', detail: `${runtimeResult.result.insights.length} runtime insights` });
621
+ } catch (error) {
622
+ ctx.warnings.push(`Runtime analysis warning: ${error instanceof Error ? error.message : String(error)}`);
623
+ }
624
+ } else if (task.description === 'Analyze and profile inference points') {
625
+ // UNIFIED: Discovery + Profiling in single LLM call
626
+ if (!ctx.scanResult) throw new Error('Scan result required');
627
+ try {
628
+ const scanRoot = ctx.scanResult.root;
629
+
630
+ // Read all source files (no artificial limit - process all candidates)
631
+ const filesToAnalyze = ctx.scanResult.files
632
+ .map(f => {
633
+ const fullPath = resolve(scanRoot, f.path);
634
+ try {
635
+ return {
636
+ path: fullPath,
637
+ content: readFileSync(fullPath, 'utf-8'),
638
+ language: f.language,
639
+ };
640
+ } catch {
641
+ return null;
642
+ }
643
+ })
644
+ .filter((f): f is { path: string; content: string; language: string } => f !== null);
645
+
646
+ if (filesToAnalyze.length === 0) {
647
+ ctx.warnings.push('Analysis skipped: no source files available');
648
+ ctx.callsites = [];
649
+ ctx.llmInsights = [];
650
+ break;
651
+ }
652
+
653
+ // Run unified analysis (discovery + profiling in one LLM call)
654
+ // Pass progress callback for Claude Code-style per-file progress updates
655
+ const orchestrator = new StaticAnalysisOrchestrator();
656
+ ctx.staticAnalysis = await orchestrator.analyze(
657
+ { files: filesToAnalyze },
658
+ (progressData) => {
659
+ // Forward progress to renderer with percent for progress bar
660
+ onProgress?.({
661
+ phase: 'analyzing',
662
+ percent: progressData.percent,
663
+ currentFile: progressData.currentFile,
664
+ detail: `${progressData.completed}/${progressData.total} files`,
665
+ });
666
+ }
667
+ );
668
+
669
+ // Extract callsites from unified analysis for rest of pipeline
670
+ const callsitesFromAnalysis: Callsite[] = [];
671
+ for (const profile of ctx.staticAnalysis.performance_profiles) {
672
+ callsitesFromAnalysis.push({
673
+ id: profile.inference_point_id,
674
+ file: profile.file.replace(scanRoot + '/', '').replace(scanRoot, ''),
675
+ line: profile.line,
676
+ provider: profile.provider as Callsite['provider'],
677
+ model: profile.model ?? null,
678
+ framework: null,
679
+ runtime: null,
680
+ patterns: {},
681
+ confidence: 0.9,
682
+ });
683
+ }
684
+ ctx.callsites = callsitesFromAnalysis;
685
+
686
+ // Convert performance profiles to insights
687
+ const performanceInsights = convertPerformanceToInsights(ctx.staticAnalysis);
688
+ ctx.llmInsights = performanceInsights as LLMInsight[];
689
+
690
+ // Build inference map
691
+ let promptMeta: MapMetadata = { llmUsed: true };
692
+ try {
693
+ const prompt = getDefaultPrompt();
694
+ promptMeta.promptId = prompt.id;
695
+ promptMeta.promptVersion = prompt.version;
696
+ } catch {
697
+ // Prompt not found, use defaults
698
+ }
699
+ ctx.inferenceMap = buildInferenceMap(ctx.opts.path, ctx.callsites, promptMeta);
700
+
701
+ onProgress?.({
702
+ phase: 'profiling',
703
+ detail: `${ctx.staticAnalysis.summary.total_optimizations} optimizations found`,
704
+ });
705
+ } catch (error) {
706
+ ctx.warnings.push(`Analysis warning: ${error instanceof Error ? error.message : String(error)}`);
707
+ ctx.callsites = [];
708
+ ctx.llmInsights = [];
709
+ ctx.inferenceMap = buildInferenceMap(ctx.opts.path, [], { llmUsed: false });
710
+ }
711
+ } else if (task.description === 'Profile performance') {
712
+ // Legacy: Skip - now handled by unified analysis above
713
+ break;
714
+ } else {
715
+ // Original static code analysis
716
+ if (!ctx.scanResult) throw new Error('Scan result required');
717
+ try {
718
+ // Agent SDK pattern: AnalyzerAgent with tool-limited semantic analysis
719
+ // Pass progress callback for visual progress bar during LLM analysis
720
+ const analyzerResult = await AnalyzerAgent.execute({
721
+ scanResult: ctx.scanResult,
722
+ onProgress: onProgress ? (data) => {
723
+ onProgress({ phase: 'analyzing', percent: data.percent, currentFile: data.currentFile });
724
+ } : undefined,
725
+ });
726
+ ctx.callsites = analyzerResult.result.callsites;
727
+ ctx.llmInsights = analyzerResult.result.llmInsights as LLMInsight[];
728
+
729
+ // Get prompt metadata for report
730
+ let promptMeta: MapMetadata = { llmUsed: ctx.llmInsights.length > 0 };
731
+ try {
732
+ const prompt = getDefaultPrompt();
733
+ promptMeta.promptId = prompt.id;
734
+ promptMeta.promptVersion = prompt.version;
735
+ } catch {
736
+ // Prompt not found, use defaults
737
+ }
738
+
739
+ ctx.inferenceMap = buildInferenceMap(ctx.opts.path, ctx.callsites, promptMeta);
740
+ onProgress?.({ phase: 'analyzing', detail: `${ctx.callsites.length} inference points` });
741
+ } catch (error) {
742
+ // Partial state: analysis failed but we can continue
743
+ ctx.warnings.push(`Analysis warning: ${error instanceof Error ? error.message : String(error)}`);
744
+ ctx.callsites = [];
745
+ ctx.llmInsights = [];
746
+ ctx.inferenceMap = buildInferenceMap(ctx.opts.path, [], { llmUsed: false });
747
+ }
748
+ }
749
+ break;
750
+
751
+ case 'parse_events': {
752
+ try {
753
+ // Build normalization options from CLI flags (PRD §6.4)
754
+ const normalizationOptions = {
755
+ format_hint: ctx.opts.formatHint as import('./types.js').FormatType | undefined,
756
+ field_hints: ctx.opts.fieldHints,
757
+ lenient: ctx.opts.lenient,
758
+ strict: ctx.opts.strict,
759
+ codebase_context: ctx.scanResult, // Pass codebase context for smarter normalization
760
+ };
761
+
762
+ // Handle --events-url: fetch from URL first
763
+ if (ctx.opts.eventsUrl) {
764
+ const eventsContent = await fetchEventsFromUrl(ctx.opts.eventsUrl);
765
+ // Write to temp file for parsing
766
+ const tempPath = '.peakinfer/.tmp_events.jsonl';
767
+ writeFileSync(tempPath, eventsContent);
768
+ ctx.events = await parseEvents(tempPath, normalizationOptions);
769
+ } else {
770
+ const eventsPath = ctx.opts.events || ctx.opts.path;
771
+ ctx.events = await parseEvents(eventsPath, normalizationOptions);
772
+ }
773
+
774
+ ctx.runtimeSummary = aggregate(ctx.events);
775
+ // Emit progress with event count
776
+ onProgress?.({ phase: 'parsing', detail: `${ctx.events.length} events` });
777
+ } catch (error) {
778
+ // Partial state: event parsing failed
779
+ ctx.warnings.push(`Events parsing warning: ${error instanceof Error ? error.message : String(error)}`);
780
+ ctx.events = [];
781
+ }
782
+ break;
783
+ }
784
+
785
+ case 'join':
786
+ // Handle both basic join AND LLM-based correlation analysis
787
+ if (task.description === 'Analyze code-runtime drift') {
788
+ // NEW: LLM-based correlation analysis (Patterns v0.2)
789
+ if (!ctx.callsites || !ctx.events || !ctx.runtimeSummary) {
790
+ ctx.warnings.push('Correlation analysis skipped: missing callsites or events');
791
+ break;
792
+ }
793
+ try {
794
+ // Emit progress: starting drift detection analysis
795
+ onProgress?.({ phase: 'correlating', detail: 'detecting code-runtime drift...' });
796
+
797
+ const correlationResult = await CorrelationAnalyzerAgent.execute({
798
+ callsites: ctx.callsites,
799
+ events: ctx.events,
800
+ runtimeSummary: ctx.runtimeSummary,
801
+ });
802
+
803
+ // Merge correlation insights
804
+ const correlationInsights = correlationResult.result.insights.map(i => ({
805
+ ...i,
806
+ source: 'llm' as const,
807
+ }));
808
+ ctx.llmInsights = [...(ctx.llmInsights || []), ...correlationInsights as LLMInsight[]];
809
+
810
+ // Update drift signals in joined output
811
+ if (ctx.joined) {
812
+ ctx.joined.drift = [
813
+ ...ctx.joined.drift,
814
+ ...correlationResult.result.driftSignals,
815
+ ];
816
+ }
817
+
818
+ onProgress?.({
819
+ phase: 'correlating',
820
+ detail: `alignment: ${Math.round(correlationResult.result.alignmentScore * 100)}%`,
821
+ });
822
+ } catch (error) {
823
+ ctx.warnings.push(`Correlation analysis warning: ${error instanceof Error ? error.message : String(error)}`);
824
+ }
825
+ } else {
826
+ // Original basic join
827
+ if (!ctx.callsites || !ctx.events) throw new Error('Callsites and events required');
828
+ // Agent SDK pattern: JoinerAgent correlates static + runtime
829
+ const joinerResult = await JoinerAgent.execute({ callsites: ctx.callsites, events: ctx.events });
830
+ ctx.joined = joinerResult.result.joined;
831
+ onProgress?.({
832
+ phase: 'correlating',
833
+ detail: `${ctx.joined.callsites.filter(c => 'usage' in c && c.usage).length} matched`,
834
+ });
835
+ }
836
+ break;
837
+
838
+ case 'load_templates':
839
+ // Templates already loaded, just verify
840
+ break;
841
+
842
+ case 'generate_insights': {
843
+ // Agent SDK pattern: InsightAgent evaluates templates
844
+ // For runtime-only mode, create synthetic inference points from events for template evaluation
845
+ let data: { callsites: Callsite[] } | JoinedOutput;
846
+
847
+ if (ctx.joined) {
848
+ data = ctx.joined;
849
+ } else if (ctx.callsites && ctx.callsites.length > 0) {
850
+ data = { callsites: ctx.callsites };
851
+ } else if (ctx.events && ctx.events.length > 0) {
852
+ // Runtime-only mode: create synthetic enriched inference points from events
853
+ data = { callsites: createSyntheticCallsitesFromEvents(ctx.events) };
854
+ } else {
855
+ data = { callsites: [] };
856
+ }
857
+
858
+ const insightResult = await InsightAgent.execute({ data, templates });
859
+ const templateInsights = insightResult.result.insights;
860
+
861
+ // Convert LLM insights to Insight format, preserving any LLM-provided impact estimates
862
+ const llmFormattedInsights: Insight[] = (ctx.llmInsights || []).map(llmInsight => ({
863
+ id: `llm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
864
+ severity: llmInsight.severity,
865
+ category: llmInsight.category,
866
+ headline: llmInsight.headline,
867
+ evidence: llmInsight.evidence,
868
+ location: llmInsight.location,
869
+ recommendation: llmInsight.recommendation,
870
+ source: 'llm' as const, // Mark as LLM-generated
871
+ // Preserve LLM-provided impact estimates if present
872
+ impact: llmInsight.impact ? {
873
+ layer: llmInsight.impact.layer,
874
+ impactType: llmInsight.impact.impactType,
875
+ estimatedImpactPercent: llmInsight.impact.estimatedImpactPercent,
876
+ effort: llmInsight.impact.effort,
877
+ confidence: 0.8, // LLM estimates have higher confidence
878
+ } : undefined,
879
+ }));
880
+
881
+ // Combine: LLM semantic insights first (phase 1), then template pattern insights (phase 2)
882
+ const combinedInsights = [...llmFormattedInsights, ...templateInsights];
883
+
884
+ // Enrich all insights with impact estimates (fills in missing ones)
885
+ ctx.insights = enrichInsightsWithImpact(combinedInsights);
886
+
887
+ // Generate stack-ranked impact summary
888
+ ctx.impactSummary = generateImpactSummary(ctx.insights);
889
+
890
+ // Emit progress with insight count
891
+ onProgress?.({ phase: 'generating', detail: `${ctx.insights.length} findings` });
892
+ break;
893
+ }
894
+
895
+ case 'generate_html':
896
+ if (!ctx.inferenceMap) throw new Error('InferenceMap required for HTML');
897
+ ctx.htmlContent = generateHTML({
898
+ inferenceMap: ctx.inferenceMap,
899
+ insights: ctx.insights || [],
900
+ joined: ctx.joined,
901
+ runtime: ctx.runtimeSummary,
902
+ });
903
+ break;
904
+
905
+ case 'generate_pdf': {
906
+ if (!ctx.htmlContent) throw new Error('HTML content required for PDF');
907
+
908
+ // Generate human-friendly PDF filename
909
+ const pdfAbsolutePath = ctx.inferenceMap?.metadata?.absolutePath || ctx.opts.path;
910
+ const pdfProjectName = pdfAbsolutePath.split('/').filter(Boolean).pop() || 'project';
911
+ const pdfProjectSlug = pdfProjectName.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').substring(0, 50);
912
+ const pdfFileName = `${pdfProjectSlug}_peakinfer_report.pdf`;
913
+ const pdfPath = `.peakinfer/${pdfFileName}`;
914
+
915
+ await generatePDF(ctx.htmlContent, pdfPath);
916
+ ctx.pdfPath = pdfPath;
917
+ break;
918
+ }
919
+
920
+ case 'save_artifacts': {
921
+ const inputs = {
922
+ repoRoot: isDirectory(ctx.opts.path) ? ctx.opts.path : undefined,
923
+ eventsPath: ctx.opts.events || (isDirectory(ctx.opts.path) ? undefined : ctx.opts.path),
924
+ offline: ctx.opts.offline,
925
+ };
926
+
927
+ // Extract project name for human-friendly report naming
928
+ const absolutePath = ctx.inferenceMap?.metadata?.absolutePath || ctx.opts.path;
929
+ const projectName = absolutePath.split('/').filter(Boolean).pop() || 'project';
930
+
931
+ saveArtifacts(
932
+ {
933
+ inferenceMap: ctx.inferenceMap,
934
+ insights: ctx.insights,
935
+ joined: ctx.joined,
936
+ runtime: ctx.runtimeSummary,
937
+ html: ctx.htmlContent,
938
+ },
939
+ '.peakinfer',
940
+ {
941
+ runId: ctx.runId,
942
+ inputs,
943
+ projectName,
944
+ }
945
+ );
946
+ break;
947
+ }
948
+
949
+ case 'save_history': {
950
+ // v1.5: Save run to history for comparison/prediction features
951
+ const mode = ctx.joined ? 'combined' : (ctx.events?.length ? 'runtime' : 'static');
952
+
953
+ // Prepare analysis data for history storage
954
+ const historyData: AnalysisData = {
955
+ inferenceMap: ctx.inferenceMap,
956
+ insights: ctx.insights,
957
+ joined: ctx.joined,
958
+ runtime: ctx.runtimeSummary,
959
+ };
960
+
961
+ // Generate human-friendly HTML path if generated
962
+ const absolutePath = ctx.inferenceMap?.metadata?.absolutePath || ctx.opts.path;
963
+ const projectName = absolutePath.split('/').filter(Boolean).pop() || 'project';
964
+ const projectSlug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').substring(0, 50);
965
+
966
+ saveRun({
967
+ path: ctx.opts.path,
968
+ analysisType: mode,
969
+ data: historyData,
970
+ htmlPath: ctx.htmlContent ? `.peakinfer/${projectSlug}_peakinfer_report.html` : undefined,
971
+ pdfPath: ctx.pdfPath,
972
+ });
973
+ break;
974
+ }
975
+
976
+ case 'compare': {
977
+ // v1.5: Compare with previous run
978
+ // Build current snapshot
979
+ const currentSnapshot: AnalysisSnapshot = {
980
+ runId: ctx.runId,
981
+ timestamp: new Date().toISOString(),
982
+ callsites: ctx.callsites || [],
983
+ insights: ctx.insights,
984
+ };
985
+
986
+ // Get baseline (specific run or latest)
987
+ let baselineRun;
988
+ if (ctx.opts.compareRunId) {
989
+ baselineRun = loadRun(ctx.opts.compareRunId);
990
+ if (!baselineRun) {
991
+ ctx.warnings.push(`Comparison skipped: run ${ctx.opts.compareRunId} not found`);
992
+ break;
993
+ }
994
+ } else {
995
+ baselineRun = getLatestRun(ctx.opts.path);
996
+ if (!baselineRun) {
997
+ ctx.warnings.push('Comparison skipped: no previous runs found');
998
+ break;
999
+ }
1000
+ }
1001
+
1002
+ // Build baseline snapshot
1003
+ const baselineSnapshot: AnalysisSnapshot = {
1004
+ runId: baselineRun.manifest.runId,
1005
+ timestamp: baselineRun.manifest.timestamp,
1006
+ callsites: baselineRun.data.inferenceMap?.callsites || [],
1007
+ insights: baselineRun.data.insights,
1008
+ };
1009
+
1010
+ // Perform comparison
1011
+ ctx.comparison = compareSnapshots(baselineSnapshot, currentSnapshot);
1012
+
1013
+ // Log summary for progress
1014
+ const summary = formatComparisonSummary(ctx.comparison);
1015
+ onProgress?.({ phase: 'generating', detail: `compared with ${baselineRun.manifest.runId.slice(0, 8)}` });
1016
+ break;
1017
+ }
1018
+
1019
+ case 'predict': {
1020
+ // v1.5: Generate deploy-time latency predictions
1021
+ if (!ctx.inferenceMap) {
1022
+ ctx.warnings.push('Prediction skipped: no inference map available');
1023
+ break;
1024
+ }
1025
+
1026
+ // Generate predictions based on inference points
1027
+ const predictionResult = generatePredictions(
1028
+ ctx.inferenceMap,
1029
+ 0, // Historical run count (can be enhanced later with actual history)
1030
+ { targetP95: ctx.opts.targetP95 }
1031
+ );
1032
+
1033
+ ctx.prediction = predictionResult;
1034
+
1035
+ // Log summary for progress
1036
+ const riskCount = predictionResult.summary.highRiskCount + predictionResult.summary.mediumRiskCount;
1037
+ onProgress?.({
1038
+ phase: 'generating',
1039
+ detail: `${predictionResult.predictions.length} predictions, ${riskCount} at risk`,
1040
+ });
1041
+ break;
1042
+ }
1043
+
1044
+ case 'counterfactuals': {
1045
+ // v1.5: Generate what-if optimization scenarios
1046
+ if (!ctx.inferenceMap) {
1047
+ ctx.warnings.push('Counterfactuals skipped: no inference map available');
1048
+ break;
1049
+ }
1050
+
1051
+ // Generate counterfactual insights
1052
+ const counterfactualResult = generateCounterfactuals(ctx.inferenceMap);
1053
+ ctx.counterfactuals = counterfactualResult;
1054
+
1055
+ // Log summary for progress
1056
+ onProgress?.({
1057
+ phase: 'generating',
1058
+ detail: `${counterfactualResult.summary.totalOpportunities} optimization opportunities`,
1059
+ });
1060
+ break;
1061
+ }
1062
+ }
1063
+ }
1064
+
1065
+ interface MapMetadata {
1066
+ promptId?: string;
1067
+ promptVersion?: string;
1068
+ llmUsed?: boolean;
1069
+ }
1070
+
1071
+ function buildInferenceMap(
1072
+ root: string,
1073
+ callsites: Callsite[],
1074
+ metadata: MapMetadata = {}
1075
+ ): InferenceMap {
1076
+ const providers = [...new Set(callsites.map(c => c.provider).filter(Boolean))] as string[];
1077
+ const models = [...new Set(callsites.map(c => c.model).filter(Boolean))] as string[];
1078
+
1079
+ const patternCounts: Record<string, number> = {};
1080
+ for (const cs of callsites) {
1081
+ for (const [pattern, enabled] of Object.entries(cs.patterns)) {
1082
+ if (enabled) {
1083
+ patternCounts[pattern] = (patternCounts[pattern] || 0) + 1;
1084
+ }
1085
+ }
1086
+ }
1087
+
1088
+ return {
1089
+ version: '0.1', // InferenceMap schema version (not CLI version)
1090
+ root,
1091
+ generatedAt: new Date().toISOString(),
1092
+ metadata: {
1093
+ absolutePath: resolve(root),
1094
+ promptId: metadata.promptId || 'peak-performance',
1095
+ promptVersion: metadata.promptVersion,
1096
+ templatesVersion: VERSION, // CLI version for audit trail
1097
+ llmProvider: metadata.llmUsed ? 'anthropic' : 'none',
1098
+ llmModel: metadata.llmUsed ? 'claude-sonnet-4-20250514' : undefined,
1099
+ },
1100
+ summary: {
1101
+ totalCallsites: callsites.length,
1102
+ providers,
1103
+ models,
1104
+ patterns: patternCounts,
1105
+ },
1106
+ callsites,
1107
+ };
1108
+ }
1109
+
1110
+ // =============================================================================
1111
+ // PUBLIC API
1112
+ // =============================================================================
1113
+
1114
+ export class Agent {
1115
+ private callbacks: AgentCallbacks;
1116
+
1117
+ constructor(callbacks: AgentCallbacks = {}) {
1118
+ this.callbacks = callbacks;
1119
+ }
1120
+
1121
+ async run(opts: AgentOptions): Promise<AgentResults> {
1122
+ const planResult = plan(opts);
1123
+ const { plan: executionPlan, runId, canResume, runDir } = planResult;
1124
+
1125
+ // Notify if resuming from cache
1126
+ if (canResume) {
1127
+ this.callbacks.onResumed?.(runId);
1128
+ }
1129
+
1130
+ this.callbacks.onPlanReady?.(executionPlan);
1131
+
1132
+ const ctx: AgentContext = {
1133
+ opts,
1134
+ runId,
1135
+ resumed: canResume,
1136
+ warnings: [],
1137
+ };
1138
+ const results: TaskResult[] = [];
1139
+
1140
+ // Load templates once (not needed if resuming)
1141
+ const templates = canResume ? [] : await loadTemplates({ offline: opts.offline });
1142
+
1143
+ for (const task of executionPlan.tasks) {
1144
+ this.callbacks.onTaskStart?.(task);
1145
+ const startTime = Date.now();
1146
+
1147
+ try {
1148
+ await executeTask(task, ctx, templates, runDir, this.callbacks.onProgress);
1149
+
1150
+ const result: TaskResult = {
1151
+ taskId: task.id,
1152
+ status: 'success',
1153
+ durationMs: Date.now() - startTime,
1154
+ };
1155
+ results.push(result);
1156
+ this.callbacks.onTaskComplete?.(task, result);
1157
+ } catch (error) {
1158
+ const result: TaskResult = {
1159
+ taskId: task.id,
1160
+ status: 'failed',
1161
+ error: error instanceof Error ? error.message : String(error),
1162
+ durationMs: Date.now() - startTime,
1163
+ };
1164
+ results.push(result);
1165
+ this.callbacks.onTaskComplete?.(task, result);
1166
+
1167
+ // Critical tasks abort execution only if not partial-safe
1168
+ if (['scan'].includes(task.type) && task.description !== 'Load cached results') {
1169
+ throw error;
1170
+ }
1171
+ // analyze and parse_events can fail gracefully (partial state)
1172
+ }
1173
+ }
1174
+
1175
+ // Notify if there were warnings (partial state)
1176
+ if (ctx.warnings.length > 0) {
1177
+ this.callbacks.onPartial?.(ctx.warnings);
1178
+ }
1179
+
1180
+ // Generate human-friendly report filename
1181
+ const absolutePath = ctx.inferenceMap?.metadata?.absolutePath || opts.path;
1182
+ const projectName = absolutePath.split('/').filter(Boolean).pop() || 'project';
1183
+ const projectSlug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').substring(0, 50);
1184
+ const reportFileName = ctx.htmlContent
1185
+ ? `.peakinfer/${projectSlug}_peakinfer_report.html`
1186
+ : undefined;
1187
+
1188
+ const agentResults: AgentResults = {
1189
+ mode: executionPlan.mode,
1190
+ runId,
1191
+ resumed: canResume,
1192
+ scanResult: ctx.scanResult,
1193
+ callsites: ctx.callsites,
1194
+ events: ctx.events,
1195
+ runtimeSummary: ctx.runtimeSummary,
1196
+ joined: ctx.joined,
1197
+ insights: ctx.insights || [],
1198
+ impactSummary: ctx.impactSummary,
1199
+ inferenceMap: ctx.inferenceMap,
1200
+ staticAnalysis: ctx.staticAnalysis,
1201
+ comparison: ctx.comparison, // v1.5: Historical comparison
1202
+ prediction: ctx.prediction, // v1.5: Deploy-time predictions
1203
+ counterfactuals: ctx.counterfactuals, // v1.5: What-if scenarios
1204
+ htmlPath: reportFileName,
1205
+ pdfPath: ctx.pdfPath,
1206
+ warnings: ctx.warnings.length > 0 ? ctx.warnings : undefined,
1207
+ };
1208
+
1209
+ // Handle --out: write output to file
1210
+ if (opts.out) {
1211
+ const outputData = {
1212
+ schema: 'peakinfer-analysis',
1213
+ version: '1.0', // Analysis export format version
1214
+ cliVersion: VERSION,
1215
+ mode: agentResults.mode,
1216
+ runId: agentResults.runId,
1217
+ timestamp: new Date().toISOString(),
1218
+ inferenceMap: agentResults.inferenceMap,
1219
+ insights: agentResults.insights,
1220
+ impactSummary: agentResults.impactSummary,
1221
+ comparison: agentResults.comparison,
1222
+ prediction: agentResults.prediction,
1223
+ counterfactuals: agentResults.counterfactuals,
1224
+ runtimeSummary: agentResults.runtimeSummary,
1225
+ };
1226
+ writeFileSync(opts.out, JSON.stringify(outputData, null, 2));
1227
+ }
1228
+
1229
+ this.callbacks.onComplete?.(agentResults);
1230
+ return agentResults;
1231
+ }
1232
+ }