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.
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +24 -7
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/persistence/postgresAutomationWebhooksMigration.d.ts +9 -0
- package/backend/dist/persistence/postgresAutomationWebhooksMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresAutomationWebhooksMigration.js +38 -0
- package/backend/dist/persistence/postgresAutomationWebhooksMigration.js.map +1 -0
- package/backend/dist/persistence/postgresMigrationRegistry.d.ts.map +1 -1
- package/backend/dist/persistence/postgresMigrationRegistry.js +4 -0
- package/backend/dist/persistence/postgresMigrationRegistry.js.map +1 -1
- package/backend/dist/persistence/postgresResourceRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/postgresResourceRepositories.js +5 -0
- package/backend/dist/persistence/postgresResourceRepositories.js.map +1 -1
- package/backend/dist/persistence/postgresWorkApprovalsMigration.d.ts +13 -0
- package/backend/dist/persistence/postgresWorkApprovalsMigration.d.ts.map +1 -0
- package/backend/dist/persistence/postgresWorkApprovalsMigration.js +69 -0
- package/backend/dist/persistence/postgresWorkApprovalsMigration.js.map +1 -0
- package/backend/dist/persistence/resourceTypes.d.ts +4 -0
- package/backend/dist/persistence/resourceTypes.d.ts.map +1 -1
- package/backend/dist/persistence/resourceTypes.js.map +1 -1
- package/backend/dist/persistence/sqliteMigrations.d.ts +3 -0
- package/backend/dist/persistence/sqliteMigrations.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteMigrations.js +156 -2
- package/backend/dist/persistence/sqliteMigrations.js.map +1 -1
- package/backend/dist/persistence/sqliteResourceRepositories.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteResourceRepositories.js +7 -0
- package/backend/dist/persistence/sqliteResourceRepositories.js.map +1 -1
- package/backend/dist/persistence/sqliteToPostgresMigration.d.ts.map +1 -1
- package/backend/dist/persistence/sqliteToPostgresMigration.js +32 -1
- package/backend/dist/persistence/sqliteToPostgresMigration.js.map +1 -1
- package/backend/dist/platform/workPersistence/postgresWorkPersistence.d.ts +10 -1
- package/backend/dist/platform/workPersistence/postgresWorkPersistence.d.ts.map +1 -1
- package/backend/dist/platform/workPersistence/postgresWorkPersistence.js +85 -5
- package/backend/dist/platform/workPersistence/postgresWorkPersistence.js.map +1 -1
- package/backend/dist/platform/workPersistence/sqliteWorkPersistence.d.ts +10 -1
- package/backend/dist/platform/workPersistence/sqliteWorkPersistence.d.ts.map +1 -1
- package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js +67 -7
- package/backend/dist/platform/workPersistence/sqliteWorkPersistence.js.map +1 -1
- package/backend/dist/platform/workPersistence/types.d.ts +37 -0
- package/backend/dist/platform/workPersistence/types.d.ts.map +1 -1
- package/backend/dist/platform/workPersistence/types.js.map +1 -1
- package/backend/dist/routes/automations.d.ts.map +1 -1
- package/backend/dist/routes/automations.js +83 -1
- package/backend/dist/routes/automations.js.map +1 -1
- package/backend/dist/routes/ollama.d.ts.map +1 -1
- package/backend/dist/routes/ollama.js +64 -0
- package/backend/dist/routes/ollama.js.map +1 -1
- package/backend/dist/routes/plugins.d.ts.map +1 -1
- package/backend/dist/routes/plugins.js +90 -0
- package/backend/dist/routes/plugins.js.map +1 -1
- package/backend/dist/routes/work.d.ts.map +1 -1
- package/backend/dist/routes/work.js +83 -0
- package/backend/dist/routes/work.js.map +1 -1
- package/backend/dist/services/authService.d.ts +2 -0
- package/backend/dist/services/authService.d.ts.map +1 -1
- package/backend/dist/services/authService.js +2 -0
- package/backend/dist/services/authService.js.map +1 -1
- package/backend/dist/services/automationService.d.ts +13 -1
- package/backend/dist/services/automationService.d.ts.map +1 -1
- package/backend/dist/services/automationService.js +31 -0
- package/backend/dist/services/automationService.js.map +1 -1
- package/backend/dist/services/notificationService.d.ts.map +1 -1
- package/backend/dist/services/notificationService.js +1 -0
- package/backend/dist/services/notificationService.js.map +1 -1
- package/backend/dist/services/ollamaService.d.ts +12 -0
- package/backend/dist/services/ollamaService.d.ts.map +1 -1
- package/backend/dist/services/ollamaService.js +26 -0
- package/backend/dist/services/ollamaService.js.map +1 -1
- package/backend/dist/services/ollamaSettingsService.d.ts +10 -0
- package/backend/dist/services/ollamaSettingsService.d.ts.map +1 -0
- package/backend/dist/services/ollamaSettingsService.js +83 -0
- package/backend/dist/services/ollamaSettingsService.js.map +1 -0
- package/backend/dist/services/workAgentGuidance.d.ts +11 -0
- package/backend/dist/services/workAgentGuidance.d.ts.map +1 -1
- package/backend/dist/services/workAgentGuidance.js +16 -2
- package/backend/dist/services/workAgentGuidance.js.map +1 -1
- package/backend/dist/services/workAgentService.d.ts +56 -0
- package/backend/dist/services/workAgentService.d.ts.map +1 -1
- package/backend/dist/services/workAgentService.js +467 -17
- package/backend/dist/services/workAgentService.js.map +1 -1
- package/backend/dist/services/workApprovalService.d.ts +67 -0
- package/backend/dist/services/workApprovalService.d.ts.map +1 -0
- package/backend/dist/services/workApprovalService.js +273 -0
- package/backend/dist/services/workApprovalService.js.map +1 -0
- package/backend/dist/services/workEventService.d.ts.map +1 -1
- package/backend/dist/services/workEventService.js +25 -0
- package/backend/dist/services/workEventService.js.map +1 -1
- package/backend/dist/services/workPolicyService.d.ts +4 -0
- package/backend/dist/services/workPolicyService.d.ts.map +1 -1
- package/backend/dist/services/workPolicyService.js +19 -0
- package/backend/dist/services/workPolicyService.js.map +1 -1
- package/backend/dist/services/workRuntimeShared.d.ts +2 -0
- package/backend/dist/services/workRuntimeShared.d.ts.map +1 -1
- package/backend/dist/services/workRuntimeShared.js.map +1 -1
- package/backend/dist/services/workTaskService.d.ts +3 -1
- package/backend/dist/services/workTaskService.d.ts.map +1 -1
- package/backend/dist/services/workTaskService.js +21 -4
- package/backend/dist/services/workTaskService.js.map +1 -1
- package/backend/dist/types/index.d.ts +3 -1
- package/backend/dist/types/index.d.ts.map +1 -1
- package/backend/dist/types/index.js.map +1 -1
- package/backend/dist/types/work.d.ts +32 -11
- package/backend/dist/types/work.d.ts.map +1 -1
- package/backend/dist/websocketServer.d.ts.map +1 -1
- package/backend/dist/websocketServer.js +14 -2
- package/backend/dist/websocketServer.js.map +1 -1
- package/backend/package.json +2 -2
- package/deploy/work-computer/Dockerfile +1 -0
- package/deploy/work-computer/tint2rc +1 -0
- package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
- package/frontend/dist/artifact-runtime/lucide-react.js +1 -1
- package/frontend/dist/artifact-runtime/mermaid.js +8 -8
- package/frontend/dist/artifact-runtime/papaparse.js +2 -2
- package/frontend/dist/artifact-runtime/plotly.js +371 -652
- package/frontend/dist/assets/index-B6xMZpzA.js +76 -0
- package/frontend/dist/assets/index-BjipfsCP.css +1 -0
- package/frontend/dist/index.html +28 -28
- package/frontend/dist/js/{ArtifactContainer-CTXxnWRo.js → ArtifactContainer-Bn6iJtaJ.js} +1 -1
- package/frontend/dist/js/{ArtifactDemoPage-CEYuIhJA.js → ArtifactDemoPage-BLF8Ok-o.js} +1 -1
- package/frontend/dist/js/{ArtifactSandboxFrame-DLOy2VOm.js → ArtifactSandboxFrame-Cs1Gu1WB.js} +1 -1
- package/frontend/dist/js/{ArtifactSlideOutPanel-DnTTc1VY.js → ArtifactSlideOutPanel-CuKq5cL0.js} +1 -1
- package/frontend/dist/js/AutomationModal-Dha4KgLI.js +1 -0
- package/frontend/dist/js/AutomationsPage-HcMCRV-D.js +1 -0
- package/frontend/dist/js/{AvatarUpload-DzL00yOL.js → AvatarUpload-CFrlSszY.js} +1 -1
- package/frontend/dist/js/{Button--pDOakZx.js → Button-CgN11mgY.js} +1 -1
- package/frontend/dist/js/{CalendarPage-Bls3TBeZ.js → CalendarPage-D_K-XrG9.js} +1 -1
- package/frontend/dist/js/ChannelsPage-D3CoImOT.js +1 -0
- package/frontend/dist/js/ChatPage-C0iBDbOq.js +14 -0
- package/frontend/dist/js/ConnectModels-D_ZbZQub.js +1 -0
- package/frontend/dist/js/{EvaluationsPage-DSgj9dT5.js → EvaluationsPage-DOJLpauA.js} +2 -2
- package/frontend/dist/js/FirstTimeSetup-DDIP_4Dd.js +1 -0
- package/frontend/dist/js/GalleryPage-C8U_F7Ns.js +1 -0
- package/frontend/dist/js/{HomePage-BebX3v-x.js → HomePage-B_aotoDU.js} +1 -1
- package/frontend/dist/js/LibreClawPage-cr2N6W00.js +1 -0
- package/frontend/dist/js/{Logo-BE9SvCC5.js → Logo-B7o7eaPU.js} +1 -1
- package/frontend/dist/js/{LogoMark-C8x1yyP3.js → LogoMark-D9g6ls0h.js} +1 -1
- package/frontend/dist/js/{ModelManagerModals-CpWAltRA.js → ModelManagerModals-C-J1d7GL.js} +1 -1
- package/frontend/dist/js/NotesPage-Bgr7R_Vm.js +3 -0
- package/frontend/dist/js/{PasswordStrengthMeter-Cdq2oQbc.js → PasswordStrengthMeter-DR-mGbLF.js} +1 -1
- package/frontend/dist/js/PersonasPage-C3D_6lIx.js +13 -0
- package/frontend/dist/js/{PluginUsagePage-jMrpL2Jb.js → PluginUsagePage-DK7MgSiu.js} +1 -1
- package/frontend/dist/js/RichMessageContent-7FxrZ4A8.js +1 -0
- package/frontend/dist/js/{RichMessageContent-CC9QZQKd.js → RichMessageContent-CQkLI7Dt.js} +1 -1
- package/frontend/dist/js/SettingsModal-CIV_Bgsf.js +19 -0
- package/frontend/dist/js/{SettingsToggle-C16gtVyi.js → SettingsToggle-Cw4OhW7B.js} +1 -1
- package/frontend/dist/js/ShareDialog-BCEVgtbN.js +1 -0
- package/frontend/dist/js/{SystemPage-Drd1neLN.js → SystemPage-BNVZtLe9.js} +1 -1
- package/frontend/dist/js/{UserManagementPage-CquciWKV.js → UserManagementPage-pPC_Svj9.js} +1 -1
- package/frontend/dist/js/WorkPage-Q33nIqkM.js +7 -0
- package/frontend/dist/js/{api-2DulPtq1.js → api-D-5hpGht.js} +3 -3
- package/frontend/dist/js/api-WR1CTYO5.js +1 -0
- package/frontend/dist/js/{appNavigation-DAGh98Yj.js → appNavigation-DfHbXBey.js} +1 -1
- package/frontend/dist/js/{appStore-Ci1X0yA-.js → appStore-B136FGdu.js} +2 -2
- package/frontend/dist/js/appVersion-Ck7dYiHe.js +1 -0
- package/frontend/dist/js/ar-Cq-nC6v9.js +2 -0
- package/frontend/dist/js/authStore-6AJLgQsD.js +1 -0
- package/frontend/dist/js/authStore-Ckz37pwu.js +4 -0
- package/frontend/dist/js/axios-CUerAKcc.js +1 -0
- package/frontend/dist/js/{babel-D7QYvEns.js → babel-8afoxhZ7.js} +1 -1
- package/frontend/dist/js/bn-BFSKytYs.js +2 -0
- package/frontend/dist/js/chatStore-Dpm3g_Ln.js +3 -0
- package/frontend/dist/js/classPrivateMethodInitSpec-DE0XIqc_.js +1 -0
- package/frontend/dist/js/config-DJwXAf8i.js +1 -0
- package/frontend/dist/js/cs-BKWXa629.js +2 -0
- package/frontend/dist/js/da-Cra_DJnV.js +2 -0
- package/frontend/dist/js/de-BQa6_UuE.js +2 -0
- package/frontend/dist/js/es-BBOHFrx1.js +2 -0
- package/frontend/dist/js/{estree-D7xT4AeP.js → estree-qxY7ACbu.js} +3 -3
- package/frontend/dist/js/{evaluationsApi-Cu5PH_qi.js → evaluationsApi-BKY-maUM.js} +1 -1
- package/frontend/dist/js/fr-C8kXs5c0.js +2 -0
- package/frontend/dist/js/hi-BEef4bjU.js +2 -0
- package/frontend/dist/js/{html-HvA9vEyn.js → html-B_cUU1Gi.js} +2 -2
- package/frontend/dist/js/id-Djipic7Y.js +2 -0
- package/frontend/dist/js/is-pazl6RLG.js +2 -0
- package/frontend/dist/js/it-DobPbTwl.js +2 -0
- package/frontend/dist/js/ja-DwYNaMNX.js +2 -0
- package/frontend/dist/js/ko-BQfXE7_o.js +2 -0
- package/frontend/dist/js/{markdown-DaF0SXNk.js → markdown-BAF0w-jL.js} +3 -3
- package/frontend/dist/js/{messageCodeStyles-DKq3DH1W.js → messageCodeStyles-CkxF6yil.js} +2 -2
- package/frontend/dist/js/ms-CkPQoE5T.js +2 -0
- package/frontend/dist/js/nl-X_QsDAtu.js +2 -0
- package/frontend/dist/js/pl-CK39gUsy.js +2 -0
- package/frontend/dist/js/{postcss-RCIJRrFm.js → postcss-B2vYq4-H.js} +1 -1
- package/frontend/dist/js/pt-C0dWyR_I.js +2 -0
- package/frontend/dist/js/{router-vendor-BGP0k6Y2.js → router-vendor-CIwPu612.js} +1 -1
- package/frontend/dist/js/ru-sNJkEz1G.js +2 -0
- package/frontend/dist/js/{standalone-BMD7E0jY.js → standalone-BqZmfuRy.js} +2 -2
- package/frontend/dist/js/sv-Bh-uxdF3.js +2 -0
- package/frontend/dist/js/{teamEventStream-C4u_py2D.js → teamEventStream-DUodK0-C.js} +1 -1
- package/frontend/dist/js/th-DPVtwZj2.js +2 -0
- package/frontend/dist/js/tr-BYNM_Tm7.js +2 -0
- package/frontend/dist/js/{ttsBatching-C9kQXFB1.js → ttsBatching-7_-p2nxR.js} +1 -1
- package/frontend/dist/js/{typescript-LOiqVRM1.js → typescript-DokwN-5Y.js} +1 -1
- package/frontend/dist/js/{ui-DUzqxWMk.js → ui-BK53RPmS.js} +2 -2
- package/frontend/dist/js/ui-vendor-CPdYrKhz.js +177 -0
- package/frontend/dist/js/uk-kvNBuYin.js +2 -0
- package/frontend/dist/js/{useChat-6yDWZSTz.js → useChat-Czg2p4Rr.js} +1 -1
- package/frontend/dist/js/useDictation-B2lYEYKn.js +1 -0
- package/frontend/dist/js/useQuery-ClAiNxQ7.js +1 -0
- package/frontend/dist/js/{useTranslation-DrnYb6Cx.js → useTranslation-C7GwbUQA.js} +1 -1
- package/frontend/dist/js/{utils-D9UstwTG.js → utils-nNyg0lfc.js} +1 -1
- package/frontend/dist/js/utils-vendor-CeUf_ngc.js +9 -0
- package/frontend/dist/js/vi-CmLoaP0K.js +2 -0
- package/frontend/dist/js/workApi-BoJb9aVF.js +1 -0
- package/frontend/dist/js/{workApi-BwDBwG62.js → workApi-CFH097y5.js} +3 -3
- package/frontend/dist/js/{yaml-CyhEdlt2.js → yaml-BXEpxPn2.js} +2 -2
- package/frontend/dist/js/zh-D2zvmvrj.js +2 -0
- package/frontend/dist/sw.js +7 -1
- package/package.json +5 -5
- package/frontend/dist/assets/index-O8rtJINp.css +0 -1
- package/frontend/dist/assets/index-f1s6QpC5.js +0 -93
- package/frontend/dist/js/AutomationModal-DB3lrK2K.js +0 -1
- package/frontend/dist/js/AutomationsPage-D3v0Cra1.js +0 -1
- package/frontend/dist/js/ChannelsPage-BnoCniNf.js +0 -1
- package/frontend/dist/js/ChatPage-BMcigJWr.js +0 -14
- package/frontend/dist/js/FirstTimeSetup-BRysXCjP.js +0 -1
- package/frontend/dist/js/GalleryPage-DUcM4dg0.js +0 -1
- package/frontend/dist/js/LibreClawPage-A37KF9_G.js +0 -1
- package/frontend/dist/js/ModelSelector-C1ipX-44.js +0 -1
- package/frontend/dist/js/NotesPage-CXSz7Hde.js +0 -3
- package/frontend/dist/js/PersonasPage-CiOBffF3.js +0 -13
- package/frontend/dist/js/RichMessageContent-CU6WTuOs.js +0 -1
- package/frontend/dist/js/SettingsModal-DP_wKehV.js +0 -19
- package/frontend/dist/js/ShareDialog-BW8iGd_i.js +0 -1
- package/frontend/dist/js/WorkPage-BgASwAGs.js +0 -6
- package/frontend/dist/js/api-BmIdrQfK.js +0 -1
- package/frontend/dist/js/appVersion-BRMnr50t.js +0 -1
- package/frontend/dist/js/ar-CpmN0TY8.js +0 -2
- package/frontend/dist/js/authStore-DIvsSaOt.js +0 -4
- package/frontend/dist/js/authStore-DoxafXFR.js +0 -1
- package/frontend/dist/js/axios-C8xWfUG8.js +0 -1
- package/frontend/dist/js/bn-BwrMg_Oh.js +0 -2
- package/frontend/dist/js/chatStore-DkpmZLOG.js +0 -3
- package/frontend/dist/js/classPrivateMethodInitSpec-DUpGnGHL.js +0 -1
- package/frontend/dist/js/config-Ckn9cand.js +0 -1
- package/frontend/dist/js/cs-f72Fnxz3.js +0 -2
- package/frontend/dist/js/da-qp8lOgkJ.js +0 -2
- package/frontend/dist/js/de-CpcLHr97.js +0 -2
- package/frontend/dist/js/es-gZHsxjG4.js +0 -2
- package/frontend/dist/js/fr-BY5aUofi.js +0 -2
- package/frontend/dist/js/hi-KCn9gIiT.js +0 -2
- package/frontend/dist/js/id-C7U6kS0J.js +0 -2
- package/frontend/dist/js/is-N9Ek-Usz.js +0 -2
- package/frontend/dist/js/it-CqaqWB8D.js +0 -2
- package/frontend/dist/js/ja-C5f9HAm1.js +0 -2
- package/frontend/dist/js/ko-BuoboDvb.js +0 -2
- package/frontend/dist/js/ms-Cu-UWc6j.js +0 -2
- package/frontend/dist/js/nl-nOUc4C_i.js +0 -2
- package/frontend/dist/js/pl-BKKwFhxj.js +0 -2
- package/frontend/dist/js/pt-B18-KYtS.js +0 -2
- package/frontend/dist/js/ru-DflEWNyb.js +0 -2
- package/frontend/dist/js/sv-CU-nPjr8.js +0 -2
- package/frontend/dist/js/th-dOxm2Hhl.js +0 -2
- package/frontend/dist/js/tr-7ThM4F8q.js +0 -2
- package/frontend/dist/js/ui-vendor-C0KwbTD4.js +0 -177
- package/frontend/dist/js/uk-Cl7Pk0Yf.js +0 -2
- package/frontend/dist/js/useQuery-CoWV2PmJ.js +0 -1
- package/frontend/dist/js/utils-vendor-DtsgPwcz.js +0 -9
- package/frontend/dist/js/vi-eljYUBOk.js +0 -2
- package/frontend/dist/js/workApi-uXUZSjsf.js +0 -1
- 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
|
-
|
|
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:
|
|
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
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
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;
|