@piagent/platform 1.5.5 → 1.6.1

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 (323) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/README.md +25 -21
  3. package/README.vi.md +34 -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 +92 -0
  24. package/benchmarks/production-v1/suite.json +46 -3
  25. package/catalog/capabilities.json +18 -18
  26. package/docs/authority-manifest.md +1 -1
  27. package/docs/auto-delegation-policy.md +38 -73
  28. package/docs/command-reference-vietnamese.md +27 -35
  29. package/docs/context-engine.md +12 -4
  30. package/docs/context-window-policy.md +37 -1
  31. package/docs/distribution-standard.md +7 -7
  32. package/docs/en/architecture.md +27 -13
  33. package/docs/languages.json +4 -4
  34. package/docs/model-options.md +8 -5
  35. package/docs/operator-manual-vietnamese.md +27 -45
  36. package/docs/project-adapters.md +2 -2
  37. package/docs/publishing-for-teams.md +10 -0
  38. package/docs/quality-benchmark.md +427 -62
  39. package/docs/release-install-policy.md +10 -9
  40. package/docs/runtime-harness-standard.md +3 -3
  41. package/docs/runtime-policy-design.md +4 -4
  42. package/docs/runtime-quality-baseline.md +13 -6
  43. package/docs/security-threat-model.md +2 -2
  44. package/docs/subagent-orchestration-capabilities.md +19 -51
  45. package/docs/subagents-and-multiagent.md +21 -47
  46. package/docs/task-implementation-contract.md +3 -3
  47. package/docs/task-lifecycle-vietnamese.md +3 -3
  48. package/docs/team-onboarding.md +2 -2
  49. package/docs/usage-observability.md +7 -2
  50. package/docs/vi/architecture.md +27 -13
  51. package/docs-site/architecture.html +1 -1
  52. package/docs-site/benchmark.html +89 -95
  53. package/docs-site/commands.html +1 -1
  54. package/docs-site/content/benchmark.html +88 -94
  55. package/docs-site/content/en/benchmark.html +134 -90
  56. package/docs-site/content/en/index.html +19 -19
  57. package/docs-site/content/en/runtime.html +4 -4
  58. package/docs-site/content/en/whats-new.html +12 -1
  59. package/docs-site/content/en/workflows.html +2 -2
  60. package/docs-site/content/index.html +19 -19
  61. package/docs-site/content/runtime.html +4 -4
  62. package/docs-site/content/whats-new.html +12 -1
  63. package/docs-site/content/workflows.html +2 -2
  64. package/docs-site/context-engine.html +1 -1
  65. package/docs-site/en/architecture.html +1 -1
  66. package/docs-site/en/benchmark.html +135 -91
  67. package/docs-site/en/commands.html +1 -1
  68. package/docs-site/en/context-engine.html +1 -1
  69. package/docs-site/en/index.html +20 -20
  70. package/docs-site/en/mcp-approval.html +1 -1
  71. package/docs-site/en/mcp-auth.html +1 -1
  72. package/docs-site/en/mcp-servers.html +1 -1
  73. package/docs-site/en/mcp.html +1 -1
  74. package/docs-site/en/permissions.html +1 -1
  75. package/docs-site/en/profiles.html +1 -1
  76. package/docs-site/en/quickstart.html +1 -1
  77. package/docs-site/en/runtime.html +5 -5
  78. package/docs-site/en/scope.html +1 -1
  79. package/docs-site/en/security.html +1 -1
  80. package/docs-site/en/team.html +1 -1
  81. package/docs-site/en/whats-new.html +13 -2
  82. package/docs-site/en/workflows.html +3 -3
  83. package/docs-site/index.html +20 -20
  84. package/docs-site/mcp-approval.html +1 -1
  85. package/docs-site/mcp-auth.html +1 -1
  86. package/docs-site/mcp-servers.html +1 -1
  87. package/docs-site/mcp.html +1 -1
  88. package/docs-site/permissions.html +1 -1
  89. package/docs-site/profiles.html +1 -1
  90. package/docs-site/quickstart.html +1 -1
  91. package/docs-site/runtime.html +5 -5
  92. package/docs-site/scope.html +1 -1
  93. package/docs-site/security.html +1 -1
  94. package/docs-site/team.html +1 -1
  95. package/docs-site/whats-new.html +13 -2
  96. package/docs-site/workflows.html +3 -3
  97. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
  98. package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
  99. package/evals/fixtures/codex-relative-efficiency-v1.invalid.json +4 -0
  100. package/evals/fixtures/codex-relative-efficiency-v1.valid.json +94 -0
  101. package/evals/fixtures/helper-request.invalid.json +1 -1
  102. package/evals/fixtures/helper-request.valid.json +1 -1
  103. package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
  104. package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
  105. package/evals/fixtures/piagent-webui/subagent-tree-v1.valid.json +7 -0
  106. package/evals/fixtures/task-authority-snapshot.valid.json +4 -4
  107. package/evals/fs4-readiness-matrix.v1.json +2 -2
  108. package/evals/fs5-causal-arm.v1.json +3 -3
  109. package/evals/fs5-pilot-protocol.v1.json +3 -3
  110. package/evals/fs5-pilot-protocol.v2.json +8 -8
  111. package/evals/fs5-pilot-protocol.v3.json +8 -8
  112. package/evals/fs5-pilot-protocol.v4.json +7 -6
  113. package/evals/fs5-pilot-protocol.v5.json +9 -7
  114. package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
  115. package/evals/runtime-conformance-v1/README.md +43 -0
  116. package/evals/runtime-conformance-v1/evaluate-paid-canary.mjs +198 -0
  117. package/evals/runtime-conformance-v1/lane.json +57 -0
  118. package/evals/runtime-conformance-v1/paid-canary.json +31 -0
  119. package/evals/runtime-conformance-v1/runner.mjs +456 -0
  120. package/package.json +3 -1
  121. package/packages/piagent-core/README.md +13 -4
  122. package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
  123. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
  124. package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
  125. package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
  126. package/packages/piagent-core/benchmark/benchmark-codex-relative-efficiency.js +698 -0
  127. package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
  128. package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
  129. package/packages/piagent-core/benchmark/benchmark-core.js +736 -664
  130. package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
  131. package/packages/piagent-core/benchmark/benchmark-forensics.js +441 -4
  132. package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
  133. package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
  134. package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
  135. package/packages/piagent-core/benchmark/benchmark-preflight.js +5 -1
  136. package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
  137. package/packages/piagent-core/benchmark/benchmark-provider-free-evidence.js +345 -0
  138. package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
  139. package/packages/piagent-core/benchmark/benchmark-record-validation.js +252 -0
  140. package/packages/piagent-core/benchmark/benchmark-report.js +130 -12
  141. package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
  142. package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
  143. package/packages/piagent-core/benchmark/benchmark-runtime-causal.js +244 -0
  144. package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +979 -0
  145. package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
  146. package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
  147. package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
  148. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
  149. package/packages/piagent-core/benchmark/benchmark-suite.js +149 -2
  150. package/packages/piagent-core/benchmark/benchmark-summary-support.js +380 -0
  151. package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
  152. package/packages/piagent-core/benchmark/benchmark-usage.js +322 -0
  153. package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
  154. package/packages/piagent-core/capabilities/runtime-integrity.js +3 -0
  155. package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
  156. package/packages/piagent-core/extensions/acceptance-behavior-proof.js +806 -0
  157. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
  158. package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
  159. package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
  160. package/packages/piagent-core/extensions/acceptance-receipt.js +123 -59
  161. package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
  162. package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
  163. package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
  164. package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
  165. package/packages/piagent-core/extensions/context-engine.js +149 -156
  166. package/packages/piagent-core/extensions/context-evidence.js +48 -0
  167. package/packages/piagent-core/extensions/context-import-links.js +296 -0
  168. package/packages/piagent-core/extensions/context-index-policy.js +4 -0
  169. package/packages/piagent-core/extensions/context-query-paths.js +57 -0
  170. package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
  171. package/packages/piagent-core/extensions/criterion-context-pack.js +435 -7
  172. package/packages/piagent-core/extensions/criterion-graph.js +82 -9
  173. package/packages/piagent-core/extensions/fallback-base-policy.ts +101 -0
  174. package/packages/piagent-core/extensions/guard-types.ts +6 -0
  175. package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
  176. package/packages/piagent-core/extensions/piagent-guard.ts +420 -260
  177. package/packages/piagent-core/extensions/task-contract-view.js +17 -3
  178. package/packages/piagent-core/extensions/task-lifecycle.js +39 -2
  179. package/packages/piagent-core/extensions/task-state.js +109 -142
  180. package/packages/piagent-core/extensions/verification-intelligence.js +52 -13
  181. package/packages/piagent-core/extensions/workspace-evidence-roots.js +193 -0
  182. package/packages/piagent-core/package.json +1 -1
  183. package/packages/piagent-core/policies/base-policy.json +44 -2
  184. package/packages/piagent-core/policy/authority-manifest.v1.json +3 -3
  185. package/packages/piagent-core/prompts/be-to-fe.md +2 -8
  186. package/packages/piagent-core/prompts/plan.md +1 -1
  187. package/packages/piagent-core/prompts/platform-improve.md +2 -6
  188. package/packages/piagent-core/prompts/review.md +2 -7
  189. package/packages/piagent-core/prompts/scout.md +1 -1
  190. package/packages/piagent-core/prompts/task.md +4 -3
  191. package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
  192. package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
  193. package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
  194. package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
  195. package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
  196. package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
  197. package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
  198. package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
  199. package/packages/piagent-core/runtime/hooks/completion-hook.ts +12 -12
  200. package/packages/piagent-core/runtime/hooks/input-hook.ts +17 -1
  201. package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
  202. package/packages/piagent-core/runtime/hooks/session-start-hook.ts +107 -3
  203. package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
  204. package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +134 -166
  205. package/packages/piagent-core/runtime/hooks/tool-result-value-helpers.ts +66 -0
  206. package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
  207. package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
  208. package/packages/piagent-core/runtime/inspection/bounded-emission-guard.ts +286 -0
  209. package/packages/piagent-core/runtime/inspection/runtime-event-store.ts +37 -7
  210. package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
  211. package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
  212. package/packages/piagent-core/runtime/inspection/task-subagent-tree.ts +27 -4
  213. package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +70 -0
  214. package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +73 -9
  215. package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
  216. package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
  217. package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
  218. package/packages/piagent-core/runtime/orchestration/helper-lifecycle.ts +16 -12
  219. package/packages/piagent-core/runtime/orchestration/minimal-delegation-policy.ts +71 -0
  220. package/packages/piagent-core/runtime/orchestration/owned-work-budget.ts +28 -20
  221. package/packages/piagent-core/runtime/orchestration/role-policy.ts +27 -9
  222. package/packages/piagent-core/runtime/orchestration/subagent-tool-policy.ts +329 -0
  223. package/packages/piagent-core/runtime/policy/block-remedy.ts +0 -4
  224. package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
  225. package/packages/piagent-core/runtime/product/efficiency-metrics.ts +7 -2
  226. package/packages/piagent-core/runtime/quality/edit-freshness-guard.ts +102 -0
  227. package/packages/piagent-core/runtime/quality/exact-output-contract.ts +22 -11
  228. package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
  229. package/packages/piagent-core/runtime/quality/performance-assurance.ts +9 -5
  230. package/packages/piagent-core/runtime/recovery/continuation-budget.ts +1 -1
  231. package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
  232. package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
  233. package/packages/piagent-core/runtime/recovery/handoff-projection.ts +4 -1
  234. package/packages/piagent-core/runtime/recovery/pristine-task-policy-refresh.ts +91 -0
  235. package/packages/piagent-core/runtime/recovery/recovery-policy.ts +7 -4
  236. package/packages/piagent-core/runtime/recovery/resume-state.ts +112 -14
  237. package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +60 -13
  238. package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
  239. package/packages/piagent-core/runtime/recovery/task-evidence-coverage-refresh.ts +217 -0
  240. package/packages/piagent-core/runtime/registration/context-commands.ts +82 -7
  241. package/packages/piagent-core/runtime/registration/knowledge-tools.ts +9 -5
  242. package/packages/piagent-core/runtime/registration/operator-request-intake.ts +49 -0
  243. package/packages/piagent-core/runtime/registration/policy-tools.ts +50 -15
  244. package/packages/piagent-core/runtime/registration/session-commands.ts +1 -1
  245. package/packages/piagent-core/runtime/registration/task-completion-tools.ts +61 -25
  246. package/packages/piagent-core/runtime/registration/task-evidence-tools.ts +21 -12
  247. package/packages/piagent-core/runtime/registration/task-start-guidance.ts +169 -0
  248. package/packages/piagent-core/runtime/registration/task-start-manifest.ts +54 -0
  249. package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
  250. package/packages/piagent-core/runtime/registration/task-start-tool.ts +108 -103
  251. package/packages/piagent-core/runtime/runtime-limits.ts +16 -2
  252. package/packages/piagent-core/runtime/session/adaptive-context-analysis.ts +461 -0
  253. package/packages/piagent-core/runtime/session/adaptive-context-governor.ts +464 -0
  254. package/packages/piagent-core/runtime/session/adaptive-context-ledger.ts +265 -0
  255. package/packages/piagent-core/runtime/session/message-signals.ts +7 -3
  256. package/packages/piagent-core/runtime/session/operator-request-carry.ts +44 -0
  257. package/packages/piagent-core/runtime/session/runtime-state.ts +161 -5
  258. package/packages/piagent-core/runtime/session/source-checkout-read-grants.ts +35 -0
  259. package/packages/piagent-core/runtime/session/system-prompt.ts +76 -14
  260. package/packages/piagent-core/runtime/session/task-contract-presentation.ts +39 -0
  261. package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
  262. package/packages/piagent-core/runtime/tools/phase-tools.ts +8 -8
  263. package/packages/piagent-core/runtime/tools/tool-groups.ts +12 -2
  264. package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
  265. package/packages/piagent-core/runtime/verification/exact-verifier-reuse.ts +101 -0
  266. package/packages/piagent-core/runtime/workflows/runtime-intake-compaction.ts +164 -0
  267. package/packages/piagent-core/runtime/workflows/task-intake.ts +207 -38
  268. package/packages/piagent-core/security/sensitive-data.js +13 -218
  269. package/packages/piagent-core/security/sensitive-project-file.js +42 -0
  270. package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
  271. package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
  272. package/packages/piagent-core/security/sensitive-text.js +234 -0
  273. package/packages/piagent-core/subagents/piagent-oracle.md +5 -3
  274. package/packages/piagent-core/subagents/piagent-planner.md +6 -4
  275. package/packages/piagent-core/subagents/piagent-reviewer.md +5 -3
  276. package/packages/piagent-core/subagents/piagent-scout.md +4 -2
  277. package/packages/piagent-core/subagents/piagent-worker.md +7 -5
  278. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-B4XehTCU.js} +1 -1
  279. package/packages/piagent-webui/dist/client/assets/index-CgOEyRPG.js +176 -0
  280. package/packages/piagent-webui/dist/client/index.html +1 -1
  281. package/packages/piagent-webui/extension/piagent-webui.ts +3 -1
  282. package/packages/piagent-webui/extension/same-session-bridge.ts +1 -1
  283. package/packages/piagent-webui/extension/session-stream-adapter.ts +33 -10
  284. package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
  285. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  286. package/packages/piagent-webui/gateway/gateway-session-stream.ts +122 -20
  287. package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
  288. package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
  289. package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
  290. package/packages/piagent-webui/gateway/session-operation-lifecycle.ts +237 -0
  291. package/packages/piagent-webui/gateway/session-operation-retry-control.ts +38 -0
  292. package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
  293. package/packages/piagent-webui/gateway/session-operation-watchdog.ts +165 -0
  294. package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
  295. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +141 -125
  296. package/packages/piagent-webui/ownership/session-lease-store.ts +5 -2
  297. package/packages/piagent-webui/server/core-inspection-provider.ts +2 -1
  298. package/packages/piagent-webui/server/loopback-server.ts +8 -0
  299. package/packages/piagent-webui/server/transcript-projection.ts +113 -9
  300. package/packages/piagent-webui/shared/text-visibility.ts +10 -0
  301. package/schemas/codex-relative-efficiency-v1.schema.json +155 -0
  302. package/schemas/piagent-webui/catalog-v1.json +9 -0
  303. package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
  304. package/schemas/piagent-webui/runtime-event-v2.schema.json +15 -5
  305. package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
  306. package/schemas/piagent-webui/subagent-tree-v1.schema.json +11 -1
  307. package/schemas/project-profile.schema.json +1 -1
  308. package/schemas/task-contract.schema.json +27 -1
  309. package/scripts/benchmark-runner-core.mjs +367 -363
  310. package/scripts/benchmark-runner-finalization.mjs +313 -0
  311. package/scripts/benchmark-runner-host-readiness.mjs +94 -0
  312. package/scripts/benchmark-runner-support.mjs +200 -0
  313. package/scripts/benchmark-session.mjs +95 -41
  314. package/scripts/configure-model-scope.sh +4 -3
  315. package/scripts/configure-subagents.sh +42 -34
  316. package/scripts/helper-evaluation.mjs +2 -2
  317. package/scripts/install-global.sh +2 -2
  318. package/scripts/pi-usage-history.mjs +37 -10
  319. package/scripts/setup.sh +2 -2
  320. package/scripts/team-doctor.sh +1 -1
  321. package/scripts/verify-local.sh +10 -1
  322. package/templates/global/settings.json +2 -1
  323. package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.js +0 -174
