@herbertgao/pi-extensions 2026.9.2 → 2026.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +3 -2
  2. package/THIRD_PARTY_NOTICES.md +24 -0
  3. package/node_modules/@herbertgao/sol-pi/LICENSE +19 -0
  4. package/node_modules/@herbertgao/sol-pi/README.md +159 -0
  5. package/node_modules/@herbertgao/sol-pi/SECURITY.md +26 -0
  6. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +19 -0
  7. package/node_modules/@herbertgao/sol-pi/agents-install.md +150 -0
  8. package/node_modules/@herbertgao/sol-pi/assets/sol-pi-hero.png +0 -0
  9. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +69 -0
  10. package/node_modules/@herbertgao/sol-pi/docs/configuration.md +75 -0
  11. package/node_modules/@herbertgao/sol-pi/package.json +76 -0
  12. package/node_modules/@herbertgao/sol-pi/scripts/check-pi-compat.mjs +32 -0
  13. package/node_modules/@herbertgao/sol-pi/scripts/check-sol-pi-config.mjs +120 -0
  14. package/node_modules/@herbertgao/sol-pi/sol-pi.example.json +10 -0
  15. package/node_modules/@herbertgao/sol-pi/src/sol-pi/config.ts +135 -0
  16. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/file-queue.ts +74 -0
  17. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +185 -0
  18. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/then-run.ts +128 -0
  19. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/archive.ts +53 -0
  20. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/candidate.ts +101 -0
  21. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/config.ts +71 -0
  22. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/index.ts +220 -0
  23. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/journal.ts +25 -0
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/provider.ts +164 -0
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/evidence-preserving-reducer/receipt.ts +177 -0
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/index.ts +227 -0
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/ledger.ts +20 -0
  28. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/observation-pack/observation.ts +252 -0
  29. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +237 -0
  30. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +455 -0
  31. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/index.ts +49 -0
  32. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/plan.ts +79 -0
  33. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +208 -0
  34. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/tools.ts +100 -0
  35. package/node_modules/@herbertgao/sol-pi/src/sol-pi/index.ts +42 -0
  36. package/node_modules/@herbertgao/sol-pi/src/sol-pi/runtime-paths.ts +17 -0
  37. package/node_modules/@herbertgao/sol-pi/src/sol-pi/tui.ts +71 -0
  38. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -2
  39. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
  40. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  41. package/node_modules/@narumitw/pi-btw/src/btw.ts +13 -2
  42. package/node_modules/pi-lens/CHANGELOG.md +176 -0
  43. package/node_modules/pi-lens/README.md +13 -8
  44. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -1
  45. package/node_modules/pi-lens/dist/clients/analysed-root.js +1 -0
  46. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +1 -1
  47. package/node_modules/pi-lens/dist/clients/biome-client.js +13 -2
  48. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +2 -0
  49. package/node_modules/pi-lens/dist/clients/cache-observability.js +122 -1
  50. package/node_modules/pi-lens/dist/clients/complexity-client.js +5 -0
  51. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  52. package/node_modules/pi-lens/dist/clients/config-resolve.js +6 -3
  53. package/node_modules/pi-lens/dist/clients/config-schema.js +17 -0
  54. package/node_modules/pi-lens/dist/clients/config-warn.js +2 -2
  55. package/node_modules/pi-lens/dist/clients/dead-code-client.js +8 -1
  56. package/node_modules/pi-lens/dist/clients/dependency-checker.js +5 -1
  57. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +38 -15
  58. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +1 -1
  59. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +5 -1
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +2 -1
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +2 -3
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +4 -3
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +2 -1
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +2 -1
  66. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +2 -1
  67. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +2 -1
  68. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +2 -1
  69. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +2 -1
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +2 -1
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +2 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +2 -1
  73. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +2 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  75. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +3 -1
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +2 -1
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +2 -1
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +2 -1
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +2 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +19 -1
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +3 -2
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +2 -1
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +2 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +2 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +2 -1
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +12 -12
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +2 -1
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +2 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +2 -2
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +12 -11
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -1
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +2 -2
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +3 -2
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +3 -2
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +2 -1
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +2 -1
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +5 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +2 -2
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +8 -2
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +2 -1
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +4 -3
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +2 -1
  109. package/node_modules/pi-lens/dist/clients/effective-config.js +12 -2
  110. package/node_modules/pi-lens/dist/clients/extension-log.js +2 -0
  111. package/node_modules/pi-lens/dist/clients/file-role.js +18 -1
  112. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +4 -0
  113. package/node_modules/pi-lens/dist/clients/formatters.js +54 -188
  114. package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -0
  115. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +42 -6
  116. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +48 -7
  117. package/node_modules/pi-lens/dist/clients/installer/index.js +196 -52
  118. package/node_modules/pi-lens/dist/clients/jscpd-client.js +9 -1
  119. package/node_modules/pi-lens/dist/clients/knip-client.js +9 -2
  120. package/node_modules/pi-lens/dist/clients/language-profile.js +12 -2
  121. package/node_modules/pi-lens/dist/clients/latency-logger.js +2 -0
  122. package/node_modules/pi-lens/dist/clients/lens-config.js +4 -0
  123. package/node_modules/pi-lens/dist/clients/lsp/config.js +36 -7
  124. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +24 -1
  125. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +78 -0
  126. package/node_modules/pi-lens/dist/clients/lsp/index.js +281 -45
  127. package/node_modules/pi-lens/dist/clients/lsp/server.js +62 -15
  128. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +37 -4
  129. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +3 -0
  130. package/node_modules/pi-lens/dist/clients/mcp/session.js +2 -0
  131. package/node_modules/pi-lens/dist/clients/opengrep-client.js +2 -0
  132. package/node_modules/pi-lens/dist/clients/package-manager.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +8 -3
  134. package/node_modules/pi-lens/dist/clients/pipeline.js +3 -2
  135. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +38 -10
  136. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +10 -9
  137. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  138. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +2 -0
  139. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +2 -0
  140. package/node_modules/pi-lens/dist/clients/ruff-client.js +18 -9
  141. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +9 -0
  142. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +5 -0
  143. package/node_modules/pi-lens/dist/clients/runtime-session.js +13 -4
  144. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +4 -1
  145. package/node_modules/pi-lens/dist/clients/runtime-turn.js +48 -1
  146. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -2
  147. package/node_modules/pi-lens/dist/clients/session-event-guard.js +69 -1
  148. package/node_modules/pi-lens/dist/clients/sg-runner.js +2 -1
  149. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +108 -0
  150. package/node_modules/pi-lens/dist/clients/test-runner-client.js +291 -76
  151. package/node_modules/pi-lens/dist/clients/tool-config.js +199 -0
  152. package/node_modules/pi-lens/dist/clients/tool-cwd.js +299 -0
  153. package/node_modules/pi-lens/dist/clients/tool-probe.js +53 -0
  154. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +42 -0
  155. package/node_modules/pi-lens/dist/clients/trivy-client.js +9 -1
  156. package/node_modules/pi-lens/dist/clients/turn-context.js +52 -0
  157. package/node_modules/pi-lens/dist/clients/widget-state.js +4 -3
  158. package/node_modules/pi-lens/dist/index.js +21969 -20005
  159. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -2
  160. package/node_modules/pi-lens/dist/mcp/server.js +135 -159
  161. package/node_modules/pi-lens/dist/tools/activate-tools.js +10 -7
  162. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +2 -13
  163. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +8 -13
  164. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +83 -39
  165. package/node_modules/pi-lens/dist/tools/effective-config.js +4 -3
  166. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +1 -10
  167. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +353 -85
  168. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +21 -110
  169. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +20 -44
  170. package/node_modules/pi-lens/dist/tools/module-report.js +5 -9
  171. package/node_modules/pi-lens/dist/tools/project-report.js +9 -17
  172. package/node_modules/pi-lens/dist/tools/render-compact.js +280 -1
  173. package/node_modules/pi-lens/dist/tools/symbol-search.js +2 -2
  174. package/node_modules/pi-lens/docs/agent-guide.md +3 -4
  175. package/node_modules/pi-lens/docs/agent-tools.md +46 -22
  176. package/node_modules/pi-lens/docs/configuration.md +9 -0
  177. package/node_modules/pi-lens/docs/environment-variables.md +0 -5
  178. package/node_modules/pi-lens/docs/features.md +5 -5
  179. package/node_modules/pi-lens/docs/globalconfig.md +11 -1
  180. package/node_modules/pi-lens/docs/mcp.md +13 -1
  181. package/node_modules/pi-lens/docs/pi-lens-fixer.md +71 -0
  182. package/node_modules/pi-lens/docs/pi-lens-investigator.md +15 -0
  183. package/node_modules/pi-lens/docs/pi-lens-monitor.md +88 -0
  184. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +35 -0
  185. package/node_modules/pi-lens/docs/pi-lens-subagent.md +12 -4
  186. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  187. package/node_modules/pi-lens/docs/real-harness.md +46 -0
  188. package/node_modules/pi-lens/docs/release-qa-baseline.md +5 -1
  189. package/node_modules/pi-lens/docs/servercapabilities.md +1 -6
  190. package/node_modules/pi-lens/docs/settings.md +37 -2
  191. package/node_modules/pi-lens/docs/tools_improvement2.md +4 -4
  192. package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
  193. package/node_modules/pi-lens/docs/usage.md +13 -3
  194. package/node_modules/pi-lens/package.json +10 -1
  195. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +7 -3
  196. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +8 -6
  197. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +19 -9
  198. package/node_modules/pi-web-access/CHANGELOG.md +29 -0
  199. package/node_modules/pi-web-access/README.md +33 -22
  200. package/node_modules/pi-web-access/abortable.ts +17 -0
  201. package/node_modules/pi-web-access/crawl4ai.ts +204 -0
  202. package/node_modules/pi-web-access/credential-source.ts +1 -0
  203. package/node_modules/pi-web-access/curator-page.ts +20 -2
  204. package/node_modules/pi-web-access/curator-run.ts +44 -0
  205. package/node_modules/pi-web-access/curator-server.ts +3 -1
  206. package/node_modules/pi-web-access/duckduckgo.ts +1 -1
  207. package/node_modules/pi-web-access/extract.ts +57 -12
  208. package/node_modules/pi-web-access/fetch-params.ts +1 -1
  209. package/node_modules/pi-web-access/gemini-search.ts +9 -5
  210. package/node_modules/pi-web-access/index.ts +25 -18
  211. package/node_modules/pi-web-access/openai-search.ts +35 -13
  212. package/node_modules/pi-web-access/package.json +2 -2
  213. package/node_modules/pi-web-access/page-query.ts +5 -2
  214. package/node_modules/pi-web-access/query-rewrite.ts +5 -2
  215. package/node_modules/pi-web-access/serpapi.ts +220 -0
  216. package/node_modules/pi-web-access/ssrf-protection.ts +5 -1
  217. package/node_modules/pi-web-access/storage.ts +10 -0
  218. package/node_modules/pi-web-access/summary-review.ts +34 -21
  219. package/node_modules/pi-web-access/utils.ts +6 -4
  220. package/package.json +7 -4
  221. package/node_modules/pi-lens/dist/tools/ast-dump.js +0 -103
