pi-ui-extend 1.0.39 → 1.0.41

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 (113) hide show
  1. package/README.md +1 -1
  2. package/dist/app/commands/command-registry.js +2 -2
  3. package/dist/app/commands/command-session-actions.d.ts +0 -1
  4. package/dist/app/commands/command-session-actions.js +22 -13
  5. package/dist/app/icons.d.ts +14 -0
  6. package/dist/app/icons.js +33 -0
  7. package/dist/app/rendering/conversation-tool-renderer.js +2 -2
  8. package/dist/app/rendering/dcp-stats.d.ts +6 -1
  9. package/dist/app/rendering/dcp-stats.js +214 -46
  10. package/dist/app/rendering/editor-panels.js +8 -5
  11. package/dist/app/session/lazy-session-manager.js +12 -1
  12. package/dist/app/session/tabs-controller.d.ts +2 -5
  13. package/dist/app/session/tabs-controller.js +12 -21
  14. package/dist/app/subagents/subagents-model.d.ts +14 -1
  15. package/dist/app/subagents/subagents-model.js +34 -15
  16. package/dist/app/types.d.ts +2 -0
  17. package/dist/bundled-extensions/session-title/config.js +1 -1
  18. package/dist/markdown-format.js +27 -9
  19. package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
  20. package/dist/schemas/pi-tools-suite-schema.js +46 -31
  21. package/external/pi-tools-suite/README.md +392 -52
  22. package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
  23. package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
  24. package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
  25. package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
  26. package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
  27. package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
  28. package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
  29. package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
  30. package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
  31. package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
  32. package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
  33. package/external/pi-tools-suite/docs/evals.md +684 -0
  34. package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
  35. package/external/pi-tools-suite/package.json +10 -3
  36. package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
  37. package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
  38. package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
  39. package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
  40. package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
  42. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
  44. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
  46. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
  47. package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
  49. package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
  50. package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
  52. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
  55. package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
  56. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
  57. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  58. package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
  59. package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
  60. package/external/pi-tools-suite/src/config.ts +1 -22
  61. package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
  62. package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
  63. package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
  64. package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
  65. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
  66. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
  67. package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
  68. package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
  69. package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
  70. package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
  71. package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
  72. package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
  73. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
  74. package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
  75. package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
  76. package/external/pi-tools-suite/src/dcp/config.ts +36 -61
  77. package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
  78. package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
  79. package/external/pi-tools-suite/src/dcp/index.ts +617 -203
  80. package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
  81. package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
  82. package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
  83. package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
  84. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
  85. package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
  86. package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
  87. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
  88. package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
  89. package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
  90. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
  91. package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
  92. package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
  93. package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
  94. package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
  95. package/external/pi-tools-suite/src/dcp/state.ts +158 -580
  96. package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
  98. package/external/pi-tools-suite/src/index.ts +9 -0
  99. package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
  100. package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
  101. package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
  102. package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
  103. package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
  104. package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
  105. package/package.json +6 -6
  106. package/schemas/pi-tools-suite.json +159 -78
  107. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
  108. package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
  109. package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
  110. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
  111. /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
  112. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
  113. /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