@@ -1,35 +1,38 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
2
 
5
- import { toolResultFingerprint } from "../../extensions/context-engine.js";
6
- import type { TaskContract } from "../../extensions/guard-types.ts";
3
+ import { estimateContextTokens, toolResultFingerprint } from "../../extensions/context-engine.js";
7
4
  import { matchesProtectedPath } from "../../extensions/policy-core.js";
8
5
  import { changedSnapshotFiles, taskDeltaFilesFromSnapshot } from "../../extensions/task-contract-view.js";
9
6
  import { classifyVerificationFailure } from "../../extensions/verification-intelligence.js";
10
- import { redactForStorage, redactSensitiveText } from "../../extensions/redaction-core.js";
7
+ import { redactForStorage } from "../../extensions/redaction-core.js";
11
8
  import { appendObservedBashResult, hashEvidenceCommand, observedBashResultFromToolResultEvent } from "../../extensions/runtime-evidence.js";
12
9
  import { workingTreeSnapshot, workingTreeSnapshotHasUnavailableEvidence } from "../../extensions/task-state.js";
13
10
  import { workingTreeObservation } from "../../extensions/working-tree-digest.js";
11
+ import { recordObservedContextEvidence } from "../context/context-evidence-qualification.ts";
12
+ import { confirmContextDeliveryFromToolResult, type ContextDeliveryConfirmationDependencies } from "../context/context-delivery.ts";
14
13
  import { recordMutationResult } from "../inspection/mutation-provenance-recorder.ts";
