@piagent/platform 1.5.5 → 1.6.0

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 (227) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +9 -8
  3. package/README.vi.md +9 -5
  4. package/adapters/web-frontend/profile.json +2 -4
  5. package/architecture/layers.json +18 -3
  6. package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
  7. package/benchmarks/deep-logic-v1/grade.mjs +272 -10
  8. package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
  9. package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
  10. package/benchmarks/deep-logic-v1/project/package.json +1 -1
  11. package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
  12. package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
  13. package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
  14. package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
  15. package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
  16. package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
  17. package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
  18. package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
  19. package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
  20. package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
  21. package/benchmarks/deep-logic-v1/suite.json +51 -3
  22. package/benchmarks/deep-logic-v1/variant.mjs +22 -1
  23. package/benchmarks/production-v1/spend-control.v1.json +68 -0
  24. package/benchmarks/production-v1/suite.json +42 -3
  25. package/catalog/capabilities.json +6 -6
  26. package/docs/command-reference-vietnamese.md +6 -6
  27. package/docs/context-engine.md +7 -1
  28. package/docs/distribution-standard.md +7 -7
  29. package/docs/en/architecture.md +1 -0
  30. package/docs/languages.json +4 -4
  31. package/docs/model-options.md +8 -5
  32. package/docs/operator-manual-vietnamese.md +13 -11
  33. package/docs/project-adapters.md +2 -2
  34. package/docs/publishing-for-teams.md +5 -0
  35. package/docs/quality-benchmark.md +271 -62
  36. package/docs/release-install-policy.md +10 -9
  37. package/docs/runtime-policy-design.md +4 -4
  38. package/docs/runtime-quality-baseline.md +13 -6
  39. package/docs/security-threat-model.md +2 -2
  40. package/docs/task-lifecycle-vietnamese.md +2 -2
  41. package/docs/team-onboarding.md +2 -2
  42. package/docs/usage-observability.md +7 -2
  43. package/docs/vi/architecture.md +1 -0
  44. package/docs-site/architecture.html +1 -1
  45. package/docs-site/benchmark.html +1 -1
  46. package/docs-site/commands.html +1 -1
  47. package/docs-site/context-engine.html +1 -1
  48. package/docs-site/en/architecture.html +1 -1
  49. package/docs-site/en/benchmark.html +1 -1
  50. package/docs-site/en/commands.html +1 -1
  51. package/docs-site/en/context-engine.html +1 -1
  52. package/docs-site/en/index.html +1 -1
  53. package/docs-site/en/mcp-approval.html +1 -1
  54. package/docs-site/en/mcp-auth.html +1 -1
  55. package/docs-site/en/mcp-servers.html +1 -1
  56. package/docs-site/en/mcp.html +1 -1
  57. package/docs-site/en/permissions.html +1 -1
  58. package/docs-site/en/profiles.html +1 -1
  59. package/docs-site/en/quickstart.html +1 -1
  60. package/docs-site/en/runtime.html +1 -1
  61. package/docs-site/en/scope.html +1 -1
  62. package/docs-site/en/security.html +1 -1
  63. package/docs-site/en/team.html +1 -1
  64. package/docs-site/en/whats-new.html +1 -1
  65. package/docs-site/en/workflows.html +1 -1
  66. package/docs-site/index.html +1 -1
  67. package/docs-site/mcp-approval.html +1 -1
  68. package/docs-site/mcp-auth.html +1 -1
  69. package/docs-site/mcp-servers.html +1 -1
  70. package/docs-site/mcp.html +1 -1
  71. package/docs-site/permissions.html +1 -1
  72. package/docs-site/profiles.html +1 -1
  73. package/docs-site/quickstart.html +1 -1
  74. package/docs-site/runtime.html +1 -1
  75. package/docs-site/scope.html +1 -1
  76. package/docs-site/security.html +1 -1
  77. package/docs-site/team.html +1 -1
  78. package/docs-site/whats-new.html +1 -1
  79. package/docs-site/workflows.html +1 -1
  80. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  81. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  82. package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
  83. package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
  84. package/evals/fs4-readiness-matrix.v1.json +2 -2
  85. package/evals/fs5-causal-arm.v1.json +2 -2
  86. package/evals/fs5-pilot-protocol.v1.json +2 -2
  87. package/evals/fs5-pilot-protocol.v4.json +6 -5
  88. package/evals/fs5-pilot-protocol.v5.json +7 -6
  89. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  90. package/package.json +3 -1
  91. package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
  92. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
  93. package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
  94. package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
  95. package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
  96. package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
  97. package/packages/piagent-core/benchmark/benchmark-core.js +732 -661
  98. package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
  99. package/packages/piagent-core/benchmark/benchmark-forensics.js +439 -4
  100. package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
  101. package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
  102. package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
  103. package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
  104. package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
  105. package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
  106. package/packages/piagent-core/benchmark/benchmark-record-validation.js +183 -0
  107. package/packages/piagent-core/benchmark/benchmark-report.js +116 -12
  108. package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
  109. package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
  110. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +782 -0
  111. package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
  112. package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
  113. package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
  114. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
  115. package/packages/piagent-core/benchmark/benchmark-suite.js +128 -2
  116. package/packages/piagent-core/benchmark/benchmark-summary-support.js +376 -0
  117. package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
  118. package/packages/piagent-core/benchmark/benchmark-usage.js +311 -0
  119. package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
  120. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
  121. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
  122. package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
  123. package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
  124. package/packages/piagent-core/extensions/acceptance-receipt.js +34 -11
  125. package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
  126. package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
  127. package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
  128. package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
  129. package/packages/piagent-core/extensions/context-engine.js +148 -145
  130. package/packages/piagent-core/extensions/context-evidence.js +48 -0
  131. package/packages/piagent-core/extensions/context-import-links.js +296 -0
  132. package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
  133. package/packages/piagent-core/extensions/criterion-context-pack.js +422 -7
  134. package/packages/piagent-core/extensions/criterion-graph.js +78 -8
  135. package/packages/piagent-core/extensions/guard-types.ts +2 -0
  136. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  137. package/packages/piagent-core/extensions/piagent-guard.ts +233 -41
  138. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  139. package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
  140. package/packages/piagent-core/extensions/task-state.js +40 -134
  141. package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
  142. package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
  143. package/packages/piagent-core/package.json +1 -1
  144. package/packages/piagent-core/policies/base-policy.json +2 -0
  145. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  146. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  147. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  148. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  149. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  150. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  151. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  152. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  153. package/packages/piagent-core/runtime/hooks/completion-hook.ts +5 -6
  154. package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
  155. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  156. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
  157. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  158. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
  159. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
  160. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  161. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  162. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  163. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
  164. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
  165. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  166. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  167. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  168. package/packages/piagent-core/runtime/orchestration/role-policy.ts +1 -1
  169. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  170. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
  171. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
  172. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  173. package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
  174. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  175. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  176. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +55 -2
  177. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  178. package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
  179. package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
  180. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
  181. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
  182. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  183. package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
  184. package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
  185. package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
  186. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  187. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  188. package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
  189. package/packages/piagent-core/security/sensitive-data.js +13 -218
  190. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  191. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  192. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  193. package/packages/piagent-core/security/sensitive-text.js +234 -0
  194. package/packages/piagent-core/subagents/piagent-worker.md +1 -1
  195. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
  196. package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
  197. package/packages/piagent-webui/dist/client/index.html +1 -1
  198. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  199. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  200. package/packages/piagent-webui/gateway/gateway-session-stream.ts +93 -7
  201. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  202. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  203. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  204. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  205. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
  206. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  207. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -124
  208. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  209. package/packages/piagent-webui/server/transcript-projection.ts +22 -4
  210. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  211. package/schemas/piagent-webui/catalog-v1.json +9 -0
  212. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  213. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  214. package/schemas/task-contract.schema.json +12 -1
  215. package/scripts/benchmark-runner-core.mjs +355 -359
  216. package/scripts/benchmark-runner-finalization.mjs +309 -0
  217. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  218. package/scripts/benchmark-runner-support.mjs +200 -0
  219. package/scripts/benchmark-session.mjs +95 -41
  220. package/scripts/configure-model-scope.sh +4 -3
  221. package/scripts/helper-evaluation.mjs +1 -1
  222. package/scripts/install-global.sh +2 -2
  223. package/scripts/pi-usage-history.mjs +37 -10
  224. package/scripts/setup.sh +2 -2
  225. package/scripts/verify-local.sh +5 -1
  226. package/templates/global/settings.json +2 -1
  227. package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
