@tea-agent/loop-agent 0.42.0-next.9 → 0.42.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 (236) hide show
  1. package/CHANGELOG.md +111 -45
  2. package/dist/application/dag/run-dag.js +8 -2
  3. package/dist/application/evaluation/budget.js +19 -1
  4. package/dist/application/task-lifecycle/advance.js +26 -8
  5. package/dist/application/task-lifecycle/observe.js +43 -29
  6. package/dist/application/task-lifecycle/plan-transitions.js +5 -4
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/program.js +1 -1
  9. package/dist/commands/dag-rerun-task.js +2 -0
  10. package/dist/commands/task-source-prepare.js +3 -1
  11. package/dist/executors/dag-pi-executor.js +2584 -600
  12. package/dist/executors/pi-executor.js +22 -1
  13. package/dist/executors/pi-extension-resolver.js +14 -2
  14. package/dist/executors/pi-sdk-executor.js +140 -39
  15. package/dist/executors/shell-executor.js +135 -55
  16. package/dist/shared/dag-failure-category.js +6 -0
  17. package/dist/shared/frontend-execution-policy.js +23 -0
  18. package/dist/task/config-types.js +4 -0
  19. package/dist/task/contract/apply.js +36 -2
  20. package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
  21. package/dist/task/source-prepare/ledger-review.js +6 -9
  22. package/dist/task/source-prepare/parse-intent.js +7 -0
  23. package/dist/task/source-prepare/semantic-intake.js +16 -26
  24. package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
  25. package/dist/task/source-references.js +48 -23
  26. package/dist/worker/console/chat/assistant-content.js +23 -2
  27. package/dist/worker/console/chat/browser-policy.js +143 -0
  28. package/dist/worker/console/chat/browser-routes.js +148 -0
  29. package/dist/worker/console/chat/chat-event-store.js +4 -2
  30. package/dist/worker/console/chat/explore-tools.js +13 -0
  31. package/dist/worker/console/chat/pi-runtime.js +173 -94
  32. package/dist/worker/console/chat/resource-loader.js +4 -1
  33. package/dist/worker/console/chat/routes.js +208 -66
  34. package/dist/worker/console/chat/scm-routes.js +217 -0
  35. package/dist/worker/console/chat/scm-service.js +283 -0
  36. package/dist/worker/console/chat/scm-tools.js +111 -0
  37. package/dist/worker/console/chat/sdd-data-alignment.js +78 -11
  38. package/dist/worker/console/chat/session-catalog.js +32 -0
  39. package/dist/worker/console/chat/session-mode-view.js +48 -0
  40. package/dist/worker/console/chat/session-mode.js +218 -0
  41. package/dist/worker/console/chat/session-store.js +27 -7
  42. package/dist/worker/console/chat/shortcuts.js +6 -0
  43. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  44. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  45. package/dist/worker/console/chat/subagents/index.js +5 -0
  46. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  47. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  48. package/dist/worker/console/chat/subagents/types.js +13 -0
  49. package/dist/worker/console/chat/terminal-routes.js +216 -0
  50. package/dist/worker/console/chat/terminal-sessions.js +284 -0
  51. package/dist/worker/console/chat/terminal-tools.js +199 -0
  52. package/dist/worker/console/chat/tool-preview.js +21 -0
  53. package/dist/worker/console/chat/tools.js +13 -1
  54. package/dist/worker/console/chat/turn-process.js +1 -0
  55. package/dist/worker/console/interview/tools.js +1 -0
  56. package/dist/worker/console/server.js +2 -28
  57. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-C9eI7nEo.js} +1 -1
  58. package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-VJWsxWhB.js} +1 -1
  59. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-BGcyiMSo.js} +1 -1
  60. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-BScnFwyi.js} +1 -1
  61. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Bl9_BsLi.js} +1 -1
  62. package/dist/worker/console/static/assets/channel-B6sQYuOE.js +1 -0
  63. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-F1uKxiUt.js} +1 -1
  64. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-rSRMynVu.js} +1 -1
  65. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-DAsJm1WD.js} +1 -1
  66. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-DQIdVI0a.js} +1 -1
  67. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-4A_8rJ-Q.js} +1 -1
  68. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-BHnT-vjh.js} +1 -1
  69. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-Rq7QKBkn.js} +1 -1
  70. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BXh-AI2u.js} +1 -1
  71. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-BblcLy8m.js} +1 -1
  72. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-zHZmVnB6.js} +1 -1
  73. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Z6s5QoeI.js +1 -0
  74. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Z6s5QoeI.js +1 -0
  75. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-Cmwz0Wi4.js} +1 -1
  76. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-Dq76MHQU.js} +1 -1
  77. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-zPahV1fM.js} +1 -1
  78. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-DQJ3xPc_.js} +1 -1
  79. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BwX7u6fv.js} +1 -1
  80. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CodnDCco.js} +1 -1
  81. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-gAjtWkxG.js} +1 -1
  82. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-CGCqz0cS.js} +1 -1
  83. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-RHwYEywd.js} +1 -1
  84. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-B6seb_p_.js} +1 -1
  85. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-CmwCKYe8.js} +1 -1
  86. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-BfNsaZ0V.js} +1 -1
  87. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-zmYgi_Z3.js} +1 -1
  88. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-BDEzzSKF.js} +1 -1
  89. package/dist/worker/console/static/assets/index-DgenUAfc.js +468 -0
  90. package/dist/worker/console/static/assets/index-aF4u-Y__.css +1 -0
  91. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-DeSz8l42.js} +1 -1
  92. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-B1LV29aH.js} +1 -1
  93. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-CCw4W2oU.js} +1 -1
  94. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BozaMoRJ.js} +1 -1
  95. package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-G1JR40SX.js} +1 -1
  96. package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-CCgPF0oA.js} +5 -5
  97. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-CEqnCHnD.js} +1 -1
  98. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-DpmQPKuI.js} +1 -1
  99. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-BP501AxR.js} +1 -1
  100. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-D1dBhl_A.js} +1 -1
  101. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-DgZDS5pY.js} +1 -1
  102. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-BzB3pyIq.js} +1 -1
  103. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-B9ZprFFP.js} +1 -1
  104. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CC2-_XrA.js} +1 -1
  105. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-UVtAUqgB.js} +1 -1
  106. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-CWcS5JYG.js} +1 -1
  107. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BooX8u1Q.js +1 -0
  108. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-C4p-dn06.js} +2 -2
  109. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-D64_eqqE.js +8 -0
  110. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-Dnz52OHX.js} +1 -1
  111. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-CzTE0Nfu.js} +1 -1
  112. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-NAR9cjQQ.js} +1 -1
  113. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-DVMk8413.js} +1 -1
  114. package/dist/worker/console/static/index.html +2 -2
  115. package/dist/worker/console/static-src/chat-view-types.js +1 -1
  116. package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
  117. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +142 -7
  118. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +44 -0
  119. package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
  120. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  121. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
  122. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
  123. package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
  124. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +33 -18
  125. package/dist/worker/console/static-src/operator-chat/useChatStream.js +48 -16
  126. package/dist/worker/console/static-src/operator-chat/useChatThread.js +170 -7
  127. package/dist/worker/console/static-src/operator-chat/useComposer.js +13 -7
  128. package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
  129. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  130. package/dist/worker/console/workspace-context.js +15 -1
  131. package/dist/worker/observe/node-transparency.js +81 -72
  132. package/dist/worker/observe/routes.js +20 -1
  133. package/dist/worker/observe/static/constants.js +22 -22
  134. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  135. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  136. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  137. package/dist/worker/observe/static/dag-inspector-humanize.js +342 -0
  138. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  139. package/dist/worker/observe/static/dom.js +20 -1
  140. package/dist/worker/observe/static/format-pool.d.ts +2 -0
  141. package/dist/worker/observe/static/format-pool.js +6 -0
  142. package/dist/worker/observe/static/format.js +7 -0
  143. package/dist/worker/observe/static/index.html +4 -4
  144. package/dist/worker/observe/static/inspect-workspace.js +34 -7
  145. package/dist/worker/observe/static/inspector-submission.js +32 -0
  146. package/dist/worker/observe/static/kpi.js +1 -0
  147. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  148. package/dist/worker/observe/static/relations.js +7 -5
  149. package/dist/worker/observe/static/router.js +13 -0
  150. package/dist/worker/observe/static/run-processing.js +2 -0
  151. package/dist/worker/observe/static/shell-chrome.js +36 -3
  152. package/dist/worker/observe/static/state.js +35 -2
  153. package/dist/worker/observe/static/styles.css +431 -39
  154. package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
  155. package/dist/worker/observe/static/task-failure-labels.js +67 -0
  156. package/dist/worker/observe/static/task-history.js +12 -0
  157. package/dist/worker/observe/static/views/batch.js +6 -13
  158. package/dist/worker/observe/static/views/dag-graph.js +51 -4
  159. package/dist/worker/observe/static/views/dag-inspector.js +1069 -426
  160. package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
  161. package/dist/worker/observe/static/views/dag.d.ts +6 -0
  162. package/dist/worker/observe/static/views/dag.js +64 -22
  163. package/dist/worker/observe/static/views/dags.js +2 -0
  164. package/dist/worker/observe/static/views/dashboard.js +21 -12
  165. package/dist/worker/observe/static/views/failures.js +21 -11
  166. package/dist/worker/observe/static/views/feature.js +11 -29
  167. package/dist/worker/observe/static/views/pool.js +37 -28
  168. package/dist/worker/observe/static/views/run.js +48 -5
  169. package/dist/worker/observe/static/views/session-timeline.js +194 -243
  170. package/dist/worker/observe/static/views/task.js +81 -62
  171. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  172. package/dist/workflows/dag/budget-enforcement.js +53 -3
  173. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  174. package/dist/workflows/dag/frontend-capacity.js +9 -0
  175. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  176. package/dist/workflows/dag/frontend-design-policy.js +103 -17
  177. package/dist/workflows/dag/frontend-durable-tools.js +193 -0
  178. package/dist/workflows/dag/frontend-execution-groups.js +24 -0
  179. package/dist/workflows/dag/frontend-implementation-contract.js +423 -41
  180. package/dist/workflows/dag/frontend-input-projection.js +76 -0
  181. package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
  182. package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
  183. package/dist/workflows/dag/frontend-plan-render.js +21 -3
  184. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  185. package/dist/workflows/dag/frontend-recovery-controller.js +32 -8
  186. package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
  187. package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
  188. package/dist/workflows/dag/frontend-recovery-run.js +146 -16
  189. package/dist/workflows/dag/frontend-review-scopes.js +139 -0
  190. package/dist/workflows/dag/frontend-risk.js +2 -0
  191. package/dist/workflows/dag/frontend-session-budget.js +249 -0
  192. package/dist/workflows/dag/frontend-shadow-dual-write.js +37 -3
  193. package/dist/workflows/dag/frontend-shape-facts.js +12 -2
  194. package/dist/workflows/dag/frontend-shape.js +46 -7
  195. package/dist/workflows/dag/frontend-test-execution-evidence.js +49 -0
  196. package/dist/workflows/dag/frontend-typed-event-store.js +14 -0
  197. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  198. package/dist/workflows/dag/frontend-writer-admission.js +2 -2
  199. package/dist/workflows/dag/init-hybrid.js +70 -35
  200. package/dist/workflows/dag/node-execution.js +225 -226
  201. package/dist/workflows/dag/prompt.js +4 -0
  202. package/dist/workflows/dag/recovery-lease.js +106 -16
  203. package/dist/workflows/dag/rerun-feedback.js +1 -0
  204. package/dist/workflows/dag/rerun-plan.js +96 -4
  205. package/dist/workflows/dag/rerun-run.js +28 -6
  206. package/dist/workflows/dag/rerun-task.js +221 -18
  207. package/dist/workflows/dag/retry-policy.js +27 -10
  208. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  209. package/dist/workflows/dag/runner.js +246 -19
  210. package/dist/workflows/dag/structured-output-repair.js +4 -1
  211. package/dist/workflows/dag/types.js +10 -3
  212. package/dist/workflows/dag/validate.js +10 -8
  213. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  214. package/docs/operations/README.md +2 -0
  215. package/docs/templates/agent-dag.schema.json +2 -2
  216. package/docs/templates/backend-test-dag.json +6 -4
  217. package/docs/templates/frontend-design-contract.md +4 -4
  218. package/docs/templates/frontend-implementation-contract.schema.json +68 -4
  219. package/docs/templates/frontend-implementation-dag.json +5 -5
  220. package/harness.json +1 -1
  221. package/package.json +8 -6
  222. package/skills/frontend-contract/SKILL.md +2 -1
  223. package/skills/frontend-contract/references/contract-protocol.md +19 -3
  224. package/skills/frontend-design-review/SKILL.md +12 -11
  225. package/skills/frontend-plan/SKILL.md +22 -2
  226. package/skills/frontend-plan/references/decision-contract.md +114 -5
  227. package/skills/frontend-plan/references/design-decisions.md +32 -0
  228. package/skills/frontend-review/SKILL.md +10 -11
  229. package/skills/frontend-scout/references/scout-evidence.md +4 -0
  230. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  231. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  232. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  233. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  234. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  235. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  236. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
