@zhushanwen/subagent-core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (330) hide show
  1. package/README.md +114 -0
  2. package/dist/chunk-3VOERJPJ.js +22 -0
  3. package/dist/chunk-A4IVZWVX.js +31 -0
  4. package/dist/execution/engine/engines/zcode/constants.cjs +54 -0
  5. package/dist/execution/engine/engines/zcode/constants.d.cts +31 -0
  6. package/dist/execution/engine/engines/zcode/constants.d.ts +31 -0
  7. package/dist/execution/engine/engines/zcode/constants.js +22 -0
  8. package/dist/execution/engine/engines/zcode/reader.cjs +276 -0
  9. package/dist/execution/engine/engines/zcode/reader.d.cts +20 -0
  10. package/dist/execution/engine/engines/zcode/reader.d.ts +20 -0
  11. package/dist/execution/engine/engines/zcode/reader.js +239 -0
  12. package/dist/execution/engine/paths.cjs +55 -0
  13. package/dist/execution/engine/paths.d.cts +8 -0
  14. package/dist/execution/engine/paths.d.ts +8 -0
  15. package/dist/execution/engine/paths.js +26 -0
  16. package/dist/execution/relay-env.cjs +62 -0
  17. package/dist/execution/relay-env.d.cts +33 -0
  18. package/dist/execution/relay-env.d.ts +33 -0
  19. package/dist/execution/relay-env.js +20 -0
  20. package/dist/index.cjs +1852 -0
  21. package/dist/index.d.cts +1184 -0
  22. package/dist/index.d.ts +1184 -0
  23. package/dist/index.js +1810 -0
  24. package/dist/types-BxyAidGf.d.cts +875 -0
  25. package/dist/types-BxyAidGf.d.ts +875 -0
  26. package/package.json +99 -0
  27. package/src/__tests__/agent-opts-resolver-schema-prompt.test.ts +142 -0
  28. package/src/__tests__/append-system-prompt-assembly.test.ts +242 -0
  29. package/src/__tests__/builtin-workflows-structure.test.ts +180 -0
  30. package/src/__tests__/derive-closed-display-parity.test.ts +93 -0
  31. package/src/__tests__/fr4-get-state-handshake.test.ts +125 -0
  32. package/src/__tests__/fr8-orphan-recovery.test.ts +259 -0
  33. package/src/__tests__/m2-append-content-probe.test.ts +73 -0
  34. package/src/__tests__/manifest-store.test.ts +368 -0
  35. package/src/__tests__/record-store-cache.test.ts +335 -0
  36. package/src/__tests__/record-store-index.test.ts +560 -0
  37. package/src/__tests__/record-store-last-line.test.ts +129 -0
  38. package/src/__tests__/review-fix-loop-script.test.ts +288 -0
  39. package/src/__tests__/review-fix-loop-utils.test.ts +2377 -0
  40. package/src/__tests__/robustness-low-batch1.test.ts +286 -0
  41. package/src/__tests__/robustness-medium-batch1.test.ts +69 -0
  42. package/src/__tests__/robustness-medium-batch2.test.ts +65 -0
  43. package/src/__tests__/robustness-medium-batch3.test.ts +30 -0
  44. package/src/__tests__/robustness-medium-batch4.test.ts +358 -0
  45. package/src/__tests__/session-runner.test.ts +69 -0
  46. package/src/__tests__/smoke.test.ts +9 -0
  47. package/src/__tests__/stream-sink.test.ts +203 -0
  48. package/src/core/__tests__/host-services.test.ts +144 -0
  49. package/src/core/__tests__/logger.test.ts +78 -0
  50. package/src/core/__tests__/notify-ports.test.ts +104 -0
  51. package/src/core/host-services.ts +97 -0
  52. package/src/core/logger.ts +47 -0
  53. package/src/core/notify-ports.ts +126 -0
  54. package/src/execution/__tests__/__fixtures__/notifier-golden-snapshots.json +53 -0
  55. package/src/execution/__tests__/__fixtures__/truncline.snapshot.json +1 -0
  56. package/src/execution/__tests__/agent-result-mapper.test.ts +150 -0
  57. package/src/execution/__tests__/alive-store.test.ts +147 -0
  58. package/src/execution/__tests__/ask-user-transit-e2e.test.ts +490 -0
  59. package/src/execution/__tests__/channel-registry-handshake.test.ts +242 -0
  60. package/src/execution/__tests__/chat-engine-routing.test.ts +610 -0
  61. package/src/execution/__tests__/chatmode-first-round-closure-spawn.test.ts +190 -0
  62. package/src/execution/__tests__/concurrency-pool.test.ts +250 -0
  63. package/src/execution/__tests__/config.test.ts +302 -0
  64. package/src/execution/__tests__/delivery-methods.test.ts +424 -0
  65. package/src/execution/__tests__/dialog-queue.test.ts +299 -0
  66. package/src/execution/__tests__/epipe-fallback.test.ts +241 -0
  67. package/src/execution/__tests__/execute-and-await-worktree.test.ts +243 -0
  68. package/src/execution/__tests__/execute-nesting.test.ts +323 -0
  69. package/src/execution/__tests__/execute-options-mapper.test.ts +200 -0
  70. package/src/execution/__tests__/execution-record.test.ts +1394 -0
  71. package/src/execution/__tests__/explicit-agent-ref-guard.test.ts +171 -0
  72. package/src/execution/__tests__/finalize-record.test.ts +367 -0
  73. package/src/execution/__tests__/finalized-marker.test.ts +104 -0
  74. package/src/execution/__tests__/format-schema-instruction.test.ts +169 -0
  75. package/src/execution/__tests__/gc-timer.test.ts +184 -0
  76. package/src/execution/__tests__/get-record-for-action-restart.test.ts +254 -0
  77. package/src/execution/__tests__/helpers/mock-extension-api.ts +30 -0
  78. package/src/execution/__tests__/helpers/spawn-mock.ts +242 -0
  79. package/src/execution/__tests__/host-mode.test.ts +87 -0
  80. package/src/execution/__tests__/lifecycle-manager-lock.test.ts +211 -0
  81. package/src/execution/__tests__/lifecycle-manager.test.ts +383 -0
  82. package/src/execution/__tests__/lifecycle-predicates.test.ts +116 -0
  83. package/src/execution/__tests__/manifest-parentid.test.ts +117 -0
  84. package/src/execution/__tests__/model-resolver.test.ts +465 -0
  85. package/src/execution/__tests__/nested-visibility-env-propagation.test.ts +287 -0
  86. package/src/execution/__tests__/nested-visibility.test.ts +325 -0
  87. package/src/execution/__tests__/output-collector.test.ts +358 -0
  88. package/src/execution/__tests__/path-encoding.test.ts +104 -0
  89. package/src/execution/__tests__/pi-invocation.test.ts +134 -0
  90. package/src/execution/__tests__/record-store.test.ts +1057 -0
  91. package/src/execution/__tests__/records-cwd-isolation.test.ts +91 -0
  92. package/src/execution/__tests__/recursive-visibility-baseline.test.ts +339 -0
  93. package/src/execution/__tests__/recursive-visibility-env.test.ts +273 -0
  94. package/src/execution/__tests__/relay-env.test.ts +42 -0
  95. package/src/execution/__tests__/resource-policy.test.ts +109 -0
  96. package/src/execution/__tests__/rpc-mode.test.ts +89 -0
  97. package/src/execution/__tests__/run-and-finalize-chatmode.test.ts +267 -0
  98. package/src/execution/__tests__/run-spawn-chatmode-settled.test.ts +253 -0
  99. package/src/execution/__tests__/run-spawn-edges.test.ts +687 -0
  100. package/src/execution/__tests__/run-spawn-integration.test.ts +933 -0
  101. package/src/execution/__tests__/run-spawn-resume.test.ts +322 -0
  102. package/src/execution/__tests__/run-spawn-rpc-mode.test.ts +196 -0
  103. package/src/execution/__tests__/run-spawn-stdout-callback-throw.test.ts +199 -0
  104. package/src/execution/__tests__/session-context-resolver.test.ts +167 -0
  105. package/src/execution/__tests__/session-file-gc.test.ts +293 -0
  106. package/src/execution/__tests__/session-reconstructor.test.ts +379 -0
  107. package/src/execution/__tests__/session-runner-epipe.test.ts +178 -0
  108. package/src/execution/__tests__/session-runner-schema-env.test.ts +350 -0
  109. package/src/execution/__tests__/spawn-args.test.ts +445 -0
  110. package/src/execution/__tests__/spawn-event-adapter-rpc.test.ts +189 -0
  111. package/src/execution/__tests__/spawn-event-adapter.test.ts +167 -0
  112. package/src/execution/__tests__/spawn-worktree-guidance.test.ts +207 -0
  113. package/src/execution/__tests__/spawned-children.test.ts +92 -0
  114. package/src/execution/__tests__/start-sync-model-guard.test.ts +150 -0
  115. package/src/execution/__tests__/stdin-writer.test.ts +462 -0
  116. package/src/execution/__tests__/stream-sink-retirement.test.ts +261 -0
  117. package/src/execution/__tests__/subagent-service-abort.test.ts +60 -0
  118. package/src/execution/__tests__/subagent-service-message-close.test.ts +629 -0
  119. package/src/execution/__tests__/subagent-service-parent-guard.test.ts +180 -0
  120. package/src/execution/__tests__/subagent-service.test.ts +788 -0
  121. package/src/execution/__tests__/subprocess-agent-runner-routing.test.ts +310 -0
  122. package/src/execution/__tests__/subprocess-agent-runner.test.ts +612 -0
  123. package/src/execution/__tests__/temp-prompt.test.ts +53 -0
  124. package/src/execution/__tests__/timeout-integration.test.ts +615 -0
  125. package/src/execution/__tests__/tombstone-store.test.ts +73 -0
  126. package/src/execution/__tests__/turn-limiter-semantics.test.ts +194 -0
  127. package/src/execution/__tests__/turn-limiter.test.ts +65 -0
  128. package/src/execution/__tests__/ui-channels.test.ts +187 -0
  129. package/src/execution/__tests__/ui-interaction-model.test.ts +67 -0
  130. package/src/execution/__tests__/ui-request-handler-factory.test.ts +369 -0
  131. package/src/execution/__tests__/ui-request-handler.test.ts +204 -0
  132. package/src/execution/__tests__/ui-request-observability.test.ts +113 -0
  133. package/src/execution/__tests__/ui-request-queue.test.ts +133 -0
  134. package/src/execution/__tests__/worktree-manager.test.ts +633 -0
  135. package/src/execution/__tests__/worktree-pid-registration.integration.test.ts +229 -0
  136. package/src/execution/__tests__/worktree-reconcile.integration.test.ts +181 -0
  137. package/src/execution/__tests__/worktree-registry.test.ts +199 -0
  138. package/src/execution/agent-registry.ts +199 -0
  139. package/src/execution/agent-result-mapper.ts +90 -0
  140. package/src/execution/alive-store.ts +119 -0
  141. package/src/execution/argv-mirror.ts +113 -0
  142. package/src/execution/best-effort.ts +38 -0
  143. package/src/execution/channel-registry-access.ts +144 -0
  144. package/src/execution/concurrency-pool.ts +116 -0
  145. package/src/execution/config.ts +165 -0
  146. package/src/execution/dialog-queue.ts +330 -0
  147. package/src/execution/engine/__tests__/common/data-dir.test.ts +78 -0
  148. package/src/execution/engine/__tests__/common/errors.test.ts +132 -0
  149. package/src/execution/engine/__tests__/common/event-journal.test.ts +177 -0
  150. package/src/execution/engine/__tests__/common/kill-chain.test.ts +192 -0
  151. package/src/execution/engine/__tests__/common/nesting-guard.test.ts +81 -0
  152. package/src/execution/engine/__tests__/common/persona-router.test.ts +123 -0
  153. package/src/execution/engine/__tests__/common/pool-manager.test.ts +154 -0
  154. package/src/execution/engine/__tests__/common/schema-emulation.test.ts +128 -0
  155. package/src/execution/engine/__tests__/conformance/__fixtures__/pi-golden-events.json +28 -0
  156. package/src/execution/engine/__tests__/conformance/agent-event-invariants.ts +141 -0
  157. package/src/execution/engine/__tests__/conformance/contract.abort.test.ts +109 -0
  158. package/src/execution/engine/__tests__/conformance/contract.agent-events.test.ts +101 -0
  159. package/src/execution/engine/__tests__/conformance/contract.probe.test.ts +77 -0
  160. package/src/execution/engine/__tests__/conformance/contract.read-degradation.test.ts +104 -0
  161. package/src/execution/engine/__tests__/conformance/engine-conformance.live.test.ts +201 -0
  162. package/src/execution/engine/__tests__/conformance/golden-replay.pi.test.ts +76 -0
  163. package/src/execution/engine/__tests__/conformance/golden-replay.zcode.test.ts +79 -0
  164. package/src/execution/engine/__tests__/engine-discovery.test.ts +87 -0
  165. package/src/execution/engine/__tests__/model-prompt.test.ts +197 -0
  166. package/src/execution/engine/__tests__/paths.test.ts +39 -0
  167. package/src/execution/engine/__tests__/registry.test.ts +120 -0
  168. package/src/execution/engine/__tests__/routing.test.ts +231 -0
  169. package/src/execution/engine/common/data-dir.ts +66 -0
  170. package/src/execution/engine/common/errors.ts +183 -0
  171. package/src/execution/engine/common/event-journal.ts +254 -0
  172. package/src/execution/engine/common/journal-replay.ts +62 -0
  173. package/src/execution/engine/common/kill-chain.ts +221 -0
  174. package/src/execution/engine/common/nesting-guard.ts +50 -0
  175. package/src/execution/engine/common/persona-router.ts +108 -0
  176. package/src/execution/engine/common/pool-manager.ts +226 -0
  177. package/src/execution/engine/common/schema-emulation.ts +189 -0
  178. package/src/execution/engine/common/session-view-projection.ts +51 -0
  179. package/src/execution/engine/engine-discovery.ts +65 -0
  180. package/src/execution/engine/engines/pi/__tests__/pi-engine.test.ts +469 -0
  181. package/src/execution/engine/engines/pi/__tests__/reader.test.ts +155 -0
  182. package/src/execution/engine/engines/pi/__tests__/task-spec-mapper.test.ts +164 -0
  183. package/src/execution/engine/engines/pi/pi-engine.ts +415 -0
  184. package/src/execution/engine/engines/pi/reader.ts +48 -0
  185. package/src/execution/engine/engines/pi/registration.ts +35 -0
  186. package/src/execution/engine/engines/pi/task-spec-mapper.ts +100 -0
  187. package/src/execution/engine/engines/zcode/__tests__/__fixtures__/zcode-golden-spawn.json +39 -0
  188. package/src/execution/engine/engines/zcode/__tests__/launcher.test.ts +150 -0
  189. package/src/execution/engine/engines/zcode/__tests__/parser.test.ts +246 -0
  190. package/src/execution/engine/engines/zcode/__tests__/preparer.test.ts +228 -0
  191. package/src/execution/engine/engines/zcode/__tests__/reader.test.ts +210 -0
  192. package/src/execution/engine/engines/zcode/__tests__/registration.test.ts +71 -0
  193. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.live.test.ts +127 -0
  194. package/src/execution/engine/engines/zcode/__tests__/zcode-engine.test.ts +580 -0
  195. package/src/execution/engine/engines/zcode/constants.ts +43 -0
  196. package/src/execution/engine/engines/zcode/golden-sample.ts +39 -0
  197. package/src/execution/engine/engines/zcode/launcher.ts +161 -0
  198. package/src/execution/engine/engines/zcode/parser.ts +436 -0
  199. package/src/execution/engine/engines/zcode/preparer.ts +363 -0
  200. package/src/execution/engine/engines/zcode/reader.ts +381 -0
  201. package/src/execution/engine/engines/zcode/registration.ts +37 -0
  202. package/src/execution/engine/engines/zcode/zcode-engine.ts +658 -0
  203. package/src/execution/engine/host-task-spec.ts +47 -0
  204. package/src/execution/engine/model-prompt.ts +158 -0
  205. package/src/execution/engine/paths.ts +42 -0
  206. package/src/execution/engine/port.ts +153 -0
  207. package/src/execution/engine/registry.ts +133 -0
  208. package/src/execution/engine/routing.ts +218 -0
  209. package/src/execution/engine/types.ts +309 -0
  210. package/src/execution/execute-options-mapper.ts +115 -0
  211. package/src/execution/execution-record.ts +984 -0
  212. package/src/execution/finalize-record.ts +254 -0
  213. package/src/execution/finalized-marker.ts +70 -0
  214. package/src/execution/get-state-handshake.ts +104 -0
  215. package/src/execution/host-mode.ts +56 -0
  216. package/src/execution/idle-gc.ts +47 -0
  217. package/src/execution/lifecycle-manager.ts +513 -0
  218. package/src/execution/lifecycle-predicates.ts +65 -0
  219. package/src/execution/manifest-store.ts +256 -0
  220. package/src/execution/model-config-service.ts +250 -0
  221. package/src/execution/model-resolver.ts +248 -0
  222. package/src/execution/notifier.ts +372 -0
  223. package/src/execution/notify-ledger.ts +580 -0
  224. package/src/execution/output-collector.ts +228 -0
  225. package/src/execution/path-encoding.ts +60 -0
  226. package/src/execution/pi-invocation.ts +120 -0
  227. package/src/execution/record-entry.ts +132 -0
  228. package/src/execution/record-store.ts +1265 -0
  229. package/src/execution/relay-env.ts +37 -0
  230. package/src/execution/session-context-resolver.ts +64 -0
  231. package/src/execution/session-file-gc.ts +120 -0
  232. package/src/execution/session-pending.ts +170 -0
  233. package/src/execution/session-reconstructor.ts +678 -0
  234. package/src/execution/session-runner.ts +1789 -0
  235. package/src/execution/sessions-index.ts +304 -0
  236. package/src/execution/spawn-event-adapter.ts +363 -0
  237. package/src/execution/stdin-writer.ts +198 -0
  238. package/src/execution/stream-sink.ts +126 -0
  239. package/src/execution/subagent-service.ts +2268 -0
  240. package/src/execution/subprocess-agent-runner.ts +317 -0
  241. package/src/execution/temp-prompt.ts +62 -0
  242. package/src/execution/tombstone-store.ts +72 -0
  243. package/src/execution/turn-limiter.ts +102 -0
  244. package/src/execution/types.ts +1023 -0
  245. package/src/execution/ui-channels.ts +216 -0
  246. package/src/execution/ui-interaction-model.ts +48 -0
  247. package/src/execution/ui-request-handler-factory.ts +243 -0
  248. package/src/execution/ui-request-observability.ts +81 -0
  249. package/src/execution/ui-request-queue.ts +184 -0
  250. package/src/execution/worktree-manager.ts +688 -0
  251. package/src/execution/worktree-registry.ts +279 -0
  252. package/src/index.ts +87 -0
  253. package/src/orchestration/__tests__/__fixtures__/worker-template.snapshot.txt +349 -0
  254. package/src/orchestration/__tests__/agent-call-catch-fallback.test.ts +202 -0
  255. package/src/orchestration/__tests__/agent-call-stream.test.ts +152 -0
  256. package/src/orchestration/__tests__/args-validator.test.ts +143 -0
  257. package/src/orchestration/__tests__/config-loader.test.ts +503 -0
  258. package/src/orchestration/__tests__/error-recovery-handlers.test.ts +809 -0
  259. package/src/orchestration/__tests__/error-recovery-postmessage-defense.test.ts +404 -0
  260. package/src/orchestration/__tests__/error-recovery-serialize-failed-result.test.ts +56 -0
  261. package/src/orchestration/__tests__/error-recovery-workflow-call.test.ts +166 -0
  262. package/src/orchestration/__tests__/execute-agent-call.test.ts +451 -0
  263. package/src/orchestration/__tests__/launcher-nested-workflow.test.ts +600 -0
  264. package/src/orchestration/__tests__/lifecycle-runid-injection.test.ts +96 -0
  265. package/src/orchestration/__tests__/lifecycle.test.ts +659 -0
  266. package/src/orchestration/__tests__/non-cloneable-return-e2e.test.ts +95 -0
  267. package/src/orchestration/__tests__/review-fix-loop-scriptpath-failfast.test.ts +183 -0
  268. package/src/orchestration/__tests__/script-lint.test.ts +831 -0
  269. package/src/orchestration/__tests__/skill-discovery.test.ts +210 -0
  270. package/src/orchestration/__tests__/test-mocks.ts +197 -0
  271. package/src/orchestration/__tests__/worker-exit-without-result.test.ts +368 -0
  272. package/src/orchestration/__tests__/worker-host.test.ts +120 -0
  273. package/src/orchestration/__tests__/worker-returnmeta-passthrough.test.ts +164 -0
  274. package/src/orchestration/__tests__/worker-script-builder-runtime.test.ts +563 -0
  275. package/src/orchestration/__tests__/worker-script-builder.test.ts +309 -0
  276. package/src/orchestration/__tests__/worker-script-template-snapshot.test.ts +129 -0
  277. package/src/orchestration/__tests__/workflow-nesting-e2e.test.ts +317 -0
  278. package/src/orchestration/__tests__/workflow-script-lint-memo.test.ts +110 -0
  279. package/src/orchestration/agent-opts-resolver.ts +174 -0
  280. package/src/orchestration/args-validator.ts +127 -0
  281. package/src/orchestration/config-loader.ts +316 -0
  282. package/src/orchestration/error-recovery.ts +1018 -0
  283. package/src/orchestration/execute-agent-call.ts +256 -0
  284. package/src/orchestration/launcher.ts +455 -0
  285. package/src/orchestration/lifecycle.ts +399 -0
  286. package/src/orchestration/models/__tests__/budget.test.ts +307 -0
  287. package/src/orchestration/models/agent-call.ts +83 -0
  288. package/src/orchestration/models/budget.ts +118 -0
  289. package/src/orchestration/models/ports.ts +164 -0
  290. package/src/orchestration/models/run-runtime.ts +104 -0
  291. package/src/orchestration/models/run-spec.ts +83 -0
  292. package/src/orchestration/models/run-state.ts +44 -0
  293. package/src/orchestration/models/trace.ts +183 -0
  294. package/src/orchestration/models/types.ts +301 -0
  295. package/src/orchestration/models/workflow-run.ts +254 -0
  296. package/src/orchestration/models/workflow-script-registry.ts +35 -0
  297. package/src/orchestration/models/workflow-script.ts +117 -0
  298. package/src/orchestration/script-lint.ts +766 -0
  299. package/src/orchestration/skill-discovery.ts +135 -0
  300. package/src/orchestration/worker-handle.ts +115 -0
  301. package/src/orchestration/worker-host.ts +98 -0
  302. package/src/orchestration/worker-script-builder.ts +421 -0
  303. package/src/orchestration/workflow-files.ts +85 -0
  304. package/src/orchestration/workflow-script-registry-impl.ts +133 -0
  305. package/src/shared/__tests__/agent-ref.test.ts +34 -0
  306. package/src/shared/__tests__/meta-parser.test.ts +304 -0
  307. package/src/shared/__tests__/model-ref.test.ts +306 -0
  308. package/src/shared/__tests__/resource-discovery-manifest-cache.test.ts +288 -0
  309. package/src/shared/__tests__/resource-discovery.test.ts +749 -0
  310. package/src/shared/__tests__/resource-meta.test.ts +51 -0
  311. package/src/shared/__tests__/schema-jsonify.test.ts +81 -0
  312. package/src/shared/__tests__/timer-delay.test.ts +61 -0
  313. package/src/shared/agent-event.ts +13 -0
  314. package/src/shared/agent-ref.ts +57 -0
  315. package/src/shared/meta-parser.ts +261 -0
  316. package/src/shared/model-ref.ts +286 -0
  317. package/src/shared/resource-discovery.ts +835 -0
  318. package/src/shared/resource-meta.ts +65 -0
  319. package/src/shared/schema-env.ts +44 -0
  320. package/src/shared/schema-jsonify.ts +58 -0
  321. package/src/shared/timer-delay.ts +54 -0
  322. package/src/shared/xml-injection.ts +35 -0
  323. package/workflows/README.md +81 -0
  324. package/workflows/_shared/agent-refs.cjs +40 -0
  325. package/workflows/chain.js +137 -0
  326. package/workflows/map-reduce.js +180 -0
  327. package/workflows/parallel.js +154 -0
  328. package/workflows/review-fix-loop-utils.cjs +1542 -0
  329. package/workflows/review-fix-loop.js +1605 -0
  330. package/workflows/scatter-gather.js +184 -0