@@ -0,0 +1,429 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { accountContextGatewayParts } from "./accounting.js";
4
+ import { parseTestBuildOutput, planProspectiveTestOutputDelivery } from "./test-output-parser.js";
5
+ import type {
6
+ ContextGatewayClassTelemetry,
7
+ ContextGatewayNativePolicyReason,
8
+ ContextGatewayNativePolicyTelemetry,
9
+ ContextGatewayObservation,
10
+ ContextGatewayTelemetrySnapshot,
11
+ ContextGatewayTestOutputTelemetry,
12
+ ContextGatewayToolClass,
13
+ } from "./types.js";
14
+
15
+ type ToolCallLike = {
16
+ toolCallId?: unknown;
17
+ toolName?: unknown;
18
+ input?: unknown;
19
+ };
20
+
21
+ type ToolResultLike = {
22
+ toolCallId?: unknown;
23
+ toolName?: unknown;
24
+ content?: unknown;
25
+ details?: unknown;
26
+ isError?: unknown;
27
+ };
28
+
29
+ const NATIVE_POLICY_REASONS = new Set<ContextGatewayNativePolicyReason>([
30
+ "invalid-wrapper-budget",
31
+ "unknown-flag",
32
+ "missing-flag-value",
33
+ "duplicate-flag",
34
+ "invalid-flag-value",
35
+ "conflicting-flags",
36
+ "compact-limit-exceeded",
37
+ "full-limit-exceeded",
38
+ ]);
39
+
40
+ type CallBinding = {
41
+ toolClass: ContextGatewayToolClass;
42
+ inputFingerprint?: string;
43
+ shellCommandScope?: "simple" | "compound" | "unknown";
44
+ };
45
+
46
+ export interface ContextGatewayRolePartBytes {
47
+ userTextBytes: number;
48
+ assistantProseBytes: number;
49
+ assistantThinkingBytes: number;
50
+ assistantToolCallArgumentBytes: number;
51
+ toolResultTextBytes: number;
52
+ controlOrUnknownTextBytes: number;
53
+ metadataDetailsBytes: number;
54
+ messageWrapperBytes: number;
55
+ unknownParts: number;
56
+ }
57
+
58
+ function emptyCounters(): ContextGatewayClassTelemetry {
59
+ return {
60
+ results: 0,
61
+ errors: 0,
62
+ contentBytes: 0,
63
+ textBytes: 0,
64
+ imageBytes: 0,
65
+ detailsBytes: 0,
66
+ upstreamTruncatedResults: 0,
67
+ overBudgetResults: 0,
68
+ potentialBytesOverBudget: 0,
69
+ };
70
+ }
71
+
72
+ function stableValue(value: unknown): unknown {
73
+ if (Array.isArray(value)) return value.map(stableValue);
74
+ if (value !== null && typeof value === "object") {
75
+ const result: Record<string, unknown> = {};
76
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
77
+ result[key] = stableValue((value as Record<string, unknown>)[key]);
78
+ }
79
+ return result;
80
+ }
81
+ if (typeof value === "bigint") return value.toString();
82
+ return value;
83
+ }
84
+
85
+ function inputFingerprint(toolName: string, input: unknown): string {
86
+ let encoded = "";
87
+ try {
88
+ encoded = JSON.stringify(stableValue(input)) ?? "";
89
+ } catch {
90
+ encoded = "[unserializable]";
91
+ }
92
+ return createHash("sha256").update(`${toolName}\u0000${encoded}`).digest("hex");
93
+ }
94
+
95
+ function shellCommandScope(input: unknown): "simple" | "compound" | "unknown" {
96
+ if (!input || typeof input !== "object" || Array.isArray(input)) return "unknown";
97
+ const command = (input as Record<string, unknown>).command;
98
+ if (typeof command !== "string" || command.trim().length === 0) return "unknown";
99
+ // Conservative only: false positives merely keep passthrough. This does not
100
+ // parse, rewrite, approve, or execute the shell command.
101
+ return /[\r\n;|`]|&&|\$\(/.test(command) ? "compound" : "simple";
102
+ }
103
+
104
+ function byteLengthJson(value: unknown): number {
105
+ if (value === undefined) return 0;
106
+ try {
107
+ const seen = new WeakSet<object>();
108
+ const json = JSON.stringify(value, (_key, item) => {
109
+ if (typeof item === "bigint") return item.toString();
110
+ if (item !== null && typeof item === "object") {
111
+ if (seen.has(item)) return "[Circular]";
112
+ seen.add(item);
113
+ }
114
+ return item;
115
+ });
116
+ return json === undefined ? 0 : Buffer.byteLength(json, "utf8");
117
+ } catch {
118
+ return 0;
119
+ }
120
+ }
121
+
122
+ function contentSizes(content: unknown): { textBytes: number; imageBytes: number; contentBytes: number } {
123
+ let textBytes = 0;
124
+ let imageBytes = 0;
125
+ if (Array.isArray(content)) {
126
+ for (const raw of content) {
127
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
128
+ const block = raw as Record<string, unknown>;
129
+ if (block.type === "text" && typeof block.text === "string") {
130
+ textBytes += Buffer.byteLength(block.text, "utf8");
131
+ } else if (block.type === "image" && typeof block.data === "string") {
132
+ imageBytes += Math.floor((block.data.length * 3) / 4);
133
+ }
134
+ }
135
+ }
136
+ return { textBytes, imageBytes, contentBytes: byteLengthJson(content) };
137
+ }
138
+
139
+ function contentText(content: unknown): string {
140
+ if (!Array.isArray(content)) return "";
141
+ let text = "";
142
+ for (const raw of content) {
143
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
144
+ const block = raw as Record<string, unknown>;
145
+ if (block.type === "text" && typeof block.text === "string") text += block.text;
146
+ }
147
+ return text;
148
+ }
149
+
150
+ function emptyTestOutputTelemetry(): ContextGatewayTestOutputTelemetry {
151
+ return {
152
+ parserVersion: 1,
153
+ results: 0,
154
+ scanLimitedResults: 0,
155
+ compactCandidates: 0,
156
+ passthroughRecommended: 0,
157
+ byClassification: { recognised: 0, partial: 0, unrecognised: 0 },
158
+ byFormat: { "bun-test": 0, tap: 0, typescript: 0, mixed: 0, unknown: 0 },
159
+ };
160
+ }
161
+
162
+ function isTruncated(details: unknown): boolean {
163
+ if (!details || typeof details !== "object" || Array.isArray(details)) return false;
164
+ const truncation = (details as Record<string, unknown>).truncation;
165
+ return Boolean(
166
+ truncation
167
+ && typeof truncation === "object"
168
+ && !Array.isArray(truncation)
169
+ && (truncation as Record<string, unknown>).truncated === true,
170
+ );
171
+ }
172
+
173
+ function nativePolicyFromResult(event: ToolResultLike): {
174
+ refused: boolean;
175
+ outputMode: "compact" | "full";
176
+ reason?: ContextGatewayNativePolicyReason;
177
+ } | undefined {
178
+ const toolName = typeof event.toolName === "string" ? event.toolName.trim().toLowerCase() : "";
179
+ if (!toolName.startsWith("repo_")) return undefined;
180
+ if (!event.details || typeof event.details !== "object" || Array.isArray(event.details)) return undefined;
181
+ const candidate = (event.details as Record<string, unknown>).nativePolicy;
182
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) return undefined;
183
+ const policy = candidate as Record<string, unknown>;
184
+ if (policy.version !== 1 || policy.profile !== "native-compact") return undefined;
185
+ if (policy.outputMode !== "compact" && policy.outputMode !== "full") return undefined;
186
+ if (typeof policy.refused !== "boolean") return undefined;
187
+ const rawReason = typeof policy.reason === "string" ? policy.reason : undefined;
188
+ const reason = rawReason
189
+ ? (NATIVE_POLICY_REASONS.has(rawReason as ContextGatewayNativePolicyReason)
190
+ ? rawReason as ContextGatewayNativePolicyReason
191
+ : "unknown")
192
+ : undefined;
193
+ return { refused: policy.refused, outputMode: policy.outputMode, ...(reason ? { reason } : {}) };
194
+ }
195
+
196
+ export function classifyContextGatewayTool(toolName: string): ContextGatewayToolClass {
197
+ const normalized = toolName.trim().toLowerCase();
198
+ if (normalized === "read") return "code-read";
199
+ if (["bash", "shell", "powershell"].includes(normalized)) return "shell";
200
+ if (normalized === "repo_search") return "repo-search";
201
+ if (normalized === "repo_ast") return "repo-ast";
202
+ if (normalized === "repo_structure") return "repo-structure";
203
+ if (normalized === "ast_grep") return "ast-grep";
204
+ if (["write", "edit", "apply_patch", "ast_apply", "multiedit"].includes(normalized)) return "mutation";
205
+ if (["web_search", "web_fetch"].includes(normalized)) return "web";
206
+ if (normalized === "subagents" || normalized.startsWith("async_subagents_")) return "subagent";
207
+ return "other";
208
+ }
209
+
210
+ export function observeToolResult(
211
+ event: ToolResultLike,
212
+ maxResultBytes: number,
213
+ options: { toolClass?: ContextGatewayToolClass; shellCommandScope?: "simple" | "compound" | "unknown" } = {},
214
+ ): ContextGatewayObservation {
215
+ const toolName = typeof event.toolName === "string" ? event.toolName : "unknown";
216
+ const sizes = contentSizes(event.content);
217
+ const detailsBytes = byteLengthJson(event.details);
218
+ const potentialBytesOverBudget = Math.max(0, sizes.contentBytes - maxResultBytes);
219
+ const toolClass = options.toolClass ?? classifyContextGatewayTool(toolName);
220
+ const completeness = isTruncated(event.details) ? "upstream-truncated" : "unknown";
221
+ const observation: ContextGatewayObservation = {
222
+ version: 1,
223
+ toolClass,
224
+ outcome: event.isError === true ? "error" : "success",
225
+ source: {
226
+ kind: "tool-result-boundary",
227
+ completeness,
228
+ contentBytes: sizes.contentBytes,
229
+ textBytes: sizes.textBytes,
230
+ imageBytes: sizes.imageBytes,
231
+ detailsBytes,
232
+ },
233
+ view: { kind: "text-content", textBytes: sizes.textBytes },
234
+ delivery: {
235
+ representation: "passthrough",
236
+ contentBytes: sizes.contentBytes,
237
+ textBytes: sizes.textBytes,
238
+ },
239
+ overBudget: sizes.contentBytes > maxResultBytes,
240
+ potentialBytesOverBudget,
241
+ };
242
+ if (toolClass === "shell") {
243
+ const parsed = parseTestBuildOutput({
244
+ text: contentText(event.content),
245
+ hostOutcome: observation.outcome,
246
+ upstreamTruncated: completeness === "upstream-truncated",
247
+ });
248
+ const commandScope = options.shellCommandScope ?? "unknown";
249
+ const prospective = planProspectiveTestOutputDelivery(parsed, maxResultBytes, { commandScope });
250
+ observation.testOutput = {
251
+ parserVersion: 1,
252
+ classification: parsed.classification,
253
+ format: parsed.format,
254
+ scanLimited: parsed.scanLimited,
255
+ terminalSummarySeen: parsed.terminalSummarySeen,
256
+ diagnosticCount: parsed.diagnostics.filter((entry) => entry.severity === "error").length,
257
+ warningCount: parsed.diagnostics.filter((entry) => entry.severity === "warning").length,
258
+ commandScope,
259
+ prospectiveDecision: prospective.decision,
260
+ prospectiveReason: prospective.reason,
261
+ };
262
+ }
263
+ return observation;
264
+ }
265
+
266
+ export class ContextGatewayTelemetry {
267
+ private totals = emptyCounters();
268
+ private byClass: Partial<Record<ContextGatewayToolClass, ContextGatewayClassTelemetry>> = {};
269
+ private bindings = new Map<string, CallBinding>();
270
+ private seenInputFingerprints = new Set<string>();
271
+ private unboundResults = 0;
272
+ private repeatCandidateCount = 0;
273
+ private retrievalCalls = 0;
274
+ private nativePolicy: ContextGatewayNativePolicyTelemetry = {
275
+ results: 0,
276
+ refusals: 0,
277
+ fullOverrides: 0,
278
+ byReason: {},
279
+ };
280
+ private testOutput = emptyTestOutputTelemetry();
281
+ private lastObservation: ContextGatewayObservation | undefined;
282
+
283
+ recordToolCall(event: ToolCallLike): void {
284
+ if (typeof event.toolCallId !== "string" || typeof event.toolName !== "string") return;
285
+ const normalizedToolName = event.toolName.trim().toLowerCase();
286
+ if (normalizedToolName === "artifact_read" || normalizedToolName === "artifact_search") {
287
+ this.retrievalCalls += 1;
288
+ }
289
+ const fingerprint = normalizedToolName === "read"
290
+ ? inputFingerprint(event.toolName, event.input)
291
+ : undefined;
292
+ if (fingerprint) {
293
+ if (this.seenInputFingerprints.has(fingerprint)) this.repeatCandidateCount += 1;
294
+ else this.seenInputFingerprints.add(fingerprint);
295
+ }
296
+ this.bindings.set(event.toolCallId, {
297
+ toolClass: classifyContextGatewayTool(event.toolName),
298
+ inputFingerprint: fingerprint,
299
+ ...(classifyContextGatewayTool(event.toolName) === "shell"
300
+ ? { shellCommandScope: shellCommandScope(event.input) }
301
+ : {}),
302
+ });
303
+ }
304
+
305
+ recordToolResult(event: ToolResultLike, maxResultBytes: number): ContextGatewayObservation {
306
+ const binding = typeof event.toolCallId === "string" ? this.bindings.get(event.toolCallId) : undefined;
307
+ const observation = observeToolResult(event, maxResultBytes, {
308
+ ...(binding ? { toolClass: binding.toolClass } : {}),
309
+ ...(binding?.shellCommandScope ? { shellCommandScope: binding.shellCommandScope } : {}),
310
+ });
311
+ if (typeof event.toolCallId !== "string" || !binding) {
312
+ this.unboundResults += 1;
313
+ // Lifecycle/reload can route a late final result to a runner that did not
314
+ // observe the originating tool_call. Keep the diagnostic count, but do
315
+ // not attribute its bytes/class/parser policy to the current session or
316
+ // branch. `record()` remains the explicit API for standalone fixtures.
317
+ return observation;
318
+ }
319
+ this.bindings.delete(event.toolCallId);
320
+ this.aggregateObservation(event, observation);
321
+ return observation;
322
+ }
323
+
324
+ pendingCallCount(): number {
325
+ return this.bindings.size;
326
+ }
327
+
328
+ clearTransientBindings(): void {
329
+ this.bindings.clear();
330
+ }
331
+
332
+ private aggregateObservation(event: ToolResultLike, observation: ContextGatewayObservation): void {
333
+ this.lastObservation = observation;
334
+ const nativePolicy = nativePolicyFromResult(event);
335
+ if (nativePolicy) {
336
+ this.nativePolicy.results += 1;
337
+ if (nativePolicy.outputMode === "full") this.nativePolicy.fullOverrides += 1;
338
+ if (nativePolicy.refused) {
339
+ this.nativePolicy.refusals += 1;
340
+ const reason = nativePolicy.reason ?? "unknown";
341
+ this.nativePolicy.byReason[reason] = (this.nativePolicy.byReason[reason] ?? 0) + 1;
342
+ }
343
+ }
344
+ if (observation.testOutput) {
345
+ const parsed = observation.testOutput;
346
+ this.testOutput.results += 1;
347
+ if (parsed.scanLimited) this.testOutput.scanLimitedResults += 1;
348
+ this.testOutput.byClassification[parsed.classification] += 1;
349
+ this.testOutput.byFormat[parsed.format] += 1;
350
+ if (parsed.prospectiveDecision === "compact-candidate") this.testOutput.compactCandidates += 1;
351
+ else this.testOutput.passthroughRecommended += 1;
352
+ }
353
+ this.add(this.totals, observation);
354
+ const classCounters = this.byClass[observation.toolClass] ?? emptyCounters();
355
+ this.byClass[observation.toolClass] = classCounters;
356
+ this.add(classCounters, observation);
357
+ }
358
+
359
+ /** Backward-compatible convenience for observation-only callers without a tool_call event. */
360
+ record(event: ToolResultLike, maxResultBytes: number): ContextGatewayObservation {
361
+ const observation = observeToolResult(event, maxResultBytes);
362
+ this.aggregateObservation(event, observation);
363
+ return observation;
364
+ }
365
+
366
+ reset(): void {
367
+ this.totals = emptyCounters();
368
+ this.byClass = {};
369
+ this.bindings.clear();
370
+ this.seenInputFingerprints.clear();
371
+ this.unboundResults = 0;
372
+ this.repeatCandidateCount = 0;
373
+ this.retrievalCalls = 0;
374
+ this.nativePolicy = { results: 0, refusals: 0, fullOverrides: 0, byReason: {} };
375
+ this.testOutput = emptyTestOutputTelemetry();
376
+ this.lastObservation = undefined;
377
+ }
378
+
379
+ snapshot(): ContextGatewayTelemetrySnapshot {
380
+ return {
381
+ version: 1,
382
+ ...this.totals,
383
+ pendingCalls: this.bindings.size,
384
+ unboundResults: this.unboundResults,
385
+ repeatCandidateCount: this.repeatCandidateCount,
386
+ retrievalCalls: this.retrievalCalls,
387
+ nativePolicy: {
388
+ ...this.nativePolicy,
389
+ byReason: { ...this.nativePolicy.byReason },
390
+ },
391
+ testOutput: {
392
+ ...this.testOutput,
393
+ byClassification: { ...this.testOutput.byClassification },
394
+ byFormat: { ...this.testOutput.byFormat },
395
+ },
396
+ byClass: Object.fromEntries(
397
+ Object.entries(this.byClass).map(([key, value]) => [key, value ? { ...value } : value]),
398
+ ) as ContextGatewayTelemetrySnapshot["byClass"],
399
+ ...(this.lastObservation ? { lastObservation: structuredClone(this.lastObservation) } : {}),
400
+ };
401
+ }
402
+
403
+ private add(target: ContextGatewayClassTelemetry, observation: ContextGatewayObservation): void {
404
+ target.results += 1;
405
+ if (observation.outcome === "error") target.errors += 1;
406
+ target.contentBytes += observation.source.contentBytes;
407
+ target.textBytes += observation.source.textBytes;
408
+ target.imageBytes += observation.source.imageBytes;
409
+ target.detailsBytes += observation.source.detailsBytes;
410
+ if (observation.source.completeness === "upstream-truncated") target.upstreamTruncatedResults += 1;
411
+ if (observation.overBudget) target.overBudgetResults += 1;
412
+ target.potentialBytesOverBudget += observation.potentialBytesOverBudget;
413
+ }
414
+ }
415
+
416
+ export function classifyRoleParts(messages: readonly unknown[]): ContextGatewayRolePartBytes {
417
+ const accounting = accountContextGatewayParts(messages);
418
+ return {
419
+ userTextBytes: accounting.categories.userText.bytes,
420
+ assistantProseBytes: accounting.categories.assistantProse.bytes,
421
+ assistantThinkingBytes: accounting.categories.assistantThinking.bytes,
422
+ assistantToolCallArgumentBytes: accounting.categories.assistantToolCallArguments.bytes,
423
+ toolResultTextBytes: accounting.categories.toolResultText.bytes,
424
+ controlOrUnknownTextBytes: accounting.categories.controlOrUnknownText.bytes,
425
+ metadataDetailsBytes: accounting.metadataDetailsBytes,
426
+ messageWrapperBytes: accounting.messageWrapperBytes,
427
+ unknownParts: accounting.unknownParts,
428
+ };
429
+ }