libre-webui 0.29.0 → 0.30.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 (180) hide show
  1. package/backend/dist/persistence/postgresAutomationWebhooksMigration.d.ts +9 -0
  2. package/backend/dist/persistence/postgresAutomationWebhooksMigration.d.ts.map +1 -0
  3. package/backend/dist/persistence/postgresAutomationWebhooksMigration.js +38 -0
  4. package/backend/dist/persistence/postgresAutomationWebhooksMigration.js.map +1 -0
  5. package/backend/dist/persistence/postgresMigrationRegistry.d.ts.map +1 -1
  6. package/backend/dist/persistence/postgresMigrationRegistry.js +4 -0
  7. package/backend/dist/persistence/postgresMigrationRegistry.js.map +1 -1
  8. package/backend/dist/persistence/postgresResourceRepositories.d.ts.map +1 -1
  9. package/backend/dist/persistence/postgresResourceRepositories.js +5 -0
  10. package/backend/dist/persistence/postgresResourceRepositories.js.map +1 -1
  11. package/backend/dist/persistence/postgresWorkApprovalsMigration.d.ts +13 -0
  12. package/backend/dist/persistence/postgresWorkApprovalsMigration.d.ts.map +1 -0
  13. package/backend/dist/persistence/postgresWorkApprovalsMigration.js +69 -0
  14. package/backend/dist/persistence/postgresWorkApprovalsMigration.js.map +1 -0
  15. package/backend/dist/persistence/resourceTypes.d.ts +4 -0
  16. package/backend/dist/persistence/resourceTypes.d.ts.map +1 -1
  17. package/backend/dist/persistence/resourceTypes.js.map +1 -1
  18. package/backend/dist/persistence/sqliteMigrations.d.ts +3 -0
  19. package/backend/dist/persistence/sqliteMigrations.d.ts.map +1 -1
  20. package/backend/dist/persistence/sqliteMigrations.js +149 -0
  21. package/backend/dist/persistence/sqliteMigrations.js.map +1 -1
  22. package/backend/dist/persistence/sqliteResourceRepositories.d.ts.map +1 -1
  23. package/backend/dist/persistence/sqliteResourceRepositories.js +7 -0
  24. package/backend/dist/persistence/sqliteResourceRepositories.js.map +1 -1
  25. package/backend/dist/persistence/sqliteToPostgresMigration.d.ts.map +1 -1
  26. package/backend/dist/persistence/sqliteToPostgresMigration.js +32 -1
  27. package/backend/dist/persistence/sqliteToPostgresMigration.js.map +1 -1
  28. package/backend/dist/platform/workPersistence/postgresWorkPersistence.d.ts +10 -1
  29. package/backend/dist/platform/workPersistence/postgresWorkPersistence.d.ts.map +1 -1
  30. package/backend/dist/platform/workPersistence/postgresWorkPersistence.js +85 -5
  31. package/backend/dist/platform/workPersistence/postgresWorkPersistence.js.map +1 -1
  32. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.d.ts +10 -1
  33. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.d.ts.map +1 -1
  34. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js +67 -7
  35. package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js.map +1 -1
  36. package/backend/dist/platform/workPersistence/types.d.ts +37 -0
  37. package/backend/dist/platform/workPersistence/types.d.ts.map +1 -1
  38. package/backend/dist/platform/workPersistence/types.js.map +1 -1
  39. package/backend/dist/routes/automations.d.ts.map +1 -1
  40. package/backend/dist/routes/automations.js +83 -1
  41. package/backend/dist/routes/automations.js.map +1 -1
  42. package/backend/dist/routes/work.d.ts.map +1 -1
  43. package/backend/dist/routes/work.js +72 -0
  44. package/backend/dist/routes/work.js.map +1 -1
  45. package/backend/dist/services/automationService.d.ts +13 -1
  46. package/backend/dist/services/automationService.d.ts.map +1 -1
  47. package/backend/dist/services/automationService.js +31 -0
  48. package/backend/dist/services/automationService.js.map +1 -1
  49. package/backend/dist/services/notificationService.d.ts.map +1 -1
  50. package/backend/dist/services/notificationService.js +1 -0
  51. package/backend/dist/services/notificationService.js.map +1 -1
  52. package/backend/dist/services/workAgentGuidance.d.ts +11 -0
  53. package/backend/dist/services/workAgentGuidance.d.ts.map +1 -1
  54. package/backend/dist/services/workAgentGuidance.js +16 -2
  55. package/backend/dist/services/workAgentGuidance.js.map +1 -1
  56. package/backend/dist/services/workAgentService.d.ts +55 -0
  57. package/backend/dist/services/workAgentService.d.ts.map +1 -1
  58. package/backend/dist/services/workAgentService.js +425 -13
  59. package/backend/dist/services/workAgentService.js.map +1 -1
  60. package/backend/dist/services/workApprovalService.d.ts +67 -0
  61. package/backend/dist/services/workApprovalService.d.ts.map +1 -0
  62. package/backend/dist/services/workApprovalService.js +273 -0
  63. package/backend/dist/services/workApprovalService.js.map +1 -0
  64. package/backend/dist/services/workEventService.d.ts.map +1 -1
  65. package/backend/dist/services/workEventService.js +25 -0
  66. package/backend/dist/services/workEventService.js.map +1 -1
  67. package/backend/dist/services/workPolicyService.d.ts +4 -0
  68. package/backend/dist/services/workPolicyService.d.ts.map +1 -1
  69. package/backend/dist/services/workPolicyService.js +19 -0
  70. package/backend/dist/services/workPolicyService.js.map +1 -1
  71. package/backend/dist/services/workRuntimeShared.d.ts +2 -0
  72. package/backend/dist/services/workRuntimeShared.d.ts.map +1 -1
  73. package/backend/dist/services/workRuntimeShared.js.map +1 -1
  74. package/backend/dist/services/workTaskService.d.ts +3 -1
  75. package/backend/dist/services/workTaskService.d.ts.map +1 -1
  76. package/backend/dist/services/workTaskService.js +21 -4
  77. package/backend/dist/services/workTaskService.js.map +1 -1
  78. package/backend/dist/types/index.d.ts +3 -1
  79. package/backend/dist/types/index.d.ts.map +1 -1
  80. package/backend/dist/types/index.js.map +1 -1
  81. package/backend/dist/types/work.d.ts +32 -11
  82. package/backend/dist/types/work.d.ts.map +1 -1
  83. package/backend/package.json +1 -1
  84. package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
  85. package/frontend/dist/assets/{index-f1s6QpC5.js → index-C04eLDsy.js} +63 -78
  86. package/frontend/dist/index.html +15 -15
  87. package/frontend/dist/js/{ArtifactContainer-CTXxnWRo.js → ArtifactContainer-ivbLUiBh.js} +1 -1
  88. package/frontend/dist/js/{ArtifactDemoPage-CEYuIhJA.js → ArtifactDemoPage-BmOteTzj.js} +1 -1
  89. package/frontend/dist/js/{ArtifactSandboxFrame-DLOy2VOm.js → ArtifactSandboxFrame-CFIXhQGP.js} +1 -1
  90. package/frontend/dist/js/{ArtifactSlideOutPanel-DnTTc1VY.js → ArtifactSlideOutPanel-DTGX1bsD.js} +1 -1
  91. package/frontend/dist/js/AutomationModal-DWjiyu3B.js +1 -0
  92. package/frontend/dist/js/{AutomationsPage-D3v0Cra1.js → AutomationsPage-DA9uLEG2.js} +1 -1
  93. package/frontend/dist/js/{AvatarUpload-DzL00yOL.js → AvatarUpload-26z_BlGH.js} +1 -1
  94. package/frontend/dist/js/{CalendarPage-Bls3TBeZ.js → CalendarPage-CFU2zsFJ.js} +1 -1
  95. package/frontend/dist/js/{ChannelsPage-BnoCniNf.js → ChannelsPage-BqUJMmjD.js} +1 -1
  96. package/frontend/dist/js/ChatPage-D8I4CNtg.js +14 -0
  97. package/frontend/dist/js/{EvaluationsPage-DSgj9dT5.js → EvaluationsPage-jmb_F4xz.js} +1 -1
  98. package/frontend/dist/js/{FirstTimeSetup-BRysXCjP.js → FirstTimeSetup-DcOvjDh4.js} +1 -1
  99. package/frontend/dist/js/{GalleryPage-DUcM4dg0.js → GalleryPage-CFYgjwey.js} +1 -1
  100. package/frontend/dist/js/{HomePage-BebX3v-x.js → HomePage-DDt6s4Ya.js} +1 -1
  101. package/frontend/dist/js/{LibreClawPage-A37KF9_G.js → LibreClawPage-BqtLshHp.js} +1 -1
  102. package/frontend/dist/js/{NotesPage-CXSz7Hde.js → NotesPage-wdPw2owK.js} +1 -1
  103. package/frontend/dist/js/{PersonasPage-CiOBffF3.js → PersonasPage-DMJ3MioQ.js} +1 -1
  104. package/frontend/dist/js/{PluginUsagePage-jMrpL2Jb.js → PluginUsagePage-Di_B_DJQ.js} +1 -1
  105. package/frontend/dist/js/RichMessageContent-S1-4RhLW.js +1 -0
  106. package/frontend/dist/js/{RichMessageContent-CC9QZQKd.js → RichMessageContent-wk0Eyab_.js} +1 -1
  107. package/frontend/dist/js/{SettingsModal-DP_wKehV.js → SettingsModal-d7I_FVJY.js} +1 -1
  108. package/frontend/dist/js/{ShareDialog-BW8iGd_i.js → ShareDialog-buTN0EBc.js} +1 -1
  109. package/frontend/dist/js/{SystemPage-Drd1neLN.js → SystemPage-CmgxjTS5.js} +1 -1
  110. package/frontend/dist/js/{UserManagementPage-CquciWKV.js → UserManagementPage-u2r5f_uX.js} +1 -1
  111. package/frontend/dist/js/WorkPage-CYth3I8M.js +7 -0
  112. package/frontend/dist/js/{api-2DulPtq1.js → api-IiXPQ8se.js} +2 -2
  113. package/frontend/dist/js/api-gEytqqET.js +1 -0
  114. package/frontend/dist/js/{appNavigation-DAGh98Yj.js → appNavigation-Bsk9DwFY.js} +1 -1
  115. package/frontend/dist/js/{appStore-Ci1X0yA-.js → appStore-D_3JwBEY.js} +2 -2
  116. package/frontend/dist/js/appVersion-Chg_Ru57.js +1 -0
  117. package/frontend/dist/js/{ar-CpmN0TY8.js → ar-BKsbznj2.js} +1 -1
  118. package/frontend/dist/js/authStore-BiWhwkJN.js +1 -0
  119. package/frontend/dist/js/authStore-jbmnSGyL.js +4 -0
  120. package/frontend/dist/js/{bn-BwrMg_Oh.js → bn-DXd4s3g6.js} +1 -1
  121. package/frontend/dist/js/{chatStore-DkpmZLOG.js → chatStore-BXWiKI6E.js} +3 -3
  122. package/frontend/dist/js/{config-Ckn9cand.js → config-CtVytjAm.js} +1 -1
  123. package/frontend/dist/js/cs-CtvHHZ6D.js +2 -0
  124. package/frontend/dist/js/da-CRCQinmr.js +2 -0
  125. package/frontend/dist/js/de-DKjz-eJm.js +2 -0
  126. package/frontend/dist/js/es-K0j99jVL.js +2 -0
  127. package/frontend/dist/js/{evaluationsApi-Cu5PH_qi.js → evaluationsApi-CNcvUpmS.js} +1 -1
  128. package/frontend/dist/js/{fr-BY5aUofi.js → fr-BcQtHFjN.js} +1 -1
  129. package/frontend/dist/js/{hi-KCn9gIiT.js → hi-B69aSfMV.js} +1 -1
  130. package/frontend/dist/js/id-DlEUyPF3.js +2 -0
  131. package/frontend/dist/js/is-DhIcxuEq.js +2 -0
  132. package/frontend/dist/js/{it-CqaqWB8D.js → it-BE6UfTtm.js} +1 -1
  133. package/frontend/dist/js/ja-BNmjqYW2.js +2 -0
  134. package/frontend/dist/js/ko-bnvJ-aG1.js +2 -0
  135. package/frontend/dist/js/{messageCodeStyles-DKq3DH1W.js → messageCodeStyles-D1-WsF_0.js} +1 -1
  136. package/frontend/dist/js/ms-BHlYr228.js +2 -0
  137. package/frontend/dist/js/nl-B0dJJcU_.js +2 -0
  138. package/frontend/dist/js/pl-BQQOCUBv.js +2 -0
  139. package/frontend/dist/js/pt-CeCixlV6.js +2 -0
  140. package/frontend/dist/js/ru-CtbW6bcV.js +2 -0
  141. package/frontend/dist/js/sv-DvyO144q.js +2 -0
  142. package/frontend/dist/js/{teamEventStream-C4u_py2D.js → teamEventStream-B8hMmTgC.js} +1 -1
  143. package/frontend/dist/js/{th-dOxm2Hhl.js → th-Bsila0Wp.js} +1 -1
  144. package/frontend/dist/js/tr-CJ9yyiQZ.js +2 -0
  145. package/frontend/dist/js/{ui-DUzqxWMk.js → ui-BxB90kIo.js} +2 -2
  146. package/frontend/dist/js/{uk-Cl7Pk0Yf.js → uk-BZnidh3w.js} +1 -1
  147. package/frontend/dist/js/{useChat-6yDWZSTz.js → useChat-CP1Bi10i.js} +1 -1
  148. package/frontend/dist/js/useDictation-DF16QY6J.js +1 -0
  149. package/frontend/dist/js/vi-DgnSoExN.js +2 -0
  150. package/frontend/dist/js/{workApi-BwDBwG62.js → workApi-BUxESSmH.js} +3 -3
  151. package/frontend/dist/js/workApi-DrTI8jZw.js +1 -0
  152. package/frontend/dist/js/zh-C3NMIgKz.js +2 -0
  153. package/package.json +3 -3
  154. package/frontend/dist/js/AutomationModal-DB3lrK2K.js +0 -1
  155. package/frontend/dist/js/ChatPage-BMcigJWr.js +0 -14
  156. package/frontend/dist/js/ModelSelector-C1ipX-44.js +0 -1
  157. package/frontend/dist/js/RichMessageContent-CU6WTuOs.js +0 -1
  158. package/frontend/dist/js/WorkPage-BgASwAGs.js +0 -6
  159. package/frontend/dist/js/api-BmIdrQfK.js +0 -1
  160. package/frontend/dist/js/appVersion-BRMnr50t.js +0 -1
  161. package/frontend/dist/js/authStore-DIvsSaOt.js +0 -4
  162. package/frontend/dist/js/authStore-DoxafXFR.js +0 -1
  163. package/frontend/dist/js/cs-f72Fnxz3.js +0 -2
  164. package/frontend/dist/js/da-qp8lOgkJ.js +0 -2
  165. package/frontend/dist/js/de-CpcLHr97.js +0 -2
  166. package/frontend/dist/js/es-gZHsxjG4.js +0 -2
  167. package/frontend/dist/js/id-C7U6kS0J.js +0 -2
  168. package/frontend/dist/js/is-N9Ek-Usz.js +0 -2
  169. package/frontend/dist/js/ja-C5f9HAm1.js +0 -2
  170. package/frontend/dist/js/ko-BuoboDvb.js +0 -2
  171. package/frontend/dist/js/ms-Cu-UWc6j.js +0 -2
  172. package/frontend/dist/js/nl-nOUc4C_i.js +0 -2
  173. package/frontend/dist/js/pl-BKKwFhxj.js +0 -2
  174. package/frontend/dist/js/pt-B18-KYtS.js +0 -2
  175. package/frontend/dist/js/ru-DflEWNyb.js +0 -2
  176. package/frontend/dist/js/sv-CU-nPjr8.js +0 -2
  177. package/frontend/dist/js/tr-7ThM4F8q.js +0 -2
  178. package/frontend/dist/js/vi-eljYUBOk.js +0 -2
  179. package/frontend/dist/js/workApi-uXUZSjsf.js +0 -1
  180. 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,27 @@ 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