14
+ import { classifyToolFailure } from "../inspection/tool-failure-classification.ts";
15
+ import { classifyDirectSubagentResult, type DirectSubagentResult } from "../orchestration/subagent-tool-policy.ts";
15
16
  import { boundedGitDiffReview } from "../quality/performance-assurance.ts";
16
17
  import { currentFileContentDigests } from "../quality/model-mutation-proof.ts";
17
18
  import { boundedPerformanceReviewResultText } from "../quality/performance-review-evidence.ts";
19
+ import { buildEditRecoveryContext, type EditRecoveryContext } from "../recovery/edit-recovery-context.ts";
20
+ import { EditRecoveryDeliveryState } from "../recovery/edit-recovery-delivery.ts";
18
21
  import { attachToolResultCompactionDetails, compactToolResultDetails, compactToolResultTextContent, type ToolResultCaptureSummary } from "../session/tool-result-compaction.ts";
19
- import { RuntimeSessionState, type ObservedTaskContext } from "../session/runtime-state.ts";
22
+ import type { ObservedTaskContext } from "../session/runtime-state.ts";
20
23
  import { observeTrajectorySync } from "../trajectory/trajectory-observability.ts";
21
24
  import type { TrajectorySyncResult } from "../trajectory/trajectory-runtime.ts";
