libre-webui 0.29.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/backend/dist/index.d.ts.map +1 -1
  2. package/backend/dist/index.js +24 -7
  3. package/backend/dist/index.js.map +1 -1
  4. package/backend/dist/persistence/postgresAutomationWebhooksMigration.d.ts +9 -0
  5. package/backend/dist/persistence/postgresAutomationWebhooksMigration.d.ts.map +1 -0
  6. package/backend/dist/persistence/postgresAutomationWebhooksMigration.js +38 -0
  7. package/backend/dist/persistence/postgresAutomationWebhooksMigration.js.map +1 -0
  8. package/backend/dist/persistence/postgresMigrationRegistry.d.ts.map +1 -1
  9. package/backend/dist/persistence/postgresMigrationRegistry.js +4 -0
  10. package/backend/dist/persistence/postgresMigrationRegistry.js.map +1 -1
  11. package/backend/dist/persistence/postgresResourceRepositories.d.ts.map +1 -1
  12. package/backend/dist/persistence/postgresResourceRepositories.js +5 -0
  13. package/backend/dist/persistence/postgresResourceRepositories.js.map +1 -1
  14. package/backend/dist/persistence/postgresWorkApprovalsMigration.d.ts +13 -0
  15. package/backend/dist/persistence/postgresWorkApprovalsMigration.d.ts.map +1 -0
  16. package/backend/dist/persistence/postgresWorkApprovalsMigration.js +69 -0
  17. package/backend/dist/persistence/postgresWorkApprovalsMigration.js.map +1 -0
  18. package/backend/dist/persistence/resourceTypes.d.ts +4 -0
  19. package/backend/dist/persistence/resourceTypes.d.ts.map +1 -1
  20. package/backend/dist/persistence/resourceTypes.js.map +1 -1
  21. package/backend/dist/persistence/sqliteMigrations.d.ts +3 -0
  22. package/backend/dist/persistence/sqliteMigrations.d.ts.map +1 -1
  23. package/backend/dist/persistence/sqliteMigrations.js +156 -2
  24. package/backend/dist/persistence/sqliteMigrations.js.map +1 -1
  25. package/backend/dist/persistence/sqliteResourceRepositories.d.ts.map +1 -1
  26. package/backend/dist/persistence/sqliteResourceRepositories.js +7 -0
  27. package/backend/dist/persistence/sqliteResourceRepositories.js.map +1 -1
  28. package/backend/dist/persistence/sqliteToPostgresMigration.d.ts.map +1 -1
  29. package/backend/dist/persistence/sqliteToPostgresMigration.js +32 -1
  30. package/backend/dist/persistence/sqliteToPostgresMigration.js.map +1 -1
  31. package/backend/dist/platform/workPersistence/postgresWorkPersistence.d.ts +10 -1
  32. package/backend/dist/platform/workPersistence/postgresWorkPersistence.d.ts.map +1 -1
  33. package/backend/dist/platform/workPersistence/postgresWorkPersistence.js +85 -5
  34. package/backend/dist/platform/workPersistence/postgresWorkPersistence.js.map +1 -1
  35. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.d.ts +10 -1
  36. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.d.ts.map +1 -1
  37. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js +67 -7
  38. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js.map +1 -1
  39. package/backend/dist/platform/workPersistence/types.d.ts +37 -0
  40. package/backend/dist/platform/workPersistence/types.d.ts.map +1 -1
  41. package/backend/dist/platform/workPersistence/types.js.map +1 -1
  42. package/backend/dist/routes/automations.d.ts.map +1 -1
  43. package/backend/dist/routes/automations.js +83 -1
  44. package/backend/dist/routes/automations.js.map +1 -1
  45. package/backend/dist/routes/ollama.d.ts.map +1 -1
  46. package/backend/dist/routes/ollama.js +64 -0
  47. package/backend/dist/routes/ollama.js.map +1 -1
  48. package/backend/dist/routes/plugins.d.ts.map +1 -1
  49. package/backend/dist/routes/plugins.js +90 -0
  50. package/backend/dist/routes/plugins.js.map +1 -1
  51. package/backend/dist/routes/work.d.ts.map +1 -1
  52. package/backend/dist/routes/work.js +83 -0
  53. package/backend/dist/routes/work.js.map +1 -1
  54. package/backend/dist/services/authService.d.ts +2 -0
  55. package/backend/dist/services/authService.d.ts.map +1 -1
  56. package/backend/dist/services/authService.js +2 -0
  57. package/backend/dist/services/authService.js.map +1 -1
  58. package/backend/dist/services/automationService.d.ts +13 -1
  59. package/backend/dist/services/automationService.d.ts.map +1 -1
  60. package/backend/dist/services/automationService.js +31 -0
  61. package/backend/dist/services/automationService.js.map +1 -1
  62. package/backend/dist/services/notificationService.d.ts.map +1 -1
  63. package/backend/dist/services/notificationService.js +1 -0
  64. package/backend/dist/services/notificationService.js.map +1 -1
  65. package/backend/dist/services/ollamaService.d.ts +12 -0
  66. package/backend/dist/services/ollamaService.d.ts.map +1 -1
  67. package/backend/dist/services/ollamaService.js +26 -0
  68. package/backend/dist/services/ollamaService.js.map +1 -1
  69. package/backend/dist/services/ollamaSettingsService.d.ts +10 -0
  70. package/backend/dist/services/ollamaSettingsService.d.ts.map +1 -0
  71. package/backend/dist/services/ollamaSettingsService.js +83 -0
  72. package/backend/dist/services/ollamaSettingsService.js.map +1 -0
  73. package/backend/dist/services/workAgentGuidance.d.ts +11 -0
  74. package/backend/dist/services/workAgentGuidance.d.ts.map +1 -1
  75. package/backend/dist/services/workAgentGuidance.js +16 -2
  76. package/backend/dist/services/workAgentGuidance.js.map +1 -1
  77. package/backend/dist/services/workAgentService.d.ts +56 -0
  78. package/backend/dist/services/workAgentService.d.ts.map +1 -1
  79. package/backend/dist/services/workAgentService.js +467 -17
  80. package/backend/dist/services/workAgentService.js.map +1 -1
  81. package/backend/dist/services/workApprovalService.d.ts +67 -0
  82. package/backend/dist/services/workApprovalService.d.ts.map +1 -0
  83. package/backend/dist/services/workApprovalService.js +273 -0
  84. package/backend/dist/services/workApprovalService.js.map +1 -0
  85. package/backend/dist/services/workEventService.d.ts.map +1 -1
  86. package/backend/dist/services/workEventService.js +25 -0
  87. package/backend/dist/services/workEventService.js.map +1 -1
  88. package/backend/dist/services/workPolicyService.d.ts +4 -0
  89. package/backend/dist/services/workPolicyService.d.ts.map +1 -1
  90. package/backend/dist/services/workPolicyService.js +19 -0
  91. package/backend/dist/services/workPolicyService.js.map +1 -1
  92. package/backend/dist/services/workRuntimeShared.d.ts +2 -0
  93. package/backend/dist/services/workRuntimeShared.d.ts.map +1 -1
  94. package/backend/dist/services/workRuntimeShared.js.map +1 -1
  95. package/backend/dist/services/workTaskService.d.ts +3 -1
  96. package/backend/dist/services/workTaskService.d.ts.map +1 -1
  97. package/backend/dist/services/workTaskService.js +21 -4
  98. package/backend/dist/services/workTaskService.js.map +1 -1
  99. package/backend/dist/types/index.d.ts +3 -1
  100. package/backend/dist/types/index.d.ts.map +1 -1
  101. package/backend/dist/types/index.js.map +1 -1
  102. package/backend/dist/types/work.d.ts +32 -11
  103. package/backend/dist/types/work.d.ts.map +1 -1
  104. package/backend/dist/websocketServer.d.ts.map +1 -1
  105. package/backend/dist/websocketServer.js +14 -2
  106. package/backend/dist/websocketServer.js.map +1 -1
  107. package/backend/package.json +2 -2
  108. package/deploy/work-computer/Dockerfile +1 -0
  109. package/deploy/work-computer/tint2rc +1 -0
  110. package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
  111. package/frontend/dist/artifact-runtime/lucide-react.js +1 -1
  112. package/frontend/dist/artifact-runtime/mermaid.js +8 -8
  113. package/frontend/dist/artifact-runtime/papaparse.js +2 -2
  114. package/frontend/dist/artifact-runtime/plotly.js +371 -652
  115. package/frontend/dist/assets/index-B6xMZpzA.js +76 -0
  116. package/frontend/dist/assets/index-BjipfsCP.css +1 -0
  117. package/frontend/dist/index.html +28 -28
  118. package/frontend/dist/js/{ArtifactContainer-CTXxnWRo.js → ArtifactContainer-Bn6iJtaJ.js} +1 -1
  119. package/frontend/dist/js/{ArtifactDemoPage-CEYuIhJA.js → ArtifactDemoPage-BLF8Ok-o.js} +1 -1
  120. package/frontend/dist/js/{ArtifactSandboxFrame-DLOy2VOm.js → ArtifactSandboxFrame-Cs1Gu1WB.js} +1 -1
  121. package/frontend/dist/js/{ArtifactSlideOutPanel-DnTTc1VY.js → ArtifactSlideOutPanel-CuKq5cL0.js} +1 -1
  122. package/frontend/dist/js/AutomationModal-Dha4KgLI.js +1 -0
  123. package/frontend/dist/js/AutomationsPage-HcMCRV-D.js +1 -0
  124. package/frontend/dist/js/{AvatarUpload-DzL00yOL.js → AvatarUpload-CFrlSszY.js} +1 -1
  125. package/frontend/dist/js/{Button--pDOakZx.js → Button-CgN11mgY.js} +1 -1
  126. package/frontend/dist/js/{CalendarPage-Bls3TBeZ.js → CalendarPage-D_K-XrG9.js} +1 -1
  127. package/frontend/dist/js/ChannelsPage-D3CoImOT.js +1 -0
  128. package/frontend/dist/js/ChatPage-C0iBDbOq.js +14 -0
  129. package/frontend/dist/js/ConnectModels-D_ZbZQub.js +1 -0
  130. package/frontend/dist/js/{EvaluationsPage-DSgj9dT5.js → EvaluationsPage-DOJLpauA.js} +2 -2
  131. package/frontend/dist/js/FirstTimeSetup-DDIP_4Dd.js +1 -0
  132. package/frontend/dist/js/GalleryPage-C8U_F7Ns.js +1 -0
  133. package/frontend/dist/js/{HomePage-BebX3v-x.js → HomePage-B_aotoDU.js} +1 -1
  134. package/frontend/dist/js/LibreClawPage-cr2N6W00.js +1 -0
  135. package/frontend/dist/js/{Logo-BE9SvCC5.js → Logo-B7o7eaPU.js} +1 -1
  136. package/frontend/dist/js/{LogoMark-C8x1yyP3.js → LogoMark-D9g6ls0h.js} +1 -1
  137. package/frontend/dist/js/{ModelManagerModals-CpWAltRA.js → ModelManagerModals-C-J1d7GL.js} +1 -1
  138. package/frontend/dist/js/NotesPage-Bgr7R_Vm.js +3 -0
  139. package/frontend/dist/js/{PasswordStrengthMeter-Cdq2oQbc.js → PasswordStrengthMeter-DR-mGbLF.js} +1 -1
  140. package/frontend/dist/js/PersonasPage-C3D_6lIx.js +13 -0
  141. package/frontend/dist/js/{PluginUsagePage-jMrpL2Jb.js → PluginUsagePage-DK7MgSiu.js} +1 -1
  142. package/frontend/dist/js/RichMessageContent-7FxrZ4A8.js +1 -0
  143. package/frontend/dist/js/{RichMessageContent-CC9QZQKd.js → RichMessageContent-CQkLI7Dt.js} +1 -1
  144. package/frontend/dist/js/SettingsModal-CIV_Bgsf.js +19 -0
  145. package/frontend/dist/js/{SettingsToggle-C16gtVyi.js → SettingsToggle-Cw4OhW7B.js} +1 -1
  146. package/frontend/dist/js/ShareDialog-BCEVgtbN.js +1 -0
  147. package/frontend/dist/js/{SystemPage-Drd1neLN.js → SystemPage-BNVZtLe9.js} +1 -1
  148. package/frontend/dist/js/{UserManagementPage-CquciWKV.js → UserManagementPage-pPC_Svj9.js} +1 -1
  149. package/frontend/dist/js/WorkPage-Q33nIqkM.js +7 -0
  150. package/frontend/dist/js/{api-2DulPtq1.js → api-D-5hpGht.js} +3 -3
  151. package/frontend/dist/js/api-WR1CTYO5.js +1 -0
  152. package/frontend/dist/js/{appNavigation-DAGh98Yj.js → appNavigation-DfHbXBey.js} +1 -1
  153. package/frontend/dist/js/{appStore-Ci1X0yA-.js → appStore-B136FGdu.js} +2 -2
  154. package/frontend/dist/js/appVersion-Ck7dYiHe.js +1 -0
  155. package/frontend/dist/js/ar-Cq-nC6v9.js +2 -0
  156. package/frontend/dist/js/authStore-6AJLgQsD.js +1 -0
  157. package/frontend/dist/js/authStore-Ckz37pwu.js +4 -0
  158. package/frontend/dist/js/axios-CUerAKcc.js +1 -0
  159. package/frontend/dist/js/{babel-D7QYvEns.js → babel-8afoxhZ7.js} +1 -1
  160. package/frontend/dist/js/bn-BFSKytYs.js +2 -0
  161. package/frontend/dist/js/chatStore-Dpm3g_Ln.js +3 -0
  162. package/frontend/dist/js/classPrivateMethodInitSpec-DE0XIqc_.js +1 -0
  163. package/frontend/dist/js/config-DJwXAf8i.js +1 -0
  164. package/frontend/dist/js/cs-BKWXa629.js +2 -0
  165. package/frontend/dist/js/da-Cra_DJnV.js +2 -0
  166. package/frontend/dist/js/de-BQa6_UuE.js +2 -0
  167. package/frontend/dist/js/es-BBOHFrx1.js +2 -0
  168. package/frontend/dist/js/{estree-D7xT4AeP.js → estree-qxY7ACbu.js} +3 -3
  169. package/frontend/dist/js/{evaluationsApi-Cu5PH_qi.js → evaluationsApi-BKY-maUM.js} +1 -1
  170. package/frontend/dist/js/fr-C8kXs5c0.js +2 -0
  171. package/frontend/dist/js/hi-BEef4bjU.js +2 -0
  172. package/frontend/dist/js/{html-HvA9vEyn.js → html-B_cUU1Gi.js} +2 -2
  173. package/frontend/dist/js/id-Djipic7Y.js +2 -0
  174. package/frontend/dist/js/is-pazl6RLG.js +2 -0
  175. package/frontend/dist/js/it-DobPbTwl.js +2 -0
  176. package/frontend/dist/js/ja-DwYNaMNX.js +2 -0
  177. package/frontend/dist/js/ko-BQfXE7_o.js +2 -0
  178. package/frontend/dist/js/{markdown-DaF0SXNk.js → markdown-BAF0w-jL.js} +3 -3
  179. package/frontend/dist/js/{messageCodeStyles-DKq3DH1W.js → messageCodeStyles-CkxF6yil.js} +2 -2
  180. package/frontend/dist/js/ms-CkPQoE5T.js +2 -0
  181. package/frontend/dist/js/nl-X_QsDAtu.js +2 -0
  182. package/frontend/dist/js/pl-CK39gUsy.js +2 -0
  183. package/frontend/dist/js/{postcss-RCIJRrFm.js → postcss-B2vYq4-H.js} +1 -1
  184. package/frontend/dist/js/pt-C0dWyR_I.js +2 -0
  185. package/frontend/dist/js/{router-vendor-BGP0k6Y2.js → router-vendor-CIwPu612.js} +1 -1
  186. package/frontend/dist/js/ru-sNJkEz1G.js +2 -0
  187. package/frontend/dist/js/{standalone-BMD7E0jY.js → standalone-BqZmfuRy.js} +2 -2
  188. package/frontend/dist/js/sv-Bh-uxdF3.js +2 -0
  189. package/frontend/dist/js/{teamEventStream-C4u_py2D.js → teamEventStream-DUodK0-C.js} +1 -1
  190. package/frontend/dist/js/th-DPVtwZj2.js +2 -0
  191. package/frontend/dist/js/tr-BYNM_Tm7.js +2 -0
  192. package/frontend/dist/js/{ttsBatching-C9kQXFB1.js → ttsBatching-7_-p2nxR.js} +1 -1
  193. package/frontend/dist/js/{typescript-LOiqVRM1.js → typescript-DokwN-5Y.js} +1 -1
  194. package/frontend/dist/js/{ui-DUzqxWMk.js → ui-BK53RPmS.js} +2 -2
  195. package/frontend/dist/js/ui-vendor-CPdYrKhz.js +177 -0
  196. package/frontend/dist/js/uk-kvNBuYin.js +2 -0
  197. package/frontend/dist/js/{useChat-6yDWZSTz.js → useChat-Czg2p4Rr.js} +1 -1
  198. package/frontend/dist/js/useDictation-B2lYEYKn.js +1 -0
  199. package/frontend/dist/js/useQuery-ClAiNxQ7.js +1 -0
  200. package/frontend/dist/js/{useTranslation-DrnYb6Cx.js → useTranslation-C7GwbUQA.js} +1 -1
  201. package/frontend/dist/js/{utils-D9UstwTG.js → utils-nNyg0lfc.js} +1 -1
  202. package/frontend/dist/js/utils-vendor-CeUf_ngc.js +9 -0
  203. package/frontend/dist/js/vi-CmLoaP0K.js +2 -0
  204. package/frontend/dist/js/workApi-BoJb9aVF.js +1 -0
  205. package/frontend/dist/js/{workApi-BwDBwG62.js → workApi-CFH097y5.js} +3 -3
  206. package/frontend/dist/js/{yaml-CyhEdlt2.js → yaml-BXEpxPn2.js} +2 -2
  207. package/frontend/dist/js/zh-D2zvmvrj.js +2 -0
  208. package/frontend/dist/sw.js +7 -1
  209. package/package.json +5 -5
  210. package/frontend/dist/assets/index-O8rtJINp.css +0 -1
  211. package/frontend/dist/assets/index-f1s6QpC5.js +0 -93
  212. package/frontend/dist/js/AutomationModal-DB3lrK2K.js +0 -1
  213. package/frontend/dist/js/AutomationsPage-D3v0Cra1.js +0 -1
  214. package/frontend/dist/js/ChannelsPage-BnoCniNf.js +0 -1
  215. package/frontend/dist/js/ChatPage-BMcigJWr.js +0 -14
  216. package/frontend/dist/js/FirstTimeSetup-BRysXCjP.js +0 -1
  217. package/frontend/dist/js/GalleryPage-DUcM4dg0.js +0 -1
  218. package/frontend/dist/js/LibreClawPage-A37KF9_G.js +0 -1
  219. package/frontend/dist/js/ModelSelector-C1ipX-44.js +0 -1
  220. package/frontend/dist/js/NotesPage-CXSz7Hde.js +0 -3
  221. package/frontend/dist/js/PersonasPage-CiOBffF3.js +0 -13
  222. package/frontend/dist/js/RichMessageContent-CU6WTuOs.js +0 -1
  223. package/frontend/dist/js/SettingsModal-DP_wKehV.js +0 -19
  224. package/frontend/dist/js/ShareDialog-BW8iGd_i.js +0 -1
  225. package/frontend/dist/js/WorkPage-BgASwAGs.js +0 -6
  226. package/frontend/dist/js/api-BmIdrQfK.js +0 -1
  227. package/frontend/dist/js/appVersion-BRMnr50t.js +0 -1
  228. package/frontend/dist/js/ar-CpmN0TY8.js +0 -2
  229. package/frontend/dist/js/authStore-DIvsSaOt.js +0 -4
  230. package/frontend/dist/js/authStore-DoxafXFR.js +0 -1
  231. package/frontend/dist/js/axios-C8xWfUG8.js +0 -1
  232. package/frontend/dist/js/bn-BwrMg_Oh.js +0 -2
  233. package/frontend/dist/js/chatStore-DkpmZLOG.js +0 -3
  234. package/frontend/dist/js/classPrivateMethodInitSpec-DUpGnGHL.js +0 -1
  235. package/frontend/dist/js/config-Ckn9cand.js +0 -1
  236. package/frontend/dist/js/cs-f72Fnxz3.js +0 -2
  237. package/frontend/dist/js/da-qp8lOgkJ.js +0 -2
  238. package/frontend/dist/js/de-CpcLHr97.js +0 -2
  239. package/frontend/dist/js/es-gZHsxjG4.js +0 -2
  240. package/frontend/dist/js/fr-BY5aUofi.js +0 -2
  241. package/frontend/dist/js/hi-KCn9gIiT.js +0 -2
  242. package/frontend/dist/js/id-C7U6kS0J.js +0 -2
  243. package/frontend/dist/js/is-N9Ek-Usz.js +0 -2
  244. package/frontend/dist/js/it-CqaqWB8D.js +0 -2
  245. package/frontend/dist/js/ja-C5f9HAm1.js +0 -2
  246. package/frontend/dist/js/ko-BuoboDvb.js +0 -2
  247. package/frontend/dist/js/ms-Cu-UWc6j.js +0 -2
  248. package/frontend/dist/js/nl-nOUc4C_i.js +0 -2
  249. package/frontend/dist/js/pl-BKKwFhxj.js +0 -2
  250. package/frontend/dist/js/pt-B18-KYtS.js +0 -2
  251. package/frontend/dist/js/ru-DflEWNyb.js +0 -2
  252. package/frontend/dist/js/sv-CU-nPjr8.js +0 -2
  253. package/frontend/dist/js/th-dOxm2Hhl.js +0 -2
  254. package/frontend/dist/js/tr-7ThM4F8q.js +0 -2
  255. package/frontend/dist/js/ui-vendor-C0KwbTD4.js +0 -177
  256. package/frontend/dist/js/uk-Cl7Pk0Yf.js +0 -2
  257. package/frontend/dist/js/useQuery-CoWV2PmJ.js +0 -1
  258. package/frontend/dist/js/utils-vendor-DtsgPwcz.js +0 -9
  259. package/frontend/dist/js/vi-eljYUBOk.js +0 -2
  260. package/frontend/dist/js/workApi-uXUZSjsf.js +0 -1
  261. package/frontend/dist/js/zh-B0sinUfQ.js +0 -2