@@ -0,0 +1,469 @@
1
+ // src/execution/engine/engines/pi/__tests__/pi-engine.test.ts
2
+ //
3
+ // PiEngine 适配层测试(P1 回填):capabilities 链路接通口径(D3)、run 委托与
4
+ // 中立声明→ExecuteOptions 映射、interact 三 action 直通、read 三级降级、probe 形状
5
+ // (C1:ok=false 时 error.recovery 非空)。fake PiEngineService 注入,不 spawn 真进程。
6
+
7
+ import * as fs from "node:fs";
8
+ import * as os from "node:os";
9
+ import * as path from "node:path";
10
+
11
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
12
+
13
+ import { createRecord } from "../../../../execution-record.ts";
14
+ import type { ExecuteOptions, SubagentRecord } from "../../../../types.ts";
15
+ import { IDENTITY_CUSTOM_TYPE } from "../../../../session-reconstructor.ts";
16
+ import { SubagentStream } from "../../../../stream-sink.ts";
17
+ import type { RunContext } from "../../../port.ts";
18
+ import type { AgentTaskSpec, EngineHandle } from "../../../types.ts";
19
+ import {
20
+ PI_ADAPTER_VERSION,
21
+ PI_ENGINE_ID,
22
+ PiEngine,
23
+ type PiEngineService,
24
+ } from "../pi-engine.ts";
25
+
26
+ // ── fake 编排服务(结构子集,记录调用) ──
27
+
28
+ interface ServiceCalls {
29
+ executeOpts?: ExecuteOptions;
30
+ signal?: AbortSignal;
31
+ onEvent?: (event: unknown) => void;
32
+ stream?: unknown;
33
+ deliverMessage?: { recordId: string; text: string; interrupt: boolean };
34
+ close?: { recordId: string; force: boolean };
35
+ cancelId?: string;
36
+ getRecordId?: string;
37
+ }
38
+
39
+ /** 构造最小完整 SubagentRecord(collectRecords 返回项——resolveRecordId 只读 id/sessionFile)。 */
40
+ function makeListedRecord(id: string, sessionFile: string | undefined): SubagentRecord {
41
+ return {
42
+ id,
43
+ agent: "reviewer",
44
+ task: "t",
45
+ slug: "s",
46
+ status: "running",
47
+ mode: "background",
48
+ startedAt: 1,
49
+ rootSessionId: undefined,
50
+ parentRecordId: undefined,
51
+ depth: 0,
52
+ endedAt: undefined,
53
+ turns: 0,
54
+ totalTokens: 0,
55
+ model: "p/m",
56
+ thinkingLevel: undefined,
57
+ eventLog: [],
58
+ displayItems: [],
59
+ ...(sessionFile !== undefined ? { sessionFile } : {}),
60
+ };
61
+ }
62
+
63
+ function makeFakeService(overrides?: {
64
+ executeResult?: Awaited<ReturnType<PiEngineService["executeAndAwait"]>>;
65
+ executeReject?: Error;
66
+ recordsById?: Map<string, ReturnType<typeof createRecord>>;
67
+ listed?: Array<{ id: string; sessionFile?: string }>;
68
+ cancelResult?: boolean;
69
+ deliverReject?: Error;
70
+ }): { service: PiEngineService; calls: ServiceCalls } {
71
+ const calls: ServiceCalls = {};
72
+ const record = createRecord("sa-fake", {
73
+ agent: "reviewer",
74
+ model: "p/m",
75
+ thinkingLevel: "high",
76
+ mode: "background",
77
+ task: "t",
78
+ slug: "s",
79
+ startedAt: 1,
80
+ controller: new AbortController(),
81
+ });
82
+ const service: PiEngineService = {
83
+ executeAndAwait: async (opts, signal, onEvent, stream) => {
84
+ calls.executeOpts = opts;
85
+ calls.signal = signal;
86
+ calls.onEvent = onEvent;
87
+ calls.stream = stream;
88
+ if (overrides?.executeReject) throw overrides.executeReject;
89
+ return (
90
+ overrides?.executeResult ?? {
91
+ content: "done",
92
+ durationMs: 42,
93
+ toolCalls: [],
94
+ }
95
+ );
96
+ },
97
+ getRecordForAction: (id) => {
98
+ calls.getRecordId = id;
99
+ // recordsById 提供时严格查表(模拟「record 不存在 → throw」路径);缺省返回默认 record
100
+ if (overrides?.recordsById) {
101
+ const found = overrides.recordsById.get(id);
102
+ if (!found) {
103
+ throw new Error(`subagent not found or not owned: ${id}`);
104
+ }
105
+ return found;
106
+ }
107
+ return record;
108
+ },
109
+ deliverMessage: async (rec, text, interrupt) => {
110
+ calls.deliverMessage = { recordId: rec.id, text, interrupt };
111
+ if (overrides?.deliverReject) throw overrides.deliverReject;
112
+ },
113
+ closeSubagent: async (rec, force) => {
114
+ calls.close = { recordId: rec.id, force };
115
+ },
116
+ cancel: (id) => {
117
+ calls.cancelId = id;
118
+ return overrides?.cancelResult ?? true;
119
+ },
120
+ collectRecords: () =>
121
+ (overrides?.listed ?? []).map(({ id, sessionFile }) => makeListedRecord(id, sessionFile)),
122
+ };
123
+ return { service, calls };
124
+ }
125
+
126
+ /** 全字段任务声明(覆盖全部泛化点)。 */
127
+ function makeSpec(): AgentTaskSpec {
128
+ return {
129
+ task: "do the thing",
130
+ slug: "do-thing",
131
+ agent: "reviewer",
132
+ model: "zai-coding-cn/glm-5.2",
133
+ effort: "high",
134
+ persona: { skillPath: "/skills/r/SKILL.md", appendSystemPrompt: ["extra"] },
135
+ schema: { type: "object", properties: { ok: { type: "boolean" } } },
136
+ maxTurns: 5,
137
+ graceTurns: 2,
138
+ fork: true,
139
+ worktree: false,
140
+ cwd: "/work",
141
+ conversation: true,
142
+ idleTimeoutMs: 99,
143
+ };
144
+ }
145
+
146
+ function makeHandle(sessionRef: Record<string, string>): EngineHandle {
147
+ return { data: { v: 1, engineId: PI_ENGINE_ID, sessionRef, poolKey: "shared", adapterVersion: PI_ADAPTER_VERSION } };
148
+ }
149
+
150
+ function makeRunCtx(overrides?: Partial<RunContext>): RunContext {
151
+ return { taskId: "sa-test", poolKey: "shared", ...overrides };
152
+ }
153
+
154
+ // ── capabilities ──
155
+
156
+ describe("PiEngine.capabilities(D3 链路接通口径)", () => {
157
+ it("id = 'pi';声明本仓 subagent 链路实际接通的能力(非 RPC 理论能力)", () => {
158
+ const engine = new PiEngine({ getService: () => null });
159
+ expect(engine.id).toBe(PI_ENGINE_ID);
160
+ expect(engine.capabilities()).toEqual({
161
+ schemaEnforcement: "native", // PI_WORKFLOW_SCHEMA env 注入链路(方案 A 唯一权威)
162
+ steer: "unsupported", // RPC 有 steer 但 spawn 链路未接通(session-runner no-op)
163
+ conversation: "native", // chatMode idle 复用 + message/close/cancel 已接通
164
+ personaInjection: "flag", // --skill / --append-system-prompt
165
+ eventGranularity: "stream", // 30+ 事件流
166
+ sandbox: "emulated", // 无 OS sandbox;worktree 隔离
167
+ sessionRead: "full", // pi JSONL 完整重建
168
+ resume: "native", // 同进程 idle 复用 + --session 冷续写
169
+ interrupt: "kill-only", // 现链路 abort = SIGTERM,rpc abort 未接通
170
+ permissionMode: "native", // argv-mirror 镜像主进程 flag
171
+ });
172
+ });
173
+ });
174
+
175
+ // ── run ──
176
+
177
+ describe("PiEngine.run", () => {
178
+ it("中立声明 → ExecuteOptions 映射 + signal/onEvent/stream/ctxModel 直通(行为零变化)", async () => {
179
+ const { service, calls } = makeFakeService();
180
+ const engine = new PiEngine({ getService: () => service });
181
+ const spec = makeSpec();
182
+ const ctxModel = { id: "m", name: "M", provider: "p", reasoning: false };
183
+ const onEvent = (e: unknown) => void e;
184
+ const stream = new SubagentStream("sa-test", { setWidget: () => {} });
185
+ const signal = new AbortController().signal;
186
+
187
+ const { handle, outcome } = await engine.run(spec, makeRunCtx({ signal, onEvent, ctxModel, stream }));
188
+
189
+ // 传参恒等(SAR 接线前的直接调用形态)
190
+ expect(calls.signal).toBe(signal);
191
+ expect(calls.onEvent).toBe(onEvent);
192
+ expect(calls.stream).toBe(stream);
193
+ expect(calls.executeOpts).toEqual({
194
+ task: "do the thing",
195
+ slug: "do-thing",
196
+ agent: "reviewer",
197
+ model: "zai-coding-cn/glm-5.2",
198
+ thinkingLevel: "high", // effort 恒等映射回
199
+ skillPath: "/skills/r/SKILL.md", // persona 还原
200
+ appendSystemPrompt: ["extra"],
201
+ schema: spec.schema,
202
+ schemaEnv: JSON.stringify(spec.schema), // schema 派生(D-A6 bridge)
203
+ maxTurns: 5,
204
+ graceTurns: 2,
205
+ ctxModel,
206
+ fork: true,
207
+ worktree: false,
208
+ cwd: "/work",
209
+ conversation: true,
210
+ idleTimeoutMs: 99,
211
+ engine: "pi", // P4 引擎留痕(D9①):实际执行引擎 id 进 record 投影链
212
+ });
213
+
214
+ // outcome 字段全集映射(缺字段在此转红——SAR outcomeToRunnerResult 的完整性依赖)
215
+ expect(outcome).toEqual({
216
+ content: "done",
217
+ parsedOutput: undefined,
218
+ usage: undefined,
219
+ durationMs: 42,
220
+ error: undefined,
221
+ sessionId: undefined,
222
+ sessionFile: undefined,
223
+ worktreePath: undefined,
224
+ toolCalls: [],
225
+ engineId: "pi",
226
+ });
227
+
228
+ // handle 自描述(engineId + sessionRef + poolKey + adapter 版本)
229
+ expect(handle.data).toMatchObject({
230
+ v: 1,
231
+ engineId: "pi",
232
+ poolKey: "shared",
233
+ adapterVersion: PI_ADAPTER_VERSION,
234
+ });
235
+ });
236
+
237
+ it("sessionFile 回填进 handle.sessionRef;ctx.poolKey 透传(空串兜底 'shared')", async () => {
238
+ const { service } = makeFakeService({
239
+ executeResult: { content: "x", sessionFile: "/tmp/s.jsonl", sessionId: "sess-1", toolCalls: [] },
240
+ });
241
+ const engine = new PiEngine({ getService: () => service });
242
+ const r1 = await engine.run({ task: "t", slug: "s" }, makeRunCtx({ poolKey: "agent-reviewer" }));
243
+ expect(r1.handle.data.sessionRef).toEqual({ sessionFile: "/tmp/s.jsonl" });
244
+ expect(r1.handle.data.poolKey).toBe("agent-reviewer");
245
+ expect(r1.outcome.sessionId).toBe("sess-1");
246
+ const r2 = await engine.run({ task: "t", slug: "s" }, makeRunCtx({ poolKey: "" }));
247
+ expect(r2.handle.data.poolKey).toBe("shared");
248
+ });
249
+
250
+ it("服务不可用(prepare 期)→ reject 且不产生 handle", async () => {
251
+ const engine = new PiEngine({ getService: () => null });
252
+ await expect(engine.run({ task: "t", slug: "s" }, makeRunCtx())).rejects.toThrow(/SubagentService unavailable/);
253
+ });
254
+
255
+ it("executeAndAwait throw(嵌套超限等创建期异常)→ 向上传播(SAR catch 兜底不变)", async () => {
256
+ const { service } = makeFakeService({ executeReject: new Error("nesting depth exceeded") });
257
+ const engine = new PiEngine({ getService: () => service });
258
+ await expect(engine.run({ task: "t", slug: "s" }, makeRunCtx())).rejects.toThrow("nesting depth exceeded");
259
+ });
260
+ });
261
+
262
+ // ── interact ──
263
+
264
+ describe("PiEngine.interact(chatMode 交互面直通)", () => {
265
+ it("message → deliverMessage(record, text, false)(interrupt 中立 action 未携带,P1 恒 followUp)", async () => {
266
+ const { service, calls } = makeFakeService();
267
+ const engine = new PiEngine({ getService: () => service });
268
+ const res = await engine.interact(makeHandle({ recordId: "sa-fake" }), { kind: "message", payload: "next round" });
269
+ expect(res).toEqual({ ok: true, delivered: true });
270
+ expect(calls.deliverMessage).toEqual({ recordId: "sa-fake", text: "next round", interrupt: false });
271
+ });
272
+
273
+ it("close → closeSubagent(record, force);payload 缺省 force=false", async () => {
274
+ const { service, calls } = makeFakeService();
275
+ const engine = new PiEngine({ getService: () => service });
276
+ await engine.interact(makeHandle({ recordId: "sa-fake" }), { kind: "close", payload: { force: true } });
277
+ expect(calls.close).toEqual({ recordId: "sa-fake", force: true });
278
+ await engine.interact(makeHandle({ recordId: "sa-fake" }), { kind: "close" });
279
+ expect(calls.close).toEqual({ recordId: "sa-fake", force: false });
280
+ });
281
+
282
+ it("cancel → service.cancel(recordId)", async () => {
283
+ const { service, calls } = makeFakeService();
284
+ const engine = new PiEngine({ getService: () => service });
285
+ const res = await engine.interact(makeHandle({ recordId: "sa-fake" }), { kind: "cancel" });
286
+ expect(res).toEqual({ ok: true, delivered: true });
287
+ expect(calls.cancelId).toBe("sa-fake");
288
+ });
289
+
290
+ it("cancel 返回 false → engine_interact_failed", async () => {
291
+ const { service } = makeFakeService({ cancelResult: false });
292
+ const engine = new PiEngine({ getService: () => service });
293
+ const res = await engine.interact(makeHandle({ recordId: "sa-fake" }), { kind: "cancel" });
294
+ expect(res.ok).toBe(false);
295
+ if (!res.ok) {
296
+ expect(res.code).toBe("engine_interact_failed");
297
+ expect(res.message).toContain("sa-fake");
298
+ }
299
+ });
300
+
301
+ it("recordId 缺席时按 sessionFile 扫描兜底定位(collectRecords 匹配)", async () => {
302
+ const { service, calls } = makeFakeService({ listed: [{ id: "sa-scan-hit", sessionFile: "/x/s.jsonl" }] });
303
+ const engine = new PiEngine({ getService: () => service });
304
+ await engine.interact(makeHandle({ sessionFile: "/x/s.jsonl" }), { kind: "message", payload: "hi" });
305
+ expect(calls.getRecordId).toBe("sa-scan-hit");
306
+ });
307
+
308
+ it("死 handle(无定位符 / record 不存在)→ engine_session_not_resumable(含 cold resume 指引)", async () => {
309
+ const { service } = makeFakeService();
310
+ const engine = new PiEngine({ getService: () => service });
311
+ const res = await engine.interact(makeHandle({}), { kind: "message", payload: "hi" });
312
+ expect(res.ok).toBe(false);
313
+ if (!res.ok) {
314
+ expect(res.code).toBe("engine_session_not_resumable");
315
+ expect(res.message).toContain("--session");
316
+ }
317
+ });
318
+
319
+ it("交互面 throw → engine_interact_failed(message 文案透传行动语言)", async () => {
320
+ const { service } = makeFakeService({
321
+ deliverReject: new Error("subagent sa-fake is not ready for a new message"),
322
+ });
323
+ const engine = new PiEngine({ getService: () => service });
324
+ const res = await engine.interact(makeHandle({ recordId: "sa-fake" }), { kind: "message", payload: "hi" });
325
+ expect(res).toEqual({
326
+ ok: false,
327
+ code: "engine_interact_failed",
328
+ message: "subagent sa-fake is not ready for a new message",
329
+ });
330
+ });
331
+
332
+ it("服务不可用 → reject", async () => {
333
+ const engine = new PiEngine({ getService: () => null });
334
+ await expect(
335
+ engine.interact(makeHandle({ recordId: "x" }), { kind: "message", payload: "hi" }),
336
+ ).rejects.toThrow(/SubagentService unavailable/);
337
+ });
338
+ });
339
+
340
+ // ── read ──
341
+
342
+ describe("PiEngine.read(D6 降级链)", () => {
343
+ let tmpDir: string;
344
+
345
+ beforeEach(() => {
346
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-engine-read-"));
347
+ });
348
+ afterEach(() => {
349
+ fs.rmSync(tmpDir, { recursive: true, force: true });
350
+ });
351
+
352
+ it("无 sessionRef → outcome-only(turns 空)", async () => {
353
+ const engine = new PiEngine({ getService: () => null });
354
+ const view = await engine.read(makeHandle({}));
355
+ expect(view).toEqual({ engineId: "pi", turns: [], source: "outcome-only" });
356
+ });
357
+
358
+ it("sessionFile 不存在/损坏 → outcome-only(不 throw)", async () => {
359
+ const engine = new PiEngine({ getService: () => null });
360
+ const view = await engine.read(makeHandle({ sessionFile: path.join(tmpDir, "missing.jsonl") }));
361
+ expect(view.source).toBe("outcome-only");
362
+ });
363
+
364
+ it("pi JSONL 原生读取(第①级):turns 重建 + usage 聚合 + 内部态剥离", async () => {
365
+ const file = path.join(tmpDir, "s.jsonl");
366
+ const lines: string[] = [
367
+ JSON.stringify({ type: "session", version: 3, id: "sess-uuid", timestamp: "2026-01-01T00:00:00.000Z", cwd: tmpDir }),
368
+ JSON.stringify({
369
+ type: "custom", id: "id-1", parentId: null, timestamp: "2026-01-01T00:00:00.000Z",
370
+ customType: IDENTITY_CUSTOM_TYPE,
371
+ data: { id: "bg-1", agent: "worker", mode: "background", task: "t", startedAt: 500 },
372
+ }),
373
+ JSON.stringify({
374
+ type: "message", id: "msg-1", parentId: "id-1", timestamp: new Date(1000).toISOString(),
375
+ message: {
376
+ role: "assistant",
377
+ content: [
378
+ { type: "text", text: "hello" },
379
+ { type: "toolCall", id: "tc-1", name: "bash", arguments: { cmd: "ls" } },
380
+ ],
381
+ usage: { input: 10, output: 20, cacheRead: 1, cacheWrite: 2, totalTokens: 33, cost: { total: 0.5 } },
382
+ stopReason: "stop",
383
+ timestamp: 1000,
384
+ },
385
+ }),
386
+ JSON.stringify({
387
+ type: "message", id: "tr-1", parentId: "msg-1", timestamp: new Date(2000).toISOString(),
388
+ message: {
389
+ role: "toolResult", toolCallId: "tc-1", toolName: "bash",
390
+ content: [{ type: "text", text: "ok" }], timestamp: 2000,
391
+ },
392
+ }),
393
+ ];
394
+ fs.writeFileSync(file, lines.join("\n") + "\n");
395
+
396
+ const engine = new PiEngine({ getService: () => null });
397
+ const view = await engine.read(makeHandle({ sessionFile: file }));
398
+ expect(view.source).toBe("native");
399
+ expect(view.engineId).toBe("pi");
400
+ expect(view.sessionId).toBe("bg-1");
401
+ expect(view.turns).toHaveLength(1);
402
+ expect(view.turns[0].text).toBe("hello");
403
+ expect(view.turns[0].closed).toBe(true);
404
+ // toolCall 纯净形状(无 _status/startedTs 内部态)
405
+ expect(view.turns[0].toolCalls[0]).toMatchObject({ toolName: "bash" });
406
+ expect("_status" in view.turns[0].toolCalls[0]).toBe(false);
407
+ // usage 聚合(turn usageDelta → AgentUsageTotal)
408
+ expect(view.usage).toEqual({ input: 10, output: 20, cacheRead: 1, cacheWrite: 2, cost: 0.5, total: 33 });
409
+ });
410
+ });
411
+
412
+ // ── probe ──
413
+
414
+ describe("PiEngine.probe(D7 + C1 形状)", () => {
415
+ const originalArgv = process.argv;
416
+ const originalExecPath = process.execPath;
417
+ const originalPath = process.env.PATH;
418
+
419
+ afterEach(() => {
420
+ Object.defineProperty(process, "argv", { value: originalArgv, configurable: true });
421
+ Object.defineProperty(process, "execPath", { value: originalExecPath, configurable: true });
422
+ process.env.PATH = originalPath;
423
+ });
424
+
425
+ it("探针成功:invocation + version 两级 check,engineVersion 实测", async () => {
426
+ const probeVersion = vi.fn(async () => "pi 0.84.1");
427
+ const engine = new PiEngine({ getService: () => null, probeVersion });
428
+ const report = await engine.probe();
429
+ expect(report.ok).toBe(true);
430
+ expect(report.engineVersion).toBe("pi 0.84.1");
431
+ expect(report.checks.map((c) => c.name)).toEqual(["invocation", "version"]);
432
+ expect(report.error).toBeUndefined();
433
+ expect(probeVersion).toHaveBeenCalledTimes(1);
434
+ });
435
+
436
+ it("版本探测失败 → ok=false 且 error.recovery 非空(C1:恢复指引必填)", async () => {
437
+ const engine = new PiEngine({ getService: () => null, probeVersion: async () => undefined });
438
+ const report = await engine.probe();
439
+ expect(report.ok).toBe(false);
440
+ expect(report.error?.code).toBe("engine_probe_failed");
441
+ expect(report.error?.recovery.length).toBeGreaterThan(0);
442
+ });
443
+
444
+ it("结果缓存:二次 probe 不重跑;force 强探重跑", async () => {
445
+ const probeVersion = vi.fn(async () => "pi 1.0");
446
+ const engine = new PiEngine({ getService: () => null, probeVersion });
447
+ const first = await engine.probe();
448
+ const second = await engine.probe();
449
+ expect(second).toBe(first); // 缓存命中(同对象)
450
+ expect(probeVersion).toHaveBeenCalledTimes(1);
451
+ await engine.probe({ force: true });
452
+ expect(probeVersion).toHaveBeenCalledTimes(2);
453
+ });
454
+
455
+ it("invocation 不可解析(PATH 无 pi)→ 第 1 check 失败,不再尝试版本探测", async () => {
456
+ // bun 虚拟脚本 + 通用 runtime → 退化为 PATH 依赖形态(command === "pi")
457
+ Object.defineProperty(process, "argv", { value: ["bun", "/$bunfs/root/pi"], configurable: true });
458
+ Object.defineProperty(process, "execPath", { value: "/usr/bin/bun", configurable: true });
459
+ process.env.PATH = path.join(os.tmpdir(), `no-pi-here-${Date.now()}`);
460
+ const probeVersion = vi.fn(async () => "should-not-run");
461
+ const engine = new PiEngine({ getService: () => null, probeVersion });
462
+ const report = await engine.probe();
463
+ expect(report.ok).toBe(false);
464
+ expect(report.checks).toHaveLength(1);
465
+ expect(report.checks[0].name).toBe("invocation");
466
+ expect(report.checks[0].ok).toBe(false);
467
+ expect(probeVersion).not.toHaveBeenCalled();
468
+ });
469
+ });
@@ -0,0 +1,155 @@
1
+ // reader.test.ts —— PiEngine reader(P5):readPiSessionView 对 session-reconstructor
2
+ // 的投影包装。断言三件事:①合法 JSONL → SessionView(turns/toolCalls strip 内部态/
3
+ // usage 聚合/source='native');②reconstructor 降级条件(文件缺失/缺 identity/无
4
+ // assistant)→ undefined 不 throw;③不泄漏 InternalToolCall 内部态(_status/startedTs)。
5
+
6
+ import * as fs from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
9
+
10
+ import { beforeAll, describe, expect, it } from "vitest";
11
+
12
+ import { readPiSessionView } from "../reader.ts";
13
+
14
+ let tmpDir: string;
15
+
16
+ beforeAll(() => {
17
+ tmpDir = fs.mkdtempSync(join(os.tmpdir(), "pi-reader-"));
18
+ });
19
+
20
+ // node:path 的 join 在 ESM 测试里直接用(避免顶部 import 与 beforeAll 顺序问题)
21
+ function join(...segs: string[]): string {
22
+ return path.join(...segs);
23
+ }
24
+
25
+ /** 最小合法 subagent JSONL:header + identity + 一轮 assistant(text/thinking/toolCall)+ toolResult。 */
26
+ function writeValidSession(file: string): void {
27
+ const lines = [
28
+ JSON.stringify({ type: "session", id: "pi-sess-1", cwd: "/proj", timestamp: "2026-08-25T10:00:00Z" }),
29
+ JSON.stringify({
30
+ type: "custom",
31
+ customType: "subagent-identity",
32
+ data: { id: "bg-1-abc", agent: "reviewer", mode: "background", task: "review code", slug: "rev", startedAt: 1756000000000 },
33
+ }),
34
+ JSON.stringify({
35
+ type: "message",
36
+ timestamp: "2026-08-25T10:00:01Z",
37
+ message: {
38
+ role: "assistant",
39
+ timestamp: 1756000001000,
40
+ content: [
41
+ { type: "thinking", thinking: "let me look" },
42
+ { type: "toolCall", id: "tc-1", name: "bash", arguments: { command: "ls" } },
43
+ ],
44
+ usage: { input: 10, output: 5, cacheRead: 1, cacheWrite: 2, cost: { total: 0.1 } },
45
+ stopReason: "toolUse",
46
+ },
47
+ }),
48
+ JSON.stringify({
49
+ type: "message",
50
+ timestamp: "2026-08-25T10:00:02Z",
51
+ message: { role: "toolResult", toolCallId: "tc-1", toolName: "bash", content: [{ type: "text", text: "file-a" }], timestamp: 1756000002000 },
52
+ }),
53
+ JSON.stringify({
54
+ type: "message",
55
+ timestamp: "2026-08-25T10:00:03Z",
56
+ message: {
57
+ role: "assistant",
58
+ timestamp: 1756000003000,
59
+ content: [{ type: "text", text: "all good" }],
60
+ usage: { input: 20, output: 8, cacheRead: 0, cacheWrite: 0, cost: { total: 0.05 } },
61
+ stopReason: "stop",
62
+ },
63
+ }),
64
+ ];
65
+ fs.writeFileSync(file, `${lines.join("\n")}\n`, "utf8");
66
+ }
67
+
68
+ describe("readPiSessionView", () => {
69
+ it("projects a valid subagent JSONL to a native SessionView", async () => {
70
+ const file = join(tmpDir, "valid.jsonl");
71
+ writeValidSession(file);
72
+
73
+ const view = await readPiSessionView(file);
74
+
75
+ expect(view).toBeDefined();
76
+ expect(view?.engineId).toBe("pi");
77
+ expect(view?.source).toBe("native");
78
+ // 两轮 assistant message → 两个 turn(toolResult 不产 turn)
79
+ expect(view?.turns).toHaveLength(2);
80
+
81
+ const [turn1, turn2] = view?.turns ?? [];
82
+ expect(turn1?.text).toBe("");
83
+ expect(turn1?.thinking).toBe("let me look");
84
+ expect(turn1?.toolCalls).toHaveLength(1);
85
+ const tc = turn1?.toolCalls[0];
86
+ expect(tc?.toolName).toBe("bash");
87
+ expect(tc?.args).toEqual({ command: "ls" });
88
+ expect(tc?.result).toEqual({ content: [{ type: "text", text: "file-a" }] });
89
+ // InternalToolCall 内部态不泄漏(导出纯净形状)
90
+ expect(tc).not.toHaveProperty("_status");
91
+ expect(tc).not.toHaveProperty("startedTs");
92
+ expect(turn1?.closed).toBe(true);
93
+
94
+ expect(turn2?.text).toBe("all good");
95
+ expect(turn2?.closed).toBe(true);
96
+
97
+ // usage 聚合:两轮 usageDelta 之和(cost 浮点累加有精度噪声,用 toBeCloseTo)
98
+ expect(view?.usage?.input).toBe(30)
99
+ expect(view?.usage?.output).toBe(13)
100
+ expect(view?.usage?.cacheRead).toBe(1)
101
+ expect(view?.usage?.cacheWrite).toBe(2)
102
+ expect(view?.usage?.cost).toBeCloseTo(0.15, 10)
103
+ expect(view?.usage?.total).toBe(46);
104
+ });
105
+
106
+ it("returns undefined (no throw) when file is missing", async () => {
107
+ await expect(readPiSessionView(join(tmpDir, "nope.jsonl"))).resolves.toBeUndefined();
108
+ });
109
+
110
+ it("returns undefined when identity entry is missing", async () => {
111
+ const file = join(tmpDir, "no-identity.jsonl");
112
+ fs.writeFileSync(
113
+ file,
114
+ `${JSON.stringify({ type: "session", id: "s" })}\n${JSON.stringify({
115
+ type: "message",
116
+ message: { role: "assistant", content: [{ type: "text", text: "hi" }], timestamp: 1 },
117
+ })}\n`,
118
+ "utf8",
119
+ );
120
+ await expect(readPiSessionView(file)).resolves.toBeUndefined();
121
+ });
122
+
123
+ it("returns undefined when no assistant message exists", async () => {
124
+ const file = join(tmpDir, "no-assistant.jsonl");
125
+ fs.writeFileSync(
126
+ file,
127
+ `${JSON.stringify({ type: "session", id: "s" })}\n${JSON.stringify({
128
+ type: "custom",
129
+ customType: "subagent-identity",
130
+ data: { id: "bg-2", agent: "w", mode: "background", task: "t", startedAt: 1 },
131
+ })}\n`,
132
+ "utf8",
133
+ );
134
+ await expect(readPiSessionView(file)).resolves.toBeUndefined();
135
+ });
136
+
137
+ it("omits usage when no message carries it", async () => {
138
+ const file = join(tmpDir, "no-usage.jsonl");
139
+ fs.writeFileSync(
140
+ file,
141
+ `${JSON.stringify({ type: "session", id: "s" })}\n${JSON.stringify({
142
+ type: "custom",
143
+ customType: "subagent-identity",
144
+ data: { id: "bg-3", agent: "w", mode: "background", task: "t", startedAt: 1 },
145
+ })}\n${JSON.stringify({
146
+ type: "message",
147
+ message: { role: "assistant", content: [{ type: "text", text: "hi" }], stopReason: "stop", timestamp: 1 },
148
+ })}\n`,
149
+ "utf8",
150
+ );
151
+ const view = await readPiSessionView(file);
152
+ expect(view?.turns).toHaveLength(1);
153
+ expect(view?.usage).toBeUndefined();
154
+ });
155
+ });