+ // Delegation stays shallow by design: a delegated run cannot delegate
107
+ // further, so two agents can never ping-pong requests at each other.
108
+ export const WORK_DELEGATION_MAX_PEERS = 12;
109
+ const WORK_DELEGATION_MESSAGE_MAX_CHARS = 8000;
110
+ const WORK_DELEGATION_REPORT_MAX_CHARS = 20000;
111
+ // Offered per run, only to hired agents with at least one peer and never
112
+ // inside a delegated run: coordination flows by messages between separate
113
+ // sandboxes, not by shared computers.
114
+ 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.", {
115
+ agent: stringProperty("The target agent's exact name, as listed in your peer roster."),
116
+ message: stringProperty('The request to send. Include every detail the agent needs, because it cannot see this conversation.'),
117
+ }, ['agent', 'message']);
94
118
  // Offered only when the task's policy enables the Work Computer and the
95
119
  // task has network access: the agent's eyes and hands on the GUI session.
96
120
  export const WORK_COMPUTER_TOOL_SCHEMAS = [
@@ -180,8 +204,11 @@ export const WORK_COMPUTER_TOOL_SCHEMAS = [
180
204
  reason: stringProperty('What you need the user to do on the screen, stated concretely.'),
181
205
  }, ['reason']),
182
206
  ];