@@ -18,6 +18,9 @@ import workModelProviderService, { WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY, WORK_
18
18
  import workEventService from './workEventService.js';
19
19
  import { buildWorkAgentSystemPrompt, buildWorkBudgetExhaustionPrompt, buildWorkComputerAmbiguityPrompt, buildWorkComputerStallPrompt, buildWorkEmptyRoundNudgePrompt, buildWorkScreenshotsUnsupportedPrompt, buildWorkStatusBlurbPrompt, workAgentSkillsForContext, WORK_WRITE_FILE_RECOMMENDED_CHARS, workToolCallBudget, } from './workAgentGuidance.js';
20
20
  import workPolicyService from './workPolicyService.js';
21
+ import workApprovalService, { GATED_WORK_TOOLS, } from './workApprovalService.js';
22
+ import { actorCanUseTools, buildToolCatalog, executeToolCall, } from './toolGatewayService.js';
23
+ import { getToolServer, hasToolServerCredential } from './toolServerService.js';
21
24
  import workRuntimeService, { WORK_COMPUTER_ACTION_LIMIT, } from './workRuntimeService.js';
22
25
  import workScreenControlService, { WORK_SCREEN_ASSIST_TIMEOUT_MS, } from './workScreenControlService.js';
23
26
  import workComputerTeachService from './workComputerTeachService.js';
@@ -91,6 +94,31 @@ export const WORK_WEB_SEARCH_TOOL_SCHEMA = functionTool('web_search', 'Search th
91
94
  description: "Optional result count. Defaults to 5, capped by the server's configured limit.",
92
95
  },
93
96
  }, ['query']);
