libre-webui 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +26 -10
  2. package/backend/dist/db.d.ts.map +1 -1
  3. package/backend/dist/db.js +63 -0
  4. package/backend/dist/db.js.map +1 -1
  5. package/backend/dist/index.d.ts.map +1 -1
  6. package/backend/dist/index.js +4 -4
  7. package/backend/dist/index.js.map +1 -1
  8. package/backend/dist/routes/chat.d.ts.map +1 -1
  9. package/backend/dist/routes/chat.js +93 -10
  10. package/backend/dist/routes/chat.js.map +1 -1
  11. package/backend/dist/routes/imageGen.d.ts.map +1 -1
  12. package/backend/dist/routes/imageGen.js +52 -29
  13. package/backend/dist/routes/imageGen.js.map +1 -1
  14. package/backend/dist/routes/plugins.d.ts.map +1 -1
  15. package/backend/dist/routes/plugins.js +218 -41
  16. package/backend/dist/routes/plugins.js.map +1 -1
  17. package/backend/dist/routes/preferences.d.ts.map +1 -1
  18. package/backend/dist/routes/preferences.js +5 -4
  19. package/backend/dist/routes/preferences.js.map +1 -1
  20. package/backend/dist/routes/tts.js +2 -2
  21. package/backend/dist/routes/tts.js.map +1 -1
  22. package/backend/dist/services/assistantCompletionService.d.ts +2 -1
  23. package/backend/dist/services/assistantCompletionService.d.ts.map +1 -1
  24. package/backend/dist/services/assistantCompletionService.js +3 -1
  25. package/backend/dist/services/assistantCompletionService.js.map +1 -1
  26. package/backend/dist/services/chatGenerationService.d.ts +5 -3
  27. package/backend/dist/services/chatGenerationService.d.ts.map +1 -1
  28. package/backend/dist/services/chatGenerationService.js +18 -6
  29. package/backend/dist/services/chatGenerationService.js.map +1 -1
  30. package/backend/dist/services/chatRequestService.d.ts +6 -4
  31. package/backend/dist/services/chatRequestService.d.ts.map +1 -1
  32. package/backend/dist/services/chatRequestService.js +10 -2
  33. package/backend/dist/services/chatRequestService.js.map +1 -1
  34. package/backend/dist/services/chatService.d.ts +2 -2
  35. package/backend/dist/services/chatService.d.ts.map +1 -1
  36. package/backend/dist/services/chatService.js +29 -17
  37. package/backend/dist/services/chatService.js.map +1 -1
  38. package/backend/dist/services/embeddingService.js +1 -1
  39. package/backend/dist/services/embeddingService.js.map +1 -1
  40. package/backend/dist/services/pluginActivationService.d.ts +11 -0
  41. package/backend/dist/services/pluginActivationService.d.ts.map +1 -0
  42. package/backend/dist/services/pluginActivationService.js +163 -0
  43. package/backend/dist/services/pluginActivationService.js.map +1 -0
  44. package/backend/dist/services/pluginCapabilityRegistryService.d.ts +1 -1
  45. package/backend/dist/services/pluginCapabilityRegistryService.d.ts.map +1 -1
  46. package/backend/dist/services/pluginCapabilityRegistryService.js +1 -1
  47. package/backend/dist/services/pluginCapabilityRegistryService.js.map +1 -1
  48. package/backend/dist/services/pluginCredentialsService.d.ts +16 -8
  49. package/backend/dist/services/pluginCredentialsService.d.ts.map +1 -1
  50. package/backend/dist/services/pluginCredentialsService.js +47 -14
  51. package/backend/dist/services/pluginCredentialsService.js.map +1 -1
  52. package/backend/dist/services/pluginEmbeddingService.d.ts +2 -2
  53. package/backend/dist/services/pluginEmbeddingService.d.ts.map +1 -1
  54. package/backend/dist/services/pluginEmbeddingService.js +33 -13
  55. package/backend/dist/services/pluginEmbeddingService.js.map +1 -1
  56. package/backend/dist/services/pluginImageGenerationService.d.ts +11 -7
  57. package/backend/dist/services/pluginImageGenerationService.d.ts.map +1 -1
  58. package/backend/dist/services/pluginImageGenerationService.js +265 -81
  59. package/backend/dist/services/pluginImageGenerationService.js.map +1 -1
  60. package/backend/dist/services/pluginService.d.ts +55 -25
  61. package/backend/dist/services/pluginService.d.ts.map +1 -1
  62. package/backend/dist/services/pluginService.js +663 -174
  63. package/backend/dist/services/pluginService.js.map +1 -1
  64. package/backend/dist/services/pluginTTSService.d.ts +4 -4
  65. package/backend/dist/services/pluginTTSService.d.ts.map +1 -1
  66. package/backend/dist/services/pluginTTSService.js +24 -28
  67. package/backend/dist/services/pluginTTSService.js.map +1 -1
  68. package/backend/dist/services/pluginVariablesService.d.ts +6 -1
  69. package/backend/dist/services/pluginVariablesService.d.ts.map +1 -1
  70. package/backend/dist/services/pluginVariablesService.js +59 -1
  71. package/backend/dist/services/pluginVariablesService.js.map +1 -1
  72. package/backend/dist/services/preferencesService.d.ts +2 -2
  73. package/backend/dist/services/preferencesService.d.ts.map +1 -1
  74. package/backend/dist/services/preferencesService.js +78 -10
  75. package/backend/dist/services/preferencesService.js.map +1 -1
  76. package/backend/dist/services/titleGenerationService.d.ts +6 -4
  77. package/backend/dist/services/titleGenerationService.d.ts.map +1 -1
  78. package/backend/dist/services/titleGenerationService.js +9 -5
  79. package/backend/dist/services/titleGenerationService.js.map +1 -1
  80. package/backend/dist/services/workAgentGuidance.d.ts +1 -0
  81. package/backend/dist/services/workAgentGuidance.d.ts.map +1 -1
  82. package/backend/dist/services/workAgentGuidance.js +2 -0
  83. package/backend/dist/services/workAgentGuidance.js.map +1 -1
  84. package/backend/dist/services/workAgentService.d.ts +5 -2
  85. package/backend/dist/services/workAgentService.d.ts.map +1 -1
  86. package/backend/dist/services/workAgentService.js +379 -53
  87. package/backend/dist/services/workAgentService.js.map +1 -1
  88. package/backend/dist/services/workModelProviderService.d.ts +8 -3
  89. package/backend/dist/services/workModelProviderService.d.ts.map +1 -1
  90. package/backend/dist/services/workModelProviderService.js +293 -33
  91. package/backend/dist/services/workModelProviderService.js.map +1 -1
  92. package/backend/dist/services/workTaskService.d.ts +2 -0
  93. package/backend/dist/services/workTaskService.d.ts.map +1 -1
  94. package/backend/dist/services/workTaskService.js +89 -14
  95. package/backend/dist/services/workTaskService.js.map +1 -1
  96. package/backend/dist/storage.d.ts.map +1 -1
  97. package/backend/dist/storage.js +9 -6
  98. package/backend/dist/storage.js.map +1 -1
  99. package/backend/dist/storageMappers.d.ts +3 -0
  100. package/backend/dist/storageMappers.d.ts.map +1 -1
  101. package/backend/dist/storageMappers.js +3 -0
  102. package/backend/dist/storageMappers.js.map +1 -1
  103. package/backend/dist/types/index.d.ts +54 -1
  104. package/backend/dist/types/index.d.ts.map +1 -1
  105. package/backend/dist/types/index.js.map +1 -1
  106. package/backend/dist/types/work.d.ts +1 -1
  107. package/backend/dist/types/work.d.ts.map +1 -1
  108. package/backend/dist/utils/chatContext.d.ts +13 -1
  109. package/backend/dist/utils/chatContext.d.ts.map +1 -1
  110. package/backend/dist/utils/chatContext.js +52 -0
  111. package/backend/dist/utils/chatContext.js.map +1 -1
  112. package/backend/dist/utils/chatProviderSelection.d.ts +15 -0
  113. package/backend/dist/utils/chatProviderSelection.d.ts.map +1 -0
  114. package/backend/dist/utils/chatProviderSelection.js +59 -0
  115. package/backend/dist/utils/chatProviderSelection.js.map +1 -0
  116. package/backend/dist/utils/imageGenerationValidation.d.ts +3 -0
  117. package/backend/dist/utils/imageGenerationValidation.d.ts.map +1 -0
  118. package/backend/dist/utils/imageGenerationValidation.js +55 -0
  119. package/backend/dist/utils/imageGenerationValidation.js.map +1 -0
  120. package/backend/dist/utils/ollamaLibrary.d.ts.map +1 -1
  121. package/backend/dist/utils/ollamaLibrary.js +81 -8
  122. package/backend/dist/utils/ollamaLibrary.js.map +1 -1
  123. package/backend/dist/utils/openAIResponsesAdapter.d.ts +88 -0
  124. package/backend/dist/utils/openAIResponsesAdapter.d.ts.map +1 -0
  125. package/backend/dist/utils/openAIResponsesAdapter.js +521 -0
  126. package/backend/dist/utils/openAIResponsesAdapter.js.map +1 -0
  127. package/backend/dist/utils/pluginChatAdapter.d.ts +7 -4
  128. package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -1
  129. package/backend/dist/utils/pluginChatAdapter.js +33 -5
  130. package/backend/dist/utils/pluginChatAdapter.js.map +1 -1
  131. package/backend/dist/utils/pluginChatContext.d.ts.map +1 -1
  132. package/backend/dist/utils/pluginChatContext.js +5 -1
  133. package/backend/dist/utils/pluginChatContext.js.map +1 -1
  134. package/backend/dist/utils/pluginConnectionVariables.d.ts +6 -0
  135. package/backend/dist/utils/pluginConnectionVariables.d.ts.map +1 -0
  136. package/backend/dist/utils/pluginConnectionVariables.js +52 -0
  137. package/backend/dist/utils/pluginConnectionVariables.js.map +1 -0
  138. package/backend/dist/utils/pluginDefinitionTrust.d.ts +9 -0
  139. package/backend/dist/utils/pluginDefinitionTrust.d.ts.map +1 -0
  140. package/backend/dist/utils/pluginDefinitionTrust.js +71 -0
  141. package/backend/dist/utils/pluginDefinitionTrust.js.map +1 -0
  142. package/backend/dist/utils/pluginStreamAdapter.d.ts +3 -0
  143. package/backend/dist/utils/pluginStreamAdapter.d.ts.map +1 -1
  144. package/backend/dist/utils/pluginStreamAdapter.js +410 -0
  145. package/backend/dist/utils/pluginStreamAdapter.js.map +1 -1
  146. package/backend/dist/utils/pluginStreaming.d.ts +7 -1
  147. package/backend/dist/utils/pluginStreaming.d.ts.map +1 -1
  148. package/backend/dist/utils/pluginStreaming.js +12 -1
  149. package/backend/dist/utils/pluginStreaming.js.map +1 -1
  150. package/backend/dist/utils/pluginValidation.d.ts +21 -2
  151. package/backend/dist/utils/pluginValidation.d.ts.map +1 -1
  152. package/backend/dist/utils/pluginValidation.js +190 -17
  153. package/backend/dist/utils/pluginValidation.js.map +1 -1
  154. package/backend/dist/utils/pluginVariableValidation.d.ts +7 -0
  155. package/backend/dist/utils/pluginVariableValidation.d.ts.map +1 -1
  156. package/backend/dist/utils/pluginVariableValidation.js +79 -4
  157. package/backend/dist/utils/pluginVariableValidation.js.map +1 -1
  158. package/backend/dist/websocketServer.d.ts.map +1 -1
  159. package/backend/dist/websocketServer.js +16 -3
  160. package/backend/dist/websocketServer.js.map +1 -1
  161. package/backend/package.json +1 -1
  162. package/frontend/dist/assets/index-CKV9Lnj8.js +5 -0
  163. package/frontend/dist/assets/index-Dme2Ozol.css +1 -0
  164. package/frontend/dist/index.html +4 -4
  165. package/frontend/dist/js/{ArtifactContainer-CXfmVx8T.js → ArtifactContainer-B0uVR8Z9.js} +1 -1
  166. package/frontend/dist/js/{ArtifactDemoPage-B1_AuMd2.js → ArtifactDemoPage-DBVPHFxk.js} +1 -1
  167. package/frontend/dist/js/{ArtifactSlideOutPanel-C0eTguNL.js → ArtifactSlideOutPanel-Cg5kAwv9.js} +1 -1
  168. package/frontend/dist/js/ChatPage-CPHlIZqU.js +16 -0
  169. package/frontend/dist/js/{FirstTimeSetup-DOoZUaRs.js → FirstTimeSetup-DksL5arK.js} +1 -1
  170. package/frontend/dist/js/GalleryPage-1mdUGo_N.js +1 -0
  171. package/frontend/dist/js/LibreClawPage-qGr1Jy8p.js +1 -0
  172. package/frontend/dist/js/{ModelManagerModals-DneqFQJC.js → ModelManagerModals-DltRprHU.js} +1 -1
  173. package/frontend/dist/js/ModelSelector-CUnwRmNU.js +1 -0
  174. package/frontend/dist/js/ModelsPage-DMYVg3LT.js +2 -0
  175. package/frontend/dist/js/PersonasPage-Ct-KKW-j.js +13 -0
  176. package/frontend/dist/js/{RichMessageContent-WQLdJ5jq.js → RichMessageContent-CGBAcUES.js} +1 -1
  177. package/frontend/dist/js/SettingsModal-BVj3gFCS.js +1 -0
  178. package/frontend/dist/js/UserManagementPage-D3tLGtTu.js +1 -0
  179. package/frontend/dist/js/WorkPage-DhTjy-kf.js +2 -0
  180. package/frontend/dist/js/api-YWoSobKv.js +19 -0
  181. package/frontend/dist/js/{ar-CCVY0-7X.js → ar-KceI0PfT.js} +1 -1
  182. package/frontend/dist/js/{bn-CoU5CSph.js → bn-DsKo2dzS.js} +1 -1
  183. package/frontend/dist/js/{cs-CR6pjIbG.js → cs-6UKhKj0A.js} +1 -1
  184. package/frontend/dist/js/{da-CkyYITRz.js → da-CJgZ3YuL.js} +1 -1
  185. package/frontend/dist/js/{de-DwNANzB-.js → de-BZAmOuCT.js} +1 -1
  186. package/frontend/dist/js/{es-TxygiaVQ.js → es-CD4u7no9.js} +1 -1
  187. package/frontend/dist/js/{estree-DPzMpjFm.js → estree-PDLT672k.js} +1 -1
  188. package/frontend/dist/js/{fr-DvEqVpZr.js → fr-Cou2jOS3.js} +1 -1
  189. package/frontend/dist/js/{hi-CyrWkyh7.js → hi-ZxkbNCyB.js} +1 -1
  190. package/frontend/dist/js/{html-R50jmBYq.js → html-CkhF9wDQ.js} +1 -1
  191. package/frontend/dist/js/{id-PJjIGHGM.js → id-DT9CqaWQ.js} +1 -1
  192. package/frontend/dist/js/{is-ChXcN1tI.js → is-BWSbAIoq.js} +1 -1
  193. package/frontend/dist/js/{it-DsgM8-MJ.js → it-CdNoTXm3.js} +1 -1
  194. package/frontend/dist/js/{ja-ivtt80N2.js → ja-CIuQg0xv.js} +1 -1
  195. package/frontend/dist/js/{ko-B8l_Ice1.js → ko-BWRUjDOH.js} +1 -1
  196. package/frontend/dist/js/{markdown-C08x2H-K.js → markdown-B1iNUbse.js} +1 -1
  197. package/frontend/dist/js/{ms-DoUBI6Qi.js → ms-gJstIj2a.js} +1 -1
  198. package/frontend/dist/js/{nl-DsX5Hj8m.js → nl-CydjVNUw.js} +1 -1
  199. package/frontend/dist/js/{pl-Db_NKL4x.js → pl-DPN9mAa5.js} +1 -1
  200. package/frontend/dist/js/{pt-CpzBhb5J.js → pt-MRXGqIjQ.js} +1 -1
  201. package/frontend/dist/js/{ru-DCXvJyXJ.js → ru-CvQ9GkJF.js} +1 -1
  202. package/frontend/dist/js/{standalone-DpMZUvjd.js → standalone-CCd6rkf-.js} +1 -1
  203. package/frontend/dist/js/{sv-DcVoybc7.js → sv-BxS2fvhU.js} +1 -1
  204. package/frontend/dist/js/{th-D0G7HyQn.js → th-DMxyn6e4.js} +1 -1
  205. package/frontend/dist/js/{tr-CMDOh4Aq.js → tr-BTPldUaA.js} +1 -1
  206. package/frontend/dist/js/{typescript-B6y_hxBm.js → typescript-DKJ1uM5L.js} +1 -1
  207. package/frontend/dist/js/ui-vendor-CAFEDSaO.js +177 -0
  208. package/frontend/dist/js/{uk-D2MMa6LP.js → uk-BGxZOKgp.js} +1 -1
  209. package/frontend/dist/js/useBaseQuery-MuQj7vA5.js +1 -0
  210. package/frontend/dist/js/useQuery-CYGPkxWR.js +1 -0
  211. package/frontend/dist/js/{vi-B8uHT8yh.js → vi-82JkwFsQ.js} +1 -1
  212. package/frontend/dist/js/{zh-CNqa_15K.js → zh-Bxxfwci3.js} +1 -1
  213. package/package.json +3 -2
  214. package/plugins/github.json +1 -2
  215. package/plugins/huggingface.json +30 -30
  216. package/plugins/mlx-lm.json +55 -0
  217. package/plugins/openai.json +53 -22
  218. package/frontend/dist/assets/index-Bv-twCuY.css +0 -1
  219. package/frontend/dist/assets/index-uCHBH_PM.js +0 -5
  220. package/frontend/dist/js/ChatPage-1bK0gDBy.js +0 -16
  221. package/frontend/dist/js/GalleryPage-6WwyH3kt.js +0 -1
  222. package/frontend/dist/js/LibreClawPage-ChHVfPLb.js +0 -1
  223. package/frontend/dist/js/ModelsPage-Dw6wdiMt.js +0 -2
  224. package/frontend/dist/js/PersonasPage-BxdI2e1-.js +0 -13
  225. package/frontend/dist/js/SettingsModal-C-cK6TNb.js +0 -1
  226. package/frontend/dist/js/UserManagementPage-DqjNXPkf.js +0 -1
  227. package/frontend/dist/js/WorkPage-MGR3odo6.js +0 -2
  228. package/frontend/dist/js/api-DD5qhhLf.js +0 -19
  229. package/frontend/dist/js/ui-vendor-DUqvZsfs.js +0 -177
  230. package/frontend/dist/js/useBaseQuery-Duml3DYp.js +0 -1
  231. package/frontend/dist/js/useQuery-C0lZwNoc.js +0 -1