@@ -1,3 +1,4 @@
1
+ import { collectFrontendUsage, createFrontendReadObservation, observeFrontendAttempt } from "../workflows/dag/frontend-session-budget.js";
1
2
  import { spawn, spawnSync } from "node:child_process";
2
3
  import { existsSync } from "node:fs";
3
4
  import { homedir } from "node:os";
@@ -453,6 +454,9 @@ export function extractPiEventStopReason(event) {
453
454
  return undefined;
454
455
  }
455
456
  export class PiJsonlStreamCollector {
457
+ frontendReads = createFrontendReadObservation();
458
+ frontendUsageEvents = [];
459
+ frontendObservation() { return { readObservation: this.frontendReads.snapshot(), usageObservation: collectFrontendUsage(this.frontendUsageEvents) }; }
456
460
  lineBuffer = "";
457
461
  discardUntilNewline = false;
458
462
  assistantText = "";
@@ -518,6 +522,13 @@ export class PiJsonlStreamCollector {
518
522
  }
519
523
  }
520
524
  consumeEvent(event) {
525
+ try {
526
+ this.frontendReads.observe(event);
527
+ const message = isRecord(event.message) ? event.message : undefined;
528
+ if (event.usage || message?.usage || event.tokenUsage)
529
+ this.frontendUsageEvents.push({ responseId: event.responseId, messageId: event.messageId, usage: event.usage, tokenUsage: event.tokenUsage, message: message ? { role: message.role, id: message.id, responseId: message.responseId, usage: message.usage } : undefined });
530
+ }
531
+ catch { /* observation only */ }
521
532
  const stopReason = extractPiEventStopReason(event);
522
533
  if (stopReason === "length" || !this.stopReason) {
523
534
  this.stopReason = stopReason ?? this.stopReason;
@@ -696,6 +707,8 @@ function shouldFallbackSdkToCli(sdkResult, validationIssues, options) {
696
707
  return true;
697
708
  if (!sdkResult.ok &&
698
709
  (sdkResult.failureCategory === "auth" ||
710
+ sdkResult.failureCategory === "output-limit" ||
711
+ sdkResult.failureCategory === "cancelled" ||
699
712
  sdkResult.failureCategory === "timeout" ||
700
713
  sdkResult.failureCategory === "termination-unconfirmed")) {
701
714
  return false;
@@ -730,6 +743,8 @@ function writerPolicyFailureResult(options, message, extras) {
730
743
  export async function executePiStep(options) {
731
744
  const backendMode = resolvePiBackend();
732
745
  const writerRequiresSdk = options.writerToolPolicy?.requireSdk === true;
746
+ if (options.reserveProviderRequest && backendMode === "cli-only")
747
+ return { ...writerPolicyFailureResult(options, "FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: actual request admission requires SDK", { sdkAttempted: false }), failureCategory: "budget_breach" };
733
748
  if (backendMode === "cli-only") {
734
749
  if (writerRequiresSdk) {
735
750
  return writerPolicyFailureResult(options, "pi writer tool policy requires SDK backend; CODE_AGENT_PI_BACKEND=cli-only is fail-closed for writers", { sdkAttempted: false, backend: "cli" });
@@ -745,6 +760,8 @@ export async function executePiStep(options) {
745
760
  const { checkPiSdkAvailability, executeSingleSdkAttempt, getActivePiSdkReuseScope, } = await import("./pi-sdk-executor.js");
746
761
  const sdkCheck = await checkPiSdkAvailability(options.repoRoot);
747
762
  if (!sdkCheck.ok) {
763
+ if (options.reserveProviderRequest)
764
+ return { ...writerPolicyFailureResult(options, `FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: ${sdkCheck.detail}`, { sdkAttempted: false }), failureCategory: "budget_breach" };
748
765
  if (writerRequiresSdk) {
749
766
  return writerPolicyFailureResult(options, `pi writer tool policy requires SDK; ${sdkCheck.detail}`, { sdkAttempted: false });
750
767
  }
@@ -773,7 +790,7 @@ export async function executePiStep(options) {
773
790
  reuseRuntimeActive,
774
791
  });
775
792
  }
776
- if (!shouldFallbackSdkToCli(sdkResult, validationIssues, options)) {
793
+ if (options.reserveProviderRequest || !shouldFallbackSdkToCli(sdkResult, validationIssues, options)) {
777
794
  const failureCategory = validationIssues.length > 0
778
795
  ? "invalid-output"
779
796
  : sdkResult.failureCategory;
@@ -798,6 +815,9 @@ export async function executePiStep(options) {
798
815
  return { ...cliResult, reuseRuntimeActive };
799
816
  }
800
817
  async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
818
+ return observeFrontendAttempt({ backend: "cli", model: [modelConfig.provider, modelConfig.model].filter(Boolean).join("/"), observer: options.onAttemptObservation }, () => executeSingleCliAttemptInternal(options, modelConfig, promptFilePath));
819
+ }
820
+ async function executeSingleCliAttemptInternal(options, modelConfig, promptFilePath) {
801
821
  const modelDisplay = modelConfig.provider && modelConfig.model
802
822
  ? `${modelConfig.provider}/${modelConfig.model}`
803
823
  : (modelConfig.model ?? "default");
@@ -900,6 +920,7 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
900
920
  });
901
921
  resolve({
902
922
  assistantText: collected.assistantText,
923
+ ...stdoutCollector.frontendObservation(),
903
924
  command: ["pi", ...recordedPiArgs],
904
925
  durationMs,
905
926
  exitCode: code,
@@ -62,9 +62,21 @@ function readSettingsPackages(settingsPath) {
62
62
  return undefined;
63
63
  }
64
64
  }
65
- /** Strip an `npm:` prefix from a settings package spec, e.g. `npm:pi-lens`. */
65
+ /**
66
+ * Strip an `npm:` prefix and a trailing `@<version>`/`@<dist-tag>` suffix
67
+ * from a settings package spec, e.g. `npm:pi-lens@3.8.72` → `pi-lens`,
68
+ * `npm:@scope/pkg@1.0.0` → `@scope/pkg`. The leading `@` of a scoped
69
+ * package name is preserved (the version separator is the LAST `@` at a
70
+ * positive index). Mirrors the console-side spec parsing semantics so a
71
+ * version-pinned install (e.g. via `pi install npm:pi-lens@3.8.72`) resolves
72
+ * identically in both surfaces.
73
+ */
66
74
  export function packageNameFromSpec(spec) {
67
- return spec.startsWith("npm:") ? spec.slice(4) : spec;
75
+ const base = spec.startsWith("npm:") ? spec.slice(4) : spec;
76
+ const at = base.lastIndexOf("@");
77
+ if (at > 0)
78
+ return base.slice(0, at);
79
+ return base;
68
80
  }
69
81
  function loadPackageInventories(repoRoot) {
70
82
  const agentDir = getPiAgentDir();
@@ -1,3 +1,5 @@
1
+ import { collectFrontendUsage, createFrontendReadObservation, observeFrontendAttempt } from "../workflows/dag/frontend-session-budget.js";
2
+ import { frontendModelCapabilities, measureFrontendRequestEnvelope } from "../shared/frontend-execution-policy.js";
1
3
  import { appendFile, mkdir } from "node:fs/promises";
2
4
  import os from "node:os";
3
5
  import path from "node:path";
@@ -7,6 +9,42 @@ import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DE
7
9
  import { serializeSessionEvent } from "./pi-event-serializer.js";
8
10
  import { PI_RECOMMENDED_COMPACTION, PI_RECOMMENDED_RETRY, } from "../shared/pi-retry-settings.js";
9
11
  import { createCompactionController, createContextPressureExtension, createSiftBridgeState, inMemoryCompactionSettings, isDagSiftEnabled, parseCompactionPolicy, parseSiftBridgeConfig, SIFT_RETRIEVE_TOOL_NAME, } from "../shared/pi-context-pressure/index.js";
12
+ /** Maximum number of same-session continuations after provider output truncation. */
13
+ export const PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX = 3;
14
+ export const PI_SDK_OUTPUT_LIMIT_CONTINUE_PROMPT = "继续。上一轮因输出长度上限(stopReason=length)截断;当前会话、工具与已完成工作仍保留。只完成尚未完成的部分,不重做已成功的写入或事实提交。";
15
+ /** Wrap the actual stream seam: tool turns, retries and compaction all await it. */
16
+ export async function installPiProviderRequestBudget(session, reserve, onFailure, capacity) {
17
+ const agent = session.agent;
18
+ const original = agent?.streamFunction;
19
+ if (!agent || typeof original !== "function")
20
+ throw Error("FRONTEND_PROVIDER_BUDGET_UNSUPPORTED: SDK actual request stream unavailable");
21
+ const { createAssistantMessageEventStream } = await import("@earendil-works/pi-ai");
22
+ agent.streamFunction = async (...args) => {
23
+ try {
24
+ if (args[2]?.signal?.aborted)
25
+ throw Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
26
+ if (capacity) {
27
+ const measurement = measureFrontendRequestEnvelope(args[1], args[0], capacity.policy);
28
+ capacity.observe(measurement);
29
+ if (measurement.exceedsEstimate)
30
+ throw Error(`FRONTEND_CONTEXT_ESTIMATE_EXCEEDED: estimated=${measurement.estimatedTokens} available=${measurement.availableInputTokens}; estimator=${measurement.estimatorVersion}; registry capability is not a measured gateway limit`);
31
+ }
32
+ await reserve();
33
+ if (args[2]?.signal?.aborted)
34
+ throw Error("FRONTEND_PROVIDER_REQUEST_ABORTED");
35
+ }
36
+ catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ onFailure(message);
39
+ const stream = createAssistantMessageEventStream();
40
+ const model = args[0] ?? {};
41
+ stream.push({ type: "error", reason: "error", error: { role: "assistant", content: [], api: model.api ?? "unknown", provider: model.provider ?? "unknown", model: model.id ?? "unknown", usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } }, stopReason: "error", errorMessage: message, timestamp: Date.now() } });
42
+ stream.end();
43
+ return stream;
44
+ }
45
+ return original.apply(agent, args);
46
+ };
47
+ }
10
48
  /** Same-session recovery after a transport stall. Exactly one continue per SDK attempt. */
11
49
  export const PI_SDK_STALL_CONTINUE_MAX = 1;
12
50
  /**
@@ -544,6 +582,9 @@ function aggregateSdkTokenUsage(samples) {
544
582
  * same session; that is in-attempt recovery, not cross-step session reuse.
545
583
  */
546
584
  export async function executeSingleSdkAttempt(options) {
585
+ return observeFrontendAttempt({ backend: "sdk", model: [options.modelConfig.provider, options.modelConfig.model].filter(Boolean).join("/"), observer: options.onAttemptObservation }, () => executeSingleSdkAttemptInternal(options));
586
+ }
587
+ async function executeSingleSdkAttemptInternal(options) {
547
588
  const modelConfig = options.modelConfig;
548
589
  const modelDisplay = modelConfig.provider && modelConfig.model
549
590
  ? `${modelConfig.provider}/${modelConfig.model}`
@@ -568,9 +609,16 @@ export async function executeSingleSdkAttempt(options) {
568
609
  let timedOut = false;
569
610
  let terminationConfirmed = true;
570
611
  let stderr = "";
612
+ let providerBudgetFailure;
613
+ let capabilitiesObservation = frontendModelCapabilities(undefined);
614
+ const requestEnvelopeObservation = { peakBytes: 0, peakEstimatedTokens: 0, estimatorVersion: "utf8-bytes-div3-v1-estimate" };
571
615
  const stdoutPreview = new BoundedTextPreview("stdout");
572
616
  const stdoutCollector = createPiJsonlStreamCollector();
617
+ let turnCollector = createPiJsonlStreamCollector();
618
+ let committedTerminalObserved = false;
573
619
  const usageSamples = [];
620
+ const observationEvents = [];
621
+ const readObservation = createFrontendReadObservation();
574
622
  let observedTurns = 0;
575
623
  let contextBudgetExhausted = false;
576
624
  let contextBudgetDetail = "";
@@ -579,6 +627,9 @@ export async function executeSingleSdkAttempt(options) {
579
627
  // length-stopped thinking-only attempt with no persisted write tool calls is
580
628
  // still distinguishable from generic empty output.
581
629
  let stopReason;
630
+ let providerErrorMessage;
631
+ let promptIndex = 0;
632
+ let outputContinuesSent = 0;
582
633
  let thinkingObserved = false;
583
634
  let writeToolCallCount = 0;
584
635
  const observeWriterThinkingExhaustionEvidence = (event) => {
@@ -660,6 +711,13 @@ export async function executeSingleSdkAttempt(options) {
660
711
  ? { additionalExtensionPaths: options.piExtensionPaths }
661
712
  : {}),
662
713
  });
714
+ capabilitiesObservation = frontendModelCapabilities(session.agent?.state?.model);
715
+ if (options.reserveProviderRequest || options.frontendExecutionPolicy)
716
+ await installPiProviderRequestBudget(session, options.reserveProviderRequest ?? (async () => { }), message => { providerBudgetFailure = message; appendStderr(message); }, options.frontendExecutionPolicy ? { policy: options.frontendExecutionPolicy, observe: measurement => {
717
+ capabilitiesObservation = measurement.capabilities;
718
+ requestEnvelopeObservation.peakBytes = Math.max(requestEnvelopeObservation.peakBytes, measurement.bytes);
719
+ requestEnvelopeObservation.peakEstimatedTokens = Math.max(requestEnvelopeObservation.peakEstimatedTokens, measurement.estimatedTokens);
720
+ } } : undefined);
663
721
  let resolveStall;
664
722
  let resolveSettlement;
665
723
  let resolveContextBudget;
@@ -718,6 +776,8 @@ export async function executeSingleSdkAttempt(options) {
718
776
  resolveContextBudget?.("context-budget");
719
777
  };
720
778
  unsubscribe = session.subscribe((event) => {
779
+ if (event.type === "tool_execution_end" && event.isError !== true && typeof event.toolName === "string" && options.outputLimitRecovery?.terminalToolNames.includes(event.toolName) && isRecord(event.result) && isRecord(event.result.details) && event.result.details.ok === true)
780
+ committedTerminalObserved = true;
721
781
  if (event.type === "agent_settled" && !settled) {
722
782
  settled = true;
723
783
  resolveSettlement?.("settled");
@@ -737,9 +797,18 @@ export async function executeSingleSdkAttempt(options) {
737
797
  // best-effort: activity must never change Pi result
738
798
  }
739
799
  }
800
+ const observationMessage = isRecord(event.message) ? event.message : undefined;
801
+ if (typeof observationMessage?.errorMessage === "string")
802
+ providerErrorMessage = observationMessage.errorMessage;
803
+ try {
804
+ readObservation.observe(event);
805
+ }
806
+ catch { /* observation must not change execution */ }
807
+ if (!(typeof observationMessage?.errorMessage === "string" && observationMessage.errorMessage.startsWith("FRONTEND_PROVIDER_")) && (event.usage || observationMessage?.usage || event.tokenUsage))
808
+ observationEvents.push({ responseId: event.responseId, messageId: event.messageId, usage: event.usage, tokenUsage: event.tokenUsage, message: observationMessage ? { role: observationMessage.role, id: observationMessage.id, responseId: observationMessage.responseId, usage: observationMessage.usage } : undefined });
740
809
  const usageSample = extractSdkUsageSample(event);
741
810
  if (usageSample)
742
- usageSamples.push(usageSample);
811
+ usageSamples.push({ ...usageSample, responseKey: usageSample.responseKey ?? `turn-${promptIndex}` });
743
812
  if (event.type === "turn_end")
744
813
  observedTurns += 1;
745
814
  checkContextBudget();
@@ -753,6 +822,7 @@ export async function executeSingleSdkAttempt(options) {
753
822
  const line = serializeSessionEvent(event);
754
823
  stdoutPreview.append(`${line}\n`);
755
824
  stdoutCollector.append(`${line}\n`);
825
+ turnCollector.append(`${line}\n`);
756
826
  sessionEventAppender?.append(line, event);
757
827
  });
758
828
  const timeoutPromise = timeoutMs > 0
@@ -763,6 +833,9 @@ export async function executeSingleSdkAttempt(options) {
763
833
  })
764
834
  : null;
765
835
  const runTurn = async (userMessage) => {
836
+ promptIndex += 1;
837
+ stopReason = undefined;
838
+ turnCollector = createPiJsonlStreamCollector();
766
839
  const settlementPromise = resetSettlement();
767
840
  const stallPromise = resetStallPromise();
768
841
  const contextBudgetPromise = options.contextBudget
@@ -808,48 +881,61 @@ export async function executeSingleSdkAttempt(options) {
808
881
  : options.userMessage;
809
882
  let continuesSent = 0;
810
883
  let raced = await runTurn(promptMessage);
811
- if (raced === "context-budget") {
812
- appendStderr(contextBudgetDetail);
813
- abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
814
- await disposeSession();
815
- }
816
- else if (raced === "stall") {
817
- abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
818
- const withinAbsoluteTimeout = timeoutMs <= 0 || Date.now() - startedAt < timeoutMs;
819
- const canContinue = abortConfirmed &&
820
- writeToolCallCount === 0 &&
821
- withinAbsoluteTimeout &&
822
- continuesSent < PI_SDK_STALL_CONTINUE_MAX;
823
- if (canContinue) {
824
- // The aborted turn's stopReason must not leak onto a recovered continue.
825
- stopReason = undefined;
826
- continuesSent += 1;
827
- persistRuntimeNote({
828
- type: "loop-agent-stall-continue",
829
- prompt: PI_SDK_STALL_CONTINUE_PROMPT,
830
- maxContinues: PI_SDK_STALL_CONTINUE_MAX,
831
- });
832
- raced = await runTurn(PI_SDK_STALL_CONTINUE_PROMPT);
833
- if (raced !== "done" && raced !== "settled") {
834
- await failClosedAfterWait(raced === "stall" ? "stall" : "absolute-timeout", raced === "stall"
835
- ? "pi SDK stall continue did not recover; treating as timeout"
836
- : "pi SDK stall continue hit the absolute timeout", true);
884
+ while (true) {
885
+ if (raced === "context-budget") {
886
+ appendStderr(contextBudgetDetail);
887
+ abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
888
+ await disposeSession();
889
+ }
890
+ else if (raced === "stall") {
891
+ abortConfirmed = await runSessionActionWithGrace("abort", () => session.abort());
892
+ const withinAbsoluteTimeout = timeoutMs <= 0 || Date.now() - startedAt < timeoutMs;
893
+ const canContinue = abortConfirmed &&
894
+ writeToolCallCount === 0 &&
895
+ withinAbsoluteTimeout &&
896
+ continuesSent < PI_SDK_STALL_CONTINUE_MAX;
897
+ if (canContinue) {
898
+ // The aborted turn's stopReason must not leak onto a recovered continue.
899
+ stopReason = undefined;
900
+ continuesSent += 1;
901
+ persistRuntimeNote({
902
+ type: "loop-agent-stall-continue",
903
+ prompt: PI_SDK_STALL_CONTINUE_PROMPT,
904
+ maxContinues: PI_SDK_STALL_CONTINUE_MAX,
905
+ });
906
+ raced = await runTurn(PI_SDK_STALL_CONTINUE_PROMPT);
907
+ if (raced !== "done" && raced !== "settled") {
908
+ await failClosedAfterWait(raced === "stall" ? "stall" : "absolute-timeout", raced === "stall"
909
+ ? "pi SDK stall continue did not recover; treating as timeout"
910
+ : "pi SDK stall continue hit the absolute timeout", true);
911
+ }
912
+ }
913
+ else {
914
+ await failClosedAfterWait("stall", !abortConfirmed
915
+ ? undefined
916
+ : writeToolCallCount > 0
917
+ ? "pi SDK stall continue skipped because write tools already ran"
918
+ : "pi SDK stall continue skipped because absolute timeout elapsed", false);
837
919
  }
838
920
  }
839
- else {
840
- await failClosedAfterWait("stall", !abortConfirmed
841
- ? undefined
842
- : writeToolCallCount > 0
843
- ? "pi SDK stall continue skipped because write tools already ran"
844
- : "pi SDK stall continue skipped because absolute timeout elapsed", false);
921
+ else if (raced === "absolute-timeout") {
922
+ await failClosedAfterWait("absolute-timeout");
845
923
  }
846
- }
847
- else if (raced === "absolute-timeout") {
848
- await failClosedAfterWait("absolute-timeout");
924
+ if (options.outputLimitRecovery && stopReason === "length" && !committedTerminalObserved && !stderr && outputContinuesSent < PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX && !timedOut) {
925
+ outputContinuesSent += 1;
926
+ const note = serializeSessionEvent({ type: "loop-agent-output-limit-continue", continue: outputContinuesSent, maxContinues: PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX, prompt: PI_SDK_OUTPUT_LIMIT_CONTINUE_PROMPT });
927
+ stdoutPreview.append(`${note}\n`);
928
+ stdoutCollector.append(`${note}\n`);
929
+ raced = await runTurn(PI_SDK_OUTPUT_LIMIT_CONTINUE_PROMPT);
930
+ continue;
931
+ }
932
+ break;
849
933
  }
850
934
  }
851
935
  catch (error) {
852
936
  const message = error instanceof Error ? error.message : String(error);
937
+ if (options.reserveProviderRequest && message.startsWith("FRONTEND_PROVIDER_"))
938
+ providerBudgetFailure = message;
853
939
  appendStderr(message);
854
940
  }
855
941
  finally {
@@ -872,20 +958,29 @@ export async function executeSingleSdkAttempt(options) {
872
958
  }
873
959
  }
874
960
  }
961
+ if (providerErrorMessage && stopReason === "error" && !stderr)
962
+ appendStderr(providerErrorMessage);
963
+ if (options.outputLimitRecovery && stopReason === "length" && !committedTerminalObserved && outputContinuesSent >= PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX) {
964
+ const note = serializeSessionEvent({ type: "loop-agent-output-limit-exhausted", maxContinues: PI_SDK_OUTPUT_LIMIT_CONTINUE_MAX });
965
+ stdoutPreview.append(`${note}\n`);
966
+ }
875
967
  const stdout = stdoutPreview.text();
876
968
  const durationMs = Date.now() - startedAt;
877
969
  const collected = stdoutCollector.finish();
878
970
  const fallbackParsed = collected.parsedEvents > 0
879
971
  ? undefined
880
972
  : extractAssistantTextFromPiJson(stdout);
973
+ const finalTurn = turnCollector.finish();
881
974
  const assistantText = collected.parsedEvents > 0
882
- ? collected.assistantText
975
+ ? finalTurn.assistantText
883
976
  : (fallbackParsed?.assistantText ?? "");
884
977
  const parsedEvents = collected.parsedEvents > 0
885
978
  ? collected.parsedEvents
886
979
  : (fallbackParsed?.parsedEvents ?? 0);
887
980
  const tokensUsed = aggregateSdkTokenUsage(usageSamples);
888
- const failureCategory = terminationConfirmed
981
+ const cancelled = stopReason === "aborted";
982
+ const outputLimitExhausted = Boolean(options.outputLimitRecovery && stopReason === "length" && !committedTerminalObserved && !timedOut && !stderr);
983
+ const failureCategory = !terminationConfirmed ? "termination-unconfirmed" : stopReason === "aborted" ? "cancelled" : outputLimitExhausted ? "output-limit" : providerBudgetFailure ? (providerBudgetFailure.includes("REQUEST_ABORTED") ? "interrupted" : providerBudgetFailure.startsWith("FRONTEND_CONTEXT_ESTIMATE_EXCEEDED") ? "context-budget-exhausted" : "budget_breach") : terminationConfirmed
889
984
  ? classifyPiFailure({
890
985
  assistantText,
891
986
  exitCode: timedOut ? 1 : stderr ? 1 : 0,
@@ -900,12 +995,14 @@ export async function executeSingleSdkAttempt(options) {
900
995
  backend: "sdk",
901
996
  command: ["pi-sdk", ...piSdkArgs],
902
997
  durationMs,
903
- exitCode: timedOut || stderr ? 1 : 0,
998
+ exitCode: timedOut || stderr || outputLimitExhausted || cancelled ? 1 : 0,
904
999
  failureCategory,
905
1000
  modelDisplay,
906
1001
  ok: terminationConfirmed &&
907
1002
  !timedOut &&
908
1003
  !stderr &&
1004
+ !outputLimitExhausted &&
1005
+ !cancelled &&
909
1006
  assistantText.length > 0 &&
910
1007
  !collected.outputTooLarge,
911
1008
  parsedEvents,
@@ -915,6 +1012,10 @@ export async function executeSingleSdkAttempt(options) {
915
1012
  outputTooLarge: collected.outputTooLarge,
916
1013
  timedOut,
917
1014
  tokensUsed,
1015
+ usageObservation: collectFrontendUsage(observationEvents),
1016
+ readObservation: readObservation.snapshot(),
1017
+ capabilitiesObservation,
1018
+ ...(options.frontendExecutionPolicy ? { requestEnvelopeObservation } : {}),
918
1019
  subagentStats: collected.subagentStats,
919
1020
  // Writer-thinking-exhausted diagnosis evidence (AC-002). Carried through
920
1021
  // the object spread in executePiStep so executeDagPiNode can read them