97
+ const externalToolSchema = (tool) => ({
98
+ type: 'function',
99
+ function: {
100
+ name: tool.name,
101
+ description: tool.description ??
102
+ `Tool ${tool.toolName} on the connected server ${tool.serverName ?? tool.serverId ?? ''}`.trim(),
103
+ parameters: tool.paramsSchema ?? { type: 'object', properties: {} },
104
+ },
105
+ });
106
+ // How long an unanswered takeover request keeps the screen session alive
107
+ // after its wait times out, so a user arriving late from the notification
108
+ // still finds the screen the agent was blocked on.
109
+ export const WORK_TAKEOVER_GRACE_MS = 15 * 60000;
110
+ // Delegation stays shallow by design: a delegated run cannot delegate
111
+ // further, so two agents can never ping-pong requests at each other.
112
+ export const WORK_DELEGATION_MAX_PEERS = 12;
113
+ const WORK_DELEGATION_MESSAGE_MAX_CHARS = 8000;
114
+ const WORK_DELEGATION_REPORT_MAX_CHARS = 20000;
115
+ // Offered per run, only to hired agents with at least one peer and never
116
+ // inside a delegated run: coordination flows by messages between separate
117
+ // sandboxes, not by shared computers.
118
+ export const WORK_MESSAGE_AGENT_TOOL_SCHEMA = functionTool('message_agent', "Delegate a request to another of the user's hired agents by name. Asynchronous: this returns immediately, the other agent works in its own separate workspace, and its report arrives in this conversation as a message when it finishes. The other agent cannot see this conversation or this workspace.", {
119
+ agent: stringProperty("The target agent's exact name, as listed in your peer roster."),
120
+ message: stringProperty('The request to send. Include every detail the agent needs, because it cannot see this conversation.'),
121
+ }, ['agent', 'message']);
94
122
  // Offered only when the task's policy enables the Work Computer and the