25
+ import { filterGrepProtectedContent, filterProtectedPathListContent } from "./tool-result-content-guards.ts";
22
26
  import { patchLineStats } from "./tool-result-metadata.ts";
27
+ import { appendToolResultText, boundedToolResultText, countChangedStringLeaves, isPlainRecord, numericExitCode, redactToolResultTextContent, successfulToolResult } from "./tool-result-value-helpers.ts";
23
28
 
29
+ export { filterGrepProtectedContent, filterProtectedPathListContent };
24
30
  type ToolResultEvent = { toolCallId?: string; toolName: string; input?: unknown; content?: unknown; details?: unknown; isError?: boolean; usage?: unknown };
25
31
  type ObservedBashResult = NonNullable<ReturnType<typeof observedBashResultFromToolResultEvent>>;
26
32
  type ObservedVerificationResult = ObservedBashResult & { outputText?: string };
27
33
  type WorkingTreeObservation = ReturnType<typeof workingTreeObservation>;
28
34
 
29
- type ToolResultHookDependencies = {
30
- state: RuntimeSessionState;
31
- activeTask: (ctx: ExtensionContext) => TaskContract | undefined;
32
- maxManifestFiles: number;
35
+ type ToolResultHookDependencies = ContextDeliveryConfirmationDependencies & {
33
36
  readProtectedPaths: (ctx: ExtensionContext) => string[];
34
37
  recordObservedBash: (observed: ObservedBashResult) => void;
35
38
  observedBashLedgerPath: (cwd: string) => string;
@@ -61,6 +64,11 @@ type ToolResultHookDependencies = {
61
64
  telemetry: (ctx: ExtensionContext, payload: Record<string, unknown>) => void;
62
65
  activity?: (ctx: ExtensionContext, payload: Record<string, unknown>) => void;
63
66
  now: () => string;
67
+ observeEditFreshness?: (
68
+ ctx: ExtensionContext,
69
+ event: ToolResultEvent,
70
+ metadata: { taskRunId?: string; successful: boolean; targetPath?: string; mutationTargets: string[] }
71
+ ) => void;
64
72
  completeSemanticRepair?: (
65
73
  ctx: ExtensionContext,
66
74
  event: ToolResultEvent,
@@ -69,164 +77,24 @@ type ToolResultHookDependencies = {
69
77
  success: boolean;
70
78
  exitCode: number;
71
79
  currentWorkingTreeDigest: string;
72
- changedPaths: string[];
80
+ changedPaths: string[]; authoredChangedPaths: string[];
73
81
  retryableFailure: boolean; correctiveFailure: boolean;
74
82
  }
75
83
  ) => TrajectorySyncResult | undefined;
76
84
  syncTrajectory?: (ctx: ExtensionContext, contextObserved: boolean) => TrajectorySyncResult | undefined;
85
+ recordSubagentResult?: (
86
+ ctx: ExtensionContext,
87
+ event: ToolResultEvent,
88
+ result: DirectSubagentResult
89
+ ) => void;
77
90
  };
78
91
 
79
- function normalizeRelative(cwd: string, candidate: unknown): string | undefined {
80
- if (typeof candidate !== "string" || candidate.trim().length === 0) return undefined;
81
- let raw = candidate.trim();
82
- try {
83
- raw = decodeURIComponent(raw);
84
- } catch {
85
- return undefined;
86
- }
87
- if (/^[A-Za-z][A-Za-z0-9+.-]*:\/\//.test(raw)) {
88
- if (!raw.toLowerCase().startsWith("file://")) return undefined;
89
- try {
90
- raw = fileURLToPath(raw);
91
- } catch {
92
- return undefined;
93
- }
94
- }
95
- const absolute = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
96
- return path.relative(cwd, absolute).split(path.sep).join("/");
97
- }
98
-
99
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
100
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
101
- const prototype = Object.getPrototypeOf(value);
102
- return prototype === Object.prototype || prototype === null;
103
- }
104
-
105
- function numericExitCode(value: unknown): number | undefined {
106
- if (typeof value === "number" && Number.isInteger(value)) return value;
107
- if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number.parseInt(value, 10);
108
- return undefined;
109
- }
110
-
111
- function successfulToolResult(event: ToolResultEvent): boolean {
112
- if (event.isError === true) return false;
113
- const details = isPlainRecord(event.details) ? event.details : {};
114
- const exitCode = numericExitCode(details.exitCode ?? details.status);
115
- return exitCode === undefined || exitCode === 0;
116
- }
117
-
118
- function boundedToolResultText(content: unknown): string {
119
- const text = typeof content === "string"
120
- ? content
121
- : Array.isArray(content)
122
- ? content
123
- .filter((block) => block && typeof block === "object" && (block as { type?: unknown }).type === "text")
124
- .map((block) => String((block as { text?: unknown }).text ?? ""))
125
- .join("\n")
126
- : "";
127
- return text.slice(-20_000);
128
- }
129
-
130
- function countChangedStringLeaves(before: unknown, after: unknown): number {
131
- if (typeof before === "string" && typeof after === "string") return before === after ? 0 : 1;
132
- if (Array.isArray(before) && Array.isArray(after)) {
133
- return before.reduce((total, item, index) => total + countChangedStringLeaves(item, after[index]), 0);
134
- }
135
- if (!before || !after || typeof before !== "object" || typeof after !== "object") return 0;
136
- return Object.entries(before as Record<string, unknown>).reduce(
137
- (total, [key, value]) => total + countChangedStringLeaves(value, (after as Record<string, unknown>)[key]),
138
- 0
139
- );
140
- }
141
-
142
- function redactToolResultTextContent(content: unknown): { content: unknown; redacted: number } {
143
- if (!Array.isArray(content)) return { content, redacted: 0 };
144
- let redacted = 0;
145
- const safeContent = content.map((block) => {
146
- if (!block || typeof block !== "object") return block;
147
- const typed = block as { type?: unknown; text?: unknown };
148
- if (typed.type !== "text" || typeof typed.text !== "string") return block;
149
- const safeText = redactSensitiveText(typed.text);
150
- if (!safeText.redacted) return block;
151
- redacted += 1;
152
- return { ...block, text: safeText.text };
153
- });
154
- return { content: safeContent, redacted };
155
- }
156
-
157
- function filterTextBlocks(
158
- content: unknown,
159
- blocked: (line: string) => boolean,
160
- emptyText: string,
161
- noticeText: (count: number) => string
162
- ): { changed: boolean; content?: unknown; redactedLines: number } {
163
- if (!Array.isArray(content)) return { changed: false, redactedLines: 0 };
164
- let changed = false;
165
- let redactedLines = 0;
166
- const filtered = content.map((block) => {
167
- if (!block || typeof block !== "object") return block;
168
- const text = (block as { type?: unknown; text?: unknown }).text;
169
- if ((block as { type?: unknown }).type !== "text" || typeof text !== "string") return block;
170
- const kept: string[] = [];
171
- let blockRedactedLines = 0;
172
- for (const line of text.split(/\r?\n/)) {
173
- if (blocked(line)) {
174
- changed = true;
175
- redactedLines += 1;
176
- blockRedactedLines += 1;
177
- } else {
178
- kept.push(line);
179
- }
180
- }
181
- if (blockRedactedLines === 0) return block;
182
- const notice = noticeText(blockRedactedLines);
183
- const nextText = kept.join("\n").trim().length > 0
184
- ? `${kept.join("\n")}\n${notice}`
185
- : `${emptyText}\n${notice}`;
186
- return { ...block, text: nextText };
187
- });
188
- return { changed, content: filtered, redactedLines };
189
- }
190
-
191
- export function filterGrepProtectedContent(content: unknown, protectedPatterns: string[]) {
192
- return filterTextBlocks(
193
- content,
194
- (line) => {
195
- const linePath = line.match(/^(.+?)(?::\d+:|-\d+-)/)?.[1];
196
- return Boolean(linePath && matchesProtectedPath(linePath, protectedPatterns));
197
- },
198
- "No matches found in non-protected paths.",
199
- (count) => `[Piagent Pi guard redacted ${count} protected grep line${count === 1 ? "" : "s"}.]`
200
- );
201
- }
202
-
203
- export function filterProtectedPathListContent(
204
- cwd: string,
205
- content: unknown,
206
- protectedPatterns: string[],
207
- basePath: string,
208
- toolName: string
209
- ) {
210
- return filterTextBlocks(
211
- content,
212
- (line) => {
213
- const trimmed = line.trim();
214
- if (!trimmed || trimmed.startsWith("[") || trimmed.startsWith("No ") || trimmed === "(empty directory)") return false;
215
- const entry = trimmed.replace(/[\\/]+$/, "");
216
- const candidates = new Set<string>();
217
- const direct = normalizeRelative(cwd, entry);
218
- const underBase = normalizeRelative(cwd, path.posix.join(basePath || ".", entry));
219
- if (direct !== undefined) candidates.add(direct);
220
- if (underBase !== undefined) candidates.add(underBase);
221
- return [...candidates].some((candidate) => matchesProtectedPath(candidate, protectedPatterns));
222
- },
223
- "No entries found in non-protected paths.",
224
- (count) => `[Piagent Pi guard redacted ${count} protected ${toolName} line${count === 1 ? "" : "s"}.]`
225
- );
226
- }
227
-
228
92
  export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResultHookDependencies): void {
93
+ const editRecoveryDelivery = new EditRecoveryDeliveryState();
94
+ pi.on("session_compact", async (_event, ctx) => editRecoveryDelivery.advanceEpoch(ctx));
95
+ pi.on("session_shutdown", async (_event, ctx) => editRecoveryDelivery.clearSession(ctx));
229
96
  pi.on("tool_result", async (event, ctx) => {
97
+ confirmContextDeliveryFromToolResult(pi, ctx, event, dependencies);
230
98
  const taskIdentity = dependencies.state.taskIdentity(ctx);
231
99
  const resultToolCallId = event.toolCallId ?? toolResultFingerprint(event.toolName, event.input, []).key;
232
100
  const readProtectedPaths = dependencies.readProtectedPaths(ctx);
@@ -245,12 +113,21 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
245
113
  }
246
114
 
247
115
  const observedContextEntry = dependencies.observedTaskContext(ctx.cwd, event, readProtectedPaths);
248
- if (observedContextEntry) dependencies.state.rememberObservedContext(ctx, observedContextEntry);
249
- const pendingContext = dependencies.state.observedContext(ctx);
116
+ recordObservedContextEvidence(ctx, observedContextEntry, taskIdentity, dependencies);
117
+ const pendingContext = dependencies.state.qualifiedTaskContext(ctx);
250
118
  const shellSnapshotBefore = dependencies.isShellTool(event.toolName)
251
119
  ? dependencies.state.consumeShellMutationSnapshot(ctx, event.toolName, event.input)
252
120
  : undefined;
253
121
  const currentTask = dependencies.activeTask(ctx);
122
+ const subagentResult = classifyDirectSubagentResult({
123
+ toolName: event.toolName,
124
+ toolInput: event.input,
125
+ content: event.content,
126
+ details: event.details,
127
+ isError: event.isError
128
+ });
129
+ if (subagentResult) dependencies.recordSubagentResult?.(ctx, event, subagentResult);
130
+ const effectiveToolError = event.isError === true || subagentResult?.failed === true;
254
131
  const eventTree = currentTask
255
132
  ? workingTreeObservation(workingTreeSnapshot(ctx.cwd) as Record<string, string>)
256
133
  : undefined;
@@ -303,10 +180,12 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
303
180
  }
304
181
  }
