pi-ui-extend 1.0.41 → 1.0.45

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 (236) hide show
  1. package/README.md +50 -13
  2. package/dist/app/app.d.ts +13 -0
  3. package/dist/app/app.js +158 -8
  4. package/dist/app/cli/install.js +2 -2
  5. package/dist/app/commands/command-controller.d.ts +4 -0
  6. package/dist/app/commands/command-controller.js +11 -0
  7. package/dist/app/commands/command-git-actions.d.ts +25 -0
  8. package/dist/app/commands/command-git-actions.js +381 -0
  9. package/dist/app/commands/command-host.d.ts +4 -0
  10. package/dist/app/commands/command-host.js +5 -2
  11. package/dist/app/commands/command-model-actions.d.ts +1 -0
  12. package/dist/app/commands/command-model-actions.js +57 -35
  13. package/dist/app/commands/command-navigation-actions.d.ts +1 -0
  14. package/dist/app/commands/command-navigation-actions.js +16 -3
  15. package/dist/app/commands/command-registry.d.ts +2 -0
  16. package/dist/app/commands/command-registry.js +15 -1
  17. package/dist/app/commands/command-session-actions.js +9 -3
  18. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  19. package/dist/app/commands/reload-context-inventory.js +70 -0
  20. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  21. package/dist/app/extensions/extension-actions-controller.js +6 -0
  22. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  23. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  24. package/dist/app/input/autocomplete-controller.js +61 -34
  25. package/dist/app/input/input-action-controller.d.ts +2 -0
  26. package/dist/app/input/input-action-controller.js +8 -1
  27. package/dist/app/input/input-controller.d.ts +2 -1
  28. package/dist/app/input/input-controller.js +7 -2
  29. package/dist/app/input/prompt-enhancer-controller.js +53 -35
  30. package/dist/app/input/voice-controller.d.ts +41 -45
  31. package/dist/app/input/voice-controller.js +351 -419
  32. package/dist/app/model/model-usage-controller.js +17 -7
  33. package/dist/app/model/model-usage-status.d.ts +4 -1
  34. package/dist/app/model/model-usage-status.js +270 -46
  35. package/dist/app/popup/menu-items-controller.d.ts +13 -3
  36. package/dist/app/popup/menu-items-controller.js +37 -21
  37. package/dist/app/popup/popup-action-controller.d.ts +12 -2
  38. package/dist/app/popup/popup-action-controller.js +77 -24
  39. package/dist/app/popup/popup-menu-controller.d.ts +36 -14
  40. package/dist/app/popup/popup-menu-controller.js +239 -69
  41. package/dist/app/rendering/dcp-stats.d.ts +9 -4
  42. package/dist/app/rendering/dcp-stats.js +40 -425
  43. package/dist/app/rendering/editor-panels.js +10 -4
  44. package/dist/app/rendering/popup-menu-renderer.d.ts +3 -5
  45. package/dist/app/rendering/popup-menu-renderer.js +42 -37
  46. package/dist/app/rendering/render-controller.js +23 -2
  47. package/dist/app/rendering/status-line-renderer.d.ts +5 -0
  48. package/dist/app/rendering/status-line-renderer.js +41 -5
  49. package/dist/app/rendering/tab-line-renderer.js +26 -20
  50. package/dist/app/runtime.d.ts +12 -1
  51. package/dist/app/runtime.js +120 -12
  52. package/dist/app/screen/mouse-controller.d.ts +2 -0
  53. package/dist/app/screen/mouse-controller.js +17 -7
  54. package/dist/app/screen/status-controller.d.ts +4 -0
  55. package/dist/app/screen/status-controller.js +5 -0
  56. package/dist/app/session/lazy-session-manager.js +34 -0
  57. package/dist/app/session/session-event-controller.d.ts +1 -0
  58. package/dist/app/session/session-event-controller.js +10 -1
  59. package/dist/app/session/session-history.d.ts +1 -0
  60. package/dist/app/session/session-history.js +12 -1
  61. package/dist/app/session/session-lifecycle-controller.d.ts +7 -1
  62. package/dist/app/session/session-lifecycle-controller.js +16 -1
  63. package/dist/app/session/tabs-controller.d.ts +26 -1
  64. package/dist/app/session/tabs-controller.js +454 -147
  65. package/dist/app/subagents/subagents-files.js +60 -1
  66. package/dist/app/subagents/subagents-model.d.ts +1 -0
  67. package/dist/app/subagents/subagents-model.js +18 -2
  68. package/dist/app/subagents/subagents-widget-controller.d.ts +1 -0
  69. package/dist/app/subagents/subagents-widget-controller.js +6 -0
  70. package/dist/app/types.d.ts +16 -1
  71. package/dist/app/workspace/workspace-actions-controller.js +10 -2
  72. package/dist/app/workspace/workspace-undo.d.ts +1 -0
  73. package/dist/app/workspace/workspace-undo.js +1 -0
  74. package/dist/bundled-extensions/question/index.js +9 -1
  75. package/dist/bundled-extensions/question/remote.d.ts +4 -0
  76. package/dist/bundled-extensions/question/remote.js +33 -0
  77. package/dist/bundled-extensions/telegram-connector/bot.d.ts +43 -0
  78. package/dist/bundled-extensions/telegram-connector/bot.js +166 -0
  79. package/dist/bundled-extensions/telegram-connector/config.d.ts +8 -0
  80. package/dist/bundled-extensions/telegram-connector/config.js +87 -0
  81. package/dist/bundled-extensions/telegram-connector/coordinator.d.ts +66 -0
  82. package/dist/bundled-extensions/telegram-connector/coordinator.js +413 -0
  83. package/dist/bundled-extensions/telegram-connector/index.d.ts +3 -0
  84. package/dist/bundled-extensions/telegram-connector/index.js +195 -0
  85. package/dist/bundled-extensions/terminal-bell/index.d.ts +0 -8
  86. package/dist/bundled-extensions/terminal-bell/index.js +0 -76
  87. package/dist/bundled-extensions/workspace-undo/index.d.ts +26 -0
  88. package/dist/bundled-extensions/workspace-undo/index.js +191 -0
  89. package/dist/config.d.ts +21 -2
  90. package/dist/config.js +159 -32
  91. package/dist/default-pix-config.js +25 -5
  92. package/dist/schemas/index.d.ts +1 -0
  93. package/dist/schemas/index.js +1 -0
  94. package/dist/schemas/pi-tools-suite-schema.d.ts +88 -62
  95. package/dist/schemas/pi-tools-suite-schema.js +54 -83
  96. package/dist/schemas/pix-schema.d.ts +19 -2
  97. package/dist/schemas/pix-schema.js +47 -5
  98. package/dist/schemas/tasks-schema.d.ts +18 -0
  99. package/dist/schemas/tasks-schema.js +41 -0
  100. package/docs/concurrency.md +9 -1
  101. package/docs/desktop-mvp.md +22 -6
  102. package/docs/desktop-task-manager.md +148 -84
  103. package/docs/release.md +30 -6
  104. package/external/pi-tools-suite/README.md +197 -84
  105. package/external/pi-tools-suite/docs/evals.md +1 -1
  106. package/external/pi-tools-suite/docs/session-recovery.md +47 -14
  107. package/external/pi-tools-suite/docs/subagent-model-pools.md +48 -30
  108. package/external/pi-tools-suite/docs/ui-qa-subagent.md +440 -0
  109. package/external/pi-tools-suite/package.json +6 -1
  110. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +2 -1
  111. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +9 -3
  112. package/external/pi-tools-suite/src/antigravity-auth/headers.ts +41 -3
  113. package/external/pi-tools-suite/src/antigravity-auth/models.ts +94 -21
  114. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +5 -17
  115. package/external/pi-tools-suite/src/antigravity-auth/payload.ts +72 -7
  116. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +13 -1
  117. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  118. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +1 -1
  119. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  120. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  121. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/browser.mjs +346 -0
  122. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/desktop.mjs +1038 -0
  123. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/backends/tui.mjs +759 -0
  124. package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/scripts/browser-qa-runner.mjs +32 -23
  125. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/chrome-devtools/chrome-devtools-provider.mjs +895 -0
  126. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/linux/linux-atspi.py +494 -0
  127. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/macos/macos-accessibility.swift +1085 -0
  128. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/bridge-client.mjs +50 -0
  129. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/native-terminal/native-terminal-host.mjs +501 -0
  130. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/drivers/windows/windows-uia.ps1 +454 -0
  131. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-auth.md +114 -0
  132. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-chrome-devtools.md +91 -0
  133. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser-playwright.md +145 -0
  134. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/browser.md +82 -0
  135. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-linux.md +26 -0
  136. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-macos.md +29 -0
  137. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop-windows.md +23 -0
  138. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/desktop.md +54 -0
  139. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-native-terminal.md +59 -0
  140. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui-pty.md +37 -0
  141. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/guides/tui.md +60 -0
  142. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa/scripts/ui-qa-runner.mjs +535 -0
  143. package/external/pi-tools-suite/src/async-subagents/agents/ui-qa.md +75 -0
  144. package/external/pi-tools-suite/src/async-subagents/commands.ts +15 -71
  145. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  146. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  147. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  148. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -5
  149. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +16 -2
  150. package/external/pi-tools-suite/src/async-subagents/core/config.ts +160 -327
  151. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +2 -1
  152. package/external/pi-tools-suite/src/async-subagents/core/registry.ts +158 -33
  153. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +21 -5
  154. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +40 -26
  155. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  156. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  157. package/external/pi-tools-suite/src/async-subagents/core/ultrawork-auto.ts +51 -45
  158. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  159. package/external/pi-tools-suite/src/async-subagents/lib.ts +16 -9
  160. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -5
  161. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +5 -4
  162. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  163. package/external/pi-tools-suite/src/coding-discipline/index.ts +90 -59
  164. package/external/pi-tools-suite/src/config.ts +55 -1
  165. package/external/pi-tools-suite/src/context-gateway/accounting-log.ts +282 -0
  166. package/external/pi-tools-suite/src/context-gateway/config.ts +77 -3
  167. package/external/pi-tools-suite/src/context-gateway/efficiency.ts +414 -0
  168. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +222 -0
  169. package/external/pi-tools-suite/src/context-gateway/index.ts +206 -19
  170. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +7 -7
  171. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  172. package/external/pi-tools-suite/src/context-gateway/types.ts +16 -3
  173. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  174. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +39 -4
  175. package/external/pi-tools-suite/src/dcp/auto-compress.ts +128 -49
  176. package/external/pi-tools-suite/src/dcp/commands.ts +32 -156
  177. package/external/pi-tools-suite/src/dcp/compress-tool.ts +177 -46
  178. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  179. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  180. package/external/pi-tools-suite/src/dcp/compression-progress.ts +22 -0
  181. package/external/pi-tools-suite/src/dcp/config.ts +114 -17
  182. package/external/pi-tools-suite/src/dcp/conversation-index.ts +36 -9
  183. package/external/pi-tools-suite/src/dcp/diagnostics.ts +41 -0
  184. package/external/pi-tools-suite/src/dcp/fresh-tool-results.ts +38 -0
  185. package/external/pi-tools-suite/src/dcp/index.ts +338 -66
  186. package/external/pi-tools-suite/src/dcp/journal.ts +126 -4
  187. package/external/pi-tools-suite/src/dcp/progress-controller.ts +2 -1
  188. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  189. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  190. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +93 -3
  191. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  192. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +36 -33
  193. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  194. package/external/pi-tools-suite/src/dcp/pruner.ts +1 -0
  195. package/external/pi-tools-suite/src/dcp/routine-pressure.ts +86 -0
  196. package/external/pi-tools-suite/src/dcp/state.ts +9 -0
  197. package/external/pi-tools-suite/src/dcp/statistics.d.ts +8 -0
  198. package/external/pi-tools-suite/src/dcp/statistics.js +156 -0
  199. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +78 -22
  200. package/external/pi-tools-suite/src/index.ts +7 -1
  201. package/external/pi-tools-suite/src/lib/project.ts +36 -1
  202. package/external/pi-tools-suite/src/model-tools/index.ts +10 -7
  203. package/external/pi-tools-suite/src/repo-discovery/index.ts +304 -4
  204. package/external/pi-tools-suite/src/resource-registry/index.ts +2551 -0
  205. package/external/pi-tools-suite/src/session-recovery/index.ts +17 -0
  206. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  207. package/external/pi-tools-suite/src/todo/index.ts +21 -0
  208. package/external/pi-tools-suite/src/todo/todo.ts +19 -1
  209. package/external/pi-tools-suite/src/tool-descriptions.ts +47 -19
  210. package/package.json +10 -9
  211. package/schemas/pi-tools-suite.json +466 -287
  212. package/schemas/pix.json +129 -11
  213. package/schemas/tasks.json +131 -0
  214. package/skills/simplify/SKILL.md +33 -5
  215. package/docs/desktop-markdown-media.md +0 -77
  216. package/external/pi-tools-suite/docs/browser-qa-subagent.md +0 -177
  217. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  218. package/external/pi-tools-suite/src/async-subagents/agents/browser-qa.md +0 -598
  219. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  220. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  221. package/skills/playwright-cli/SKILL.md +0 -420
  222. package/skills/playwright-cli/references/element-attributes.md +0 -23
  223. package/skills/playwright-cli/references/playwright-tests.md +0 -50
  224. package/skills/playwright-cli/references/request-mocking.md +0 -87
  225. package/skills/playwright-cli/references/running-code.md +0 -241
  226. package/skills/playwright-cli/references/session-management.md +0 -273
  227. package/skills/playwright-cli/references/spec-driven-testing.md +0 -311
  228. package/skills/playwright-cli/references/storage-state.md +0 -290
  229. package/skills/playwright-cli/references/test-generation.md +0 -142
  230. package/skills/playwright-cli/references/tracing.md +0 -154
  231. package/skills/playwright-cli/references/video-recording.md +0 -147
  232. package/skills/spec-lite/SKILL.md +0 -140
  233. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-auth.example.jsonc +0 -0
  234. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/examples/qa-flow.example.jsonc +0 -0
  235. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.LICENSE +0 -0
  236. /package/external/pi-tools-suite/src/async-subagents/agents/{browser-qa → ui-qa/browser}/vendor/fflate.mjs +0 -0