95
123
  // task has network access: the agent's eyes and hands on the GUI session.
96
124
  export const WORK_COMPUTER_TOOL_SCHEMAS = [
@@ -180,8 +208,11 @@ export const WORK_COMPUTER_TOOL_SCHEMAS = [
180
208
  reason: stringProperty('What you need the user to do on the screen, stated concretely.'),
181
209
  }, ['reason']),
182
210
  ];
183
- export async function workToolSchemasForTask(task) {
211
+ export async function workToolSchemasForTask(task, options = {}) {
184
212
  const schemas = [...WORK_TOOL_SCHEMAS];
213
+ if (options.messageAgent === true) {
214
+ schemas.push(WORK_MESSAGE_AGENT_TOOL_SCHEMA);
215
+ }
185
216
  if (task.networkEnabled &&
186
217
  (await isWebSearchAvailable()) &&
187
218
  (await userCanUseWebSearch(await userModel.getUserById(task.userId)))) {
@@ -192,6 +223,27 @@ export async function workToolSchemasForTask(task) {
192
223
  }
193
224
  return schemas;
194
225
  }
226
+ /** The delegation marker on the message that started a run, if any. */
227
+ export function delegationSourceOf(messages, runId) {
228
+ for (const message of messages) {
229
+ if (message.runId !== runId || message.role !== 'user')
230
+ continue;
231
+ const delegation = message.metadata?.delegation;
232
+ if (!delegation || typeof delegation !== 'object')
233
+ continue;
234
+ const source = delegation;
235
+ if (typeof source.fromTaskId === 'string' &&
236
+ typeof source.fromRunId === 'string' &&
237
+ typeof source.fromAgent === 'string') {
238
+ return {
239
+ fromTaskId: source.fromTaskId,
240
+ fromRunId: source.fromRunId,
241
+ fromAgent: source.fromAgent,
242
+ };
243
+ }
244
+ }
245
+ return undefined;
246
+ }
195
247
  export class WorkAgentService {
196
248
  constructor() {
197
249
  this.controllers = new Map();
@@ -515,6 +567,7 @@ export class WorkAgentService {
515
567
  const durableAttemptIdentity = String((await getDurableJobRuntime().service.getByIdempotency(userId, WORK_EXECUTE_IDEMPOTENCY_SCOPE, runId))?.attemptCount ?? 0);
516
568
  this.controllers.set(runId, controller);
517
569
  let task;
570
+ let runDelegation;
518
571
  let releaseExecutionLease;
519
572
  let executionContainerSettled = false;
520
573
  const settleExecutionContainer = async () => {
@@ -604,7 +657,29 @@ export class WorkAgentService {
604
657
  };
605
658
  const providerRoutingFingerprint = await workModelProviderService.getRoutingFingerprint(run.model, providerSelection, userId);
606
659
  const providerStateScope = await workModelProviderService.getResponsesStateScope(run.model, providerSelection, userId);
607
- const messages = await this.contextMessages(task, roundLimit, computerAvailable, taughtSkills, providerStateScope, run, persona);
660
+ // A5 approvals: whether side-effecting tool calls pause for review.
661
+ // Resolved once per run — the policy force-flag or the task opt-in;
662
+ // per-call rules are re-read at gate time so an Always-allow decision
663
+ // takes effect on the very next call.
664
+ const approvalsActive = (await workPolicyService.resolve(task.policyId)).approvalsRequired ===
665
+ true || task.approvalsEnabled === true;
666
+ // A6 delegation: a run started by another agent reports back on
667
+ // completion and cannot delegate further (no ping-pong loops). Peers
668
+ // are the user's other hired agents, injected into the roster.
669
+ runDelegation = delegationSourceOf(await workTaskService.getMessages(taskId), runId);
670
+ const peerAgents = task.isAgent === true && !runDelegation
671
+ ? (await workTaskService.listTaskRecords(userId))
672
+ .filter(record => record.isAgent && record.id !== task.id)
673
+ .slice(0, WORK_DELEGATION_MAX_PEERS)
674
+ .map(record => ({
675
+ name: record.title,
676
+ ...(record.statusBlurb ? { status: record.statusBlurb } : {}),
677
+ }))
678
+ : [];
679
+ // Connected tool servers (MCP/OpenAPI): resolved once per run so the
680
+ // offered set is stable across rounds.
681
+ const externalTools = await this.externalToolsForRun(task);
682
+ const messages = await this.contextMessages(task, roundLimit, computerAvailable, taughtSkills, providerStateScope, run, persona, approvalsActive, peerAgents, runDelegation, externalTools ? [...externalTools.byName.keys()] : []);
608
683
  let totalToolCalls = 0;
609
684
  let accumulatedInputTokens = 0;
610
685
  let accumulatedOutputTokens = 0;
@@ -680,7 +755,12 @@ export class WorkAgentService {
680
755
  response = await workModelProviderService.generateChatStreamResponse({
681
756
  model: run.model,
682
757
  messages,
683
- tools: await workToolSchemasForTask(task),
758
+ tools: [
759
+ ...(await workToolSchemasForTask(task, {
760
+ messageAgent: peerAgents.length > 0,
761
+ })),
762
+ ...(externalTools?.schemas ?? []),
763
+ ],
684
764
  stream: true,
685
765
  }, providerSelection, userId, {
686
766
  onContent: delta => contentStream.push(delta),
@@ -844,10 +924,15 @@ export class WorkAgentService {
844
924
  sourceKey: `work-run:${runId}:completed`,
845
925
  });
846
926
  }
927
+ await this.reportDelegationOutcome(task, runDelegation, 'completed', finalContent);
847
928
  return;
848
929
  }
849
930
  const toolValidationErrors = new Map();
850
931
  for (const call of toolCalls) {
932
+ // Connected tools validate their own arguments in the gateway;
933
+ // static validation only covers the built-in registry.
934
+ if (externalTools?.byName.has(call.function.name))
935
+ continue;
851
936
  const validationError = validateToolCallArguments(call);
852
937
  if (validationError)
853
938
  toolValidationErrors.set(call, validationError);
@@ -873,8 +958,18 @@ export class WorkAgentService {
873
958
  }
874
959
  for (const call of toolCalls) {
875
960
  await this.throwIfCancelled(runId, controller);
961
+ const externalEntry = externalTools?.byName.get(call.function.name);
876
962
  const toolCallMetadata = {
877
963
  ...summarizeToolCall(call),
964
+ ...(externalEntry
965
+ ? {
966
+ external: true,
967
+ ...(externalEntry.serverName
968
+ ? { server: externalEntry.serverName }
969
+ : {}),
970
+ argumentsPreview: JSON.stringify(call.function.arguments ?? {}).slice(0, 500),
971
+ }
972
+ : {}),
878
973
  round: round + 1,
879
974
  };
880
975
  const toolCallMessage = await workTaskService.addMessage(taskId, runId, 'assistant', 'tool_call', `Calling ${call.function.name}`, toolCallMetadata);
@@ -908,14 +1003,34 @@ export class WorkAgentService {
908
1003
  if (hasInvalidToolArguments) {
909
1004
  throw new WorkAgentHttpError('This tool call was not executed because another call in the same model response had incomplete or invalid arguments. Retry the batch with smaller payloads.', 400, 'WORK_TOOL_BATCH_NOT_EXECUTED');
910
1005
  }
911
- const result = await this.executeTool(task, call, {
912
- signal: controller.signal,
913
- computer: computerContext,
914
- runId,
915
- });
916
- toolOutput = result.content;
917
- toolImages = result.images;
918
- toolMetadata = { ...toolMetadata, ...result.metadata };
1006
+ const approvalVerdict = approvalsActive &&
1007
+ (GATED_WORK_TOOLS.has(call.function.name) ||
1008
+ externalTools?.byName.get(call.function.name)?.sideEffect ===
1009
+ true)
1010
+ ? await this.awaitToolApproval(task, call, toolCallMetadata, runId, userId, controller.signal, durableAttemptIdentity)
1011
+ : 'approved';
1012
+ if (approvalVerdict === 'denied') {
1013
+ toolOutput =
1014
+ 'The user denied this action. Do not retry it as-is; adjust your plan or ask the user what to do instead.';
1015
+ toolMetadata = { ...toolMetadata, approval: 'denied' };
1016
+ }
1017
+ else if (approvalVerdict === 'expired') {
1018
+ toolOutput =
1019
+ 'This action was not executed: the approval request timed out with no decision from the user.';
1020
+ toolMetadata = { ...toolMetadata, approval: 'expired' };
1021
+ }
1022
+ else {
1023
+ const result = await this.executeTool(task, call, {
1024
+ signal: controller.signal,
1025
+ computer: computerContext,
1026
+ runId,
1027
+ delegated: runDelegation !== undefined,
1028
+ external: externalTools,
1029
+ });
1030
+ toolOutput = result.content;
1031
+ toolImages = result.images;
1032
+ toolMetadata = { ...toolMetadata, ...result.metadata };
1033
+ }
919
1034
  }
920
1035
  catch (error) {
921
1036
  toolOutput =
@@ -970,6 +1085,12 @@ export class WorkAgentService {
970
1085
  // text observation, and only the most recent screenshots stay in
971
1086
  // model context so a long computer session cannot flood tokens.
972
1087
  retainRecentWorkImages(messages);
1088
+ // An unanswered approval hands the run off instead of burning the
1089
+ // remaining budget on decisions no one is there to make.
1090
+ if (toolMetadata.approval === 'expired') {
1091
+ budgetReason = 'approval-timeout';
1092
+ break roundLoop;
1093
+ }
973
1094
  if ((call.function.name === 'computer_act' ||
974
1095
  call.function.name === 'computer_observe') &&
975
1096
  typeof toolMetadata.screenshotSha256 === 'string') {
@@ -1045,7 +1166,9 @@ export class WorkAgentService {
1045
1166
  role: 'user',
1046
1167
  content: `${buildWorkBudgetExhaustionPrompt()}\n${budgetReason === 'computer-stall'
1047
1168
  ? 'The run was stopped because repeated computer actions kept producing an unchanged screen even after a recovery notice.'
1048
- : `The ${budgetReason} budget was reached.`}`,
1169
+ : budgetReason === 'approval-timeout'
1170
+ ? 'The run was stopped because an action was still waiting for user approval when the request timed out.'
1171
+ : `The ${budgetReason} budget was reached.`}`,
1049
1172
  },
1050
1173
  ],
1051
1174
  tools: [],
@@ -1127,6 +1250,7 @@ export class WorkAgentService {
1127
1250
  sourceKey: `work-run:${runId}:needs_input`,
1128
1251
  });
1129
1252
  }
1253
+ await this.reportDelegationOutcome(task, runDelegation, 'needs_input', handoffContent);
1130
1254
  }
1131
1255
  catch (error) {
1132
1256
  const currentRun = await workTaskService.getRun(runId);
@@ -1146,6 +1270,7 @@ export class WorkAgentService {
1146
1270
  status: 'cancelled',
1147
1271
  error: 'Cancelled by user.',
1148
1272
  }, 'terminal:cancelled');
1273
+ await this.reportDelegationOutcome(task, runDelegation, 'cancelled');
1149
1274
  }
1150
1275
  }
1151
1276
  catch (cleanupError) {
@@ -1171,6 +1296,9 @@ export class WorkAgentService {
1171
1296
  status: 'cancelled',
1172
1297
  error: 'Cancelled by user.',
1173
1298
  }, 'terminal:cancelled');
1299
+ if (task) {
1300
+ await this.reportDelegationOutcome(task, runDelegation, 'cancelled');
1301
+ }
1174
1302
  }
1175
1303
  return;
1176
1304
  }
@@ -1199,6 +1327,9 @@ export class WorkAgentService {
1199
1327
  sourceKey: `work-run:${runId}:failed`,
1200
1328
  });
1201
1329
  }
1330
+ if (task) {
1331
+ await this.reportDelegationOutcome(task, runDelegation, 'failed', message);
1332
+ }
1202
1333
  }
1203
1334
  finally {
1204
1335
  this.controllers.delete(runId);
@@ -1247,7 +1378,7 @@ export class WorkAgentService {
1247
1378
  throw new WorkAgentHttpError('The provider routing changed while this Work run was active. Start a new run with the updated provider configuration.', 409, 'WORK_PROVIDER_ROUTING_CHANGED');
1248
1379
  }
1249
1380
  }
1250
- async contextMessages(task, roundLimit, computerAvailable, taughtSkills, providerStateScope, provider = task, persona) {
1381
+ async contextMessages(task, roundLimit, computerAvailable, taughtSkills, providerStateScope, provider = task, persona, approvalsActive, peerAgents = [], delegatedBy, connectedTools = []) {
1251
1382
  const persisted = restorePersistedWorkContext(await workTaskService.getRecentModelContextMessages(task.id, 30), provider, providerStateScope);
1252
1383
  return [
1253
1384
  {
@@ -1257,6 +1388,10 @@ export class WorkAgentService {
1257
1388
  computerAvailable,
1258
1389
  ...(taughtSkills.length > 0 ? { taughtSkills } : {}),
1259
1390
  ...(persona ? { persona } : {}),
1391
+ ...(approvalsActive ? { approvalsActive } : {}),
1392
+ ...(peerAgents.length > 0 ? { peerAgents } : {}),
1393
+ ...(delegatedBy ? { delegatedBy: delegatedBy.fromAgent } : {}),
1394
+ ...(connectedTools.length > 0 ? { connectedTools } : {}),
1260
1395
  previewPort: workRuntimeService.previewPort,
1261
1396
  roundBudget: roundLimit,
1262
1397
  commandTimeoutMs: workRuntimeService.limits.commandTimeoutMs,
@@ -1268,6 +1403,32 @@ export class WorkAgentService {
1268
1403
  }
1269
1404
  async executeTool(task, call, context = {}) {
1270
1405
  const args = call.function.arguments;
1406
+ // Connected tool servers: dynamic names resolved through the run's
1407
+ // catalog before the built-in switch. Every guard the chat surface has
1408
+ // is re-applied inside executeToolCall (visibility, enablement,
1409
+ // credentials, SSRF-hardened egress).
1410
+ const external = context.external?.byName.get(call.function.name);
1411
+ if (external && context.external) {
1412
+ if (!task.networkEnabled) {
1413
+ throw new WorkAgentHttpError('Connected tools are not available for this task.', 403, 'WORK_TOOLS_UNAVAILABLE');
1414
+ }
1415
+ const result = await executeToolCall({
1416
+ actor: context.external.actor,
1417
+ tool: external,
1418
+ argumentsJson: JSON.stringify(args ?? {}),
1419
+ selection: context.external.selection,
1420
+ ...(context.signal ? { signal: context.signal } : {}),
1421
+ });
1422
+ return {
1423
+ content: result.text.slice(0, workRuntimeService.limits.maxOutputChars),
1424
+ metadata: {
1425
+ external: true,
1426
+ ...(external.serverName ? { server: external.serverName } : {}),
1427
+ ...(result.isError ? { error: true } : {}),
1428
+ ...(result.truncated ? { outputTruncated: true } : {}),
1429
+ },
1430
+ };
1431
+ }
1271
1432
  switch (call.function.name) {
1272
1433
  case 'list_files': {
1273
1434
  const result = await workRuntimeService.listFiles(task, optionalString(args.path) || '.');
@@ -1368,6 +1529,61 @@ export class WorkAgentService {
1368
1529
  }
1369
1530
  return result;
1370
1531
  }
1532
+ case 'message_agent': {
1533
+ if (context.delegated === true) {
1534
+ return {
1535
+ content: 'This run was itself delegated by another agent, and delegated runs cannot delegate further. Complete the request yourself and include any blocker in your report.',
1536
+ metadata: { outcome: 'depth-limited' },
1537
+ };
1538
+ }
1539
+ const agentName = requiredString(args.agent, 'agent')
1540
+ .trim()
1541
+ .slice(0, 200);
1542
+ const message = requiredString(args.message, 'message').slice(0, WORK_DELEGATION_MESSAGE_MAX_CHARS);
1543
+ const peers = (await workTaskService.listTaskRecords(task.userId)).filter(record => record.isAgent && record.id !== task.id);
1544
+ const target = peers.find(record => record.title.trim().toLowerCase() === agentName.toLowerCase());
1545
+ if (!target) {
1546
+ const roster = peers
1547
+ .slice(0, WORK_DELEGATION_MAX_PEERS)
1548
+ .map(record => record.title)
1549
+ .join(', ');
1550
+ return {
1551
+ content: roster
1552
+ ? `No hired agent is named "${agentName}". The user's agents are: ${roster}. Use the exact name, or handle the request yourself.`
1553
+ : 'The user has no other hired agents, so there is no one to delegate to. Handle the request yourself.',
1554
+ metadata: { outcome: 'unknown-agent' },
1555
+ };
1556
+ }
1557
+ try {
1558
+ const detail = await workTaskService.createRun(target.id, task.userId, message, undefined, undefined, {
1559
+ delegation: {
1560
+ fromTaskId: task.id,
1561
+ fromRunId: context.runId ?? '',
1562
+ fromAgent: task.title,
1563
+ },
1564
+ });
1565
+ return {
1566
+ content: `Delegated to ${target.title}. It works in its own separate workspace and cannot see this conversation; its report will arrive here as a message when it finishes. Continue with other work or wrap up your response.`,
1567
+ metadata: {
1568
+ outcome: 'delegated',
1569
+ delegatedTaskId: target.id,
1570
+ ...(detail.activeRun?.id
1571
+ ? { delegatedRunId: detail.activeRun.id }
1572
+ : {}),
1573
+ delegatedAgent: target.title,
1574
+ },
1575
+ };
1576
+ }
1577
+ catch (error) {
1578
+ if (error instanceof WorkConflictError) {
1579
+ return {
1580
+ content: `${target.title} is busy with another run right now. Try again later, do the work yourself, or tell the user.`,
1581
+ metadata: { outcome: 'busy' },
1582
+ };
1583
+ }
1584
+ throw error;
1585
+ }
1586
+ }
1371
1587
  case 'request_takeover': {
1372
1588
  await this.assertComputerToolsAvailable(task);
1373
1589
  if ((await workPolicyService.resolve(task.policyId)).takeoverEnabled ===
@@ -1390,10 +1606,44 @@ export class WorkAgentService {
1390
1606
  ? `work-takeover:${context.runId}`
1391
1607
  : `work-takeover:${task.id}:${Date.now()}`,
1392
1608
  });
1393
- const outcome = await workScreenControlService.waitForAssist(task.id, reason, {
1394
- timeoutMs: WORK_SCREEN_ASSIST_TIMEOUT_MS,
1395
- signal: context.signal,
1396
- });
1609
+ // Hold the screen session for the whole wait — and, when nobody
1610
+ // answers in time, through a grace window after it. Without the
1611
+ // hold, a run_command settle or run-end cleanup stops the sandbox,
1612
+ // and a user who arrives from the notification a few minutes late
1613
+ // takes over a fresh start page instead of the screen the agent
1614
+ // was blocked on.
1615
+ let releaseTakeoverHold;
1616
+ try {
1617
+ releaseTakeoverHold =
1618
+ await workRuntimeService.beginScreenSession(task);
1619
+ }
1620
+ catch (error) {
1621
+ // Best effort: a hold failure must never block the request.
1622
+ logger.warn(`Could not hold the screen for a takeover request on ${task.id}:`, error);
1623
+ }
1624
+ let holdThroughGrace = false;
1625
+ let outcome;
1626
+ try {
1627
+ outcome = await workScreenControlService.waitForAssist(task.id, reason, {
1628
+ timeoutMs: WORK_SCREEN_ASSIST_TIMEOUT_MS,
1629
+ signal: context.signal,
1630
+ });
1631
+ holdThroughGrace = outcome === 'timeout';
1632
+ }
1633
+ finally {
1634
+ if (releaseTakeoverHold) {
1635
+ const release = releaseTakeoverHold;
1636
+ if (holdThroughGrace) {
1637
+ const timer = setTimeout(() => void release(), WORK_TAKEOVER_GRACE_MS);
1638
+ timer.unref?.();
1639
+ }
1640
+ else {
1641
+ // An active takeover (or a cancelled run) carries its own
1642
+ // viewer holds — or none should remain at all.
1643
+ void release();
1644
+ }
1645
+ }
1646
+ }
1397
1647
  if (outcome === 'released') {
1398
1648
  return {
1399
1649
  content: 'The user took control of the computer and has handed it back. Use computer_observe to see the current screen before continuing.',
@@ -1442,6 +1692,167 @@ export class WorkAgentService {
1442
1692
  * never fails the run. The source key is run-scoped because dedupe is
1443
1693
  * permanent per user until the inbox cap prunes it.
1444
1694
  */
1695
+ /**
1696
+ * The user's configured tool servers, resolved once per run. Backend
1697
+ * egress, so an offline task offers none — the web_search rationale.
1698
+ * Servers whose per-user credential is missing are filtered at offer
1699
+ * time, because an autonomous run cannot prompt for one; persona
1700
+ * bindings narrow the servers exactly as they do in chat.
1701
+ */
1702
+ async externalToolsForRun(task) {
1703
+ if (!task.networkEnabled)
1704
+ return undefined;
1705
+ try {
1706
+ const user = await userModel.getUserById(task.userId);
1707
+ const actor = {
1708
+ userId: task.userId,
1709
+ ...(user?.role ? { role: user.role } : {}),
1710
+ };
1711
+ if (!(await actorCanUseTools(actor)))
1712
+ return undefined;
1713
+ let serverIds;
1714
+ if (task.personaId) {
1715
+ try {
1716
+ const { personaService } = await import('./personaService.js');
1717
+ const bindings = (await personaService.getPersonaById(task.personaId, task.userId))?.bindings;
1718
+ if (bindings?.tool_server_ids)
1719
+ serverIds = bindings.tool_server_ids;
1720
+ }
1721
+ catch {
1722
+ // A vanished persona leaves the selection unrestricted, matching
1723
+ // how the run already degrades to no persona identity.
1724
+ }
1725
+ }
1726
+ const catalog = await buildToolCatalog(actor, {}, {
1727
+ builtinTools: [],
1728
+ ...(serverIds ? { serverIds } : {}),
1729
+ });
1730
+ const builtinNames = new Set([
1731
+ ...WORK_TOOL_SCHEMAS,
1732
+ WORK_WEB_SEARCH_TOOL_SCHEMA,
1733
+ WORK_MESSAGE_AGENT_TOOL_SCHEMA,
1734
+ ...WORK_COMPUTER_TOOL_SCHEMAS,
1735
+ ].map(schema => schema.function?.name ?? ''));
1736
+ const credentialed = new Map();
1737
+ const byName = new Map();
1738
+ const schemas = [];
1739
+ for (const tool of catalog.tools) {
1740
+ if (tool.source === 'builtin' || !tool.serverId)
1741
+ continue;
1742
+ if (builtinNames.has(tool.name))
1743
+ continue;
1744
+ let usable = credentialed.get(tool.serverId);
1745
+ if (usable === undefined) {
1746
+ const server = await getToolServer(tool.serverId);
1747
+ usable =
1748
+ server !== undefined &&
1749
+ server !== null &&
1750
+ (server.authMode === 'none' ||
1751
+ (await hasToolServerCredential(task.userId, tool.serverId)));
1752
+ credentialed.set(tool.serverId, usable);
1753
+ }
1754
+ if (!usable)
1755
+ continue;
1756
+ byName.set(tool.name, tool);
1757
+ schemas.push(externalToolSchema(tool));
1758
+ }
1759
+ if (byName.size === 0)
1760
+ return undefined;
1761
+ return { actor, byName, schemas, selection: catalog.selection };
1762
+ }
1763
+ catch (error) {
1764
+ logger.warn(`Connected tools are unavailable for task ${task.id}:`, error);
1765
+ return undefined;
1766
+ }
1767
+ }
1768
+ /**
1769
+ * Deliver a delegated run's outcome back into the delegating agent's
1770
+ * conversation. Mid-run when the delegator is still working (picked up at
1771
+ * its next round boundary), an ordinary conversation message otherwise —
1772
+ * never an auto-started run, so two agents cannot ping-pong each other.
1773
+ * Best effort: the delegated run's own rows are the source of truth.
1774
+ */
1775
+ async reportDelegationOutcome(task, delegation, status, summary) {
1776
+ if (!delegation)
1777
+ return;
1778
+ try {
1779
+ const delegator = await workTaskService.getTaskRecord(delegation.fromTaskId, task.userId);
1780
+ if (!delegator)
1781
+ return;
1782
+ const statusLine = status === 'completed'
1783
+ ? 'finished'
1784
+ : status === 'needs_input'
1785
+ ? 'stopped and needs input'
1786
+ : status === 'failed'
1787
+ ? 'failed'
1788
+ : 'was cancelled';
1789
+ const bounded = (summary ?? '')
1790
+ .trim()
1791
+ .slice(0, WORK_DELEGATION_REPORT_MAX_CHARS);
1792
+ const content = `Report from ${task.title} — the delegated request ${statusLine}.\n\n${bounded || '(no summary was produced)'}`;
1793
+ const activeRun = await workTaskService.getActiveRun(delegation.fromTaskId);
1794
+ const deliverMidRun = activeRun !== undefined && isActiveRunStatus(activeRun.status);
1795
+ await workTaskService.addMessage(delegation.fromTaskId, deliverMidRun ? activeRun.id : undefined, 'user', 'message', content, {
1796
+ ...(deliverMidRun ? { midRun: true } : {}),
1797
+ delegationReport: {
1798
+ fromTaskId: task.id,
1799
+ fromAgent: task.title,
1800
+ status,
1801
+ },
1802
+ });
1803
+ }
1804
+ catch (error) {
1805
+ logger.warn(`Delegation report from task ${task.id} failed:`, error);
1806
+ }
1807
+ }
1808
+ /**
1809
+ * The approval gate. Rules are re-read per call so an Always-allow
1810
+ * decision covers the very next matching call; a pending approval rides
1811
+ * the event stream (and the snapshot) so any open view can decide, and
1812
+ * notifies because the user may not be watching. Denial and timeout come
1813
+ * back as verdicts the dispatch loop turns into ordinary tool results —
1814
+ * never silent execution.
1815
+ */
1816
+ async awaitToolApproval(task, call, summary, runId, userId, signal, durableAttemptIdentity) {
1817
+ if (await workApprovalService.callIsPreapproved(task.id, call.function.name, summary)) {
1818
+ return 'approved';
1819
+ }
1820
+ const pending = await workApprovalService.createPending({
1821
+ taskId: task.id,
1822
+ runId,
1823
+ userId,
1824
+ toolCallId: call.id,
1825
+ toolName: call.function.name,
1826
+ summary,
1827
+ });
1828
+ await workEventService.publish(task.id, runId, 'approval', {
1829
+ approvalId: pending.approvalId,
1830
+ toolCallId: call.id,
1831
+ name: call.function.name,
1832
+ summary: pending.summary,
1833
+ status: 'pending',
1834
+ expiresAt: pending.expiresAt,
1835
+ }, `approval:${pending.approvalId}:pending`, durableAttemptIdentity);
1836
+ await this.notifyAgentLifecycle(task, {
1837
+ type: 'work-approval',
1838
+ title: `${task.title} is waiting for your approval`,
1839
+ body: summarizeApprovalBody(call.function.name, summary),
1840
+ sourceKey: `work-approval:${pending.approvalId}`,
1841
+ });
1842
+ const verdict = await workApprovalService.waitForDecision(task.id, pending.approvalId, signal);
1843
+ const status = verdict.status === 'approved'
1844
+ ? 'approved'
1845
+ : verdict.status === 'denied'
1846
+ ? 'denied'
1847
+ : 'expired';
1848
+ await workEventService.publish(task.id, runId, 'approval', {
1849
+ approvalId: pending.approvalId,
1850
+ toolCallId: call.id,
1851
+ name: call.function.name,
1852
+ status,
1853
+ }, `approval:${pending.approvalId}:resolved`, durableAttemptIdentity);
1854
+ return status;
1855
+ }
1445
1856
  async notifyAgentLifecycle(task, input) {
1446
1857
  try {
1447
1858
  const { notificationService } = await import('./notificationService.js');
@@ -2012,6 +2423,8 @@ function validateToolCallArguments(call) {
2012
2423
  return invalidRequiredString('reason');
2013
2424
  case 'run_command':
2014
2425
  return invalidRequiredString('command');
2426
+ case 'message_agent':
2427
+ return invalidRequiredString('agent') || invalidRequiredString('message');
2015
2428
  default:
2016
2429
  return `The model requested an unknown tool: ${call.function.name}.`;
2017
2430
  }
@@ -2019,6 +2432,39 @@ function validateToolCallArguments(call) {
2019
2432
  function isActiveRunStatus(status) {
2020
2433
  return status === 'queued' || status === 'preparing' || status === 'running';
2021
2434
  }
2435
+ /** One-line human description of a gated call for the approval notification. */
2436
+ function summarizeApprovalBody(toolName, summary) {
2437
+ switch (toolName) {
2438
+ case 'run_command':
2439
+ return typeof summary.command === 'string'
2440
+ ? `Run: ${summary.command.slice(0, 200)}`
2441
+ : 'Run a command';
2442
+ case 'delete_file':
2443
+ return typeof summary.path === 'string'
2444
+ ? `Delete ${summary.path}`
2445
+ : 'Delete a file';
2446
+ case 'move_file':
2447
+ return typeof summary.from === 'string' && typeof summary.to === 'string'
2448
+ ? `Move ${summary.from} to ${summary.to}`
2449
+ : 'Move a file';
2450
+ case 'message_agent':
2451
+ return typeof summary.agent === 'string'
2452
+ ? `Delegate to ${summary.agent}`
2453
+ : 'Delegate to another agent';
2454
+ case 'computer_act': {
2455
+ const count = typeof summary.actionCount === 'number' ? summary.actionCount : null;
2456
+ return count === 1
2457
+ ? '1 computer action on the screen'
2458
+ : count !== null
2459
+ ? `${count} computer actions on the screen`
2460
+ : 'Computer actions on the screen';
2461
+ }
2462
+ default:
2463
+ return summary.external === true && typeof summary.server === 'string'
2464
+ ? `Call ${toolName} on ${summary.server}`
2465
+ : toolName;
2466
+ }
2467
+ }
2022
2468
  function summarizeToolCall(call) {
2023
2469
  const args = call.function.arguments;
2024
2470
  const metadata = {
@@ -2065,6 +2511,10 @@ function summarizeToolCall(call) {
2065
2511
  metadata.timeoutMs = args.timeout_ms;
2066
2512
  }
2067
2513
  break;
2514
+ case 'message_agent':
2515
+ includeString('agent', args.agent, 200);
2516
+ includeString('message', args.message, 500);
2517
+ break;
2068
2518
  case 'start_preview':
2069
2519
  includeString('command', args.command, 2000);
2070
2520
  break;