@qjp88995/agent-runtime 0.0.0-stage → 0.1.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 (477) hide show
  1. package/dist/agent-extensions.d.ts +30 -0
  2. package/dist/agent-extensions.d.ts.map +1 -0
  3. package/dist/agent-extensions.js +4 -0
  4. package/dist/agent-extensions.js.map +1 -0
  5. package/dist/builtins/ask-outcome.d.ts +49 -0
  6. package/dist/builtins/ask-outcome.d.ts.map +1 -0
  7. package/dist/builtins/ask-outcome.js +128 -0
  8. package/dist/builtins/ask-outcome.js.map +1 -0
  9. package/dist/builtins/ask-schema.d.ts +110 -0
  10. package/dist/builtins/ask-schema.d.ts.map +1 -0
  11. package/dist/builtins/ask-schema.js +124 -0
  12. package/dist/builtins/ask-schema.js.map +1 -0
  13. package/dist/builtins/ask.service.d.ts +210 -0
  14. package/dist/builtins/ask.service.d.ts.map +1 -0
  15. package/dist/builtins/ask.service.js +373 -0
  16. package/dist/builtins/ask.service.js.map +1 -0
  17. package/dist/builtins/ask.tool.d.ts +49 -0
  18. package/dist/builtins/ask.tool.d.ts.map +1 -0
  19. package/dist/builtins/ask.tool.js +109 -0
  20. package/dist/builtins/ask.tool.js.map +1 -0
  21. package/dist/builtins/index.d.ts +8 -0
  22. package/dist/builtins/index.d.ts.map +1 -0
  23. package/dist/builtins/index.js +8 -0
  24. package/dist/builtins/index.js.map +1 -0
  25. package/dist/builtins/todo-schema.d.ts +90 -0
  26. package/dist/builtins/todo-schema.d.ts.map +1 -0
  27. package/dist/builtins/todo-schema.js +97 -0
  28. package/dist/builtins/todo-schema.js.map +1 -0
  29. package/dist/builtins/todo-settle.hook.d.ts +110 -0
  30. package/dist/builtins/todo-settle.hook.d.ts.map +1 -0
  31. package/dist/builtins/todo-settle.hook.js +174 -0
  32. package/dist/builtins/todo-settle.hook.js.map +1 -0
  33. package/dist/builtins/todo.tool.d.ts +59 -0
  34. package/dist/builtins/todo.tool.d.ts.map +1 -0
  35. package/dist/builtins/todo.tool.js +107 -0
  36. package/dist/builtins/todo.tool.js.map +1 -0
  37. package/dist/context/context-budget-tracker.d.ts +185 -0
  38. package/dist/context/context-budget-tracker.d.ts.map +1 -0
  39. package/dist/context/context-budget-tracker.js +400 -0
  40. package/dist/context/context-budget-tracker.js.map +1 -0
  41. package/dist/context/context-budget.d.ts +48 -0
  42. package/dist/context/context-budget.d.ts.map +1 -0
  43. package/dist/context/context-budget.js +89 -0
  44. package/dist/context/context-budget.js.map +1 -0
  45. package/dist/context/index.d.ts +3 -0
  46. package/dist/context/index.d.ts.map +1 -0
  47. package/dist/context/index.js +3 -0
  48. package/dist/context/index.js.map +1 -0
  49. package/dist/extensions/repeat-call.hook.d.ts +32 -0
  50. package/dist/extensions/repeat-call.hook.d.ts.map +1 -0
  51. package/dist/extensions/repeat-call.hook.js +54 -0
  52. package/dist/extensions/repeat-call.hook.js.map +1 -0
  53. package/dist/hooks/index.d.ts +3 -0
  54. package/dist/hooks/index.d.ts.map +1 -0
  55. package/dist/hooks/index.js +3 -0
  56. package/dist/hooks/index.js.map +1 -0
  57. package/dist/hooks/turn-hook.registry.d.ts +51 -0
  58. package/dist/hooks/turn-hook.registry.d.ts.map +1 -0
  59. package/dist/hooks/turn-hook.registry.js +233 -0
  60. package/dist/hooks/turn-hook.registry.js.map +1 -0
  61. package/dist/hooks/turn-hook.types.d.ts +179 -0
  62. package/dist/hooks/turn-hook.types.d.ts.map +1 -0
  63. package/dist/hooks/turn-hook.types.js +48 -0
  64. package/dist/hooks/turn-hook.types.js.map +1 -0
  65. package/dist/images/byte-cache.d.ts +24 -0
  66. package/dist/images/byte-cache.d.ts.map +1 -0
  67. package/dist/images/byte-cache.js +51 -0
  68. package/dist/images/byte-cache.js.map +1 -0
  69. package/dist/images/delivery.d.ts +115 -0
  70. package/dist/images/delivery.d.ts.map +1 -0
  71. package/dist/images/delivery.js +321 -0
  72. package/dist/images/delivery.js.map +1 -0
  73. package/dist/images/index.d.ts +5 -0
  74. package/dist/images/index.d.ts.map +1 -0
  75. package/dist/images/index.js +5 -0
  76. package/dist/images/index.js.map +1 -0
  77. package/dist/images/prompt.d.ts +151 -0
  78. package/dist/images/prompt.d.ts.map +1 -0
  79. package/dist/images/prompt.js +172 -0
  80. package/dist/images/prompt.js.map +1 -0
  81. package/dist/images/vendor-files.d.ts +93 -0
  82. package/dist/images/vendor-files.d.ts.map +1 -0
  83. package/dist/images/vendor-files.js +156 -0
  84. package/dist/images/vendor-files.js.map +1 -0
  85. package/dist/in-process/conversation-store.d.ts +64 -0
  86. package/dist/in-process/conversation-store.d.ts.map +1 -0
  87. package/dist/in-process/conversation-store.js +222 -0
  88. package/dist/in-process/conversation-store.js.map +1 -0
  89. package/dist/in-process/coordination.d.ts +54 -0
  90. package/dist/in-process/coordination.d.ts.map +1 -0
  91. package/dist/in-process/coordination.js +137 -0
  92. package/dist/in-process/coordination.js.map +1 -0
  93. package/dist/in-process/image-source.d.ts +15 -0
  94. package/dist/in-process/image-source.d.ts.map +1 -0
  95. package/dist/in-process/image-source.js +27 -0
  96. package/dist/in-process/image-source.js.map +1 -0
  97. package/dist/in-process/index.d.ts +36 -0
  98. package/dist/in-process/index.d.ts.map +1 -0
  99. package/dist/in-process/index.js +37 -0
  100. package/dist/in-process/index.js.map +1 -0
  101. package/dist/in-process/question-store.d.ts +28 -0
  102. package/dist/in-process/question-store.d.ts.map +1 -0
  103. package/dist/in-process/question-store.js +122 -0
  104. package/dist/in-process/question-store.js.map +1 -0
  105. package/dist/in-process/run-store.d.ts +36 -0
  106. package/dist/in-process/run-store.d.ts.map +1 -0
  107. package/dist/in-process/run-store.js +81 -0
  108. package/dist/in-process/run-store.js.map +1 -0
  109. package/dist/in-process/session-store.d.ts +33 -0
  110. package/dist/in-process/session-store.d.ts.map +1 -0
  111. package/dist/in-process/session-store.js +93 -0
  112. package/dist/in-process/session-store.js.map +1 -0
  113. package/dist/in-process/stream-bus.d.ts +52 -0
  114. package/dist/in-process/stream-bus.d.ts.map +1 -0
  115. package/dist/in-process/stream-bus.js +200 -0
  116. package/dist/in-process/stream-bus.js.map +1 -0
  117. package/dist/in-process/vendor-file-cache.d.ts +11 -0
  118. package/dist/in-process/vendor-file-cache.d.ts.map +1 -0
  119. package/dist/in-process/vendor-file-cache.js +47 -0
  120. package/dist/in-process/vendor-file-cache.js.map +1 -0
  121. package/dist/index.d.ts +21 -0
  122. package/dist/index.d.ts.map +1 -0
  123. package/dist/index.js +21 -0
  124. package/dist/index.js.map +1 -0
  125. package/dist/loop/active-tick.registry.d.ts +31 -0
  126. package/dist/loop/active-tick.registry.d.ts.map +1 -0
  127. package/dist/loop/active-tick.registry.js +45 -0
  128. package/dist/loop/active-tick.registry.js.map +1 -0
  129. package/dist/loop/index.d.ts +6 -0
  130. package/dist/loop/index.d.ts.map +1 -0
  131. package/dist/loop/index.js +6 -0
  132. package/dist/loop/index.js.map +1 -0
  133. package/dist/loop/loop-driver.d.ts +127 -0
  134. package/dist/loop/loop-driver.d.ts.map +1 -0
  135. package/dist/loop/loop-driver.js +199 -0
  136. package/dist/loop/loop-driver.js.map +1 -0
  137. package/dist/loop/message-hub.d.ts +236 -0
  138. package/dist/loop/message-hub.d.ts.map +1 -0
  139. package/dist/loop/message-hub.js +439 -0
  140. package/dist/loop/message-hub.js.map +1 -0
  141. package/dist/loop/orphan-run.sweeper.d.ts +79 -0
  142. package/dist/loop/orphan-run.sweeper.d.ts.map +1 -0
  143. package/dist/loop/orphan-run.sweeper.js +145 -0
  144. package/dist/loop/orphan-run.sweeper.js.map +1 -0
  145. package/dist/loop/root-host.d.ts +51 -0
  146. package/dist/loop/root-host.d.ts.map +1 -0
  147. package/dist/loop/root-host.js +65 -0
  148. package/dist/loop/root-host.js.map +1 -0
  149. package/dist/ports/context-store.d.ts +69 -0
  150. package/dist/ports/context-store.d.ts.map +1 -0
  151. package/dist/ports/context-store.js +2 -0
  152. package/dist/ports/context-store.js.map +1 -0
  153. package/dist/ports/conversation-store.d.ts +144 -0
  154. package/dist/ports/conversation-store.d.ts.map +1 -0
  155. package/dist/ports/conversation-store.js +2 -0
  156. package/dist/ports/conversation-store.js.map +1 -0
  157. package/dist/ports/coordination.d.ts +76 -0
  158. package/dist/ports/coordination.d.ts.map +1 -0
  159. package/dist/ports/coordination.js +12 -0
  160. package/dist/ports/coordination.js.map +1 -0
  161. package/dist/ports/image-source.d.ts +26 -0
  162. package/dist/ports/image-source.d.ts.map +1 -0
  163. package/dist/ports/image-source.js +2 -0
  164. package/dist/ports/image-source.js.map +1 -0
  165. package/dist/ports/index.d.ts +11 -0
  166. package/dist/ports/index.d.ts.map +1 -0
  167. package/dist/ports/index.js +4 -0
  168. package/dist/ports/index.js.map +1 -0
  169. package/dist/ports/logger.d.ts +14 -0
  170. package/dist/ports/logger.d.ts.map +1 -0
  171. package/dist/ports/logger.js +23 -0
  172. package/dist/ports/logger.js.map +1 -0
  173. package/dist/ports/question-store.d.ts +91 -0
  174. package/dist/ports/question-store.d.ts.map +1 -0
  175. package/dist/ports/question-store.js +8 -0
  176. package/dist/ports/question-store.js.map +1 -0
  177. package/dist/ports/run-store.d.ts +66 -0
  178. package/dist/ports/run-store.d.ts.map +1 -0
  179. package/dist/ports/run-store.js +2 -0
  180. package/dist/ports/run-store.js.map +1 -0
  181. package/dist/ports/session-store.d.ts +72 -0
  182. package/dist/ports/session-store.d.ts.map +1 -0
  183. package/dist/ports/session-store.js +9 -0
  184. package/dist/ports/session-store.js.map +1 -0
  185. package/dist/ports/stream-bus.d.ts +52 -0
  186. package/dist/ports/stream-bus.d.ts.map +1 -0
  187. package/dist/ports/stream-bus.js +2 -0
  188. package/dist/ports/stream-bus.js.map +1 -0
  189. package/dist/ports/vendor-file-cache.d.ts +46 -0
  190. package/dist/ports/vendor-file-cache.d.ts.map +1 -0
  191. package/dist/ports/vendor-file-cache.js +2 -0
  192. package/dist/ports/vendor-file-cache.js.map +1 -0
  193. package/dist/schema/agent-event.d.ts +35 -0
  194. package/dist/schema/agent-event.d.ts.map +1 -0
  195. package/dist/schema/agent-event.js +46 -0
  196. package/dist/schema/agent-event.js.map +1 -0
  197. package/dist/schema/agent-run.d.ts +23 -0
  198. package/dist/schema/agent-run.d.ts.map +1 -0
  199. package/dist/schema/agent-run.js +37 -0
  200. package/dist/schema/agent-run.js.map +1 -0
  201. package/dist/schema/index.d.ts +4 -0
  202. package/dist/schema/index.d.ts.map +1 -0
  203. package/dist/schema/index.js +4 -0
  204. package/dist/schema/index.js.map +1 -0
  205. package/dist/schema/message-part.d.ts +73 -0
  206. package/dist/schema/message-part.d.ts.map +1 -0
  207. package/dist/schema/message-part.js +90 -0
  208. package/dist/schema/message-part.js.map +1 -0
  209. package/dist/subagent/delegate.tool.d.ts +145 -0
  210. package/dist/subagent/delegate.tool.d.ts.map +1 -0
  211. package/dist/subagent/delegate.tool.js +329 -0
  212. package/dist/subagent/delegate.tool.js.map +1 -0
  213. package/dist/subagent/index.d.ts +10 -0
  214. package/dist/subagent/index.d.ts.map +1 -0
  215. package/dist/subagent/index.js +10 -0
  216. package/dist/subagent/index.js.map +1 -0
  217. package/dist/subagent/milestone-body.d.ts +82 -0
  218. package/dist/subagent/milestone-body.d.ts.map +1 -0
  219. package/dist/subagent/milestone-body.js +112 -0
  220. package/dist/subagent/milestone-body.js.map +1 -0
  221. package/dist/subagent/output-scan.d.ts +14 -0
  222. package/dist/subagent/output-scan.d.ts.map +1 -0
  223. package/dist/subagent/output-scan.js +41 -0
  224. package/dist/subagent/output-scan.js.map +1 -0
  225. package/dist/subagent/registry-spawn-policy.d.ts +32 -0
  226. package/dist/subagent/registry-spawn-policy.d.ts.map +1 -0
  227. package/dist/subagent/registry-spawn-policy.js +48 -0
  228. package/dist/subagent/registry-spawn-policy.js.map +1 -0
  229. package/dist/subagent/roster-block.d.ts +45 -0
  230. package/dist/subagent/roster-block.d.ts.map +1 -0
  231. package/dist/subagent/roster-block.js +69 -0
  232. package/dist/subagent/roster-block.js.map +1 -0
  233. package/dist/subagent/situation-block.d.ts +16 -0
  234. package/dist/subagent/situation-block.d.ts.map +1 -0
  235. package/dist/subagent/situation-block.js +71 -0
  236. package/dist/subagent/situation-block.js.map +1 -0
  237. package/dist/subagent/spawn-guards.d.ts +32 -0
  238. package/dist/subagent/spawn-guards.d.ts.map +1 -0
  239. package/dist/subagent/spawn-guards.js +39 -0
  240. package/dist/subagent/spawn-guards.js.map +1 -0
  241. package/dist/subagent/subagent-registry.d.ts +150 -0
  242. package/dist/subagent/subagent-registry.d.ts.map +1 -0
  243. package/dist/subagent/subagent-registry.js +167 -0
  244. package/dist/subagent/subagent-registry.js.map +1 -0
  245. package/dist/subagent/subagent-sessions.d.ts +213 -0
  246. package/dist/subagent/subagent-sessions.d.ts.map +1 -0
  247. package/dist/subagent/subagent-sessions.js +295 -0
  248. package/dist/subagent/subagent-sessions.js.map +1 -0
  249. package/dist/testing/contracts.d.ts +37 -0
  250. package/dist/testing/contracts.d.ts.map +1 -0
  251. package/dist/testing/contracts.js +607 -0
  252. package/dist/testing/contracts.js.map +1 -0
  253. package/dist/testing/index.d.ts +5 -0
  254. package/dist/testing/index.d.ts.map +1 -0
  255. package/dist/testing/index.js +5 -0
  256. package/dist/testing/index.js.map +1 -0
  257. package/dist/testing/stub.d.ts +28 -0
  258. package/dist/testing/stub.d.ts.map +1 -0
  259. package/dist/testing/stub.js +3 -0
  260. package/dist/testing/stub.js.map +1 -0
  261. package/dist/tools/agent-tool.base.d.ts +186 -0
  262. package/dist/tools/agent-tool.base.d.ts.map +1 -0
  263. package/dist/tools/agent-tool.base.js +243 -0
  264. package/dist/tools/agent-tool.base.js.map +1 -0
  265. package/dist/tools/agent-tool.interface.d.ts +95 -0
  266. package/dist/tools/agent-tool.interface.d.ts.map +1 -0
  267. package/dist/tools/agent-tool.interface.js +2 -0
  268. package/dist/tools/agent-tool.interface.js.map +1 -0
  269. package/dist/tools/builtin-names.d.ts +18 -0
  270. package/dist/tools/builtin-names.d.ts.map +1 -0
  271. package/dist/tools/builtin-names.js +20 -0
  272. package/dist/tools/builtin-names.js.map +1 -0
  273. package/dist/tools/index.d.ts +9 -0
  274. package/dist/tools/index.d.ts.map +1 -0
  275. package/dist/tools/index.js +6 -0
  276. package/dist/tools/index.js.map +1 -0
  277. package/dist/tools/tool-definition.d.ts +18 -0
  278. package/dist/tools/tool-definition.d.ts.map +1 -0
  279. package/dist/tools/tool-definition.js +77 -0
  280. package/dist/tools/tool-definition.js.map +1 -0
  281. package/dist/tools/tool-meta.d.ts +55 -0
  282. package/dist/tools/tool-meta.d.ts.map +1 -0
  283. package/dist/tools/tool-meta.js +2 -0
  284. package/dist/tools/tool-meta.js.map +1 -0
  285. package/dist/tools/tool-registry.d.ts +190 -0
  286. package/dist/tools/tool-registry.d.ts.map +1 -0
  287. package/dist/tools/tool-registry.js +379 -0
  288. package/dist/tools/tool-registry.js.map +1 -0
  289. package/dist/tools/tool-result.d.ts +41 -0
  290. package/dist/tools/tool-result.d.ts.map +1 -0
  291. package/dist/tools/tool-result.js +19 -0
  292. package/dist/tools/tool-result.js.map +1 -0
  293. package/dist/tools/tool-wire.d.ts +24 -0
  294. package/dist/tools/tool-wire.d.ts.map +1 -0
  295. package/dist/tools/tool-wire.js +2 -0
  296. package/dist/tools/tool-wire.js.map +1 -0
  297. package/dist/turn/agent-runtime.d.ts +31 -0
  298. package/dist/turn/agent-runtime.d.ts.map +1 -0
  299. package/dist/turn/agent-runtime.js +30 -0
  300. package/dist/turn/agent-runtime.js.map +1 -0
  301. package/dist/turn/chunk-projection.d.ts +32 -0
  302. package/dist/turn/chunk-projection.d.ts.map +1 -0
  303. package/dist/turn/chunk-projection.js +62 -0
  304. package/dist/turn/chunk-projection.js.map +1 -0
  305. package/dist/turn/compaction.d.ts +48 -0
  306. package/dist/turn/compaction.d.ts.map +1 -0
  307. package/dist/turn/compaction.js +89 -0
  308. package/dist/turn/compaction.js.map +1 -0
  309. package/dist/turn/context-overflow.d.ts +11 -0
  310. package/dist/turn/context-overflow.d.ts.map +1 -0
  311. package/dist/turn/context-overflow.js +53 -0
  312. package/dist/turn/context-overflow.js.map +1 -0
  313. package/dist/turn/context-pressure.d.ts +100 -0
  314. package/dist/turn/context-pressure.d.ts.map +1 -0
  315. package/dist/turn/context-pressure.js +21 -0
  316. package/dist/turn/context-pressure.js.map +1 -0
  317. package/dist/turn/events-to-steps.d.ts +8 -0
  318. package/dist/turn/events-to-steps.d.ts.map +1 -0
  319. package/dist/turn/events-to-steps.js +64 -0
  320. package/dist/turn/events-to-steps.js.map +1 -0
  321. package/dist/turn/index.d.ts +17 -0
  322. package/dist/turn/index.d.ts.map +1 -0
  323. package/dist/turn/index.js +16 -0
  324. package/dist/turn/index.js.map +1 -0
  325. package/dist/turn/json-preview.d.ts +22 -0
  326. package/dist/turn/json-preview.d.ts.map +1 -0
  327. package/dist/turn/json-preview.js +34 -0
  328. package/dist/turn/json-preview.js.map +1 -0
  329. package/dist/turn/json-string-field.d.ts +30 -0
  330. package/dist/turn/json-string-field.d.ts.map +1 -0
  331. package/dist/turn/json-string-field.js +125 -0
  332. package/dist/turn/json-string-field.js.map +1 -0
  333. package/dist/turn/loop-reminder.d.ts +79 -0
  334. package/dist/turn/loop-reminder.d.ts.map +1 -0
  335. package/dist/turn/loop-reminder.js +0 -0
  336. package/dist/turn/loop-reminder.js.map +1 -0
  337. package/dist/turn/nul-safe.d.ts +21 -0
  338. package/dist/turn/nul-safe.d.ts.map +1 -0
  339. package/dist/turn/nul-safe.js +23 -0
  340. package/dist/turn/nul-safe.js.map +1 -0
  341. package/dist/turn/parts-from-events.d.ts +71 -0
  342. package/dist/turn/parts-from-events.d.ts.map +1 -0
  343. package/dist/turn/parts-from-events.js +217 -0
  344. package/dist/turn/parts-from-events.js.map +1 -0
  345. package/dist/turn/parts-to-model-messages.d.ts +158 -0
  346. package/dist/turn/parts-to-model-messages.d.ts.map +1 -0
  347. package/dist/turn/parts-to-model-messages.js +334 -0
  348. package/dist/turn/parts-to-model-messages.js.map +1 -0
  349. package/dist/turn/pending-work.d.ts +10 -0
  350. package/dist/turn/pending-work.d.ts.map +1 -0
  351. package/dist/turn/pending-work.js +14 -0
  352. package/dist/turn/pending-work.js.map +1 -0
  353. package/dist/turn/provider-error.d.ts +45 -0
  354. package/dist/turn/provider-error.d.ts.map +1 -0
  355. package/dist/turn/provider-error.js +154 -0
  356. package/dist/turn/provider-error.js.map +1 -0
  357. package/dist/turn/tool-input-preview.d.ts +94 -0
  358. package/dist/turn/tool-input-preview.d.ts.map +1 -0
  359. package/dist/turn/tool-input-preview.js +143 -0
  360. package/dist/turn/tool-input-preview.js.map +1 -0
  361. package/dist/turn/tool-input-progress.d.ts +47 -0
  362. package/dist/turn/tool-input-progress.d.ts.map +1 -0
  363. package/dist/turn/tool-input-progress.js +50 -0
  364. package/dist/turn/tool-input-progress.js.map +1 -0
  365. package/dist/turn/turn-envelope.d.ts +184 -0
  366. package/dist/turn/turn-envelope.d.ts.map +1 -0
  367. package/dist/turn/turn-envelope.js +2 -0
  368. package/dist/turn/turn-envelope.js.map +1 -0
  369. package/dist/turn/turn-journal.d.ts +49 -0
  370. package/dist/turn/turn-journal.d.ts.map +1 -0
  371. package/dist/turn/turn-journal.js +51 -0
  372. package/dist/turn/turn-journal.js.map +1 -0
  373. package/dist/turn/turn-run.d.ts +487 -0
  374. package/dist/turn/turn-run.d.ts.map +1 -0
  375. package/dist/turn/turn-run.js +1167 -0
  376. package/dist/turn/turn-run.js.map +1 -0
  377. package/dist/turn/usage.d.ts +27 -0
  378. package/dist/turn/usage.d.ts.map +1 -0
  379. package/dist/turn/usage.js +51 -0
  380. package/dist/turn/usage.js.map +1 -0
  381. package/package.json +55 -3
  382. package/src/agent-extensions.ts +33 -0
  383. package/src/builtins/ask-outcome.ts +168 -0
  384. package/src/builtins/ask-schema.ts +186 -0
  385. package/src/builtins/ask.service.ts +482 -0
  386. package/src/builtins/ask.tool.ts +138 -0
  387. package/src/builtins/index.ts +25 -0
  388. package/src/builtins/todo-schema.ts +108 -0
  389. package/src/builtins/todo-settle.hook.ts +221 -0
  390. package/src/builtins/todo.tool.ts +134 -0
  391. package/src/context/context-budget-tracker.ts +519 -0
  392. package/src/context/context-budget.ts +124 -0
  393. package/src/context/index.ts +17 -0
  394. package/src/extensions/repeat-call.hook.ts +77 -0
  395. package/src/hooks/index.ts +19 -0
  396. package/src/hooks/turn-hook.registry.ts +307 -0
  397. package/src/hooks/turn-hook.types.ts +210 -0
  398. package/src/images/byte-cache.ts +61 -0
  399. package/src/images/delivery.ts +508 -0
  400. package/src/images/index.ts +35 -0
  401. package/src/images/prompt.ts +289 -0
  402. package/src/images/vendor-files.ts +235 -0
  403. package/src/in-process/conversation-store.ts +304 -0
  404. package/src/in-process/coordination.ts +177 -0
  405. package/src/in-process/image-source.ts +29 -0
  406. package/src/in-process/index.ts +52 -0
  407. package/src/in-process/question-store.ts +155 -0
  408. package/src/in-process/run-store.ts +115 -0
  409. package/src/in-process/session-store.ts +118 -0
  410. package/src/in-process/stream-bus.ts +244 -0
  411. package/src/in-process/vendor-file-cache.ts +67 -0
  412. package/src/index.ts +28 -0
  413. package/src/loop/active-tick.registry.ts +49 -0
  414. package/src/loop/index.ts +25 -0
  415. package/src/loop/loop-driver.ts +275 -0
  416. package/src/loop/message-hub.ts +532 -0
  417. package/src/loop/orphan-run.sweeper.ts +204 -0
  418. package/src/loop/root-host.ts +119 -0
  419. package/src/ports/context-store.ts +71 -0
  420. package/src/ports/conversation-store.ts +165 -0
  421. package/src/ports/coordination.ts +81 -0
  422. package/src/ports/image-source.ts +26 -0
  423. package/src/ports/index.ts +56 -0
  424. package/src/ports/logger.ts +34 -0
  425. package/src/ports/question-store.ts +117 -0
  426. package/src/ports/run-store.ts +69 -0
  427. package/src/ports/session-store.ts +80 -0
  428. package/src/ports/stream-bus.ts +59 -0
  429. package/src/ports/vendor-file-cache.ts +54 -0
  430. package/src/schema/agent-event.ts +69 -0
  431. package/src/schema/agent-run.ts +44 -0
  432. package/src/schema/index.ts +25 -0
  433. package/src/schema/message-part.ts +100 -0
  434. package/src/subagent/delegate.tool.ts +476 -0
  435. package/src/subagent/index.ts +63 -0
  436. package/src/subagent/milestone-body.ts +144 -0
  437. package/src/subagent/output-scan.ts +56 -0
  438. package/src/subagent/registry-spawn-policy.ts +85 -0
  439. package/src/subagent/roster-block.ts +103 -0
  440. package/src/subagent/situation-block.ts +83 -0
  441. package/src/subagent/spawn-guards.ts +54 -0
  442. package/src/subagent/subagent-registry.ts +283 -0
  443. package/src/subagent/subagent-sessions.ts +496 -0
  444. package/src/testing/contracts.ts +882 -0
  445. package/src/testing/index.ts +14 -0
  446. package/src/testing/stub.ts +28 -0
  447. package/src/tools/agent-tool.base.ts +361 -0
  448. package/src/tools/agent-tool.interface.ts +101 -0
  449. package/src/tools/builtin-names.ts +26 -0
  450. package/src/tools/index.ts +39 -0
  451. package/src/tools/tool-definition.ts +93 -0
  452. package/src/tools/tool-meta.ts +60 -0
  453. package/src/tools/tool-registry.ts +513 -0
  454. package/src/tools/tool-result.ts +45 -0
  455. package/src/tools/tool-wire.ts +24 -0
  456. package/src/turn/agent-runtime.ts +43 -0
  457. package/src/turn/chunk-projection.ts +81 -0
  458. package/src/turn/compaction.ts +103 -0
  459. package/src/turn/context-overflow.ts +54 -0
  460. package/src/turn/context-pressure.ts +114 -0
  461. package/src/turn/events-to-steps.ts +73 -0
  462. package/src/turn/index.ts +71 -0
  463. package/src/turn/json-preview.ts +36 -0
  464. package/src/turn/json-string-field.ts +134 -0
  465. package/src/turn/loop-reminder.ts +0 -0
  466. package/src/turn/nul-safe.ts +22 -0
  467. package/src/turn/parts-from-events.ts +248 -0
  468. package/src/turn/parts-to-model-messages.ts +428 -0
  469. package/src/turn/pending-work.ts +14 -0
  470. package/src/turn/provider-error.ts +160 -0
  471. package/src/turn/tool-input-preview.ts +223 -0
  472. package/src/turn/tool-input-progress.ts +76 -0
  473. package/src/turn/turn-envelope.ts +185 -0
  474. package/src/turn/turn-journal.ts +102 -0
  475. package/src/turn/turn-run.ts +1479 -0
  476. package/src/turn/usage.ts +56 -0
  477. package/README.md +0 -3