@@ -14,13 +14,15 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import workModelProviderService from './workModelProviderService.js';
17
+ import workModelProviderService, { WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY, WORK_TOOL_ARGUMENTS_ERROR_MESSAGE, } from './workModelProviderService.js';
18
18
  import workEventService from './workEventService.js';
19
- import { buildWorkAgentSystemPrompt, buildWorkBudgetExhaustionPrompt, WORK_AGENT_SKILLS, workToolCallBudget, } from './workAgentGuidance.js';
19
+ import { buildWorkAgentSystemPrompt, buildWorkBudgetExhaustionPrompt, WORK_AGENT_SKILLS, WORK_WRITE_FILE_RECOMMENDED_CHARS, workToolCallBudget, } from './workAgentGuidance.js';
20
20
  import workRuntimeService from './workRuntimeService.js';
21
- import workTaskService, { WorkConflictError, WorkNotFoundError, } from './workTaskService.js';
21
+ import workTaskService, { WORK_MESSAGE_METADATA_MAX_BYTES, WorkConflictError, WorkNotFoundError, } from './workTaskService.js';
22
22
  import { createLogger } from '../utils/logger.js';
23
+ import { boundedOpenAIResponsesOutputItems, OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY, OPENAI_RESPONSES_STATE_DROPPED_METADATA_KEY, OPENAI_RESPONSES_STATE_SCOPE_METADATA_KEY, } from '../utils/openAIResponsesAdapter.js';
23
24
  const logger = createLogger('services:work-agent');