183
- export async function workToolSchemasForTask(task) {
207
+ export async function workToolSchemasForTask(task, options = {}) {
184
208
  const schemas = [...WORK_TOOL_SCHEMAS];
209
+ if (options.messageAgent === true) {
210
+ schemas.push(WORK_MESSAGE_AGENT_TOOL_SCHEMA);
211
+ }
185
212
  if (task.networkEnabled &&
186
213
  (await isWebSearchAvailable()) &&
187
214
  (await userCanUseWebSearch(await userModel.getUserById(task.userId)))) {
@@ -192,6 +219,27 @@ export async function workToolSchemasForTask(task) {
192
219
  }
193
220
  return schemas;
194
221
  }
222
+ /** The delegation marker on the message that started a run, if any. */
223
+ export function delegationSourceOf(messages, runId) {
224
+ for (const message of messages) {
225
+ if (message.runId !== runId || message.role !== 'user')
226
+ continue;
227
+ const delegation = message.metadata?.delegation;
228
+ if (!delegation || typeof delegation !== 'object')
229
+ continue;
230
+ const source = delegation;
231
+ if (typeof source.fromTaskId === 'string' &&
232
+ typeof source.fromRunId === 'string' &&
233
+ typeof source.fromAgent === 'string') {
234
+ return {
235
+ fromTaskId: source.fromTaskId,
236
+ fromRunId: source.fromRunId,
237
+ fromAgent: source.fromAgent,
238
+ };
239
+ }
240
+ }
241
+ return undefined;
242
+ }
195
243
  export class WorkAgentService {
196
244
  constructor() {
197
245
  this.controllers = new Map();
@@ -515,6 +563,7 @@ export class WorkAgentService {
515
563
  const durableAttemptIdentity = String((await getDurableJobRuntime().service.getByIdempotency(userId, WORK_EXECUTE_IDEMPOTENCY_SCOPE, runId))?.attemptCount ?? 0);
516
564
  this.controllers.set(runId, controller);
517
565
  let task;
566
+ let runDelegation;
518
567
  let releaseExecutionLease;
519
568
  let executionContainerSettled = false;
520
569
  const settleExecutionContainer = async () => {
@@ -604,7 +653,29 @@ export class WorkAgentService {
604
653
  };
605
654
  const providerRoutingFingerprint = await workModelProviderService.getRoutingFingerprint(run.model, providerSelection, userId);
606
655
  const providerStateScope = await workModelProviderService.getResponsesStateScope(run.model, providerSelection, userId);
607
- const messages = await this.contextMessages(task, roundLimit, computerAvailable, taughtSkills, providerStateScope, run, persona);
656
+ // A5 approvals: whether side-effecting tool calls pause for review.
657
+ // Resolved once per run — the policy force-flag or the task opt-in;
658
+ // per-call rules are re-read at gate time so an Always-allow decision
659
+ // takes effect on the very next call.
660
+ const approvalsActive = (await workPolicyService.resolve(task.policyId)).approvalsRequired ===
661
+ true || task.approvalsEnabled === true;
662
+ // A6 delegation: a run started by another agent reports back on
663
+ // completion and cannot delegate further (no ping-pong loops). Peers
664
+ // are the user's other hired agents, injected into the roster.
665
+ runDelegation = delegationSourceOf(await workTaskService.getMessages(taskId), runId);
666
+ const peerAgents = task.isAgent === true && !runDelegation
667
+ ? (await workTaskService.listTaskRecords(userId))
668
+ .filter(record => record.isAgent && record.id !== task.id)
669
+ .slice(0, WORK_DELEGATION_MAX_PEERS)
670
+ .map(record => ({
671
+ name: record.title,
672
+ ...(record.statusBlurb ? { status: record.statusBlurb } : {}),
673
+ }))
674
+ : [];
675
+ // Connected tool servers (MCP/OpenAPI): resolved once per run so the
676
+ // offered set is stable across rounds.
677
+ const externalTools = await this.externalToolsForRun(task);
678
+ const messages = await this.contextMessages(task, roundLimit, computerAvailable, taughtSkills, providerStateScope, run, persona, approvalsActive, peerAgents, runDelegation, externalTools ? [...externalTools.byName.keys()] : []);
608
679
  let totalToolCalls = 0;
609
680
  let accumulatedInputTokens = 0;
610
681
  let accumulatedOutputTokens = 0;
@@ -680,7 +751,12 @@ export class WorkAgentService {
680
751
  response = await workModelProviderService.generateChatStreamResponse({
681
752
  model: run.model,
682
753
  messages,
683
- tools: await workToolSchemasForTask(task),
754
+ tools: [
755
+ ...(await workToolSchemasForTask(task, {
756
+ messageAgent: peerAgents.length > 0,
757
+ })),
758
+ ...(externalTools?.schemas ?? []),
759
+ ],
684
760
  stream: true,
685
761
  }, providerSelection, userId, {
686
762
  onContent: delta => contentStream.push(delta),
@@ -844,10 +920,15 @@ export class WorkAgentService {
844
920
  sourceKey: `work-run:${runId}:completed`,
845
921
  });
846
922
  }
923
+ await this.reportDelegationOutcome(task, runDelegation, 'completed', finalContent);
847
924
  return;
848
925
  }
849
926
  const toolValidationErrors = new Map();
850
927
  for (const call of toolCalls) {
928
+ // Connected tools validate their own arguments in the gateway;
929
+ // static validation only covers the built-in registry.
930
+ if (externalTools?.byName.has(call.function.name))
931
+ continue;
851
932
  const validationError = validateToolCallArguments(call);
852
933
  if (validationError)
853
934
  toolValidationErrors.set(call, validationError);
@@ -873,8 +954,18 @@ export class WorkAgentService {
873
954
  }
874
955
  for (const call of toolCalls) {
875
956
  await this.throwIfCancelled(runId, controller);
957
+ const externalEntry = externalTools?.byName.get(call.function.name);
876
958
  const toolCallMetadata = {
877
959
  ...summarizeToolCall(call),
960
+ ...(externalEntry
961
+ ? {
962
+ external: true,
963
+ ...(externalEntry.serverName
964
+ ? { server: externalEntry.serverName }
965
+ : {}),
966
+ argumentsPreview: JSON.stringify(call.function.arguments ?? {}).slice(0, 500),
967
+ }
968
+ : {}),
878
969
  round: round + 1,
879
970
  };
880
971
  const toolCallMessage = await workTaskService.addMessage(taskId, runId, 'assistant', 'tool_call', `Calling ${call.function.name}`, toolCallMetadata);
@@ -908,14 +999,34 @@ export class WorkAgentService {
908
999
  if (hasInvalidToolArguments) {
909
1000
  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
1001
  }
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 };
1002
+ const approvalVerdict = approvalsActive &&
1003
+ (GATED_WORK_TOOLS.has(call.function.name) ||
1004
+ externalTools?.byName.get(call.function.name)?.sideEffect ===
1005
+ true)
1006
+ ? await this.awaitToolApproval(task, call, toolCallMetadata, runId, userId, controller.signal, durableAttemptIdentity)
1007
+ : 'approved';
1008
+ if (approvalVerdict === 'denied') {
1009
+ toolOutput =
1010
+ 'The user denied this action. Do not retry it as-is; adjust your plan or ask the user what to do instead.';
1011
+ toolMetadata = { ...toolMetadata, approval: 'denied' };
1012
+ }
1013
+ else if (approvalVerdict === 'expired') {
1014
+ toolOutput =
1015
+ 'This action was not executed: the approval request timed out with no decision from the user.';
1016
+ toolMetadata = { ...toolMetadata, approval: 'expired' };
1017
+ }
1018
+ else {
1019
+ const result = await this.executeTool(task, call, {
1020
+ signal: controller.signal,
1021
+ computer: computerContext,
1022
+ runId,
1023
+ delegated: runDelegation !== undefined,
1024
+ external: externalTools,
1025
+ });
1026
+ toolOutput = result.content;
1027
+ toolImages = result.images;
1028
+ toolMetadata = { ...toolMetadata, ...result.metadata };
1029
+ }
919
1030
  }
920
1031
  catch (error) {
921
1032
  toolOutput =
@@ -970,6 +1081,12 @@ export class WorkAgentService {
970
1081
  // text observation, and only the most recent screenshots stay in
971
1082
  // model context so a long computer session cannot flood tokens.
972
1083
  retainRecentWorkImages(messages);
1084
+ // An unanswered approval hands the run off instead of burning the
1085
+ // remaining budget on decisions no one is there to make.
1086
+ if (toolMetadata.approval === 'expired') {
1087
+ budgetReason = 'approval-timeout';
1088
+ break roundLoop;
1089
+ }
973
1090
  if ((call.function.name === 'computer_act' ||
974
1091
  call.function.name === 'computer_observe') &&
975
1092
  typeof toolMetadata.screenshotSha256 === 'string') {
@@ -1045,7 +1162,9 @@ export class WorkAgentService {
1045
1162
  role: 'user',
1046
1163
  content: `${buildWorkBudgetExhaustionPrompt()}\n${budgetReason === 'computer-stall'
1047
1164
  ? 'The run was stopped because repeated computer actions kept producing an unchanged screen even after a recovery notice.'
1048
- : `The ${budgetReason} budget was reached.`}`,
1165
+ : budgetReason === 'approval-timeout'
1166
+ ? 'The run was stopped because an action was still waiting for user approval when the request timed out.'
1167
+ : `The ${budgetReason} budget was reached.`}`,
1049
1168
  },
1050
1169
  ],
1051
1170
  tools: [],
@@ -1127,6 +1246,7 @@ export class WorkAgentService {
1127
1246
  sourceKey: `work-run:${runId}:needs_input`,
1128
1247
  });
1129
1248
  }
1249
+ await this.reportDelegationOutcome(task, runDelegation, 'needs_input', handoffContent);
1130
1250
  }
1131
1251
  catch (error) {
1132
1252
  const currentRun = await workTaskService.getRun(runId);
@@ -1146,6 +1266,7 @@ export class WorkAgentService {
1146
1266
  status: 'cancelled',
1147
1267
  error: 'Cancelled by user.',
1148
1268
  }, 'terminal:cancelled');
1269
+ await this.reportDelegationOutcome(task, runDelegation, 'cancelled');
1149
1270
  }
1150
1271
  }
1151
1272
  catch (cleanupError) {
@@ -1171,6 +1292,9 @@ export class WorkAgentService {
1171
1292
  status: 'cancelled',
1172
1293
  error: 'Cancelled by user.',
1173
1294
  }, 'terminal:cancelled');
1295
+ if (task) {
1296
+ await this.reportDelegationOutcome(task, runDelegation, 'cancelled');
1297
+ }
1174
1298
  }
1175
1299
  return;
1176
1300
  }
@@ -1199,6 +1323,9 @@ export class WorkAgentService {
1199
1323
  sourceKey: `work-run:${runId}:failed`,
1200
1324
  });
1201
1325
  }
1326
+ if (task) {
1327
+ await this.reportDelegationOutcome(task, runDelegation, 'failed', message);
1328
+ }
1202
1329
  }
1203
1330
  finally {
1204
1331
  this.controllers.delete(runId);
@@ -1247,7 +1374,7 @@ export class WorkAgentService {
1247
1374
  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
1375
  }
1249
1376
  }
1250
- async contextMessages(task, roundLimit, computerAvailable, taughtSkills, providerStateScope, provider = task, persona) {
1377
+ async contextMessages(task, roundLimit, computerAvailable, taughtSkills, providerStateScope, provider = task, persona, approvalsActive, peerAgents = [], delegatedBy, connectedTools = []) {
1251
1378
  const persisted = restorePersistedWorkContext(await workTaskService.getRecentModelContextMessages(task.id, 30), provider, providerStateScope);
1252
1379
  return [
1253
1380
  {
@@ -1257,6 +1384,10 @@ export class WorkAgentService {
1257
1384
  computerAvailable,
1258
1385
  ...(taughtSkills.length > 0 ? { taughtSkills } : {}),
1259
1386
  ...(persona ? { persona } : {}),
1387
+ ...(approvalsActive ? { approvalsActive } : {}),
1388
+ ...(peerAgents.length > 0 ? { peerAgents } : {}),
1389
+ ...(delegatedBy ? { delegatedBy: delegatedBy.fromAgent } : {}),
1390
+ ...(connectedTools.length > 0 ? { connectedTools } : {}),
1260
1391
  previewPort: workRuntimeService.previewPort,
1261
1392
  roundBudget: roundLimit,
1262
1393
  commandTimeoutMs: workRuntimeService.limits.commandTimeoutMs,
@@ -1268,6 +1399,32 @@ export class WorkAgentService {
1268
1399
  }
1269
1400
  async executeTool(task, call, context = {}) {
1270
1401
  const args = call.function.arguments;
1402
+ // Connected tool servers: dynamic names resolved through the run's
1403
+ // catalog before the built-in switch. Every guard the chat surface has
1404
+ // is re-applied inside executeToolCall (visibility, enablement,
1405
+ // credentials, SSRF-hardened egress).
1406
+ const external = context.external?.byName.get(call.function.name);
1407
+ if (external && context.external) {
1408
+ if (!task.networkEnabled) {
1409
+ throw new WorkAgentHttpError('Connected tools are not available for this task.', 403, 'WORK_TOOLS_UNAVAILABLE');
1410
+ }
1411
+ const result = await executeToolCall({
1412
+ actor: context.external.actor,
1413
+ tool: external,
1414
+ argumentsJson: JSON.stringify(args ?? {}),
1415
+ selection: context.external.selection,
1416
+ ...(context.signal ? { signal: context.signal } : {}),
1417
+ });
1418
+ return {
1419
+ content: result.text.slice(0, workRuntimeService.limits.maxOutputChars),
1420
+ metadata: {
1421
+ external: true,
1422
+ ...(external.serverName ? { server: external.serverName } : {}),
1423
+ ...(result.isError ? { error: true } : {}),
1424
+ ...(result.truncated ? { outputTruncated: true } : {}),
1425
+ },
1426
+ };
1427
+ }
1271
1428
  switch (call.function.name) {
1272
1429
  case 'list_files': {
1273
1430
  const result = await workRuntimeService.listFiles(task, optionalString(args.path) || '.');
@@ -1368,6 +1525,61 @@ export class WorkAgentService {
1368
1525
  }
1369
1526
  return result;
1370
1527
  }
1528
+ case 'message_agent': {
1529
+ if (context.delegated === true) {
1530
+ return {
1531
+ 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.',
1532
+ metadata: { outcome: 'depth-limited' },
1533
+ };
1534
+ }
1535
+ const agentName = requiredString(args.agent, 'agent')
1536
+ .trim()
1537
+ .slice(0, 200);
1538
+ const message = requiredString(args.message, 'message').slice(0, WORK_DELEGATION_MESSAGE_MAX_CHARS);
1539
+ const peers = (await workTaskService.listTaskRecords(task.userId)).filter(record => record.isAgent && record.id !== task.id);
1540
+ const target = peers.find(record => record.title.trim().toLowerCase() === agentName.toLowerCase());
1541
+ if (!target) {
1542
+ const roster = peers
1543
+ .slice(0, WORK_DELEGATION_MAX_PEERS)
1544
+ .map(record => record.title)
1545
+ .join(', ');
1546
+ return {
1547
+ content: roster
1548
+ ? `No hired agent is named "${agentName}". The user's agents are: ${roster}. Use the exact name, or handle the request yourself.`
1549
+ : 'The user has no other hired agents, so there is no one to delegate to. Handle the request yourself.',
1550
+ metadata: { outcome: 'unknown-agent' },
1551
+ };
1552
+ }
1553
+ try {
1554
+ const detail = await workTaskService.createRun(target.id, task.userId, message, undefined, undefined, {
1555
+ delegation: {
1556
+ fromTaskId: task.id,
1557
+ fromRunId: context.runId ?? '',
1558
+ fromAgent: task.title,
1559
+ },
1560
+ });
1561
+ return {
1562
+ 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.`,
1563
+ metadata: {
1564
+ outcome: 'delegated',
1565
+ delegatedTaskId: target.id,
1566
+ ...(detail.activeRun?.id
1567
+ ? { delegatedRunId: detail.activeRun.id }
1568
+ : {}),
1569
+ delegatedAgent: target.title,
1570
+ },
1571
+ };
1572
+ }
1573
+ catch (error) {
1574
+ if (error instanceof WorkConflictError) {
1575
+ return {
1576
+ content: `${target.title} is busy with another run right now. Try again later, do the work yourself, or tell the user.`,
1577
+ metadata: { outcome: 'busy' },
1578
+ };
1579
+ }
1580
+ throw error;
1581
+ }
1582
+ }
1371
1583
  case 'request_takeover': {
1372
1584
  await this.assertComputerToolsAvailable(task);
1373
1585
  if ((await workPolicyService.resolve(task.policyId)).takeoverEnabled ===
@@ -1442,6 +1654,167 @@ export class WorkAgentService {
1442
1654
  * never fails the run. The source key is run-scoped because dedupe is
1443
1655
  * permanent per user until the inbox cap prunes it.
1444
1656
  */
1657
+ /**
1658
+ * The user's configured tool servers, resolved once per run. Backend
1659
+ * egress, so an offline task offers none — the web_search rationale.
1660
+ * Servers whose per-user credential is missing are filtered at offer
1661
+ * time, because an autonomous run cannot prompt for one; persona
1662
+ * bindings narrow the servers exactly as they do in chat.
1663
+ */
1664
+ async externalToolsForRun(task) {
1665
+ if (!task.networkEnabled)
1666
+ return undefined;
1667
+ try {
1668
+ const user = await userModel.getUserById(task.userId);
1669
+ const actor = {
1670
+ userId: task.userId,
1671
+ ...(user?.role ? { role: user.role } : {}),
1672
+ };
1673
+ if (!(await actorCanUseTools(actor)))
1674
+ return undefined;
1675
+ let serverIds;
1676
+ if (task.personaId) {
1677
+ try {
1678
+ const { personaService } = await import('./personaService.js');
1679
+ const bindings = (await personaService.getPersonaById(task.personaId, task.userId))?.bindings;
1680
+ if (bindings?.tool_server_ids)
1681
+ serverIds = bindings.tool_server_ids;
1682
+ }
1683
+ catch {
1684
+ // A vanished persona leaves the selection unrestricted, matching
1685
+ // how the run already degrades to no persona identity.
1686
+ }
1687
+ }
1688
+ const catalog = await buildToolCatalog(actor, {}, {
1689
+ builtinTools: [],
1690
+ ...(serverIds ? { serverIds } : {}),
1691
+ });
1692
+ const builtinNames = new Set([
1693
+ ...WORK_TOOL_SCHEMAS,
1694
+ WORK_WEB_SEARCH_TOOL_SCHEMA,
1695
+ WORK_MESSAGE_AGENT_TOOL_SCHEMA,
1696
+ ...WORK_COMPUTER_TOOL_SCHEMAS,
1697
+ ].map(schema => schema.function?.name ?? ''));
1698
+ const credentialed = new Map();
1699
+ const byName = new Map();
1700
+ const schemas = [];
1701
+ for (const tool of catalog.tools) {
1702
+ if (tool.source === 'builtin' || !tool.serverId)
1703
+ continue;
1704
+ if (builtinNames.has(tool.name))
1705
+ continue;
1706
+ let usable = credentialed.get(tool.serverId);
1707
+ if (usable === undefined) {
1708
+ const server = await getToolServer(tool.serverId);
1709
+ usable =
1710
+ server !== undefined &&
1711
+ server !== null &&
1712
+ (server.authMode === 'none' ||
1713
+ (await hasToolServerCredential(task.userId, tool.serverId)));
1714
+ credentialed.set(tool.serverId, usable);
1715
+ }
1716
+ if (!usable)
1717
+ continue;
1718
+ byName.set(tool.name, tool);
1719
+ schemas.push(externalToolSchema(tool));
1720
+ }
1721
+ if (byName.size === 0)
1722
+ return undefined;
1723
+ return { actor, byName, schemas, selection: catalog.selection };
1724
+ }
1725
+ catch (error) {
1726
+ logger.warn(`Connected tools are unavailable for task ${task.id}:`, error);
1727
+ return undefined;
1728
+ }
1729
+ }
1730
+ /**
1731
+ * Deliver a delegated run's outcome back into the delegating agent's
1732
+ * conversation. Mid-run when the delegator is still working (picked up at
1733
+ * its next round boundary), an ordinary conversation message otherwise —
1734
+ * never an auto-started run, so two agents cannot ping-pong each other.
1735
+ * Best effort: the delegated run's own rows are the source of truth.
1736
+ */
1737
+ async reportDelegationOutcome(task, delegation, status, summary) {
1738
+ if (!delegation)
1739
+ return;
1740
+ try {
1741
+ const delegator = await workTaskService.getTaskRecord(delegation.fromTaskId, task.userId);
1742
+ if (!delegator)
1743
+ return;
1744
+ const statusLine = status === 'completed'
1745
+ ? 'finished'
1746
+ : status === 'needs_input'
1747
+ ? 'stopped and needs input'
1748
+ : status === 'failed'
1749
+ ? 'failed'
1750
+ : 'was cancelled';
1751
+ const bounded = (summary ?? '')
1752
+ .trim()
1753
+ .slice(0, WORK_DELEGATION_REPORT_MAX_CHARS);
1754
+ const content = `Report from ${task.title} — the delegated request ${statusLine}.\n\n${bounded || '(no summary was produced)'}`;
1755
+ const activeRun = await workTaskService.getActiveRun(delegation.fromTaskId);
1756
+ const deliverMidRun = activeRun !== undefined && isActiveRunStatus(activeRun.status);
1757
+ await workTaskService.addMessage(delegation.fromTaskId, deliverMidRun ? activeRun.id : undefined, 'user', 'message', content, {
1758
+ ...(deliverMidRun ? { midRun: true } : {}),
1759
+ delegationReport: {
1760
+ fromTaskId: task.id,
1761
+ fromAgent: task.title,
1762
+ status,
1763
+ },
1764
+ });
1765
+ }
1766
+ catch (error) {
1767
+ logger.warn(`Delegation report from task ${task.id} failed:`, error);
1768
+ }
1769
+ }
1770
+ /**
1771
+ * The approval gate. Rules are re-read per call so an Always-allow
1772
+ * decision covers the very next matching call; a pending approval rides
1773
+ * the event stream (and the snapshot) so any open view can decide, and
1774
+ * notifies because the user may not be watching. Denial and timeout come
1775
+ * back as verdicts the dispatch loop turns into ordinary tool results —
1776
+ * never silent execution.
1777
+ */
1778
+ async awaitToolApproval(task, call, summary, runId, userId, signal, durableAttemptIdentity) {
1779
+ if (await workApprovalService.callIsPreapproved(task.id, call.function.name, summary)) {
1780
+ return 'approved';
1781
+ }
1782
+ const pending = await workApprovalService.createPending({
1783
+ taskId: task.id,
1784
+ runId,
1785
+ userId,
1786
+ toolCallId: call.id,
1787
+ toolName: call.function.name,
1788
+ summary,
1789
+ });
1790
+ await workEventService.publish(task.id, runId, 'approval', {
1791
+ approvalId: pending.approvalId,
1792
+ toolCallId: call.id,
1793
+ name: call.function.name,
1794
+ summary: pending.summary,
1795
+ status: 'pending',
1796
+ expiresAt: pending.expiresAt,
1797
+ }, `approval:${pending.approvalId}:pending`, durableAttemptIdentity);
1798
+ await this.notifyAgentLifecycle(task, {
1799
+ type: 'work-approval',
1800
+ title: `${task.title} is waiting for your approval`,
1801
+ body: summarizeApprovalBody(call.function.name, summary),
1802
+ sourceKey: `work-approval:${pending.approvalId}`,
1803
+ });
1804
+ const verdict = await workApprovalService.waitForDecision(task.id, pending.approvalId, signal);
1805
+ const status = verdict.status === 'approved'
1806
+ ? 'approved'
1807
+ : verdict.status === 'denied'
1808
+ ? 'denied'
1809
+ : 'expired';
1810
+ await workEventService.publish(task.id, runId, 'approval', {
1811
+ approvalId: pending.approvalId,
1812
+ toolCallId: call.id,
1813
+ name: call.function.name,
1814
+ status,
1815
+ }, `approval:${pending.approvalId}:resolved`, durableAttemptIdentity);
1816
+ return status;
1817
+ }
1445
1818
  async notifyAgentLifecycle(task, input) {
1446
1819
  try {
1447
1820
  const { notificationService } = await import('./notificationService.js');
@@ -2012,6 +2385,8 @@ function validateToolCallArguments(call) {
2012
2385
  return invalidRequiredString('reason');
2013
2386
  case 'run_command':
2014
2387
  return invalidRequiredString('command');
2388
+ case 'message_agent':
2389
+ return invalidRequiredString('agent') || invalidRequiredString('message');
2015
2390
  default:
2016
2391
  return `The model requested an unknown tool: ${call.function.name}.`;
2017
2392
  }
@@ -2019,6 +2394,39 @@ function validateToolCallArguments(call) {
2019
2394
  function isActiveRunStatus(status) {
2020
2395
  return status === 'queued' || status === 'preparing' || status === 'running';
2021
2396
  }
2397
+ /** One-line human description of a gated call for the approval notification. */
2398
+ function summarizeApprovalBody(toolName, summary) {
2399
+ switch (toolName) {
2400
+ case 'run_command':
2401
+ return typeof summary.command === 'string'
2402
+ ? `Run: ${summary.command.slice(0, 200)}`
2403
+ : 'Run a command';
2404
+ case 'delete_file':
2405
+ return typeof summary.path === 'string'
2406
+ ? `Delete ${summary.path}`
2407
+ : 'Delete a file';
2408
+ case 'move_file':
2409
+ return typeof summary.from === 'string' && typeof summary.to === 'string'
2410
+ ? `Move ${summary.from} to ${summary.to}`
2411
+ : 'Move a file';
2412
+ case 'message_agent':
2413
+ return typeof summary.agent === 'string'
2414
+ ? `Delegate to ${summary.agent}`
2415
+ : 'Delegate to another agent';
2416
+ case 'computer_act': {
2417
+ const count = typeof summary.actionCount === 'number' ? summary.actionCount : null;
2418
+ return count === 1
2419
+ ? '1 computer action on the screen'
2420
+ : count !== null
2421
+ ? `${count} computer actions on the screen`
2422
+ : 'Computer actions on the screen';
2423
+ }
2424
+ default:
2425
+ return summary.external === true && typeof summary.server === 'string'
2426
+ ? `Call ${toolName} on ${summary.server}`
2427
+ : toolName;
2428
+ }
2429
+ }
2022
2430
  function summarizeToolCall(call) {
2023
2431
  const args = call.function.arguments;
2024
2432
  const metadata = {
@@ -2065,6 +2473,10 @@ function summarizeToolCall(call) {
2065
2473
  metadata.timeoutMs = args.timeout_ms;
2066
2474
  }
2067
2475
  break;
2476
+ case 'message_agent':
2477
+ includeString('agent', args.agent, 200);
2478
+ includeString('message', args.message, 500);
2479
+ break;
2068
2480
  case 'start_preview':
2069
2481
  includeString('command', args.command, 2000);
2070
2482
  break;