@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
@@ -0,0 +1,286 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export type EmissionSuppressionReason =
4
+ | "empty"
5
+ | "filler"
6
+ | "duplicate-exact"
7
+ | "duplicate-normalized"
8
+ | "update-budget";
9
+
10
+ export type BoundedEmissionCandidate = {
11
+ channel: string;
12
+ identityKey: string;
13
+ updateKey: string;
14
+ text: string;
15
+ actionable: boolean;
16
+ reference: string;
17
+ };
18
+
19
+ export type EmissionDecision = {
20
+ accepted: boolean;
21
+ reason: EmissionSuppressionReason | null;
22
+ priorReference: string | null;
23
+ };
24
+
25
+ export type BoundedEmissionTelemetry = {
26
+ schemaVersion: 1;
27
+ capacity: number;
28
+ retained: number;
29
+ primed: number;
30
+ accepted: number;
31
+ suppressed: number;
32
+ suppressedByReason: Record<EmissionSuppressionReason, number>;
33
+ };
34
+
35
+ type RetainedEmission = {
36
+ exactKey: string;
37
+ normalizedKey: string;
38
+ updateKey: string | null;
39
+ reference: string;
40
+ };
41
+
42
+ const ANSI = /\u001b(?:\[[0-?]*[ -/]*[@-~]|[@-_])/g;
43
+ const DEFAULT_CAPACITY = 512;
44
+ const MIN_CAPACITY = 1;
45
+ const MAX_CAPACITY = 16_384;
46
+
47
+ const FILLER = new Set([
48
+ "abort", "all good", "carry on", "complete", "continue", "done", "finished", "halt", "lgtm",
49
+ "looks good", "no action", "no action needed", "no concern", "no concerns", "no further advice",
50
+ "no further advice needed", "no further input", "no further input needed", "no issue", "no issues",
51
+ "no issue continue", "no notes", "nothing to add", "nothing to flag", "nothing to report", "ok", "okay",
52
+ "stop", "stop here", "stop now", "task complete", "task done",
53
+ "da xong", "đã xong", "khong can lam gi", "không cần làm gì", "khong co gi de bao cao", "không có gì để báo cáo",
54
+ "khong co loi", "không có lỗi", "khong co van de", "không có vấn đề", "on", "ổn", "tiep tuc", "tiếp tục", "xong"
55
+ ]);
56
+
57
+ function digest(value: string): string {
58
+ return createHash("sha256").update(value).digest("hex");
59
+ }
60
+
61
+ function exactEmissionText(value: string): string {
62
+ return String(value ?? "")
63
+ .replace(ANSI, "")
64
+ .normalize("NFKC")
65
+ .replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ")
66
+ .replace(/\s+/g, " ")
67
+ .trim();
68
+ }
69
+
70
+ /** Stable comparison form for reviewer notes and compact runtime event summaries. */
71
+ export function normalizeEmissionText(value: string): string {
72
+ return exactEmissionText(value)
73
+ .toLowerCase()
74
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
75
+ .trim();
76
+ }
77
+
78
+ function canonical(value: unknown): unknown {
79
+ if (Array.isArray(value)) return value.map(canonical);
80
+ if (!value || typeof value !== "object") return value;
81
+ const prototype = Object.getPrototypeOf(value);
82
+ if (prototype !== Object.prototype && prototype !== null) return String(value);
83
+ const record = value as Record<string, unknown>;
84
+ return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonical(record[key])]));
85
+ }
86
+
87
+ function canonicalText(value: unknown): string {
88
+ return JSON.stringify(canonical(value));
89
+ }
90
+
91
+ function textField(payload: Record<string, unknown>): string | null {
92
+ for (const field of ["note", "advice", "finding", "message", "summary"]) {
93
+ if (typeof payload[field] === "string" && payload[field].trim()) return payload[field];
94
+ }
95
+ return null;
96
+ }
97
+
98
+ function reviewerRole(payload: Record<string, unknown>): string | null {
99
+ for (const field of ["role", "agentRole", "actorRole", "sourceRole"]) {
100
+ const value = String(payload[field] ?? "").toLowerCase();
101
+ if (value === "reviewer" || value === "piagent-reviewer") return value;
102
+ }
103
+ const emissionType = String(payload.emissionType ?? "").toLowerCase();
104
+ return emissionType === "reviewer-note" || emissionType === "review-note" ? "reviewer" : null;
105
+ }
106
+
107
+ type RuntimeEmissionLike = {
108
+ kind: string;
109
+ sessionRef: string;
110
+ taskRunId: string | null;
111
+ agentOperationId: string | null;
112
+ turnIndex: number | null;
113
+ toolCallId: string | null;
114
+ revision: object;
115
+ payload: Record<string, unknown>;
116
+ };
117
+
118
+ /**
119
+ * Select only bounded activity, reviewer-note and operation emissions. Message
120
+ * deltas/completions are deliberately outside this boundary, so a final user-
121
+ * visible response can never be suppressed by this guard.
122
+ */
123
+ export function runtimeEventEmissionCandidate(event: RuntimeEmissionLike, reference: string): BoundedEmissionCandidate | null {
124
+ const payload = event.payload ?? {};
125
+ const revision = canonicalText(event.revision);
126
+ // Lifecycle state is authoritative. Classify it before reviewer metadata so
127
+ // a note attached to a started/finished event can never hide that state.
128
+ if (event.kind.startsWith("activity.")) {
129
+ const state = payload.state ?? event.kind.slice("activity.".length);
130
+ return {
131
+ channel: "activity",
132
+ identityKey: canonicalText([event.agentOperationId, event.toolCallId, payload.activityRef, event.kind, state]),
133
+ updateKey: canonicalText([revision, event.agentOperationId, event.toolCallId, event.kind, state]),
134
+ text: canonicalText(payload),
135
+ actionable: false,
136
+ reference
137
+ };
138
+ }
139
+ if (event.kind.startsWith("agent-operation.")) {
140
+ const state = payload.state ?? payload.settlement ?? payload.status ?? event.kind.slice("agent-operation.".length);
141
+ return {
142
+ channel: "operation",
143
+ identityKey: canonicalText([event.agentOperationId, event.kind, state]),
144
+ updateKey: canonicalText([revision, event.agentOperationId, event.kind, state]),
145
+ text: canonicalText(payload),
146
+ actionable: false,
147
+ reference
148
+ };
149
+ }
150
+ const role = reviewerRole(payload), note = role ? textField(payload) : null;
151
+ if (role && note) {
152
+ const explicitUpdate = payload.updateRef ?? payload.reviewerRevision ?? payload.revision;
153
+ return {
154
+ channel: "reviewer",
155
+ // A session may host many unrelated tasks. Dedupe reviewer prose across
156
+ // retries/operations of one durable task, but never across task runs.
157
+ identityKey: canonicalText([event.sessionRef, event.taskRunId, role]),
158
+ updateKey: canonicalText([event.sessionRef, event.agentOperationId, event.turnIndex, explicitUpdate ?? revision]),
159
+ text: note,
160
+ actionable: true,
161
+ reference
162
+ };
163
+ }
164
+ return null;
165
+ }
166
+
167
+ /** A small in-memory policy gate. It stores only hashes, never emitted prose. */
168
+ export class BoundedEmissionGuard {
169
+ readonly capacity: number;
170
+ readonly #retained: RetainedEmission[] = [];
171
+ readonly #exact = new Map<string, string>();
172
+ readonly #normalized = new Map<string, string>();
173
+ readonly #updates = new Map<string, string>();
174
+ #primed = 0;
175
+ #accepted = 0;
176
+ readonly #suppressed: Record<EmissionSuppressionReason, number> = {
177
+ empty: 0,
178
+ filler: 0,
179
+ "duplicate-exact": 0,
180
+ "duplicate-normalized": 0,
181
+ "update-budget": 0
182
+ };
183
+
184
+ constructor(options: { capacity?: number } = {}) {
185
+ const candidate = Number.isFinite(options.capacity) ? Math.floor(Number(options.capacity)) : DEFAULT_CAPACITY;
186
+ this.capacity = Math.max(MIN_CAPACITY, Math.min(MAX_CAPACITY, candidate));
187
+ }
188
+
189
+ #keys(candidate: BoundedEmissionCandidate): { exactKey: string; normalizedKey: string; updateKey: string | null; exact: string; normalized: string } {
190
+ const exact = exactEmissionText(candidate.text), normalized = normalizeEmissionText(exact);
191
+ const scope = `${candidate.channel}\0${candidate.identityKey}`;
192
+ return {
193
+ exact,
194
+ normalized,
195
+ exactKey: digest(`${scope}\0exact\0${exact}`),
196
+ normalizedKey: digest(`${scope}\0normalized\0${normalized}`),
197
+ updateKey: candidate.actionable ? digest(`${candidate.channel}\0update\0${candidate.updateKey}`) : null
198
+ };
199
+ }
200
+
201
+ #reject(reason: EmissionSuppressionReason, priorReference: string | null = null, count = true): EmissionDecision {
202
+ if (count) this.#suppressed[reason] += 1;
203
+ return { accepted: false, reason, priorReference };
204
+ }
205
+
206
+ #inspect(candidate: BoundedEmissionCandidate, countSuppression: boolean): EmissionDecision {
207
+ const keys = this.#keys(candidate);
208
+ if (candidate.actionable && !keys.exact) return this.#reject("empty", null, countSuppression);
209
+ if (candidate.actionable && FILLER.has(keys.normalized)) return this.#reject("filler", null, countSuppression);
210
+ const exact = this.#exact.get(keys.exactKey);
211
+ if (exact) return this.#reject("duplicate-exact", exact, countSuppression);
212
+ const normalized = this.#normalized.get(keys.normalizedKey);
213
+ if (normalized) return this.#reject("duplicate-normalized", normalized, countSuppression);
214
+ const update = keys.updateKey ? this.#updates.get(keys.updateKey) : undefined;
215
+ if (update) return this.#reject("update-budget", update, countSuppression);
216
+ return { accepted: true, reason: null, priorReference: null };
217
+ }
218
+
219
+ inspect(candidate: BoundedEmissionCandidate): EmissionDecision {
220
+ return this.#inspect(candidate, true);
221
+ }
222
+
223
+ /** Seed retained history without counting old suppression as live telemetry. */
224
+ prime(candidate: BoundedEmissionCandidate): boolean {
225
+ if (!this.#inspect(candidate, false).accepted) return false;
226
+ this.recordAccepted(candidate, { primed: true });
227
+ return true;
228
+ }
229
+
230
+ recordAccepted(candidate: BoundedEmissionCandidate, options: { primed?: boolean } = {}): void {
231
+ const keys = this.#keys(candidate);
232
+ const record = { exactKey: keys.exactKey, normalizedKey: keys.normalizedKey, updateKey: keys.updateKey, reference: candidate.reference };
233
+ this.#retained.push(record);
234
+ this.#exact.set(record.exactKey, record.reference);
235
+ this.#normalized.set(record.normalizedKey, record.reference);
236
+ if (record.updateKey) this.#updates.set(record.updateKey, record.reference);
237
+ if (options.primed) this.#primed += 1;
238
+ else this.#accepted += 1;
239
+ while (this.#retained.length > this.capacity) {
240
+ const stale = this.#retained.shift() as RetainedEmission;
241
+ if (this.#exact.get(stale.exactKey) === stale.reference) this.#exact.delete(stale.exactKey);
242
+ if (this.#normalized.get(stale.normalizedKey) === stale.reference) this.#normalized.delete(stale.normalizedKey);
243
+ if (stale.updateKey && this.#updates.get(stale.updateKey) === stale.reference) this.#updates.delete(stale.updateKey);
244
+ }
245
+ }
246
+
247
+ /** Remove fingerprints when their durable event leaves the retention window. */
248
+ forget(reference: string): boolean {
249
+ const kept = this.#retained.filter((record) => record.reference !== reference);
250
+ if (kept.length === this.#retained.length) return false;
251
+ this.#retained.length = 0;
252
+ this.#retained.push(...kept);
253
+ this.#exact.clear();
254
+ this.#normalized.clear();
255
+ this.#updates.clear();
256
+ for (const record of this.#retained) {
257
+ this.#exact.set(record.exactKey, record.reference);
258
+ this.#normalized.set(record.normalizedKey, record.reference);
259
+ if (record.updateKey) this.#updates.set(record.updateKey, record.reference);
260
+ }
261
+ return true;
262
+ }
263
+
264
+ reset(): void {
265
+ this.#retained.length = 0;
266
+ this.#exact.clear();
267
+ this.#normalized.clear();
268
+ this.#updates.clear();
269
+ this.#primed = 0;
270
+ this.#accepted = 0;
271
+ for (const reason of Object.keys(this.#suppressed) as EmissionSuppressionReason[]) this.#suppressed[reason] = 0;
272
+ }
273
+
274
+ telemetry(): BoundedEmissionTelemetry {
275
+ const suppressedByReason = { ...this.#suppressed };
276
+ return {
277
+ schemaVersion: 1,
278
+ capacity: this.capacity,
279
+ retained: this.#retained.length,
280
+ primed: this.#primed,
281
+ accepted: this.#accepted,
282
+ suppressed: Object.values(suppressedByReason).reduce((sum, value) => sum + value, 0),
283
+ suppressedByReason
284
+ };
285
+ }
286
+ }
@@ -3,6 +3,12 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  import { ensurePrivateStateDirectory, resolveLocalStatePath } from "../../extensions/local-state-path.js";
6
+ import {
7
+ BoundedEmissionGuard,
8
+ type BoundedEmissionTelemetry,
9
+ type EmissionSuppressionReason,
10
+ runtimeEventEmissionCandidate
11
+ } from "./bounded-emission-guard.ts";
6
12
 
7
13
  export const RUNTIME_EVENT_SCHEMA_VERSION = 2 as const;
8
14
  export type RuntimeEventRevision = {
@@ -57,6 +63,9 @@ export type RuntimeEventReplay = {
57
63
  lastAvailableSequence: number | null;
58
64
  reasonCode: string | null;
59
65
  };
66
+ export type RuntimeEventAppendResult =
67
+ | { event: RuntimeEventV2; appended: true; suppressionReason: null }
68
+ | { event: RuntimeEventV2 | null; appended: false; suppressionReason: EmissionSuppressionReason | "event-id-duplicate" };
60
69
 
61
70
  const EVENT_KINDS = new Set([
62
71
  "runtime.started", "runtime.health-changed", "runtime.disconnected", "runtime.resync-required", "runtime.resynced", "runtime.phase-changed",
@@ -158,15 +167,17 @@ function readSegment(root: string, file: string): RuntimeEventV2[] {
158
167
  export class RuntimeEventStore {
159
168
  readonly directory: string;
160
169
  readonly corruptions: string[] = [];
161
- readonly options: { projectRoot: string; projectRef: string; runtimeInstanceId: string; sessionRef: string; maxEventsPerSegment?: number; maxSegments?: number };
170
+ readonly options: { projectRoot: string; projectRef: string; runtimeInstanceId: string; sessionRef: string; maxEventsPerSegment?: number; maxSegments?: number; emissionGuardCapacity?: number };
162
171
  private events: RuntimeEventV2[] = [];
163
172
  private segments: Array<{ file: string; start: number; count: number }> = [];
164
173
  private eventById = new Map<string, RuntimeEventV2>();
174
+ private readonly emissionGuard: BoundedEmissionGuard;
165
175
  private readonly maxEventsPerSegment: number;
166
176
  private readonly maxSegments: number;
167
177
 
168
- constructor(options: { projectRoot: string; projectRef: string; runtimeInstanceId: string; sessionRef: string; maxEventsPerSegment?: number; maxSegments?: number }) {
178
+ constructor(options: { projectRoot: string; projectRef: string; runtimeInstanceId: string; sessionRef: string; maxEventsPerSegment?: number; maxSegments?: number; emissionGuardCapacity?: number }) {
169
179
  this.options = options;
180
+ this.emissionGuard = new BoundedEmissionGuard({ capacity: options.emissionGuardCapacity });
170
181
  this.maxEventsPerSegment = Math.max(2, Math.min(1_000, options.maxEventsPerSegment ?? 500));
171
182
  this.maxSegments = Math.max(2, Math.min(20, options.maxSegments ?? 10));
172
183
  this.directory = storeDirectory(options.projectRoot, options.runtimeInstanceId, options.sessionRef);
@@ -195,11 +206,13 @@ export class RuntimeEventStore {
195
206
  if (previous && record.writerSequence !== previous + 1) throw new Error("event sequence gap");
196
207
  if (this.eventById.has(record.eventId)) throw new Error("duplicate event identity");
197
208
  previous = record.writerSequence; this.events.push(record); this.eventById.set(record.eventId, record);
209
+ const candidate = runtimeEventEmissionCandidate(record, record.eventId);
210
+ if (candidate) this.emissionGuard.prime(candidate);
198
211
  }
199
212
  this.segments.push({ file: name, start, count: records.length });
200
213
  }
201
214
  } catch {
202
- this.events = []; this.segments = []; this.eventById.clear(); this.corruptions.push("event-store-corrupt");
215
+ this.events = []; this.segments = []; this.eventById.clear(); this.emissionGuard.reset(); this.corruptions.push("event-store-corrupt");
203
216
  }
204
217
  }
205
218
 
@@ -208,14 +221,27 @@ export class RuntimeEventStore {
208
221
  retention(): { eventRetentionCount: number; eventRetentionSeconds: number } {
209
222
  return { eventRetentionCount: this.maxEventsPerSegment * this.maxSegments, eventRetentionSeconds: 0 };
210
223
  }
224
+ emissionTelemetry(): BoundedEmissionTelemetry { return this.emissionGuard.telemetry(); }
211
225
 
212
- append(draft: RuntimeEventDraft, recordedAt = new Date().toISOString()): { event: RuntimeEventV2; appended: boolean } {
226
+ append(draft: RuntimeEventDraft, recordedAt = new Date().toISOString()): RuntimeEventAppendResult {
213
227
  if (this.resyncRequired()) throw new Error("WebUI event store requires resync");
214
228
  if (draft.projectRef !== this.options.projectRef || draft.runtimeInstanceId !== this.options.runtimeInstanceId || draft.sessionRef !== this.options.sessionRef) throw new Error("Runtime event identity does not match its store");
215
229
  const id = eventId(draft), duplicate = this.eventById.get(id);
216
- if (duplicate) return { event: structuredClone(duplicate), appended: false };
230
+ const candidate = runtimeEventEmissionCandidate(draft, id);
231
+ if (duplicate) {
232
+ const decision = candidate ? this.emissionGuard.inspect(candidate) : null;
233
+ return { event: structuredClone(duplicate), appended: false,
234
+ suppressionReason: decision?.reason ?? "event-id-duplicate" };
235
+ }
217
236
  const sequence = (this.events.at(-1)?.writerSequence ?? 0) + 1;
218
237
  const event = validateRuntimeEventEnvelope({ schemaVersion: 2, eventId: id, eventCursor: eventCursor(sequence, id), writerSequence: sequence, recordedAt, ...structuredClone(draft) });
238
+ if (candidate) {
239
+ const decision = this.emissionGuard.inspect(candidate);
240
+ if (!decision.accepted) {
241
+ const prior = decision.priorReference ? this.eventById.get(decision.priorReference) ?? null : null;
242
+ return { event: prior ? structuredClone(prior) : null, appended: false, suppressionReason: decision.reason as EmissionSuppressionReason };
243
+ }
244
+ }
219
245
  let segment = this.segments.at(-1);
220
246
  if (!segment || segment.count >= this.maxEventsPerSegment) {
221
247
  segment = { file: segmentName(sequence), start: sequence, count: 0 }; this.segments.push(segment);
@@ -228,8 +254,9 @@ export class RuntimeEventStore {
228
254
  const descriptor = fs.openSync(target, flags, 0o600);
229
255
  try { fs.writeFileSync(descriptor, `${JSON.stringify(event)}\n`); fs.fsyncSync(descriptor); fs.fchmodSync(descriptor, 0o600); } finally { fs.closeSync(descriptor); }
230
256
  segment.count += 1; this.events.push(event); this.eventById.set(event.eventId, event);
257
+ if (candidate) this.emissionGuard.recordAccepted(candidate);
231
258
  try { this.enforceRetention(); } catch { this.corruptions.push("event-retention-failed"); }
232
- return { event: structuredClone(event), appended: true };
259
+ return { event: structuredClone(event), appended: true, suppressionReason: null };
233
260
  }
234
261
 
235
262
  private enforceRetention(): void {
@@ -240,7 +267,10 @@ export class RuntimeEventStore {
240
267
  if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("event retention target is unsafe");
241
268
  fs.unlinkSync(target);
242
269
  const discarded = this.events.splice(0, removed.count);
243
- for (const event of discarded) this.eventById.delete(event.eventId);
270
+ for (const event of discarded) {
271
+ this.eventById.delete(event.eventId);
272
+ this.emissionGuard.forget(event.eventId);
273
+ }
244
274
  }
245
275
  }
246
276
 
@@ -96,7 +96,7 @@ export function projectTaskCompactionHistory(input: {
96
96
  let malformed = inspected.corruptions;
97
97
  const matching = inspected.records.flatMap((record: any, ordinal: number) => {
98
98
  if (record?.taskId !== input.task.taskId || record?.taskRunId !== input.task.taskRunId || record?.sessionId !== input.task.sessionId) return [];
99
- if (record.schemaVersion !== 1 || !timestamp(record.recordedAt)) { malformed += 1; return []; }
99
+ if (![1, 2].includes(record.schemaVersion) || !timestamp(record.recordedAt)) { malformed += 1; return []; }
100
100
  if (record.event === "session_compact") {
101
101
  if (typeof record.willRetry !== "boolean" || typeof record.fromExtension !== "boolean") { malformed += 1; return []; }
102
102
  return [{ ordinal, recordedAt: record.recordedAt, kind: "context-compaction", title: "Context đã được compact",
@@ -60,7 +60,7 @@ export function projectTaskHandoffHistory(input: { cwd: string; task: Task; iden
60
60
  const matching = inspected.records.flatMap((record: any, ordinal: number) => {
61
61
  if (record?.event !== "handoff_projection_written" || record.taskId !== input.task.taskId
62
62
  || record.taskRunId !== input.task.taskRunId || record.sessionId !== input.task.sessionId) return [];
63
- if (record.schemaVersion !== 1 || !timestamp(record.recordedAt) || typeof record.completionApproved !== "boolean") { malformed += 1; return []; }
63
+ if (![1, 2].includes(record.schemaVersion) || !timestamp(record.recordedAt) || typeof record.completionApproved !== "boolean") { malformed += 1; return []; }
64
64
  const action = display(record.recoveryAction, 80) || "unknown", phase = display(record.phase, 64) || null;
65
65
  return [{ ordinal, recordedAt: record.recordedAt, phase, completionApproved: record.completionApproved, projectedAction: action }];
66
66
  });
@@ -5,7 +5,7 @@ import { inspectOwnedWorkBudget, type OwnedWorkReservation } from "../orchestrat
5
5
  import { taskRunOpaqueRef } from "./task-run-index.ts";
6
6
 
7
7
  type Task = { taskId: string; taskRunId: string; trace?: { outcome?: unknown }; orchestration?: Record<string, unknown>;
8
- acceptanceReceipt?: { helperUsage?: { used?: unknown; helpers?: unknown[] } }; [key: string]: any };
8
+ acceptanceReceipt?: { helperUsage?: { used?: unknown; decision?: unknown; projectedSavingsRatio?: unknown; reasonCodes?: unknown; helpers?: unknown[] } }; [key: string]: any };
9
9
  type Identity = { projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string; taskRunId: string;
10
10
  agentOperationId: null; toolCallId: null };
11
11
 
@@ -53,9 +53,32 @@ function orchestration(task: Task) {
53
53
  };
54
54
  }
55
55
 
56
+ function delegation(task: Task) {
57
+ const usage = task.acceptanceReceipt?.helperUsage;
58
+ const explicitDecision = usage?.decision;
59
+ const action = explicitDecision === "dispatch" || explicitDecision === "skip"
60
+ ? explicitDecision
61
+ : usage?.used === true ? "dispatch" : "skip";
62
+ const reasonCodes = Array.isArray(usage?.reasonCodes)
63
+ ? [...new Set(usage.reasonCodes.map((item) => String(item ?? "").trim()).filter((item) => /^[a-z0-9_.:-]+$/.test(item)).slice(0, 16))]
64
+ : action === "dispatch" ? ["helper-dispatched"] : ["parent-direct-default", "delegation-evidence-missing"];
65
+ const projected = usage?.projectedSavingsRatio;
66
+ return {
67
+ action,
68
+ reasonCodes,
69
+ projectedNetSavingsRatio: typeof projected === "number" && Number.isFinite(projected)
70
+ ? Math.max(-10, Math.min(1, projected))
71
+ : null,
72
+ minimumRequiredNetSavingsRatio: 0.3,
73
+ budget: { maxConcurrent: 1, maxTotal: 1, maxRetries: 0, maxWriters: 0 }
74
+ };
75
+ }
76
+
56
77
  function unavailable(identity: Identity, runRef: string, generatedAt: string, reasonCode: string) {
57
78
  return { schemaVersion: 1, version: "piagent-webui-subagent-tree-v1", generatedAt, identity: structuredClone(identity), runRef,
58
79
  state: "unavailable", treeRevision: null, evidenceState: "unknown", orchestration: { mode: "unknown", subagents: "unknown" },
80
+ delegation: { action: "unknown", reasonCodes: [reasonCode], projectedNetSavingsRatio: null,
81
+ minimumRequiredNetSavingsRatio: 0.3, budget: { maxConcurrent: 1, maxTotal: 1, maxRetries: 0, maxWriters: 0 } },
59
82
  parent: null, children: [], writer: { state: "unknown", ownerNodeRef: null },
60
83
  nestedLineage: { state: "unavailable", reasonCode: "no-durable-nested-lineage" },
61
84
  summary: { total: 0, active: 0, completed: 0, staleResults: 0, readOnly: 0, singleWriter: 0 }, warnings: [],
@@ -66,7 +89,7 @@ export function projectTaskSubagentTree(input: { cwd: string; task: Task; identi
66
89
  const generatedAt = input.generatedAt ?? new Date().toISOString(), runRef = taskRunOpaqueRef(input.task.taskRunId);
67
90
  const inspected = inspectOwnedWorkBudget(input.cwd, input.task.taskId, input.task.taskRunId, generatedAt);
68
91
  if (inspected.state === "corrupt") return unavailable(input.identity, runRef, generatedAt, inspected.reasonCode ?? "helper-budget-corrupt");
69
- const policy = orchestration(input.task), parentRef = opaque("parent", input.task.taskRunId), receipts = helperReceipts(input.task);
92
+ const policy = orchestration(input.task), delegationDecision = delegation(input.task), parentRef = opaque("parent", input.task.taskRunId), receipts = helperReceipts(input.task);
70
93
  const children = inspected.reservations.map((item) => child(input.task.taskRunId, parentRef, item, receipts.get(item.deduplicationKey)));
71
94
  const matchedReceipts = new Set(inspected.reservations.map((item) => item.deduplicationKey).filter((key) => receipts.has(key)));
72
95
  const unmatchedReceipts = Math.max(0, receipts.size - matchedReceipts.size);
@@ -90,10 +113,10 @@ export function projectTaskSubagentTree(input: { cwd: string; task: Task; identi
90
113
  staleResults: children.filter((item) => item.result.state === "stale-result").length,
91
114
  readOnly: children.filter((item) => item.authority === "read-only").length,
92
115
  singleWriter: children.filter((item) => item.authority === "single-writer").length };
93
- const treeRevision = `subagent-tree.${createHash("sha256").update(JSON.stringify({ policy, parentState, terminal: inspected.terminal,
116
+ const treeRevision = `subagent-tree.${createHash("sha256").update(JSON.stringify({ policy, delegationDecision, parentState, terminal: inspected.terminal,
94
117
  children, writer, evidenceState })).digest("hex")}`;
95
118
  return { schemaVersion: 1, version: "piagent-webui-subagent-tree-v1", generatedAt, identity: structuredClone(input.identity), runRef,
96
- state: "ready", treeRevision, evidenceState, orchestration: policy,
119
+ state: "ready", treeRevision, evidenceState, orchestration: policy, delegation: delegationDecision,
97
120
  parent: { nodeRef: parentRef, lifecycleState: parentState, budgetTerminal: inspected.terminal, mergeOwner: "parent" }, children, writer,
98
121
  nestedLineage: { state: "unavailable", reasonCode: "no-durable-nested-lineage" }, summary, warnings,
99
122
  health: warnings.length ? { state: "degraded", reasonCode: "subagent-tree-incomplete", message: "Some helper/subagent evidence is incomplete." }
@@ -0,0 +1,70 @@
1
+ export type ToolFailureReasonCode =
2
+ | "target-not-found"
3
+ | "search-target-missing"
4
+ | "edit-anchor-not-unique"
5
+ | "edit-anchor-stale"
6
+ | "helper-dispatch-rejected"
7
+ | "helper-insufficient-evidence"
8
+ | "tool-result-failed";
9
+
10
+ function boundedText(content: unknown, depth = 0): string {
11
+ if (depth > 2) return "";
12
+ const nested = content && typeof content === "object" && !Array.isArray(content)
13
+ ? (content as { content?: unknown }).content
14
+ : undefined;
15
+ const text = typeof content === "string" ? content : Array.isArray(content)
16
+ ? content.filter((item) => item && typeof item === "object" && (item as { type?: unknown }).type === "text")
17
+ .map((item) => String((item as { text?: unknown }).text ?? "")).join("\n")
18
+ : nested !== undefined ? boundedText(nested, depth + 1) : "";
19
+ return text.slice(0, 8_192);
20
+ }
21
+
22
+ function commandFromInput(input: unknown): string {
23
+ if (!input || typeof input !== "object") return "";
24
+ const direct = (input as { command?: unknown }).command;
25
+ if (typeof direct === "string") return direct;
26
+ const args = (input as { args?: unknown }).args;
27
+ return args && typeof args === "object" && typeof (args as { command?: unknown }).command === "string"
28
+ ? (args as { command: string }).command : "";
29
+ }
30
+
31
+ export function classifyToolFailure(toolName: string, isError: boolean, content: unknown, input?: unknown): ToolFailureReasonCode | null {
32
+ const text = boundedText(content);
33
+ const subagent = /(?:^|[._-])subagents?(?:$|[._-])/i.test(toolName);
34
+ // Some subagent providers return a successful tool envelope even though no
35
+ // child was launched, or a zero-exit child explicitly reports that it could
36
+ // inspect no evidence. Canonical replay must not turn either outcome into a
37
+ // green "passed" Activity row merely because `isError` is false.
38
+ if (subagent && /spawn limit reached|no children were started|declared run cannot fit/i.test(text)) {
39
+ return "helper-dispatch-rejected";
40
+ }
41
+ if (subagent && /scope inspected:[^\n]{0,16}\bnone\b|insufficient-evidence rule|no source (?:content|evidence)[^\n]{0,80}(?:accessible|collected)/i.test(text)) {
42
+ return "helper-insufficient-evidence";
43
+ }
44
+ if (!isError) return null;
45
+ // Only the registered, policy-governed `edit` tool may receive an automatic
46
+ // current-file recovery snapshot. A similarly named third-party `replace`
47
+ // tool is not necessarily covered by Piagent's mutation guard.
48
+ if (/^edit$/i.test(toolName)) {
49
+ if (/\b(?:oldtext|old text)\b[^\n]{0,240}\b(?:unique|occurrences?)\b|\bfound\s+\d+\s+occurrences?\b/i.test(text)) {
50
+ return "edit-anchor-not-unique";
51
+ }
52
+ if (/could not find (?:the )?exact text|\b(?:oldtext|old text)\b[^\n]{0,240}\b(?:match|mismatch|stale|changed|find)\b|\b(?:generation|content) drift\b/i.test(text)) {
53
+ return "edit-anchor-stale";
54
+ }
55
+ }
56
+ const readLike = /(?:^|[._-])(?:read|document[_-]?read)(?:$|[._-])/i.test(toolName);
57
+ if (readLike && /\bENOENT\b|no such file or directory|file (?:does not exist|not found)|cannot find the (?:file|path)/i.test(text)) {
58
+ return "target-not-found";
59
+ }
60
+ const shellLike = /^(?:bash|shell|exec|command)$/i.test(toolName);
61
+ const searchCommand = /^\s*(?:rg|grep|git\s+grep)\b/.test(commandFromInput(input));
62
+ const missingTarget = /no such file or directory|cannot find the (?:file|path)|\bos error 2\b/i.test(text);
63
+ const usefulMatches = text.split(/\r?\n/).some((line) => !/^\s*(?:rg|grep):/i.test(line) && /^.+?:\d+:/.test(line));
64
+ if (shellLike && searchCommand && missingTarget && usefulMatches) return "search-target-missing";
65
+ return "tool-result-failed";
66
+ }
67
+
68
+ export function handledToolFailure(reasonCode: unknown): boolean {
69
+ return reasonCode === "search-target-missing";
70
+ }