@wenathlan/devthink 2.0.15 → 2.0.19

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 (444) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/{dist/bridge.d.ts → bridge.d.ts} +1 -1
  3. package/bridge.d.ts.map +1 -0
  4. package/bridge.js +1877 -0
  5. package/bun.d.ts.map +1 -0
  6. package/bun.js +52329 -0
  7. package/caps/background.json +712 -0
  8. package/caps/cli.json +360 -0
  9. package/caps/library.json +3611 -0
  10. package/caps/mcp.json +126 -0
  11. package/caps/pagebridge.json +377 -0
  12. package/caps/popup.json +51 -0
  13. package/caps/sidepanel.json +522 -0
  14. package/checksums.txt +142 -0
  15. package/cli.d.ts.map +1 -0
  16. package/{dist/cli.js → cli.js} +2 -2
  17. package/companion.d.ts.map +1 -0
  18. package/companion.js +389 -0
  19. package/crossbrowser.d.ts.map +1 -0
  20. package/crossbrowser.js +1191 -0
  21. package/dashdone.d.ts.map +1 -0
  22. package/dashdone.js +368 -0
  23. package/deno.d.ts.map +1 -0
  24. package/deno.js +52325 -0
  25. package/devthink.umd.js +52346 -0
  26. package/fixtures/bun.ts +7 -0
  27. package/fixtures/cjs.js +6 -0
  28. package/fixtures/deno.ts +6 -0
  29. package/fixtures/example-org-pagestate.json +22 -0
  30. package/fixtures/importers/automa-workflow.json +18 -0
  31. package/fixtures/importers/selenium-side.json +20 -0
  32. package/fixtures/importers/tabular-plan.csv +7 -0
  33. package/fixtures/importers/uivision-macro.json +10 -0
  34. package/fixtures/importers/v1-plan.json +13 -0
  35. package/fixtures/mcp-client.mjs +14 -0
  36. package/fixtures/pages/checkout.html +192 -0
  37. package/fixtures/pages/dashboard.html +110 -0
  38. package/fixtures/pages/feed.html +113 -0
  39. package/fixtures/pages/product-grid.html +150 -0
  40. package/fixtures/plans/form-inventory-plan.json +11 -0
  41. package/fixtures/plans/release-notes-plan.json +11 -0
  42. package/fixtures/recipes/agentscompete.json +11 -0
  43. package/fixtures/recipes/agentsforms.json +11 -0
  44. package/fixtures/recipes/agentsmonitor.json +11 -0
  45. package/fixtures/recipes/agentsreview.json +29 -0
  46. package/fixtures/recipes/agentswarm.json +11 -0
  47. package/fixtures/recipes/fillcalendar.json +12 -0
  48. package/fixtures/recipes/fillcheckout.json +13 -0
  49. package/fixtures/recipes/filldependentdropdowns.json +12 -0
  50. package/fixtures/recipes/fillfromcsv.json +13 -0
  51. package/fixtures/recipes/fillloginform.json +12 -0
  52. package/fixtures/recipes/fillmultistepwizard.json +13 -0
  53. package/fixtures/recipes/fillregistration.json +12 -0
  54. package/fixtures/recipes/fillsearchfilters.json +12 -0
  55. package/fixtures/recipes/filltypeahead.json +11 -0
  56. package/fixtures/recipes/monitoralerts.json +12 -0
  57. package/fixtures/recipes/monitoravailability.json +11 -0
  58. package/fixtures/recipes/monitornetworkquiet.json +12 -0
  59. package/fixtures/recipes/monitorpagechanges.json +11 -0
  60. package/fixtures/recipes/monitorpricechange.json +11 -0
  61. package/fixtures/recipes/monitorschedule.json +12 -0
  62. package/fixtures/recipes/scrapedynamicprices.json +13 -0
  63. package/fixtures/recipes/scrapeimagegallery.json +12 -0
  64. package/fixtures/recipes/scrapeinfinitefeed.json +13 -0
  65. package/fixtures/recipes/scrapenestedlists.json +12 -0
  66. package/fixtures/recipes/scrapeproductgrid.json +12 -0
  67. package/fixtures/recipes/scrapereviews.json +12 -0
  68. package/fixtures/recipes/scrapesearchresults.json +12 -0
  69. package/fixtures/recipes/scrapetablepagination.json +12 -0
  70. package/fixtures/recipes/testaccessibility.json +34 -0
  71. package/fixtures/recipes/testconsoleerrors.json +12 -0
  72. package/fixtures/recipes/testdeeplinks.json +12 -0
  73. package/fixtures/recipes/testformvalidation.json +13 -0
  74. package/fixtures/recipes/testlayoutshift.json +12 -0
  75. package/fixtures/recipes/testlinkcheck.json +12 -0
  76. package/fixtures/recipes/testloadtiming.json +12 -0
  77. package/fixtures/recipes/testvisualdiff.json +12 -0
  78. package/fixtures/workflows/changelog-digest-workflow.json +13 -0
  79. package/fixtures/workflows/form-survey-workflow.json +13 -0
  80. package/gallery.json +706 -0
  81. package/hardening.d.ts.map +1 -0
  82. package/hardening.js +188 -0
  83. package/headless.d.ts.map +1 -0
  84. package/headless.js +11522 -0
  85. package/http.d.ts.map +1 -0
  86. package/http.js +2165 -0
  87. package/index.cjs +54682 -0
  88. package/index.d.ts.map +1 -0
  89. package/index.js +52310 -0
  90. package/index.neutral.js +46343 -0
  91. package/{web/manifest.json → manifest.json} +1 -1
  92. package/mcp.d.ts.map +1 -0
  93. package/mcp.js +20515 -0
  94. package/memory.d.ts.map +1 -0
  95. package/{memory.ts → memory.js} +2310 -3834
  96. package/nativehost.template.json +10 -0
  97. package/node.cjs +54686 -0
  98. package/node.d.ts.map +1 -0
  99. package/pack.d.ts.map +1 -0
  100. package/pack.js +805 -0
  101. package/package.json +83 -176
  102. package/policy.d.ts.map +1 -0
  103. package/{policy.ts → policy.js} +4679 -5073
  104. package/progress.d.ts.map +1 -0
  105. package/progress.js +645 -0
  106. package/protocol.d.ts.map +1 -0
  107. package/protocol.js +8334 -0
  108. package/schemas/audit.schema.json +226 -0
  109. package/schemas/capability.schema.json +113 -0
  110. package/schemas/envelope.schema.json +162 -0
  111. package/schemas/memory.schema.json +51 -0
  112. package/schemas/observation.schema.json +113 -0
  113. package/schemas/plan.schema.json +208 -0
  114. package/schemas/progress.schema.json +126 -0
  115. package/schemas/proposal.schema.json +68 -0
  116. package/schemas/session.schema.json +82 -0
  117. package/schemas/tool.schema.json +94 -0
  118. package/server.d.ts.map +1 -0
  119. package/server.js +28877 -0
  120. package/umd-example.html +18 -0
  121. package/umd.d.ts.map +1 -0
  122. package/accounts.ts +0 -2087
  123. package/agent.ts +0 -2331
  124. package/antigravity.ts +0 -2139
  125. package/apifreeze.ts +0 -4146
  126. package/auth.ts +0 -642
  127. package/background.ts +0 -16837
  128. package/bridge.ts +0 -2080
  129. package/build.ts +0 -54
  130. package/bun.ts +0 -26
  131. package/capture.ts +0 -1614
  132. package/checksecrets.ts +0 -72
  133. package/cjs.ts +0 -9
  134. package/cli.ts +0 -3500
  135. package/commands.ts +0 -1101
  136. package/companion.ts +0 -450
  137. package/compatibility.ts +0 -37
  138. package/config.ts +0 -3137
  139. package/constants.ts +0 -943
  140. package/core.ts +0 -1473
  141. package/crossbrowser.ts +0 -1359
  142. package/dashdone.ts +0 -605
  143. package/data.ts +0 -943
  144. package/database.ts +0 -133
  145. package/debug.ts +0 -850
  146. package/deno.ts +0 -27
  147. package/devthink.ts +0 -707
  148. package/dist/accounts.d.ts +0 -409
  149. package/dist/agent.d.ts +0 -837
  150. package/dist/antigravity.d.ts +0 -380
  151. package/dist/apifreeze.d.ts +0 -113
  152. package/dist/auth.d.ts +0 -224
  153. package/dist/capture.d.ts +0 -595
  154. package/dist/cjs.d.ts +0 -6
  155. package/dist/commands.d.ts +0 -310
  156. package/dist/compatibility.d.ts +0 -17
  157. package/dist/config.d.ts +0 -1853
  158. package/dist/constants.d.ts +0 -637
  159. package/dist/core.d.ts +0 -361
  160. package/dist/data.d.ts +0 -330
  161. package/dist/database.d.ts +0 -24
  162. package/dist/debug.d.ts +0 -270
  163. package/dist/engine.d.ts +0 -74
  164. package/dist/environments.d.ts +0 -244
  165. package/dist/evidence.d.ts +0 -277
  166. package/dist/export.d.ts +0 -311
  167. package/dist/fingerprint.d.ts +0 -594
  168. package/dist/flow.d.ts +0 -300
  169. package/dist/gates.d.ts +0 -103
  170. package/dist/identity.d.ts +0 -51
  171. package/dist/ids.d.ts +0 -7
  172. package/dist/llm.d.ts +0 -652
  173. package/dist/models.d.ts +0 -1807
  174. package/dist/net.d.ts +0 -325
  175. package/dist/neutral.d.ts +0 -41
  176. package/dist/oauth.d.ts +0 -703
  177. package/dist/page.d.ts +0 -1166
  178. package/dist/perf.d.ts +0 -481
  179. package/dist/plan.d.ts +0 -134
  180. package/dist/project.d.ts +0 -395
  181. package/dist/providers.d.ts +0 -37
  182. package/dist/quota.d.ts +0 -453
  183. package/dist/recovery.d.ts +0 -621
  184. package/dist/request.d.ts +0 -1492
  185. package/dist/run.d.ts +0 -779
  186. package/dist/runtime.d.ts +0 -212
  187. package/dist/search.d.ts +0 -375
  188. package/dist/security.d.ts +0 -534
  189. package/dist/serve.d.ts +0 -136
  190. package/dist/session.d.ts +0 -296
  191. package/dist/storage.d.ts +0 -53
  192. package/dist/streaming.d.ts +0 -1119
  193. package/dist/swarm.d.ts +0 -328
  194. package/dist/tools.d.ts +0 -170
  195. package/dist/types.d.ts +0 -8357
  196. package/dist/utils.d.ts +0 -82
  197. package/dist/version.d.ts +0 -7
  198. package/dist/versionregistry.d.ts +0 -406
  199. package/dist/views.d.ts +0 -579
  200. package/dist/workbenchsession.d.ts +0 -69
  201. package/dist/workflow.d.ts +0 -649
  202. package/docs/readme.md +0 -92
  203. package/docs/schemas/antigravity.json +0 -192
  204. package/engine.ts +0 -1524
  205. package/environments.ts +0 -1005
  206. package/evidence.ts +0 -643
  207. package/export.ts +0 -889
  208. package/fingerprint.ts +0 -1458
  209. package/flow.ts +0 -1190
  210. package/gates.ts +0 -307
  211. package/gateway.ts +0 -34
  212. package/hardening.ts +0 -191
  213. package/headless.ts +0 -369
  214. package/http.ts +0 -2110
  215. package/identity.ts +0 -246
  216. package/ids.ts +0 -26
  217. package/index.ts +0 -369
  218. package/llm.ts +0 -2682
  219. package/mcp.ts +0 -1835
  220. package/models.ts +0 -3805
  221. package/modes.ts +0 -43
  222. package/net.ts +0 -1214
  223. package/neutral.ts +0 -362
  224. package/node.ts +0 -26
  225. package/oauth.ts +0 -2216
  226. package/offscreen.ts +0 -170
  227. package/pack.ts +0 -956
  228. package/page.ts +0 -7824
  229. package/pagebridge.ts +0 -1116
  230. package/perf.ts +0 -957
  231. package/plan.ts +0 -672
  232. package/plugin.ts +0 -76
  233. package/prisma.config.ts +0 -27
  234. package/progress.ts +0 -1308
  235. package/project.ts +0 -1291
  236. package/protocol.ts +0 -4187
  237. package/providers.ts +0 -246
  238. package/quota.ts +0 -1887
  239. package/recovery.ts +0 -2433
  240. package/request.ts +0 -4482
  241. package/run.ts +0 -2135
  242. package/runtime.ts +0 -526
  243. package/search.ts +0 -1760
  244. package/security.ts +0 -1673
  245. package/serve.ts +0 -419
  246. package/server.ts +0 -5538
  247. package/session.ts +0 -1162
  248. package/storage.ts +0 -182
  249. package/streaming.ts +0 -4101
  250. package/swarm.ts +0 -884
  251. package/sync.ts +0 -56
  252. package/tests/agentcert.test.ts +0 -128
  253. package/tests/agentfleet.test.ts +0 -214
  254. package/tests/agentreview.test.ts +0 -241
  255. package/tests/agentstream.test.ts +0 -335
  256. package/tests/agentwork.test.ts +0 -356
  257. package/tests/apifreeze.test.ts +0 -290
  258. package/tests/apimap.test.ts +0 -112
  259. package/tests/approvalgate.test.ts +0 -175
  260. package/tests/artifactmanifest.test.ts +0 -204
  261. package/tests/attentionfeed.test.ts +0 -163
  262. package/tests/backgroundruns.test.ts +0 -100
  263. package/tests/batchscheduling.test.ts +0 -131
  264. package/tests/blackboard.test.ts +0 -238
  265. package/tests/bridge.test.ts +0 -1088
  266. package/tests/browsercoverage.test.ts +0 -134
  267. package/tests/browserpolyfills.test.ts +0 -124
  268. package/tests/build.mjs +0 -1260
  269. package/tests/clientauth.test.ts +0 -223
  270. package/tests/clitools.test.ts +0 -578
  271. package/tests/confirmgates.test.ts +0 -190
  272. package/tests/containerpack.test.ts +0 -143
  273. package/tests/controlflow.test.ts +0 -2120
  274. package/tests/coordination.test.ts +0 -345
  275. package/tests/copyscreen.test.ts +0 -139
  276. package/tests/core.test.ts +0 -417
  277. package/tests/costcert.test.ts +0 -95
  278. package/tests/crossbrowser.test.ts +0 -115
  279. package/tests/cspaudit.test.ts +0 -79
  280. package/tests/dashdone.test.ts +0 -615
  281. package/tests/datagrid.test.ts +0 -137
  282. package/tests/doccheck.test.ts +0 -107
  283. package/tests/dombatch.test.ts +0 -109
  284. package/tests/emulation.test.ts +0 -282
  285. package/tests/environments.test.ts +0 -259
  286. package/tests/evidenceviews.test.ts +0 -88
  287. package/tests/exporttools.test.ts +0 -185
  288. package/tests/firefoxprep.test.ts +0 -134
  289. package/tests/flowlibrary.test.ts +0 -242
  290. package/tests/flowrun.test.ts +0 -303
  291. package/tests/forensics.test.ts +0 -439
  292. package/tests/gateway.test.ts +0 -939
  293. package/tests/headlesslib.test.ts +0 -171
  294. package/tests/httpstream.test.ts +0 -277
  295. package/tests/immutablelog.test.ts +0 -157
  296. package/tests/inboundguard.test.ts +0 -177
  297. package/tests/lazyload.test.ts +0 -72
  298. package/tests/librarymodes.test.ts +0 -623
  299. package/tests/llm.test.ts +0 -696
  300. package/tests/manifest.test.ts +0 -226
  301. package/tests/maskinputs.test.ts +0 -150
  302. package/tests/matrixverify.test.ts +0 -111
  303. package/tests/mavenpack.test.ts +0 -75
  304. package/tests/mcpserver.test.ts +0 -704
  305. package/tests/memory.test.ts +0 -4155
  306. package/tests/memorycare.test.ts +0 -230
  307. package/tests/migrateplan.test.ts +0 -349
  308. package/tests/minimization.test.ts +0 -414
  309. package/tests/modelroute.test.ts +0 -112
  310. package/tests/multiagent.test.ts +0 -440
  311. package/tests/nativebridge.test.ts +0 -807
  312. package/tests/navigation.test.ts +0 -284
  313. package/tests/nugetpack.test.ts +0 -83
  314. package/tests/orchestration.test.ts +0 -449
  315. package/tests/originpolicy.test.ts +0 -405
  316. package/tests/outputcompare.test.ts +0 -97
  317. package/tests/package.test.ts +0 -91
  318. package/tests/pagebridge.test.ts +0 -5909
  319. package/tests/pentest.test.ts +0 -75
  320. package/tests/perfrecords.test.ts +0 -257
  321. package/tests/permdiff.test.ts +0 -85
  322. package/tests/phishguard.test.ts +0 -128
  323. package/tests/pickerviews.test.ts +0 -155
  324. package/tests/pipelines.test.ts +0 -381
  325. package/tests/planlint.test.ts +0 -240
  326. package/tests/planreview.test.ts +0 -383
  327. package/tests/platformtargets.test.ts +0 -56
  328. package/tests/policy.test.ts +0 -8284
  329. package/tests/portability.test.ts +0 -96
  330. package/tests/progress.test.ts +0 -1036
  331. package/tests/promptlibrary.test.ts +0 -116
  332. package/tests/protocol.test.ts +0 -4793
  333. package/tests/publishingpipeline.test.ts +0 -314
  334. package/tests/quickactions.test.ts +0 -164
  335. package/tests/readiness.test.ts +0 -78
  336. package/tests/recipes.test.ts +0 -163
  337. package/tests/redactshots.test.ts +0 -216
  338. package/tests/registernvidiakeys.ts +0 -51
  339. package/tests/relayserve.test.ts +0 -304
  340. package/tests/releasenotes.test.ts +0 -276
  341. package/tests/resilience.test.ts +0 -467
  342. package/tests/resourceaware.test.ts +0 -187
  343. package/tests/runbudget.test.ts +0 -125
  344. package/tests/runhistory.test.ts +0 -299
  345. package/tests/runreplay.test.ts +0 -113
  346. package/tests/runresume.test.ts +0 -184
  347. package/tests/runstate.test.ts +0 -288
  348. package/tests/runtimeadapters.test.ts +0 -251
  349. package/tests/safariskeleton.test.ts +0 -115
  350. package/tests/sandboxframe.test.ts +0 -196
  351. package/tests/sbom.test.ts +0 -105
  352. package/tests/secretvault.test.ts +0 -188
  353. package/tests/selcache.test.ts +0 -111
  354. package/tests/sessioninterface.test.ts +0 -791
  355. package/tests/sessions.test.ts +0 -591
  356. package/tests/siteprefs.test.ts +0 -228
  357. package/tests/snapshotdelta.test.ts +0 -165
  358. package/tests/statusviews.test.ts +0 -237
  359. package/tests/stepmeter.test.ts +0 -200
  360. package/tests/streamparse.test.ts +0 -163
  361. package/tests/stylepruning.test.ts +0 -90
  362. package/tests/surfaces.test.ts +0 -282
  363. package/tests/sweep.test.ts +0 -78
  364. package/tests/syncbridge.test.ts +0 -164
  365. package/tests/taskqueue.test.ts +0 -206
  366. package/tests/toolcalls.test.ts +0 -336
  367. package/tests/toolcatalog.test.ts +0 -201
  368. package/tests/torture-agent.test.ts +0 -706
  369. package/tests/torture-auth.test.ts +0 -461
  370. package/tests/torture-commands.test.ts +0 -895
  371. package/tests/torture-data.test.ts +0 -923
  372. package/tests/torture-environments.test.ts +0 -285
  373. package/tests/torture-export.test.ts +0 -263
  374. package/tests/torture-http.test.ts +0 -1379
  375. package/tests/torture-llm.test.ts +0 -808
  376. package/tests/torture-memory.test.ts +0 -1084
  377. package/tests/torture-plan.test.ts +0 -829
  378. package/tests/torture-run.test.ts +0 -626
  379. package/tests/torture-session.test.ts +0 -928
  380. package/tests/torture-swarm.test.ts +0 -1245
  381. package/tests/torture-tools.test.ts +0 -964
  382. package/tests/torture-workflow.test.ts +0 -2354
  383. package/tests/tourviews.test.ts +0 -52
  384. package/tests/transparency.test.ts +0 -147
  385. package/tests/trigger.test.ts +0 -941
  386. package/tests/v1sunset.test.ts +0 -428
  387. package/tests/virtlist.test.ts +0 -94
  388. package/tests/vision.test.ts +0 -540
  389. package/tests/vsixpack.test.ts +0 -145
  390. package/tests/webapi.test.ts +0 -523
  391. package/tests/workflow.test.ts +0 -1785
  392. package/tests/workfloweditor.test.ts +0 -759
  393. package/tests/xpipack.test.ts +0 -128
  394. package/tools.ts +0 -734
  395. package/types.ts +0 -9908
  396. package/umd.ts +0 -9
  397. package/utils.ts +0 -249
  398. package/version.ts +0 -8
  399. package/versionregistry.ts +0 -1310
  400. package/views.ts +0 -2153
  401. package/vitest.config.ts +0 -12
  402. package/web/App.tsx +0 -78
  403. package/web/README.md +0 -47
  404. package/web/capacitor.config.ts +0 -46
  405. package/web/const.ts +0 -19
  406. package/web/control.shell.tsx +0 -66
  407. package/web/dashboardpage.ts +0 -554
  408. package/web/db.ts +0 -493
  409. package/web/design.html +0 -541
  410. package/web/gatewayview/Gateway.tsx +0 -345
  411. package/web/gatewayview/config.ts +0 -1042
  412. package/web/gatewayview/definition.ts +0 -174
  413. package/web/icons.ts +0 -15
  414. package/web/optionspage.ts +0 -691
  415. package/web/popup.ts +0 -1651
  416. package/web/schema.prisma +0 -176
  417. package/web/sidepanel.ts +0 -7941
  418. package/web/sitemanifest.json +0 -13
  419. package/web/terminal-ui.tsx +0 -622
  420. package/web/transparencypage.ts +0 -541
  421. package/web/vite.config.ts +0 -39
  422. package/web/workspace.ts +0 -23
  423. package/workbenchmemory.ts +0 -104
  424. package/workbenchsession.ts +0 -304
  425. package/workflow.ts +0 -4495
  426. /package/{dist/bun.d.ts → bun.d.ts} +0 -0
  427. /package/{dist/cli.d.ts → cli.d.ts} +0 -0
  428. /package/{dist/companion.d.ts → companion.d.ts} +0 -0
  429. /package/{dist/crossbrowser.d.ts → crossbrowser.d.ts} +0 -0
  430. /package/{dist/dashdone.d.ts → dashdone.d.ts} +0 -0
  431. /package/{dist/deno.d.ts → deno.d.ts} +0 -0
  432. /package/{dist/hardening.d.ts → hardening.d.ts} +0 -0
  433. /package/{dist/headless.d.ts → headless.d.ts} +0 -0
  434. /package/{dist/http.d.ts → http.d.ts} +0 -0
  435. /package/{dist/index.d.ts → index.d.ts} +0 -0
  436. /package/{dist/mcp.d.ts → mcp.d.ts} +0 -0
  437. /package/{dist/memory.d.ts → memory.d.ts} +0 -0
  438. /package/{dist/node.d.ts → node.d.ts} +0 -0
  439. /package/{dist/pack.d.ts → pack.d.ts} +0 -0
  440. /package/{dist/policy.d.ts → policy.d.ts} +0 -0
  441. /package/{dist/progress.d.ts → progress.d.ts} +0 -0
  442. /package/{dist/protocol.d.ts → protocol.d.ts} +0 -0
  443. /package/{dist/server.d.ts → server.d.ts} +0 -0
  444. /package/{dist/umd.d.ts → umd.d.ts} +0 -0