@@ -0,0 +1,882 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import type {
5
+ SessionLock,
6
+ SubagentAwaitLease,
7
+ TreeBudget,
8
+ WakeChannel,
9
+ } from '../ports/coordination.js';
10
+ import {
11
+ type NewQuestion,
12
+ QuestionConflictError,
13
+ type QuestionStore,
14
+ } from '../ports/question-store.js';
15
+ import type { RunStore } from '../ports/run-store.js';
16
+ import type { SessionStore } from '../ports/session-store.js';
17
+ import type { StreamBus } from '../ports/stream-bus.js';
18
+ import type {
19
+ VendorFileCache,
20
+ VendorFileRecord,
21
+ } from '../ports/vendor-file-cache.js';
22
+ import type { AgentEvent } from '../schema/agent-event.js';
23
+
24
+ /**
25
+ * 端口的契约用例:进程内实现与各适配包(Redis 等)跑同一套,保证换实现不换语义。
26
+ * 每个用例用随机 id,适配器实现可以共用一个后端实例。
27
+ *
28
+ * 用法:`describeStreamBusContract('redis', () => makeBus())`。
29
+ * 工厂可以返回 `{ impl, cleanup }`,cleanup 在用例结束时调用。
30
+ */
31
+ type Factory<T> = () =>
32
+ | T
33
+ | { impl: T; cleanup?: () => void | Promise<void> }
34
+ | Promise<T | { impl: T; cleanup?: () => void | Promise<void> }>;
35
+
36
+ async function make<T extends object>(factory: Factory<T>) {
37
+ const made = await factory();
38
+ if ('impl' in made && typeof made.impl === 'object') {
39
+ return made as { impl: T; cleanup?: () => void | Promise<void> };
40
+ }
41
+ return { impl: made as T };
42
+ }
43
+
44
+ function withImpl<T extends object>(
45
+ factory: Factory<T>,
46
+ body: (impl: T) => Promise<void>
47
+ ) {
48
+ return async () => {
49
+ const { impl, cleanup } = await make(factory);
50
+ try {
51
+ await body(impl);
52
+ } finally {
53
+ await cleanup?.();
54
+ }
55
+ };
56
+ }
57
+
58
+ const id = () => randomUUID();
59
+
60
+ function event(
61
+ streamId: string,
62
+ rootSessionId: string,
63
+ seq: number,
64
+ type: AgentEvent['type'] = 'text-delta'
65
+ ): AgentEvent {
66
+ return {
67
+ seq,
68
+ streamId,
69
+ rootSessionId,
70
+ sessionId: rootSessionId,
71
+ runId: streamId,
72
+ parentRunId: null,
73
+ depth: 0,
74
+ ts: Date.now(),
75
+ type,
76
+ payload: { seq },
77
+ };
78
+ }
79
+
80
+ async function take<T>(it: AsyncIterable<T>, n: number): Promise<T[]> {
81
+ const out: T[] = [];
82
+ if (n === 0) return out;
83
+ for await (const v of it) {
84
+ out.push(v);
85
+ if (out.length >= n) break;
86
+ }
87
+ return out;
88
+ }
89
+
90
+ export function describeStreamBusContract(
91
+ name: string,
92
+ factory: Factory<StreamBus>
93
+ ) {
94
+ describe(`StreamBus contract: ${name}`, () => {
95
+ it(
96
+ 'open 分配从 1 起的 seq,重复 open 抛错',
97
+ withImpl(factory, async bus => {
98
+ const s = id();
99
+ const { nextSeq } = bus.open(s);
100
+ expect([nextSeq(), nextSeq()]).toEqual([1, 2]);
101
+ expect(() => bus.open(s)).toThrow();
102
+ bus.close(s);
103
+ })
104
+ );
105
+
106
+ it(
107
+ 'subscribe 补发 fromSeq 起的历史再接活流,close 后结束',
108
+ withImpl(factory, async bus => {
109
+ const s = id();
110
+ const root = id();
111
+ bus.open(s);
112
+ bus.publish(event(s, root, 1));
113
+ bus.publish(event(s, root, 2));
114
+ const got = (async () => {
115
+ const out: number[] = [];
116
+ for await (const e of bus.subscribe(s, 2)) out.push(e.seq);
117
+ return out;
118
+ })();
119
+ await new Promise(r => setTimeout(r, 10));
120
+ bus.publish(event(s, root, 3));
121
+ bus.close(s);
122
+ expect(await got).toEqual([2, 3]);
123
+ expect(bus.isActive(s)).toBe(false);
124
+ })
125
+ );
126
+
127
+ it(
128
+ 'close 之后 publish 被丢弃',
129
+ withImpl(factory, async bus => {
130
+ const s = id();
131
+ const root = id();
132
+ bus.open(s);
133
+ bus.publish(event(s, root, 1));
134
+ bus.close(s);
135
+ bus.publish(event(s, root, 2));
136
+ const seqs: number[] = [];
137
+ for await (const e of bus.subscribe(s)) seqs.push(e.seq);
138
+ expect(seqs).toEqual([1]);
139
+ })
140
+ );
141
+
142
+ it(
143
+ 'subscribe 的 signal abort 让等待中的迭代结束',
144
+ withImpl(factory, async bus => {
145
+ const s = id();
146
+ bus.open(s);
147
+ const ac = new AbortController();
148
+ const done = (async () => {
149
+ for await (const _ of bus.subscribe(s, 1, ac.signal)) {
150
+ // 不会有事件
151
+ }
152
+ return 'ended';
153
+ })();
154
+ await new Promise(r => setTimeout(r, 10));
155
+ ac.abort();
156
+ expect(await done).toBe('ended');
157
+ bus.close(s);
158
+ })
159
+ );
160
+
161
+ it(
162
+ 'abort 触发 signalOf 的信号',
163
+ withImpl(factory, async bus => {
164
+ const s = id();
165
+ bus.open(s);
166
+ const signal = bus.signalOf(s);
167
+ expect(signal?.aborted).toBe(false);
168
+ bus.abort(s);
169
+ expect(signal?.aborted).toBe(true);
170
+ bus.close(s);
171
+ })
172
+ );
173
+
174
+ it(
175
+ '会话面聚合同一根会话下的多条流与会话级事件,游标可续订',
176
+ withImpl(factory, async bus => {
177
+ const root = id();
178
+ const a = id();
179
+ const b = id();
180
+ bus.open(a);
181
+ bus.open(b);
182
+ const ac = new AbortController();
183
+ const first = take(bus.subscribeSession(root, undefined, ac.signal), 3);
184
+ await new Promise(r => setTimeout(r, 10));
185
+ bus.publish(event(a, root, 1));
186
+ bus.publish(event(b, root, 1));
187
+ bus.publishSessionEvent(event(root, root, 0, 'session-title'));
188
+ const entries = await first;
189
+ expect(entries.map(e => e.event.streamId)).toEqual([a, b, root]);
190
+ expect(entries[2].event.type).toBe('session-title');
191
+
192
+ // 从第一条之后续订:拿到后两条。
193
+ const ac2 = new AbortController();
194
+ const resumed = await take(
195
+ bus.subscribeSession(root, entries[0].id, ac2.signal),
196
+ 2
197
+ );
198
+ expect(resumed.map(e => e.id)).toEqual([entries[1].id, entries[2].id]);
199
+ ac.abort();
200
+ ac2.abort();
201
+ bus.close(a);
202
+ bus.close(b);
203
+ })
204
+ );
205
+
206
+ it(
207
+ '别的根会话的事件不进来',
208
+ withImpl(factory, async bus => {
209
+ const root = id();
210
+ const other = id();
211
+ const s = id();
212
+ bus.open(s);
213
+ const ac = new AbortController();
214
+ const got = take(bus.subscribeSession(root, undefined, ac.signal), 1);
215
+ await new Promise(r => setTimeout(r, 10));
216
+ bus.publish({ ...event(s, other, 1) });
217
+ bus.publishSessionEvent(event(root, root, 0, 'session-title'));
218
+ const [entry] = await got;
219
+ expect(entry.event.rootSessionId).toBe(root);
220
+ ac.abort();
221
+ bus.close(s);
222
+ })
223
+ );
224
+ });
225
+ }
226
+
227
+ export function describeSessionLockContract(
228
+ name: string,
229
+ factory: Factory<SessionLock>
230
+ ) {
231
+ describe(`SessionLock contract: ${name}`, () => {
232
+ it(
233
+ '同一会话只有一个持有者;释放后可再抢',
234
+ withImpl(factory, async lock => {
235
+ const s = id();
236
+ const t1 = await lock.acquire(s);
237
+ expect(t1).toBeTypeOf('string');
238
+ expect(await lock.acquire(s)).toBeNull();
239
+ expect(await lock.isHeld(s)).toBe(true);
240
+ await lock.release(s, t1 as string);
241
+ expect(await lock.isHeld(s)).toBe(false);
242
+ const t2 = await lock.acquire(s);
243
+ expect(t2).not.toBeNull();
244
+ expect(t2).not.toBe(t1);
245
+ await lock.release(s, t2 as string);
246
+ })
247
+ );
248
+
249
+ it(
250
+ 'token 不匹配时 renew 失败、release 不生效',
251
+ withImpl(factory, async lock => {
252
+ const s = id();
253
+ const t = (await lock.acquire(s)) as string;
254
+ expect(await lock.renew(s, 'not-the-token')).toBe(false);
255
+ await lock.release(s, 'not-the-token');
256
+ expect(await lock.isHeld(s)).toBe(true);
257
+ expect(await lock.renew(s, t)).toBe(true);
258
+ await lock.release(s, t);
259
+ })
260
+ );
261
+
262
+ it(
263
+ '不同会话互不影响',
264
+ withImpl(factory, async lock => {
265
+ const a = await lock.acquire(id());
266
+ const b = await lock.acquire(id());
267
+ expect(a).not.toBeNull();
268
+ expect(b).not.toBeNull();
269
+ })
270
+ );
271
+ });
272
+ }
273
+
274
+ export function describeWakeChannelContract(
275
+ name: string,
276
+ factory: Factory<WakeChannel>
277
+ ) {
278
+ describe(`WakeChannel contract: ${name}`, () => {
279
+ it(
280
+ '订阅者收到 wake 与 idle,各走各的频道;取消订阅后不再收到',
281
+ withImpl(factory, async ch => {
282
+ const s = id();
283
+ const wakes: string[] = [];
284
+ const idles: string[] = [];
285
+ const offWake = ch.onWake(x => wakes.push(x));
286
+ const offIdle = ch.onIdle(x => idles.push(x));
287
+ await ch.wake(s);
288
+ await ch.idle(s);
289
+ await waitFor(() => wakes.length === 1 && idles.length === 1);
290
+ expect(wakes).toEqual([s]);
291
+ expect(idles).toEqual([s]);
292
+ offWake();
293
+ offIdle();
294
+ await ch.wake(s);
295
+ await new Promise(r => setTimeout(r, 30));
296
+ expect(wakes).toEqual([s]);
297
+ })
298
+ );
299
+
300
+ it(
301
+ '投递是异步的:wake() 返回前处理函数不会被同步调用',
302
+ withImpl(factory, async ch => {
303
+ let called = false;
304
+ const off = ch.onWake(() => {
305
+ called = true;
306
+ });
307
+ const p = ch.wake(id());
308
+ expect(called).toBe(false);
309
+ await p;
310
+ await waitFor(() => called);
311
+ off();
312
+ })
313
+ );
314
+ });
315
+ }
316
+
317
+ export function describeSubagentAwaitLeaseContract(
318
+ name: string,
319
+ factory: Factory<SubagentAwaitLease>
320
+ ) {
321
+ describe(`SubagentAwaitLease contract: ${name}`, () => {
322
+ it(
323
+ 'begin 后 isAwaited 为真且不消费;claim 恰好一方胜出',
324
+ withImpl(factory, async lease => {
325
+ const c = id();
326
+ expect(await lease.isAwaited(c)).toBe(false);
327
+ expect(await lease.begin(c)).toBe(true);
328
+ expect(await lease.isAwaited(c)).toBe(true);
329
+ expect(await lease.isAwaited(c)).toBe(true);
330
+ expect(await lease.claim(c, 'finalizer')).toBe(true);
331
+ expect(await lease.claim(c, 'parent')).toBe(false);
332
+ expect(await lease.isAwaited(c)).toBe(false);
333
+ })
334
+ );
335
+
336
+ it(
337
+ '没 begin 过:finalizer 抢不到(照常回投)',
338
+ withImpl(factory, async lease => {
339
+ expect(await lease.claim(id(), 'finalizer')).toBe(false);
340
+ })
341
+ );
342
+
343
+ it(
344
+ 'renew 不会凭空建出租约',
345
+ withImpl(factory, async lease => {
346
+ const c = id();
347
+ await lease.renew(c);
348
+ expect(await lease.isAwaited(c)).toBe(false);
349
+ })
350
+ );
351
+ });
352
+ }
353
+
354
+ export function describeTreeBudgetContract(
355
+ name: string,
356
+ factory: Factory<TreeBudget>
357
+ ) {
358
+ describe(`TreeBudget contract: ${name}`, () => {
359
+ it(
360
+ '到上限即拒;释放后可再占;不留负值',
361
+ withImpl(factory, async budget => {
362
+ const r = id();
363
+ expect(await budget.tryIncr(r, 2)).toBe(true);
364
+ expect(await budget.tryIncr(r, 2)).toBe(true);
365
+ expect(await budget.tryIncr(r, 2)).toBe(false);
366
+ await budget.decr(r);
367
+ expect(await budget.tryIncr(r, 2)).toBe(true);
368
+ await budget.decr(r);
369
+ await budget.decr(r);
370
+ await budget.decr(r); // 多减一次
371
+ expect(await budget.tryIncr(r, 1)).toBe(true);
372
+ expect(await budget.tryIncr(r, 1)).toBe(false);
373
+ })
374
+ );
375
+
376
+ it(
377
+ '并发 tryIncr 不超发',
378
+ withImpl(factory, async budget => {
379
+ const r = id();
380
+ const results = await Promise.all(
381
+ Array.from({ length: 10 }, () => budget.tryIncr(r, 3))
382
+ );
383
+ expect(results.filter(Boolean)).toHaveLength(3);
384
+ })
385
+ );
386
+
387
+ it(
388
+ 'set 覆盖计数',
389
+ withImpl(factory, async budget => {
390
+ const r = id();
391
+ await budget.set(r, 3);
392
+ expect(await budget.tryIncr(r, 3)).toBe(false);
393
+ await budget.set(r, 0);
394
+ expect(await budget.tryIncr(r, 3)).toBe(true);
395
+ })
396
+ );
397
+ });
398
+ }
399
+
400
+ export function describeRunStoreContract(
401
+ name: string,
402
+ factory: Factory<RunStore>,
403
+ /** 建一条能被 `findStaleRunning(0)` 看到的旧跑次所需的等待(毫秒),缺省 5。 */
404
+ staleDelayMs = 5
405
+ ) {
406
+ describe(`RunStore contract: ${name}`, () => {
407
+ const open = (
408
+ store: RunStore,
409
+ over: Partial<{
410
+ id: string;
411
+ sessionId: string | null;
412
+ parentRunId: string | null;
413
+ depth: number;
414
+ }> = {}
415
+ ) => {
416
+ const runId = over.id ?? id();
417
+ return store
418
+ .open({
419
+ id: runId,
420
+ streamId: runId,
421
+ parentRunId: over.parentRunId ?? null,
422
+ depth: over.depth ?? 0,
423
+ sessionId: over.sessionId === undefined ? id() : over.sessionId,
424
+ prompt: null,
425
+ model: { modelId: 'm', label: 'v / m', reasoningEffort: 'medium' },
426
+ })
427
+ .then(() => runId);
428
+ };
429
+
430
+ it(
431
+ 'open 后可查会话与深度;缺记录给缺省值',
432
+ withImpl(factory, async store => {
433
+ const sessionId = id();
434
+ const runId = await open(store, { sessionId, depth: 2 });
435
+ expect(await store.getSessionId(runId)).toBe(sessionId);
436
+ expect(await store.getDepth(runId)).toBe(2);
437
+ expect(await store.getSessionId(id())).toBeNull();
438
+ expect(await store.getDepth(id())).toBe(0);
439
+ })
440
+ );
441
+
442
+ it(
443
+ 'markInterrupted 只对 running 生效,且只成功一次',
444
+ withImpl(factory, async store => {
445
+ const runId = await open(store);
446
+ expect(await store.markInterrupted(runId, 'killed')).toBe(true);
447
+ expect(await store.markInterrupted(runId, 'killed')).toBe(false);
448
+
449
+ const done = await open(store);
450
+ await store.finish({
451
+ id: done,
452
+ status: 'completed',
453
+ stopReason: 'stop',
454
+ text: 'ok',
455
+ steps: [],
456
+ });
457
+ expect(await store.markInterrupted(done, 'killed')).toBe(false);
458
+ })
459
+ );
460
+
461
+ it(
462
+ 'findStaleRunning 只出仍在 running 且够旧的',
463
+ withImpl(factory, async store => {
464
+ const running = await open(store);
465
+ const finished = await open(store);
466
+ await store.finish({
467
+ id: finished,
468
+ status: 'completed',
469
+ stopReason: 'stop',
470
+ text: '',
471
+ steps: [],
472
+ });
473
+ await new Promise(r => setTimeout(r, staleDelayMs));
474
+ const ids = (await store.findStaleRunning(0)).map(r => r.id);
475
+ expect(ids).toContain(running);
476
+ expect(ids).not.toContain(finished);
477
+ expect(
478
+ (await store.findStaleRunning(60 * 60_000)).map(r => r.id)
479
+ ).not.toContain(running);
480
+ })
481
+ );
482
+ });
483
+ }
484
+
485
+ /** 轮询等条件成立(契约用例不依赖 vitest 的 fake timers,适配器实现也能跑)。 */
486
+ async function waitFor(cond: () => boolean, timeoutMs = 2000) {
487
+ const start = Date.now();
488
+ while (!cond()) {
489
+ if (Date.now() - start > timeoutMs) throw new Error('waitFor timed out');
490
+ await new Promise(r => setTimeout(r, 5));
491
+ }
492
+ }
493
+
494
+ /**
495
+ * `SessionStore` 契约(子会话行与里程碑游标)。`listSubagentSessionsForSweep` 要看消息与
496
+ * 更新时间,跟宿主的消息表绑在一起,不在这里测。
497
+ */
498
+ export function describeSessionStoreContract(
499
+ name: string,
500
+ factory: Factory<SessionStore>
501
+ ) {
502
+ describe(`SessionStore contract: ${name}`, () => {
503
+ const child = (store: SessionStore, rootSessionId: string) =>
504
+ store.createSubSession({
505
+ rootSessionId,
506
+ parentRunId: id(),
507
+ subagentName: 'general-purpose',
508
+ userId: null,
509
+ title: 't',
510
+ metadata: { k: 'v' },
511
+ attrs: { a: 1 },
512
+ });
513
+
514
+ it(
515
+ '建子会话:存活(游标 null)、读回同一行;不存在的读回 null',
516
+ withImpl(factory, async store => {
517
+ const root = id();
518
+ const made = await child(store, root);
519
+ expect(made).toMatchObject({
520
+ kind: 'subagent',
521
+ rootSessionId: root,
522
+ subagentName: 'general-purpose',
523
+ lastMilestoneSeq: null,
524
+ metadata: { k: 'v' },
525
+ attrs: { a: 1 },
526
+ });
527
+ expect(await store.getSession(made.id)).toEqual(made);
528
+ expect(await store.getSession(id())).toBeNull();
529
+ })
530
+ );
531
+
532
+ it(
533
+ 'advanceMilestone 是 CAS:只有期望值对上才写',
534
+ withImpl(factory, async store => {
535
+ const made = await child(store, id());
536
+ expect(await store.advanceMilestone(made.id, 3, 5)).toBe(false);
537
+ expect(await store.advanceMilestone(made.id, null, 5)).toBe(true);
538
+ expect(await store.advanceMilestone(made.id, null, 6)).toBe(false);
539
+ expect(await store.advanceMilestone(made.id, 5, 9)).toBe(true);
540
+ expect((await store.getSession(made.id))?.lastMilestoneSeq).toBe(9);
541
+ })
542
+ );
543
+
544
+ it(
545
+ '并发推进同一个旧游标:恰好一个成功',
546
+ withImpl(factory, async store => {
547
+ const made = await child(store, id());
548
+ const results = await Promise.all(
549
+ Array.from({ length: 5 }, (_, i) =>
550
+ store.advanceMilestone(made.id, null, 10 + i)
551
+ )
552
+ );
553
+ expect(results.filter(Boolean)).toHaveLength(1);
554
+ })
555
+ );
556
+
557
+ it(
558
+ 'countAliveSubsByRoot 只数游标为 null 的子会话',
559
+ withImpl(factory, async store => {
560
+ const r1 = id();
561
+ const r2 = id();
562
+ const a = await child(store, r1);
563
+ await child(store, r1);
564
+ await child(store, r2);
565
+ await store.advanceMilestone(a.id, null, 1);
566
+ const counts = new Map(
567
+ (await store.countAliveSubsByRoot()).map(c => [
568
+ c.rootSessionId,
569
+ c.count,
570
+ ])
571
+ );
572
+ expect(counts.get(r1)).toBe(1);
573
+ expect(counts.get(r2)).toBe(1);
574
+ })
575
+ );
576
+ });
577
+ }
578
+
579
+ export function describeQuestionStoreContract(
580
+ name: string,
581
+ factory: Factory<QuestionStore>
582
+ ) {
583
+ describe(`QuestionStore contract: ${name}`, () => {
584
+ const item = {
585
+ question: '用哪种开场?',
586
+ header: '开场',
587
+ options: [{ label: 'A' }, { label: 'B' }],
588
+ multiSelect: false,
589
+ defaultOption: 'A',
590
+ };
591
+ const input = (over: Partial<NewQuestion> = {}): NewQuestion => ({
592
+ sessionId: id(),
593
+ userId: id(),
594
+ runId: id(),
595
+ toolCallId: id(),
596
+ questions: [item],
597
+ ...over,
598
+ });
599
+
600
+ it(
601
+ '落一行 pending:期限按存储时钟从落行起算;不限时为 null;get / probe 读回同一行',
602
+ withImpl(factory, async store => {
603
+ const before = (await store.now()).getTime();
604
+ const { question, swept } = await store.open(
605
+ input({ timeoutSeconds: 300 })
606
+ );
607
+ const after = (await store.now()).getTime();
608
+ expect(swept).toEqual([]);
609
+ expect(question).toMatchObject({
610
+ status: 'pending',
611
+ answers: null,
612
+ resolvedAt: null,
613
+ questions: [item],
614
+ });
615
+ const created = question.createdAt.getTime();
616
+ expect(created).toBeGreaterThanOrEqual(before - 1000);
617
+ expect(created).toBeLessThanOrEqual(after + 1000);
618
+ expect(question.deadlineAt!.getTime() - created).toBe(300_000);
619
+ expect(await store.get(question.id)).toEqual(question);
620
+ expect(await store.probe(question.id, 3600)).toEqual({
621
+ question,
622
+ due: false,
623
+ overdue: false,
624
+ });
625
+
626
+ const untimed = await store.open(input());
627
+ expect(untimed.question.deadlineAt).toBeNull();
628
+ })
629
+ );
630
+
631
+ it(
632
+ 'get 限定会话:别的会话当不存在;不存在的 get / probe 都是 null',
633
+ withImpl(factory, async store => {
634
+ const { question } = await store.open(input());
635
+ expect(await store.get(question.id, question.sessionId)).not.toBeNull();
636
+ expect(await store.get(question.id, id())).toBeNull();
637
+ expect(await store.get(id())).toBeNull();
638
+ expect(await store.probe(id(), 60)).toBeNull();
639
+ })
640
+ );
641
+
642
+ it(
643
+ 'probe:从落行起过了 maxWaitSeconds 即 overdue',
644
+ withImpl(factory, async store => {
645
+ const { question } = await store.open(input());
646
+ expect((await store.probe(question.id, 0))?.overdue).toBe(true);
647
+ })
648
+ );
649
+
650
+ it(
651
+ '同一会话同一轮已有 pending → QuestionConflictError,原行不动;别的轮次的 pending 作废让位并返回',
652
+ withImpl(factory, async store => {
653
+ const sessionId = id();
654
+ const runId = id();
655
+ const first = await store.open(input({ sessionId, runId }));
656
+ await expect(
657
+ store.open(input({ sessionId, runId }))
658
+ ).rejects.toBeInstanceOf(QuestionConflictError);
659
+ expect((await store.get(first.question.id))?.status).toBe('pending');
660
+
661
+ const next = await store.open(input({ sessionId }));
662
+ expect(next.swept.map(r => [r.id, r.status])).toEqual([
663
+ [first.question.id, 'cancelled'],
664
+ ]);
665
+ expect(next.swept[0].resolvedAt).not.toBeNull();
666
+ expect((await store.get(first.question.id))?.status).toBe('cancelled');
667
+
668
+ // 别的会话的 pending 不受影响。
669
+ const other = await store.open(input());
670
+ await store.open(input());
671
+ expect((await store.get(other.question.id))?.status).toBe('pending');
672
+ })
673
+ );
674
+
675
+ it(
676
+ 'settle 是 CAS:只有第一个生效,之后返回 null 且不改写',
677
+ withImpl(factory, async store => {
678
+ const { question } = await store.open(input());
679
+ const answers = [{ header: '开场', labels: ['B'], other: null }];
680
+ const won = await store.settle(question.id, 'answered', answers);
681
+ expect(won).toMatchObject({ status: 'answered', answers });
682
+ expect(won?.resolvedAt).not.toBeNull();
683
+ expect(await store.settle(question.id, 'expired', null)).toBeNull();
684
+ expect(await store.get(question.id)).toEqual(won);
685
+ expect(await store.settle(id(), 'cancelled', null)).toBeNull();
686
+ })
687
+ );
688
+
689
+ it(
690
+ '并发 settle 恰有一个生效',
691
+ withImpl(factory, async store => {
692
+ const { question } = await store.open(input());
693
+ const results = await Promise.all([
694
+ store.settle(question.id, 'answered', []),
695
+ store.settle(question.id, 'expired', []),
696
+ store.settle(question.id, 'cancelled', null),
697
+ ]);
698
+ expect(results.filter(r => r !== null)).toHaveLength(1);
699
+ })
700
+ );
701
+
702
+ it(
703
+ 'settleSession:只改这个会话的 pending(或只改点名的那一个),返回被改的行',
704
+ withImpl(factory, async store => {
705
+ const sessionId = id();
706
+ const done = await store.open(input({ sessionId }));
707
+ await store.settle(done.question.id, 'answered', []);
708
+ const live = await store.open(input({ sessionId }));
709
+ const elsewhere = await store.open(input());
710
+
711
+ expect(
712
+ await store.settleSession(sessionId, 'superseded', id())
713
+ ).toEqual([]);
714
+ const changed = await store.settleSession(sessionId, 'cancelled');
715
+ expect(changed.map(r => [r.id, r.status])).toEqual([
716
+ [live.question.id, 'cancelled'],
717
+ ]);
718
+ expect((await store.get(done.question.id))?.status).toBe('answered');
719
+ expect((await store.get(elsewhere.question.id))?.status).toBe(
720
+ 'pending'
721
+ );
722
+ expect(await store.settleSession(sessionId, 'cancelled')).toEqual([]);
723
+ })
724
+ );
725
+
726
+ it(
727
+ 'hold:仅 pending 且有期限时摘掉期限;不满足(不限时、已停表、已有结局、别的会话)返回 null',
728
+ withImpl(factory, async store => {
729
+ const sessionId = id();
730
+ const { question } = await store.open(
731
+ input({ sessionId, timeoutSeconds: 60 })
732
+ );
733
+ expect(await store.hold(question.id, id())).toBeNull();
734
+ expect(await store.hold(question.id, sessionId)).toMatchObject({
735
+ status: 'pending',
736
+ deadlineAt: null,
737
+ });
738
+ expect(await store.hold(question.id, sessionId)).toBeNull();
739
+ expect((await store.probe(question.id, 3600))?.due).toBe(false);
740
+
741
+ const untimed = await store.open(input());
742
+ expect(
743
+ await store.hold(untimed.question.id, untimed.question.sessionId)
744
+ ).toBeNull();
745
+ })
746
+ );
747
+
748
+ it(
749
+ 'list:这个会话的全部问题,按落行顺序',
750
+ withImpl(factory, async store => {
751
+ const sessionId = id();
752
+ const a = await store.open(input({ sessionId }));
753
+ const b = await store.open(input({ sessionId }));
754
+ await store.open(input());
755
+ expect(
756
+ (await store.list(sessionId)).map(r => [r.id, r.status])
757
+ ).toEqual([
758
+ [a.question.id, 'cancelled'],
759
+ [b.question.id, 'pending'],
760
+ ]);
761
+ expect(await store.list(id())).toEqual([]);
762
+ })
763
+ );
764
+ });
765
+ }
766
+
767
+ export function describeVendorFileCacheContract(
768
+ name: string,
769
+ factory: Factory<VendorFileCache>
770
+ ) {
771
+ describe(`VendorFileCache contract: ${name}`, () => {
772
+ const record = (
773
+ over: Partial<VendorFileRecord> = {}
774
+ ): VendorFileRecord => ({
775
+ vendor: `v-${id()}`,
776
+ account: 'k1',
777
+ contentHash: id(),
778
+ fileId: `file-${id()}`,
779
+ src: null,
780
+ mediaType: 'image/png',
781
+ bytes: 3,
782
+ expiresAt: null,
783
+ lastUsedAt: new Date(1_000),
784
+ ...over,
785
+ });
786
+
787
+ it(
788
+ 'find 按厂商 + 字节查,跨账号都给;别家厂商的不给',
789
+ withImpl(factory, async cache => {
790
+ const a = record();
791
+ const b = record({
792
+ vendor: a.vendor,
793
+ contentHash: a.contentHash,
794
+ account: 'k2',
795
+ });
796
+ const other = record({ contentHash: a.contentHash });
797
+ await cache.put(a);
798
+ await cache.put(b);
799
+ await cache.put(other);
800
+ const found = await cache.find(a.vendor, [a.contentHash, id()]);
801
+ expect(found.map(r => r.account).sort()).toEqual(['k1', 'k2']);
802
+ expect(found.find(r => r.account === 'k1')).toEqual(a);
803
+ expect(await cache.find(a.vendor, [])).toEqual([]);
804
+ })
805
+ );
806
+
807
+ it(
808
+ 'put 按 (厂商, 账号, 字节) 覆盖',
809
+ withImpl(factory, async cache => {
810
+ const a = record();
811
+ await cache.put(a);
812
+ await cache.put({
813
+ ...a,
814
+ fileId: 'file-new',
815
+ expiresAt: new Date(5_000),
816
+ });
817
+ const found = await cache.find(a.vendor, [a.contentHash]);
818
+ expect(found).toHaveLength(1);
819
+ expect(found[0]).toMatchObject({
820
+ fileId: 'file-new',
821
+ expiresAt: new Date(5_000),
822
+ });
823
+ })
824
+ );
825
+
826
+ it(
827
+ 'touch 只改 lastUsedAt;记录不在不报错',
828
+ withImpl(factory, async cache => {
829
+ const a = record();
830
+ await cache.put(a);
831
+ await cache.touch(a, new Date(9_000));
832
+ const [found] = await cache.find(a.vendor, [a.contentHash]);
833
+ expect(found.lastUsedAt).toEqual(new Date(9_000));
834
+ expect(found.fileId).toBe(a.fileId);
835
+ await cache.touch(record(), new Date(9_000));
836
+ })
837
+ );
838
+
839
+ it(
840
+ 'forget 只删这个账号下点名的 file id',
841
+ withImpl(factory, async cache => {
842
+ const a = record();
843
+ const b = record({
844
+ vendor: a.vendor,
845
+ account: 'k2',
846
+ contentHash: a.contentHash,
847
+ });
848
+ const c = record({ vendor: a.vendor });
849
+ await cache.put(a);
850
+ await cache.put(b);
851
+ await cache.put(c);
852
+ await cache.forget(a.vendor, 'k1', [a.fileId, b.fileId]);
853
+ const left = await cache.find(a.vendor, [a.contentHash, c.contentHash]);
854
+ expect(left.map(r => r.fileId).sort()).toEqual(
855
+ [b.fileId, c.fileId].sort()
856
+ );
857
+ })
858
+ );
859
+
860
+ it(
861
+ 'sweepExpired 删到期的,永久的与未到期的留下',
862
+ withImpl(factory, async cache => {
863
+ const vendor = `v-${id()}`;
864
+ const expired = record({ vendor, expiresAt: new Date(1_000) });
865
+ const live = record({ vendor, expiresAt: new Date(10_000) });
866
+ const forever = record({ vendor });
867
+ for (const r of [expired, live, forever]) await cache.put(r);
868
+ expect(
869
+ await cache.sweepExpired(new Date(5_000))
870
+ ).toBeGreaterThanOrEqual(1);
871
+ const left = await cache.find(vendor, [
872
+ expired.contentHash,
873
+ live.contentHash,
874
+ forever.contentHash,
875
+ ]);
876
+ expect(left.map(r => r.fileId).sort()).toEqual(
877
+ [live.fileId, forever.fileId].sort()
878
+ );
879
+ })
880
+ );
881
+ });
882
+ }