@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
@@ -8,6 +8,8 @@ import { workingTreeEvidenceDigest, workingTreeSnapshotUsesCurrentAlgorithm } fr
8
8
  import { inspectTaskContinuationBudget } from "../recovery/continuation-budget.ts";
9
9
  import { readHandoffProjection } from "../recovery/handoff-projection.ts";
10
10
  import type { ActivityInspectorEvent, CurrentActivity } from "../product/activity-inspector.ts";
11
+ import { recoveredToolCalls } from "./activity-recovery.ts";
12
+ import { classifyToolFailure, handledToolFailure } from "./tool-failure-classification.ts";
11
13
  import { projectCriteriaFileVerifier, type CriteriaLinkProjection } from "./criteria-links.ts";
12
14
  import { collectSourceChangeViews, type SourceChangeDocument, type WebUiIdentity } from "./source-change-projection.ts";
13
15
 
@@ -82,6 +84,60 @@ function scopedEvents(events: ActivityInspectorEvent[], sessionId: string, task?
82
84
  return [...deduped.values()].sort((left, right) => String(left.recordedAt).localeCompare(String(right.recordedAt))).slice(-2_000);
83
85
  }
84
86
 
87
+ type CanonicalToolResult = { content: unknown; isError: boolean; toolName: string; recordedAt?: string };
88
+
89
+ function canonicalToolEvidence(sessionEntries: unknown[]): {
90
+ inputs: Map<string, unknown>;
91
+ results: Map<string, CanonicalToolResult>;
92
+ } {
93
+ const inputs = new Map<string, unknown>(), results = new Map<string, CanonicalToolResult>();
94
+ for (const entry of sessionEntries) {
95
+ if (!entry || typeof entry !== "object" || (entry as any).type !== "message") continue;
96
+ const message = (entry as any).message;
97
+ if (message?.role === "assistant" && Array.isArray(message.content)) {
98
+ for (const block of message.content) if (block?.type === "toolCall" && typeof block.id === "string") inputs.set(block.id, block.arguments);
99
+ }
100
+ if (message?.role === "toolResult" && typeof message.toolCallId === "string") {
101
+ const recordedAt = TIMESTAMP.test(String((entry as any).timestamp ?? message.timestamp ?? ""))
102
+ ? String((entry as any).timestamp ?? message.timestamp) : undefined;
103
+ results.set(message.toolCallId, { content: message.content, isError: message.isError === true,
104
+ toolName: typeof message.toolName === "string" ? message.toolName : "", recordedAt });
105
+ }
106
+ }
107
+ return { inputs, results };
108
+ }
109
+
110
+ function reconciledToolResults(events: ActivityInspectorEvent[], evidence: ReturnType<typeof canonicalToolEvidence>): ActivityInspectorEvent[] {
111
+ const resultIds = new Set(events.filter((event) => event.event === "tool_result" && event.toolCallId).map((event) => event.toolCallId as string));
112
+ const reconciled = events.map((event) => {
113
+ if (event.event !== "tool_result" || !event.toolCallId) return event;
114
+ const raw = evidence.results.get(event.toolCallId);
115
+ if (!raw) return event;
116
+ const reasonCode = classifyToolFailure(raw.toolName || event.toolName, raw.isError, raw.content, evidence.inputs.get(event.toolCallId));
117
+ if (!event.isError) {
118
+ // A newer explicit success remains authoritative over an older canonical
119
+ // hard error. The exception is a provider-level helper soft failure:
120
+ // legacy telemetry could only see `isError=false`, while the canonical
121
+ // result proves that no useful child actually ran.
122
+ if (raw.isError || !["helper-dispatch-rejected", "helper-insufficient-evidence"].includes(String(reasonCode))) return event;
123
+ return { ...event, isError: true, reasonCode };
124
+ }
125
+ return reasonCode ? { ...event, reasonCode } : event;
126
+ });
127
+ for (const call of events) {
128
+ if (call.event !== "tool_call" || !call.toolCallId || resultIds.has(call.toolCallId)) continue;
129
+ const raw = evidence.results.get(call.toolCallId);
130
+ if (!raw) continue;
131
+ const reasonCode = classifyToolFailure(raw.toolName || call.toolName, raw.isError, raw.content, evidence.inputs.get(call.toolCallId));
132
+ const effectiveError = raw.isError || Boolean(reasonCode && !handledToolFailure(reasonCode));
133
+ reconciled.push({ activityId: `canonical-result:${call.toolCallId}`, event: "tool_result",
134
+ recordedAt: raw.recordedAt ?? call.recordedAt, sessionId: call.sessionId, taskRunId: call.taskRunId,
135
+ toolCallId: call.toolCallId, toolName: raw.toolName || call.toolName, isError: effectiveError,
136
+ ...(reasonCode ? { reasonCode } : {}) });
137
+ }
138
+ return reconciled.sort((left, right) => String(left.recordedAt ?? "").localeCompare(String(right.recordedAt ?? "")));
139
+ }
140
+
85
141
  function sourceSummary(document: SourceChangeDocument | null, view: "task" | "working-tree" | "staged") {
86
142
  if (!document || document.availability.state === "unavailable") return {
87
143
  view, base: view === "task" ? "task-baseline" : "HEAD", state: "unavailable", revision: null,
@@ -109,21 +165,26 @@ function sourceSummary(document: SourceChangeDocument | null, view: "task" | "wo
109
165
  function activityProjection(events: ActivityInspectorEvent[], current: CurrentActivity[]) {
110
166
  const results = new Map(events.filter((event) => event.event === "tool_result").map((event) => [event.toolCallId, event]));
111
167
  const decisions = new Map(events.filter((event) => event.event === "tool_decision").map((event) => [event.toolCallId, event]));
168
+ const recoveries = recoveredToolCalls(events);
112
169
  const calls = events.filter((event) => event.event === "tool_call").slice(-200);
113
170
  const projected = calls.map((call) => {
114
171
  const result = results.get(call.toolCallId), decision = decisions.get(call.toolCallId);
115
172
  const blocked = decision?.decision === "blocked";
116
- const failed = !blocked && Boolean(result && (result.isError === true || (typeof result.exitCode === "number" && result.exitCode !== 0)));
117
- const state = blocked ? "blocked" : !result ? "running" : failed ? "failed" : "passed";
173
+ const handled = Boolean(result && handledToolFailure(result.reasonCode));
174
+ const failed = !blocked && !handled && Boolean(result && (result.isError === true || (typeof result.exitCode === "number" && result.exitCode !== 0)));
175
+ const recovery = call.toolCallId ? recoveries.get(call.toolCallId) : undefined;
176
+ const state = blocked ? "blocked" : !result ? "running" : failed && !recovery ? "failed" : "passed";
118
177
  const toolName = display(call.toolName ?? "unknown", 80).replace(/[^A-Za-z0-9._:@~-]/g, "-") || "unknown";
119
178
  const rawId = String(call.toolCallId ?? call.activityId ?? `${call.recordedAt}:${toolName}`);
120
179
  const command = display(call.command ?? call.targetPath ?? toolName, 65_536);
121
180
  return {
122
181
  activityRef: token("activity", [rawId, call.recordedAt]), kind: ["bash", "shell", "exec"].includes(toolName) ? "command" : "tool", state,
123
- label: display(state === "running" ? `${toolName} running` : `${toolName} ${state}`, 500), preview: command,
182
+ label: display(recovery ? `${toolName} recovered` : handled ? `${toolName} warning` : state === "running" ? `${toolName} running` : `${toolName} ${state}`, 500), preview: command,
124
183
  toolCallId: token("tool", rawId), toolName, commandDigest: call.command ? `sha256:${hash(call.command)}` : null,
125
- logRef: null, exitCode: typeof result?.exitCode === "number" ? result.exitCode : null, exitCodeExact: result?.exitCodeExact === true,
126
- startedAt: timestamp(call.recordedAt), finishedAt: result || blocked ? timestamp(result?.recordedAt ?? decision?.recordedAt, timestamp(call.recordedAt)) : null
184
+ logRef: null, exitCode: recovery?.exitCode ?? (typeof result?.exitCode === "number" ? result.exitCode : null),
185
+ exitCodeExact: recovery?.exitCodeExact ?? (result?.exitCodeExact === true),
186
+ startedAt: timestamp(call.recordedAt), finishedAt: recovery ? timestamp(recovery.recoveredAt, timestamp(call.recordedAt))
187
+ : result || blocked ? timestamp(result?.recordedAt ?? decision?.recordedAt, timestamp(call.recordedAt)) : null
127
188
  };
128
189
  });
129
190
  const callIds = new Set(calls.map((call) => String(call.toolCallId ?? "")));
@@ -274,12 +335,15 @@ export async function buildWebUiInspectionProjection(input: {
274
335
  const taskRevision = input.task ? webUiTaskRevision(input.task) : null;
275
336
  const views = await collectSourceChangeViews({ cwd: input.cwd, identity, generatedAt, taskRevision,
276
337
  isProtectedPath: (_root, repoPath) => Boolean(matchesProtectedPath(repoPath, effectiveProtectedPaths)) });
277
- const events = scopedEvents(input.events ?? [], input.sessionId, input.task);
338
+ const canonicalEvidence = canonicalToolEvidence(input.sessionEntries ?? []);
339
+ const events = reconciledToolResults(scopedEvents(input.events ?? [], input.sessionId, input.task), canonicalEvidence);
278
340
  const linked = projectCriteriaFileVerifier({ cwd: input.cwd, task: input.task, sourceViews: views, currentSnapshot,
279
341
  protectedPaths: input.protectedPaths, events, at: new Date(generatedAt) });
280
342
  const sourceChanges = { task: sourceSummary(linked.sourceViews.task, "task"), workingTree: sourceSummary(linked.sourceViews.workingTree, "working-tree"), staged: sourceSummary(linked.sourceViews.staged, "staged") };
281
343
  const currentDigest = workingTreeSnapshotUsesCurrentAlgorithm(currentSnapshot) ? workingTreeEvidenceDigest(currentSnapshot) : null;
282
- const current = input.current ?? [], controlState = input.task?.trace.outcome === "pending" ? "active" as const : "terminal" as const;
344
+ const current = (input.current ?? []).filter((item) => (item.status ?? "running") !== "running" || !canonicalEvidence.results.has(item.toolCallId));
345
+ const runningCurrent = current.find((item) => (item.status ?? "running") === "running");
346
+ const controlState = input.task?.trace.outcome === "pending" ? "active" as const : "terminal" as const;
283
347
  const context = contextProjection(input.contextUsage, generatedAt), usageFacts = usage(input.sessionEntries ?? []);
284
348
  const eventCursor = input.eventCursor ?? token("event-cursor", events.map((event) => event.activityId ?? [event.event, event.toolCallId, event.recordedAt]));
285
349
  const runtimeRevision = token("runtime-rev", [taskRevision, sourceChanges.workingTree.revision, sourceChanges.staged.revision, eventCursor, current]);
@@ -300,8 +364,8 @@ export async function buildWebUiInspectionProjection(input: {
300
364
  capabilities: capabilities(identity, generatedAt, input.resyncRequired === true, input.eventReplay),
301
365
  session: {
302
366
  connectionState: input.resyncRequired ? "resync-required" : "connected", connectionReason: input.resyncRequired ? "event-replay-gap" : null, displayName: input.task?.sessionName ?? null,
303
- operation: current.some((item) => (item.status ?? "running") === "running")
304
- ? { liveness: "running", operationRef: null, hostPhase: { state: "known", value: "tool", evidence: "derived", reasonCode: null }, startedAt: timestamp(current[0]?.startedAt, generatedAt), settledAt: null, reasonCode: null }
367
+ operation: runningCurrent
368
+ ? { liveness: "running", operationRef: null, hostPhase: { state: "known", value: "tool", evidence: "derived", reasonCode: null }, startedAt: timestamp(runningCurrent.startedAt, generatedAt), settledAt: null, reasonCode: null }
305
369
  : { liveness: "idle", operationRef: null, hostPhase: { state: "known", value: "idle", evidence: "derived", reasonCode: null }, startedAt: null, settledAt: null, reasonCode: null },
306
370
  controlState: input.task ? controlState : "active", taskOutcome: input.task?.trace.outcome ?? null, approvalState: approvals.state,
307
371
  verificationState: linked.verification.state, permissionProfile: { state: "unavailable", value: null, evidence: null, reasonCode: "permission-profile-not-projected" },
@@ -50,14 +50,22 @@ function positiveInteger(value: unknown): number | null {
50
50
  return Number.isInteger(value) && Number(value) > 0 && Number(value) <= 10_000_000 ? Number(value) : null;
51
51
  }
52
52
 
53
- function thinkingLevels(value: unknown): string[] | null {
54
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
55
- const levels = Object.entries(value)
56
- .filter(([, mapped]) => mapped !== null && mapped !== false)
57
- .map(([level]) => text(level))
58
- .filter(Boolean)
59
- .sort();
60
- return levels.length > 0 ? [...new Set(levels)].slice(0, 16) : [];
53
+ const PI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
54
+
55
+ function thinkingLevels(value: ModelLike): string[] | null {
56
+ if (typeof value.reasoning !== "boolean") return null;
57
+ if (!value.reasoning) return ["off"];
58
+ const mapping = value.thinkingLevelMap && typeof value.thinkingLevelMap === "object" && !Array.isArray(value.thinkingLevelMap)
59
+ ? value.thinkingLevelMap as Record<string, unknown>
60
+ : {};
61
+ // Match Pi's getSupportedThinkingLevels semantics. Reasoning models support
62
+ // the standard host levels through `high` unless a model explicitly marks a
63
+ // level null. `xhigh` and `max` are opt-in capabilities.
64
+ return PI_THINKING_LEVELS.filter((level) => {
65
+ const mapped = mapping[level];
66
+ if (mapped === null) return false;
67
+ return level === "xhigh" || level === "max" ? mapped !== undefined : true;
68
+ });
61
69
  }
62
70
 
63
71
  function boundedModel(value: ModelLike): AuthenticatedModelCatalogEntry | undefined {
@@ -71,7 +79,7 @@ function boundedModel(value: ModelLike): AuthenticatedModelCatalogEntry | undefi
71
79
  contextWindow: positiveInteger(value.contextWindow),
72
80
  reasoning: typeof value.reasoning === "boolean" ? value.reasoning : null,
73
81
  imageInput: input ? input.includes("image") : null,
74
- supportedThinkingLevels: thinkingLevels(value.thinkingLevelMap)
82
+ supportedThinkingLevels: thinkingLevels(value)
75
83
  };
76
84
  }
77
85
 
@@ -0,0 +1,83 @@
1
+ type ProviderPayload = Record<string, unknown>;
2
+
3
+ const GPT56_CODEX_MODEL = /^gpt-5\.6-(?:luna|sol|terra)$/;
4
+ const PROVIDER_EFFORT_BY_HOST_LEVEL: Readonly<Record<string, string>> = Object.freeze({
5
+ off: "none",
6
+ minimal: "low",
7
+ low: "low",
8
+ medium: "medium",
9
+ high: "high",
10
+ xhigh: "xhigh",
11
+ max: "max"
12
+ });
13
+
14
+ export type OpenAiCodexReasoningNormalization = {
15
+ payload: unknown;
16
+ applicable: boolean;
17
+ changed: boolean;
18
+ hostThinkingLevel: string;
19
+ expectedProviderEffort: string | null;
20
+ providerEffort: string | null;
21
+ reasonCode: "not-applicable" | "observed" | "off-normalized-to-none" | "minimal-normalized-to-low" | "provider-effort-normalized";
22
+ };
23
+
24
+ function record(value: unknown): ProviderPayload | null {
25
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value as ProviderPayload : null;
26
+ }
27
+
28
+ function effortFromPayload(payload: ProviderPayload): string | null {
29
+ const reasoning = record(payload.reasoning);
30
+ return typeof reasoning?.effort === "string" ? reasoning.effort : null;
31
+ }
32
+
33
+ /**
34
+ * Compatibility bridge for the pinned Pi host's OpenAI Codex serializer.
35
+ *
36
+ * Pi names the provider's `none` effort `off`. Pi 0.84.1 currently drops
37
+ * `off` before its request builder, which omits the reasoning object and lets
38
+ * GPT-5.6 use its server default. Enforce the same-model/effort invariant for
39
+ * every host level, while keeping the correction deliberately narrow to the
40
+ * three authenticated GPT-5.6 Codex models whose capability map we audit.
41
+ */
42
+ export function normalizeOpenAiCodexReasoningPayload(input: {
43
+ payload: unknown;
44
+ provider: unknown;
45
+ modelId: unknown;
46
+ hostThinkingLevel: unknown;
47
+ }): OpenAiCodexReasoningNormalization {
48
+ const provider = String(input.provider ?? "");
49
+ const modelId = String(input.modelId ?? "");
50
+ const hostThinkingLevel = String(input.hostThinkingLevel ?? "");
51
+ const payload = record(input.payload);
52
+ const expectedProviderEffort = PROVIDER_EFFORT_BY_HOST_LEVEL[hostThinkingLevel] ?? null;
53
+ const applicable = provider === "openai-codex" && GPT56_CODEX_MODEL.test(modelId)
54
+ && payload !== null && payload.model === modelId && expectedProviderEffort !== null;
55
+ if (!applicable || !payload) {
56
+ return { payload: input.payload, applicable: false, changed: false, hostThinkingLevel,
57
+ expectedProviderEffort, providerEffort: payload ? effortFromPayload(payload) : null, reasonCode: "not-applicable" };
58
+ }
59
+
60
+ const currentEffort = effortFromPayload(payload);
61
+ if (currentEffort === expectedProviderEffort) {
62
+ return { payload: input.payload, applicable: true, changed: false, hostThinkingLevel,
63
+ expectedProviderEffort, providerEffort: currentEffort, reasonCode: "observed" };
64
+ }
65
+
66
+ const currentReasoning = record(payload.reasoning) ?? {};
67
+ const normalizedReasoning = {
68
+ ...currentReasoning,
69
+ effort: expectedProviderEffort,
70
+ ...(typeof currentReasoning.summary === "string" ? {} : { summary: "auto" })
71
+ };
72
+ return {
73
+ payload: { ...payload, reasoning: normalizedReasoning },
74
+ applicable: true,
75
+ changed: true,
76
+ hostThinkingLevel,
77
+ expectedProviderEffort,
78
+ providerEffort: expectedProviderEffort,
79
+ reasonCode: hostThinkingLevel === "off" ? "off-normalized-to-none"
80
+ : hostThinkingLevel === "minimal" ? "minimal-normalized-to-low"
81
+ : "provider-effort-normalized"
82
+ };
83
+ }
@@ -0,0 +1,238 @@
1
+ import crypto from "node:crypto";
2
+
3
+ type JsonRecord = Record<string, unknown>;
4
+ type BoundReason = "instructions-unavailable" | "instructions-too-large" | "tools-invalid" | "tools-too-large"
5
+ | "input-invalid" | "input-too-large" | "deferred-tools-invalid" | "deferred-tools-too-large"
6
+ | "surface-too-deep" | "surface-too-large" | "surface-not-json" | "surface-cyclic";
7
+
8
+ const MAX_INSTRUCTIONS_CHARS = 1_000_000;
9
+ const MAX_TOOLS = 512;
10
+ const MAX_INPUT_ITEMS = 4_096;
11
+ const MAX_SURFACE_DEPTH = 32;
12
+ const MAX_SURFACE_NODES = 50_000;
13
+ const MAX_SURFACE_STRING_CHARS = 2_000_000;
14
+ const MAX_CONTAINER_ITEMS = 4_096;
15
+ const PUBLIC_ID = /^[A-Za-z0-9][A-Za-z0-9._:@~-]{0,159}$/;
16
+ const CONTROL_VALUE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
17
+
18
+ export type ProviderWireFingerprint = {
19
+ applicable: boolean;
20
+ state: "known" | "unavailable";
21
+ reasonCode: BoundReason | "not-applicable" | null;
22
+ modelId: string | null;
23
+ instructionsHash: string | null;
24
+ baseInstructionsHash: string | null;
25
+ instructionNormalization: "host-relocation-v1" | "none" | null;
26
+ instructionChars: number | null;
27
+ orderedToolSurfaceHash: string | null;
28
+ toolCount: number | null;
29
+ deferredToolSurfaceHash: string | null;
30
+ deferredToolCount: number | null;
31
+ deferredToolBatchCount: number | null;
32
+ reasoningEffort: string | null;
33
+ textVerbosity: string | null;
34
+ toolChoiceKind: string | null;
35
+ toolChoiceHash: string | null;
36
+ requestPrefixFingerprint: string | null;
37
+ };
38
+
39
+ class SurfaceBoundError extends Error {
40
+ readonly reasonCode: BoundReason;
41
+ constructor(reasonCode: BoundReason) { super(reasonCode); this.reasonCode = reasonCode; }
42
+ }
43
+
44
+ function record(value: unknown): JsonRecord | null {
45
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value as JsonRecord : null;
46
+ }
47
+
48
+ function hash(value: string): string {
49
+ return crypto.createHash("sha256").update(value).digest("hex");
50
+ }
51
+
52
+ function control(value: unknown): string | null {
53
+ return typeof value === "string" && CONTROL_VALUE.test(value) ? value : null;
54
+ }
55
+
56
+ function canonicalJson(value: unknown): string {
57
+ const budget = { nodes: 0, stringChars: 0, active: new WeakSet<object>() };
58
+ const visit = (item: unknown, depth: number, inArray: boolean): unknown => {
59
+ budget.nodes += 1;
60
+ if (budget.nodes > MAX_SURFACE_NODES) throw new SurfaceBoundError("surface-too-large");
61
+ if (depth > MAX_SURFACE_DEPTH) throw new SurfaceBoundError("surface-too-deep");
62
+ if (item === null || typeof item === "boolean") return item;
63
+ if (typeof item === "string") {
64
+ budget.stringChars += item.length;
65
+ if (budget.stringChars > MAX_SURFACE_STRING_CHARS) throw new SurfaceBoundError("surface-too-large");
66
+ return item;
67
+ }
68
+ if (typeof item === "number") return Number.isFinite(item) ? item : null;
69
+ if (["undefined", "function", "symbol"].includes(typeof item)) return inArray ? null : undefined;
70
+ if (typeof item === "bigint" || typeof item !== "object") throw new SurfaceBoundError("surface-not-json");
71
+ if (budget.active.has(item)) throw new SurfaceBoundError("surface-cyclic");
72
+ budget.active.add(item);
73
+ try {
74
+ if (Array.isArray(item)) {
75
+ if (item.length > MAX_CONTAINER_ITEMS) throw new SurfaceBoundError("surface-too-large");
76
+ return item.map((entry) => visit(entry, depth + 1, true));
77
+ }
78
+ const entries = Object.entries(item as JsonRecord)
79
+ .filter(([, entry]) => !["undefined", "function", "symbol"].includes(typeof entry));
80
+ if (entries.length > MAX_CONTAINER_ITEMS) throw new SurfaceBoundError("surface-too-large");
81
+ entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
82
+ const output: JsonRecord = {};
83
+ for (const [key, entry] of entries) {
84
+ budget.stringChars += key.length;
85
+ if (budget.stringChars > MAX_SURFACE_STRING_CHARS) throw new SurfaceBoundError("surface-too-large");
86
+ output[key] = visit(entry, depth + 1, false);
87
+ }
88
+ return output;
89
+ } finally {
90
+ budget.active.delete(item);
91
+ }
92
+ };
93
+ return JSON.stringify(visit(value, 0, false));
94
+ }
95
+
96
+ function unavailable(modelId: string, reasonCode: BoundReason): ProviderWireFingerprint {
97
+ return { applicable: true, state: "unavailable", reasonCode, modelId, instructionsHash: null,
98
+ baseInstructionsHash: null, instructionNormalization: null, instructionChars: null,
99
+ orderedToolSurfaceHash: null, toolCount: null, deferredToolSurfaceHash: null, deferredToolCount: null,
100
+ deferredToolBatchCount: null, reasoningEffort: null, textVerbosity: null, toolChoiceKind: null,
101
+ toolChoiceHash: null, requestPrefixFingerprint: null };
102
+ }
103
+
104
+ function xmlEscape(value: string): string {
105
+ return value
106
+ .replace(/&/g, "&amp;")
107
+ .replace(/</g, "&lt;")
108
+ .replace(/>/g, "&gt;")
109
+ .replace(/"/g, "&quot;")
110
+ .replace(/'/g, "&apos;");
111
+ }
112
+
113
+ function boundedHostPaths(value: unknown): string[] {
114
+ if (typeof value !== "string" || value.length === 0 || value.length > 32_768 || value.includes("\0")) return [];
115
+ if (/^[\\/]+$/.test(value) || /^[A-Za-z]:[\\/]?$/.test(value)) return [];
116
+ const native = value.replace(/[\\/]+$/, "");
117
+ const slash = native.replace(/\\/g, "/");
118
+ return [...new Set([native, slash].filter(Boolean))];
119
+ }
120
+
121
+ function normalizeSkillLocations(instructions: string, platformRoot: unknown): string {
122
+ const roots = boundedHostPaths(platformRoot);
123
+ if (roots.length === 0) return instructions;
124
+ const startTag = "<available_skills>";
125
+ const endTag = "</available_skills>";
126
+ let cursor = 0;
127
+ let output = "";
128
+ while (cursor < instructions.length) {
129
+ const start = instructions.indexOf(startTag, cursor);
130
+ if (start < 0) return output + instructions.slice(cursor);
131
+ const endStart = instructions.indexOf(endTag, start + startTag.length);
132
+ if (endStart < 0) return output + instructions.slice(cursor);
133
+ const end = endStart + endTag.length;
134
+ let section = instructions.slice(start, end);
135
+ for (const root of roots) {
136
+ const locationPrefix = `<location>${xmlEscape(root)}`;
137
+ section = section
138
+ .replaceAll(`${locationPrefix}/`, "<location>[PIAGENT_PLATFORM_ROOT]/")
139
+ .replaceAll(`${locationPrefix}\\`, "<location>[PIAGENT_PLATFORM_ROOT]/")
140
+ .replaceAll(`${locationPrefix}</location>`, "<location>[PIAGENT_PLATFORM_ROOT]</location>");
141
+ }
142
+ output += instructions.slice(cursor, start) + section;
143
+ cursor = end;
144
+ }
145
+ return output;
146
+ }
147
+
148
+ function normalizeHostOwnedRelocation(instructions: string, workingDirectory: unknown, platformRoot: unknown): {
149
+ value: string;
150
+ mode: "host-relocation-v1" | "none";
151
+ } {
152
+ const cwdVariants = boundedHostPaths(workingDirectory);
153
+ let value = normalizeSkillLocations(instructions, platformRoot);
154
+ for (const cwd of cwdVariants) {
155
+ const cwdLine = `Current working directory: ${cwd}`;
156
+ if (value.endsWith(cwdLine)) {
157
+ value = `${value.slice(0, -cwdLine.length)}Current working directory: [PIAGENT_PROJECT_ROOT]`;
158
+ }
159
+ value = value
160
+ .replaceAll(`<project_instructions path="${cwd}">`, '<project_instructions path="[PIAGENT_PROJECT_ROOT]">')
161
+ .replaceAll(`<project_instructions path="${cwd}/`, '<project_instructions path="[PIAGENT_PROJECT_ROOT]/')
162
+ .replaceAll(`<project_instructions path="${cwd}\\`, '<project_instructions path="[PIAGENT_PROJECT_ROOT]/');
163
+ }
164
+ return value === instructions
165
+ ? { value, mode: "none" }
166
+ : { value, mode: "host-relocation-v1" };
167
+ }
168
+
169
+ export function buildOpenAiCodexWireFingerprint(input: {
170
+ payload: unknown;
171
+ provider: unknown;
172
+ modelId: unknown;
173
+ workingDirectory?: unknown;
174
+ platformRoot?: unknown;
175
+ }): ProviderWireFingerprint {
176
+ const payload = record(input.payload);
177
+ const provider = typeof input.provider === "string" ? input.provider : "";
178
+ const modelId = typeof input.modelId === "string" && PUBLIC_ID.test(input.modelId) ? input.modelId : "";
179
+ if (provider !== "openai-codex" || !payload || !modelId || payload.model !== modelId) {
180
+ return { ...unavailable("", "surface-not-json"), applicable: false, state: "unavailable",
181
+ reasonCode: "not-applicable", modelId: null };
182
+ }
183
+ if (typeof payload.instructions !== "string") return unavailable(modelId, "instructions-unavailable");
184
+ if (payload.instructions.length > MAX_INSTRUCTIONS_CHARS) return unavailable(modelId, "instructions-too-large");
185
+ const tools = payload.tools === undefined ? [] : payload.tools;
186
+ if (!Array.isArray(tools)) return unavailable(modelId, "tools-invalid");
187
+ if (tools.length > MAX_TOOLS) return unavailable(modelId, "tools-too-large");
188
+ const requestInput = payload.input === undefined ? [] : payload.input;
189
+ if (!Array.isArray(requestInput)) return unavailable(modelId, "input-invalid");
190
+ if (requestInput.length > MAX_INPUT_ITEMS) return unavailable(modelId, "input-too-large");
191
+
192
+ const deferredBatches: Array<{ inputIndex: number; tools: unknown[] }> = [];
193
+ let deferredToolCount = 0;
194
+ for (let index = 0; index < requestInput.length; index += 1) {
195
+ const item = record(requestInput[index]);
196
+ if (item?.type !== "tool_search_output") continue;
197
+ if (!Array.isArray(item.tools)) return unavailable(modelId, "deferred-tools-invalid");
198
+ deferredToolCount += item.tools.length;
199
+ if (deferredToolCount > MAX_TOOLS) return unavailable(modelId, "deferred-tools-too-large");
200
+ deferredBatches.push({ inputIndex: index, tools: item.tools });
201
+ }
202
+
203
+ try {
204
+ const instructionsHash = hash(payload.instructions);
205
+ const normalizedInstructions = normalizeHostOwnedRelocation(payload.instructions, input.workingDirectory, input.platformRoot);
206
+ const baseInstructionsHash = hash(normalizedInstructions.value);
207
+ // Object keys are canonicalized because their order is semantic noise;
208
+ // arrays are preserved so changing provider-visible tool order is detected.
209
+ const orderedToolSurfaceHash = hash(canonicalJson(tools));
210
+ const deferredToolSurfaceHash = hash(canonicalJson(deferredBatches));
211
+ const toolChoice = payload.tool_choice ?? null;
212
+ const toolChoiceHash = hash(canonicalJson(toolChoice));
213
+ const reasoningEffort = control(record(payload.reasoning)?.effort);
214
+ const textVerbosity = control(record(payload.text)?.verbosity);
215
+ const toolChoiceKind = typeof toolChoice === "string" ? control(toolChoice) : record(toolChoice) ? "structured" : "none";
216
+ const requestPrefixFingerprint = hash(JSON.stringify({
217
+ schemaVersion: 1,
218
+ modelId,
219
+ instructionsHash,
220
+ baseInstructionsHash,
221
+ orderedToolSurfaceHash,
222
+ toolCount: tools.length,
223
+ deferredToolSurfaceHash,
224
+ deferredToolCount,
225
+ deferredToolBatchCount: deferredBatches.length,
226
+ reasoningEffort,
227
+ textVerbosity,
228
+ toolChoiceHash
229
+ }));
230
+ return { applicable: true, state: "known", reasonCode: null, modelId, instructionsHash,
231
+ baseInstructionsHash, instructionNormalization: normalizedInstructions.mode,
232
+ instructionChars: payload.instructions.length, orderedToolSurfaceHash, toolCount: tools.length,
233
+ deferredToolSurfaceHash, deferredToolCount, deferredToolBatchCount: deferredBatches.length,
234
+ reasoningEffort, textVerbosity, toolChoiceKind, toolChoiceHash, requestPrefixFingerprint };
235
+ } catch (error) {
236
+ return unavailable(modelId, error instanceof SurfaceBoundError ? error.reasonCode : "surface-not-json");
237
+ }
238
+ }
@@ -5,18 +5,19 @@ import type { RuntimeModelSnapshot } from "../model/runtime-snapshot.ts";
5
5
  import type { SolverDecision, TaskFeatures } from "../solver/solver-types.ts";
6
6
  import { bindRole, type RoleBinding } from "./role-binder.ts";
7
7
  import { AUTOMATIC_OWNED_WORK_CEILINGS, OwnedWorkBudgetController } from "./owned-work-budget.ts";
8
+ import { assessMinimalDelegation, type MinimalDelegationEvidence } from "./minimal-delegation-policy.ts";
8
9
  import { createHelperRequest, defaultRolePolicy, type HelperRequest, type HelperRole } from "./role-policy.ts";
9
10
 
10
11
  export const HELPERS_MODE_VALUES = Object.freeze(["off", "recommend", "on"] as const);
11
12
  export type HelpersMode = typeof HELPERS_MODE_VALUES[number];
12
- export type HelperDecision = { mode: HelpersMode; action: "solo" | "recommend" | "dispatch" | "unavailable" | "blocked"; role: HelperRole | null; reasonCodes: string[]; binding: RoleBinding | null; request: HelperRequest | null };
13
+ export type HelperDecision = { mode: HelpersMode; action: "solo" | "recommend" | "dispatch" | "unavailable" | "blocked"; role: HelperRole | null; reasonCodes: string[]; projectedSavingsRatio: number | null; binding: RoleBinding | null; request: HelperRequest | null };
13
14
  export type HelperDispatchResult = { status: "succeeded" | "failed" | "timeout" | "cancelled"; calls: number; tokens: number; output: string; summary?: string };
14
- export type HelperUsageReceipt = { role: HelperRole; disposition: string; reasonCodes: string[]; requestRef: string; outputDigest: string | null; summary: string | null; mergeOwner: "parent" | null; calls: number; tokens: number; helperUsed: boolean };
15
+ export type HelperUsageReceipt = { role: HelperRole; disposition: string; decision: "dispatch" | "skip"; reasonCodes: string[]; projectedSavingsRatio: number | null; requestRef: string; outputDigest: string | null; summary: string | null; mergeOwner: "parent" | null; calls: number; tokens: number; helperUsed: boolean };
15
16
  type DispatchAdapter = (request: HelperRequest, signal: AbortSignal) => Promise<HelperDispatchResult>;
16
17
  type DispatchOptions = { timeoutMs?: number };
17
18
 
18
19
  function digest(value: unknown): string { return crypto.createHash("sha256").update(String(value ?? "")).digest("hex"); }
19
- export function helpersMode(value = process.env.PIAGENT_HELPERS_MODE): HelpersMode { return HELPERS_MODE_VALUES.includes(value as HelpersMode) ? value as HelpersMode : "recommend"; }
20
+ export function helpersMode(value = process.env.PIAGENT_HELPERS_MODE): HelpersMode { return HELPERS_MODE_VALUES.includes(value as HelpersMode) ? value as HelpersMode : "off"; }
20
21
 
21
22
  export function selectHelperRole(input: { features: TaskFeatures; solver: SolverDecision; confidence?: string; conflictingEvidence?: boolean; repeatedSourceFailure?: boolean; independentReviewUseful?: boolean }): { role: HelperRole | null; reasons: string[] } {
22
23
  if (input.features.riskLane === "tiny") return { role: null, reasons: ["tiny-task-solo"] };
@@ -31,20 +32,23 @@ export class HelperLifecycleRuntime {
31
32
  readonly #budgets: OwnedWorkBudgetController;
32
33
  readonly #active = new Map<string, Set<AbortController>>();
33
34
  constructor(budgets = new OwnedWorkBudgetController()) { this.#budgets = budgets; }
34
- decide(input: { mode: HelpersMode; objective: string; taskId: string; taskRunId: string; sessionId: string; taskScope: string[]; parentAllowedTools: string[]; features: TaskFeatures; solver: SolverDecision; runtime: RuntimeModelSnapshot; catalog: AuthenticatedModelCatalog; confidence?: string; conflictingEvidence?: boolean; repeatedSourceFailure?: boolean; independentReviewUseful?: boolean }): HelperDecision {
35
- if (input.mode === "off") return { mode: input.mode, action: "solo", role: null, reasonCodes: ["helpers-off"], binding: null, request: null };
36
- const selection = selectHelperRole(input); if (!selection.role) return { mode: input.mode, action: "solo", role: null, reasonCodes: selection.reasons, binding: null, request: null };
35
+ decide(input: { mode: HelpersMode; objective: string; taskId: string; taskRunId: string; sessionId: string; taskScope: string[]; parentAllowedTools: string[]; features: TaskFeatures; solver: SolverDecision; runtime: RuntimeModelSnapshot; catalog: AuthenticatedModelCatalog; delegationEvidence?: MinimalDelegationEvidence; confidence?: string; conflictingEvidence?: boolean; repeatedSourceFailure?: boolean; independentReviewUseful?: boolean }): HelperDecision {
36
+ if (input.mode === "off") return { mode: input.mode, action: "solo", role: null, reasonCodes: ["helpers-off", "parent-direct-default"], projectedSavingsRatio: null, binding: null, request: null };
37
+ const selection = selectHelperRole(input); if (!selection.role) return { mode: input.mode, action: "solo", role: null, reasonCodes: selection.reasons, projectedSavingsRatio: null, binding: null, request: null };
38
+ const delegation = assessMinimalDelegation(input.delegationEvidence);
39
+ if (!delegation.eligible) return { mode: input.mode, action: "solo", role: null, reasonCodes: [...selection.reasons, ...delegation.reasonCodes], projectedSavingsRatio: delegation.projectedSavingsRatio, binding: null, request: null };
37
40
  const policy = defaultRolePolicy(selection.role, input.taskScope);
38
41
  const binding = bindRole({ policy, features: input.features, solver: input.solver, runtime: input.runtime, catalog: input.catalog, helperBudgetAvailable: true });
39
- if (binding.disposition !== "recommended") return { mode: input.mode, action: "unavailable", role: selection.role, reasonCodes: [...selection.reasons, ...binding.reasonCodes], binding, request: null };
42
+ if (binding.disposition !== "recommended") return { mode: input.mode, action: "unavailable", role: selection.role, reasonCodes: [...selection.reasons, ...delegation.reasonCodes, ...binding.reasonCodes], projectedSavingsRatio: delegation.projectedSavingsRatio, binding, request: null };
40
43
  const request = createHelperRequest({ policy, objective: input.objective, taskId: input.taskId, taskRunId: input.taskRunId, sessionId: input.sessionId, parentReadScope: input.taskScope, parentWriteScope: input.taskScope, parentAllowedTools: input.parentAllowedTools, requestedReadScope: input.taskScope, requestedWriteScope: [], model: { provider: binding.provider as string, modelId: binding.modelId as string, effort: binding.effort as string, source: binding.mappingVersion }, singleWriterOwnership: null });
41
44
  const action = input.mode === "recommend" ? "recommend" : policy.authority === "read-only" ? "dispatch" : "blocked";
42
- return { mode: input.mode, action, role: selection.role, reasonCodes: action === "blocked" ? [...selection.reasons, "automatic-worker-disabled"] : selection.reasons, binding, request };
45
+ const reasonCodes = [...selection.reasons, ...delegation.reasonCodes];
46
+ return { mode: input.mode, action, role: selection.role, reasonCodes: action === "blocked" ? [...reasonCodes, "automatic-worker-disabled"] : reasonCodes, projectedSavingsRatio: delegation.projectedSavingsRatio, binding, request };
43
47
  }
44
48
  async dispatch(cwd: string, decision: HelperDecision, adapter: DispatchAdapter, options: DispatchOptions = {}): Promise<HelperUsageReceipt> {
45
- if (decision.action !== "dispatch" || !decision.request || !decision.role) return { role: decision.role ?? "scout", disposition: decision.action, reasonCodes: decision.reasonCodes, requestRef: "none", outputDigest: null, summary: null, mergeOwner: null, calls: 0, tokens: 0, helperUsed: false };
49
+ if (decision.action !== "dispatch" || !decision.request || !decision.role) return { role: decision.role ?? "scout", disposition: decision.action, decision: "skip", reasonCodes: decision.reasonCodes, projectedSavingsRatio: decision.projectedSavingsRatio, requestRef: "none", outputDigest: null, summary: null, mergeOwner: null, calls: 0, tokens: 0, helperUsed: false };
46
50
  const reservation = this.#budgets.reserve(cwd, decision.request, undefined, AUTOMATIC_OWNED_WORK_CEILINGS);
47
- if (reservation.decision !== "reserved" || !reservation.reservationId) return { role: decision.role, disposition: reservation.reason, reasonCodes: [...decision.reasonCodes, reservation.reason], requestRef: decision.request.deduplicationKey, outputDigest: null, summary: null, mergeOwner: null, calls: 0, tokens: 0, helperUsed: false };
51
+ if (reservation.decision !== "reserved" || !reservation.reservationId) return { role: decision.role, disposition: reservation.reason, decision: "skip", reasonCodes: [...decision.reasonCodes, reservation.reason], projectedSavingsRatio: decision.projectedSavingsRatio, requestRef: decision.request.deduplicationKey, outputDigest: null, summary: null, mergeOwner: null, calls: 0, tokens: 0, helperUsed: false };
48
52
  const controller = new AbortController(), active = this.#active.get(decision.request.taskRunId) ?? new Set<AbortController>();
49
53
  active.add(controller); this.#active.set(decision.request.taskRunId, active);
50
54
  const ceilingMs = decision.request.ceilings.timeSeconds * 1000;
@@ -66,10 +70,10 @@ export class HelperLifecycleRuntime {
66
70
  const released = this.#budgets.release(cwd, decision.request, reservation.reservationId, terminal, { calls: result.calls, tokens: result.tokens, output: result.output });
67
71
  const calls = Math.min(Math.max(0, Number(result.calls) || 0), decision.request.ceilings.calls + 1);
68
72
  const tokens = Math.min(Math.max(0, Number(result.tokens) || 0), decision.request.contextBudget + 1);
69
- if (!released.accepted) return { role: decision.role, disposition: released.status === "cancelled" ? "cancelled" : "stale-result", reasonCodes: [...decision.reasonCodes, released.reason], requestRef: decision.request.deduplicationKey, outputDigest: null, summary: null, mergeOwner: null, calls, tokens, helperUsed: true };
73
+ if (!released.accepted) return { role: decision.role, disposition: released.status === "cancelled" ? "cancelled" : "stale-result", decision: "dispatch", reasonCodes: [...decision.reasonCodes, released.reason], projectedSavingsRatio: decision.projectedSavingsRatio, requestRef: decision.request.deduplicationKey, outputDigest: null, summary: null, mergeOwner: null, calls, tokens, helperUsed: true };
70
74
  const withinBudget = released.reason === "released", succeeded = result.status === "succeeded" && withinBudget;
71
75
  const summary = succeeded && typeof result.summary === "string" ? redactSensitiveText(result.summary).text.replace(/\s+/g, " ").trim().slice(0, 1000) || null : null;
72
- return { role: decision.role, disposition: withinBudget ? result.status : "budget-exceeded", reasonCodes: withinBudget ? decision.reasonCodes : [...decision.reasonCodes, released.reason], requestRef: decision.request.deduplicationKey, outputDigest: succeeded && result.output ? digest(result.output) : null, summary, mergeOwner: summary ? "parent" : null, calls, tokens, helperUsed: true };
76
+ return { role: decision.role, disposition: withinBudget ? result.status : "budget-exceeded", decision: "dispatch", reasonCodes: withinBudget ? decision.reasonCodes : [...decision.reasonCodes, released.reason], projectedSavingsRatio: decision.projectedSavingsRatio, requestRef: decision.request.deduplicationKey, outputDigest: succeeded && result.output ? digest(result.output) : null, summary, mergeOwner: summary ? "parent" : null, calls, tokens, helperUsed: true };
73
77
  }
74
78
  cancelTask(cwd: string, decision: HelperDecision, now = new Date().toISOString()): number {
75
79
  if (!decision.request) return 0;
@@ -0,0 +1,71 @@
1
+ export const MINIMAL_DELEGATION_POLICY_VERSION = "minimal-delegation-v1" as const;
2
+
3
+ export const MINIMAL_DELEGATION_LIMITS = Object.freeze({
4
+ maxConcurrentHelpers: 1,
5
+ maxTotalHelpers: 1,
6
+ maxRetries: 0,
7
+ maxHelperCalls: 8,
8
+ maxTransferTokens: 2_048,
9
+ minimumProjectedSavingsRatio: 0.3
10
+ });
11
+
12
+ export type MinimalDelegationEvidence = {
13
+ source: "runtime-estimate";
14
+ independentWorkstreams: string[];
15
+ canRunWithoutParentOutput: boolean;
16
+ estimatedSoloTokens: number;
17
+ estimatedTotalTokensWithHelper: number;
18
+ transferTokens: number;
19
+ inheritedParentTokens: number;
20
+ retryOfDeterministicFailure: boolean;
21
+ };
22
+
23
+ export type MinimalDelegationAssessment = {
24
+ eligible: boolean;
25
+ projectedSavingsRatio: number | null;
26
+ reasonCodes: string[];
27
+ };
28
+
29
+ function boundedInteger(value: unknown, minimum: number, maximum: number): value is number {
30
+ return Number.isInteger(value) && Number(value) >= minimum && Number(value) <= maximum;
31
+ }
32
+
33
+ function validWorkstreams(value: unknown): value is string[] {
34
+ return Array.isArray(value)
35
+ && value.length >= 2
36
+ && value.length <= 8
37
+ && new Set(value).size === value.length
38
+ && value.every((item) => typeof item === "string" && /^[a-z0-9][a-z0-9:._/-]{0,127}$/i.test(item));
39
+ }
40
+
41
+ /**
42
+ * Helpers are an optimization, not a default reasoning layer. A helper is only
43
+ * eligible when the runtime can show two genuinely independent lanes, a fresh
44
+ * minimal handoff, and at least 30% projected token savings after merge cost.
45
+ * The estimate is advisory, while the one-helper ledger below is enforcing.
46
+ */
47
+ export function assessMinimalDelegation(evidence?: MinimalDelegationEvidence): MinimalDelegationAssessment {
48
+ if (!evidence) return { eligible: false, projectedSavingsRatio: null, reasonCodes: ["delegation-evidence-missing", "parent-direct-default"] };
49
+
50
+ const reasons: string[] = [];
51
+ if (evidence.source !== "runtime-estimate") reasons.push("delegation-estimate-not-runtime-owned");
52
+ if (!validWorkstreams(evidence.independentWorkstreams)) reasons.push("independent-workstreams-not-proven");
53
+ if (evidence.canRunWithoutParentOutput !== true) reasons.push("helper-depends-on-parent-output");
54
+ if (!boundedInteger(evidence.estimatedSoloTokens, 1, 100_000_000)
55
+ || !boundedInteger(evidence.estimatedTotalTokensWithHelper, 1, 100_000_000)) reasons.push("delegation-token-estimate-invalid");
56
+ if (!boundedInteger(evidence.transferTokens, 0, MINIMAL_DELEGATION_LIMITS.maxTransferTokens)) reasons.push("helper-context-transfer-too-large");
57
+ if (evidence.inheritedParentTokens !== 0) reasons.push("parent-history-inheritance-forbidden");
58
+ if (evidence.retryOfDeterministicFailure !== false) reasons.push("deterministic-helper-retry-forbidden");
59
+
60
+ const projectedSavingsRatio = boundedInteger(evidence.estimatedSoloTokens, 1, 100_000_000)
61
+ && boundedInteger(evidence.estimatedTotalTokensWithHelper, 1, 100_000_000)
62
+ ? (evidence.estimatedSoloTokens - evidence.estimatedTotalTokensWithHelper) / evidence.estimatedSoloTokens
63
+ : null;
64
+ if (projectedSavingsRatio === null || projectedSavingsRatio < MINIMAL_DELEGATION_LIMITS.minimumProjectedSavingsRatio) {
65
+ reasons.push("projected-token-savings-below-30pct");
66
+ }
67
+
68
+ return reasons.length
69
+ ? { eligible: false, projectedSavingsRatio, reasonCodes: [...new Set(reasons), "parent-direct-default"] }
70
+ : { eligible: true, projectedSavingsRatio, reasonCodes: ["independent-workstreams-proven", "projected-token-savings-at-least-30pct", "isolated-minimal-context"] };
71
+ }