25
+ export const WORK_PROVIDER_STATE_METADATA_KEY = 'workProviderState';
24
26
  export const WORK_TOOL_SCHEMAS = [
25
27
  functionTool('list_files', 'List direct children of a workspace directory.', {
26
28
  path: stringProperty('Relative directory path. Defaults to workspace root.'),
@@ -28,7 +30,7 @@ export const WORK_TOOL_SCHEMAS = [
28
30
  functionTool('read_file', 'Read a UTF-8 text file from the persistent workspace.', { path: stringProperty('Relative file path.') }, ['path']),
29
31
  functionTool('write_file', 'Create or replace a UTF-8 text file in the persistent workspace.', {
30
32
  path: stringProperty('Relative file path.'),
31
- content: stringProperty('Complete file content.'),
33
+ 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
34
  }, ['path', 'content']),
33
35
  functionTool('search_files', 'Search text recursively in workspace files.', {
34
36
  query: stringProperty('Literal text to find.'),
@@ -245,7 +247,13 @@ export class WorkAgentService {
245
247
  workTaskService.updateTaskStatus(taskId, 'running');
246
248
  const roundLimit = workRuntimeService.limits.maxRounds;
247
249
  const toolCallLimit = workToolCallBudget(roundLimit);
248
- const messages = this.contextMessages(task, roundLimit);
250
+ const providerSelection = {
251
+ providerType: run.providerType,
252
+ providerId: run.providerId,
253
+ };
254
+ const providerRoutingFingerprint = workModelProviderService.getRoutingFingerprint(run.model, providerSelection, userId);
255
+ const providerStateScope = workModelProviderService.getResponsesStateScope(run.model, providerSelection, userId);
256
+ const messages = this.contextMessages(task, roundLimit, providerStateScope, run);
249
257
  let totalToolCalls = 0;
250
258
  let accumulatedInputTokens = 0;
251
259
  let accumulatedOutputTokens = 0;
@@ -267,18 +275,13 @@ export class WorkAgentService {
267
275
  const roundStartedAt = Date.now();
268
276
  let response;
269
277
  try {
278
+ this.assertStableProviderRouting(run, userId, providerRoutingFingerprint);
270
279
  response = await workModelProviderService.generateChatStreamResponse({
271
280
  model: run.model,
272
281
  messages,
273
282
  tools: WORK_TOOL_SCHEMAS,
274
- options: run.providerType === 'plugin'
275
- ? { num_predict: 4096 }
276
- : undefined,
277
283
  stream: true,
278
- }, {
279
- providerType: run.providerType,
280
- providerId: run.providerId,
281
- }, userId, {
284
+ }, providerSelection, userId, {
282
285
  onContent: delta => contentStream.push(delta),
283
286
  onReasoning: delta => reasoningStream.push(delta),
284
287
  onUsage: usage => {
@@ -303,9 +306,30 @@ export class WorkAgentService {
303
306
  streamedReasoningTotal = reasoningStream.currentTotal;
304
307
  }
305
308
  this.throwIfCancelled(runId, controller);
309
+ assertCompleteProviderResponse(response);
306
310
  accumulatedInputTokens += roundInputTokens;
307
311
  accumulatedOutputTokens += roundOutputTokens;
308
312
  const toolCalls = normalizeToolCalls(response);
313
+ const boundedProviderOutput = boundedOpenAIResponsesOutputItems(response.message.providerMetadata?.[OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY]);
314
+ const providerMetadata = response.message.providerMetadata;
315
+ const hasResponsesStateMetadata = providerMetadata?.[OPENAI_RESPONSES_STATE_DROPPED_METADATA_KEY] ===
316
+ true ||
317
+ providerMetadata?.[OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY] !==
318
+ undefined ||
319
+ providerMetadata?.[OPENAI_RESPONSES_STATE_SCOPE_METADATA_KEY] !==
320
+ undefined;
321
+ const providerStateMetadata = toPersistedWorkProviderState(run, providerMetadata);
322
+ if (toolCalls.length > 0 &&
323
+ (providerStateScope || hasResponsesStateMetadata) &&
324
+ (providerMetadata?.[OPENAI_RESPONSES_STATE_DROPPED_METADATA_KEY] ===
325
+ true ||
326
+ !providerStateScope ||
327
+ providerMetadata?.[OPENAI_RESPONSES_STATE_SCOPE_METADATA_KEY] !==
328
+ providerStateScope ||
329
+ !boundedProviderOutput.items ||
330
+ !providerStateMetadata)) {
331
+ throw new WorkAgentHttpError('The provider returned tool calls without bounded durable replay state.', 502, 'WORK_PROVIDER_INVALID_TOOL_CALLS');
332
+ }
309
333
  totalToolCalls += toolCalls.length;
310
334
  if (totalToolCalls > toolCallLimit) {
311
335
  budgetReason = 'tool-call';
@@ -319,7 +343,7 @@ export class WorkAgentService {
319
343
  if (toolCalls.length === 0) {
320
344
  const finalContent = assistantContent ||
321
345
  'The model completed without returning a text response.';
322
- workTaskService.addMessage(taskId, runId, 'assistant', 'message', finalContent);
346
+ workTaskService.addMessage(taskId, runId, 'assistant', 'message', finalContent, providerStateMetadata);
323
347
  // Keep completion hidden until the disposable container has either
324
348
  // stopped or been retained for a verified live preview. Consumers
325
349
  // can then refresh files immediately without racing teardown.
@@ -335,13 +359,26 @@ export class WorkAgentService {
335
359
  });
336
360
  return;
337
361
  }
362
+ const toolValidationErrors = new Map();
363
+ for (const call of toolCalls) {
364
+ const validationError = validateToolCallArguments(call);
365
+ if (validationError)
366
+ toolValidationErrors.set(call, validationError);
367
+ }
368
+ const hasInvalidToolArguments = toolValidationErrors.size > 0;
338
369
  messages.push({
339
370
  role: 'assistant',
340
371
  content: assistantContent,
341
372
  tool_calls: toolCalls,
373
+ ...(response.message.providerMetadata
374
+ ? { providerMetadata: response.message.providerMetadata }
375
+ : {}),
342
376
  });
343
377
  if (assistantContent) {
344
- workTaskService.addMessage(taskId, runId, 'assistant', 'message', assistantContent);
378
+ workTaskService.addMessage(taskId, runId, 'assistant', 'message', assistantContent, providerStateMetadata);
379
+ }
380
+ else if (providerStateMetadata) {
381
+ workTaskService.addMessage(taskId, runId, 'assistant', 'provider_state', '', providerStateMetadata);
345
382
  }
346
383
  for (const call of toolCalls) {
347
384
  this.throwIfCancelled(runId, controller);
@@ -368,6 +405,13 @@ export class WorkAgentService {
368
405
  toolName: call.function.name,
369
406
  };
370
407
  try {
408
+ const validationError = toolValidationErrors.get(call);
409
+ if (validationError) {
410
+ throw new WorkAgentHttpError(validationError, 400, 'WORK_INVALID_TOOL_ARGUMENTS');
411
+ }
412
+ if (hasInvalidToolArguments) {
413
+ 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');
414
+ }
371
415
  const result = await this.executeTool(task, call);
372
416
  toolOutput = result.content;
373
417
  toolMetadata = { ...toolMetadata, ...result.metadata };
@@ -397,6 +441,7 @@ export class WorkAgentService {
397
441
  role: 'tool',
398
442
  content: toolOutput,
399
443
  tool_name: call.function.name,
444
+ tool_call_id: call.id,
400
445
  });
401
446
  }
402
447
  }
@@ -415,6 +460,7 @@ export class WorkAgentService {
415
460
  let handoffResponse;
416
461
  try {
417
462
  try {
463
+ this.assertStableProviderRouting(run, userId, providerRoutingFingerprint);
418
464
  handoffResponse =
419
465
  await workModelProviderService.generateChatStreamResponse({
420
466
  model: run.model,
@@ -430,10 +476,7 @@ export class WorkAgentService {
430
476
  ? { num_predict: 2048 }
431
477
  : undefined,
432
478
  stream: true,
433
- }, {
434
- providerType: run.providerType,
435
- providerId: run.providerId,
436
- }, userId, {
479
+ }, providerSelection, userId, {
437
480
  onContent: delta => handoffContentStream.push(delta),
438
481
  onReasoning: delta => handoffReasoningStream.push(delta),
439
482
  onUsage: usage => {
@@ -451,6 +494,7 @@ export class WorkAgentService {
451
494
  });
452
495
  },
453
496
  }, controller.signal);
497
+ assertCompleteProviderResponse(handoffResponse);
454
498
  }
455
499
  catch (error) {
456
500
  if (controller.signal.aborted)
@@ -479,7 +523,12 @@ export class WorkAgentService {
479
523
  }
480
524
  const handoffContent = boundUtf8(handoffResponse.message?.content?.trim() ||
481
525
  `The run reached its ${budgetReason} budget. Start a follow-up run to continue in the same workspace.`, 100000);
482
- workTaskService.addMessage(taskId, runId, 'assistant', 'message', handoffContent, { budgetHandoff: true, budgetReason });
526
+ const handoffProviderStateMetadata = toPersistedWorkProviderState(run, handoffResponse.message.providerMetadata);
527
+ workTaskService.addMessage(taskId, runId, 'assistant', 'message', handoffContent, {
528
+ budgetHandoff: true,
529
+ budgetReason,
530
+ ...handoffProviderStateMetadata,
531
+ });
483
532
  await settleExecutionContainer();
484
533
  this.throwIfCancelled(runId, controller);
485
534
  if (!isActiveRunStatus(workTaskService.getRun(runId)?.status))
@@ -589,13 +638,17 @@ export class WorkAgentService {
589
638
  logger.error(`Could not stop idle Work container ${task.containerName}:`, error);
590
639
  }
591
640
  }
592
- contextMessages(task, roundLimit) {
593
- const persisted = workTaskService
594
- .getRecentConversationMessages(task.id, 30)
595
- .map(message => ({
596
- role: message.role,
597
- content: message.content,
598
- }));
641
+ assertStableProviderRouting(run, userId, expectedFingerprint) {
642
+ const currentFingerprint = workModelProviderService.getRoutingFingerprint(run.model, {
643
+ providerType: run.providerType,
644
+ providerId: run.providerId,
645
+ }, userId);
646
+ if (currentFingerprint !== expectedFingerprint) {
647
+ 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');
648
+ }
649
+ }
650
+ contextMessages(task, roundLimit, providerStateScope, provider = task) {
651
+ const persisted = restorePersistedWorkContext(workTaskService.getRecentModelContextMessages(task.id, 30), provider, providerStateScope);
599
652
  return [
600
653
  {
601
654
  role: 'system',
@@ -717,55 +770,298 @@ export class WorkAgentHttpError extends Error {
717
770
  this.code = code;
718
771
  }
719
772
  }
773
+ function toPersistedWorkProviderState(run, providerMetadata) {
774
+ const boundedOutput = boundedOpenAIResponsesOutputItems(providerMetadata?.[OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY]);
775
+ if (providerMetadata?.[OPENAI_RESPONSES_STATE_DROPPED_METADATA_KEY] === true ||
776
+ !boundedOutput.items ||
777
+ typeof providerMetadata?.[OPENAI_RESPONSES_STATE_SCOPE_METADATA_KEY] !==
778
+ 'string') {
779
+ return undefined;
780
+ }
781
+ const persisted = {
782
+ [WORK_PROVIDER_STATE_METADATA_KEY]: {
783
+ providerType: run.providerType,
784
+ ...(run.providerId ? { providerId: run.providerId } : {}),
785
+ model: run.model,
786
+ providerMetadata: {
787
+ ...providerMetadata,
788
+ [OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY]: boundedOutput.items,
789
+ },
790
+ },
791
+ };
792
+ try {
793
+ return Buffer.byteLength(JSON.stringify(persisted), 'utf8') <=
794
+ WORK_MESSAGE_METADATA_MAX_BYTES
795
+ ? persisted
796
+ : undefined;
797
+ }
798
+ catch {
799
+ return undefined;
800
+ }
801
+ }
802
+ export function restorePersistedWorkContext(messages, provider, expectedStateScope) {
803
+ const restored = [];
804
+ let pendingGroup;
805
+ const flushPendingGroup = () => {
806
+ if (!pendingGroup)
807
+ return;
808
+ if (pendingGroup.invalid) {
809
+ if (pendingGroup.fallback)
810
+ restored.push(pendingGroup.fallback);
811
+ pendingGroup = undefined;
812
+ return;
813
+ }
814
+ restored.push(pendingGroup.assistant, ...pendingGroup.results);
815
+ for (const call of pendingGroup.calls) {
816
+ if (pendingGroup.resultIds.has(call.id))
817
+ continue;
818
+ restored.push({
819
+ role: 'tool',
820
+ content: 'Tool execution was interrupted; outcome unknown. Inspect the workspace before retrying.',
821
+ tool_name: call.name,
822
+ tool_call_id: call.id,
823
+ });
824
+ }
825
+ pendingGroup = undefined;
826
+ };
827
+ for (const message of messages) {
828
+ if (message.kind === 'provider_state' ||
829
+ (message.role === 'assistant' && message.kind === 'message')) {
830
+ flushPendingGroup();
831
+ const providerMetadata = matchingWorkProviderMetadata(message, provider, expectedStateScope);
832
+ const responseCalls = providerMetadata
833
+ ? responseFunctionCalls(providerMetadata)
834
+ : [];
835
+ const rawFunctionCallCount = Array.isArray(providerMetadata?.[OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY])
836
+ ? providerMetadata[OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY].filter(item => objectValue(item)?.type === 'function_call').length
837
+ : 0;
838
+ const expectedCallIds = new Set(responseCalls.map(call => call.id));
839
+ if (message.kind === 'provider_state' && !providerMetadata) {
840
+ continue;
841
+ }
842
+ if (message.kind !== 'message' && message.kind !== 'provider_state') {
843
+ continue;
844
+ }
845
+ if (expectedCallIds.size !== responseCalls.length ||
846
+ rawFunctionCallCount !== responseCalls.length) {
847
+ if (message.kind === 'message') {
848
+ restored.push({ role: 'assistant', content: message.content });
849
+ }
850
+ continue;
851
+ }
852
+ const assistant = {
853
+ role: 'assistant',
854
+ content: message.content,
855
+ ...(responseCalls.length > 0
856
+ ? {
857
+ tool_calls: responseCalls.map(call => ({
858
+ id: call.id,
859
+ type: 'function',
860
+ function: {
861
+ name: call.name,
862
+ arguments: call.arguments,
863
+ },
864
+ })),
865
+ }
866
+ : {}),
867
+ ...(providerMetadata ? { providerMetadata } : {}),
868
+ };
869
+ if (responseCalls.length === 0) {
870
+ restored.push(assistant);
871
+ continue;
872
+ }
873
+ pendingGroup = {
874
+ assistant,
875
+ ...(message.kind === 'message'
876
+ ? {
877
+ fallback: {
878
+ role: 'assistant',
879
+ content: message.content,
880
+ },
881
+ }
882
+ : {}),
883
+ calls: responseCalls,
884
+ expectedCallIds,
885
+ results: [],
886
+ resultIds: new Set(),
887
+ invalid: false,
888
+ };
889
+ continue;
890
+ }
891
+ if (message.role === 'user' && message.kind === 'message') {
892
+ flushPendingGroup();
893
+ restored.push({ role: 'user', content: message.content });
894
+ continue;
895
+ }
896
+ if (message.role !== 'tool' || message.kind !== 'tool_result') {
897
+ continue;
898
+ }
899
+ const toolCallId = typeof message.metadata?.toolCallId === 'string'
900
+ ? message.metadata.toolCallId
901
+ : undefined;
902
+ if (!toolCallId || !pendingGroup)
903
+ continue;
904
+ if (!pendingGroup.expectedCallIds.has(toolCallId) ||
905
+ pendingGroup.resultIds.has(toolCallId)) {
906
+ pendingGroup.invalid = true;
907
+ continue;
908
+ }
909
+ const matchingCall = pendingGroup.calls.find(call => call.id === toolCallId);
910
+ if (!matchingCall) {
911
+ pendingGroup.invalid = true;
912
+ continue;
913
+ }
914
+ const persistedName = typeof message.metadata?.name === 'string'
915
+ ? message.metadata.name
916
+ : undefined;
917
+ pendingGroup.results.push({
918
+ role: 'tool',
919
+ content: message.content,
920
+ tool_name: persistedName || matchingCall.name,
921
+ tool_call_id: toolCallId,
922
+ });
923
+ pendingGroup.resultIds.add(toolCallId);
924
+ }
925
+ flushPendingGroup();
926
+ return restored;
927
+ }
928
+ function matchingWorkProviderMetadata(message, provider, expectedStateScope) {
929
+ const state = objectValue(message.metadata?.[WORK_PROVIDER_STATE_METADATA_KEY]);
930
+ const providerMetadata = objectValue(state?.providerMetadata);
931
+ const boundedOutput = boundedOpenAIResponsesOutputItems(providerMetadata?.[OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY]);
932
+ if (state?.providerType !== provider.providerType ||
933
+ (state.providerId || undefined) !== (provider.providerId || undefined) ||
934
+ state.model !== provider.model ||
935
+ !expectedStateScope ||
936
+ providerMetadata?.[OPENAI_RESPONSES_STATE_SCOPE_METADATA_KEY] !==
937
+ expectedStateScope ||
938
+ !boundedOutput.items) {
939
+ return undefined;
940
+ }
941
+ return {
942
+ ...providerMetadata,
943
+ [OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY]: boundedOutput.items,
944
+ };
945
+ }
946
+ function responseFunctionCalls(providerMetadata) {
947
+ const outputItems = providerMetadata[OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY];
948
+ if (!Array.isArray(outputItems))
949
+ return [];
950
+ return outputItems.flatMap(itemValue => {
951
+ const item = objectValue(itemValue);
952
+ if (item?.type !== 'function_call' ||
953
+ typeof item.name !== 'string' ||
954
+ item.name.length === 0 ||
955
+ typeof item.call_id !== 'string' ||
956
+ item.call_id.length === 0) {
957
+ return [];
958
+ }
959
+ return [
960
+ {
961
+ id: item.call_id,
962
+ name: item.name,
963
+ arguments: item.arguments ?? '',
964
+ },
965
+ ];
966
+ });
967
+ }
968
+ function objectValue(value) {
969
+ return value && typeof value === 'object' && !Array.isArray(value)
970
+ ? value
971
+ : undefined;
972
+ }
973
+ function assertCompleteProviderResponse(response) {
974
+ if (!response.done_reason?.startsWith('incomplete:'))
975
+ return;
976
+ const reason = response.done_reason.slice('incomplete:'.length) || 'unknown';
977
+ throw new WorkAgentHttpError(`The provider returned an incomplete response (${reason}). Retry with a larger output-token limit or a smaller request.`, 502, 'WORK_PROVIDER_INCOMPLETE_RESPONSE');
978
+ }
720
979
  export function normalizeToolCalls(response) {
721
980
  const raw = response.message?.tool_calls;
722
981
  if (!Array.isArray(raw))
723
982
  return [];
724
- return raw
725
- .flatMap((value, index) => {
726
- if (!value || typeof value !== 'object')
727
- return [];
983
+ const responsesMetadata = response.message?.providerMetadata;
984
+ const isResponsesCallBatch = typeof responsesMetadata?.[OPENAI_RESPONSES_STATE_SCOPE_METADATA_KEY] ===
985
+ 'string' ||
986
+ responsesMetadata?.[OPENAI_RESPONSES_STATE_DROPPED_METADATA_KEY] === true ||
987
+ Array.isArray(responsesMetadata?.[OPENAI_RESPONSES_OUTPUT_ITEMS_METADATA_KEY]);
988
+ const calls = [];
989
+ for (const [index, value] of raw.entries()) {
990
+ if (!value || typeof value !== 'object') {
991
+ if (isResponsesCallBatch) {
992
+ throw new WorkAgentHttpError('The provider returned a malformed tool call.', 502, 'WORK_PROVIDER_INVALID_TOOL_CALLS');
993
+ }
994
+ continue;
995
+ }
728
996
  const record = value;
729
997
  const fn = record.function && typeof record.function === 'object'
730
998
  ? record.function
731
999
  : undefined;
732
- if (!fn || typeof fn.name !== 'string')
733
- return [];
1000
+ if (!fn || typeof fn.name !== 'string' || !fn.name) {
1001
+ if (isResponsesCallBatch) {
1002
+ throw new WorkAgentHttpError('The provider returned a tool call without a function name.', 502, 'WORK_PROVIDER_INVALID_TOOL_CALLS');
1003
+ }
1004
+ continue;
1005
+ }
734
1006
  let args = {};
1007
+ let argumentError;
735
1008
  if (fn.arguments && typeof fn.arguments === 'object') {
736
1009
  args = fn.arguments;
737
1010
  }
738
1011
  else if (typeof fn.arguments === 'string') {
739
1012
  try {
740
1013
  const parsed = JSON.parse(fn.arguments);
741
- if (parsed && typeof parsed === 'object') {
1014
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
742
1015
  args = parsed;
743
1016
  }
1017
+ else if (fn.arguments.trim()) {
1018
+ argumentError = WORK_TOOL_ARGUMENTS_ERROR_MESSAGE;
1019
+ }
744
1020
  }
745
1021
  catch {
746
1022
  args = {};
1023
+ if (fn.arguments.trim()) {
1024
+ argumentError = WORK_TOOL_ARGUMENTS_ERROR_MESSAGE;
1025
+ }
747
1026
  }
748
1027
  }
749
- return [
750
- {
751
- id: typeof record.id === 'string'
752
- ? record.id
753
- : `tool-${Date.now()}-${index}`,
754
- ...(typeof record.thoughtSignature === 'string'
755
- ? { thoughtSignature: record.thoughtSignature }
756
- : {}),
757
- ...(record.providerMetadata &&
758
- typeof record.providerMetadata === 'object' &&
759
- !Array.isArray(record.providerMetadata)
760
- ? {
761
- providerMetadata: record.providerMetadata,
762
- }
763
- : {}),
764
- function: { name: fn.name, arguments: args },
765
- },
766
- ];
767
- })
768
- .slice(0, 16);
1028
+ const providerMetadata = record.providerMetadata &&
1029
+ typeof record.providerMetadata === 'object' &&
1030
+ !Array.isArray(record.providerMetadata)
1031
+ ? {
1032
+ ...record.providerMetadata,
1033
+ }
1034
+ : {};
1035
+ if (argumentError) {
1036
+ providerMetadata[WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY] = argumentError;
1037
+ }
1038
+ const explicitId = typeof record.id === 'string' ? record.id : undefined;
1039
+ if (explicitId !== undefined && !explicitId.trim()) {
1040
+ throw new WorkAgentHttpError('The provider returned a tool call with an empty ID.', 502, 'WORK_PROVIDER_INVALID_TOOL_CALLS');
1041
+ }
1042
+ if (isResponsesCallBatch && explicitId === undefined) {
1043
+ throw new WorkAgentHttpError('The Responses provider returned a tool call without an exact call_id.', 502, 'WORK_PROVIDER_INVALID_TOOL_CALLS');
1044
+ }
1045
+ calls.push({
1046
+ id: explicitId ?? `tool-${Date.now()}-${index}`,
1047
+ ...(typeof record.thoughtSignature === 'string'
1048
+ ? { thoughtSignature: record.thoughtSignature }
1049
+ : {}),
1050
+ ...(Object.keys(providerMetadata).length > 0 ? { providerMetadata } : {}),
1051
+ function: { name: fn.name, arguments: args },
1052
+ });
1053
+ }
1054
+ if (calls.length > 16) {
1055
+ throw new WorkAgentHttpError('The provider returned more than 16 tool calls in one response.', 502, 'WORK_PROVIDER_INVALID_TOOL_CALLS');
1056
+ }
1057
+ const callIds = new Set();
1058
+ for (const call of calls) {
1059
+ if (callIds.has(call.id)) {
1060
+ throw new WorkAgentHttpError(`The provider returned duplicate tool call ID "${call.id}".`, 502, 'WORK_PROVIDER_INVALID_TOOL_CALLS');
1061
+ }
1062
+ callIds.add(call.id);
1063
+ }
1064
+ return calls;
769
1065
  }
770
1066
  function functionTool(name, description, properties, required = []) {
771
1067
  return {
@@ -798,6 +1094,36 @@ function optionalInteger(value) {
798
1094
  ? value
799
1095
  : undefined;
800
1096
  }
1097
+ function validateToolCallArguments(call) {
1098
+ const providerArgumentError = call.providerMetadata?.[WORK_TOOL_ARGUMENTS_ERROR_METADATA_KEY];
1099
+ if (typeof providerArgumentError === 'string') {
1100
+ return providerArgumentError;
1101
+ }
1102
+ const args = call.function.arguments;
1103
+ const invalidRequiredString = (name, allowEmpty = false) => {
1104
+ const value = args[name];
1105
+ if (typeof value === 'string' && (allowEmpty || value.trim())) {
1106
+ return undefined;
1107
+ }
1108
+ 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.`;
1109
+ };
1110
+ switch (call.function.name) {
1111
+ case 'list_files':
1112
+ case 'start_preview':
1113
+ case 'stop_preview':
1114
+ return undefined;
1115
+ case 'read_file':
1116
+ return invalidRequiredString('path');
1117
+ case 'write_file':
1118
+ return (invalidRequiredString('path') || invalidRequiredString('content', true));
1119
+ case 'search_files':
1120
+ return invalidRequiredString('query');
1121
+ case 'run_command':
1122
+ return invalidRequiredString('command');
1123
+ default:
1124
+ return `The model requested an unknown tool: ${call.function.name}.`;
1125
+ }
1126
+ }
801
1127
  function isActiveRunStatus(status) {
802
1128
  return status === 'queued' || status === 'preparing' || status === 'running';
803
1129
  }