@@ -0,0 +1,60 @@
1
+ import { assertionsProveErrorMapping } from "./acceptance-error-classes.js";
2
+ import { executableRejectionAssertions } from "./acceptance-executable-evidence.js";
3
+
4
+ export function callableAssertionMode(bodies, name) {
5
+ const callable = bodies.get(name);
6
+ return callable ? (callable.asynchronous ? "rejects" : "throws") : undefined;
7
+ }
8
+
9
+ /** Keep executable rejection assertions bound to the file that imported them. */
10
+ export function boundRejectionTestEvidence(input) {
11
+ const codeByPath = new Map(input.testCodeEntries.map((entry) => [entry.path, entry.code]));
12
+ const cache = new Map();
13
+ const assertionsForBinding = (binding) => {
14
+ const key = `${binding.testPath}\u0000${binding.testName}`;
15
+ if (cache.has(key)) return cache.get(key);
16
+ const code = codeByPath.get(binding.testPath);
17
+ const assertions = typeof code === "string" ? executableRejectionAssertions(code, new Set([binding.testName]))
18
+ .filter((assertion) => assertion.targets.includes(binding.testName)) : [];
19
+ cache.set(key, assertions);
20
+ return assertions;
21
+ };
22
+ const requestedError = (assertion) => input.requestedErrors.length === 0
23
+ || input.requestedErrors.some((name) => assertion.errorClasses.includes(name));
24
+ const bindingGroups = input.bindings.map((binding) => {
25
+ const allAssertions = (binding.testBindings ?? []).flatMap(assertionsForBinding);
26
+ return { binding, allAssertions, assertions: allAssertions.filter(requestedError) };
27
+ });
28
+ const testCodes = input.testCodeEntries.length > 0
29
+ ? input.testCodeEntries.map((entry) => entry.code) : [input.fallbackTestCode];
30
+ const structuralGroups = input.structuralTargets.map((target) => {
31
+ const allAssertions = testCodes.flatMap((code) => executableRejectionAssertions(
32
+ code, new Set([`*.${target}`])
33
+ ).filter((assertion) => assertion.targets.includes(`*.${target}`)));
34
+ return { target, allAssertions, assertions: allAssertions.filter(requestedError) };
35
+ });
36
+ const groups = [...bindingGroups, ...structuralGroups];
37
+ const assertions = groups.flatMap((group) => group.assertions);
38
+ const assertionModesMatch = (items, targets, expectedMode) => {
39
+ const relevant = items.filter((item) => item.targets.some((target) => targets.includes(target)));
40
+ return Boolean(expectedMode) && relevant.length > 0 && relevant.every((item) => item.mode === expectedMode);
41
+ };
42
+ const mappingOk = input.requestedErrors.length <= 1 || Boolean(input.requestedErrorMapping
43
+ && groups.length > 0
44
+ && bindingGroups.every(({ binding, allAssertions }) => assertionsProveErrorMapping(
45
+ allAssertions, binding.testNames, input.requestedErrorMapping
46
+ ))
47
+ && structuralGroups.every(({ target, allAssertions }) => assertionsProveErrorMapping(
48
+ allAssertions, [`*.${target}`], input.requestedErrorMapping
49
+ )));
50
+ const targetOk = assertions.length > 0
51
+ && bindingGroups.every(({ binding, assertions: items }) => binding.testNames.length > 0 && items.length > 0)
52
+ && structuralGroups.every(({ assertions: items }) => items.length > 0);
53
+ const modeOk = bindingGroups.every(({ binding, assertions: items }) => assertionModesMatch(
54
+ items, binding.testNames, callableAssertionMode(input.bodyMaps.get(binding.sourcePath) ?? new Map(), binding.sourceName)
55
+ )) && structuralGroups.every(({ target, assertions: items }) => {
56
+ const matches = [...input.bodyMaps.values()].filter((bodies) => bodies.has(target));
57
+ return assertionModesMatch(items, [`*.${target}`], matches.length === 1 ? callableAssertionMode(matches[0], target) : undefined);
58
+ });
59
+ return { assertions, mappingOk, modeOk, targetOk };
60
+ }
@@ -0,0 +1,302 @@
1
+ function ratio(numerator, denominator) {
2
+ return denominator > 0 ? numerator / denominator : 0;
3
+ }
4
+
5
+ function normalizeRelative(value) {
6
+ return String(value ?? "").replaceAll("\\", "/").replace(/^\.\/+/, "").replace(/\/+/g, "/");
7
+ }
8
+
9
+ function taskBindings(events) {
10
+ return new Map(events
11
+ .filter((event) => event.event === "turn_task_bound" && typeof event.sessionId === "string" && typeof event.turnId === "string" && typeof event.taskRunId === "string")
12
+ .map((event) => [`${event.sessionId}\0${event.turnId}`, event.taskRunId]));
13
+ }
14
+
15
+ function evidenceCoverage(comparable, observed) {
16
+ return ratio(comparable, observed);
17
+ }
18
+
19
+ const MAX_CONTEXT_METRIC_AGGREGATE = 1_000_000_000;
20
+
21
+ function boundedContextMetricInteger(value, { positive = false } = {}) {
22
+ return Number.isSafeInteger(value)
23
+ && value >= (positive ? 1 : 0)
24
+ && value <= MAX_CONTEXT_METRIC_AGGREGATE;
25
+ }
26
+
27
+ export function editRecoveryContextMetrics(events) {
28
+ const sessionStarts = new Map();
29
+ for (const [index, event] of events.entries()) {
30
+ if (event.event !== "session_start" || typeof event.sessionId !== "string" || !event.sessionId) continue;
31
+ const existing = sessionStarts.get(event.sessionId) ?? { firstIndex: index, valid: true };
32
+ existing.valid &&= event.editRecoveryContextTelemetryVersion === 1;
33
+ sessionStarts.set(event.sessionId, existing);
34
+ }
35
+ const receipts = new Map(), results = new Map();
36
+ let invalidReceipts = 0, invalidResults = 0;
37
+ let recoveryFailures = 0, comparableRecoveryFailures = 0, suppressedRecoveryFailures = 0;
38
+ const recoveryKey = (event) => typeof event.sessionId === "string" && event.sessionId
39
+ && typeof event.toolCallId === "string" && event.toolCallId && event.toolCallId.length <= 200
40
+ ? `${event.sessionId}\0${event.toolCallId}` : "";
41
+ const recoveryPath = (value) => {
42
+ const normalized = normalizeRelative(value);
43
+ if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")
44
+ || normalized.startsWith("/") || /^[A-Za-z]:\//.test(normalized)) return "";
45
+ return normalized;
46
+ };
47
+ for (const [index, event] of events.entries()) {
48
+ if (event.event === "edit_recovery_context") {
49
+ const key = recoveryKey(event), targetPath = recoveryPath(event.targetPath);
50
+ const valid = key && targetPath && /^[a-f0-9]{64}$/.test(String(event.contentHash ?? ""))
51
+ && boundedContextMetricInteger(event.originalChars)
52
+ && boundedContextMetricInteger(event.injectedChars, { positive: true })
53
+ && event.injectedChars > event.originalChars
54
+ && boundedContextMetricInteger(event.injectedEstimatedTokens, { positive: true })
55
+ && event.sensitiveContentRedacted === false;
56
+ if (!valid) {
57
+ invalidReceipts += 1;
58
+ continue;
59
+ }
60
+ const values = receipts.get(key) ?? [];
61
+ values.push({ ...event, index, targetPath });
62
+ receipts.set(key, values);
63
+ continue;
64
+ }
65
+ if (event.event !== "tool_result") continue;
66
+ const recoveryFailure = ["edit-anchor-not-unique", "edit-anchor-stale"].includes(event.reasonCode);
67
+ if (!recoveryFailure) {
68
+ if (event.editRecoveryContext !== undefined) invalidResults += 1;
69
+ continue;
70
+ }
71
+ recoveryFailures += 1;
72
+ const key = recoveryKey(event), targetPath = recoveryPath(event.targetPath);
73
+ const valid = key && targetPath && String(event.toolName ?? "").toLowerCase() === "edit"
74
+ && event.isError === true && typeof event.editRecoveryContext === "boolean"
75
+ && (event.editRecoveryContext === true
76
+ ? boundedContextMetricInteger(event.editRecoveryInjectedChars, { positive: true })
77
+ && boundedContextMetricInteger(event.editRecoveryEstimatedTokens, { positive: true })
78
+ : event.editRecoveryInjectedChars === undefined && event.editRecoveryEstimatedTokens === undefined);
79
+ if (!valid) {
80
+ continue;
81
+ }
82
+ const capability = sessionStarts.get(event.sessionId);
83
+ if (capability?.valid === true && capability.firstIndex < index) {
84
+ comparableRecoveryFailures += 1;
85
+ if (event.editRecoveryContext === false) suppressedRecoveryFailures += 1;
86
+ }
87
+ if (event.editRecoveryContext === false) continue;
88
+ const values = results.get(key) ?? [];
89
+ values.push({ ...event, index, targetPath });
90
+ results.set(key, values);
91
+ }
92
+ const recoveryKeys = new Set([...receipts.keys(), ...results.keys()]);
93
+ let recoveryEvents = invalidReceipts + invalidResults;
94
+ let comparableEvents = 0;
95
+ let injectedChars = 0;
96
+ let injectedEstimatedTokens = 0;
97
+ for (const key of recoveryKeys) {
98
+ const receiptGroup = receipts.get(key) ?? [], resultGroup = results.get(key) ?? [];
99
+ recoveryEvents += Math.max(receiptGroup.length, resultGroup.length);
100
+ if (receiptGroup.length !== 1 || resultGroup.length !== 1) continue;
101
+ const receipt = receiptGroup[0], result = resultGroup[0];
102
+ const capability = sessionStarts.get(receipt.sessionId);
103
+ if (capability?.valid !== true || capability.firstIndex >= receipt.index
104
+ || receipt.index >= result.index || receipt.targetPath !== result.targetPath
105
+ || String(receipt.taskRunId ?? "") !== String(result.taskRunId ?? "")
106
+ || receipt.injectedChars !== result.editRecoveryInjectedChars
107
+ || receipt.injectedEstimatedTokens !== result.editRecoveryEstimatedTokens
108
+ || injectedChars + receipt.injectedChars > MAX_CONTEXT_METRIC_AGGREGATE
109
+ || injectedEstimatedTokens + receipt.injectedEstimatedTokens > MAX_CONTEXT_METRIC_AGGREGATE) continue;
110
+ comparableEvents += 1;
111
+ injectedChars += receipt.injectedChars;
112
+ injectedEstimatedTokens += receipt.injectedEstimatedTokens;
113
+ }
114
+ return {
115
+ editRecoveryContextEvents: recoveryEvents,
116
+ comparableEditRecoveryContextEvents: comparableEvents,
117
+ uncomparableEditRecoveryContextEvents: recoveryEvents - comparableEvents,
118
+ editRecoveryContextEvidenceCoverage: evidenceCoverage(comparableEvents, recoveryEvents),
119
+ editRecoveryContextCount: comparableEvents,
120
+ editRecoveryInjectedChars: injectedChars,
121
+ editRecoveryEstimatedTokens: injectedEstimatedTokens,
122
+ editRecoveryFailures: recoveryFailures,
123
+ comparableEditRecoveryFailures: comparableRecoveryFailures,
124
+ uncomparableEditRecoveryFailures: recoveryFailures - comparableRecoveryFailures,
125
+ editRecoveryFailureEvidenceCoverage: evidenceCoverage(comparableRecoveryFailures, recoveryFailures),
126
+ editRecoverySuppressedFailures: suppressedRecoveryFailures
127
+ };
128
+ }
129
+
130
+ export function contextMetricPartition(event, taskByTurn = new Map()) {
131
+ const sessionId = typeof event.sessionId === "string" ? event.sessionId : "";
132
+ const turnId = typeof event.turnId === "string" ? event.turnId : "";
133
+ const taskRunId = typeof event.taskRunId === "string" ? event.taskRunId : taskByTurn.get(`${sessionId}\0${turnId}`) ?? "";
134
+ if (!sessionId || !taskRunId) return "";
135
+ return [sessionId, taskRunId, event.model ?? "unknown", event.thinkingLevel ?? "unknown"].join("\0");
136
+ }
137
+
138
+ export function prefixEfficiencyMetrics(events) {
139
+ const taskByTurn = taskBindings(events), partitions = new Map();
140
+ const prefixPrompts = events.filter((event) => event.event === "agent_prompt").length;
141
+ for (const event of events) {
142
+ if (event.event !== "agent_prompt" || typeof event.prefixSurfaceHash !== "string") continue;
143
+ const partition = contextMetricPartition(event, taskByTurn);
144
+ if (!partition) continue;
145
+ const values = partitions.get(partition) ?? [];
146
+ values.push(event.prefixSurfaceHash);
147
+ partitions.set(partition, values);
148
+ }
149
+ let prompts = 0, transitions = 0, changes = 0;
150
+ const rates = [], turnsPerEpoch = [];
151
+ for (const hashes of partitions.values()) {
152
+ const partitionTransitions = Math.max(0, hashes.length - 1);
153
+ let partitionChanges = 0;
154
+ for (let index = 1; index < hashes.length; index += 1) if (hashes[index] !== hashes[index - 1]) partitionChanges += 1;
155
+ prompts += hashes.length; transitions += partitionTransitions; changes += partitionChanges;
156
+ if (partitionTransitions > 0) rates.push(ratio(partitionChanges, partitionTransitions));
157
+ turnsPerEpoch.push(ratio(hashes.length, 1 + partitionChanges));
158
+ }
159
+ return {
160
+ prefixPrompts,
161
+ comparablePrefixPrompts: prompts,
162
+ prefixEvidenceCoverage: evidenceCoverage(prompts, prefixPrompts),
163
+ prefixTransitions: transitions,
164
+ prefixChanges: changes,
165
+ prefixChangeRate: ratio(changes, transitions),
166
+ macroPrefixChangeRate: ratio(rates.reduce((sum, value) => sum + value, 0), rates.length),
167
+ averageTurnsPerPrefixEpoch: ratio(prompts, partitions.size + changes),
168
+ macroTurnsPerPrefixEpoch: ratio(turnsPerEpoch.reduce((sum, value) => sum + value, 0), turnsPerEpoch.length)
169
+ };
170
+ }
171
+
172
+ const READ_TOOLS = new Set(["read", "grep", "find", "ls"]);
173
+ const MUTATION_TOOLS = new Set(["write", "edit", "apply_patch", "patch"]);
174
+
175
+ function normalizedPaths(value) {
176
+ return [...new Set((Array.isArray(value) ? value : [])
177
+ .map(normalizeRelative)
178
+ .filter(Boolean))];
179
+ }
180
+
181
+ function successfulMutationPaths(event) {
182
+ if (event.event !== "tool_result") return null;
183
+ const explicit = [event.changedPaths, event.mutationPaths, event.targetPaths]
184
+ .find((value) => Array.isArray(value));
185
+ if (explicit) {
186
+ const paths = normalizedPaths(explicit);
187
+ if (paths.length > 0) return paths;
188
+ }
189
+ if (event.isError === true) return null;
190
+ if (!MUTATION_TOOLS.has(String(event.toolName ?? "").toLowerCase())) return null;
191
+ const target = normalizeRelative(event.targetPath);
192
+ return target ? [target] : [];
193
+ }
194
+
195
+ function pathAffected(readPath, mutationPaths) {
196
+ if (!readPath || mutationPaths.length === 0) return true;
197
+ return mutationPaths.some((mutationPath) => readPath === mutationPath
198
+ || readPath === "."
199
+ || mutationPath.startsWith(`${readPath}/`)
200
+ || readPath.startsWith(`${mutationPath}/`));
201
+ }
202
+
203
+ export function readEfficiencyMetrics(events) {
204
+ const taskByTurn = taskBindings(events), seenByPartition = new Map(), partitionTotals = new Map();
205
+ let readCalls = 0, comparableReadCalls = 0, duplicateReads = 0;
206
+ for (const event of events) {
207
+ const mutationPaths = successfulMutationPaths(event);
208
+ if (mutationPaths) {
209
+ const mutationPartition = contextMetricPartition(event, taskByTurn);
210
+ if (!mutationPartition) continue;
211
+ const [sessionId, taskRunId] = mutationPartition.split("\0");
212
+ const taskPrefix = `${sessionId}\0${taskRunId}\0`;
213
+ for (const [partition, seen] of seenByPartition) {
214
+ if (!partition.startsWith(taskPrefix)) continue;
215
+ for (const [key, readPath] of seen) {
216
+ if (pathAffected(readPath, mutationPaths)) seen.delete(key);
217
+ }
218
+ }
219
+ continue;
220
+ }
221
+ if (event.event !== "tool_call" || !READ_TOOLS.has(event.toolName)) continue;
222
+ readCalls += 1;
223
+ const partition = contextMetricPartition(event, taskByTurn);
224
+ const inputHash = typeof event.inputHash === "string" ? event.inputHash : "";
225
+ if (!partition || !inputHash) continue;
226
+ comparableReadCalls += 1;
227
+ const seen = seenByPartition.get(partition) ?? new Map();
228
+ const totals = partitionTotals.get(partition) ?? { calls: 0, duplicates: 0 };
229
+ totals.calls += 1;
230
+ // targetHash deliberately does not participate here. Two reads of one path
231
+ // with different ranges or queries are different calls; only the exact
232
+ // canonical input may be classified as a repeated read.
233
+ const key = `${event.toolName}\0${inputHash}`;
234
+ if (seen.has(key)) {
235
+ duplicateReads += 1;
236
+ totals.duplicates += 1;
237
+ } else {
238
+ seen.set(key, normalizeRelative(event.targetPath));
239
+ }
240
+ seenByPartition.set(partition, seen);
241
+ partitionTotals.set(partition, totals);
242
+ }
243
+ const macroRates = [...partitionTotals.values()].map((item) => ratio(item.duplicates, item.calls));
244
+ return {
245
+ readCalls,
246
+ comparableReadCalls,
247
+ uncomparableReadCalls: Math.max(0, readCalls - comparableReadCalls),
248
+ readEvidenceCoverage: evidenceCoverage(comparableReadCalls, readCalls),
249
+ duplicateReads,
250
+ duplicateReadRate: ratio(duplicateReads, comparableReadCalls),
251
+ macroDuplicateReadRate: ratio(macroRates.reduce((sum, value) => sum + value, 0), macroRates.length)
252
+ };
253
+ }
254
+
255
+ function injectionEquivalenceKey(item) {
256
+ if (!item || typeof item !== "object" || typeof item.path !== "string" || typeof item.payloadHash !== "string"
257
+ || typeof item.fileContentHash !== "string" || typeof item.representation !== "string") return "";
258
+ return JSON.stringify([normalizeRelative(item.path), item.fileContentHash, item.payloadHash, item.representation,
259
+ Array.isArray(item.ranges) ? item.ranges : [], Number.isInteger(item.generation) ? item.generation : 0]);
260
+ }
261
+
262
+ export function injectionEfficiencyMetrics(events) {
263
+ const taskByTurn = taskBindings(events), seen = new Map(), partitionTotals = new Map();
264
+ let receipts = 0, comparableReceipts = 0, occurrences = 0, comparable = 0, duplicates = 0, tokens = 0, comparableTokens = 0, duplicateTokens = 0;
265
+ for (const event of events) {
266
+ if (event.event !== "context_pack_injected") continue;
267
+ receipts += 1;
268
+ const partition = contextMetricPartition(event, taskByTurn);
269
+ if (!Array.isArray(event.selectedItems)) continue;
270
+ if (partition) comparableReceipts += 1;
271
+ const partitionSeen = seen.get(partition) ?? new Set(), totals = partitionTotals.get(partition) ?? { occurrences: 0, duplicates: 0 };
272
+ for (const item of event.selectedItems) {
273
+ const estimatedTokens = Math.max(0, Number(item?.estimatedTokens ?? 0));
274
+ occurrences += 1; tokens += estimatedTokens;
275
+ const key = injectionEquivalenceKey(item);
276
+ if (!partition || !key || event.rehydration === true || event.source === "compaction-rehydrate") continue;
277
+ comparable += 1; comparableTokens += estimatedTokens; totals.occurrences += 1;
278
+ if (partitionSeen.has(key)) {
279
+ duplicates += 1; duplicateTokens += estimatedTokens; totals.duplicates += 1;
280
+ } else partitionSeen.add(key);
281
+ }
282
+ seen.set(partition, partitionSeen); partitionTotals.set(partition, totals);
283
+ }
284
+ const macroRates = [...partitionTotals.values()].filter((item) => item.occurrences > 0).map((item) => ratio(item.duplicates, item.occurrences));
285
+ return {
286
+ injectionReceipts: receipts,
287
+ comparableInjectionReceipts: comparableReceipts,
288
+ injectionReceiptCoverage: evidenceCoverage(comparableReceipts, receipts),
289
+ injectedPathOccurrences: occurrences,
290
+ comparableInjectionItems: comparable,
291
+ injectionItemCoverage: evidenceCoverage(comparable, occurrences),
292
+ duplicateInjections: duplicates,
293
+ duplicateInjectionRate: ratio(duplicates, comparable),
294
+ duplicateInjectionOccurrenceRate: ratio(duplicates, occurrences),
295
+ macroDuplicateInjectionRate: ratio(macroRates.reduce((sum, value) => sum + value, 0), macroRates.length),
296
+ injectedPathTokens: tokens,
297
+ comparableInjectionTokens: comparableTokens,
298
+ duplicateInjectionTokens: duplicateTokens,
299
+ duplicateInjectionTokenRate: ratio(duplicateTokens, comparableTokens),
300
+ duplicateInjectionObservedTokenRate: ratio(duplicateTokens, tokens)
301
+ };
302
+ }
@@ -0,0 +1,167 @@
1
+ import fs from "node:fs";
2
+
3
+ import {
4
+ editRecoveryContextMetrics,
5
+ injectionEfficiencyMetrics,
6
+ prefixEfficiencyMetrics,
7
+ readEfficiencyMetrics
8
+ } from "./context-efficiency-metrics.js";
9
+ import { ensurePrivateStateDirectory, resolveLocalStatePath } from "./local-state-path.js";
10
+
11
+ function ratio(numerator, denominator) {
12
+ return denominator > 0 ? numerator / denominator : 0;
13
+ }
14
+ function evidenceStatus(observed, comparable) {
15
+ if (observed === 0) return "not-observed";
16
+ if (comparable === 0) return "unavailable";
17
+ return comparable === observed ? "complete" : "partial";
18
+ }
19
+ export function writeContextEfficiencyReport(cwd, events, dependencies) {
20
+ const prompts = events.filter((event) => event.event === "agent_prompt"), toolCalls = events.filter((event) => event.event === "tool_call"), toolResults = events.filter((event) => event.event === "tool_result"), packs = events.filter((event) => event.event === "context_pack");
21
+ const offeredPacks = events.filter((event) => event.event === "context_pack_offered"), injectedPacks = events.filter((event) => event.event === "context_pack_injected");
22
+ const compactions = events.filter((event) => event.event === "session_compact");
23
+ const readMetrics = readEfficiencyMetrics(events);
24
+ const comparableToolResults = toolResults.filter((event) => Number.isFinite(event.outputChars)
25
+ && event.outputChars >= 0 && typeof event.repeated === "boolean");
26
+ const toolResultEvidenceCoverage = ratio(comparableToolResults.length, toolResults.length);
27
+ const outputChars = comparableToolResults.reduce((sum, event) => sum + event.outputChars, 0);
28
+ const duplicateOutputChars = comparableToolResults.reduce((sum, event) => sum + (event.repeated ? event.outputChars : 0), 0);
29
+ const activeToolPrompts = prompts.filter((event) => Number.isFinite(Number(event.activeTools)));
30
+ const schemaPrompts = prompts.filter((event) => Number.isFinite(Number(event.systemPromptTokens)) && Number.isFinite(Number(event.toolSchemaTokens)));
31
+ const averageActiveTools = activeToolPrompts.length > 0
32
+ ? activeToolPrompts.reduce((sum, event) => sum + Number(event.activeTools), 0) / activeToolPrompts.length
33
+ : 0;
34
+ const averageSystemPromptTokens = schemaPrompts.length > 0
35
+ ? schemaPrompts.reduce((sum, event) => sum + Number(event.systemPromptTokens), 0) / schemaPrompts.length
36
+ : 0;
37
+ const averageToolSchemaTokens = schemaPrompts.length > 0
38
+ ? schemaPrompts.reduce((sum, event) => sum + Number(event.toolSchemaTokens), 0) / schemaPrompts.length
39
+ : 0;
40
+ const comparableConfidencePacks = packs.filter((event) => ["none", "low", "medium", "high"].includes(event.confidence));
41
+ const contextPackEvidenceCoverage = ratio(comparableConfidencePacks.length, packs.length);
42
+ const lowConfidencePacks = comparableConfidencePacks.filter((event) => ["none", "low"].includes(event.confidence)).length;
43
+ const feedback = dependencies.retrievalFeedback();
44
+ const prefixMetrics = prefixEfficiencyMetrics(events), injectionMetrics = injectionEfficiencyMetrics(events);
45
+ const editRecoveryMetrics = editRecoveryContextMetrics(events);
46
+ const duplicateOutputRate = ratio(duplicateOutputChars, outputChars);
47
+ const schemaToSystemRatio = ratio(averageToolSchemaTokens, averageSystemPromptTokens);
48
+ const schemaPrefixShare = ratio(averageToolSchemaTokens, averageSystemPromptTokens + averageToolSchemaTokens);
49
+ const lowConfidenceRate = ratio(lowConfidencePacks, comparableConfidencePacks.length);
50
+ const activeToolPenalty = Math.max(0, Math.min(1, (averageActiveTools - 12) / 24));
51
+ const wasteScore = Math.round(100 * (
52
+ readMetrics.duplicateReadRate * 0.3
53
+ + duplicateOutputRate * 0.25
54
+ + Math.min(1, schemaPrefixShare * 3) * 0.2
55
+ + lowConfidenceRate * 0.15
56
+ + activeToolPenalty * 0.1
57
+ ));
58
+ const scoreEvidenceCoverage = (
59
+ readMetrics.readEvidenceCoverage * 0.3
60
+ + toolResultEvidenceCoverage * 0.25
61
+ + ratio(schemaPrompts.length, prompts.length) * 0.2
62
+ + contextPackEvidenceCoverage * 0.15
63
+ + ratio(activeToolPrompts.length, prompts.length) * 0.1
64
+ );
65
+ const scoreEvidenceComplete = scoreEvidenceCoverage >= 1 - 1e-12;
66
+ const scoreEvidenceStatus = scoreEvidenceComplete ? "complete" : scoreEvidenceCoverage === 0 ? "unavailable" : "partial";
67
+ const recommendations = [];
68
+ if (readMetrics.comparableReadCalls > 0 && readMetrics.duplicateReadRate > 0.2) recommendations.push("Repeated reads are high within comparable task/session partitions; reuse the current working set before searching again.");
69
+ if (duplicateOutputRate > 0.15) recommendations.push("Repeated tool output is high; prefer delta results and narrower verification.");
70
+ if (schemaPrefixShare > 0.15 || averageActiveTools > 20) recommendations.push("Tool surface is large; activate only the workflow groups needed for the next turn.");
71
+ if (lowConfidenceRate > 0.4) recommendations.push("Retrieval confidence is low; rebuild the index or run one bounded finder pass.");
72
+ if (feedback.selected >= 4 && feedback.utilizationRate < 0.45) recommendations.push("Few injected paths receive successful mutation evidence; reduce pack breadth or improve task-specific ranking signals.");
73
+ if (feedback.selected >= 4 && feedback.fallbackRereadRate > 0.35) recommendations.push("Injected paths are frequently read again; improve snapshot completeness instead of treating fallback reads as positive retrieval feedback.");
74
+ if (readMetrics.readCalls > 0 && readMetrics.readEvidenceCoverage < 1) recommendations.push("Read telemetry coverage is incomplete; duplicate-read rates exclude calls without task/session/input identity.");
75
+ if (toolResults.length > 0 && toolResultEvidenceCoverage < 1) recommendations.push("Tool-result telemetry coverage is incomplete; duplicate-output rates exclude results without finite output size and an explicit repeated classification.");
76
+ if (packs.length > 0 && contextPackEvidenceCoverage < 1) recommendations.push("Context-pack telemetry coverage is incomplete; low-confidence rates exclude packs without a recognized confidence value.");
77
+ if (injectionMetrics.injectionReceipts > 0 && injectionMetrics.injectionReceiptCoverage < 1) recommendations.push("Injection receipt coverage is incomplete; zero duplicate injection waste is not established for unattributed receipts.");
78
+ if (injectionMetrics.injectedPathOccurrences > 0 && injectionMetrics.injectionItemCoverage < 1) recommendations.push("Injection item coverage is incomplete; duplicate injection rates exclude selected paths without canonical content and payload receipts.");
79
+ if (editRecoveryMetrics.editRecoveryContextEvents > 0 && editRecoveryMetrics.editRecoveryContextEvidenceCoverage < 1) recommendations.push("Edit-recovery telemetry coverage is incomplete; recovery context totals exclude malformed or unbounded recovery receipts.");
80
+ if (editRecoveryMetrics.editRecoveryFailures > 0 && editRecoveryMetrics.editRecoveryFailureEvidenceCoverage < 1) recommendations.push("Edit-recovery failure classification coverage is incomplete; a missing recovery context cannot be interpreted as a policy decision.");
81
+ if (!scoreEvidenceComplete) recommendations.push("Context-waste score is unavailable because one or more weighted telemetry lanes have incomplete evidence; do not interpret missing observations as zero waste.");
82
+ if (recommendations.length === 0) recommendations.push("No dominant context waste signal was detected in the sampled events.");
83
+ const report = {
84
+ schemaVersion: 3,
85
+ source: "piagent",
86
+ generatedAt: dependencies.nowIso(),
87
+ sample: {
88
+ events: events.length,
89
+ prompts: prompts.length,
90
+ toolCalls: toolCalls.length,
91
+ toolResults: toolResults.length,
92
+ contextPacks: packs.length,
93
+ contextPacksOffered: offeredPacks.length, contextPacksInjected: injectedPacks.length,
94
+ compactions: compactions.length,
95
+ editRecoveryContexts: editRecoveryMetrics.editRecoveryContextEvents,
96
+ editRecoveryFailures: editRecoveryMetrics.editRecoveryFailures
97
+ },
98
+ metrics: {
99
+ averageActiveTools: Number(averageActiveTools.toFixed(2)),
100
+ averageSystemPromptTokens: Math.round(averageSystemPromptTokens),
101
+ averageToolSchemaTokens: Math.round(averageToolSchemaTokens),
102
+ toolSchemaShare: Number(schemaPrefixShare.toFixed(4)),
103
+ toolSchemaPrefixShare: Number(schemaPrefixShare.toFixed(4)),
104
+ toolSchemaToSystemRatio: Number(schemaToSystemRatio.toFixed(4)),
105
+ ...Object.fromEntries(Object.entries(readMetrics).map(([key, value]) => [key, Number(value.toFixed(4))])),
106
+ outputChars,
107
+ duplicateOutputChars,
108
+ duplicateOutputRate: Number(duplicateOutputRate.toFixed(4)),
109
+ comparableToolResults: comparableToolResults.length,
110
+ uncomparableToolResults: Math.max(0, toolResults.length - comparableToolResults.length),
111
+ toolResultEvidenceCoverage: Number(toolResultEvidenceCoverage.toFixed(4)),
112
+ lowConfidencePacks,
113
+ lowConfidenceRate: Number(lowConfidenceRate.toFixed(4)),
114
+ comparableConfidencePacks: comparableConfidencePacks.length,
115
+ uncomparableConfidencePacks: Math.max(0, packs.length - comparableConfidencePacks.length),
116
+ contextPackEvidenceCoverage: Number(contextPackEvidenceCoverage.toFixed(4)),
117
+ contextSelections: feedback.selected,
118
+ contextSelectionsUsed: feedback.used,
119
+ contextSelectionsMutationCredited: feedback.used,
120
+ contextSelectionsUnused: feedback.unused,
121
+ contextUtilizationRate: Number(feedback.utilizationRate.toFixed(4)),
122
+ contextFallbackRereads: feedback.fallbackRereads,
123
+ contextFallbackRereadRate: Number(feedback.fallbackRereadRate.toFixed(4)),
124
+ ...Object.fromEntries(Object.entries({ ...prefixMetrics, ...injectionMetrics }).map(([key, value]) => [key, Number(value.toFixed(4))])),
125
+ ...Object.fromEntries(Object.entries(editRecoveryMetrics).map(([key, value]) => [key, Number(value.toFixed(4))])),
126
+ contextWasteScore: scoreEvidenceComplete ? wasteScore : null,
127
+ contextWasteScoreEstimate: wasteScore,
128
+ contextWasteScoreEvidenceCoverage: Number(scoreEvidenceCoverage.toFixed(4))
129
+ },
130
+ coverage: {
131
+ reads: { status: evidenceStatus(readMetrics.readCalls, readMetrics.comparableReadCalls), observed: readMetrics.readCalls, comparable: readMetrics.comparableReadCalls, rate: Number(readMetrics.readEvidenceCoverage.toFixed(4)) },
132
+ toolResults: { status: evidenceStatus(toolResults.length, comparableToolResults.length), observed: toolResults.length, comparable: comparableToolResults.length, rate: Number(toolResultEvidenceCoverage.toFixed(4)) },
133
+ contextPacks: { status: evidenceStatus(packs.length, comparableConfidencePacks.length), observed: packs.length, comparable: comparableConfidencePacks.length, rate: Number(contextPackEvidenceCoverage.toFixed(4)) },
134
+ prefixPrompts: { status: evidenceStatus(prefixMetrics.prefixPrompts, prefixMetrics.comparablePrefixPrompts), observed: prefixMetrics.prefixPrompts, comparable: prefixMetrics.comparablePrefixPrompts, rate: Number(prefixMetrics.prefixEvidenceCoverage.toFixed(4)) },
135
+ injectionReceipts: { status: evidenceStatus(injectionMetrics.injectionReceipts, injectionMetrics.comparableInjectionReceipts), observed: injectionMetrics.injectionReceipts, comparable: injectionMetrics.comparableInjectionReceipts, rate: Number(injectionMetrics.injectionReceiptCoverage.toFixed(4)) },
136
+ injectionItems: { status: evidenceStatus(injectionMetrics.injectedPathOccurrences, injectionMetrics.comparableInjectionItems), observed: injectionMetrics.injectedPathOccurrences, comparable: injectionMetrics.comparableInjectionItems, rate: Number(injectionMetrics.injectionItemCoverage.toFixed(4)) },
137
+ editRecoveryContexts: { status: evidenceStatus(editRecoveryMetrics.editRecoveryContextEvents, editRecoveryMetrics.comparableEditRecoveryContextEvents), observed: editRecoveryMetrics.editRecoveryContextEvents, comparable: editRecoveryMetrics.comparableEditRecoveryContextEvents, rate: Number(editRecoveryMetrics.editRecoveryContextEvidenceCoverage.toFixed(4)) },
138
+ editRecoveryFailures: { status: evidenceStatus(editRecoveryMetrics.editRecoveryFailures, editRecoveryMetrics.comparableEditRecoveryFailures), observed: editRecoveryMetrics.editRecoveryFailures, comparable: editRecoveryMetrics.comparableEditRecoveryFailures, rate: Number(editRecoveryMetrics.editRecoveryFailureEvidenceCoverage.toFixed(4)) },
139
+ retrievalSelections: { status: evidenceStatus(feedback.observedSelected, feedback.selected), observed: feedback.observedSelected, comparable: feedback.selected, rate: Number(ratio(feedback.selected, feedback.observedSelected).toFixed(4)) },
140
+ wasteScore: { status: scoreEvidenceStatus, rate: Number(scoreEvidenceCoverage.toFixed(4)) }
141
+ },
142
+ methodology: {
143
+ scoreRange: "0-100; lower is better; contextWasteScore is null unless all weighted telemetry lanes are complete, while contextWasteScoreEstimate remains explicitly diagnostic",
144
+ weights: {
145
+ duplicateReads: 0.3,
146
+ duplicateOutput: 0.25,
147
+ toolSchemaPrefixShare: 0.2,
148
+ lowConfidenceRetrieval: 0.15,
149
+ activeToolExcess: 0.1
150
+ },
151
+ retrievalFeedback: "Positive-only reranking consumes host-confirmed context_pack_injected events and credits a path only after a successful direct mutation result (or explicit changed-path evidence) in the same session/task/model/thinking partition. Fallback reads are reported separately and never increase ranking.",
152
+ readMetrics: "Duplicate reads require the same tool and exact inputHash within one session, task, model, and thinking-level partition. targetHash is not used as a substitute. Calls lacking comparable identity remain visible in coverage and are excluded from the rate.",
153
+ outputMetrics: "Duplicate-output rates use only tool results with a finite non-negative outputChars value and an explicit boolean repeated classification. Coverage exposes every excluded result instead of treating missing evidence as zero output or non-duplicate output.",
154
+ confidenceMetrics: "Low-confidence rates use only context-pack events with a recognized none/low/medium/high confidence value. Coverage exposes missing or unknown confidence instead of counting it as a non-low result.",
155
+ prefixMetrics: "Canonical provider tool schemas are compared only within session, task, model, and thinking-level partitions. First-turn task attribution uses turn_task_bound. Coverage distinguishes missing prefix evidence from a stable prefix.", injectionMetrics: "Duplicates require the same path, file hash, payload hash, representation, ranges, and generation within one task/session partition. Unconfirmed and post-compaction rehydration events are excluded from duplicate attribution; coverage exposes excluded receipts/items.",
156
+ editRecoveryMetrics: "Recovery count, characters, and estimated tokens require one versioned session capability marker plus an exact standalone edit_recovery_context receipt/tool_result pair. Original tool-result outputChars stays separate, so the failed tool output and the returned current-file snapshot are never added together.",
157
+ schemaMetrics: "toolSchemaPrefixShare is tool schema tokens divided by system-plus-tool-schema tokens. toolSchemaToSystemRatio preserves the former tool/system diagnostic explicitly; toolSchemaShare is a compatibility alias for the corrected prefix share.",
158
+ note: "This is an operational signal, not a quality verdict. Compare it with task acceptance and verification results."
159
+ },
160
+ recommendations
161
+ };
162
+ const paths = dependencies.contextEnginePaths();
163
+ ensurePrivateStateDirectory(cwd, paths.root, "Context report directory");
164
+ const reportPath = resolveLocalStatePath(cwd, paths.report, { label: "Context efficiency report" });
165
+ fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
166
+ return report;
167
+ }