package/agent.ts DELETED
@@ -1,2331 +0,0 @@
1
- /** The agent module of the 1.1.88 consolidation: every correlated variation of the agent logic interned in this one file, so the module family carries one surface without duplicate variations. */
2
-
3
- /* ── Merged from agentfleet.ts: the 1.1.88 consolidation interns the correlated agentfleet logic here, so no variation of the same file lives beside another. ── */
4
- import type {
5
- agentbudget,
6
- agentrecord,
7
- agentscope,
8
- agentrole,
9
- budgetstate,
10
- runrecord,
11
- runstate,
12
- taskqueue,
13
- toolnamespace,
14
- agentidentity,
15
- agentmailbox,
16
- agentmessage,
17
- messagerouting,
18
- comparisonrecord,
19
- consensusrecord,
20
- consensusvote,
21
- escalationrecord,
22
- replayrecord,
23
- reviewrecord,
24
- reviewverdict,
25
- callcontext,
26
- cancelframe,
27
- capabilityset,
28
- eventkind,
29
- jsonrpcframe,
30
- progressnotice,
31
- promptdef,
32
- protocoleventsubscription,
33
- resourcewatch,
34
- samplingrequest,
35
- streamchunk,
36
- aggregatecell,
37
- aggregaterecord,
38
- arbitrationcase,
39
- arbitrationverdict,
40
- costentry,
41
- depthlimit,
42
- interleaveevent,
43
- lessonrecord,
44
- loadreport,
45
- spawnrecord,
46
- subagentspec,
47
- tasklane,
48
- agentevent,
49
- agenteventkind,
50
- agentusage,
51
- killswitch,
52
- spawnrequest,
53
- swarmstate,
54
- taskitem,
55
- workflowrun,
56
- actionrisk,
57
- blackboard,
58
- blackboardsection,
59
- conflictscan,
60
- conflictwriter,
61
- handoffrecord,
62
- mergeentry,
63
- mergerule,
64
- outputcomparison,
65
- resourcelock,
66
- resultreport,
67
- swarmaction,
68
- swarmcost,
69
- } from "./types.js";
70
-
71
- /**
72
- * Agent fleet logic of the 1.1.72 family.
73
- * Every correlated rule for the fleet registry, the unique lowercase agent naming with its reserved words, the scope intersection that narrows the requested origins and action kinds to the session grants, the read only observer scope, the per agent budget grant from user configuration with its spending and refusal, the remaining budget report, the single agent pause that leaves the peers and their run records untouched, and the global killswitch that cancels every agent run and clears every queue in one call lives in this file.
74
- * The fleet never bypasses review: the scope keeps every step inside the session grants, every budget ceiling stays a user choice with no hardcoded cap, and the killswitch needs the user action because no agent ever triggers it.
75
- */
76
-
77
- /** The reserved fleet names no agent may take: the user, the operator, the human and the system identities stay outside the agent namespace. */
78
- export const reservedagentnames: ReadonlySet<string> = new Set(["user", "operator", "human", "system"]);
79
-
80
- /** Assigns one fleet record with its unique lowercase name: the helper lowercases the user proposal, refuses names outside the lowercase identifier rule, reserved words and duplicates, and registers the record with its role and home origin before its first run. */
81
- export function agentname(input: {
82
- records: agentrecord[];
83
- id: string;
84
- proposed: string;
85
- role?: agentrole;
86
- origin: string;
87
- now: number;
88
- }): agentrecord {
89
- const name = input.proposed.trim().toLowerCase();
90
- if (name === "") throw new Error("The agent needs its user chosen name; agent naming stays a user choice.");
91
- if (!/^[a-z][a-z0-9]*$/.test(name))
92
- throw new Error(
93
- `The agent name ${input.proposed.trim()} must stay a lowercase identifier of letters and digits starting with a letter.`,
94
- );
95
- if (reservedagentnames.has(name))
96
- throw new Error(
97
- `The agent name ${name} is reserved; the user, the operator, the human and the system identities never belong to an agent.`,
98
- );
99
- if (input.records.some((record) => record.name === name))
100
- throw new Error(`The agent name ${name} is already registered; fleet names stay unique.`);
101
- if (input.id.trim() === "") throw new Error("The fleet record needs its id.");
102
- if (input.origin.trim() === "")
103
- throw new Error(
104
- "The fleet record needs its home origin; every agent works from the origin its scope intersects with the session grants.",
105
- );
106
- return {
107
- id: input.id.trim(),
108
- name,
109
- role: input.role ?? "worker",
110
- origin: input.origin.trim(),
111
- state: "active",
112
- registeredat: input.now,
113
- lastseenat: input.now,
114
- };
115
- }
116
-
117
- /** Intersects the requested scope with the session grants: the requested origins keep only the granted entries, the requested action kinds keep only the allowed catalog entries, an absent request stays unbounded inside the grants, and a request that lands entirely outside the grants refuses loudly instead of silently widening. */
118
- export function agentscopeof(input: {
119
- agentid: string;
120
- requested?: { origins?: string[]; toolnamespaces?: toolnamespace[]; actionkinds?: string[] };
121
- grants?: string[];
122
- allowedkinds?: string[];
123
- }): agentscope {
124
- if (input.agentid.trim() === "") throw new Error("The scope needs its agent id.");
125
- const requested = input.requested ?? {};
126
- const requestedorigins = (requested.origins ?? []).map((origin) => origin.trim()).filter((origin) => origin !== "");
127
- const origins =
128
- input.grants === undefined ? requestedorigins : requestedorigins.filter((origin) => input.grants!.includes(origin));
129
- if (input.grants !== undefined && requestedorigins.length > 0 && origins.length === 0)
130
- throw new Error(
131
- `The requested origins ${requestedorigins.join(", ")} sit outside the session grants ${input.grants.join(", ")}; the scope never widens past the grants.`,
132
- );
133
- const requestedkinds = (requested.actionkinds ?? []).map((kind) => kind.trim()).filter((kind) => kind !== "");
134
- const actionkinds =
135
- input.allowedkinds === undefined
136
- ? requestedkinds
137
- : requestedkinds.filter((kind) => input.allowedkinds!.includes(kind));
138
- if (input.allowedkinds !== undefined && requestedkinds.length > 0 && actionkinds.length === 0)
139
- throw new Error(
140
- `The requested action kinds ${requestedkinds.join(", ")} sit outside the allowed catalog; the scope narrows every step to the allowed kinds only.`,
141
- );
142
- const toolnamespaces = requested.toolnamespaces ?? [];
143
- return { agentid: input.agentid.trim(), origins, toolnamespaces, ...(actionkinds.length > 0 ? { actionkinds } : {}) };
144
- }
145
-
146
- /** Builds the read only scope of an observer agent: the observer reads the page and the fleet state through the read side kinds only and never acts on it. */
147
- export function readonlyscope(input: { agentid: string; readkinds: string[] }): agentscope {
148
- if (input.agentid.trim() === "") throw new Error("The read only scope needs its agent id.");
149
- const readkinds = input.readkinds.map((kind) => kind.trim()).filter((kind) => kind !== "");
150
- if (readkinds.length === 0)
151
- throw new Error("The read only scope needs its read side action kinds; an observer with no kinds reads nothing.");
152
- return {
153
- agentid: input.agentid.trim(),
154
- origins: [],
155
- toolnamespaces: ["memory", "system"],
156
- actionkinds: readkinds,
157
- readonly: true,
158
- };
159
- }
160
-
161
- /** Grants one per agent budget from user configuration: the ceilings copy the user values with no hardcoded cap and the fresh budgetstate starts at zero spent. */
162
- export function agentbudgetof(input: {
163
- agentid: string;
164
- maxsteps?: number;
165
- maxtokens?: number;
166
- maxdurationms?: number;
167
- currency?: string;
168
- now: number;
169
- }): { budget: agentbudget; state: budgetstate } {
170
- if (input.agentid.trim() === "") throw new Error("The budget needs its agent id.");
171
- const ceilings: Array<[string, number | undefined]> = [
172
- ["maxsteps", input.maxsteps],
173
- ["maxtokens", input.maxtokens],
174
- ["maxdurationms", input.maxdurationms],
175
- ];
176
- for (const [label, value] of ceilings) {
177
- if (value !== undefined && (!Number.isFinite(value) || value <= 0))
178
- throw new Error(
179
- `The ${label} ceiling must stay a positive number; every ceiling stays the user's choice with no engine default.`,
180
- );
181
- }
182
- const agentid = input.agentid.trim();
183
- return {
184
- budget: {
185
- agentid,
186
- ...(input.maxtokens !== undefined ? { maxtokens: input.maxtokens } : {}),
187
- ...(input.maxsteps !== undefined ? { maxsteps: input.maxsteps } : {}),
188
- ...(input.maxdurationms !== undefined ? { maxdurationms: input.maxdurationms } : {}),
189
- ...(input.currency !== undefined && input.currency.trim() !== "" ? { currency: input.currency.trim() } : {}),
190
- configuredat: input.now,
191
- },
192
- state: {
193
- agentid,
194
- spentsteps: 0,
195
- spenttokens: 0,
196
- spentdurationms: 0,
197
- ...(input.maxsteps !== undefined ? { maxsteps: input.maxsteps } : {}),
198
- ...(input.maxtokens !== undefined ? { maxtokens: input.maxtokens } : {}),
199
- ...(input.maxdurationms !== undefined ? { maxdurationms: input.maxdurationms } : {}),
200
- updatedat: input.now,
201
- },
202
- };
203
- }
204
-
205
- /** Spends one unit per executed step against the budget state: the spend adds its steps, tokens and duration, and a spend past a user configured ceiling refuses the execution until the user raises the ceiling or stops the agent. */
206
- export function spendbudget(input: {
207
- state: budgetstate;
208
- steps?: number;
209
- tokens?: number;
210
- durationms?: number;
211
- now: number;
212
- }): budgetstate {
213
- const steps = input.steps ?? 1;
214
- const tokens = input.tokens ?? 0;
215
- const durationms = input.durationms ?? 0;
216
- for (const [label, value] of [
217
- ["steps", steps],
218
- ["tokens", tokens],
219
- ["durationms", durationms],
220
- ] as Array<[string, number]>) {
221
- if (!Number.isFinite(value) || value < 0)
222
- throw new Error(
223
- `The ${label} spend stays a finite non negative number; the budget never recharges or takes a nonsensical amount.`,
224
- );
225
- }
226
- const nextsteps = input.state.spentsteps + steps;
227
- const nexttokens = input.state.spenttokens + tokens;
228
- const nextduration = input.state.spentdurationms + durationms;
229
- if (input.state.maxsteps !== undefined && nextsteps > input.state.maxsteps)
230
- throw new Error(
231
- `The agent ${input.state.agentid} already executed ${input.state.spentsteps} of its ${input.state.maxsteps} budgeted steps; the user raises the ceiling or stops the agent.`,
232
- );
233
- if (input.state.maxtokens !== undefined && nexttokens > input.state.maxtokens)
234
- throw new Error(
235
- `The agent ${input.state.agentid} already spent ${input.state.spenttokens} of its ${input.state.maxtokens} budgeted tokens; the user raises the ceiling or stops the agent.`,
236
- );
237
- if (input.state.maxdurationms !== undefined && nextduration > input.state.maxdurationms)
238
- throw new Error(
239
- `The agent ${input.state.agentid} already spent ${input.state.spentdurationms} of its ${input.state.maxdurationms} budgeted milliseconds; the user raises the ceiling or stops the agent.`,
240
- );
241
- return {
242
- ...input.state,
243
- spentsteps: nextsteps,
244
- spenttokens: nexttokens,
245
- spentdurationms: nextduration,
246
- updatedat: input.now,
247
- };
248
- }
249
-
250
- /** Reports the remaining budget of one agent: every ceiling the user configured reports its remaining value while an absent ceiling reports unbounded. */
251
- export function budgetremaining(state: budgetstate): {
252
- steps?: number;
253
- tokens?: number;
254
- durationms?: number;
255
- reason: string;
256
- } {
257
- const steps = state.maxsteps === undefined ? undefined : Math.max(0, state.maxsteps - state.spentsteps);
258
- const tokens = state.maxtokens === undefined ? undefined : Math.max(0, state.maxtokens - state.spenttokens);
259
- const durationms =
260
- state.maxdurationms === undefined ? undefined : Math.max(0, state.maxdurationms - state.spentdurationms);
261
- const parts: string[] = [];
262
- parts.push(steps === undefined ? "steps unbounded" : `${steps} of ${state.maxsteps} steps left`);
263
- parts.push(tokens === undefined ? "tokens unbounded" : `${tokens} of ${state.maxtokens} tokens left`);
264
- parts.push(
265
- durationms === undefined ? "duration unbounded" : `${durationms} of ${state.maxdurationms} milliseconds left`,
266
- );
267
- return {
268
- ...(steps !== undefined ? { steps } : {}),
269
- ...(tokens !== undefined ? { tokens } : {}),
270
- ...(durationms !== undefined ? { durationms } : {}),
271
- reason: `The agent ${state.agentid} spent ${state.spentsteps} steps, ${state.spenttokens} tokens and ${state.spentdurationms} milliseconds: ${parts.join(", ")}.`,
272
- };
273
- }
274
-
275
- /** Pauses one agent of the fleet without stopping its peers: the pause holds only the named agent while every other record and every other run record stays exactly as it was. */
276
- export function pauseagent(input: { records: agentrecord[]; agentid: string; now: number }): agentrecord[] {
277
- const agent = input.records.find((record) => record.id === input.agentid);
278
- if (!agent) throw new Error(`The agent ${input.agentid} is not registered in the fleet.`);
279
- if (agent.state === "stopped") throw new Error(`The agent ${agent.name} is stopped; a stopped agent needs no pause.`);
280
- return input.records.map((record) =>
281
- record.id === input.agentid ? { ...record, state: "paused" as const, lastseenat: input.now } : record,
282
- );
283
- }
284
-
285
- /** Resumes one paused agent of the fleet: the resume lifts only the named agent while the peers never changed. */
286
- export function unpauseagent(input: { records: agentrecord[]; agentid: string; now: number }): agentrecord[] {
287
- const agent = input.records.find((record) => record.id === input.agentid);
288
- if (!agent) throw new Error(`The agent ${input.agentid} is not registered in the fleet.`);
289
- if (agent.state !== "paused") throw new Error(`The agent ${agent.name} is not paused.`);
290
- return input.records.map((record) =>
291
- record.id === input.agentid ? { ...record, state: "active" as const, lastseenat: input.now } : record,
292
- );
293
- }
294
-
295
- /** Engages the global killswitch in one call: every fleet record stops, every agent run cancels, every queued item of every queue clears, and the return carries one stop entry per agent so the audit trail records each stopped agent. */
296
- export function engagekillswitch(input: {
297
- records: agentrecord[];
298
- runs: runrecord[];
299
- queues?: taskqueue[];
300
- reason?: string;
301
- now: number;
302
- }): {
303
- records: agentrecord[];
304
- runs: runrecord[];
305
- queues: taskqueue[];
306
- stopped: Array<{ agentid: string; name: string; runs: string[]; queued: number }>;
307
- engagedat: number;
308
- reason: string;
309
- } {
310
- const reason =
311
- input.reason !== undefined && input.reason.trim() !== "" ? input.reason.trim() : "The user engaged the killswitch.";
312
- const terminal: runstate[] = ["completed", "failed", "cancelled", "rolledback", "done"];
313
- const runs = input.runs.map((run) =>
314
- run.agentid !== undefined && !terminal.includes(run.state)
315
- ? { ...run, state: "cancelled" as runstate, updatedat: input.now }
316
- : run,
317
- );
318
- const cancelled = runs.filter(
319
- (run) =>
320
- run.agentid !== undefined &&
321
- run.state === "cancelled" &&
322
- input.runs.find((original) => original.runid === run.runid)?.state !== "cancelled",
323
- );
324
- const queues = (input.queues ?? []).map((queue) => ({ ...queue, items: [], claims: [] }));
325
- const queuedperagent = new Map<string, number>();
326
- for (const queue of input.queues ?? []) {
327
- for (const claim of queue.claims) queuedperagent.set(claim.agentid, (queuedperagent.get(claim.agentid) ?? 0) + 1);
328
- for (const item of queue.items)
329
- if (item.state === "queued") queuedperagent.set("fleet", (queuedperagent.get("fleet") ?? 0) + 1);
330
- }
331
- const stopped = input.records.map((record) => ({
332
- agentid: record.id,
333
- name: record.name,
334
- runs: cancelled.filter((run) => run.agentid === record.id).map((run) => run.runid),
335
- queued:
336
- (queuedperagent.get(record.id) ?? 0) +
337
- (record.state === "active" || record.state === "paused" ? (queuedperagent.get("fleet") ?? 0) : 0),
338
- }));
339
- return {
340
- records: input.records.map((record) => ({ ...record, state: "stopped" as const, lastseenat: input.now })),
341
- runs,
342
- queues,
343
- stopped,
344
- engagedat: input.now,
345
- reason,
346
- };
347
- }
348
-
349
- /** Reads the fleet at a glance for the sidepanel and the protocol envelope: the records with their control states, the budget states, the open escalations and reviews and the timestamp of the last killswitch stop. */
350
- export function fleetoverview(input: {
351
- records: agentrecord[];
352
- budgets: budgetstate[];
353
- openescalations: number;
354
- openreviews: number;
355
- killswitchat?: number;
356
- }): {
357
- agents: number;
358
- active: number;
359
- paused: number;
360
- stopped: number;
361
- openescalations: number;
362
- openreviews: number;
363
- lastkillswitchat?: number;
364
- } {
365
- return {
366
- agents: input.records.length,
367
- active: input.records.filter((record) => record.state === "active").length,
368
- paused: input.records.filter((record) => record.state === "paused").length,
369
- stopped: input.records.filter((record) => record.state === "stopped").length,
370
- openescalations: input.openescalations,
371
- openreviews: input.openreviews,
372
- ...(input.killswitchat !== undefined ? { lastkillswitchat: input.killswitchat } : {}),
373
- };
374
- }
375
-
376
- /* ── Merged from agentmailbox.ts: the 1.1.88 consolidation interns the correlated agentmailbox logic here, so no variation of the same file lives beside another. ── */
377
-
378
- /**
379
- * Agent mailbox logic of the 1.1.58 multi agent family.
380
- * Every correlated rule for the direct, broadcast and role addressed routing, the inbox and outbox records, the unread counters and the receive drain with ack tracking lives in this file.
381
- * The messages stay inside the swarm: a message that carries page content grades as an egress event in the audit trail, and no mailbox delivery ever bypasses a consent gate.
382
- */
383
-
384
- /** The broadcast recipient marker: a message addressed to it reaches every agent of the swarm but the sender. */
385
- export const broadcastrecipient = "*";
386
-
387
- /** Returns the mailbox of one agent; an agent without a mailbox yet reads an empty one. */
388
- export function mailboxof(mailboxes: agentmailbox[], agentid: string): agentmailbox {
389
- return mailboxes.find((mailbox) => mailbox.agentid === agentid) ?? { agentid, inbox: [], outbox: [], unread: 0 };
390
- }
391
-
392
- /** Returns every agent id of one role for the role addressed routing. */
393
- export function roleaddress(agents: agentidentity[], role: string): string[] {
394
- return agents.filter((agent) => agent.role === role && agent.state !== "stopped").map((agent) => agent.id);
395
- }
396
-
397
- /** Resolves the recipients of one message: a direct message names one agent, a broadcast reaches every live agent but the sender and a role message reaches every live agent of the role. */
398
- export function resolverecipients(input: {
399
- agents: agentidentity[];
400
- senderid: string;
401
- recipient: string;
402
- routing: messagerouting;
403
- }): string[] {
404
- if (input.routing === "broadcast")
405
- return input.agents
406
- .filter((agent) => agent.id !== input.senderid && agent.state !== "stopped")
407
- .map((agent) => agent.id);
408
- if (input.routing === "role") return roleaddress(input.agents, input.recipient);
409
- const direct = input.agents.find((agent) => agent.id === input.recipient);
410
- if (!direct) throw new Error(`The direct message names the recipient ${input.recipient} which is not registered.`);
411
- return [direct.id];
412
- }
413
-
414
- /** Delivers one message to the recipient inboxes: the sender outbox records it, the unread counters rise and the delivery stays visible to every reader of the swarm. */
415
- export function sendmessage(input: {
416
- mailboxes: agentmailbox[];
417
- agents: agentidentity[];
418
- id: string;
419
- senderid: string;
420
- recipient: string;
421
- routing: messagerouting;
422
- payload: string;
423
- now: number;
424
- }): agentmailbox[] {
425
- if (input.payload.trim() === "") throw new Error("The agent message needs its payload.");
426
- if (input.routing === "direct" && input.senderid === input.recipient)
427
- throw new Error("A direct message never addresses its own sender.");
428
- const message: agentmessage = {
429
- id: input.id,
430
- senderid: input.senderid,
431
- recipient: input.recipient,
432
- routing: input.routing,
433
- payload: input.payload,
434
- sentat: input.now,
435
- };
436
- const recipients = resolverecipients({
437
- agents: input.agents,
438
- senderid: input.senderid,
439
- recipient: input.recipient,
440
- routing: input.routing,
441
- });
442
- const known = new Set([
443
- ...input.mailboxes.map((mailbox) => mailbox.agentid),
444
- ...recipients,
445
- ...(input.agents.some((agent) => agent.id === input.senderid) ? [input.senderid] : []),
446
- ]);
447
- return [...known].map((agentid) => {
448
- const mailbox = mailboxof(input.mailboxes, agentid);
449
- const delivered = recipients.includes(agentid);
450
- const sent = agentid === input.senderid;
451
- return {
452
- agentid,
453
- inbox: delivered ? [message, ...mailbox.inbox] : mailbox.inbox,
454
- outbox: sent ? [message, ...mailbox.outbox] : mailbox.outbox,
455
- unread: delivered ? mailbox.unread + 1 : mailbox.unread,
456
- };
457
- });
458
- }
459
-
460
- /** Drains one inbox with ack tracking: every message receives its read time, the unread counter resets and the drain returns the read messages oldest first. */
461
- export function receivemessages(input: { mailboxes: agentmailbox[]; agentid: string; now: number }): {
462
- mailboxes: agentmailbox[];
463
- messages: agentmessage[];
464
- } {
465
- const mailbox = mailboxof(input.mailboxes, input.agentid);
466
- const messages = [...mailbox.inbox]
467
- .sort((one, two) => one.sentat - two.sentat)
468
- .map((message) => ({ ...message, ...(message.readat === undefined ? { readat: input.now } : {}) }));
469
- const drained: agentmailbox = { agentid: input.agentid, inbox: messages, outbox: mailbox.outbox, unread: 0 };
470
- return { mailboxes: input.mailboxes.map((entry) => (entry.agentid === input.agentid ? drained : entry)), messages };
471
- }
472
-
473
- /** Returns the unread count of one agent mailbox. */
474
- export function unreadcount(mailboxes: agentmailbox[], agentid: string): number {
475
- return mailboxof(mailboxes, agentid).unread;
476
- }
477
-
478
- /* ── Merged from agentreview.ts: the 1.1.88 consolidation interns the correlated agentreview logic here, so no variation of the same file lives beside another. ── */
479
-
480
- /**
481
- * Agent review logic of the 1.1.72 family.
482
- * Every correlated rule for the human escalation hold that blocks the raising agent until the user answers, the agent to agent review requests with the reviewer verdict recorded beside the original output, the runreplay capture of the ordered steps and results of one agent run with its reconstruction from the audit trail when the run memory is gone, the field by field outputcompare of two competing agent outputs and the consensusvote with one vote per agent, the user configured quorum rule and the dissenting votes kept for the audit trail lives in this file.
483
- * The review layer never touches the page and never bypasses a gate: an escalation stays human decided, a review request stays between two agents of the shared origin, a replay reads the records only and every vote counts exactly once.
484
- */
485
-
486
- /** Reads the escalation hold of one agent: an open escalation blocks its raising agent until the human answers, so the run of a blocked agent waits instead of guessing. */
487
- export function escalationblock(input: { escalations: escalationrecord[]; agentid: string }): {
488
- blocked: boolean;
489
- open: number;
490
- reason: string;
491
- } {
492
- const open = input.escalations.filter(
493
- (escalation) => escalation.agentid === input.agentid && escalation.state === "open",
494
- );
495
- if (open.length === 0)
496
- return {
497
- blocked: false,
498
- open: 0,
499
- reason: `The agent ${input.agentid} carries no open escalation; its steps continue through the same review.`,
500
- };
501
- return {
502
- blocked: true,
503
- open: open.length,
504
- reason: `The agent ${input.agentid} waits behind ${open.length} open escalation${open.length === 1 ? "" : "s"}: ${open.map((escalation) => escalation.subject).join("; ")}; only the human answer lifts the hold.`,
505
- };
506
- }
507
-
508
- /** Sends one agent output to a peer agent for review: the review record carries the original output exactly as produced and waits open until the reviewing agent records its verdict. */
509
- export function reviewrecordof(input: {
510
- id: string;
511
- fromagentid: string;
512
- toagentid: string;
513
- subject: string;
514
- output: string;
515
- now: number;
516
- }): reviewrecord {
517
- if (input.id.trim() === "") throw new Error("The review request needs its id.");
518
- if (input.fromagentid.trim() === "") throw new Error("The review request names the agent whose output it carries.");
519
- if (input.toagentid.trim() === "") throw new Error("The review request names the reviewing agent.");
520
- if (input.fromagentid === input.toagentid)
521
- throw new Error(
522
- "A review request moves one output between two different agents; an agent never reviews its own output.",
523
- );
524
- if (input.subject.trim() === "") throw new Error("The review request needs its subject in plain language.");
525
- if (input.output.trim() === "")
526
- throw new Error("The review request needs the original output under review; the verdict lands beside it.");
527
- return {
528
- id: input.id.trim(),
529
- fromagentid: input.fromagentid.trim(),
530
- toagentid: input.toagentid.trim(),
531
- subject: input.subject.trim(),
532
- output: input.output,
533
- state: "open",
534
- requestedat: input.now,
535
- };
536
- }
537
-
538
- /** Records the reviewer verdict beside the original output: the answered review keeps the verdict, the issues the reviewer named and the answer time while the original output stays untouched. */
539
- export function recordverdict(input: {
540
- records: reviewrecord[];
541
- id: string;
542
- reviewerid: string;
543
- verdict: reviewverdict;
544
- issues?: string[];
545
- now: number;
546
- }): reviewrecord {
547
- const record = input.records.find((entry) => entry.id === input.id);
548
- if (!record) throw new Error(`The review request ${input.id} does not exist.`);
549
- if (record.state === "answered")
550
- throw new Error(`The review request ${record.id} already carries its verdict; the verdict never rewrites.`);
551
- if (input.reviewerid.trim() === "") throw new Error("The verdict names the reviewing agent that recorded it.");
552
- if (input.reviewerid !== record.toagentid)
553
- throw new Error(
554
- `The review request ${record.id} waits for the agent ${record.toagentid}; the agent ${input.reviewerid} never answers a review addressed to another agent.`,
555
- );
556
- const issues = (input.issues ?? []).map((issue) => issue.trim()).filter((issue) => issue !== "");
557
- if (input.verdict !== "approve" && issues.length === 0)
558
- throw new Error(
559
- `A ${input.verdict} verdict names its issues in plain language; the requesting agent reads exactly what failed the review.`,
560
- );
561
- return {
562
- ...record,
563
- state: "answered",
564
- verdict: input.verdict,
565
- ...(issues.length > 0 ? { issues } : {}),
566
- answeredat: input.now,
567
- };
568
- }
569
-
570
- /** Captures one runreplay of an agent run: the ordered steps with their results and times freeze into one replay record the audit trail keeps per agent. */
571
- export function runreplay(input: {
572
- id: string;
573
- agentid: string;
574
- runid: string;
575
- steps: Array<{ stepid: string; kind: string; summary: string; state: string; at: number }>;
576
- now: number;
577
- }): replayrecord {
578
- if (input.id.trim() === "") throw new Error("The runreplay capture needs its id.");
579
- if (input.agentid.trim() === "") throw new Error("The runreplay capture names its agent.");
580
- if (input.runid.trim() === "") throw new Error("The runreplay capture names its run.");
581
- if (input.steps.length === 0)
582
- throw new Error("The runreplay capture needs at least one recorded step; an empty run replays nothing.");
583
- const steps = [...input.steps].sort((one, two) => one.at - two.at || (one.stepid < two.stepid ? -1 : 1));
584
- return {
585
- id: input.id.trim(),
586
- agentid: input.agentid.trim(),
587
- runid: input.runid.trim(),
588
- steps,
589
- capturedat: input.now,
590
- };
591
- }
592
-
593
- /** Reconstructs one runreplay from the audit trail when the run memory is gone: the recorded events of the agent and its run order into the same replay shape a live capture produced, and the reconstruction marks itself. */
594
- export function reconstructreplay(input: {
595
- id: string;
596
- agentid: string;
597
- runid: string;
598
- events: Array<{
599
- id: string;
600
- kind: string;
601
- summary: string;
602
- at: number;
603
- agentid?: string;
604
- stepid?: string;
605
- planid?: string;
606
- }>;
607
- now: number;
608
- }): replayrecord {
609
- const owned = input.events.filter(
610
- (event) => event.agentid === input.agentid && (event.planid === undefined || event.planid === input.runid),
611
- );
612
- if (owned.length === 0)
613
- throw new Error(
614
- `The audit trail carries no event of the agent ${input.agentid} for the run ${input.runid}; the reconstruction replays only what the trail recorded.`,
615
- );
616
- const steps = owned
617
- .filter((event) => event.stepid !== undefined)
618
- .map((event) => ({
619
- stepid: event.stepid as string,
620
- kind: event.kind,
621
- summary: event.summary,
622
- state:
623
- event.kind === "action" || event.kind === "complete" ? "done" : event.kind === "error" ? "failed" : "recorded",
624
- at: event.at,
625
- }))
626
- .sort((one, two) => one.at - two.at || (one.stepid < two.stepid ? -1 : 1));
627
- if (steps.length === 0)
628
- throw new Error(
629
- `The audit trail of the agent ${input.agentid} names no step of the run ${input.runid}; the reconstruction needs the step ids the trail carried.`,
630
- );
631
- return {
632
- id: input.id.trim(),
633
- agentid: input.agentid.trim(),
634
- runid: input.runid.trim(),
635
- steps,
636
- reconstructed: true,
637
- capturedat: input.now,
638
- };
639
- }
640
-
641
- /** Aligns two competing agent outputs field by field: every field of both sides grades matching when the values agree, conflicting when they differ and missing when one side carries no value, so the user reads exactly where the agents disagree. */
642
- export function outputcompare(input: {
643
- id: string;
644
- subject: string;
645
- left: { agentid: string; fields: Record<string, string> };
646
- right: { agentid: string; fields: Record<string, string> };
647
- now: number;
648
- }): comparisonrecord {
649
- if (input.id.trim() === "") throw new Error("The output comparison needs its id.");
650
- if (input.subject.trim() === "") throw new Error("The output comparison needs its subject in plain language.");
651
- if (input.left.agentid.trim() === "" || input.right.agentid.trim() === "")
652
- throw new Error("The output comparison names both agents of its competing outputs.");
653
- if (input.left.agentid === input.right.agentid)
654
- throw new Error("The output comparison contrasts two different agents; one agent never competes with itself.");
655
- const keys = [...new Set([...Object.keys(input.left.fields), ...Object.keys(input.right.fields)])].sort();
656
- const matching: string[] = [];
657
- const conflicting: string[] = [];
658
- const missing: string[] = [];
659
- for (const key of keys) {
660
- const left = input.left.fields[key];
661
- const right = input.right.fields[key];
662
- if (left !== undefined && right !== undefined) (left === right ? matching : conflicting).push(key);
663
- else missing.push(key);
664
- }
665
- return {
666
- id: input.id.trim(),
667
- subject: input.subject.trim(),
668
- left: { agentid: input.left.agentid.trim(), fields: input.left.fields },
669
- right: { agentid: input.right.agentid.trim(), fields: input.right.fields },
670
- matching,
671
- conflicting,
672
- missing,
673
- comparedat: input.now,
674
- };
675
- }
676
-
677
- /** Collects one vote per agent on one proposal and computes the outcome by the user configured quorum rule: duplicate voters refuse, yes votes carry the proposal at the quorum, a complete round under the quorum fails it, an incomplete round stays open and every dissenting vote stays in the record for the audit trail. */
678
- export function consensusrecordof(input: {
679
- id: string;
680
- proposal: string;
681
- votes: Array<{ agentid: string; vote: consensusvote; reason?: string; castat: number }>;
682
- quorum: number;
683
- voters?: number;
684
- now: number;
685
- }): consensusrecord {
686
- if (input.id.trim() === "") throw new Error("The consensus record needs its id.");
687
- if (input.proposal.trim() === "") throw new Error("The consensus record needs its proposal in plain language.");
688
- if (!Number.isInteger(input.quorum) || input.quorum < 1)
689
- throw new Error("The consensus quorum stays a positive whole number the user configured.");
690
- const seen = new Set<string>();
691
- const votes: consensusrecord["votes"] = [];
692
- for (const vote of input.votes) {
693
- if (vote.agentid.trim() === "") throw new Error("Every vote names its agent; the weight stays one vote per agent.");
694
- if (seen.has(vote.agentid))
695
- throw new Error(`The agent ${vote.agentid} already voted; the vote weight stays one per agent.`);
696
- seen.add(vote.agentid);
697
- votes.push({
698
- agentid: vote.agentid.trim(),
699
- vote: vote.vote,
700
- ...(vote.reason !== undefined && vote.reason.trim() !== "" ? { reason: vote.reason.trim() } : {}),
701
- castat: vote.castat,
702
- });
703
- }
704
- const tally = {
705
- yes: votes.filter((vote) => vote.vote === "yes").length,
706
- no: votes.filter((vote) => vote.vote === "no").length,
707
- abstain: votes.filter((vote) => vote.vote === "abstain").length,
708
- };
709
- const complete = input.voters !== undefined && votes.length >= input.voters;
710
- const outcome = tally.yes >= input.quorum ? "carried" : complete ? "failed" : "open";
711
- return {
712
- id: input.id.trim(),
713
- proposal: input.proposal.trim(),
714
- votes,
715
- tally,
716
- quorum: input.quorum,
717
- outcome,
718
- ...(outcome !== "open" ? { closedat: input.now } : {}),
719
- };
720
- }
721
-
722
- /* ── Merged from agentstream.ts: the 1.1.88 consolidation interns the correlated agentstream logic here, so no variation of the same file lives beside another. ── */
723
- import { randomid } from "./memory.js";
724
-
725
- /**
726
- * Agent stream of the 1.1.56 agent protocol part three.
727
- * Every subscription, resource, sampling, prompt, streaming, progress and cancellation concern lives in this file: the event subscriptions with their kinds and filters, the event notifications that push protocol events to matching subscribers, the resource watchers with their page state baselines and the page state deltas they deliver, the sampling callbacks that ask a client model for a completion behind its declared capabilities and the page content grant, the prompt defs exposed as callable tools with their template rendering, the stream chunks of progressive tool results, the progress notices of long tools with their cancel hints and the cancellation frames that abort an in flight tool call while preserving its partial result.
728
- * The stream stays pure and consent-first: a subscription never widens what the session grants, a sampling prompt never leaves the browser with ungranted page content and a cancellation never bypasses the audit trail.
729
- */
730
-
731
- /** The protocol event kinds a client may subscribe to; the callstarted kind mirrors every tool call including calls with side effects. */
732
- export const protocoleventkinds: eventkind[] = [
733
- "callstarted",
734
- "callresult",
735
- "streamchunk",
736
- "progress",
737
- "resourcedelta",
738
- "sampling",
739
- "cancellation",
740
- ];
741
-
742
- /** The observation event kinds that stay read only: every kind except the callstarted mutation mirror. */
743
- export const readonlyeventkinds: eventkind[] = [
744
- "callresult",
745
- "streamchunk",
746
- "progress",
747
- "resourcedelta",
748
- "sampling",
749
- "cancellation",
750
- ];
751
-
752
- /** Registers one client event subscription: the kinds validate against the protocol event kinds while an absent kinds list subscribes the read only observation kinds so a subscription never widens what the session grants. */
753
- export function subscriberegister(input: {
754
- clientid: string;
755
- kinds?: string[];
756
- origin?: string;
757
- tool?: string;
758
- now: number;
759
- id?: string;
760
- }): { subscription?: protocoleventsubscription; reason?: string } {
761
- if (input.clientid.trim() === "") return { reason: "The event subscription needs the paired client it belongs to." };
762
- const kinds =
763
- input.kinds === undefined || input.kinds.length === 0 ? [...readonlyeventkinds] : [...new Set(input.kinds)];
764
- for (const kind of kinds) {
765
- if (!protocoleventkinds.includes(kind as eventkind))
766
- return { reason: `The event kind ${kind} is not a protocol event kind.` };
767
- }
768
- if (input.origin !== undefined && input.origin.trim() === "")
769
- return { reason: "The origin filter of an event subscription must name an origin or stay absent." };
770
- if (input.tool !== undefined && input.tool.trim() === "")
771
- return { reason: "The tool filter of an event subscription must name a tool or stay absent." };
772
- return {
773
- subscription: {
774
- id: input.id ?? randomid(),
775
- clientid: input.clientid,
776
- kinds: kinds as eventkind[],
777
- ...(input.origin !== undefined ? { origin: input.origin } : {}),
778
- ...(input.tool !== undefined ? { tool: input.tool } : {}),
779
- createdat: input.now,
780
- },
781
- };
782
- }
783
-
784
- /** Cancels one client event subscription; the record stays for the audit trail with its cancel time. */
785
- export function unsubscriberegister(
786
- subscriptions: protocoleventsubscription[],
787
- id: string,
788
- now: number,
789
- ): protocoleventsubscription[] {
790
- return subscriptions.map((subscription) =>
791
- subscription.id === id && subscription.canceledat === undefined
792
- ? { ...subscription, canceledat: now }
793
- : subscription,
794
- );
795
- }
796
-
797
- /** Pushes one protocol event to every matching subscriber: active subscriptions whose kinds carry the event kind, whose origin filter matches when set and whose tool filter matches when set receive one notification frame; the delivery stamps the last delivery time. */
798
- export function notifyevent(input: {
799
- subscriptions: protocoleventsubscription[];
800
- kind: eventkind;
801
- origin?: string;
802
- tool?: string;
803
- payload?: Record<string, unknown>;
804
- now: number;
805
- }): {
806
- deliveries: Array<{ subscriptionid: string; clientid: string; frame: jsonrpcframe }>;
807
- subscriptions: protocoleventsubscription[];
808
- } {
809
- const deliveries: Array<{ subscriptionid: string; clientid: string; frame: jsonrpcframe }> = [];
810
- const subscriptions = input.subscriptions.map((subscription) => {
811
- if (subscription.canceledat !== undefined) return subscription;
812
- if (!subscription.kinds.includes(input.kind)) return subscription;
813
- if (subscription.origin !== undefined && input.origin !== undefined && subscription.origin !== input.origin)
814
- return subscription;
815
- if (subscription.tool !== undefined && input.tool !== undefined && subscription.tool !== input.tool)
816
- return subscription;
817
- deliveries.push({
818
- subscriptionid: subscription.id,
819
- clientid: subscription.clientid,
820
- frame: {
821
- jsonrpc: "2.0",
822
- method: "events/notify",
823
- params: {
824
- subscriptionid: subscription.id,
825
- kind: input.kind,
826
- ...(input.origin !== undefined ? { origin: input.origin } : {}),
827
- ...(input.tool !== undefined ? { tool: input.tool } : {}),
828
- ...(input.payload !== undefined ? { payload: input.payload } : {}),
829
- at: input.now,
830
- },
831
- },
832
- });
833
- return { ...subscription, lastdeliveredat: input.now };
834
- });
835
- return { deliveries, subscriptions };
836
- }
837
-
838
- /** Starts one page state watcher for a client: the resource names the watched page state and the baseline freezes the page state at watch time so later deltas compare against it. */
839
- export function watchresource(input: {
840
- clientid: string;
841
- resource: string;
842
- state?: Record<string, unknown>;
843
- now: number;
844
- id?: string;
845
- }): { watch?: resourcewatch; reason?: string } {
846
- if (input.clientid.trim() === "") return { reason: "The resource watcher needs the paired client it belongs to." };
847
- if (input.resource.trim() === "") return { reason: "The resource watcher needs the page state resource it watches." };
848
- return {
849
- watch: {
850
- id: input.id ?? randomid(),
851
- clientid: input.clientid,
852
- resource: input.resource,
853
- baseline: input.state ?? {},
854
- createdat: input.now,
855
- },
856
- };
857
- }
858
-
859
- /** Cancels one page state watcher; the record stays for the audit trail with its cancel time. */
860
- export function unwatchresource(watches: resourcewatch[], id: string, now: number): resourcewatch[] {
861
- return watches.map((watch) =>
862
- watch.id === id && watch.canceledat === undefined ? { ...watch, canceledat: now } : watch,
863
- );
864
- }
865
-
866
- /** Pushes the page state deltas of one resource to its active watchers: every changed key against the baseline lands in the delta, the baseline absorbs the new state and the delivery stamps the last delivery time. */
867
- export function notifyresource(input: {
868
- watches: resourcewatch[];
869
- resource: string;
870
- state: Record<string, unknown>;
871
- now: number;
872
- }): {
873
- deliveries: Array<{ watchid: string; clientid: string; delta: Record<string, unknown> }>;
874
- watches: resourcewatch[];
875
- } {
876
- const deliveries: Array<{ watchid: string; clientid: string; delta: Record<string, unknown> }> = [];
877
- const watches = input.watches.map((watch) => {
878
- if (watch.canceledat !== undefined || watch.resource !== input.resource) return watch;
879
- const delta: Record<string, unknown> = {};
880
- for (const [key, value] of Object.entries(input.state)) {
881
- if (!(key in watch.baseline) || watch.baseline[key] !== value) delta[key] = value;
882
- }
883
- if (Object.keys(delta).length === 0) return watch;
884
- deliveries.push({ watchid: watch.id, clientid: watch.clientid, delta });
885
- return { ...watch, baseline: { ...input.state }, lastdeliveredat: input.now };
886
- });
887
- return { deliveries, watches };
888
- }
889
-
890
- /** Sends one sampling callback to a client model: the client must have declared its sampling capability, the page content stays stripped unless the user granted it and the request records its provenance for the round trip. */
891
- export function requestsampling(input: {
892
- clientid: string;
893
- capabilities?: capabilityset;
894
- prompt: string;
895
- system?: string;
896
- pagecontent?: string;
897
- pagegrant?: boolean;
898
- maxtokens?: number;
899
- now: number;
900
- id?: string;
901
- }): { request?: samplingrequest; reason?: string } {
902
- if (input.clientid.trim() === "") return { reason: "The sampling callback needs the paired client it addresses." };
903
- if (input.prompt.trim() === "") return { reason: "The sampling callback needs its prompt." };
904
- if (input.capabilities?.sampling === false)
905
- return { reason: "The client declared no sampling capability and the callback is refused." };
906
- if (input.maxtokens !== undefined && (!Number.isFinite(input.maxtokens) || input.maxtokens <= 0))
907
- return { reason: "The granted maximum tokens of a sampling callback must stay a positive user value." };
908
- const granted = input.pagegrant === true;
909
- const pagecontent = granted ? input.pagecontent : undefined;
910
- const prompt =
911
- granted || input.pagecontent === undefined
912
- ? input.prompt
913
- : `${input.prompt}\nThe page content stays stripped because the user granted none.`;
914
- return {
915
- request: {
916
- id: input.id ?? randomid(),
917
- clientid: input.clientid,
918
- prompt,
919
- ...(input.system !== undefined ? { system: input.system } : {}),
920
- ...(pagecontent !== undefined ? { pagecontent } : {}),
921
- ...(input.maxtokens !== undefined ? { maxtokens: input.maxtokens } : {}),
922
- state: "pending",
923
- requestedat: input.now,
924
- },
925
- };
926
- }
927
-
928
- /** Completes one sampling round trip: the client answer closes the pending request with its provenance times while answered, refused and unknown requests stay untouched. */
929
- export function answersampling(input: {
930
- requests: samplingrequest[];
931
- id: string;
932
- answer?: string;
933
- refused?: boolean;
934
- now: number;
935
- }): { requests: samplingrequest[]; request?: samplingrequest; reason?: string } {
936
- const match = input.requests.find((request) => request.id === input.id);
937
- if (match === undefined) return { requests: input.requests, reason: "The sampling answer names no stored request." };
938
- if (match.state !== "pending")
939
- return { requests: input.requests, reason: "The sampling request already closed its round trip." };
940
- const request: samplingrequest = {
941
- ...match,
942
- state: input.refused === true ? "refused" : "answered",
943
- answeredat: input.now,
944
- ...(input.refused !== true && input.answer !== undefined ? { answer: input.answer } : {}),
945
- };
946
- return { requests: input.requests.map((candidate) => (candidate.id === input.id ? request : candidate)), request };
947
- }
948
-
949
- /** Lists the prompt defs the server exposes as callable tools: every prompt carries its name, description, declared arguments and render template. */
950
- export function listprompts(): promptdef[] {
951
- return [
952
- {
953
- name: "runreview",
954
- description:
955
- "Renders the run review prompt that asks the user model to summarize the executed steps of the approved plan behind the consent gates.",
956
- arguments: [
957
- { name: "objective", description: "The objective of the approved plan under review.", required: true },
958
- { name: "steps", description: "The executed step summaries the review covers.", required: true },
959
- { name: "tone", description: "The tone of the summary.", default: "plain" },
960
- ],
961
- template:
962
- "Review the run of the objective {{objective}}. Summarize the executed steps: {{steps}}. Keep the tone {{tone}} and state every refusal the consent gates raised.",
963
- },
964
- {
965
- name: "pagesummary",
966
- description:
967
- "Renders the page summary prompt that condenses the observed page state of the session tab into the summary the client model asked for.",
968
- arguments: [
969
- { name: "url", description: "The url of the observed page.", required: true },
970
- {
971
- name: "observations",
972
- description: "The observed page state sections the summary condenses.",
973
- required: true,
974
- },
975
- ],
976
- template:
977
- "Summarize the page at {{url}} from the observations: {{observations}}. Name nothing the observations leave out.",
978
- },
979
- {
980
- name: "failuretriage",
981
- description:
982
- "Renders the failure triage prompt that classifies a failed tool call through the retry hints of the structured error it produced.",
983
- arguments: [
984
- { name: "tool", description: "The namespaced tool that failed.", required: true },
985
- { name: "error", description: "The structured error of the failed call.", required: true },
986
- ],
987
- template:
988
- "Triage the failure of the {{tool}} tool: {{error}}. Classify it as retryable, a busy window or a consent refusal and propose the next reviewed step.",
989
- },
990
- ];
991
- }
992
-
993
- /** Renders one prompt def with its arguments: every double braced placeholder substitutes its argument value while a required argument left empty keeps its placeholder so the review sees the gap. */
994
- export function renderprompt(prompt: promptdef, args: Record<string, unknown>): string {
995
- return prompt.template.replace(/\{\{\s*([a-z0-9]+)\s*\}\}/g, (whole, name: string) => {
996
- const value = args[name];
997
- if (value === undefined || value === null) return whole;
998
- return typeof value === "string" ? value : JSON.stringify(value);
999
- });
1000
- }
1001
-
1002
- /** Calls one prompt: the render validates the declared arguments, substitutes the defaults the prompt reviewed and returns the rendered prompt with its arguments as one tool call the client dispatches behind the consent gates. */
1003
- export function callprompt(input: { name: string; args?: Record<string, unknown>; now?: number }): {
1004
- toolcall?: { name: string; params: Record<string, unknown> };
1005
- rendered?: string;
1006
- reason?: string;
1007
- } {
1008
- const prompt = listprompts().find((candidate) => candidate.name === input.name);
1009
- if (prompt === undefined) return { reason: `The server exposes no prompt named ${input.name}.` };
1010
- const args = input.args ?? {};
1011
- const findings: string[] = [];
1012
- const resolved: Record<string, unknown> = {};
1013
- for (const argument of prompt.arguments) {
1014
- const value = args[argument.name];
1015
- if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) {
1016
- if (argument.default !== undefined) resolved[argument.name] = argument.default;
1017
- else if (argument.required === true)
1018
- findings.push(`The prompt argument ${argument.name} is required and stays empty.`);
1019
- else resolved[argument.name] = "";
1020
- } else {
1021
- resolved[argument.name] = value;
1022
- }
1023
- }
1024
- if (findings.length > 0) return { reason: findings.join(" ") };
1025
- return {
1026
- rendered: renderprompt(prompt, resolved),
1027
- toolcall: {
1028
- name: `prompts.${prompt.name}`,
1029
- params: { prompt: prompt.name, arguments: resolved, rendered: renderprompt(prompt, resolved) },
1030
- },
1031
- };
1032
- }
1033
-
1034
- /** Emits one stream chunk of a progressive tool result: the sequence number orders the chunks while the done marker closes the stream. */
1035
- export function streamchunkof(input: {
1036
- callid: string;
1037
- seq: number;
1038
- content: string;
1039
- done?: boolean;
1040
- now: number;
1041
- }): streamchunk {
1042
- return { callid: input.callid, seq: input.seq, content: input.content, done: input.done === true, at: input.now };
1043
- }
1044
-
1045
- /** Splits one tool result content into ordered stream chunks; the final chunk carries the done marker so the client reassembles the progressive result. */
1046
- export function chunkcontent(input: { callid: string; content: string; size?: number; now: number }): streamchunk[] {
1047
- const size = input.size !== undefined && Number.isFinite(input.size) && input.size > 0 ? Math.floor(input.size) : 80;
1048
- const parts: string[] = [];
1049
- for (let index = 0; index < input.content.length; index += size) parts.push(input.content.slice(index, index + size));
1050
- const slices = parts.length > 0 ? parts : [""];
1051
- return slices.map((content, index) =>
1052
- streamchunkof({
1053
- callid: input.callid,
1054
- seq: index + 1,
1055
- content,
1056
- done: index === slices.length - 1,
1057
- now: input.now + index,
1058
- }),
1059
- );
1060
- }
1061
-
1062
- /** Reassembles ordered stream chunks into the progressive result content they carry. */
1063
- export function assemblechunks(chunks: streamchunk[]): string {
1064
- return [...chunks]
1065
- .sort((one, two) => one.seq - two.seq)
1066
- .map((chunk) => chunk.content)
1067
- .join("");
1068
- }
1069
-
1070
- /** Emits one progress notice of a long tool call: the percent, the plain language message and the cancel hint the panel renders. */
1071
- export function notifyprogress(input: {
1072
- callid: string;
1073
- percent?: number;
1074
- message: string;
1075
- cancellable?: boolean;
1076
- now: number;
1077
- }): progressnotice {
1078
- return {
1079
- callid: input.callid,
1080
- ...(input.percent !== undefined ? { percent: input.percent } : {}),
1081
- message: input.message,
1082
- cancellable: input.cancellable !== false,
1083
- at: input.now,
1084
- };
1085
- }
1086
-
1087
- /** Builds one cancellation frame that aborts an in flight tool call: the call id and the reason in plain language. */
1088
- export function cancelframeof(input: { callid: string; reason?: string; now: number }): cancelframe {
1089
- return { callid: input.callid, ...(input.reason !== undefined ? { reason: input.reason } : {}), at: input.now };
1090
- }
1091
-
1092
- /** Aborts one in flight tool call on its cancellation frame: the call context turns cancelled with its end time while the partial result survives for the answer; a finished or unknown call reports why nothing aborted. */
1093
- export function canceltool(input: {
1094
- contexts: callcontext[];
1095
- callid: string;
1096
- reason?: string;
1097
- partial?: callcontext["partial"];
1098
- now: number;
1099
- }): { contexts: callcontext[]; context?: callcontext; reason?: string } {
1100
- const match = input.contexts.find((context) => context.callid === input.callid);
1101
- if (match === undefined)
1102
- return { contexts: input.contexts, reason: `The cancellation frame names no call context ${input.callid}.` };
1103
- if (match.state !== "inflight")
1104
- return { contexts: input.contexts, reason: `The call ${input.callid} already left the in flight state.` };
1105
- const context: callcontext = {
1106
- ...match,
1107
- state: "cancelled",
1108
- endedat: input.now,
1109
- ...(input.partial !== undefined ? { partial: input.partial } : {}),
1110
- };
1111
- return {
1112
- contexts: input.contexts.map((candidate) => (candidate.callid === input.callid ? context : candidate)),
1113
- context,
1114
- };
1115
- }
1116
-
1117
- /* ── Merged from agentwork.ts: the 1.1.88 consolidation interns the correlated agentwork logic here, so no variation of the same file lives beside another. ── */
1118
-
1119
- /**
1120
- * Agent work logic of the 1.1.73 family.
1121
- * Every correlated rule for the on demand sub agent spawning under the user configured depth limit with the parent scope copied into the child unless narrowed, the lineage depth computed from the spawn records, the aggregatereport that merges parallel agent outputs into one document with per agent sections, provenance and conflicts resolved by policy order or sent to escalation, the interleaved timeline that orders the actions of every agent into one merged view with the per agent lanes kept visible, the lessonshare that records reusable lessons of finished runs, serves the matching lessons to agents starting similar tasks and counts reuse while stale lessons decay, the arbitrate cases that open when two agents contest one resource and grant it to the first requester or the priority lane winner and release when the holder finishes, the prioritylane ranking that keeps sensitive steps in the interactive lane and prevents lane starvation through the round robin fallback, the scaleworkers load reports per origin with their spawn and pause suggestions that never act without consent, and the costshare ledger that attributes shared costs to the agents that caused them lives in this file.
1122
- * The work layer never bypasses a gate: a spawn stays inside the parent scope, the depth ceiling stays the user's choice with no hardcoded number, an aggregation never rewrites the original outputs, a verdict never bypasses the sessionlock or the origin grants, a suggestion never spawns a worker without consent and every lesson sanitizes before storage.
1123
- */
1124
-
1125
- /** Spawns one sub agent under its parent: the child registers with its unique lowercase name through the same agentname rule of the fleet, the spawn refuses a paused or stopped parent, the child depth sits exactly one level under the parent lineage depth, the parent scope copies into the child unless the spec narrows it, and a narrowing that widens past the parent refuses loudly. */
1126
- export function spawnsubagent(input: {
1127
- records: agentrecord[];
1128
- spawns: spawnrecord[];
1129
- spec: subagentspec;
1130
- parentscope?: agentscope;
1131
- id: string;
1132
- name?: string;
1133
- now: number;
1134
- }): { record: agentrecord; scope: agentscope; spawn: spawnrecord } {
1135
- const parent = input.records.find((record) => record.id === input.spec.parentid);
1136
- if (!parent)
1137
- throw new Error(
1138
- `The spawn request names the parent ${input.spec.parentid} which the fleet registry does not carry.`,
1139
- );
1140
- if (input.spec.objective.trim() === "")
1141
- throw new Error(
1142
- "The spawn request needs its parent objective in plain language; the child works on the objective its parent handed over.",
1143
- );
1144
- const parentdepth = depthoflineage({ spawns: input.spawns, agentid: parent.id });
1145
- if (!Number.isInteger(input.spec.depth) || input.spec.depth <= 0)
1146
- throw new Error("The spawn depth stays a positive whole number of the lineage.");
1147
- if (input.spec.depth !== parentdepth + 1)
1148
- throw new Error(
1149
- `The spawn request depth ${input.spec.depth} must sit exactly one level under the parent lineage depth ${parentdepth}.`,
1150
- );
1151
- if (parent.state === "paused")
1152
- throw new Error(`The parent ${parent.name} sits paused; a paused parent spawns no child until its resume.`);
1153
- if (parent.state === "stopped")
1154
- throw new Error(`The parent ${parent.name} is stopped; a stopped parent spawns no child.`);
1155
- const record = agentname({
1156
- records: input.records,
1157
- id: input.id,
1158
- proposed:
1159
- input.name !== undefined && input.name.trim() !== "" ? input.name : `${parent.name}sub${input.spec.depth}`,
1160
- ...(input.spec.role !== undefined ? { role: input.spec.role } : { role: "worker" as agentrole }),
1161
- origin: parent.origin,
1162
- now: input.now,
1163
- });
1164
- const scope = childscopeof({
1165
- agentid: record.id,
1166
- ...(input.parentscope !== undefined ? { parent: input.parentscope } : {}),
1167
- ...(input.spec.narrowscope !== undefined ? { narrowed: input.spec.narrowscope } : {}),
1168
- });
1169
- const spawn: spawnrecord = {
1170
- id: `${record.id}:spawn`,
1171
- parentid: parent.id,
1172
- childid: record.id,
1173
- role: record.role,
1174
- depth: input.spec.depth,
1175
- objective: input.spec.objective.trim(),
1176
- at: input.now,
1177
- };
1178
- return { record, scope, spawn };
1179
- }
1180
-
1181
- /** Copies the parent scope into the child unless narrowed: a narrowing keeps only the parent origins and action kinds so the child scope stays a subset of its parent, and a narrowing that lands entirely outside the parent refuses instead of silently widening. */
1182
- export function childscopeof(input: {
1183
- agentid: string;
1184
- parent?: agentscope;
1185
- narrowed?: { origins?: string[]; actionkinds?: string[] };
1186
- }): agentscope {
1187
- if (input.agentid.trim() === "") throw new Error("The child scope needs its agent id.");
1188
- const parent = input.parent;
1189
- if (parent === undefined) return { agentid: input.agentid.trim(), origins: [], toolnamespaces: [] };
1190
- const narrowedorigins = (input.narrowed?.origins ?? [])
1191
- .map((origin) => origin.trim())
1192
- .filter((origin) => origin !== "");
1193
- const origins =
1194
- narrowedorigins.length > 0 ? narrowedorigins.filter((origin) => parent.origins.includes(origin)) : parent.origins;
1195
- if (narrowedorigins.length > 0 && origins.length === 0)
1196
- throw new Error(
1197
- `The narrowed origins ${narrowedorigins.join(", ")} sit outside the parent scope ${parent.origins.join(", ")}; the child scope never widens past its parent.`,
1198
- );
1199
- const narrowedkinds = (input.narrowed?.actionkinds ?? []).map((kind) => kind.trim()).filter((kind) => kind !== "");
1200
- const parentkinds = parent.actionkinds ?? [];
1201
- const actionkinds =
1202
- narrowedkinds.length > 0 ? narrowedkinds.filter((kind) => parentkinds.includes(kind)) : parentkinds;
1203
- if (narrowedkinds.length > 0 && actionkinds.length === 0)
1204
- throw new Error(
1205
- `The narrowed action kinds ${narrowedkinds.join(", ")} sit outside the parent scope; the child scope never widens past its parent.`,
1206
- );
1207
- return {
1208
- agentid: input.agentid.trim(),
1209
- origins,
1210
- toolnamespaces: parent.toolnamespaces,
1211
- ...(actionkinds.length > 0 ? { actionkinds } : {}),
1212
- ...(parent.readonly === true ? { readonly: true } : {}),
1213
- };
1214
- }
1215
-
1216
- /** Computes the spawn depth of one agent lineage: the chain of spawn records from the agent up through its parents counts one level per spawn, an agent nobody spawned sits at depth zero, and a cycle in the lineage refuses instead of looping. */
1217
- export function depthoflineage(input: { spawns: spawnrecord[]; agentid: string }): number {
1218
- let current = input.agentid;
1219
- let depth = 0;
1220
- const seen = new Set<string>([current]);
1221
- for (;;) {
1222
- const spawn = input.spawns.find((entry) => entry.childid === current);
1223
- if (!spawn) return depth;
1224
- if (seen.has(spawn.parentid))
1225
- throw new Error(
1226
- `The spawn lineage of the agent ${input.agentid} carries a cycle at ${spawn.parentid}; the depth never loops.`,
1227
- );
1228
- seen.add(spawn.parentid);
1229
- current = spawn.parentid;
1230
- depth += 1;
1231
- }
1232
- }
1233
-
1234
- /** Checks one spawn depth against the user configured depth limit: the depth refuses past the configured ceiling while an absent limit stays unbounded because the ceiling stays the user's choice with no hardcoded number. */
1235
- export function depthlimitof(input: { spawns: spawnrecord[]; agentid: string; limit: depthlimit }): {
1236
- depth: number;
1237
- allowed: boolean;
1238
- reason: string;
1239
- } {
1240
- const depth = depthoflineage({ spawns: input.spawns, agentid: input.agentid });
1241
- if (input.limit.maxdepth === undefined)
1242
- return {
1243
- depth,
1244
- allowed: true,
1245
- reason: `The lineage depth of the agent ${input.agentid} sits at ${depth}; the user configured no depth ceiling so the spawn recursion stays unbounded.`,
1246
- };
1247
- if (depth > input.limit.maxdepth)
1248
- return {
1249
- depth,
1250
- allowed: false,
1251
- reason: `The lineage depth ${depth} of the agent ${input.agentid} passes the user configured depth limit ${input.limit.maxdepth}; the spawn refuses until the user raises the limit.`,
1252
- };
1253
- return {
1254
- depth,
1255
- allowed: true,
1256
- reason: `The lineage depth ${depth} of the agent ${input.agentid} sits inside the user configured depth limit ${input.limit.maxdepth}.`,
1257
- };
1258
- }
1259
-
1260
- /** Merges the parallel outputs of several agents into one aggregatereport: every agent keeps its own section with its run provenance, a section two agents wrote conflicts, a conflict resolves through the user configured policy order that names its agent precedence while a conflict without an order stays unresolved and the record stays open for the escalation. */
1261
- export function aggregatereport(input: {
1262
- id: string;
1263
- subject: string;
1264
- cells: aggregatecell[];
1265
- conflictorder?: string[];
1266
- expected?: string[];
1267
- now: number;
1268
- }): aggregaterecord {
1269
- if (input.id.trim() === "") throw new Error("The aggregatereport needs its id.");
1270
- if (input.subject.trim() === "") throw new Error("The aggregatereport needs its subject in plain language.");
1271
- const cells = input.cells.filter((cell) => cell.agentid.trim() !== "" && cell.section.trim() !== "");
1272
- if (cells.length === 0)
1273
- throw new Error("The aggregatereport merges at least one agent output; an empty parallel run aggregates nothing.");
1274
- const sections = new Map<string, aggregatecell[]>();
1275
- for (const cell of cells) {
1276
- const key = cell.section.trim();
1277
- sections.set(key, [...(sections.get(key) ?? []), { ...cell, agentid: cell.agentid.trim(), section: key }]);
1278
- }
1279
- const conflicts: aggregaterecord["conflicts"] = [];
1280
- for (const [key, writers] of [...sections.entries()].sort((one, two) => (one[0] < two[0] ? -1 : 1))) {
1281
- if (writers.length < 2) continue;
1282
- const ordered =
1283
- input.conflictorder !== undefined
1284
- ? writers.slice().sort((one, two) => {
1285
- const oneindex = input.conflictorder!.indexOf(one.agentid);
1286
- const twoindex = input.conflictorder!.indexOf(two.agentid);
1287
- return (
1288
- (oneindex === -1 ? Number.MAX_SAFE_INTEGER : oneindex) -
1289
- (twoindex === -1 ? Number.MAX_SAFE_INTEGER : twoindex)
1290
- );
1291
- })
1292
- : writers;
1293
- const winner = ordered[0] as aggregatecell;
1294
- const resolved = input.conflictorder !== undefined && input.conflictorder.includes(winner.agentid);
1295
- conflicts.push({ key, ...(resolved ? { resolvedby: winner.agentid } : {}) });
1296
- }
1297
- const unresolved = conflicts.filter((conflict) => conflict.resolvedby === undefined);
1298
- const complete =
1299
- input.expected === undefined || input.expected.every((agentid) => cells.some((cell) => cell.agentid === agentid));
1300
- const state: aggregaterecord["state"] = unresolved.length > 0 || !complete ? "open" : "merged";
1301
- return {
1302
- id: input.id.trim(),
1303
- subject: input.subject.trim(),
1304
- cells,
1305
- conflicts,
1306
- ...(input.expected !== undefined ? { expected: input.expected } : {}),
1307
- state,
1308
- ...(state === "merged" ? { mergedat: input.now } : {}),
1309
- createdat: input.now,
1310
- };
1311
- }
1312
-
1313
- /** Orders the actions of different agents into one interleaved timeline: the events sort by their time and id across every agent while each event carries its lane so the merged view keeps every per agent lane visible inside the one stream. */
1314
- export function interleave(input: {
1315
- events: Array<{ id?: string; agentid: string; kind: string; summary: string; at: number; lane?: string }>;
1316
- lanes: tasklane[];
1317
- }): interleaveevent[] {
1318
- const lanemap = new Map<string, string>();
1319
- for (const lane of input.lanes) for (const agentid of lane.agentids ?? []) lanemap.set(agentid, lane.name);
1320
- const events: interleaveevent[] = input.events.map((event, index) => ({
1321
- id: event.id !== undefined && event.id.trim() !== "" ? event.id.trim() : `ie${index}`,
1322
- agentid: event.agentid,
1323
- kind: event.kind,
1324
- summary: event.summary,
1325
- lane:
1326
- event.lane !== undefined && event.lane.trim() !== ""
1327
- ? event.lane.trim()
1328
- : (lanemap.get(event.agentid) ?? "background"),
1329
- at: event.at,
1330
- }));
1331
- if (events.length === 0)
1332
- throw new Error(
1333
- "The interleaved timeline orders at least one agent action; an empty fleet view interleaves nothing.",
1334
- );
1335
- return events.sort((one, two) => one.at - two.at || (one.id < two.id ? -1 : 1));
1336
- }
1337
-
1338
- /** Records one reusable lesson of a finished run: the finding in plain language, the agent that found it and the origin it holds on, with the reuse count starting at zero until another agent serves it. */
1339
- export function lessonrecordof(input: {
1340
- id: string;
1341
- agentid: string;
1342
- finding: string;
1343
- origin: string;
1344
- now: number;
1345
- }): lessonrecord {
1346
- if (input.id.trim() === "") throw new Error("The lesson needs its id.");
1347
- if (input.agentid.trim() === "") throw new Error("The lesson names the agent that found it.");
1348
- if (input.finding.trim() === "")
1349
- throw new Error("The lesson needs its finding in plain language; the fleet reads exactly what the run learned.");
1350
- if (input.origin.trim() === "")
1351
- throw new Error("The lesson names its origin; the matches prefer the lessons of the same site.");
1352
- return {
1353
- id: input.id.trim(),
1354
- agentid: input.agentid.trim(),
1355
- finding: input.finding.trim(),
1356
- origin: input.origin.trim(),
1357
- reusecount: 0,
1358
- recordedat: input.now,
1359
- };
1360
- }
1361
-
1362
- /** Serves the matching lessons to an agent starting a similar task: the lessons of the same origin match first, the findings that share words with the task follow, and the reuse count of every served lesson grows so the fleet reads which lessons carry their weight. */
1363
- export function lessonmatches(input: {
1364
- lessons: lessonrecord[];
1365
- task: string;
1366
- origin?: string;
1367
- limit?: number;
1368
- }): lessonrecord[] {
1369
- if (input.task.trim() === "") return [];
1370
- const words = new Set(
1371
- input.task
1372
- .toLowerCase()
1373
- .split(/[^a-z0-9]+/)
1374
- .filter((word) => word.length > 3),
1375
- );
1376
- const scored = input.lessons
1377
- .map((lesson) => {
1378
- const lessonwords = lesson.finding
1379
- .toLowerCase()
1380
- .split(/[^a-z0-9]+/)
1381
- .filter((word) => word.length > 3);
1382
- const overlap = lessonwords.filter((word) => words.has(word)).length;
1383
- const originmatch = input.origin !== undefined && lesson.origin === input.origin ? 1 : 0;
1384
- return { lesson, score: originmatch * 2 + overlap };
1385
- })
1386
- .filter((entry) => entry.score > 0)
1387
- .sort((one, two) => two.score - one.score || two.lesson.reusecount - one.lesson.reusecount);
1388
- const limit =
1389
- input.limit !== undefined && Number.isFinite(input.limit) && input.limit > 0
1390
- ? Math.floor(input.limit)
1391
- : scored.length;
1392
- return scored.slice(0, limit).map((entry) => entry.lesson);
1393
- }
1394
-
1395
- /** Counts one reuse of a served lesson: the reuse count grows by one and the last used time moves to now while the lessons nobody serves stay exactly as they were. */
1396
- export function lessonreuse(input: { lessons: lessonrecord[]; id: string; now: number }): lessonrecord[] {
1397
- const lesson = input.lessons.find((entry) => entry.id === input.id);
1398
- if (!lesson) throw new Error(`The lesson ${input.id} does not exist; the reuse counts only the served lessons.`);
1399
- return input.lessons.map((entry) =>
1400
- entry.id === input.id ? { ...entry, reusecount: entry.reusecount + 1, lastusedat: input.now } : entry,
1401
- );
1402
- }
1403
-
1404
- /** Decays the stale lessons: a lesson nobody reused for the whole user configured window retires from the serving list while the lessons still carrying reuse stay; an absent window keeps every lesson because the decay stays the user's choice. */
1405
- export function lessondecay(input: { lessons: lessonrecord[]; now: number; stalewindow?: number }): lessonrecord[] {
1406
- if (input.stalewindow === undefined || !Number.isFinite(input.stalewindow) || input.stalewindow <= 0)
1407
- return input.lessons;
1408
- const stalewindow: number = input.stalewindow;
1409
- return input.lessons.filter((lesson) => {
1410
- if (lesson.reusecount > 0) return true;
1411
- const age = input.now - (lesson.lastusedat ?? lesson.recordedat);
1412
- return age <= stalewindow;
1413
- });
1414
- }
1415
-
1416
- /** Opens one arbitration case of a contested resource and grants its first verdict: the case needs at least two requesting agents because one requester contests nothing, the first requester holds the resource by default, a requester of a higher priority lane takes the grant over the first requester, and the verdict names its lane and its reason for the audit trail. */
1417
- export function arbitrationcaseof(input: {
1418
- id: string;
1419
- resource: string;
1420
- origin: string;
1421
- requesters: string[];
1422
- lanes: tasklane[];
1423
- now: number;
1424
- }): arbitrationcase {
1425
- if (input.id.trim() === "") throw new Error("The arbitration case needs its id.");
1426
- if (input.resource.trim() === "") throw new Error("The arbitration case names its contested resource.");
1427
- if (input.origin.trim() === "") throw new Error("The arbitration case names the origin of its contested resource.");
1428
- const requesters = [...new Set(input.requesters.map((id) => id.trim()).filter((id) => id !== ""))];
1429
- if (requesters.length < 2)
1430
- throw new Error(
1431
- `The arbitration case of ${input.resource.trim()} needs at least two requesting agents; one requester contests nothing.`,
1432
- );
1433
- const lanepriority = new Map<string, number>();
1434
- for (const lane of input.lanes) for (const agentid of lane.agentids ?? []) lanepriority.set(agentid, lane.priority);
1435
- const first = requesters[0] as string;
1436
- const winner = requesters
1437
- .slice(1)
1438
- .reduce(
1439
- (lead, candidate) => ((lanepriority.get(candidate) ?? 0) > (lanepriority.get(lead) ?? 0) ? candidate : lead),
1440
- first,
1441
- );
1442
- const lane = input.lanes.find((entry) => (entry.agentids ?? []).includes(winner));
1443
- const verdict: arbitrationverdict = {
1444
- holderagentid: winner,
1445
- lane: lane?.name ?? "background",
1446
- reason:
1447
- winner === first
1448
- ? `The first requester ${winner} holds ${input.resource.trim()} by default; no lane of a peer outranks it.`
1449
- : `The requester ${winner} of the ${lane?.name ?? "background"} lane outranks the first requester through its user configured priority; the verdict honors the lane.`,
1450
- grantedat: input.now,
1451
- };
1452
- return {
1453
- id: input.id.trim(),
1454
- resource: input.resource.trim(),
1455
- origin: input.origin.trim(),
1456
- requesterids: requesters,
1457
- state: "granted",
1458
- verdict,
1459
- openedat: input.now,
1460
- };
1461
- }
1462
-
1463
- /** Releases the verdict of one arbitration case when its holder finishes: the release frees the resource for the next requester and closes the case while a release without the finished holder refuses. */
1464
- export function releasecase(input: {
1465
- cases: arbitrationcase[];
1466
- id: string;
1467
- holderfinished: boolean;
1468
- now: number;
1469
- }): arbitrationcase {
1470
- const record = input.cases.find((entry) => entry.id === input.id);
1471
- if (!record) throw new Error(`The arbitration case ${input.id} does not exist.`);
1472
- if (record.state !== "granted")
1473
- throw new Error(`The arbitration case ${record.id} carries no granted verdict to release.`);
1474
- if (!input.holderfinished)
1475
- throw new Error(
1476
- `The holder ${record.verdict?.holderagentid} of ${record.resource} still works; the verdict releases only when the holder finishes.`,
1477
- );
1478
- return { ...record, state: "released", closedat: input.now };
1479
- }
1480
-
1481
- /** Ranks the queued tasks by the user configured lane priority: the sensitive tasks stay in the interactive lane whatever lane asked for them, the interactive lane always serves first, and every further lane serves one task per round robin cycle in its priority order so no lane starves behind a busy higher priority lane. */
1482
- export function prioritylaneof(input: {
1483
- tasks: Array<{ id: string; lane: string; priority?: number; sensitive?: boolean; enqueuedat: number }>;
1484
- lanes: tasklane[];
1485
- }): Array<{ id: string; lane: string; priority?: number; sensitive?: boolean; enqueuedat: number }> {
1486
- const tasks = input.tasks.map((task) => ({ ...task, lane: task.sensitive === true ? "interactive" : task.lane }));
1487
- const order = (lane: string): number => {
1488
- const found = input.lanes.find((entry) => entry.name === lane);
1489
- if (found?.interactive === true) return Number.MAX_SAFE_INTEGER;
1490
- return found?.priority ?? 0;
1491
- };
1492
- const groups = [...new Set(tasks.map((task) => task.lane))].sort((one, two) => order(two) - order(one));
1493
- const queues = new Map<string, typeof tasks>();
1494
- for (const lane of groups)
1495
- queues.set(
1496
- lane,
1497
- tasks
1498
- .filter((task) => task.lane === lane)
1499
- .sort((one, two) => (two.priority ?? 0) - (one.priority ?? 0) || one.enqueuedat - two.enqueuedat),
1500
- );
1501
- const ordered: typeof tasks = [];
1502
- for (;;) {
1503
- let served = false;
1504
- for (const lane of groups) {
1505
- const queue = queues.get(lane) ?? [];
1506
- const next = queue.shift();
1507
- if (next !== undefined) {
1508
- ordered.push(next);
1509
- served = true;
1510
- }
1511
- }
1512
- if (!served) return ordered;
1513
- }
1514
- }
1515
-
1516
- /** Builds one load report sample of an origin: the concurrency of the live workers and the latency of the waits freeze into one sample the scaleworkers pass reads. */
1517
- export function loadreportof(input: { origin: string; concurrency: number; latency: number; now: number }): loadreport {
1518
- if (input.origin.trim() === "") throw new Error("The load report names its origin.");
1519
- if (!Number.isFinite(input.concurrency) || input.concurrency < 0)
1520
- throw new Error("The load report concurrency stays zero or positive.");
1521
- if (!Number.isFinite(input.latency) || input.latency < 0)
1522
- throw new Error("The load report latency stays zero or positive milliseconds.");
1523
- return {
1524
- origin: input.origin.trim(),
1525
- concurrency: Math.floor(input.concurrency),
1526
- latency: Math.round(input.latency),
1527
- sampledat: input.now,
1528
- };
1529
- }
1530
-
1531
- /** Reads the scaleworkers suggestions from the load reports: an origin whose load stays low suggests spawning one more worker, an origin whose latency passes the throttle threshold suggests pausing a worker, and every suggestion stays a suggestion because the spawn itself needs the user consent; absent thresholds hold the suggestions. */
1532
- export function scalesuggestion(input: {
1533
- reports: loadreport[];
1534
- lowthreshold?: number;
1535
- throttlethreshold?: number;
1536
- }): Array<{ origin: string; suggestion: "spawn" | "pause" | "hold"; reason: string }> {
1537
- return input.reports.map((report) => {
1538
- if (
1539
- input.throttlethreshold !== undefined &&
1540
- Number.isFinite(input.throttlethreshold) &&
1541
- input.throttlethreshold > 0 &&
1542
- report.latency >= input.throttlethreshold
1543
- )
1544
- return {
1545
- origin: report.origin,
1546
- suggestion: "pause" as const,
1547
- reason: `The origin ${report.origin} throttles at ${report.latency} milliseconds of latency past the threshold ${input.throttlethreshold}; pausing one worker eases the site, and the pause needs the user.`,
1548
- };
1549
- if (
1550
- input.lowthreshold !== undefined &&
1551
- Number.isFinite(input.lowthreshold) &&
1552
- input.lowthreshold > 0 &&
1553
- report.concurrency <= input.lowthreshold &&
1554
- report.latency < input.lowthreshold
1555
- )
1556
- return {
1557
- origin: report.origin,
1558
- suggestion: "spawn" as const,
1559
- reason: `The origin ${report.origin} carries its load of ${report.concurrency} worker${report.concurrency === 1 ? "" : "s"} with ${report.latency} milliseconds of latency; spawning one more worker shares the work, and the spawn needs the user consent.`,
1560
- };
1561
- return {
1562
- origin: report.origin,
1563
- suggestion: "hold" as const,
1564
- reason: `The origin ${report.origin} carries ${report.concurrency} worker${report.concurrency === 1 ? "" : "s"} at ${report.latency} milliseconds of latency; the fleet holds its size.`,
1565
- };
1566
- });
1567
- }
1568
-
1569
- /** Builds one cost entry of the shared fleet ledger: the agent that caused the cost, the run it belongs to, the units it spent and the plain language description of what the units bought. */
1570
- export function costentryof(input: {
1571
- agentid: string;
1572
- runid?: string;
1573
- units: number;
1574
- description: string;
1575
- now: number;
1576
- }): costentry {
1577
- if (input.agentid.trim() === "") throw new Error("The cost entry names the agent that caused it.");
1578
- if (!Number.isFinite(input.units) || input.units <= 0)
1579
- throw new Error("The cost entry carries a positive number of units; a zero cost never enters the ledger.");
1580
- if (input.description.trim() === "")
1581
- throw new Error("The cost entry describes what its units bought in plain language.");
1582
- return {
1583
- agentid: input.agentid.trim(),
1584
- ...(input.runid !== undefined && input.runid.trim() !== "" ? { runid: input.runid.trim() } : {}),
1585
- units: input.units,
1586
- description: input.description.trim(),
1587
- at: input.now,
1588
- };
1589
- }
1590
-
1591
- /** Splits the shared costs of the fleet ledger to the agents that caused them: a cost one agent caused attributes fully to it while a cost several agents share through the same description splits equally among its causers, and every agent reads its own share. */
1592
- export function sharedcostsplit(input: {
1593
- entries: costentry[];
1594
- }): Array<{ agentid: string; units: number; share: number; sharedwith?: string[] }> {
1595
- const solo = new Map<string, number>();
1596
- const groups = new Map<string, Map<string, number>>();
1597
- for (const entry of input.entries) {
1598
- const group = groups.get(entry.description) ?? new Map<string, number>();
1599
- group.set(entry.agentid, (group.get(entry.agentid) ?? 0) + entry.units);
1600
- groups.set(entry.description, group);
1601
- }
1602
- for (const group of groups.values()) {
1603
- const agents = [...group.keys()];
1604
- if (agents.length === 1) {
1605
- const agentid = agents[0] as string;
1606
- solo.set(agentid, (solo.get(agentid) ?? 0) + (group.get(agentid) ?? 0));
1607
- }
1608
- }
1609
- const shared = new Map<string, { units: number; peers: Set<string> }>();
1610
- for (const group of groups.values()) {
1611
- const agents = [...group.keys()];
1612
- if (agents.length < 2) continue;
1613
- const total = [...group.values()].reduce((sum, units) => sum + units, 0);
1614
- const share = total / agents.length;
1615
- for (const agentid of agents) {
1616
- const current = shared.get(agentid) ?? { units: 0, peers: new Set<string>() };
1617
- current.units += share;
1618
- for (const peer of agents) if (peer !== agentid) current.peers.add(peer);
1619
- shared.set(agentid, current);
1620
- }
1621
- }
1622
- const agentids = [...new Set([...solo.keys(), ...shared.keys()])].sort();
1623
- return agentids.map((agentid) => {
1624
- const soloamount = solo.get(agentid) ?? 0;
1625
- const sharedamount = shared.get(agentid);
1626
- return {
1627
- agentid,
1628
- units: soloamount + (sharedamount?.units ?? 0),
1629
- share: soloamount,
1630
- ...(sharedamount !== undefined ? { sharedwith: [...sharedamount.peers].sort() } : {}),
1631
- };
1632
- });
1633
- }
1634
-
1635
- /* ── Merged from multiagent.ts: the 1.1.88 consolidation interns the correlated multiagent logic here, so no variation of the same file lives beside another. ── */
1636
- import { newworkflowrun } from "./workflow.js";
1637
-
1638
- /**
1639
- * Multi agent lifecycle logic of the 1.1.58 family.
1640
- * Every correlated rule for agent identities, roles, tab binding, sub agent spawning with depth limits, pause, resume, stop, the killswitch, per agent budgets and permission scopes, per agent usage and the swarm overview lives in this file.
1641
- * The swarm never bypasses review: an agent claims queue tasks and proposes work through the same plan review a single agent passes, and the killswitch stays available with no configuration barrier.
1642
- */
1643
-
1644
- /** The default tool namespaces of a role: planners compose and read, workers also act on the browser, critics review agent outputs read only, verifiers re-read the page to check claims and observers stay read only; a custom role grades with the worker defaults until the user narrows its scope. */
1645
- export function roledefaults(role: agentrole): { toolnamespaces: toolnamespace[]; description: string } {
1646
- if (role === "planner")
1647
- return {
1648
- toolnamespaces: ["workflow", "memory", "system"],
1649
- description: "Planners compose reviewed plans and read memory; they never act on the page themselves.",
1650
- };
1651
- if (role === "observer")
1652
- return {
1653
- toolnamespaces: ["memory", "system"],
1654
- description: "Observers read the shared memory and the system reports only.",
1655
- };
1656
- if (role === "critic")
1657
- return {
1658
- toolnamespaces: ["workflow", "memory", "system"],
1659
- description: "Critics review the outputs of the other agents read only; they never act on the page themselves.",
1660
- };
1661
- if (role === "verifier")
1662
- return {
1663
- toolnamespaces: ["browser", "memory", "system"],
1664
- description: "Verifiers re-read the page to check the claims of the other agents; their checks stay read side.",
1665
- };
1666
- return {
1667
- toolnamespaces: ["browser", "workflow", "memory", "system"],
1668
- description:
1669
- role === "worker"
1670
- ? "Workers execute the reviewed steps of approved plans."
1671
- : `The custom role ${role} carries the worker defaults until the user narrows its scope.`,
1672
- };
1673
- }
1674
-
1675
- /** Registers one agent identity bound to a tab; the user names the agent for dashboards and audit, and one tab never holds two agents. */
1676
- export function registeragent(input: {
1677
- agents: agentidentity[];
1678
- id: string;
1679
- name: string;
1680
- tabid?: number;
1681
- sessionid?: string;
1682
- role?: agentrole;
1683
- now: number;
1684
- }): agentidentity[] {
1685
- if (input.name.trim() === "")
1686
- throw new Error("The agent needs the user chosen name; agent naming stays a user choice.");
1687
- if (
1688
- input.tabid !== undefined &&
1689
- input.agents.some((agent) => agent.tabid === input.tabid && agent.state !== "stopped")
1690
- )
1691
- throw new Error(
1692
- `Tab ${input.tabid} already holds the agent ${input.agents.find((agent) => agent.tabid === input.tabid)?.name ?? "another agent"}; one tab binds one agent.`,
1693
- );
1694
- if (input.id !== "" && input.agents.some((agent) => agent.id === input.id))
1695
- throw new Error(`The agent id ${input.id} is already registered.`);
1696
- const agent: agentidentity = {
1697
- id: input.id,
1698
- name: input.name.trim(),
1699
- role: input.role ?? "worker",
1700
- depth: 0,
1701
- state: "active",
1702
- ...(input.tabid !== undefined ? { tabid: input.tabid } : {}),
1703
- ...(input.sessionid !== undefined ? { sessionid: input.sessionid } : {}),
1704
- registeredat: input.now,
1705
- heartbeatat: input.now,
1706
- };
1707
- return [agent, ...input.agents];
1708
- }
1709
-
1710
- /** Attaches one role to a registered agent; the role carries its defaults and the change reads in the audit trail. */
1711
- export function assignrole(input: {
1712
- agents: agentidentity[];
1713
- agentid: string;
1714
- role: agentrole;
1715
- now: number;
1716
- }): agentidentity[] {
1717
- const agent = input.agents.find((entry) => entry.id === input.agentid);
1718
- if (!agent) throw new Error(`The agent ${input.agentid} is not registered.`);
1719
- return input.agents.map((entry) =>
1720
- entry.id === input.agentid ? { ...entry, role: input.role, heartbeatat: input.now } : entry,
1721
- );
1722
- }
1723
-
1724
- /** Binds one agent to one tab session; a tab bound to another live agent refuses the binding, so one tab runs one agent. */
1725
- export function opentabagent(input: {
1726
- agents: agentidentity[];
1727
- agentid: string;
1728
- tabid: number;
1729
- sessionid?: string;
1730
- now: number;
1731
- }): agentidentity[] {
1732
- const agent = input.agents.find((entry) => entry.id === input.agentid);
1733
- if (!agent) throw new Error(`The agent ${input.agentid} is not registered.`);
1734
- const holder = input.agents.find(
1735
- (entry) => entry.tabid === input.tabid && entry.id !== input.agentid && entry.state !== "stopped",
1736
- );
1737
- if (holder) throw new Error(`Tab ${input.tabid} already holds the agent ${holder.name}; one tab binds one agent.`);
1738
- return input.agents.map((entry) =>
1739
- entry.id === input.agentid
1740
- ? {
1741
- ...entry,
1742
- tabid: input.tabid,
1743
- ...(input.sessionid !== undefined ? { sessionid: input.sessionid } : {}),
1744
- heartbeatat: input.now,
1745
- }
1746
- : entry,
1747
- );
1748
- }
1749
-
1750
- /** Spawns one sub agent under a spawn request; the child carries its parent, its depth and the requested role, and the spawn refuses recursion past the configured depth limit. */
1751
- export function spawn(input: {
1752
- agents: agentidentity[];
1753
- request: spawnrequest;
1754
- limit: depthlimit;
1755
- id: string;
1756
- name?: string;
1757
- now: number;
1758
- }): agentidentity[] {
1759
- const parent = input.agents.find((entry) => entry.id === input.request.parentid);
1760
- if (!parent) throw new Error(`The spawn request names the parent ${input.request.parentid} which is not registered.`);
1761
- if (input.request.task.trim() === "") throw new Error("The spawn request needs its task in plain language.");
1762
- if (input.request.depth !== parent.depth + 1)
1763
- throw new Error(
1764
- `The spawn request depth ${input.request.depth} must sit exactly one level under the parent depth ${parent.depth}.`,
1765
- );
1766
- if (input.limit.maxdepth !== undefined && input.request.depth > input.limit.maxdepth)
1767
- throw new Error(
1768
- `The spawn refuses recursion at depth ${input.request.depth}: the user configured depth limit stops at ${input.limit.maxdepth}.`,
1769
- );
1770
- const child: agentidentity = {
1771
- id: input.id,
1772
- name:
1773
- input.name?.trim() !== "" && input.name !== undefined
1774
- ? input.name.trim()
1775
- : `${parent.name} sub ${input.request.depth}`,
1776
- role: input.request.role,
1777
- parentid: parent.id,
1778
- depth: input.request.depth,
1779
- state: "active",
1780
- registeredat: input.now,
1781
- heartbeatat: input.now,
1782
- };
1783
- return [child, ...input.agents];
1784
- }
1785
-
1786
- /** Pauses one agent without stopping the others; a paused agent claims no task until its resume. */
1787
- export function pauseone(input: { agents: agentidentity[]; agentid: string; now: number }): agentidentity[] {
1788
- const agent = input.agents.find((entry) => entry.id === input.agentid);
1789
- if (!agent) throw new Error(`The agent ${input.agentid} is not registered.`);
1790
- if (agent.state === "stopped") throw new Error(`The agent ${agent.name} is stopped; a stopped agent needs no pause.`);
1791
- return input.agents.map((entry) =>
1792
- entry.id === input.agentid ? { ...entry, state: "paused" as const, heartbeatat: input.now } : entry,
1793
- );
1794
- }
1795
-
1796
- /** Resumes one paused agent; the other agents never changed. */
1797
- export function resumeone(input: { agents: agentidentity[]; agentid: string; now: number }): agentidentity[] {
1798
- const agent = input.agents.find((entry) => entry.id === input.agentid);
1799
- if (!agent) throw new Error(`The agent ${input.agentid} is not registered.`);
1800
- if (agent.state !== "paused") throw new Error(`The agent ${agent.name} is not paused.`);
1801
- return input.agents.map((entry) =>
1802
- entry.id === input.agentid ? { ...entry, state: "active" as const, heartbeatat: input.now } : entry,
1803
- );
1804
- }
1805
-
1806
- /** Stops one agent; its claims release through the queue requeue pass. */
1807
- export function stopone(input: { agents: agentidentity[]; agentid: string; now: number }): agentidentity[] {
1808
- const agent = input.agents.find((entry) => entry.id === input.agentid);
1809
- if (!agent) throw new Error(`The agent ${input.agentid} is not registered.`);
1810
- return input.agents.map((entry) =>
1811
- entry.id === input.agentid ? { ...entry, state: "stopped" as const, heartbeatat: input.now } : entry,
1812
- );
1813
- }
1814
-
1815
- /** Triggers the killswitch and cancels every agent run at once: every agent stops whatever it holds and the switch records why. */
1816
- export function killall(input: { agents: agentidentity[]; reason?: string; now: number }): {
1817
- agents: agentidentity[];
1818
- killswitch: killswitch;
1819
- } {
1820
- return {
1821
- agents: input.agents.map((agent) => ({ ...agent, state: "stopped" as const, heartbeatat: input.now })),
1822
- killswitch: {
1823
- engaged: true,
1824
- engagedat: input.now,
1825
- ...(input.reason !== undefined && input.reason.trim() !== "" ? { reason: input.reason } : {}),
1826
- },
1827
- };
1828
- }
1829
-
1830
- /** Disarms the killswitch so the user may register or resume agents again. */
1831
- export function disarmkillswitch(now: number): killswitch {
1832
- return { engaged: false, engagedat: now };
1833
- }
1834
-
1835
- /** Checks one agent against its budget: a reached token, cost or step ceiling halts the agent until the user raises the ceiling; absent ceilings never halt. */
1836
- export function agentbudgetcheck(input: { agent: agentidentity; usage: agentusage | undefined }): {
1837
- halted: boolean;
1838
- reason?: string;
1839
- } {
1840
- const budget = input.agent.budget;
1841
- const usage = input.usage;
1842
- if (!budget || !usage) return { halted: false };
1843
- if (budget.maxtokens !== undefined && usage.tokens > budget.maxtokens)
1844
- return {
1845
- halted: true,
1846
- reason: `The agent ${input.agent.name} spent ${usage.tokens} tokens past its ceiling of ${budget.maxtokens}; the user raises the ceiling or stops the agent.`,
1847
- };
1848
- if (budget.maxcost !== undefined && usage.cost > budget.maxcost)
1849
- return {
1850
- halted: true,
1851
- reason: `The agent ${input.agent.name} spent ${usage.cost} past its cost ceiling of ${budget.maxcost}; the user raises the ceiling or stops the agent.`,
1852
- };
1853
- if (budget.maxsteps !== undefined && usage.steps > budget.maxsteps)
1854
- return {
1855
- halted: true,
1856
- reason: `The agent ${input.agent.name} executed ${usage.steps} steps past its ceiling of ${budget.maxsteps}; the user raises the ceiling or stops the agent.`,
1857
- };
1858
- return { halted: false };
1859
- }
1860
-
1861
- /** Checks one agent scope: an origin or a tool namespace outside the grant refuses, and an absent scope stays unbounded inside the session grants. */
1862
- export function scopegate(input: { scope: agentscope | undefined; origin?: string; namespace?: toolnamespace }): {
1863
- allowed: boolean;
1864
- reason?: string;
1865
- } {
1866
- if (!input.scope) return { allowed: true };
1867
- if (input.origin !== undefined && input.scope.origins.length > 0 && !input.scope.origins.includes(input.origin))
1868
- return { allowed: false, reason: `The agent scope grants no access to the origin ${input.origin}.` };
1869
- if (
1870
- input.namespace !== undefined &&
1871
- input.scope.toolnamespaces.length > 0 &&
1872
- !input.scope.toolnamespaces.includes(input.namespace)
1873
- )
1874
- return { allowed: false, reason: `The agent scope grants no access to the ${input.namespace} tool namespace.` };
1875
- return { allowed: true };
1876
- }
1877
-
1878
- /** Refreshes one agent heartbeat so the panel and the queue read a live agent. */
1879
- export function agentheartbeat(input: { agents: agentidentity[]; agentid: string; now: number }): agentidentity[] {
1880
- return input.agents.map((entry) => (entry.id === input.agentid ? { ...entry, heartbeatat: input.now } : entry));
1881
- }
1882
-
1883
- /** Accumulates one usage delta into the per agent usage counters held against the agent budget. */
1884
- export function recordagentusage(input: {
1885
- usage: agentusage | undefined;
1886
- agentid: string;
1887
- tokens?: number;
1888
- cost?: number;
1889
- steps?: number;
1890
- now: number;
1891
- }): agentusage {
1892
- const base = input.usage ?? { agentid: input.agentid, tokens: 0, cost: 0, steps: 0, updatedat: input.now };
1893
- return {
1894
- agentid: input.agentid,
1895
- tokens: base.tokens + (input.tokens ?? 0),
1896
- cost: base.cost + (input.cost ?? 0),
1897
- steps: base.steps + (input.steps ?? 0),
1898
- updatedat: input.now,
1899
- };
1900
- }
1901
-
1902
- /** Builds one per agent run context that reuses the workflow engine: the run id carries the agent and the task so concurrent agent runs stay isolated through the same run records the workflow engine keeps. */
1903
- export function agentruncontext(input: { agent: agentidentity; task: taskitem; now: number }): {
1904
- agentid: string;
1905
- taskid: string;
1906
- run: workflowrun;
1907
- } {
1908
- return {
1909
- agentid: input.agent.id,
1910
- taskid: input.task.id,
1911
- run: newworkflowrun({ id: `${input.agent.id}:${input.task.id}`, workflowid: input.task.lane, now: input.now }),
1912
- };
1913
- }
1914
-
1915
- /** Builds one agent lifecycle event notification for the protocol envelope and the audit trail. */
1916
- export function agenteventof(input: {
1917
- id: string;
1918
- kind: agenteventkind;
1919
- summary: string;
1920
- agentid?: string;
1921
- taskid?: string;
1922
- now: number;
1923
- }): agentevent {
1924
- return {
1925
- id: input.id,
1926
- kind: input.kind,
1927
- summary: input.summary,
1928
- ...(input.agentid !== undefined ? { agentid: input.agentid } : {}),
1929
- ...(input.taskid !== undefined ? { taskid: input.taskid } : {}),
1930
- at: input.now,
1931
- };
1932
- }
1933
-
1934
- /** Returns the swarm at a glance: the agents with their live states, the tasks by claim state and the message counters. */
1935
- export function swarmoverview(input: { agents: agentidentity[]; queue: taskqueue; mailboxes: agentmailbox[] }): {
1936
- agents: number;
1937
- active: number;
1938
- paused: number;
1939
- stopped: number;
1940
- tasks: number;
1941
- queued: number;
1942
- claimed: number;
1943
- done: number;
1944
- cancelled: number;
1945
- messages: number;
1946
- unread: number;
1947
- } {
1948
- const claimed = input.queue.items.filter((item) => item.state === "claimed").length;
1949
- return {
1950
- agents: input.agents.length,
1951
- active: input.agents.filter((agent) => agent.state === "active").length,
1952
- paused: input.agents.filter((agent) => agent.state === "paused").length,
1953
- stopped: input.agents.filter((agent) => agent.state === "stopped").length,
1954
- tasks: input.queue.items.length,
1955
- queued: input.queue.items.filter((item) => item.state === "queued").length,
1956
- claimed,
1957
- done: input.queue.items.filter((item) => item.state === "done").length,
1958
- cancelled: input.queue.items.filter((item) => item.state === "cancelled").length,
1959
- messages: input.mailboxes.reduce((total, mailbox) => total + mailbox.inbox.length, 0),
1960
- unread: input.mailboxes.reduce((total, mailbox) => total + mailbox.unread, 0),
1961
- };
1962
- }
1963
-
1964
- /** Reads the swarm state snapshot the protocol envelope and the panel render: the agents, the queue, the mailboxes and the killswitch. */
1965
- export function swarmstateof(input: {
1966
- agents: agentidentity[];
1967
- queue: taskqueue;
1968
- mailboxes: agentmailbox[];
1969
- killswitch: killswitch;
1970
- }): swarmstate {
1971
- return { agents: input.agents, queue: input.queue, mailboxes: input.mailboxes, killswitch: input.killswitch };
1972
- }
1973
-
1974
- /* ── Merged from coordination.ts: the 1.1.88 consolidation interns the correlated coordination logic here, so no variation of the same file lives beside another. ── */
1975
- import { postentry } from "./swarm.js";
1976
-
1977
- /**
1978
- * Multi agent coordination logic of the 1.1.59 family.
1979
- * Every correlated rule for tab handoffs between agents mid run, resource locks keyed by one origin and one selector, conflict scans of overlapping writes, result merging with conflict rules and provenance, output comparison, the interleaved swarm timeline, verified lessons on the blackboard, the aggregate report across agents, the shared cost accounting and the replay of one agent run from the audit trail lives in this file.
1980
- * Coordination never bypasses review: a handoff preserves the original session grants, a lock only serializes work the same review already approved and every merged report that includes page content grades as an egress event on export.
1981
- */
1982
-
1983
- /** Composes the lock key of exactly one origin and one selector; a lock never spans unrelated origins because the key names one target of one origin. */
1984
- export function lockkey(origin: string, selector: string): string {
1985
- return `${origin}|${selector}`;
1986
- }
1987
-
1988
- /** Packages one tab handoff for transfer: the transferring agent names the receiving agent, the tab and the task state in plain language; the record waits prepared until its transfer. */
1989
- export function preparehandoff(input: {
1990
- agents: agentidentity[];
1991
- id: string;
1992
- fromagentid: string;
1993
- toagentid: string;
1994
- taskstate: string;
1995
- tabid?: number;
1996
- reason?: string;
1997
- now: number;
1998
- }): handoffrecord {
1999
- if (!input.agents.some((agent) => agent.id === input.fromagentid))
2000
- throw new Error(`The handoff names the transferring agent ${input.fromagentid} which is not registered.`);
2001
- if (!input.agents.some((agent) => agent.id === input.toagentid))
2002
- throw new Error(`The handoff names the receiving agent ${input.toagentid} which is not registered.`);
2003
- if (input.fromagentid === input.toagentid)
2004
- throw new Error("A handoff moves a task between two different agents; an agent never hands off to itself.");
2005
- if (input.taskstate.trim() === "")
2006
- throw new Error(
2007
- "The handoff needs its packaged task state in plain language; the resume continues exactly from it.",
2008
- );
2009
- const from = input.agents.find((agent) => agent.id === input.fromagentid)!;
2010
- const tabid = input.tabid ?? from.tabid;
2011
- if (tabid === undefined)
2012
- throw new Error("The handoff needs its tab id; the transferring agent holds no tab to hand off.");
2013
- return {
2014
- id: input.id,
2015
- fromagentid: input.fromagentid,
2016
- toagentid: input.toagentid,
2017
- tabid,
2018
- taskstate: input.taskstate,
2019
- state: "prepared",
2020
- ...(input.reason !== undefined && input.reason.trim() !== "" ? { reason: input.reason } : {}),
2021
- createdat: input.now,
2022
- };
2023
- }
2024
-
2025
- /** Moves one tab binding between agents: the prepared handoff transfers the tab from the transferring agent to the receiving agent under the one agent per tab rule, and the record marks the transfer. */
2026
- export function transferhandoff(input: {
2027
- agents: agentidentity[];
2028
- handoffs: handoffrecord[];
2029
- id: string;
2030
- now: number;
2031
- }): { agents: agentidentity[]; handoffs: handoffrecord[] } {
2032
- const record = input.handoffs.find((entry) => entry.id === input.id);
2033
- if (!record) throw new Error(`The handoff ${input.id} does not exist.`);
2034
- if (record.state !== "prepared")
2035
- throw new Error(`The handoff ${record.id} is ${record.state}; only a prepared handoff transfers.`);
2036
- const receiver = input.agents.find((agent) => agent.id === record.toagentid);
2037
- if (!receiver) throw new Error(`The receiving agent ${record.toagentid} is not registered.`);
2038
- if (receiver.state === "stopped")
2039
- throw new Error(
2040
- `The receiving agent ${receiver.name} is stopped; the handoff waits for its resume or another receiver.`,
2041
- );
2042
- const holder = input.agents.find(
2043
- (agent) => agent.tabid === record.tabid && agent.id !== record.fromagentid && agent.state !== "stopped",
2044
- );
2045
- if (holder) throw new Error(`Tab ${record.tabid} already holds the agent ${holder.name}; one tab binds one agent.`);
2046
- const agents = input.agents.map((agent) => {
2047
- if (agent.id === record.fromagentid) {
2048
- const { tabid, ...rest } = agent;
2049
- void tabid;
2050
- return rest;
2051
- }
2052
- if (agent.id === record.toagentid && record.tabid !== undefined) return { ...agent, tabid: record.tabid };
2053
- return agent;
2054
- });
2055
- return {
2056
- agents,
2057
- handoffs: input.handoffs.map((entry) =>
2058
- entry.id === input.id ? { ...entry, state: "transferred" as const, transferredat: input.now } : entry,
2059
- ),
2060
- };
2061
- }
2062
-
2063
- /** Continues one transferred task from its packaged state: the receiving agent owns the task from the recorded task state and the record marks the resume. */
2064
- export function resumehandoff(input: { handoffs: handoffrecord[]; id: string; now: number }): handoffrecord {
2065
- const record = input.handoffs.find((entry) => entry.id === input.id);
2066
- if (!record) throw new Error(`The handoff ${input.id} does not exist.`);
2067
- if (record.state !== "transferred")
2068
- throw new Error(`The handoff ${record.id} is ${record.state}; only a transferred handoff resumes.`);
2069
- return { ...record, state: "resumed", resumedat: input.now };
2070
- }
2071
-
2072
- /** Acquires one resource lock keyed by one origin and one selector: an exclusive lock refuses every second holder while a shared lock admits further shared holders only; a held key refuses the newcomer whatever the kinds. */
2073
- export function acquirelock(input: {
2074
- locks: resourcelock[];
2075
- holder: string;
2076
- origin: string;
2077
- selector: string;
2078
- kind?: "exclusive" | "shared";
2079
- expiresat?: number;
2080
- now: number;
2081
- }): { locks: resourcelock[]; acquired: boolean; reason: string } {
2082
- if (input.holder.trim() === "") throw new Error("The lock needs its holder agent id.");
2083
- if (input.origin.trim() === "") throw new Error("The lock needs its origin; a lock never spans unrelated origins.");
2084
- if (input.selector.trim() === "") throw new Error("The lock needs its selector of the origin.");
2085
- const kind = input.kind ?? "exclusive";
2086
- const key = lockkey(input.origin.trim(), input.selector.trim());
2087
- const held = input.locks.filter((lock) => lock.key === key);
2088
- if (held.some((lock) => lock.holder === input.holder))
2089
- return { locks: input.locks, acquired: false, reason: `The agent ${input.holder} already holds the lock ${key}.` };
2090
- if (held.length > 0) {
2091
- if (held.some((lock) => lock.kind === "exclusive") || kind === "exclusive")
2092
- return {
2093
- locks: input.locks,
2094
- acquired: false,
2095
- reason: `The lock ${key} is held ${held.some((lock) => lock.kind === "exclusive") ? "exclusively" : "shared"}; the ${kind} request of ${input.holder} refuses.`,
2096
- };
2097
- }
2098
- const lock: resourcelock = {
2099
- key,
2100
- holder: input.holder,
2101
- kind,
2102
- origin: input.origin.trim(),
2103
- selector: input.selector.trim(),
2104
- acquiredat: input.now,
2105
- ...(input.expiresat !== undefined ? { expiresat: input.expiresat } : {}),
2106
- };
2107
- return {
2108
- locks: [...input.locks, lock],
2109
- acquired: true,
2110
- reason: `The ${kind} lock ${key} went to the agent ${input.holder}.`,
2111
- };
2112
- }
2113
-
2114
- /** Releases one lock with the holder named: the freed key returns to the pool for the next agent. */
2115
- export function releaselock(input: { locks: resourcelock[]; key: string; holder: string; now: number }): {
2116
- locks: resourcelock[];
2117
- released: boolean;
2118
- } {
2119
- const lock = input.locks.find((entry) => entry.key === input.key && entry.holder === input.holder);
2120
- if (!lock) return { locks: input.locks, released: false };
2121
- return {
2122
- locks: input.locks.filter((entry) => entry.key !== input.key || entry.holder !== input.holder),
2123
- released: true,
2124
- };
2125
- }
2126
-
2127
- /** Returns the abandoned locks to the pool: a lock past its user configured expiry releases on the sweep while a lock without an expiry never expires. */
2128
- export function expirelocks(input: { locks: resourcelock[]; now: number }): {
2129
- locks: resourcelock[];
2130
- expired: string[];
2131
- } {
2132
- const stale = input.locks.filter((lock) => lock.expiresat !== undefined && input.now > lock.expiresat);
2133
- if (stale.length === 0) return { locks: input.locks, expired: [] };
2134
- const keys = new Set(stale.map((lock) => `${lock.key}:${lock.holder}`));
2135
- return { locks: input.locks.filter((lock) => !keys.has(`${lock.key}:${lock.holder}`)), expired: [...keys] };
2136
- }
2137
-
2138
- /** Scans the parallel writes for overlapping targets before the runs start: two writers on the same origin and selector overlap, the scan lists the overlapping targets with their writers and the suggested ordering keeps the writers deterministic by agent id. */
2139
- export function scanconflicts(input: { id: string; writers: conflictwriter[]; now: number }): conflictscan {
2140
- const targets = new Map<string, conflictwriter[]>();
2141
- for (const writer of input.writers) {
2142
- const key = lockkey(writer.origin, writer.selector);
2143
- targets.set(key, [...(targets.get(key) ?? []), writer]);
2144
- }
2145
- const overlaps = [...targets.entries()]
2146
- .filter(([, writers]) => writers.length > 1)
2147
- .map(([key, writers]) => ({
2148
- origin: writers[0]!.origin,
2149
- selector: writers[0]!.selector,
2150
- writers: writers.map((writer) => writer.agentid),
2151
- }));
2152
- const overlappingagents = new Set(overlaps.flatMap((entry) => entry.writers));
2153
- return {
2154
- id: input.id,
2155
- writers: input.writers,
2156
- overlaps,
2157
- suggestedorder: input.writers
2158
- .filter((writer) => overlappingagents.has(writer.agentid))
2159
- .map((writer) => writer.agentid)
2160
- .filter((agentid, index, all) => all.indexOf(agentid) === index)
2161
- .sort((one, two) => (one < two ? -1 : 1)),
2162
- clean: overlaps.length === 0,
2163
- scannedat: input.now,
2164
- };
2165
- }
2166
-
2167
- /** Folds the parallel results into one report with conflict rules: the entries group by key, first keeps the earliest value, last keeps the latest, preferagent keeps the value of the agent the user named and fail refuses the fold on any conflict; every merged value keeps its provenance and every conflict lands its resolution note. */
2168
- export function mergeresults(input: { entries: mergeentry[]; rule: mergerule; preferagent?: string; now: number }): {
2169
- entries: mergeentry[];
2170
- conflicts: string[];
2171
- refused: boolean;
2172
- } {
2173
- const keys = new Map<string, mergeentry[]>();
2174
- for (const entry of input.entries) {
2175
- keys.set(entry.key, [...(keys.get(entry.key) ?? []), entry]);
2176
- }
2177
- const conflicts: string[] = [];
2178
- const merged: mergeentry[] = [];
2179
- for (const [key, entries] of keys) {
2180
- const ordered = [...entries].sort((one, two) => one.mergedat - two.mergedat);
2181
- if (ordered.length === 1) {
2182
- merged.push(ordered[0]!);
2183
- continue;
2184
- }
2185
- if (input.rule === "fail") {
2186
- conflicts.push(
2187
- `The key ${key} carries ${ordered.length} parallel values from ${ordered.map((entry) => entry.agentid).join(", ")}; the fail rule refuses the fold.`,
2188
- );
2189
- continue;
2190
- }
2191
- const winner =
2192
- input.rule === "first"
2193
- ? ordered[0]!
2194
- : input.rule === "last"
2195
- ? ordered[ordered.length - 1]!
2196
- : (ordered.find((entry) => entry.agentid === input.preferagent) ?? ordered[ordered.length - 1]!);
2197
- const note =
2198
- input.rule === "preferagent" &&
2199
- input.preferagent !== undefined &&
2200
- !ordered.some((entry) => entry.agentid === input.preferagent)
2201
- ? `The preferagent rule names the agent ${input.preferagent} which wrote no value; the latest value of ${winner.agentid} stayed.`
2202
- : `The ${input.rule} rule kept the value of ${winner.agentid} from ${ordered.map((entry) => entry.agentid).join(", ")}.`;
2203
- conflicts.push(`The key ${key}: ${note}`);
2204
- merged.push({ ...winner, id: `${winner.id}:merged`, conflict: note });
2205
- }
2206
- return { entries: merged, conflicts, refused: input.rule === "fail" && conflicts.length > 0 };
2207
- }
2208
-
2209
- /** Builds the final report across agents: the outputs fold under the merge rule the user picked, the sections group by task and every section keeps the agent sources of its entries with the provenance. */
2210
- export function swarmreport(input: {
2211
- id: string;
2212
- title: string;
2213
- outputs: mergeentry[];
2214
- rule: mergerule;
2215
- preferagent?: string;
2216
- confidence?: string;
2217
- now: number;
2218
- }): { report: resultreport; conflicts: string[]; refused: boolean } {
2219
- if (input.title.trim() === "") throw new Error("The report needs its title.");
2220
- const fold = mergeresults({
2221
- entries: input.outputs,
2222
- rule: input.rule,
2223
- ...(input.preferagent !== undefined ? { preferagent: input.preferagent } : {}),
2224
- now: input.now,
2225
- });
2226
- const groups = new Map<string, mergeentry[]>();
2227
- for (const entry of fold.entries) {
2228
- const group = entry.taskid ?? "general";
2229
- groups.set(group, [...(groups.get(group) ?? []), entry]);
2230
- }
2231
- const sections = [...groups.entries()].map(([taskid, entries]) => ({
2232
- title: `Task ${taskid}`,
2233
- entries,
2234
- sources: [
2235
- ...new Set(
2236
- input.outputs.filter((output) => (output.taskid ?? "general") === taskid).map((output) => output.agentid),
2237
- ),
2238
- ],
2239
- }));
2240
- return {
2241
- report: {
2242
- id: input.id,
2243
- title: input.title.trim(),
2244
- sections,
2245
- sources: [...new Set(input.outputs.map((output) => output.agentid))],
2246
- ...(input.confidence !== undefined && input.confidence.trim() !== "" ? { confidence: input.confidence } : {}),
2247
- createdat: input.now,
2248
- },
2249
- conflicts: fold.conflicts,
2250
- refused: fold.refused,
2251
- };
2252
- }
2253
-
2254
- /** Contrasts the competing agent outputs for the user: the comparison lists every output with its agent and names the keys where the agents disagree. */
2255
- export function compareoutputs(input: {
2256
- id: string;
2257
- subject: string;
2258
- outputs: Array<{ agentid: string; value: string }>;
2259
- now: number;
2260
- }): outputcomparison {
2261
- if (input.subject.trim() === "") throw new Error("The comparison needs its subject.");
2262
- if (input.outputs.length < 2) throw new Error("The comparison contrasts at least two competing outputs.");
2263
- const differences = input.outputs
2264
- .filter((output) => output.value !== input.outputs[0]?.value)
2265
- .map(
2266
- (output) =>
2267
- `The agent ${output.agentid} answers ${output.value} while the agent ${input.outputs[0]!.agentid} answers ${input.outputs[0]!.value}.`,
2268
- );
2269
- return { id: input.id, subject: input.subject, outputs: input.outputs, differences, comparedat: input.now };
2270
- }
2271
-
2272
- /** Interleaves the actions of every agent into one stream: the actions order by time and the ties keep their arrival order, so one timeline reads the whole swarm. */
2273
- export function interleavetimeline(actions: swarmaction[]): swarmaction[] {
2274
- return [...actions].sort((one, two) => one.at - two.at || (one.id < two.id ? -1 : 1));
2275
- }
2276
-
2277
- /** Writes one verified lesson to the blackboard: the lesson lands in the findings section under its own key with the agent and its verifier named, so every agent of the swarm reads it. */
2278
- export function sharelesson(input: {
2279
- board: blackboard;
2280
- id: string;
2281
- agentid: string;
2282
- statement: string;
2283
- verifiedby: string;
2284
- section?: blackboardsection;
2285
- consentclass?: actionrisk;
2286
- now: number;
2287
- }): blackboard {
2288
- if (input.statement.trim() === "") throw new Error("The lesson needs its statement in plain language.");
2289
- if (input.verifiedby.trim() === "")
2290
- throw new Error("The lesson needs its verifier; only a verified lesson lands on the board.");
2291
- return postentry({
2292
- board: input.board,
2293
- id: input.id,
2294
- key: `lesson:${input.statement.trim().slice(0, 40)}`,
2295
- value: `${input.statement.trim()} (verified by ${input.verifiedby.trim()})`,
2296
- section: input.section ?? "findings",
2297
- author: input.agentid.trim() === "" ? "user" : input.agentid,
2298
- consentclass: input.consentclass ?? "read",
2299
- now: input.now,
2300
- });
2301
- }
2302
-
2303
- /** Sums the per agent usage into the swarm totals of the shared cost accounting: tokens, cost and steps of every agent in one record. */
2304
- export function swarmcosts(input: { usage: agentusage[]; currency?: string; now: number }): swarmcost {
2305
- return {
2306
- agents: input.usage.length,
2307
- tokens: input.usage.reduce((total, usage) => total + usage.tokens, 0),
2308
- cost: input.usage.reduce((total, usage) => total + usage.cost, 0),
2309
- steps: input.usage.reduce((total, usage) => total + usage.steps, 0),
2310
- ...(input.currency !== undefined && input.currency.trim() !== "" ? { currency: input.currency } : {}),
2311
- computedat: input.now,
2312
- };
2313
- }
2314
-
2315
- /** Rebuilds one agent run from the audit trail: the actions that name the agent order by time into one replay stream the panel scrolls. */
2316
- export function replayagentrun(input: {
2317
- events: Array<{ id: string; kind: string; summary: string; at: number; agentid?: string }>;
2318
- agentid: string;
2319
- }): swarmaction[] {
2320
- return interleavetimeline(
2321
- input.events
2322
- .filter((event) => event.agentid === input.agentid)
2323
- .map((event) => ({
2324
- id: event.id,
2325
- kind: event.kind,
2326
- summary: event.summary,
2327
- at: event.at,
2328
- ...(event.agentid !== undefined ? { agentid: event.agentid } : {}),
2329
- })),
2330
- );
2331
- }