305
182
  const outputText = boundedToolResultText(event.content);
183
+ const toolFailureReasonCode = classifyToolFailure(event.toolName, effectiveToolError, event.content, event.input);
184
+ const failureReasonCode = subagentResult?.reasonCode ?? toolFailureReasonCode;
306
185
  const performanceReviewOutputText = boundedPerformanceReviewResultText(event.content);
307
186
  const observedExitCode = observed?.exitCode
308
187
  ?? numericExitCode(isPlainRecord(event.details) ? event.details.exitCode ?? event.details.status : undefined)
309
- ?? (event.isError === true ? 1 : 0);
188
+ ?? (effectiveToolError ? 1 : 0);
310
189
  const failure = observedExitCode === 0
311
190
  ? undefined
312
191
  : classifyVerificationFailure(outputText, observedExitCode);
@@ -326,6 +205,7 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
326
205
  exitCode: observedExitCode,
327
206
  currentWorkingTreeDigest: currentTreeDigest,
328
207
  changedPaths: [...new Set([...directMutationResult.changedPaths, ...shellChangedPaths])].sort(),
208
+ authoredChangedPaths: Object.keys(directMutationResult.recordedDigests).sort(),
329
209
  retryableFailure: failure?.retryable === true, correctiveFailure: failure?.sourceMutationPermission === "eligible-in-scope" && failure?.confidence === "high"
330
210
  })
331
211
  : undefined;
@@ -389,6 +269,37 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
389
269
  let resultContent: unknown = event.content;
390
270
  let resultDetails: unknown = event.details;
391
271
  let resultChanged = false;