@@ -0,0 +1,414 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ import { contextGatewayReadIdentity } from "./telemetry.js";
4
+ import type { ContextGatewayObservation } from "./types.js";
5
+
6
+ type ToolCallLike = {
7
+ toolCallId?: unknown;
8
+ toolName?: unknown;
9
+ input?: unknown;
10
+ };
11
+
12
+ type ToolResultLike = {
13
+ toolCallId?: unknown;
14
+ toolName?: unknown;
15
+ content?: unknown;
16
+ details?: unknown;
17
+ isError?: unknown;
18
+ };
19
+
20
+ type AssistantMessageLike = {
21
+ provider?: unknown;
22
+ model?: unknown;
23
+ stopReason?: unknown;
24
+ usage?: unknown;
25
+ };
26
+
27
+ export type ContextGatewayRetrievalKind =
28
+ | "artifact"
29
+ | "session-recovery"
30
+ | "repeat-read"
31
+ | "read-continuation";
32
+
33
+ export interface ContextGatewayEfficiencySnapshot {
34
+ toolCalls: number;
35
+ toolResults: number;
36
+ enforcedResults: number;
37
+ sourceContentBytes: number;
38
+ deliveredContentBytes: number;
39
+ grossBytesSaved: number;
40
+ grossEstimatedTokensSaved: number;
41
+ retrievalCalls: number;
42
+ retrievalResults: number;
43
+ retrievalBytes: number;
44
+ retrievalEstimatedTokens: number;
45
+ conservativeNetBytes: number;
46
+ conservativeNetEstimatedTokens: number;
47
+ providerAttempts: number;
48
+ providerCompletions: number;
49
+ providerInputTokens: number;
50
+ providerOutputTokens: number;
51
+ providerCacheReadTokens: number;
52
+ providerCacheWriteTokens: number;
53
+ providerTotalTokens: number;
54
+ providerCost: number;
55
+ byToolClass: Partial<Record<ContextGatewayObservation["toolClass"], {
56
+ results: number;
57
+ enforcedResults: number;
58
+ sourceContentBytes: number;
59
+ deliveredContentBytes: number;
60
+ grossBytesSaved: number;
61
+ grossEstimatedTokensSaved: number;
62
+ }>>;
63
+ byRetrievalKind: Partial<Record<ContextGatewayRetrievalKind, {
64
+ calls: number;
65
+ results: number;
66
+ bytes: number;
67
+ estimatedTokens: number;
68
+ }>>;
69
+ }
70
+
71
+ export interface ContextGatewayEfficiencyToolCall {
72
+ callRef: string;
73
+ toolName: string;
74
+ retrievalKind?: ContextGatewayRetrievalKind;
75
+ }
76
+
77
+ export interface ContextGatewayEfficiencyToolResult extends ContextGatewayEfficiencyToolCall {
78
+ toolClass: ContextGatewayObservation["toolClass"];
79
+ outcome: ContextGatewayObservation["outcome"];
80
+ representation: ContextGatewayObservation["delivery"]["representation"];
81
+ overBudget: boolean;
82
+ potentialBytesOverBudget: number;
83
+ sourceContentBytes: number;
84
+ deliveredContentBytes: number;
85
+ sourceEstimatedTokens: number;
86
+ deliveredEstimatedTokens: number;
87
+ grossBytesSaved: number;
88
+ grossEstimatedTokensSaved: number;
89
+ retrievalBytes: number;
90
+ retrievalEstimatedTokens: number;
91
+ }
92
+
93
+ export interface ContextGatewayProviderCompletion {
94
+ provider?: string;
95
+ model?: string;
96
+ stopReason?: string;
97
+ attemptsSincePreviousCompletion: number;
98
+ usage: {
99
+ input: number;
100
+ output: number;
101
+ cacheRead: number;
102
+ cacheWrite: number;
103
+ totalTokens: number;
104
+ cost: number;
105
+ };
106
+ }
107
+
108
+ type BoundCall = ContextGatewayEfficiencyToolCall;
109
+ type TokenCounter = ((text: string) => number) | undefined;
110
+
111
+ const MAX_TRACKED_BINDINGS = 4_096;
112
+ const MAX_TRACKED_READ_IDENTITIES = 4_096;
113
+ const MAX_TRACKED_ARTIFACT_SOURCES = 4_096;
114
+
115
+ let optionalTokenCounter: TokenCounter | null = null;
116
+
117
+ function loadOptionalTokenCounter(): TokenCounter {
118
+ if (optionalTokenCounter !== null) return optionalTokenCounter;
119
+ try {
120
+ const require = createRequire(import.meta.url);
121
+ const tokenizer = require("@anthropic-ai/tokenizer") as {
122
+ countTokens?: unknown;
123
+ default?: { countTokens?: unknown };
124
+ };
125
+ const counter = tokenizer.countTokens ?? tokenizer.default?.countTokens;
126
+ optionalTokenCounter = typeof counter === "function" ? counter as (text: string) => number : undefined;
127
+ } catch {
128
+ optionalTokenCounter = undefined;
129
+ }
130
+ return optionalTokenCounter;
131
+ }
132
+
133
+ export function estimateContextGatewayTokens(text: string): number {
134
+ if (!text) return 0;
135
+ const counter = loadOptionalTokenCounter();
136
+ if (counter) {
137
+ try {
138
+ const counted = counter(text);
139
+ if (Number.isFinite(counted)) return Math.max(0, Math.round(counted));
140
+ } catch {
141
+ // Fall through to the same cheap approximation used when no tokenizer is installed.
142
+ }
143
+ }
144
+ return Math.max(0, Math.round(text.length / 4));
145
+ }
146
+
147
+ function contentText(content: unknown): string {
148
+ if (typeof content === "string") return content;
149
+ if (!Array.isArray(content)) return "";
150
+ let text = "";
151
+ for (const part of content) {
152
+ if (!part || typeof part !== "object" || Array.isArray(part)) continue;
153
+ const block = part as Record<string, unknown>;
154
+ if (block.type === "text" && typeof block.text === "string") text += block.text;
155
+ }
156
+ return text;
157
+ }
158
+
159
+ function finiteNonNegative(value: unknown): number {
160
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
161
+ }
162
+
163
+ function assistantUsage(message: AssistantMessageLike): ContextGatewayProviderCompletion["usage"] {
164
+ const usage = message.usage && typeof message.usage === "object" && !Array.isArray(message.usage)
165
+ ? message.usage as Record<string, unknown>
166
+ : {};
167
+ const costValue = usage.cost && typeof usage.cost === "object" && !Array.isArray(usage.cost)
168
+ ? finiteNonNegative((usage.cost as Record<string, unknown>).total)
169
+ : 0;
170
+ return {
171
+ input: finiteNonNegative(usage.input),
172
+ output: finiteNonNegative(usage.output),
173
+ cacheRead: finiteNonNegative(usage.cacheRead),
174
+ cacheWrite: finiteNonNegative(usage.cacheWrite),
175
+ totalTokens: finiteNonNegative(usage.totalTokens),
176
+ cost: costValue,
177
+ };
178
+ }
179
+
180
+ function retrievalKindForToolName(toolName: string): ContextGatewayRetrievalKind | undefined {
181
+ if (toolName === "artifact_read" || toolName === "artifact_search") return "artifact";
182
+ if (["session_overview", "session_read_section", "session_search", "session_recovery_context"].includes(toolName)) {
183
+ return "session-recovery";
184
+ }
185
+ return undefined;
186
+ }
187
+
188
+ function artifactHandlePaths(details: unknown): string[] {
189
+ if (!details || typeof details !== "object" || Array.isArray(details)) return [];
190
+ const record = details as Record<string, unknown>;
191
+ const paths: string[] = [];
192
+ for (const key of ["fullOutputPath", "artifactPath", "outputPath"]) {
193
+ const value = record[key];
194
+ if (typeof value === "string" && value.trim()) paths.push(value);
195
+ }
196
+ const artifacts = record.artifacts;
197
+ if (artifacts && typeof artifacts === "object" && !Array.isArray(artifacts)) {
198
+ for (const value of Object.values(artifacts as Record<string, unknown>)) {
199
+ if (typeof value === "string" && value.trim()) paths.push(value);
200
+ }
201
+ }
202
+ return paths;
203
+ }
204
+
205
+ function rememberBounded(set: Set<string>, value: string, maximum: number): void {
206
+ if (set.delete(value)) {
207
+ set.add(value);
208
+ return;
209
+ }
210
+ if (set.size >= maximum) {
211
+ const oldest = set.values().next().value;
212
+ if (oldest !== undefined) set.delete(oldest);
213
+ }
214
+ set.add(value);
215
+ }
216
+
217
+ function bindBounded(map: Map<string, BoundCall>, key: string, value: BoundCall): void {
218
+ if (!map.has(key) && map.size >= MAX_TRACKED_BINDINGS) {
219
+ const oldest = map.keys().next().value;
220
+ if (oldest !== undefined) map.delete(oldest);
221
+ }
222
+ map.set(key, value);
223
+ }
224
+
225
+ function emptySnapshot(): ContextGatewayEfficiencySnapshot {
226
+ return {
227
+ toolCalls: 0,
228
+ toolResults: 0,
229
+ enforcedResults: 0,
230
+ sourceContentBytes: 0,
231
+ deliveredContentBytes: 0,
232
+ grossBytesSaved: 0,
233
+ grossEstimatedTokensSaved: 0,
234
+ retrievalCalls: 0,
235
+ retrievalResults: 0,
236
+ retrievalBytes: 0,
237
+ retrievalEstimatedTokens: 0,
238
+ conservativeNetBytes: 0,
239
+ conservativeNetEstimatedTokens: 0,
240
+ providerAttempts: 0,
241
+ providerCompletions: 0,
242
+ providerInputTokens: 0,
243
+ providerOutputTokens: 0,
244
+ providerCacheReadTokens: 0,
245
+ providerCacheWriteTokens: 0,
246
+ providerTotalTokens: 0,
247
+ providerCost: 0,
248
+ byToolClass: {},
249
+ byRetrievalKind: {},
250
+ };
251
+ }
252
+
253
+ export class ContextGatewayEfficiencyTracker {
254
+ private totals = emptySnapshot();
255
+ private bindings = new Map<string, BoundCall>();
256
+ private seenExactReads = new Set<string>();
257
+ private seenReadSources = new Set<string>();
258
+ private artifactReadSources = new Set<string>();
259
+ private callSequence = 0;
260
+ private attemptsAtPreviousCompletion = 0;
261
+
262
+ recordToolCall(event: ToolCallLike): ContextGatewayEfficiencyToolCall | undefined {
263
+ if (typeof event.toolCallId !== "string" || typeof event.toolName !== "string") return undefined;
264
+ const toolName = event.toolName.trim().toLowerCase();
265
+ let retrievalKind = retrievalKindForToolName(toolName);
266
+ if (toolName === "read") {
267
+ const identity = contextGatewayReadIdentity(event.input);
268
+ if (identity.source && this.artifactReadSources.has(identity.source)) {
269
+ retrievalKind = "artifact";
270
+ rememberBounded(this.artifactReadSources, identity.source, MAX_TRACKED_ARTIFACT_SOURCES);
271
+ }
272
+ else if (identity.exact && this.seenExactReads.has(identity.exact)) retrievalKind = "repeat-read";
273
+ else if (identity.source && this.seenReadSources.has(identity.source)) retrievalKind = "read-continuation";
274
+ if (identity.exact) rememberBounded(this.seenExactReads, identity.exact, MAX_TRACKED_READ_IDENTITIES);
275
+ if (identity.source) rememberBounded(this.seenReadSources, identity.source, MAX_TRACKED_READ_IDENTITIES);
276
+ }
277
+
278
+ const binding: BoundCall = {
279
+ callRef: `c${++this.callSequence}`,
280
+ toolName,
281
+ ...(retrievalKind ? { retrievalKind } : {}),
282
+ };
283
+ bindBounded(this.bindings, event.toolCallId, binding);
284
+ this.totals.toolCalls += 1;
285
+ if (retrievalKind) {
286
+ this.totals.retrievalCalls += 1;
287
+ const counters = this.totals.byRetrievalKind[retrievalKind] ?? { calls: 0, results: 0, bytes: 0, estimatedTokens: 0 };
288
+ counters.calls += 1;
289
+ this.totals.byRetrievalKind[retrievalKind] = counters;
290
+ }
291
+ return { ...binding };
292
+ }
293
+
294
+ recordToolResult(
295
+ event: ToolResultLike,
296
+ observation: ContextGatewayObservation,
297
+ deliveredContent: unknown = event.content,
298
+ ): ContextGatewayEfficiencyToolResult | undefined {
299
+ if (typeof event.toolCallId !== "string") return undefined;
300
+ const binding = this.bindings.get(event.toolCallId);
301
+ if (!binding) return undefined;
302
+ this.bindings.delete(event.toolCallId);
303
+
304
+ const sourceEstimatedTokens = estimateContextGatewayTokens(contentText(event.content));
305
+ const deliveredEstimatedTokens = estimateContextGatewayTokens(contentText(deliveredContent));
306
+ const grossEstimatedTokensSaved = Math.max(0, sourceEstimatedTokens - deliveredEstimatedTokens);
307
+ const grossBytesSaved = observation.actualBytesSaved;
308
+ const retrievalBytes = binding.retrievalKind ? observation.delivery.contentBytes : 0;
309
+ const retrievalEstimatedTokens = binding.retrievalKind ? deliveredEstimatedTokens : 0;
310
+
311
+ this.totals.toolResults += 1;
312
+ this.totals.sourceContentBytes += observation.source.contentBytes;
313
+ this.totals.deliveredContentBytes += observation.delivery.contentBytes;
314
+ if (observation.delivery.representation !== "passthrough") this.totals.enforcedResults += 1;
315
+ this.totals.grossBytesSaved += grossBytesSaved;
316
+ this.totals.grossEstimatedTokensSaved += grossEstimatedTokensSaved;
317
+ const classCounters = this.totals.byToolClass[observation.toolClass] ?? {
318
+ results: 0,
319
+ enforcedResults: 0,
320
+ sourceContentBytes: 0,
321
+ deliveredContentBytes: 0,
322
+ grossBytesSaved: 0,
323
+ grossEstimatedTokensSaved: 0,
324
+ };
325
+ classCounters.results += 1;
326
+ if (observation.delivery.representation !== "passthrough") classCounters.enforcedResults += 1;
327
+ classCounters.sourceContentBytes += observation.source.contentBytes;
328
+ classCounters.deliveredContentBytes += observation.delivery.contentBytes;
329
+ classCounters.grossBytesSaved += grossBytesSaved;
330
+ classCounters.grossEstimatedTokensSaved += grossEstimatedTokensSaved;
331
+ this.totals.byToolClass[observation.toolClass] = classCounters;
332
+ if (binding.retrievalKind) {
333
+ this.totals.retrievalResults += 1;
334
+ this.totals.retrievalBytes += retrievalBytes;
335
+ this.totals.retrievalEstimatedTokens += retrievalEstimatedTokens;
336
+ const counters = this.totals.byRetrievalKind[binding.retrievalKind]
337
+ ?? { calls: 0, results: 0, bytes: 0, estimatedTokens: 0 };
338
+ counters.results += 1;
339
+ counters.bytes += retrievalBytes;
340
+ counters.estimatedTokens += retrievalEstimatedTokens;
341
+ this.totals.byRetrievalKind[binding.retrievalKind] = counters;
342
+ }
343
+ this.refreshNet();
344
+ for (const artifactPath of artifactHandlePaths(event.details)) {
345
+ const source = contextGatewayReadIdentity({ path: artifactPath }).source;
346
+ if (source) rememberBounded(this.artifactReadSources, source, MAX_TRACKED_ARTIFACT_SOURCES);
347
+ }
348
+
349
+ return {
350
+ ...binding,
351
+ toolClass: observation.toolClass,
352
+ outcome: observation.outcome,
353
+ representation: observation.delivery.representation,
354
+ overBudget: observation.overBudget,
355
+ potentialBytesOverBudget: observation.potentialBytesOverBudget,
356
+ sourceContentBytes: observation.source.contentBytes,
357
+ deliveredContentBytes: observation.delivery.contentBytes,
358
+ sourceEstimatedTokens,
359
+ deliveredEstimatedTokens,
360
+ grossBytesSaved,
361
+ grossEstimatedTokensSaved,
362
+ retrievalBytes,
363
+ retrievalEstimatedTokens,
364
+ };
365
+ }
366
+
367
+ recordProviderAttempt(): number {
368
+ this.totals.providerAttempts += 1;
369
+ return this.totals.providerAttempts;
370
+ }
371
+
372
+ recordProviderCompletion(message: AssistantMessageLike): ContextGatewayProviderCompletion {
373
+ const usage = assistantUsage(message);
374
+ this.totals.providerCompletions += 1;
375
+ this.totals.providerInputTokens += usage.input;
376
+ this.totals.providerOutputTokens += usage.output;
377
+ this.totals.providerCacheReadTokens += usage.cacheRead;
378
+ this.totals.providerCacheWriteTokens += usage.cacheWrite;
379
+ this.totals.providerTotalTokens += usage.totalTokens;
380
+ this.totals.providerCost += usage.cost;
381
+ const attemptsSincePreviousCompletion = this.totals.providerAttempts - this.attemptsAtPreviousCompletion;
382
+ this.attemptsAtPreviousCompletion = this.totals.providerAttempts;
383
+ return {
384
+ ...(typeof message.provider === "string" ? { provider: message.provider } : {}),
385
+ ...(typeof message.model === "string" ? { model: message.model } : {}),
386
+ ...(typeof message.stopReason === "string" ? { stopReason: message.stopReason } : {}),
387
+ attemptsSincePreviousCompletion,
388
+ usage,
389
+ };
390
+ }
391
+
392
+ clearTransientBindings(): void {
393
+ this.bindings.clear();
394
+ }
395
+
396
+ reset(): void {
397
+ this.totals = emptySnapshot();
398
+ this.bindings.clear();
399
+ this.seenExactReads.clear();
400
+ this.seenReadSources.clear();
401
+ this.artifactReadSources.clear();
402
+ this.callSequence = 0;
403
+ this.attemptsAtPreviousCompletion = 0;
404
+ }
405
+
406
+ snapshot(): ContextGatewayEfficiencySnapshot {
407
+ return structuredClone(this.totals);
408
+ }
409
+
410
+ private refreshNet(): void {
411
+ this.totals.conservativeNetBytes = this.totals.grossBytesSaved - this.totals.retrievalBytes;
412
+ this.totals.conservativeNetEstimatedTokens = this.totals.grossEstimatedTokensSaved - this.totals.retrievalEstimatedTokens;
413
+ }
414
+ }
@@ -0,0 +1,222 @@
1
+ import type { ShellCommandClassification } from "../shell-command-policy.js";
2
+ import { parseTestBuildOutput, planProspectiveTestOutputDelivery } from "./test-output-parser.js";
3
+ import type { ContextGatewayToolClass } from "./types.js";
4
+
5
+ type ResultContent = unknown;
6
+
7
+ export interface ContextGatewayEnforcementPlan {
8
+ representation: "passthrough" | "test-build-compact" | "web-recoverable-compact";
9
+ content: ResultContent;
10
+ sourceContentBytes: number;
11
+ contentBytes: number;
12
+ textBytes: number;
13
+ reason: string;
14
+ }
15
+
16
+ function byteLengthJson(value: unknown): number {
17
+ if (value === undefined) return 0;
18
+ try {
19
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
20
+ } catch {
21
+ return 0;
22
+ }
23
+ }
24
+
25
+ function contentText(content: unknown): string {
26
+ if (typeof content === "string") return content;
27
+ if (!Array.isArray(content)) return "";
28
+ return content
29
+ .filter((part) => part && typeof part === "object" && !Array.isArray(part)
30
+ && (part as Record<string, unknown>).type === "text"
31
+ && typeof (part as Record<string, unknown>).text === "string")
32
+ .map((part) => (part as Record<string, unknown>).text as string)
33
+ .join("\n");
34
+ }
35
+
36
+ function textOnlyContent(content: unknown): boolean {
37
+ return Array.isArray(content)
38
+ && content.every((part) => part && typeof part === "object" && !Array.isArray(part)
39
+ && (part as Record<string, unknown>).type === "text"
40
+ && typeof (part as Record<string, unknown>).text === "string");
41
+ }
42
+
43
+ function isRecord(value: unknown): value is Record<string, unknown> {
44
+ return value !== null && typeof value === "object" && !Array.isArray(value);
45
+ }
46
+
47
+ function boundedTextBytes(text: string, maximumBytes: number): string {
48
+ const encoder = new TextEncoder();
49
+ if (encoder.encode(text).byteLength <= maximumBytes) return text;
50
+ const suffix = "\n… [compact view truncated; full structured result is recoverable from raw session details]";
51
+ const suffixBytes = encoder.encode(suffix).byteLength;
52
+ if (maximumBytes <= suffixBytes) {
53
+ let low = 0;
54
+ let high = text.length;
55
+ while (low < high) {
56
+ const mid = Math.ceil((low + high) / 2);
57
+ if (encoder.encode(text.slice(0, mid)).byteLength <= maximumBytes) low = mid;
58
+ else high = mid - 1;
59
+ }
60
+ return text.slice(0, low);
61
+ }
62
+ const target = Math.max(0, maximumBytes - suffixBytes);
63
+ let low = 0;
64
+ let high = text.length;
65
+ while (low < high) {
66
+ const mid = Math.ceil((low + high) / 2);
67
+ if (encoder.encode(text.slice(0, mid)).byteLength <= target) low = mid;
68
+ else high = mid - 1;
69
+ }
70
+ return `${text.slice(0, low).trimEnd()}${suffix}`;
71
+ }
72
+
73
+ function boundedProviderTextContent(text: string, maximumBytes: number): {
74
+ content: Array<{ type: "text"; text: string }>;
75
+ contentBytes: number;
76
+ textBytes: number;
77
+ } {
78
+ const limit = Math.max(1, Math.floor(maximumBytes));
79
+ let low = 1;
80
+ let high = limit;
81
+ let bestText = boundedTextBytes(text, 1);
82
+ let bestContent = [{ type: "text" as const, text: bestText }];
83
+ let bestContentBytes = byteLengthJson(bestContent);
84
+
85
+ while (low <= high) {
86
+ const textBudget = Math.floor((low + high) / 2);
87
+ const candidateText = boundedTextBytes(text, textBudget);
88
+ const candidateContent = [{ type: "text" as const, text: candidateText }];
89
+ const candidateBytes = byteLengthJson(candidateContent);
90
+ if (candidateBytes <= limit) {
91
+ bestText = candidateText;
92
+ bestContent = candidateContent;
93
+ bestContentBytes = candidateBytes;
94
+ low = textBudget + 1;
95
+ } else {
96
+ high = textBudget - 1;
97
+ }
98
+ }
99
+
100
+ return {
101
+ content: bestContent,
102
+ contentBytes: bestContentBytes,
103
+ textBytes: new TextEncoder().encode(bestText).byteLength,
104
+ };
105
+ }
106
+
107
+ function planRecoverableWebCompact(details: unknown, toolCallId: string | undefined, maximumBytes: number): string | undefined {
108
+ if (!isRecord(details)) return undefined;
109
+ const recovery = [
110
+ "[Context Gateway: over-budget web result compacted for provider context.]",
111
+ "Full structured source is retained in the raw session toolResult.details and can be recovered with session-recovery.",
112
+ toolCallId ? `Recovery key: toolCallId=${toolCallId}` : undefined,
113
+ ].filter((line): line is string => Boolean(line));
114
+
115
+ if (Array.isArray(details.results)) {
116
+ const results = details.results.filter(isRecord);
117
+ if (results.length === 0) return undefined;
118
+ const lines = [...recovery, `Search results: ${results.length}`];
119
+ for (let index = 0; index < results.length; index++) {
120
+ const result = results[index]!;
121
+ const title = typeof result.title === "string" ? result.title : `Result ${index + 1}`;
122
+ const url = typeof result.url === "string" ? result.url : "";
123
+ const content = typeof result.content === "string" ? result.content.replace(/\s+/g, " ").trim() : "";
124
+ lines.push(`${index + 1}. ${title}`);
125
+ if (url) lines.push(` URL: ${url}`);
126
+ if (content) lines.push(` ${content.slice(0, 900)}${content.length > 900 ? "…" : ""}`);
127
+ }
128
+ return boundedTextBytes(lines.join("\n"), maximumBytes);
129
+ }
130
+
131
+ if (typeof details.content === "string") {
132
+ const title = typeof details.title === "string" ? details.title : "Fetched document";
133
+ const links = Array.isArray(details.links) ? details.links.filter((link): link is string => typeof link === "string") : [];
134
+ const lines = [
135
+ ...recovery,
136
+ `Title: ${title}`,
137
+ `Content preview: ${details.content.slice(0, Math.max(1_000, maximumBytes))}`,
138
+ ...(links.length > 0 ? ["Links:", ...links.slice(0, 8).map((link) => ` - ${link}`)] : []),
139
+ ];
140
+ return boundedTextBytes(lines.join("\n"), maximumBytes);
141
+ }
142
+
143
+ return undefined;
144
+ }
145
+
146
+ function upstreamTruncated(details: unknown): boolean {
147
+ if (!details || typeof details !== "object" || Array.isArray(details)) return false;
148
+ const record = details as Record<string, unknown>;
149
+ if (record.truncated === true) return true;
150
+ const truncation = record.truncation;
151
+ return !!truncation && typeof truncation === "object" && !Array.isArray(truncation)
152
+ && (truncation as Record<string, unknown>).truncated === true;
153
+ }
154
+
155
+ /**
156
+ * Selective storeless enforcement. It never performs generic truncation.
157
+ * Recognised complete test/build output and structured recoverable web results
158
+ * have explicit compact contracts; other results remain passthrough.
159
+ */
160
+ export function planContextGatewayEnforcement(input: {
161
+ event: { content?: unknown; details?: unknown; isError?: boolean };
162
+ toolCallId?: string;
163
+ toolClass: ContextGatewayToolClass;
164
+ shell: ShellCommandClassification;
165
+ budgetBytes: number;
166
+ maxInlineBytes: number;
167
+ }): ContextGatewayEnforcementPlan {
168
+ const sourceText = contentText(input.event.content);
169
+ const sourceContentBytes = byteLengthJson(input.event.content);
170
+ const passthrough = (reason: string): ContextGatewayEnforcementPlan => ({
171
+ representation: "passthrough",
172
+ content: input.event.content,
173
+ sourceContentBytes,
174
+ contentBytes: sourceContentBytes,
175
+ textBytes: new TextEncoder().encode(sourceText).byteLength,
176
+ reason,
177
+ });
178
+
179
+ if (input.toolClass === "web") {
180
+ if (!textOnlyContent(input.event.content)) return passthrough("non-text-content");
181
+ if (sourceContentBytes <= input.budgetBytes) return passthrough("within-class-budget");
182
+ const compactText = planRecoverableWebCompact(input.event.details, input.toolCallId, input.maxInlineBytes);
183
+ if (!compactText) return passthrough("web-recovery-source-unavailable");
184
+ const bounded = boundedProviderTextContent(compactText, input.maxInlineBytes);
185
+ return {
186
+ representation: "web-recoverable-compact",
187
+ content: bounded.content,
188
+ sourceContentBytes,
189
+ contentBytes: bounded.contentBytes,
190
+ textBytes: bounded.textBytes,
191
+ reason: "recoverable-structured-web-details",
192
+ };
193
+ }
194
+
195
+ if (input.toolClass !== "shell") return passthrough("class-not-enforced");
196
+ if (input.shell.scope !== "simple" || input.shell.kind !== "test-build") {
197
+ return passthrough("shell-not-proven-test-build");
198
+ }
199
+ if (!textOnlyContent(input.event.content)) return passthrough("non-text-content");
200
+ if (sourceContentBytes <= input.budgetBytes) return passthrough("within-class-budget");
201
+
202
+ const parsed = parseTestBuildOutput({
203
+ text: sourceText,
204
+ hostOutcome: input.event.isError === true ? "error" : "success",
205
+ upstreamTruncated: upstreamTruncated(input.event.details),
206
+ });
207
+ const compact = planProspectiveTestOutputDelivery(parsed, input.maxInlineBytes, {
208
+ commandScope: input.shell.scope,
209
+ });
210
+ if (compact.decision !== "compact-candidate" || !compact.text) {
211
+ return passthrough(compact.reason);
212
+ }
213
+ const content = [{ type: "text" as const, text: compact.text }];
214
+ return {
215
+ representation: "test-build-compact",
216
+ content,
217
+ sourceContentBytes,
218
+ contentBytes: byteLengthJson(content),
219
+ textBytes: new TextEncoder().encode(compact.text).byteLength,
220
+ reason: compact.reason,
221
+ };
222
+ }