@yeaft/webchat-agent 1.0.294 → 1.0.296

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.
@@ -265,7 +265,7 @@ export function workItemToolPolicySnapshot(workDir, attachmentRefs = [], mcpTool
265
265
  };
266
266
  }
267
267
 
268
- function wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive) {
268
+ function wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive, operationLifecycle = null) {
269
269
  return {
270
270
  ...tool,
271
271
  async execute(input, ctx) {
@@ -273,12 +273,22 @@ function wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunAct
273
273
  const checkedInput = tool.name.startsWith('mcp__')
274
274
  ? input
275
275
  : assertToolInput(tool.name, input, canonicalDir, canonicalAttachmentFiles);
276
- const output = await tool.execute(checkedInput, {
277
- ...ctx,
278
- cwd: canonicalDir,
279
- workDir: canonicalDir,
280
- imageAllowlist: canonicalAttachmentFiles.map(file => file.root),
281
- });
276
+ const trackOperation = typeof operationLifecycle === 'function'
277
+ && tool.isReadOnly?.(checkedInput) !== true;
278
+ const operation = trackOperation ? operationLifecycle(tool.name, checkedInput) : null;
279
+ let output;
280
+ try {
281
+ output = await tool.execute(checkedInput, {
282
+ ...ctx,
283
+ cwd: canonicalDir,
284
+ workDir: canonicalDir,
285
+ imageAllowlist: canonicalAttachmentFiles.map(file => file.root),
286
+ });
287
+ } catch (error) {
288
+ operation?.complete('unknown', { error: String(error?.message || error) });
289
+ throw error;
290
+ }
291
+ operation?.complete('applied', { outputHash: hashMainlineSnapshot({ output: String(output || '') }) });
282
292
  if (!isRunActive()) throw new Error('Work Center Run lease was lost during tool execution');
283
293
  if (['FileRead', 'ViewImage'].includes(tool.name) && typeof output === 'string') {
284
294
  const withoutFilePaths = canonicalAttachmentFiles.reduce(
@@ -512,7 +522,9 @@ export function createSubmitWorkItemReplanTool({ vps, workItem, action, actions,
512
522
  });
513
523
  }
514
524
 
515
- export function createWorkItemToolRegistry({ workDir, attachmentFiles = [], isRunActive, mcpTools = [], runTools = [] }) {
525
+ export function createWorkItemToolRegistry({
526
+ workDir, attachmentFiles = [], isRunActive, mcpTools = [], runTools = [], operationLifecycle = null,
527
+ }) {
516
528
  const canonicalDir = canonicalWorkDir(path.resolve(workDir));
517
529
  const canonicalAttachmentFiles = attachmentFiles.map(file => ({
518
530
  ...file,
@@ -522,14 +534,20 @@ export function createWorkItemToolRegistry({ workDir, attachmentFiles = [], isRu
522
534
  const hasAttachments = canonicalAttachmentFiles.length > 0;
523
535
  for (const tool of allTools) {
524
536
  if (!WORK_ITEM_TOOL_ALLOWLIST.has(tool.name) || (hasAttachments && tool.name === 'Bash')) continue;
525
- registry.register(wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive));
537
+ registry.register(wrapWorkItemTool(
538
+ tool, canonicalDir, canonicalAttachmentFiles, isRunActive, operationLifecycle,
539
+ ));
526
540
  }
527
541
  for (const tool of mcpTools) {
528
542
  if (!tool?.name?.startsWith('mcp__')) continue;
529
- registry.register(wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive));
543
+ registry.register(wrapWorkItemTool(
544
+ tool, canonicalDir, canonicalAttachmentFiles, isRunActive, operationLifecycle,
545
+ ));
530
546
  }
531
547
  for (const tool of runTools) {
532
- registry.register(wrapWorkItemTool(tool, canonicalDir, canonicalAttachmentFiles, isRunActive));
548
+ registry.register(wrapWorkItemTool(
549
+ tool, canonicalDir, canonicalAttachmentFiles, isRunActive, operationLifecycle,
550
+ ));
533
551
  }
534
552
  return registry;
535
553
  }
@@ -1016,12 +1034,38 @@ export class WorkItemRunner {
1016
1034
  attachmentContext.files.map(file => file.ref),
1017
1035
  [...mcpToolNames, ...runToolNames],
1018
1036
  );
1037
+ let operationOrdinal = 0;
1038
+ const operationLifecycle = (toolName, input) => {
1039
+ operationOrdinal += 1;
1040
+ const idempotencyKey = `${run.id}:tool:${operationOrdinal}`;
1041
+ this.store.createOperation({
1042
+ workItemId: workItem.id,
1043
+ actionId: action.id,
1044
+ runId: run.id,
1045
+ operationType: toolName,
1046
+ idempotencyKey,
1047
+ replayPolicy: 'never_automatic',
1048
+ payload: { inputHash: hashMainlineSnapshot(input) },
1049
+ });
1050
+ const claimed = this.store.claimOperation(idempotencyKey, ownerBootId, run.leaseEpoch, false);
1051
+ if (!claimed) throw new Error(`Work Center could not claim Operation ${idempotencyKey}`);
1052
+ return {
1053
+ complete: (effectStatus, result) => {
1054
+ if (!this.store.completeOperation(
1055
+ idempotencyKey, ownerBootId, run.leaseEpoch, effectStatus, result,
1056
+ )) {
1057
+ throw new Error(`Work Center Operation ${idempotencyKey} lost its execution fence`);
1058
+ }
1059
+ },
1060
+ };
1061
+ };
1019
1062
  const toolRegistry = createWorkItemToolRegistry({
1020
1063
  workDir,
1021
1064
  attachmentFiles: attachmentContext.files,
1022
1065
  isRunActive,
1023
1066
  mcpTools: workspaceRuntime.mcpTools,
1024
1067
  runTools,
1068
+ operationLifecycle,
1025
1069
  });
1026
1070
  const config = {
1027
1071
  ...runtime.config,
@@ -1136,6 +1180,7 @@ export class WorkItemRunner {
1136
1180
  if (typeof registerInputWake === 'function') {
1137
1181
  registerInputWake(() => engine.wakeForPendingUserMessage?.());
1138
1182
  }
1183
+ const pendingEntriesById = new Map();
1139
1184
  const drainPendingUserMessages = () => {
1140
1185
  const pending = this.store.listPendingActionInputs?.(
1141
1186
  action.id, run.id, ownerBootId, run.leaseEpoch,
@@ -1149,13 +1194,49 @@ export class WorkItemRunner {
1149
1194
  const content = [item.text, attachmentLines.length > 0
1150
1195
  ? `Additional WorkItem attachments:\n${attachmentLines.join('\n')}` : '']
1151
1196
  .filter(Boolean).join('\n\n');
1152
- if (!content || !this.store.acknowledgeActionInput?.(
1153
- item.id, action.id, run.id, ownerBootId, run.leaseEpoch,
1154
- )) continue;
1155
- accepted.push({ content, preview: item.text || '[attachments]' });
1197
+ if (!content) continue;
1198
+ pendingEntriesById.set(String(item.id), item);
1199
+ accepted.push({
1200
+ content,
1201
+ preview: item.text || '[attachments]',
1202
+ durableInputId: String(item.id),
1203
+ });
1156
1204
  }
1157
1205
  return accepted;
1158
1206
  };
1207
+ const prepareProviderRequest = ({ entries, system, messages, model }) => {
1208
+ const durableEntries = entries
1209
+ .filter(entry => entry?.durableInputId)
1210
+ .map(entry => pendingEntriesById.get(String(entry.durableInputId)))
1211
+ .filter(Boolean);
1212
+ const requestBody = { model, system, messages };
1213
+ const turn = this.store.prepareEngineTurn?.(
1214
+ action.id, run.id, ownerBootId, run.leaseEpoch, durableEntries,
1215
+ { requestBody, dispatchCapability: 'unknown' },
1216
+ );
1217
+ if (!turn) throw new Error('Work Center could not persist the next provider turn');
1218
+ return turn;
1219
+ };
1220
+ const startProviderRequest = turn => {
1221
+ if (!turn) return;
1222
+ const claimed = this.store.claimEngineTurn?.(turn.id, ownerBootId, run.leaseEpoch);
1223
+ if (!claimed) throw new Error('Work Center provider turn lost its Run lease before dispatch');
1224
+ };
1225
+ const finishProviderRequest = (turn, result) => {
1226
+ if (!turn) return;
1227
+ if (!this.store.consumeEngineTurn?.(turn.id, ownerBootId, run.leaseEpoch, result)) {
1228
+ throw new Error('Work Center provider response lost its EngineTurn fence');
1229
+ }
1230
+ };
1231
+ const failProviderRequest = (turn, error) => {
1232
+ if (!turn) return;
1233
+ const failure = this.store.failEngineTurn?.(turn.id, ownerBootId, run.leaseEpoch, error);
1234
+ if (failure && failure.allowRetry === false) {
1235
+ error.retryable = false;
1236
+ error.workItemFailureKind = 'provider_dispatch_unknown';
1237
+ error.workItemFailureCode = 'engine_turn_dispatch_unknown';
1238
+ }
1239
+ };
1159
1240
  try {
1160
1241
  const prompt = v2Execution
1161
1242
  ? `${renderMainlineContextSnapshot(mainline.contextSnapshot)}${fixedPromptSuffix}`
@@ -1180,9 +1261,14 @@ export class WorkItemRunner {
1180
1261
  workDir,
1181
1262
  userAlreadyPersisted: true,
1182
1263
  drainPendingUserMessages,
1264
+ prepareProviderRequest,
1265
+ startProviderRequest,
1266
+ finishProviderRequest,
1267
+ failProviderRequest,
1183
1268
  closePendingUserInput: () => this.store.closeRunInput(
1184
1269
  run.id, ownerBootId, run.leaseEpoch,
1185
1270
  ),
1271
+
1186
1272
  collabToolPolicy: 'single-vp',
1187
1273
  })) {
1188
1274
  if (event?.type === 'loop') {
@@ -315,6 +315,47 @@ export class WorkCenterService {
315
315
  this.#emit({ type: 'work_item.deleted', workItem: { id, revision: deleted.revision } });
316
316
  return { id, deleted: true, cleanupWarning };
317
317
  }
318
+ case 'post_work_item_message': {
319
+ const clientMessageId = requiredString(payload.clientMessageId, 'clientMessageId');
320
+ const target = payload.target && typeof payload.target === 'object' ? payload.target : {};
321
+ if (target.kind === 'coordinator') {
322
+ return this.handle('work_item_message', { ...payload, clientMessageId }, requestContext);
323
+ }
324
+ if (target.kind === 'action') {
325
+ return this.handle('action_input', {
326
+ ...payload,
327
+ clientMessageId,
328
+ actionId: target.actionId,
329
+ generation: target.generation,
330
+ }, requestContext);
331
+ }
332
+ if (target.kind === 'request') {
333
+ const id = requiredString(payload.id, 'id');
334
+ const workItem = this.#requiredItem(id);
335
+ const action = this.#requiredAction(workItem, target.actionId);
336
+ if (action.status === 'failed' && !String(payload.text || '').trim()
337
+ && (!Array.isArray(payload.files) || payload.files.length === 0)) {
338
+ const detail = this.controller.retry(id, {
339
+ expected: {
340
+ actionId: action.id,
341
+ revision: payload.revision,
342
+ generation: target.generation,
343
+ statuses: ['failed'],
344
+ },
345
+ });
346
+ this.watcher.abortInvalidWorkItemRuns(id);
347
+ this.#emit({ type: 'action.retried', workItem: detail });
348
+ return detail;
349
+ }
350
+ return this.handle('action_input', {
351
+ ...payload,
352
+ clientMessageId,
353
+ actionId: target.actionId,
354
+ generation: target.generation,
355
+ }, requestContext);
356
+ }
357
+ throw new Error('WorkItem message target is invalid');
358
+ }
318
359
  case 'work_item_message': {
319
360
  if (!this.coordinator) throw new Error('Work Center Coordinator is unavailable');
320
361
  const id = requiredString(payload.id, 'id');
@@ -332,6 +373,7 @@ export class WorkCenterService {
332
373
  planRevision: payload.planRevision,
333
374
  ledgerRevision: payload.ledgerRevision,
334
375
  coordinatorRevision: payload.coordinatorRevision,
376
+ clientMessageId: typeof payload.clientMessageId === 'string' ? payload.clientMessageId : null,
335
377
  addedAttachments,
336
378
  attachments: [...(workItem.attachments || []), ...addedAttachments],
337
379
  }, {
@@ -387,6 +429,7 @@ export class WorkCenterService {
387
429
  actionId: typeof payload.actionId === 'string' ? payload.actionId : '',
388
430
  revision: payload.revision,
389
431
  generation,
432
+ clientMessageId: typeof payload.clientMessageId === 'string' ? payload.clientMessageId : null,
390
433
  addedAttachmentCount: addedAttachments.length,
391
434
  addedAttachments,
392
435
  attachments: [...(workItem.attachments || []), ...addedAttachments],
@@ -584,6 +627,18 @@ export class WorkCenterService {
584
627
  }
585
628
 
586
629
  start() {
630
+ for (const recoverable of this.store.getRecoverableCoordinatorTurns?.() || []) {
631
+ const started = this.store.resumeCoordinatorTurn(recoverable.workItemId, recoverable.turnId);
632
+ if (!started) continue;
633
+ const task = this.coordinator.resume(started, {
634
+ text: recoverable.text || '',
635
+ recovery: Boolean(recoverable.recovery),
636
+ addedAttachments: Array.isArray(recoverable.addedAttachments)
637
+ ? recoverable.addedAttachments : [],
638
+ onUpdate: (type, detail) => this.#emit({ type, workItem: detail }),
639
+ }).task;
640
+ task?.catch?.(() => {});
641
+ }
587
642
  this.#scanFailureRecoveries();
588
643
  if (!this.recoveryTimer) {
589
644
  this.recoveryTimer = setInterval(