272
+ let editRecovery: EditRecoveryContext | undefined;
273
+ if (subagentResult) {
274
+ const classification = {
275
+ schemaVersion: 1,
276
+ spawned: subagentResult.spawned,
277
+ failed: subagentResult.failed,
278
+ reasonCode: subagentResult.reasonCode,
279
+ disposition: subagentResult.disposition,
280
+ role: subagentResult.role,
281
+ requestRef: subagentResult.requestRef,
282
+ calls: subagentResult.calls,
283
+ tokens: subagentResult.tokens
284
+ };
285
+ resultDetails = isPlainRecord(resultDetails)
286
+ ? { ...resultDetails, piagentSubagentOutcome: classification }
287
+ : { ...(resultDetails === undefined ? {} : { value: resultDetails }), piagentSubagentOutcome: classification };
288
+ if (subagentResult.failed) {
289
+ resultContent = appendToolResultText(
290
+ resultContent,
291
+ `[Piagent helper outcome: failed (${subagentResult.reasonCode ?? "helper-run-failed"}). Treat this result as insufficient evidence; continue in the parent and do not retry the helper.]`
292
+ );
293
+ }
294
+ resultChanged = true;
295
+ }
296
+ const resultTarget = dependencies.extractLikelyPath(ctx.cwd, isPlainRecord(event.input) ? event.input : {});
297
+ dependencies.observeEditFreshness?.(ctx, event, {
298
+ taskRunId: taskIdentity?.taskRunId,
299
+ successful: successfulToolResult(event),
300
+ targetPath: resultTarget,
301
+ mutationTargets: directMutationTargets
302
+ });
392
303
  if (event.toolName === "grep") {
393
304
  const filtered = filterGrepProtectedContent(resultContent, readProtectedPaths);
394
305
  if (filtered.changed) {
@@ -412,6 +323,44 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
412
323
  }
413
324
  }
414
325
 
326
+ const recovery = buildEditRecoveryContext({
327
+ cwd: ctx.cwd,
328
+ targetPath: resultTarget,
329
+ reasonCode: toolFailureReasonCode,
330
+ protectedPaths: readProtectedPaths
331
+ });
332
+ if (recovery) {
333
+ if (editRecoveryDelivery.reserve(ctx, taskIdentity?.taskRunId ?? "", recovery.key)) {
334
+ editRecovery = recovery;
335
+ const injectedChars = recovery.text.length;
336
+ const injectedEstimatedTokens = estimateContextTokens(recovery.text);
337
+ const recoveryDetails = {
338
+ schemaVersion: 1,
339
+ targetPath: dependencies.redactText(recovery.targetPath),
340
+ contentHash: recovery.contentHash,
341
+ originalChars: recovery.originalChars,
342
+ injectedChars,
343
+ injectedEstimatedTokens,
344
+ sensitiveContentRedacted: recovery.redacted
345
+ };
346
+ resultDetails = isPlainRecord(resultDetails)
347
+ ? { ...resultDetails, piagentEditRecovery: recoveryDetails }
348
+ : { ...(resultDetails === undefined ? {} : { value: resultDetails }), piagentEditRecovery: recoveryDetails };
349
+ resultChanged = true;
350
+ dependencies.telemetry(ctx, {
351
+ event: "edit_recovery_context",
352
+ toolCallId: resultToolCallId,
353
+ taskRunId: taskIdentity?.taskRunId,
354
+ targetPath: dependencies.redactText(recovery.targetPath),
355
+ contentHash: recovery.contentHash,
356
+ originalChars: recovery.originalChars,
357
+ injectedChars,
358
+ injectedEstimatedTokens,
359
+ sensitiveContentRedacted: recovery.redacted
360
+ });
361
+ }
362
+ }
363
+
415
364
  const safeContent = redactToolResultTextContent(resultContent);
416
365
  const safeDetails = redactForStorage(resultDetails);
417
366
  const sensitiveValuesRedacted = safeContent.redacted + countChangedStringLeaves(resultDetails, safeDetails);
@@ -428,7 +377,7 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
428
377
  outputHash: fingerprint.outputHash,
429
378
  recordedAt: dependencies.now()
430
379
  });