@@ -0,0 +1,252 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ import { createHash } from "node:crypto";
6
+ import { constants } from "node:fs";
7
+ import { type FileHandle, lstat, mkdir, open } from "node:fs/promises";
8
+ import { dirname, join } from "node:path";
9
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
10
+ import type { TextContent, ToolResultMessage } from "@earendil-works/pi-ai";
11
+
12
+ /** Only tool results larger than this participate. */
13
+ export const THRESHOLD_BYTES = 10 * 1024;
14
+ /** Provider requests that still carry the full payload before the placeholder takes over. */
15
+ export const FULL_SENDS = 2;
16
+ /** Placeholder excerpt budget, split evenly between head and tail, whole lines only. */
17
+ export const PLACEHOLDER_EXCERPT_BYTES = 1024;
18
+
19
+ const CHARS_PER_TOKEN = 4;
20
+ const OBSERVATION_ID_PATTERN = /^obs_[a-f0-9]{24}$/u;
21
+ const READ_OBJECT_FLAGS = constants.O_RDONLY | constants.O_NOFOLLOW;
22
+ const CREATE_OBJECT_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW;
23
+
24
+ /**
25
+ * Receipts from the evidence-preserving reducer are already a reduction of a
26
+ * long log. Packing them again would replace verified evidence with an excerpt.
27
+ */
28
+ const EVIDENCE_REDUCER_RECEIPT_PREFIX = "sol_pi_evidence_receipt_v1";
29
+
30
+ export interface Observation {
31
+ readonly id: string;
32
+ readonly contentHash: string;
33
+ readonly filePath: string;
34
+ readonly toolName: string;
35
+ readonly text: string;
36
+ readonly bytes: number;
37
+ readonly lines: number;
38
+ readonly tokens: number;
39
+ }
40
+
41
+ export function hash(value: string | Buffer): string {
42
+ return createHash("sha256").update(value).digest("hex");
43
+ }
44
+
45
+ export function estimateTokens(text: string): number {
46
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
47
+ }
48
+
49
+ export function countLines(text: string): number {
50
+ if (text.length === 0) return 0;
51
+ let lines = text.endsWith("\n") ? 0 : 1;
52
+ for (const character of text) {
53
+ if (character === "\n") lines += 1;
54
+ }
55
+ return lines;
56
+ }
57
+
58
+ function countBufferLines(buffer: Buffer): number {
59
+ if (buffer.length === 0) return 0;
60
+ let lines = buffer[buffer.length - 1] === 0x0a ? 0 : 1;
61
+ for (const byte of buffer) {
62
+ if (byte === 0x0a) lines += 1;
63
+ }
64
+ return lines;
65
+ }
66
+
67
+ export function isPureTextResult(message: AgentMessage): message is ToolResultMessage {
68
+ return (
69
+ message.role === "toolResult" &&
70
+ !message.isError &&
71
+ message.content.length > 0 &&
72
+ message.content.every((block) => block.type === "text")
73
+ );
74
+ }
75
+
76
+ function textFromResult(message: ToolResultMessage): string {
77
+ return (message.content as TextContent[]).map((block) => block.text).join("\n");
78
+ }
79
+
80
+ function containsReducerReceipt(text: string): boolean {
81
+ return text.split("\n").some((line) => line === EVIDENCE_REDUCER_RECEIPT_PREFIX);
82
+ }
83
+
84
+ /**
85
+ * Archived payloads live under SoL-Pi's session-derived runtime root.
86
+ *
87
+ * They are content addressed inside one session. A resume reuses the same
88
+ * directory; a fork rebuilds its own object from the unmodified session history.
89
+ */
90
+ export function observationPath(runtimeRoot: string, id: string): string {
91
+ return join(runtimeRoot, "observation-pack", "objects", `${id}.txt`);
92
+ }
93
+
94
+ export function isObservationId(id: string): boolean {
95
+ return OBSERVATION_ID_PATTERN.test(id);
96
+ }
97
+
98
+ export function createObservation(message: ToolResultMessage, runtimeRoot: string): Observation | undefined {
99
+ const text = textFromResult(message);
100
+ if (containsReducerReceipt(text)) return undefined;
101
+ const bytes = Buffer.byteLength(text, "utf8");
102
+ if (bytes <= THRESHOLD_BYTES) return undefined;
103
+ if (!runtimeRoot) throw new Error("Persistent SoL-Pi runtime directory is unavailable");
104
+
105
+ const contentHash = hash(text);
106
+ const id = `obs_${hash(`${message.toolName}\0${message.toolCallId}\0${contentHash}`).slice(0, 24)}`;
107
+ return {
108
+ id,
109
+ contentHash,
110
+ filePath: observationPath(runtimeRoot, id),
111
+ toolName: message.toolName,
112
+ text,
113
+ bytes,
114
+ lines: countLines(text),
115
+ tokens: estimateTokens(text),
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Write the payload to its content-addressed path, refusing symlinks and
121
+ * verifying an existing object byte for byte before reusing it.
122
+ */
123
+ export async function ensureStored(observation: Observation): Promise<void> {
124
+ const directoryPath = dirname(observation.filePath);
125
+ await mkdir(directoryPath, { recursive: true, mode: 0o700 });
126
+ const directoryStats = await lstat(directoryPath);
127
+ if (!directoryStats.isDirectory() || directoryStats.isSymbolicLink()) {
128
+ throw new Error(`Observation directory is not a regular directory for ${observation.id}`);
129
+ }
130
+
131
+ let handle: FileHandle | undefined;
132
+ try {
133
+ handle = await open(observation.filePath, CREATE_OBJECT_FLAGS, 0o600);
134
+ await handle.writeFile(observation.text, { encoding: "utf8" });
135
+ } catch (error) {
136
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "EEXIST") throw error;
137
+ const existingHandle = await open(observation.filePath, READ_OBJECT_FLAGS);
138
+ try {
139
+ const existing = await existingHandle.stat();
140
+ if (!existing.isFile()) {
141
+ throw new Error(`Content-addressed observation is not a regular file for ${observation.id}`, { cause: error });
142
+ }
143
+ if (existing.size !== observation.bytes) {
144
+ throw new Error(`Content-addressed observation size mismatch for ${observation.id}`, { cause: error });
145
+ }
146
+ const existingContent = await existingHandle.readFile();
147
+ if (hash(existingContent) !== observation.contentHash) {
148
+ throw new Error(`Content-addressed observation hash mismatch for ${observation.id}`, { cause: error });
149
+ }
150
+ } finally {
151
+ await existingHandle.close();
152
+ }
153
+ } finally {
154
+ await handle?.close();
155
+ }
156
+ }
157
+
158
+ function completeLineExcerpt(text: string, budgetBytes: number, fromEnd: boolean): string {
159
+ const lines = text.split(/(?<=\n)/);
160
+ const selected: string[] = [];
161
+ let selectedBytes = 0;
162
+ let index = fromEnd ? lines.length - 1 : 0;
163
+
164
+ while (index >= 0 && index < lines.length) {
165
+ const line = lines[index];
166
+ if (line === undefined) break;
167
+ const lineBytes = Buffer.byteLength(line, "utf8");
168
+ if (selectedBytes + lineBytes > budgetBytes) break;
169
+ if (fromEnd) selected.unshift(line);
170
+ else selected.push(line);
171
+ selectedBytes += lineBytes;
172
+ index += fromEnd ? -1 : 1;
173
+ }
174
+
175
+ return selected.join("");
176
+ }
177
+
178
+ export function placeholderFor(observation: Observation): string {
179
+ const headBudget = Math.floor(PLACEHOLDER_EXCERPT_BYTES / 2);
180
+ const tailBudget = PLACEHOLDER_EXCERPT_BYTES - headBudget;
181
+ const head = completeLineExcerpt(observation.text, headBudget, false);
182
+ const tail = completeLineExcerpt(observation.text, tailBudget, true);
183
+ return [
184
+ `[large tool result replaced after its first ${FULL_SENDS} provider requests]`,
185
+ `id: ${observation.id}`,
186
+ `tool: ${observation.toolName}`,
187
+ `original_bytes: ${observation.bytes}`,
188
+ `original_lines: ${observation.lines}`,
189
+ `estimated_tokens: ${observation.tokens}`,
190
+ `retrieve: call obs_recall with {"id":"${observation.id}","offset":0}; continue with returned next_offset`,
191
+ `[first complete lines, up to ${headBudget} bytes]`,
192
+ head,
193
+ `[middle omitted; last complete lines, up to ${tailBudget} bytes]`,
194
+ tail,
195
+ `[${observation.bytes} original bytes omitted]`,
196
+ ].join("\n");
197
+ }
198
+
199
+ export interface RecallChunk {
200
+ readonly text: string;
201
+ readonly bytes: number;
202
+ readonly lines: number;
203
+ readonly nextOffset: number;
204
+ readonly eof: boolean;
205
+ }
206
+
207
+ function trimUtf8End(buffer: Buffer, limit: number): number {
208
+ let end = limit;
209
+ while (end > 0 && end < buffer.length && ((buffer[end] ?? 0) & 0xc0) === 0x80) end -= 1;
210
+ return end;
211
+ }
212
+
213
+ export async function readRecallChunk(
214
+ path: string,
215
+ offset: number,
216
+ limits: { readonly maxBytes: number; readonly maxLines: number },
217
+ ): Promise<RecallChunk> {
218
+ const handle = await open(path, READ_OBJECT_FLAGS);
219
+ try {
220
+ const fileStats = await handle.stat();
221
+ if (!fileStats.isFile()) throw new Error("Stored observation is not a regular file");
222
+ if (offset > fileStats.size) throw new Error(`Offset ${offset} exceeds observation size ${fileStats.size}`);
223
+
224
+ const available = Math.max(0, fileStats.size - offset);
225
+ const buffer = Buffer.alloc(Math.min(available, limits.maxBytes + 4));
226
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, offset);
227
+ let end = Math.min(bytesRead, limits.maxBytes);
228
+ let newlineCount = 0;
229
+
230
+ for (let index = 0; index < end; index += 1) {
231
+ if (buffer[index] !== 0x0a) continue;
232
+ newlineCount += 1;
233
+ if (newlineCount === limits.maxLines) {
234
+ end = index + 1;
235
+ break;
236
+ }
237
+ }
238
+
239
+ end = trimUtf8End(buffer, end);
240
+ const chunk = buffer.subarray(0, end);
241
+ const nextOffset = offset + chunk.length;
242
+ return {
243
+ text: chunk.toString("utf8"),
244
+ bytes: chunk.length,
245
+ lines: countBufferLines(chunk),
246
+ nextOffset,
247
+ eof: nextOffset >= fileStats.size,
248
+ };
249
+ } finally {
250
+ await handle.close();
251
+ }
252
+ }
@@ -0,0 +1,237 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export type CompactionEconomics = {
7
+ readonly remainingRequestScale: number;
8
+ readonly remainingRequestStddevK: number;
9
+ readonly windowReserveTokens: number;
10
+ readonly firstCompactionRequestScale: number;
11
+ readonly subsequentCompactionMargin: number;
12
+ };
13
+
14
+ export const DEFAULT_COMPACTION_ECONOMICS: CompactionEconomics = Object.freeze({
15
+ remainingRequestScale: 1,
16
+ remainingRequestStddevK: 0,
17
+ windowReserveTokens: 16_384,
18
+ firstCompactionRequestScale: 2,
19
+ subsequentCompactionMargin: 1.5,
20
+ });
21
+
22
+ export type CompactionReason =
23
+ | "economic"
24
+ | "window_protection"
25
+ | "deferred_economic"
26
+ | "deferred_subsequent_margin"
27
+ | "deferred_carried_debt"
28
+ | "horizon_unavailable"
29
+ | "cache_ratio_unavailable"
30
+ | "native_not_compactable"
31
+ | "non_positive_saving";
32
+
33
+ export type RequestHorizonEstimate = {
34
+ readonly completedBoundaryRequestCounts: readonly number[];
35
+ readonly requestsPerBoundaryMean: number;
36
+ readonly requestsPerBoundaryLowerBound: number;
37
+ readonly unboundedExpectedRemainingRequests: number;
38
+ readonly averageContextTokenIncrement: number | null;
39
+ readonly windowRequestUpperBound: number | null;
40
+ readonly expectedRemainingRequests: number;
41
+ };
42
+
43
+ export type CompactionDecision = {
44
+ readonly writeTokens: number;
45
+ readonly archiveTokens: number;
46
+ readonly memoTokens: number;
47
+ readonly contextTokens: number;
48
+ readonly completedBoundaryRequestCounts: readonly number[] | null;
49
+ readonly requestsPerBoundaryMean: number | null;
50
+ readonly requestsPerBoundaryLowerBound: number | null;
51
+ readonly unboundedExpectedRemainingRequests: number | null;
52
+ readonly averageContextTokenIncrement: number | null;
53
+ readonly windowRequestUpperBound: number | null;
54
+ readonly expectedRemainingRequests: number | null;
55
+ readonly breakevenRequests: number | null;
56
+ readonly combinedBreakevenRequests: number | null;
57
+ readonly effectiveHorizonRequests: number | null;
58
+ readonly cacheWriteReadRatio: number | null;
59
+ readonly incrementalCacheCostRatio: number | null;
60
+ readonly priorCompactionCount: number;
61
+ readonly carriedDebtTokens: number;
62
+ readonly cacheDebtRepaymentTokens: number;
63
+ readonly compact: boolean;
64
+ readonly reason: CompactionReason;
65
+ };
66
+
67
+ const MINIMUM_VARIANCE_SAMPLES = 3;
68
+ const SMALL_SAMPLE_SCALE = 0.5;
69
+
70
+ export function estimateRemainingRequests(input: {
71
+ readonly completedBoundaryRequestCounts: readonly number[];
72
+ readonly remainingBoundaries: number;
73
+ readonly scale: number;
74
+ readonly standardDeviationK: number;
75
+ readonly contextTokens: number;
76
+ readonly contextWindowTokens: number | null;
77
+ readonly averageContextTokenIncrement: number | null;
78
+ }): RequestHorizonEstimate {
79
+ const mean =
80
+ input.completedBoundaryRequestCounts.reduce((total, count) => total + count, 0) /
81
+ Math.max(1, input.completedBoundaryRequestCounts.length);
82
+ let lowerBound = mean;
83
+ if (input.standardDeviationK !== 0) {
84
+ if (input.completedBoundaryRequestCounts.length < MINIMUM_VARIANCE_SAMPLES) {
85
+ lowerBound *= SMALL_SAMPLE_SCALE;
86
+ } else {
87
+ const variance = input.completedBoundaryRequestCounts.reduce(
88
+ (total, count) => total + (count - mean) ** 2,
89
+ 0,
90
+ );
91
+ const deviation = Math.sqrt(variance / (input.completedBoundaryRequestCounts.length - 1));
92
+ lowerBound = Math.max(0, mean - input.standardDeviationK * deviation);
93
+ }
94
+ }
95
+
96
+ const unboundedExpectedRemainingRequests =
97
+ 1 + Math.floor(lowerBound * Math.max(0, input.remainingBoundaries) * input.scale);
98
+ const windowRequestUpperBound =
99
+ input.contextWindowTokens === null ||
100
+ input.averageContextTokenIncrement === null ||
101
+ input.averageContextTokenIncrement <= 0
102
+ ? null
103
+ : Math.max(
104
+ 0,
105
+ Math.floor((input.contextWindowTokens - input.contextTokens) / input.averageContextTokenIncrement),
106
+ );
107
+
108
+ return {
109
+ completedBoundaryRequestCounts: [...input.completedBoundaryRequestCounts],
110
+ requestsPerBoundaryMean: mean,
111
+ requestsPerBoundaryLowerBound: lowerBound,
112
+ unboundedExpectedRemainingRequests,
113
+ averageContextTokenIncrement: input.averageContextTokenIncrement,
114
+ windowRequestUpperBound,
115
+ expectedRemainingRequests:
116
+ windowRequestUpperBound === null
117
+ ? unboundedExpectedRemainingRequests
118
+ : Math.min(unboundedExpectedRemainingRequests, windowRequestUpperBound),
119
+ };
120
+ }
121
+
122
+ export function decideCompaction(input: {
123
+ readonly writeTokens: number;
124
+ readonly archiveTokens: number;
125
+ readonly memoTokens: number;
126
+ readonly contextTokens: number;
127
+ readonly completedBoundaryRequestCounts: readonly number[] | null;
128
+ readonly remainingBoundaries: number;
129
+ readonly averageContextTokenIncrement: number | null;
130
+ readonly contextWindowTokens: number | null;
131
+ readonly priorCompactionCount: number;
132
+ readonly carriedDebtTokens: number;
133
+ readonly cacheDebtRepaymentTokens: number;
134
+ readonly cacheWriteReadRatio: number | null;
135
+ readonly economics: CompactionEconomics;
136
+ }): CompactionDecision {
137
+ const horizon =
138
+ input.completedBoundaryRequestCounts === null
139
+ ? null
140
+ : estimateRemainingRequests({
141
+ completedBoundaryRequestCounts: input.completedBoundaryRequestCounts,
142
+ remainingBoundaries: input.remainingBoundaries,
143
+ scale: input.economics.remainingRequestScale,
144
+ standardDeviationK: input.economics.remainingRequestStddevK,
145
+ contextTokens: input.contextTokens,
146
+ contextWindowTokens: input.contextWindowTokens,
147
+ averageContextTokenIncrement: input.averageContextTokenIncrement,
148
+ });
149
+ const savingTokens = input.archiveTokens - input.memoTokens;
150
+ const incrementalCacheCostRatio =
151
+ input.cacheWriteReadRatio === null ? null : Math.max(0, input.cacheWriteReadRatio - 1);
152
+ const breakevenRequests =
153
+ savingTokens > 0 && incrementalCacheCostRatio !== null
154
+ ? (input.writeTokens * incrementalCacheCostRatio) / savingTokens
155
+ : null;
156
+ const combinedBreakevenRequests =
157
+ savingTokens > 0 && incrementalCacheCostRatio !== null
158
+ ? (input.carriedDebtTokens + input.writeTokens * incrementalCacheCostRatio) / savingTokens
159
+ : null;
160
+ const firstCompaction = input.priorCompactionCount === 0;
161
+ const effectiveHorizonRequests =
162
+ horizon === null
163
+ ? null
164
+ : firstCompaction
165
+ ? Math.min(
166
+ horizon.expectedRemainingRequests * input.economics.firstCompactionRequestScale,
167
+ horizon.windowRequestUpperBound ?? Number.POSITIVE_INFINITY,
168
+ )
169
+ : horizon.expectedRemainingRequests;
170
+ const windowProtection =
171
+ input.contextWindowTokens !== null &&
172
+ input.contextTokens >= input.contextWindowTokens - input.economics.windowReserveTokens;
173
+ const baseEconomic =
174
+ horizon !== null &&
175
+ horizon.expectedRemainingRequests > 0 &&
176
+ breakevenRequests !== null &&
177
+ breakevenRequests <= horizon.expectedRemainingRequests;
178
+ const firstEconomic =
179
+ firstCompaction &&
180
+ effectiveHorizonRequests !== null &&
181
+ effectiveHorizonRequests > 0 &&
182
+ breakevenRequests !== null &&
183
+ breakevenRequests <= effectiveHorizonRequests;
184
+ const subsequentMarginOpen =
185
+ !firstCompaction &&
186
+ horizon !== null &&
187
+ breakevenRequests !== null &&
188
+ breakevenRequests * input.economics.subsequentCompactionMargin <= horizon.expectedRemainingRequests;
189
+ const carriedDebtGateOpen =
190
+ !firstCompaction &&
191
+ horizon !== null &&
192
+ combinedBreakevenRequests !== null &&
193
+ combinedBreakevenRequests <= horizon.expectedRemainingRequests;
194
+ const economic = firstCompaction ? firstEconomic : baseEconomic && subsequentMarginOpen && carriedDebtGateOpen;
195
+ const compressible = savingTokens > 0;
196
+ const compact = compressible && (windowProtection || economic);
197
+
198
+ return {
199
+ writeTokens: input.writeTokens,
200
+ archiveTokens: input.archiveTokens,
201
+ memoTokens: input.memoTokens,
202
+ contextTokens: input.contextTokens,
203
+ ...(horizon ?? {
204
+ completedBoundaryRequestCounts: null,
205
+ requestsPerBoundaryMean: null,
206
+ requestsPerBoundaryLowerBound: null,
207
+ unboundedExpectedRemainingRequests: null,
208
+ averageContextTokenIncrement: input.averageContextTokenIncrement,
209
+ windowRequestUpperBound: null,
210
+ expectedRemainingRequests: null,
211
+ }),
212
+ breakevenRequests,
213
+ combinedBreakevenRequests,
214
+ effectiveHorizonRequests,
215
+ cacheWriteReadRatio: input.cacheWriteReadRatio,
216
+ incrementalCacheCostRatio,
217
+ priorCompactionCount: input.priorCompactionCount,
218
+ carriedDebtTokens: input.carriedDebtTokens,
219
+ cacheDebtRepaymentTokens: input.cacheDebtRepaymentTokens,
220
+ compact,
221
+ reason: !compressible
222
+ ? "non_positive_saving"
223
+ : windowProtection
224
+ ? "window_protection"
225
+ : economic
226
+ ? "economic"
227
+ : horizon === null
228
+ ? "horizon_unavailable"
229
+ : breakevenRequests === null
230
+ ? "cache_ratio_unavailable"
231
+ : !firstCompaction && baseEconomic && !subsequentMarginOpen
232
+ ? "deferred_subsequent_margin"
233
+ : !firstCompaction && baseEconomic && !carriedDebtGateOpen
234
+ ? "deferred_carried_debt"
235
+ : "deferred_economic",
236
+ };
237
+ }