libre-webui 0.14.3 → 0.15.1
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/README.md +117 -33
- package/backend/dist/db.d.ts.map +1 -1
- package/backend/dist/db.js +84 -0
- package/backend/dist/db.js.map +1 -1
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +99 -35
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/middleware/auth.d.ts.map +1 -1
- package/backend/dist/middleware/auth.js +27 -2
- package/backend/dist/middleware/auth.js.map +1 -1
- package/backend/dist/routes/plugins.d.ts.map +1 -1
- package/backend/dist/routes/plugins.js +1 -1
- package/backend/dist/routes/plugins.js.map +1 -1
- package/backend/dist/routes/users.d.ts.map +1 -1
- package/backend/dist/routes/users.js +40 -6
- package/backend/dist/routes/users.js.map +1 -1
- package/backend/dist/routes/work.d.ts +3 -0
- package/backend/dist/routes/work.d.ts.map +1 -0
- package/backend/dist/routes/work.js +604 -0
- package/backend/dist/routes/work.js.map +1 -0
- package/backend/dist/services/ollamaService.d.ts +7 -2
- package/backend/dist/services/ollamaService.d.ts.map +1 -1
- package/backend/dist/services/ollamaService.js +8 -8
- package/backend/dist/services/ollamaService.js.map +1 -1
- package/backend/dist/services/pluginService.d.ts.map +1 -1
- package/backend/dist/services/pluginService.js +15 -15
- package/backend/dist/services/pluginService.js.map +1 -1
- package/backend/dist/services/preferencesService.d.ts.map +1 -1
- package/backend/dist/services/preferencesService.js +1 -0
- package/backend/dist/services/preferencesService.js.map +1 -1
- package/backend/dist/services/workAgentGuidance.d.ts +24 -0
- package/backend/dist/services/workAgentGuidance.d.ts.map +1 -0
- package/backend/dist/services/workAgentGuidance.js +131 -0
- package/backend/dist/services/workAgentGuidance.js.map +1 -0
- package/backend/dist/services/workAgentService.d.ts +33 -0
- package/backend/dist/services/workAgentService.d.ts.map +1 -0
- package/backend/dist/services/workAgentService.js +935 -0
- package/backend/dist/services/workAgentService.js.map +1 -0
- package/backend/dist/services/workEventService.d.ts +40 -0
- package/backend/dist/services/workEventService.d.ts.map +1 -0
- package/backend/dist/services/workEventService.js +324 -0
- package/backend/dist/services/workEventService.js.map +1 -0
- package/backend/dist/services/workModelProviderService.d.ts +51 -0
- package/backend/dist/services/workModelProviderService.d.ts.map +1 -0
- package/backend/dist/services/workModelProviderService.js +952 -0
- package/backend/dist/services/workModelProviderService.js.map +1 -0
- package/backend/dist/services/workRuntimeService.d.ts +143 -0
- package/backend/dist/services/workRuntimeService.d.ts.map +1 -0
- package/backend/dist/services/workRuntimeService.js +1860 -0
- package/backend/dist/services/workRuntimeService.js.map +1 -0
- package/backend/dist/services/workTaskService.d.ts +96 -0
- package/backend/dist/services/workTaskService.d.ts.map +1 -0
- package/backend/dist/services/workTaskService.js +718 -0
- package/backend/dist/services/workTaskService.js.map +1 -0
- package/backend/dist/types/index.d.ts +3 -0
- 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 +209 -0
- package/backend/dist/types/work.d.ts.map +1 -0
- package/backend/dist/types/work.js +18 -0
- package/backend/dist/types/work.js.map +1 -0
- package/backend/dist/utils/ollamaLibrary.d.ts.map +1 -1
- package/backend/dist/utils/ollamaLibrary.js +81 -8
- package/backend/dist/utils/ollamaLibrary.js.map +1 -1
- package/backend/dist/utils/pluginChatAdapter.d.ts +9 -0
- package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -1
- package/backend/dist/utils/pluginChatAdapter.js +32 -6
- package/backend/dist/utils/pluginChatAdapter.js.map +1 -1
- package/backend/dist/utils/pluginStreamAdapter.d.ts +12 -0
- package/backend/dist/utils/pluginStreamAdapter.d.ts.map +1 -1
- package/backend/dist/utils/pluginStreamAdapter.js +168 -19
- package/backend/dist/utils/pluginStreamAdapter.js.map +1 -1
- package/backend/dist/utils/pluginStreaming.d.ts +10 -0
- package/backend/dist/utils/pluginStreaming.d.ts.map +1 -1
- package/backend/dist/utils/pluginStreaming.js.map +1 -1
- package/backend/dist/utils/pluginValidation.d.ts +1 -0
- package/backend/dist/utils/pluginValidation.d.ts.map +1 -1
- package/backend/dist/utils/pluginValidation.js +3 -0
- package/backend/dist/utils/pluginValidation.js.map +1 -1
- package/backend/package.json +1 -1
- package/electron/main.js +39 -16
- package/frontend/dist/assets/index-DkShhNBf.js +5 -0
- package/frontend/dist/assets/index-bdSWBsAM.css +1 -0
- package/frontend/dist/index.html +5 -4
- package/frontend/dist/js/{ArtifactContainer-CuvIIoHC.js → ArtifactContainer-BHSBrlQB.js} +1 -1
- package/frontend/dist/js/{ArtifactDemoPage-DI0QzAee.js → ArtifactDemoPage-BHGJmJAT.js} +1 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-s8vcE5Ea.js +1 -0
- package/frontend/dist/js/ChatPage-B6Hm_pdu.js +16 -0
- package/frontend/dist/js/FirstTimeSetup-BYWAVAdE.js +1 -0
- package/frontend/dist/js/GalleryPage-CH4f65q8.js +1 -0
- package/frontend/dist/js/LibreClawPage-CcH7M3mJ.js +1 -0
- package/frontend/dist/js/{ModelManagerModals-DfgizeSq.js → ModelManagerModals-DltRprHU.js} +1 -1
- package/frontend/dist/js/ModelSelector-T1KZtIde.js +1 -0
- package/frontend/dist/js/ModelsPage-C1bM_Yoc.js +2 -0
- package/frontend/dist/js/PersonasPage-T_PrhnPG.js +13 -0
- package/frontend/dist/js/RichMessageContent-BdU5vXz9.js +3 -0
- package/frontend/dist/js/SettingsModal-BVj-D1L4.js +1 -0
- package/frontend/dist/js/UserManagementPage-Pix71oRW.js +1 -0
- package/frontend/dist/js/WorkPage-DdOVGyj_.js +2 -0
- package/frontend/dist/js/api-Do6u0tMM.js +19 -0
- package/frontend/dist/js/{ar-UnnZB1Qy.js → ar-KceI0PfT.js} +2 -2
- package/frontend/dist/js/babel-BnLuavNv.js +16 -0
- package/frontend/dist/js/{bn-uZXaDyeV.js → bn-DsKo2dzS.js} +2 -2
- package/frontend/dist/js/{cs-CCXlnUEB.js → cs-6UKhKj0A.js} +2 -2
- package/frontend/dist/js/{da--BpVVGPp.js → da-CJgZ3YuL.js} +2 -2
- package/frontend/dist/js/{de-DZd9lsBc.js → de-BZAmOuCT.js} +2 -2
- package/frontend/dist/js/{es-Bm968oUX.js → es-CD4u7no9.js} +2 -2
- package/frontend/dist/js/estree-Bu_-Wi6O.js +44 -0
- package/frontend/dist/js/fr-Cou2jOS3.js +2 -0
- package/frontend/dist/js/{hi-C6kwkL7d.js → hi-ZxkbNCyB.js} +2 -2
- package/frontend/dist/js/html-Ca764Rus.js +25 -0
- package/frontend/dist/js/{id-CDfqnnXS.js → id-DT9CqaWQ.js} +2 -2
- package/frontend/dist/js/{is-Dnd_7Jqv.js → is-BWSbAIoq.js} +2 -2
- package/frontend/dist/js/{it-BM0j6_Sz.js → it-CdNoTXm3.js} +2 -2
- package/frontend/dist/js/{ja-xGblKDDf.js → ja-CIuQg0xv.js} +2 -2
- package/frontend/dist/js/{ko-CQ2d_Oj0.js → ko-BWRUjDOH.js} +2 -2
- package/frontend/dist/js/markdown-DaiQGPEs.js +70 -0
- package/frontend/dist/js/{ms-CFpRuv_a.js → ms-gJstIj2a.js} +2 -2
- package/frontend/dist/js/{nl-D6oTztmP.js → nl-CydjVNUw.js} +2 -2
- package/frontend/dist/js/{pl-ByuvABul.js → pl-DPN9mAa5.js} +2 -2
- package/frontend/dist/js/postcss-DSDkwgFE.js +62 -0
- package/frontend/dist/js/{pt-CNFV21T7.js → pt-MRXGqIjQ.js} +2 -2
- package/frontend/dist/js/router-vendor-BjDI9mRe.js +1 -0
- package/frontend/dist/js/{ru-CRsSOntz.js → ru-CvQ9GkJF.js} +2 -2
- package/frontend/dist/js/standalone-DWWnsbIr.js +29 -0
- package/frontend/dist/js/{sv-BRkwjyS5.js → sv-BxS2fvhU.js} +2 -2
- package/frontend/dist/js/{th-DigqsQEB.js → th-DMxyn6e4.js} +2 -2
- package/frontend/dist/js/{tr-B5aMUMJE.js → tr-BTPldUaA.js} +2 -2
- package/frontend/dist/js/typescript-CatjJeHm.js +22 -0
- package/frontend/dist/js/ui-vendor-CAFEDSaO.js +177 -0
- package/frontend/dist/js/{uk-BUYi3llT.js → uk-BGxZOKgp.js} +2 -2
- package/frontend/dist/js/useBaseQuery-CVNuhuID.js +1 -0
- package/frontend/dist/js/useQuery-CYhC_da4.js +1 -0
- package/frontend/dist/js/{vi-hQIcXi3m.js → vi-82JkwFsQ.js} +2 -2
- package/frontend/dist/js/yaml-mLy9chOK.js +154 -0
- package/frontend/dist/js/{zh-CUDZ3JDv.js → zh-Bxxfwci3.js} +2 -2
- package/package.json +4 -2
- package/plugins/kimi-code.json +6 -37
- package/plugins/mlx-lm.json +55 -0
- package/frontend/dist/assets/index-BKzwVaAM.js +0 -16
- package/frontend/dist/assets/index-BPvF-fpc.css +0 -1
- package/frontend/dist/js/ArtifactSlideOutPanel-BzBemzHy.js +0 -1
- package/frontend/dist/js/ChatPage-DqpXctC2.js +0 -16
- package/frontend/dist/js/FirstTimeSetup-DRWeCsVI.js +0 -1
- package/frontend/dist/js/GalleryPage-CgD8fVx0.js +0 -1
- package/frontend/dist/js/LibreClawPage-B_Yon2fQ.js +0 -1
- package/frontend/dist/js/ModelsPage-CAKQ7J-b.js +0 -2
- package/frontend/dist/js/PersonasPage-Dj6Bka4I.js +0 -13
- package/frontend/dist/js/RichMessageContent-DP7XoxIa.js +0 -3
- package/frontend/dist/js/SettingsModal-BFyYhZYE.js +0 -1
- package/frontend/dist/js/UserManagementPage-CGMnhZr1.js +0 -1
- package/frontend/dist/js/fr-RlC0rD8I.js +0 -2
- package/frontend/dist/js/router-vendor-Bbvzv1ZC.js +0 -1
- package/frontend/dist/js/ui-vendor-CurEAeCe.js +0 -177
- package/frontend/dist/js/useBaseQuery-B1on3xcz.js +0 -1
- package/frontend/dist/js/useQuery-CXvo2RBT.js +0 -1
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Libre WebUI
|
|
3
|
+
* Copyright (C) 2025 Kroonen AI, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import workModelProviderService, { WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY, WORK_TOOL_ARGUMENTS_ERROR_MESSAGE, } from './workModelProviderService.js';
|
|
18
|
+
import workEventService from './workEventService.js';
|
|
19
|
+
import { buildWorkAgentSystemPrompt, buildWorkBudgetExhaustionPrompt, WORK_AGENT_SKILLS, WORK_WRITE_FILE_RECOMMENDED_CHARS, workToolCallBudget, } from './workAgentGuidance.js';
|
|
20
|
+
import workRuntimeService from './workRuntimeService.js';
|
|
21
|
+
import workTaskService, { WorkConflictError, WorkNotFoundError, } from './workTaskService.js';
|
|
22
|
+
import { createLogger } from '../utils/logger.js';
|
|
23
|
+
const logger = createLogger('services:work-agent');
|
|
24
|
+
export const WORK_TOOL_SCHEMAS = [
|
|
25
|
+
functionTool('list_files', 'List direct children of a workspace directory.', {
|
|
26
|
+
path: stringProperty('Relative directory path. Defaults to workspace root.'),
|
|
27
|
+
}),
|
|
28
|
+
functionTool('read_file', 'Read a UTF-8 text file from the persistent workspace.', { path: stringProperty('Relative file path.') }, ['path']),
|
|
29
|
+
functionTool('write_file', 'Create or replace a UTF-8 text file in the persistent workspace.', {
|
|
30
|
+
path: stringProperty('Relative file path.'),
|
|
31
|
+
content: stringProperty(`Complete file content. Keep one write below ${WORK_WRITE_FILE_RECOMMENDED_CHARS.toLocaleString('en-US')} characters and split larger implementations into focused files.`),
|
|
32
|
+
}, ['path', 'content']),
|
|
33
|
+
functionTool('search_files', 'Search text recursively in workspace files.', {
|
|
34
|
+
query: stringProperty('Literal text to find.'),
|
|
35
|
+
path: stringProperty('Relative directory path. Defaults to workspace root.'),
|
|
36
|
+
}, ['query']),
|
|
37
|
+
functionTool('run_command', 'Run a shell command inside the isolated workspace container.', {
|
|
38
|
+
command: stringProperty('Shell command to run from /workspace.'),
|
|
39
|
+
timeout_ms: {
|
|
40
|
+
type: 'integer',
|
|
41
|
+
description: 'Optional timeout in milliseconds, capped at 600000.',
|
|
42
|
+
},
|
|
43
|
+
}, ['command']),
|
|
44
|
+
functionTool('start_preview', 'Start the workspace web application on the managed preview port. When command is omitted, Libre WebUI detects a package.json dev script or a static index.html.', {
|
|
45
|
+
command: stringProperty(`Optional custom server command. It must listen on 0.0.0.0:${workRuntimeService.previewPort}.`),
|
|
46
|
+
}),
|
|
47
|
+
functionTool('stop_preview', 'Stop the running workspace preview.', {}),
|
|
48
|
+
];
|
|
49
|
+
export class WorkAgentService {
|
|
50
|
+
constructor() {
|
|
51
|
+
this.controllers = new Map();
|
|
52
|
+
this.executions = new Map();
|
|
53
|
+
}
|
|
54
|
+
start(taskId, runId, userId) {
|
|
55
|
+
queueMicrotask(() => {
|
|
56
|
+
const execution = this.execute(taskId, runId, userId)
|
|
57
|
+
.catch(error => {
|
|
58
|
+
logger.error(`Unhandled Work run failure for ${runId}:`, error);
|
|
59
|
+
})
|
|
60
|
+
.finally(() => {
|
|
61
|
+
if (this.executions.get(runId) === execution) {
|
|
62
|
+
this.executions.delete(runId);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
this.executions.set(runId, execution);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async cancel(taskId, userId) {
|
|
69
|
+
const task = workTaskService.requireMutableTaskRecord(taskId, userId);
|
|
70
|
+
return this.cancelTask(task, userId);
|
|
71
|
+
}
|
|
72
|
+
async removeTask(taskId, userId) {
|
|
73
|
+
await this.removeTaskInternal(taskId, userId, false);
|
|
74
|
+
}
|
|
75
|
+
async cancelTask(task, userId) {
|
|
76
|
+
const taskId = task.id;
|
|
77
|
+
const run = workTaskService.getActiveRun(taskId);
|
|
78
|
+
if (!run) {
|
|
79
|
+
throw new WorkConflictError('This Work task has no active run.');
|
|
80
|
+
}
|
|
81
|
+
this.controllers.get(run.id)?.abort();
|
|
82
|
+
await workRuntimeService.stopContainer(task);
|
|
83
|
+
await this.executions.get(run.id);
|
|
84
|
+
if (isActiveRunStatus(workTaskService.getRun(run.id)?.status)) {
|
|
85
|
+
workTaskService.updateRun(run.id, 'cancelled', {
|
|
86
|
+
error: 'Cancelled by user.',
|
|
87
|
+
finished: true,
|
|
88
|
+
});
|
|
89
|
+
workTaskService.updateTaskStatus(taskId, 'cancelled');
|
|
90
|
+
workTaskService.updatePreview(taskId, 'stopped');
|
|
91
|
+
workEventService.publish(taskId, run.id, 'done', {
|
|
92
|
+
status: 'cancelled',
|
|
93
|
+
error: 'Cancelled by user.',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return workTaskService.requireTaskDetail(taskId, userId);
|
|
97
|
+
}
|
|
98
|
+
async removeTasksForUser(userId) {
|
|
99
|
+
workTaskService.beginUserRetirement(userId);
|
|
100
|
+
try {
|
|
101
|
+
const tasks = workTaskService.listTaskRecords(userId);
|
|
102
|
+
for (const task of tasks) {
|
|
103
|
+
await this.removeTaskInternal(task.id, userId, true);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
workTaskService.releaseUserRetirement(userId);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async revokeWorkAccessForUser(userId, revoke) {
|
|
112
|
+
workTaskService.beginUserRetirement(userId);
|
|
113
|
+
const tasks = workTaskService.listTaskRecords(userId);
|
|
114
|
+
const suspendedTaskIds = [];
|
|
115
|
+
try {
|
|
116
|
+
for (const task of tasks) {
|
|
117
|
+
workRuntimeService.beginTaskSuspension(task.id);
|
|
118
|
+
suspendedTaskIds.push(task.id);
|
|
119
|
+
}
|
|
120
|
+
// Persist revocation before depending on Docker cleanup. Even when a
|
|
121
|
+
// daemon outage prevents teardown, current-role authorization denies
|
|
122
|
+
// every subsequent Work request and a retry/restart can finish cleanup.
|
|
123
|
+
const result = await revoke();
|
|
124
|
+
const activeRuns = new Map(tasks.map(task => [task.id, workTaskService.getActiveRun(task.id)]));
|
|
125
|
+
for (const run of activeRuns.values()) {
|
|
126
|
+
if (run)
|
|
127
|
+
this.controllers.get(run.id)?.abort();
|
|
128
|
+
}
|
|
129
|
+
const cleanupResults = await Promise.allSettled(tasks.map(async (task) => {
|
|
130
|
+
const run = activeRuns.get(task.id);
|
|
131
|
+
try {
|
|
132
|
+
// Do not wait behind a helper or preview lifecycle operation before
|
|
133
|
+
// revoking its execution. The task suspension above prevents new
|
|
134
|
+
// work; the serialized stop below catches a concurrent create.
|
|
135
|
+
await workRuntimeService.interruptContainer(task);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
logger.warn(`Immediate Work interruption failed for task ${task.id}; retrying through the lifecycle queue:`, error);
|
|
139
|
+
}
|
|
140
|
+
await workRuntimeService.stopContainer(task);
|
|
141
|
+
if (run &&
|
|
142
|
+
isActiveRunStatus(workTaskService.getRun(run.id)?.status)) {
|
|
143
|
+
workTaskService.updateRun(run.id, 'cancelled', {
|
|
144
|
+
error: 'Administrator access was revoked.',
|
|
145
|
+
finished: true,
|
|
146
|
+
});
|
|
147
|
+
workTaskService.updateTaskStatus(task.id, 'cancelled');
|
|
148
|
+
workEventService.publish(task.id, run.id, 'done', {
|
|
149
|
+
status: 'cancelled',
|
|
150
|
+
error: 'Administrator access was revoked.',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
workTaskService.updatePreview(task.id, 'stopped');
|
|
154
|
+
if (run)
|
|
155
|
+
await this.executions.get(run.id);
|
|
156
|
+
}));
|
|
157
|
+
const cleanupFailure = cleanupResults.find(cleanupResult => cleanupResult.status === 'rejected');
|
|
158
|
+
if (cleanupFailure?.status === 'rejected') {
|
|
159
|
+
throw cleanupFailure.reason;
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
for (const taskId of suspendedTaskIds) {
|
|
165
|
+
workRuntimeService.releaseTaskSuspension(taskId);
|
|
166
|
+
}
|
|
167
|
+
workTaskService.releaseUserRetirement(userId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
releaseUserRetirement(userId) {
|
|
171
|
+
workTaskService.releaseUserRetirement(userId);
|
|
172
|
+
}
|
|
173
|
+
async removeTaskInternal(taskId, userId, allowRetiringUser) {
|
|
174
|
+
const task = workTaskService.beginTaskRetirement(taskId, userId, allowRetiringUser);
|
|
175
|
+
try {
|
|
176
|
+
if (workTaskService.getActiveRun(taskId)) {
|
|
177
|
+
await this.cancelTask(task, userId);
|
|
178
|
+
}
|
|
179
|
+
await workRuntimeService.removeTask(task, allowRetiringUser);
|
|
180
|
+
try {
|
|
181
|
+
workTaskService.deleteTask(taskId, userId);
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
workRuntimeService.finalizeTaskRemoval(taskId);
|
|
185
|
+
}
|
|
186
|
+
workTaskService.finalizeTaskRetirement(taskId);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
workTaskService.releaseTaskRetirement(taskId);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async shutdown() {
|
|
194
|
+
workRuntimeService.beginShutdown();
|
|
195
|
+
for (const controller of this.controllers.values()) {
|
|
196
|
+
controller.abort();
|
|
197
|
+
}
|
|
198
|
+
const tasks = workTaskService.listAllTaskRecords();
|
|
199
|
+
const results = await Promise.allSettled(tasks.map(task => workRuntimeService.stopContainer(task)));
|
|
200
|
+
await Promise.allSettled(this.executions.values());
|
|
201
|
+
return {
|
|
202
|
+
stopped: results.filter(result => result.status === 'fulfilled').length,
|
|
203
|
+
failed: results.filter(result => result.status === 'rejected').length,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
async execute(taskId, runId, userId) {
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
this.controllers.set(runId, controller);
|
|
209
|
+
let task;
|
|
210
|
+
let releaseExecutionLease;
|
|
211
|
+
let executionContainerSettled = false;
|
|
212
|
+
const settleExecutionContainer = async () => {
|
|
213
|
+
if (!task || executionContainerSettled)
|
|
214
|
+
return;
|
|
215
|
+
await this.cleanupExecutionContainer(task);
|
|
216
|
+
executionContainerSettled = true;
|
|
217
|
+
};
|
|
218
|
+
try {
|
|
219
|
+
task = workTaskService.requireTaskRecord(taskId, userId);
|
|
220
|
+
const run = workTaskService.getRun(runId);
|
|
221
|
+
if (!run || run.taskId !== taskId) {
|
|
222
|
+
throw new WorkNotFoundError('Work run not found.');
|
|
223
|
+
}
|
|
224
|
+
// A cancel request can arrive in the microtask gap between persisting a
|
|
225
|
+
// queued run and starting its executor. Never revive a terminal run.
|
|
226
|
+
if (run.status !== 'queued') {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
workTaskService.updateRun(runId, 'preparing', { started: true });
|
|
230
|
+
workTaskService.updateTaskStatus(taskId, 'preparing');
|
|
231
|
+
workEventService.publish(taskId, runId, 'run_state', {
|
|
232
|
+
status: 'preparing',
|
|
233
|
+
phase: 'preparing',
|
|
234
|
+
});
|
|
235
|
+
for (const skill of WORK_AGENT_SKILLS) {
|
|
236
|
+
workEventService.publish(taskId, runId, 'skill_loaded', {
|
|
237
|
+
id: skill.id,
|
|
238
|
+
name: skill.title,
|
|
239
|
+
description: skill.instructions[0],
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
releaseExecutionLease = await workRuntimeService.prepare(task, controller.signal);
|
|
243
|
+
this.throwIfCancelled(runId, controller);
|
|
244
|
+
workTaskService.updateRun(runId, 'running', { started: true });
|
|
245
|
+
workTaskService.updateTaskStatus(taskId, 'running');
|
|
246
|
+
const roundLimit = workRuntimeService.limits.maxRounds;
|
|
247
|
+
const toolCallLimit = workToolCallBudget(roundLimit);
|
|
248
|
+
const messages = this.contextMessages(task, roundLimit);
|
|
249
|
+
let totalToolCalls = 0;
|
|
250
|
+
let accumulatedInputTokens = 0;
|
|
251
|
+
let accumulatedOutputTokens = 0;
|
|
252
|
+
let streamedAssistantTotal = '';
|
|
253
|
+
let streamedReasoningTotal = '';
|
|
254
|
+
let budgetReason = 'round';
|
|
255
|
+
roundLoop: for (let round = 0; round < roundLimit; round++) {
|
|
256
|
+
this.throwIfCancelled(runId, controller);
|
|
257
|
+
workEventService.publish(taskId, runId, 'run_state', {
|
|
258
|
+
status: 'running',
|
|
259
|
+
phase: 'thinking',
|
|
260
|
+
round: round + 1,
|
|
261
|
+
roundLimit,
|
|
262
|
+
});
|
|
263
|
+
const contentStream = new WorkDeltaPublisher(taskId, runId, 'assistant_delta', streamedAssistantTotal);
|
|
264
|
+
const reasoningStream = new WorkDeltaPublisher(taskId, runId, 'reasoning_delta', streamedReasoningTotal);
|
|
265
|
+
let roundInputTokens = 0;
|
|
266
|
+
let roundOutputTokens = 0;
|
|
267
|
+
const roundStartedAt = Date.now();
|
|
268
|
+
let response;
|
|
269
|
+
try {
|
|
270
|
+
response = await workModelProviderService.generateChatStreamResponse({
|
|
271
|
+
model: run.model,
|
|
272
|
+
messages,
|
|
273
|
+
tools: WORK_TOOL_SCHEMAS,
|
|
274
|
+
stream: true,
|
|
275
|
+
}, {
|
|
276
|
+
providerType: run.providerType,
|
|
277
|
+
providerId: run.providerId,
|
|
278
|
+
}, userId, {
|
|
279
|
+
onContent: delta => contentStream.push(delta),
|
|
280
|
+
onReasoning: delta => reasoningStream.push(delta),
|
|
281
|
+
onUsage: usage => {
|
|
282
|
+
roundInputTokens = usage.promptTokens ?? roundInputTokens;
|
|
283
|
+
roundOutputTokens = usage.completionTokens ?? roundOutputTokens;
|
|
284
|
+
workEventService.publish(taskId, runId, 'usage', {
|
|
285
|
+
inputTokens: accumulatedInputTokens + roundInputTokens,
|
|
286
|
+
outputTokens: accumulatedOutputTokens + roundOutputTokens,
|
|
287
|
+
totalTokens: accumulatedInputTokens +
|
|
288
|
+
roundInputTokens +
|
|
289
|
+
accumulatedOutputTokens +
|
|
290
|
+
roundOutputTokens,
|
|
291
|
+
durationMs: Date.now() - roundStartedAt,
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
}, controller.signal);
|
|
295
|
+
}
|
|
296
|
+
finally {
|
|
297
|
+
contentStream.flush();
|
|
298
|
+
reasoningStream.flush();
|
|
299
|
+
streamedAssistantTotal = contentStream.currentTotal;
|
|
300
|
+
streamedReasoningTotal = reasoningStream.currentTotal;
|
|
301
|
+
}
|
|
302
|
+
this.throwIfCancelled(runId, controller);
|
|
303
|
+
accumulatedInputTokens += roundInputTokens;
|
|
304
|
+
accumulatedOutputTokens += roundOutputTokens;
|
|
305
|
+
const toolCalls = normalizeToolCalls(response);
|
|
306
|
+
totalToolCalls += toolCalls.length;
|
|
307
|
+
if (totalToolCalls > toolCallLimit) {
|
|
308
|
+
budgetReason = 'tool-call';
|
|
309
|
+
break roundLoop;
|
|
310
|
+
}
|
|
311
|
+
const assistantContent = boundUtf8(response.message?.content?.trim() || '', 100000);
|
|
312
|
+
const reasoningContent = boundUtf8(response.message?.thinking?.trim() || '', 100000);
|
|
313
|
+
if (reasoningContent) {
|
|
314
|
+
workTaskService.addMessage(taskId, runId, 'assistant', 'reasoning', reasoningContent, { providerExposed: true, round: round + 1 });
|
|
315
|
+
}
|
|
316
|
+
if (toolCalls.length === 0) {
|
|
317
|
+
const finalContent = assistantContent ||
|
|
318
|
+
'The model completed without returning a text response.';
|
|
319
|
+
workTaskService.addMessage(taskId, runId, 'assistant', 'message', finalContent);
|
|
320
|
+
// Keep completion hidden until the disposable container has either
|
|
321
|
+
// stopped or been retained for a verified live preview. Consumers
|
|
322
|
+
// can then refresh files immediately without racing teardown.
|
|
323
|
+
await settleExecutionContainer();
|
|
324
|
+
this.throwIfCancelled(runId, controller);
|
|
325
|
+
if (!isActiveRunStatus(workTaskService.getRun(runId)?.status)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
workTaskService.updateRun(runId, 'completed', { finished: true });
|
|
329
|
+
workTaskService.updateTaskStatus(taskId, 'completed');
|
|
330
|
+
workEventService.publish(taskId, runId, 'done', {
|
|
331
|
+
status: 'completed',
|
|
332
|
+
});
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const toolValidationErrors = new Map();
|
|
336
|
+
for (const call of toolCalls) {
|
|
337
|
+
const validationError = validateToolCallArguments(call);
|
|
338
|
+
if (validationError)
|
|
339
|
+
toolValidationErrors.set(call, validationError);
|
|
340
|
+
}
|
|
341
|
+
const hasInvalidToolArguments = toolValidationErrors.size > 0;
|
|
342
|
+
messages.push({
|
|
343
|
+
role: 'assistant',
|
|
344
|
+
content: assistantContent,
|
|
345
|
+
tool_calls: toolCalls,
|
|
346
|
+
});
|
|
347
|
+
if (assistantContent) {
|
|
348
|
+
workTaskService.addMessage(taskId, runId, 'assistant', 'message', assistantContent);
|
|
349
|
+
}
|
|
350
|
+
for (const call of toolCalls) {
|
|
351
|
+
this.throwIfCancelled(runId, controller);
|
|
352
|
+
const toolCallMetadata = summarizeToolCall(call);
|
|
353
|
+
const toolCallMessage = workTaskService.addMessage(taskId, runId, 'assistant', 'tool_call', `Calling ${call.function.name}`, toolCallMetadata);
|
|
354
|
+
workEventService.publish(taskId, runId, 'run_state', {
|
|
355
|
+
status: 'running',
|
|
356
|
+
phase: 'using_tool',
|
|
357
|
+
round: round + 1,
|
|
358
|
+
roundLimit,
|
|
359
|
+
});
|
|
360
|
+
workEventService.publish(taskId, runId, 'tool_call', {
|
|
361
|
+
toolCallId: call.id,
|
|
362
|
+
name: call.function.name,
|
|
363
|
+
arguments: toolCallMetadata,
|
|
364
|
+
metadata: toolCallMetadata,
|
|
365
|
+
phase: 'running',
|
|
366
|
+
message: toolCallMessage,
|
|
367
|
+
});
|
|
368
|
+
let toolOutput;
|
|
369
|
+
let toolMetadata = {
|
|
370
|
+
name: call.function.name,
|
|
371
|
+
toolCallId: call.id,
|
|
372
|
+
toolName: call.function.name,
|
|
373
|
+
};
|
|
374
|
+
try {
|
|
375
|
+
const validationError = toolValidationErrors.get(call);
|
|
376
|
+
if (validationError) {
|
|
377
|
+
throw new WorkAgentHttpError(validationError, 400, 'WORK_INVALID_TOOL_ARGUMENTS');
|
|
378
|
+
}
|
|
379
|
+
if (hasInvalidToolArguments) {
|
|
380
|
+
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');
|
|
381
|
+
}
|
|
382
|
+
const result = await this.executeTool(task, call);
|
|
383
|
+
toolOutput = result.content;
|
|
384
|
+
toolMetadata = { ...toolMetadata, ...result.metadata };
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
toolOutput =
|
|
388
|
+
error instanceof Error
|
|
389
|
+
? `Tool error: ${error.message}`
|
|
390
|
+
: 'Tool error.';
|
|
391
|
+
toolMetadata.error = true;
|
|
392
|
+
}
|
|
393
|
+
const boundedOutput = boundPersistedToolOutput(toolOutput);
|
|
394
|
+
if (boundedOutput !== toolOutput) {
|
|
395
|
+
toolMetadata.outputTruncated = true;
|
|
396
|
+
toolOutput = boundedOutput;
|
|
397
|
+
}
|
|
398
|
+
const toolResultMessage = workTaskService.addMessage(taskId, runId, 'tool', 'tool_result', toolOutput, toolMetadata);
|
|
399
|
+
workEventService.publish(taskId, runId, 'tool_result', {
|
|
400
|
+
toolCallId: call.id,
|
|
401
|
+
name: call.function.name,
|
|
402
|
+
phase: toolMetadata.error ? 'failed' : 'completed',
|
|
403
|
+
content: toolOutput,
|
|
404
|
+
error: toolMetadata.error === true,
|
|
405
|
+
message: toolResultMessage,
|
|
406
|
+
});
|
|
407
|
+
messages.push({
|
|
408
|
+
role: 'tool',
|
|
409
|
+
content: toolOutput,
|
|
410
|
+
tool_name: call.function.name,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
this.throwIfCancelled(runId, controller);
|
|
415
|
+
workEventService.publish(taskId, runId, 'run_state', {
|
|
416
|
+
status: 'running',
|
|
417
|
+
phase: 'responding',
|
|
418
|
+
round: roundLimit,
|
|
419
|
+
roundLimit,
|
|
420
|
+
});
|
|
421
|
+
const handoffContentStream = new WorkDeltaPublisher(taskId, runId, 'assistant_delta', streamedAssistantTotal);
|
|
422
|
+
const handoffReasoningStream = new WorkDeltaPublisher(taskId, runId, 'reasoning_delta', streamedReasoningTotal);
|
|
423
|
+
let handoffInputTokens = 0;
|
|
424
|
+
let handoffOutputTokens = 0;
|
|
425
|
+
const handoffStartedAt = Date.now();
|
|
426
|
+
let handoffResponse;
|
|
427
|
+
try {
|
|
428
|
+
try {
|
|
429
|
+
handoffResponse =
|
|
430
|
+
await workModelProviderService.generateChatStreamResponse({
|
|
431
|
+
model: run.model,
|
|
432
|
+
messages: [
|
|
433
|
+
...messages,
|
|
434
|
+
{
|
|
435
|
+
role: 'user',
|
|
436
|
+
content: `${buildWorkBudgetExhaustionPrompt()}\nThe ${budgetReason} budget was reached.`,
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
tools: [],
|
|
440
|
+
options: run.providerType === 'plugin'
|
|
441
|
+
? { num_predict: 2048 }
|
|
442
|
+
: undefined,
|
|
443
|
+
stream: true,
|
|
444
|
+
}, {
|
|
445
|
+
providerType: run.providerType,
|
|
446
|
+
providerId: run.providerId,
|
|
447
|
+
}, userId, {
|
|
448
|
+
onContent: delta => handoffContentStream.push(delta),
|
|
449
|
+
onReasoning: delta => handoffReasoningStream.push(delta),
|
|
450
|
+
onUsage: usage => {
|
|
451
|
+
handoffInputTokens = usage.promptTokens ?? handoffInputTokens;
|
|
452
|
+
handoffOutputTokens =
|
|
453
|
+
usage.completionTokens ?? handoffOutputTokens;
|
|
454
|
+
workEventService.publish(taskId, runId, 'usage', {
|
|
455
|
+
inputTokens: accumulatedInputTokens + handoffInputTokens,
|
|
456
|
+
outputTokens: accumulatedOutputTokens + handoffOutputTokens,
|
|
457
|
+
totalTokens: accumulatedInputTokens +
|
|
458
|
+
handoffInputTokens +
|
|
459
|
+
accumulatedOutputTokens +
|
|
460
|
+
handoffOutputTokens,
|
|
461
|
+
durationMs: Date.now() - handoffStartedAt,
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
}, controller.signal);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
if (controller.signal.aborted)
|
|
468
|
+
throw error;
|
|
469
|
+
const fallback = `The run reached its ${budgetReason} budget. The final provider handoff was unavailable${error instanceof Error ? `: ${error.message}` : '.'}\n\nStart a follow-up run to continue in the same durable workspace.`;
|
|
470
|
+
handoffContentStream.push(fallback);
|
|
471
|
+
handoffResponse = {
|
|
472
|
+
model: run.model,
|
|
473
|
+
created_at: new Date().toISOString(),
|
|
474
|
+
message: {
|
|
475
|
+
role: 'assistant',
|
|
476
|
+
content: fallback,
|
|
477
|
+
},
|
|
478
|
+
done: true,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
handoffContentStream.flush();
|
|
484
|
+
handoffReasoningStream.flush();
|
|
485
|
+
}
|
|
486
|
+
this.throwIfCancelled(runId, controller);
|
|
487
|
+
const handoffReasoning = boundUtf8(handoffResponse.message?.thinking?.trim() || '', 100000);
|
|
488
|
+
if (handoffReasoning) {
|
|
489
|
+
workTaskService.addMessage(taskId, runId, 'assistant', 'reasoning', handoffReasoning, { providerExposed: true, budgetHandoff: true });
|
|
490
|
+
}
|
|
491
|
+
const handoffContent = boundUtf8(handoffResponse.message?.content?.trim() ||
|
|
492
|
+
`The run reached its ${budgetReason} budget. Start a follow-up run to continue in the same workspace.`, 100000);
|
|
493
|
+
workTaskService.addMessage(taskId, runId, 'assistant', 'message', handoffContent, { budgetHandoff: true, budgetReason });
|
|
494
|
+
await settleExecutionContainer();
|
|
495
|
+
this.throwIfCancelled(runId, controller);
|
|
496
|
+
if (!isActiveRunStatus(workTaskService.getRun(runId)?.status))
|
|
497
|
+
return;
|
|
498
|
+
workTaskService.updateRun(runId, 'needs_input', { finished: true });
|
|
499
|
+
workTaskService.updateTaskStatus(taskId, 'needs_input');
|
|
500
|
+
workEventService.publish(taskId, runId, 'done', {
|
|
501
|
+
status: 'needs_input',
|
|
502
|
+
budgetReason,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
const currentRun = workTaskService.getRun(runId);
|
|
507
|
+
if (currentRun?.status === 'cancelled' || controller.signal.aborted) {
|
|
508
|
+
if (task) {
|
|
509
|
+
try {
|
|
510
|
+
await workRuntimeService.stopContainer(task);
|
|
511
|
+
executionContainerSettled = true;
|
|
512
|
+
if (isActiveRunStatus(workTaskService.getRun(runId)?.status)) {
|
|
513
|
+
workTaskService.updateRun(runId, 'cancelled', {
|
|
514
|
+
error: 'Cancelled by user.',
|
|
515
|
+
finished: true,
|
|
516
|
+
});
|
|
517
|
+
workTaskService.updateTaskStatus(taskId, 'cancelled');
|
|
518
|
+
workTaskService.updatePreview(taskId, 'stopped');
|
|
519
|
+
workEventService.publish(taskId, runId, 'done', {
|
|
520
|
+
status: 'cancelled',
|
|
521
|
+
error: 'Cancelled by user.',
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (cleanupError) {
|
|
526
|
+
logger.error(`Could not stop cancelled Work task ${taskId}:`, cleanupError);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
const message = error instanceof Error
|
|
532
|
+
? error.message
|
|
533
|
+
: 'Work run failed unexpectedly.';
|
|
534
|
+
await settleExecutionContainer();
|
|
535
|
+
const settledRun = workTaskService.getRun(runId);
|
|
536
|
+
if (controller.signal.aborted || settledRun?.status === 'cancelled') {
|
|
537
|
+
if (isActiveRunStatus(settledRun?.status)) {
|
|
538
|
+
workTaskService.updateRun(runId, 'cancelled', {
|
|
539
|
+
error: 'Cancelled by user.',
|
|
540
|
+
finished: true,
|
|
541
|
+
});
|
|
542
|
+
workTaskService.updateTaskStatus(taskId, 'cancelled');
|
|
543
|
+
workTaskService.updatePreview(taskId, 'stopped');
|
|
544
|
+
workEventService.publish(taskId, runId, 'done', {
|
|
545
|
+
status: 'cancelled',
|
|
546
|
+
error: 'Cancelled by user.',
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (!isActiveRunStatus(settledRun?.status)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
workTaskService.addMessage(taskId, runId, 'assistant', 'error', message);
|
|
555
|
+
workTaskService.updateRun(runId, 'failed', {
|
|
556
|
+
error: message,
|
|
557
|
+
finished: true,
|
|
558
|
+
});
|
|
559
|
+
workTaskService.updateTaskStatus(taskId, 'failed');
|
|
560
|
+
workEventService.publish(taskId, runId, 'error', {
|
|
561
|
+
message,
|
|
562
|
+
code: error instanceof WorkAgentHttpError ? error.code : undefined,
|
|
563
|
+
});
|
|
564
|
+
workEventService.publish(taskId, runId, 'done', {
|
|
565
|
+
status: 'failed',
|
|
566
|
+
error: message,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
this.controllers.delete(runId);
|
|
571
|
+
try {
|
|
572
|
+
await settleExecutionContainer();
|
|
573
|
+
}
|
|
574
|
+
finally {
|
|
575
|
+
releaseExecutionLease?.();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async cleanupExecutionContainer(task) {
|
|
580
|
+
try {
|
|
581
|
+
// A preview is intentionally allowed to outlive the model run so the
|
|
582
|
+
// user can inspect it in the Work pane. Preserve only a verified ready
|
|
583
|
+
// preview; every other run leaves no reason for the idle tail container
|
|
584
|
+
// to remain running.
|
|
585
|
+
if (await workRuntimeService.isPreviewRunning(task))
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
logger.warn(`Could not verify the preview after Work run ${task.id}; stopping its container:`, error);
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
await workRuntimeService.stopContainer(task);
|
|
593
|
+
if (workTaskService.getTaskRecord(task.id, task.userId)) {
|
|
594
|
+
workTaskService.updatePreview(task.id, 'stopped');
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
// Cleanup failures must remain visible without obscuring the model
|
|
599
|
+
// outcome; the caller publishes terminal run state after this returns.
|
|
600
|
+
logger.error(`Could not stop idle Work container ${task.containerName}:`, error);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
contextMessages(task, roundLimit) {
|
|
604
|
+
const persisted = workTaskService
|
|
605
|
+
.getRecentConversationMessages(task.id, 30)
|
|
606
|
+
.map(message => ({
|
|
607
|
+
role: message.role,
|
|
608
|
+
content: message.content,
|
|
609
|
+
}));
|
|
610
|
+
return [
|
|
611
|
+
{
|
|
612
|
+
role: 'system',
|
|
613
|
+
content: buildWorkAgentSystemPrompt({
|
|
614
|
+
networkEnabled: task.networkEnabled,
|
|
615
|
+
previewPort: workRuntimeService.previewPort,
|
|
616
|
+
roundBudget: roundLimit,
|
|
617
|
+
commandTimeoutMs: workRuntimeService.limits.commandTimeoutMs,
|
|
618
|
+
maxOutputChars: workRuntimeService.limits.maxOutputChars,
|
|
619
|
+
}),
|
|
620
|
+
},
|
|
621
|
+
...persisted,
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
async executeTool(task, call) {
|
|
625
|
+
const args = call.function.arguments;
|
|
626
|
+
switch (call.function.name) {
|
|
627
|
+
case 'list_files': {
|
|
628
|
+
const result = await workRuntimeService.listFiles(task, optionalString(args.path) || '.');
|
|
629
|
+
return {
|
|
630
|
+
content: JSON.stringify(result.entries, null, 2).slice(0, workRuntimeService.limits.maxOutputChars),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
case 'read_file': {
|
|
634
|
+
const result = await workRuntimeService.readFile(task, requiredString(args.path, 'path'));
|
|
635
|
+
return {
|
|
636
|
+
content: result.content.slice(0, workRuntimeService.limits.maxOutputChars),
|
|
637
|
+
metadata: { path: result.path, size: result.size },
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
case 'write_file': {
|
|
641
|
+
const result = await workRuntimeService.writeFile(task, requiredString(args.path, 'path'), requiredString(args.content, 'content', true));
|
|
642
|
+
return {
|
|
643
|
+
content: `Wrote ${result.size} bytes to ${result.path}.`,
|
|
644
|
+
metadata: { path: result.path, size: result.size },
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
case 'search_files': {
|
|
648
|
+
const result = await workRuntimeService.searchFiles(task, requiredString(args.query, 'query'), optionalString(args.path) || '.');
|
|
649
|
+
return commandResult(result);
|
|
650
|
+
}
|
|
651
|
+
case 'run_command': {
|
|
652
|
+
const timeout = optionalInteger(args.timeout_ms);
|
|
653
|
+
const result = await workRuntimeService.runCommand(task, requiredString(args.command, 'command'), timeout);
|
|
654
|
+
return commandResult(result);
|
|
655
|
+
}
|
|
656
|
+
case 'start_preview': {
|
|
657
|
+
const url = await workRuntimeService.startPreview(task, optionalString(args.command), {
|
|
658
|
+
onStarting: () => workTaskService.beginPreview(task.id, task.userId, true),
|
|
659
|
+
onRunning: previewUrl => workTaskService.updatePreview(task.id, 'running', previewUrl),
|
|
660
|
+
onFailed: () => workTaskService.updatePreview(task.id, 'failed'),
|
|
661
|
+
});
|
|
662
|
+
return {
|
|
663
|
+
content: `Preview started at ${url}.`,
|
|
664
|
+
metadata: { previewUrl: url },
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
case 'stop_preview':
|
|
668
|
+
await workRuntimeService.stopPreview(task, {
|
|
669
|
+
onStopped: () => workTaskService.updatePreview(task.id, 'stopped'),
|
|
670
|
+
});
|
|
671
|
+
return { content: 'Preview stopped.' };
|
|
672
|
+
default:
|
|
673
|
+
throw new WorkAgentHttpError(`Unknown tool: ${call.function.name}`, 400, 'WORK_UNKNOWN_TOOL');
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
throwIfCancelled(runId, controller) {
|
|
677
|
+
if (controller.signal.aborted ||
|
|
678
|
+
workTaskService.getRun(runId)?.status === 'cancelled') {
|
|
679
|
+
throw new WorkAgentHttpError('Work run was cancelled.', 409, 'WORK_RUN_CANCELLED');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
class WorkDeltaPublisher {
|
|
684
|
+
constructor(taskId, runId, type, initialTotal = '') {
|
|
685
|
+
this.taskId = taskId;
|
|
686
|
+
this.runId = runId;
|
|
687
|
+
this.type = type;
|
|
688
|
+
this.pending = '';
|
|
689
|
+
this.total = '';
|
|
690
|
+
this.separatorPending = '';
|
|
691
|
+
this.total = initialTotal;
|
|
692
|
+
this.separatorPending = initialTotal ? '\n\n' : '';
|
|
693
|
+
}
|
|
694
|
+
get currentTotal() {
|
|
695
|
+
return this.total;
|
|
696
|
+
}
|
|
697
|
+
push(delta) {
|
|
698
|
+
if (!delta)
|
|
699
|
+
return;
|
|
700
|
+
const next = `${this.separatorPending}${delta}`;
|
|
701
|
+
this.separatorPending = '';
|
|
702
|
+
this.pending += next;
|
|
703
|
+
this.total = boundUtf8(this.total + next, 100000);
|
|
704
|
+
if (this.timer)
|
|
705
|
+
return;
|
|
706
|
+
this.timer = setTimeout(() => this.flush(), 32);
|
|
707
|
+
this.timer.unref?.();
|
|
708
|
+
}
|
|
709
|
+
flush() {
|
|
710
|
+
if (this.timer) {
|
|
711
|
+
clearTimeout(this.timer);
|
|
712
|
+
this.timer = undefined;
|
|
713
|
+
}
|
|
714
|
+
if (!this.pending)
|
|
715
|
+
return;
|
|
716
|
+
const delta = this.pending;
|
|
717
|
+
this.pending = '';
|
|
718
|
+
workEventService.publish(this.taskId, this.runId, this.type, {
|
|
719
|
+
delta,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
export class WorkAgentHttpError extends Error {
|
|
724
|
+
constructor(message, status, code) {
|
|
725
|
+
super(message);
|
|
726
|
+
this.name = 'WorkAgentHttpError';
|
|
727
|
+
this.status = status;
|
|
728
|
+
this.code = code;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
export function normalizeToolCalls(response) {
|
|
732
|
+
const raw = response.message?.tool_calls;
|
|
733
|
+
if (!Array.isArray(raw))
|
|
734
|
+
return [];
|
|
735
|
+
return raw
|
|
736
|
+
.flatMap((value, index) => {
|
|
737
|
+
if (!value || typeof value !== 'object')
|
|
738
|
+
return [];
|
|
739
|
+
const record = value;
|
|
740
|
+
const fn = record.function && typeof record.function === 'object'
|
|
741
|
+
? record.function
|
|
742
|
+
: undefined;
|
|
743
|
+
if (!fn || typeof fn.name !== 'string')
|
|
744
|
+
return [];
|
|
745
|
+
let args = {};
|
|
746
|
+
let argumentError;
|
|
747
|
+
if (fn.arguments && typeof fn.arguments === 'object') {
|
|
748
|
+
args = fn.arguments;
|
|
749
|
+
}
|
|
750
|
+
else if (typeof fn.arguments === 'string') {
|
|
751
|
+
try {
|
|
752
|
+
const parsed = JSON.parse(fn.arguments);
|
|
753
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
754
|
+
args = parsed;
|
|
755
|
+
}
|
|
756
|
+
else if (fn.arguments.trim()) {
|
|
757
|
+
argumentError = WORK_TOOL_ARGUMENTS_ERROR_MESSAGE;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
args = {};
|
|
762
|
+
if (fn.arguments.trim()) {
|
|
763
|
+
argumentError = WORK_TOOL_ARGUMENTS_ERROR_MESSAGE;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
const providerMetadata = record.providerMetadata &&
|
|
768
|
+
typeof record.providerMetadata === 'object' &&
|
|
769
|
+
!Array.isArray(record.providerMetadata)
|
|
770
|
+
? {
|
|
771
|
+
...record.providerMetadata,
|
|
772
|
+
}
|
|
773
|
+
: {};
|
|
774
|
+
if (argumentError) {
|
|
775
|
+
providerMetadata[WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY] =
|
|
776
|
+
argumentError;
|
|
777
|
+
}
|
|
778
|
+
return [
|
|
779
|
+
{
|
|
780
|
+
id: typeof record.id === 'string'
|
|
781
|
+
? record.id
|
|
782
|
+
: `tool-${Date.now()}-${index}`,
|
|
783
|
+
...(typeof record.thoughtSignature === 'string'
|
|
784
|
+
? { thoughtSignature: record.thoughtSignature }
|
|
785
|
+
: {}),
|
|
786
|
+
...(Object.keys(providerMetadata).length > 0
|
|
787
|
+
? { providerMetadata }
|
|
788
|
+
: {}),
|
|
789
|
+
function: { name: fn.name, arguments: args },
|
|
790
|
+
},
|
|
791
|
+
];
|
|
792
|
+
})
|
|
793
|
+
.slice(0, 16);
|
|
794
|
+
}
|
|
795
|
+
function functionTool(name, description, properties, required = []) {
|
|
796
|
+
return {
|
|
797
|
+
type: 'function',
|
|
798
|
+
function: {
|
|
799
|
+
name,
|
|
800
|
+
description,
|
|
801
|
+
parameters: {
|
|
802
|
+
type: 'object',
|
|
803
|
+
properties,
|
|
804
|
+
required,
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
function stringProperty(description) {
|
|
810
|
+
return { type: 'string', description };
|
|
811
|
+
}
|
|
812
|
+
function requiredString(value, name, allowEmpty = false) {
|
|
813
|
+
if (typeof value !== 'string' || (!allowEmpty && !value.trim())) {
|
|
814
|
+
throw new WorkAgentHttpError(`Tool argument "${name}" must be a string.`, 400, 'WORK_INVALID_TOOL_ARGUMENT');
|
|
815
|
+
}
|
|
816
|
+
return value;
|
|
817
|
+
}
|
|
818
|
+
function optionalString(value) {
|
|
819
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
820
|
+
}
|
|
821
|
+
function optionalInteger(value) {
|
|
822
|
+
return typeof value === 'number' && Number.isInteger(value)
|
|
823
|
+
? value
|
|
824
|
+
: undefined;
|
|
825
|
+
}
|
|
826
|
+
function validateToolCallArguments(call) {
|
|
827
|
+
const providerArgumentError = call.providerMetadata?.[WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY];
|
|
828
|
+
if (typeof providerArgumentError === 'string') {
|
|
829
|
+
return providerArgumentError;
|
|
830
|
+
}
|
|
831
|
+
const args = call.function.arguments;
|
|
832
|
+
const invalidRequiredString = (name, allowEmpty = false) => {
|
|
833
|
+
const value = args[name];
|
|
834
|
+
if (typeof value === 'string' && (allowEmpty || value.trim())) {
|
|
835
|
+
return undefined;
|
|
836
|
+
}
|
|
837
|
+
return `The model returned invalid arguments for ${call.function.name}: "${name}" must be a string. Retry with every required field; split large write_file content into focused files.`;
|
|
838
|
+
};
|
|
839
|
+
switch (call.function.name) {
|
|
840
|
+
case 'list_files':
|
|
841
|
+
case 'start_preview':
|
|
842
|
+
case 'stop_preview':
|
|
843
|
+
return undefined;
|
|
844
|
+
case 'read_file':
|
|
845
|
+
return invalidRequiredString('path');
|
|
846
|
+
case 'write_file':
|
|
847
|
+
return (invalidRequiredString('path') || invalidRequiredString('content', true));
|
|
848
|
+
case 'search_files':
|
|
849
|
+
return invalidRequiredString('query');
|
|
850
|
+
case 'run_command':
|
|
851
|
+
return invalidRequiredString('command');
|
|
852
|
+
default:
|
|
853
|
+
return `The model requested an unknown tool: ${call.function.name}.`;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function isActiveRunStatus(status) {
|
|
857
|
+
return status === 'queued' || status === 'preparing' || status === 'running';
|
|
858
|
+
}
|
|
859
|
+
function summarizeToolCall(call) {
|
|
860
|
+
const args = call.function.arguments;
|
|
861
|
+
const metadata = {
|
|
862
|
+
name: call.function.name,
|
|
863
|
+
toolCallId: call.id,
|
|
864
|
+
};
|
|
865
|
+
const includeString = (key, value, maxLength = 1024) => {
|
|
866
|
+
if (typeof value !== 'string')
|
|
867
|
+
return;
|
|
868
|
+
metadata[key] = value.slice(0, maxLength);
|
|
869
|
+
if (value.length > maxLength)
|
|
870
|
+
metadata[`${key}Truncated`] = true;
|
|
871
|
+
};
|
|
872
|
+
switch (call.function.name) {
|
|
873
|
+
case 'write_file':
|
|
874
|
+
includeString('path', args.path);
|
|
875
|
+
if (typeof args.content === 'string') {
|
|
876
|
+
metadata.contentLength = Buffer.byteLength(args.content, 'utf8');
|
|
877
|
+
}
|
|
878
|
+
break;
|
|
879
|
+
case 'read_file':
|
|
880
|
+
case 'list_files':
|
|
881
|
+
includeString('path', args.path);
|
|
882
|
+
break;
|
|
883
|
+
case 'search_files':
|
|
884
|
+
includeString('path', args.path);
|
|
885
|
+
includeString('query', args.query, 500);
|
|
886
|
+
break;
|
|
887
|
+
case 'run_command':
|
|
888
|
+
includeString('command', args.command, 2000);
|
|
889
|
+
if (typeof args.timeout_ms === 'number') {
|
|
890
|
+
metadata.timeoutMs = args.timeout_ms;
|
|
891
|
+
}
|
|
892
|
+
break;
|
|
893
|
+
case 'start_preview':
|
|
894
|
+
includeString('command', args.command, 2000);
|
|
895
|
+
break;
|
|
896
|
+
default:
|
|
897
|
+
break;
|
|
898
|
+
}
|
|
899
|
+
return metadata;
|
|
900
|
+
}
|
|
901
|
+
function boundPersistedToolOutput(value) {
|
|
902
|
+
const maxLength = 20000;
|
|
903
|
+
if (value.length <= maxLength)
|
|
904
|
+
return value;
|
|
905
|
+
const half = Math.floor(maxLength / 2);
|
|
906
|
+
return `${value.slice(0, half)}\n... tool output truncated ...\n${value.slice(-half)}`;
|
|
907
|
+
}
|
|
908
|
+
function boundUtf8(value, maxBytes) {
|
|
909
|
+
if (Buffer.byteLength(value, 'utf8') <= maxBytes)
|
|
910
|
+
return value;
|
|
911
|
+
const suffix = '\n... response truncated ...';
|
|
912
|
+
const budget = Math.max(0, maxBytes - Buffer.byteLength(suffix, 'utf8'));
|
|
913
|
+
const prefix = Buffer.from(value, 'utf8')
|
|
914
|
+
.subarray(0, budget)
|
|
915
|
+
.toString('utf8')
|
|
916
|
+
.replace(/\uFFFD$/, '');
|
|
917
|
+
return `${prefix}${suffix}`;
|
|
918
|
+
}
|
|
919
|
+
function commandResult(result) {
|
|
920
|
+
const sections = [`exit_code: ${result.exitCode}`];
|
|
921
|
+
if (result.stdout)
|
|
922
|
+
sections.push(`stdout:\n${result.stdout}`);
|
|
923
|
+
if (result.stderr)
|
|
924
|
+
sections.push(`stderr:\n${result.stderr}`);
|
|
925
|
+
return {
|
|
926
|
+
content: sections.join('\n'),
|
|
927
|
+
metadata: {
|
|
928
|
+
exitCode: result.exitCode,
|
|
929
|
+
truncated: result.truncated,
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
export const workAgentService = new WorkAgentService();
|
|
934
|
+
export default workAgentService;
|
|
935
|
+
//# sourceMappingURL=workAgentService.js.map
|