431
- if (repeated && ["read", "grep", "find", "ls"].includes(event.toolName) && fingerprint.outputChars > 0) {
380
+ if (repeated && ["read", "grep", "find", "ls", "subagent"].includes(event.toolName) && fingerprint.outputChars > 0) {
432
381
  resultContent = [{
433
382
  type: "text",
434
383
  text: `[Piagent delta: unchanged ${event.toolName} result; ${fingerprint.outputChars} chars / ${fingerprint.outputLines} lines match the previous identical call.]`
@@ -463,11 +412,22 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
463
412
  if (compactionCaptures.length > 0) {
464
413
  resultDetails = attachToolResultCompactionDetails(resultDetails, compactionCaptures);
465
414
  }
415
+ // Compact the original error independently, then append the already bounded
416
+ // and sanitized recovery snapshot. Otherwise a small but line-dense file can
417
+ // be compacted into a preview and cease to be a complete one-shot recovery.
418
+ if (editRecovery) {
419
+ resultContent = appendToolResultText(resultContent, editRecovery.text);
420
+ resultChanged = true;
421
+ }
466
422
 
467
423
  const resultFingerprintId = event.toolCallId ?? fingerprint.key;
468
424
  const record = dependencies.activity ?? dependencies.telemetry;
469
- const resultTarget = dependencies.extractLikelyPath(ctx.cwd, isPlainRecord(event.input) ? event.input : {});
425
+ const changedPaths = [...new Set([...directMutationResult.changedPaths, ...shellChangedPaths])]
426
+ .filter((filePath) => !matchesProtectedPath(filePath, readProtectedPaths))
427
+ .map((filePath) => dependencies.redactText(filePath))
428
+ .sort();
470
429
  const lineStats = patchLineStats(event.details);
430
+ const editRecoveryFailure = ["edit-anchor-not-unique", "edit-anchor-stale"].includes(String(toolFailureReasonCode));
471
431
  record(ctx, {
472
432
  activityId: `result:${resultFingerprintId}`,
473
433
  event: "tool_result",
@@ -475,6 +435,7 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
475
435
  toolCallId: resultFingerprintId,
476
436
  toolName: event.toolName,
477
437
  targetPath: resultTarget ? dependencies.redactText(resultTarget) : undefined,
438
+ changedPaths: changedPaths.length > 0 ? changedPaths : undefined,
478
439
  inputHash: fingerprint.inputHash,
479
440
  outputHash: fingerprint.outputHash,
480
441
  outputChars: fingerprint.outputChars,
@@ -483,7 +444,14 @@ export function registerToolResultHook(pi: ExtensionAPI, dependencies: ToolResul
483
444
  compacted: compactionCaptures.length > 0,
484
445
  compactedCaptures: compactionCaptures.length,
485
446
  sensitiveValuesRedacted,
486
- isError: event.isError,
447
+ isError: effectiveToolError,
448
+ reasonCode: failureReasonCode,
449
+ // An explicit false is evidence too: it proves that every classified
450
+ // edit-anchor failure passed through the recovery policy even when the
451
+ // target was too large, private, protected, or otherwise ineligible.
452
+ editRecoveryContext: editRecoveryFailure ? Boolean(editRecovery) : undefined,
453
+ editRecoveryInjectedChars: editRecovery?.text.length,
454
+ editRecoveryEstimatedTokens: editRecovery ? estimateContextTokens(editRecovery.text) : undefined,
487
455
  exitCode: observed?.exitCode ?? (dependencies.isShellTool(event.toolName) ? event.isError ? 1 : 0 : undefined),
488
456
  exitCodeExact: observed?.exitCode !== undefined,
489
457
  ...lineStats,
@@ -0,0 +1,66 @@
1
+ import { redactSensitiveText } from "../../extensions/redaction-core.js";
2
+
3
+ export function isPlainRecord(value: unknown): value is Record<string, unknown> {
4
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
5
+ const prototype = Object.getPrototypeOf(value);
6
+ return prototype === Object.prototype || prototype === null;
7
+ }
8
+
9
+ export function numericExitCode(value: unknown): number | undefined {
10
+ if (typeof value === "number" && Number.isInteger(value)) return value;
11
+ if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number.parseInt(value, 10);
12
+ return undefined;
13
+ }
14
+
15
+ export function successfulToolResult(event: { isError?: boolean; details?: unknown }): boolean {
16
+ if (event.isError === true) return false;
17
+ const details = isPlainRecord(event.details) ? event.details : {};
18
+ const exitCode = numericExitCode(details.exitCode ?? details.status);
19
+ return exitCode === undefined || exitCode === 0;
20
+ }
21
+
22
+ export function boundedToolResultText(content: unknown): string {
23
+ const text = typeof content === "string"
24
+ ? content
25
+ : Array.isArray(content)
26
+ ? content
27
+ .filter((block) => block && typeof block === "object" && (block as { type?: unknown }).type === "text")
28
+ .map((block) => String((block as { text?: unknown }).text ?? ""))
29
+ .join("\n")
30
+ : "";
31
+ return text.slice(-20_000);
32
+ }
33
+
34
+ export function countChangedStringLeaves(before: unknown, after: unknown): number {
35
+ if (typeof before === "string" && typeof after === "string") return before === after ? 0 : 1;
36
+ if (Array.isArray(before) && Array.isArray(after)) {
37
+ return before.reduce((total, item, index) => total + countChangedStringLeaves(item, after[index]), 0);
38
+ }
39
+ if (!before || !after || typeof before !== "object" || typeof after !== "object") return 0;
40
+ return Object.entries(before as Record<string, unknown>).reduce(
41
+ (total, [key, value]) => total + countChangedStringLeaves(value, (after as Record<string, unknown>)[key]),
42
+ 0
43
+ );
44
+ }
45
+
46
+ export function redactToolResultTextContent(content: unknown): { content: unknown; redacted: number } {
47
+ if (!Array.isArray(content)) return { content, redacted: 0 };
48
+ let redacted = 0;
49
+ const safeContent = content.map((block) => {
50
+ if (!block || typeof block !== "object") return block;
51
+ const typed = block as { type?: unknown; text?: unknown };
52
+ if (typed.type !== "text" || typeof typed.text !== "string") return block;
53
+ const safeText = redactSensitiveText(typed.text);
54
+ if (!safeText.redacted) return block;
55
+ redacted += 1;
56
+ return { ...block, text: safeText.text };
57
+ });
58
+ return { content: safeContent, redacted };
59
+ }
60
+
61
+ export function appendToolResultText(content: unknown, text: string): unknown[] {
62
+ const block = { type: "text", text };
63
+ if (Array.isArray(content)) return [...content, block];
64
+ if (typeof content === "string" && content) return [{ type: "text", text: content }, block];
65
+ return [block];
66
+ }
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
 
3
3
  import { redactSensitiveText } from "../../extensions/redaction-core.js";
4
4
  import type { ActivityInspectorEvent } from "../product/activity-inspector.ts";
5
+ import { handledToolFailure } from "./tool-failure-classification.ts";
5
6
  import type { RuntimeEventDraft, RuntimeEventRevision } from "./runtime-event-store.ts";
6
7
 
7
8
  export type RuntimeEventIdentity = {
@@ -52,7 +53,8 @@ export function adaptActivityTelemetryEvent(input: ActivityEventAdapterInput): R
52
53
  const operationRef = hasOperation ? opaque("operation", input.identity.agentOperationId) : null;
53
54
  const toolCallRef = hasOperation ? opaque("tool", rawToolCall) : null;
54
55
  const activityType = hasOperation ? "tool" : ["bash", "shell", "exec"].includes(toolName) ? "command" : "other";
55
- const failed = eventName === "tool_result" && (event.isError === true || typeof event.exitCode === "number" && event.exitCode !== 0);
56
+ const resultFailure = eventName === "tool_result" && (event.isError === true || typeof event.exitCode === "number" && event.exitCode !== 0);
57
+ const failed = resultFailure && !handledToolFailure(event.reasonCode);
56
58
  const blocked = eventName === "tool_decision";
57
59
  // `tool_call` is emitted before policy authorization. Calling it "started"
58
60
  // made the Dashboard claim a command was running while it was still waiting
@@ -64,7 +66,7 @@ export function adaptActivityTelemetryEvent(input: ActivityEventAdapterInput): R
64
66
  const preview = safeText(previewValue, 4000);
65
67
  const storedRedactions = typeof event.sensitiveValuesRedacted === "number" && Number.isInteger(event.sensitiveValuesRedacted)
66
68
  ? Math.max(0, Math.min(10_000, event.sensitiveValuesRedacted)) : 0;
67
- const reasonCode = blocked ? "policy-blocked" : failed ? "tool-result-failed" : null;
69
+ const reasonCode = blocked ? "policy-blocked" : resultFailure ? safeText(event.reasonCode ?? "tool-result-failed", 80).text || "tool-result-failed" : null;
68
70
  return {
69
71
  ...base(input.identity, input.revision, observedAt), agentOperationId: operationRef, toolCallId: toolCallRef,
70
72
  kind, evidence: "derived",
@@ -0,0 +1,104 @@
1
+ import type { ActivityInspectorEvent } from "../product/activity-inspector.ts";
2
+
3
+ const RECOVERY_WINDOW_MS = 15 * 60 * 1_000;
4
+ const SHELL_TOOLS = new Set(["bash", "shell", "exec", "command"]);
5
+ const MUTATION_TOOLS = /(?:^|[._-])(?:apply[_-]?patch|edit|write|create[_-]?file|replace)(?:$|[._-])/i;
6
+
7
+ export type ActivityRecovery = {
8
+ recoveredAt: string;
9
+ recoveryToolCallId: string;
10
+ recoveryToolName: string;
11
+ exitCode: number | null;
12
+ exitCodeExact: boolean;
13
+ };
14
+
15
+ function resultFailed(event: ActivityInspectorEvent | undefined): boolean {
16
+ return Boolean(event && (event.isError === true || typeof event.exitCode === "number" && event.exitCode !== 0));
17
+ }
18
+
19
+ function time(value: unknown): number {
20
+ const parsed = typeof value === "string" ? Date.parse(value) : Number.NaN;
21
+ return Number.isFinite(parsed) ? parsed : Number.NaN;
22
+ }
23
+
24
+ function normalized(value: unknown): string {
25
+ return typeof value === "string" ? value.replaceAll("\\", "/").replace(/\s+/g, " ").trim() : "";
26
+ }
27
+
28
+ function family(toolName: unknown): "read" | "mutation" | "shell" | "other" {
29
+ const value = String(toolName ?? "").toLowerCase();
30
+ if (SHELL_TOOLS.has(value)) return "shell";
31
+ if (MUTATION_TOOLS.test(value)) return "mutation";
32
+ if (/(?:^|[._-])(?:read|document[_-]?read)(?:$|[._-])/.test(value)) return "read";
33
+ return "other";
34
+ }
35
+
36
+ function likelyCorrectedReadTarget(failed: ActivityInspectorEvent, candidate: ActivityInspectorEvent): boolean {
37
+ if (family(failed.toolName) !== "read" || family(candidate.toolName) !== "read") return false;
38
+ const failedPath = normalized(failed.targetPath).toLowerCase();
39
+ const candidatePath = normalized(candidate.targetPath).toLowerCase();
40
+ const failedSlash = failedPath.lastIndexOf("/"), candidateSlash = candidatePath.lastIndexOf("/");
41
+ if (!failedPath || !candidatePath || failedPath.slice(0, failedSlash) !== candidatePath.slice(0, candidateSlash)) return false;
42
+ const failedName = failedPath.slice(failedSlash + 1), candidateName = candidatePath.slice(candidateSlash + 1);
43
+ const failedExtension = failedName.includes(".") ? failedName.slice(failedName.lastIndexOf(".")) : "";
44
+ const candidateExtension = candidateName.includes(".") ? candidateName.slice(candidateName.lastIndexOf(".")) : "";
45
+ if (failedExtension !== candidateExtension) return false;
46
+ const tokens = (value: string) => new Set(value.slice(0, value.length - failedExtension.length).split(/[^a-z0-9]+/).filter(Boolean));
47
+ const failedTokens = tokens(failedName), candidateTokens = tokens(candidateName);
48
+ const overlap = [...failedTokens].filter((token) => candidateTokens.has(token)).length;
49
+ return overlap >= 2 && overlap === Math.min(failedTokens.size, candidateTokens.size);
50
+ }
51
+
52
+ function sameLogicalTarget(failed: ActivityInspectorEvent, candidate: ActivityInspectorEvent): boolean {
53
+ const failedPath = normalized(failed.targetPath), candidatePath = normalized(candidate.targetPath);
54
+ if (failedPath && candidatePath && failedPath === candidatePath) {
55
+ const failedFamily = family(failed.toolName), candidateFamily = family(candidate.toolName);
56
+ return failedFamily === candidateFamily || failedFamily === "read" && candidateFamily === "mutation";
57
+ }
58
+ const failedCommand = normalized(failed.command), candidateCommand = normalized(candidate.command);
59
+ return Boolean(failedCommand && candidateCommand && failedCommand === candidateCommand
60
+ && family(failed.toolName) === "shell" && family(candidate.toolName) === "shell");
61
+ }
62
+
63
+ export function recoveredToolCalls(events: readonly ActivityInspectorEvent[]): ReadonlyMap<string, ActivityRecovery> {
64
+ const results = new Map<string, { event: ActivityInspectorEvent; order: number }>();
65
+ for (let order = 0; order < events.length; order += 1) {
66
+ const event = events[order];
67
+ if (event?.event === "tool_result" && event.toolCallId) results.set(event.toolCallId, { event, order });
68
+ }
69
+ const segments = new Map<number, number>();
70
+ let segment = 0;
71
+ for (let order = 0; order < events.length; order += 1) {
72
+ if (events[order]?.event === "user_input") segment += 1;
73
+ segments.set(order, segment);
74
+ }
75
+ const calls = events.map((event, order) => ({ event, order }))
76
+ .filter((entry) => entry.event?.event === "tool_call" && Boolean(entry.event.toolCallId));
77
+ const recovered = new Map<string, ActivityRecovery>();
78
+ for (let failedCallIndex = 0; failedCallIndex < calls.length; failedCallIndex += 1) {
79
+ const failedCall = calls[failedCallIndex];
80
+ const failedResult = results.get(failedCall.event.toolCallId!);
81
+ if (!failedResult || !resultFailed(failedResult.event)) continue;
82
+ const failedAt = time(failedResult.event.recordedAt ?? failedCall.event.recordedAt);
83
+ const match = calls.find((candidate, candidateIndex) => {
84
+ if (candidate.order <= failedResult.order || segments.get(candidate.order) !== segments.get(failedCall.order)) return false;
85
+ const candidateResult = results.get(candidate.event.toolCallId!);
86
+ if (!candidateResult || resultFailed(candidateResult.event)) return false;
87
+ const recoveredAt = time(candidateResult.event.recordedAt ?? candidate.event.recordedAt);
88
+ return Number.isFinite(failedAt) && Number.isFinite(recoveredAt) && recoveredAt >= failedAt
89
+ && recoveredAt - failedAt <= RECOVERY_WINDOW_MS
90
+ && (sameLogicalTarget(failedCall.event, candidate.event)
91
+ || candidateIndex === failedCallIndex + 1 && likelyCorrectedReadTarget(failedCall.event, candidate.event));
92
+ });
93
+ if (!match) continue;
94
+ const recoveryResult = results.get(match.event.toolCallId!)!.event;
95
+ recovered.set(failedCall.event.toolCallId!, {
96
+ recoveredAt: String(recoveryResult.recordedAt ?? match.event.recordedAt),
97
+ recoveryToolCallId: match.event.toolCallId!,
98
+ recoveryToolName: String(match.event.toolName ?? "unknown"),
99
+ exitCode: typeof recoveryResult.exitCode === "number" ? recoveryResult.exitCode : null,
100
+ exitCodeExact: recoveryResult.exitCodeExact === true
101
+ });
102
+ }
103
+ return recovered;
104
+ }