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.
- 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 +149 -0
- 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/work.d.ts.map +1 -1
- package/backend/dist/routes/work.js +72 -0
- package/backend/dist/routes/work.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/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 +55 -0
- package/backend/dist/services/workAgentService.d.ts.map +1 -1
- package/backend/dist/services/workAgentService.js +425 -13
- 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/package.json +1 -1
- package/frontend/dist/artifact-runtime/.build-fingerprint +1 -1
- package/frontend/dist/assets/{index-f1s6QpC5.js → index-C04eLDsy.js} +63 -78
- package/frontend/dist/index.html +15 -15
- package/frontend/dist/js/{ArtifactContainer-CTXxnWRo.js → ArtifactContainer-ivbLUiBh.js} +1 -1
- package/frontend/dist/js/{ArtifactDemoPage-CEYuIhJA.js → ArtifactDemoPage-BmOteTzj.js} +1 -1
- package/frontend/dist/js/{ArtifactSandboxFrame-DLOy2VOm.js → ArtifactSandboxFrame-CFIXhQGP.js} +1 -1
- package/frontend/dist/js/{ArtifactSlideOutPanel-DnTTc1VY.js → ArtifactSlideOutPanel-DTGX1bsD.js} +1 -1
- package/frontend/dist/js/AutomationModal-DWjiyu3B.js +1 -0
- package/frontend/dist/js/{AutomationsPage-D3v0Cra1.js → AutomationsPage-DA9uLEG2.js} +1 -1
- package/frontend/dist/js/{AvatarUpload-DzL00yOL.js → AvatarUpload-26z_BlGH.js} +1 -1
- package/frontend/dist/js/{CalendarPage-Bls3TBeZ.js → CalendarPage-CFU2zsFJ.js} +1 -1
- package/frontend/dist/js/{ChannelsPage-BnoCniNf.js → ChannelsPage-BqUJMmjD.js} +1 -1
- package/frontend/dist/js/ChatPage-D8I4CNtg.js +14 -0
- package/frontend/dist/js/{EvaluationsPage-DSgj9dT5.js → EvaluationsPage-jmb_F4xz.js} +1 -1
- package/frontend/dist/js/{FirstTimeSetup-BRysXCjP.js → FirstTimeSetup-DcOvjDh4.js} +1 -1
- package/frontend/dist/js/{GalleryPage-DUcM4dg0.js → GalleryPage-CFYgjwey.js} +1 -1
- package/frontend/dist/js/{HomePage-BebX3v-x.js → HomePage-DDt6s4Ya.js} +1 -1
- package/frontend/dist/js/{LibreClawPage-A37KF9_G.js → LibreClawPage-BqtLshHp.js} +1 -1
- package/frontend/dist/js/{NotesPage-CXSz7Hde.js → NotesPage-wdPw2owK.js} +1 -1
- package/frontend/dist/js/{PersonasPage-CiOBffF3.js → PersonasPage-DMJ3MioQ.js} +1 -1
- package/frontend/dist/js/{PluginUsagePage-jMrpL2Jb.js → PluginUsagePage-Di_B_DJQ.js} +1 -1
- package/frontend/dist/js/RichMessageContent-S1-4RhLW.js +1 -0
- package/frontend/dist/js/{RichMessageContent-CC9QZQKd.js → RichMessageContent-wk0Eyab_.js} +1 -1
- package/frontend/dist/js/{SettingsModal-DP_wKehV.js → SettingsModal-d7I_FVJY.js} +1 -1
- package/frontend/dist/js/{ShareDialog-BW8iGd_i.js → ShareDialog-buTN0EBc.js} +1 -1
- package/frontend/dist/js/{SystemPage-Drd1neLN.js → SystemPage-CmgxjTS5.js} +1 -1
- package/frontend/dist/js/{UserManagementPage-CquciWKV.js → UserManagementPage-u2r5f_uX.js} +1 -1
- package/frontend/dist/js/WorkPage-CYth3I8M.js +7 -0
- package/frontend/dist/js/{api-2DulPtq1.js → api-IiXPQ8se.js} +2 -2
- package/frontend/dist/js/api-gEytqqET.js +1 -0
- package/frontend/dist/js/{appNavigation-DAGh98Yj.js → appNavigation-Bsk9DwFY.js} +1 -1
- package/frontend/dist/js/{appStore-Ci1X0yA-.js → appStore-D_3JwBEY.js} +2 -2
- package/frontend/dist/js/appVersion-Chg_Ru57.js +1 -0
- package/frontend/dist/js/{ar-CpmN0TY8.js → ar-BKsbznj2.js} +1 -1
- package/frontend/dist/js/authStore-BiWhwkJN.js +1 -0
- package/frontend/dist/js/authStore-jbmnSGyL.js +4 -0
- package/frontend/dist/js/{bn-BwrMg_Oh.js → bn-DXd4s3g6.js} +1 -1
- package/frontend/dist/js/{chatStore-DkpmZLOG.js → chatStore-BXWiKI6E.js} +3 -3
- package/frontend/dist/js/{config-Ckn9cand.js → config-CtVytjAm.js} +1 -1
- package/frontend/dist/js/cs-CtvHHZ6D.js +2 -0
- package/frontend/dist/js/da-CRCQinmr.js +2 -0
- package/frontend/dist/js/de-DKjz-eJm.js +2 -0
- package/frontend/dist/js/es-K0j99jVL.js +2 -0
- package/frontend/dist/js/{evaluationsApi-Cu5PH_qi.js → evaluationsApi-CNcvUpmS.js} +1 -1
- package/frontend/dist/js/{fr-BY5aUofi.js → fr-BcQtHFjN.js} +1 -1
- package/frontend/dist/js/{hi-KCn9gIiT.js → hi-B69aSfMV.js} +1 -1
- package/frontend/dist/js/id-DlEUyPF3.js +2 -0
- package/frontend/dist/js/is-DhIcxuEq.js +2 -0
- package/frontend/dist/js/{it-CqaqWB8D.js → it-BE6UfTtm.js} +1 -1
- package/frontend/dist/js/ja-BNmjqYW2.js +2 -0
- package/frontend/dist/js/ko-bnvJ-aG1.js +2 -0
- package/frontend/dist/js/{messageCodeStyles-DKq3DH1W.js → messageCodeStyles-D1-WsF_0.js} +1 -1
- package/frontend/dist/js/ms-BHlYr228.js +2 -0
- package/frontend/dist/js/nl-B0dJJcU_.js +2 -0
- package/frontend/dist/js/pl-BQQOCUBv.js +2 -0
- package/frontend/dist/js/pt-CeCixlV6.js +2 -0
- package/frontend/dist/js/ru-CtbW6bcV.js +2 -0
- package/frontend/dist/js/sv-DvyO144q.js +2 -0
- package/frontend/dist/js/{teamEventStream-C4u_py2D.js → teamEventStream-B8hMmTgC.js} +1 -1
- package/frontend/dist/js/{th-dOxm2Hhl.js → th-Bsila0Wp.js} +1 -1
- package/frontend/dist/js/tr-CJ9yyiQZ.js +2 -0
- package/frontend/dist/js/{ui-DUzqxWMk.js → ui-BxB90kIo.js} +2 -2
- package/frontend/dist/js/{uk-Cl7Pk0Yf.js → uk-BZnidh3w.js} +1 -1
- package/frontend/dist/js/{useChat-6yDWZSTz.js → useChat-CP1Bi10i.js} +1 -1
- package/frontend/dist/js/useDictation-DF16QY6J.js +1 -0
- package/frontend/dist/js/vi-DgnSoExN.js +2 -0
- package/frontend/dist/js/{workApi-BwDBwG62.js → workApi-BUxESSmH.js} +3 -3
- package/frontend/dist/js/workApi-DrTI8jZw.js +1 -0
- package/frontend/dist/js/zh-C3NMIgKz.js +2 -0
- package/package.json +3 -3
- package/frontend/dist/js/AutomationModal-DB3lrK2K.js +0 -1
- package/frontend/dist/js/ChatPage-BMcigJWr.js +0 -14
- package/frontend/dist/js/ModelSelector-C1ipX-44.js +0 -1
- package/frontend/dist/js/RichMessageContent-CU6WTuOs.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/authStore-DIvsSaOt.js +0 -4
- package/frontend/dist/js/authStore-DoxafXFR.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/id-C7U6kS0J.js +0 -2
- package/frontend/dist/js/is-N9Ek-Usz.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/tr-7ThM4F8q.js +0 -2
- 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,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
|
-
|
|
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:
|
|
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
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
:
|
|
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;
|