@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.
- package/index.js +1 -2
- package/local-runtime/server/handlers/client-work-center.js +1 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +181 -173
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/engine.js +27 -3
- package/yeaft/work-center/bridge.js +5 -1
- package/yeaft/work-center/controller.js +6 -2
- package/yeaft/work-center/coordinator.js +51 -11
- package/yeaft/work-center/durable-model.js +618 -0
- package/yeaft/work-center/projection.js +8 -1
- package/yeaft/work-center/runner.js +101 -15
- package/yeaft/work-center/service.js +55 -0
- package/yeaft/work-center/store.js +774 -23
|
@@ -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
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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({
|
|
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(
|
|
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(
|
|
543
|
+
registry.register(wrapWorkItemTool(
|
|
544
|
+
tool, canonicalDir, canonicalAttachmentFiles, isRunActive, operationLifecycle,
|
|
545
|
+
));
|
|
530
546
|
}
|
|
531
547
|
for (const tool of runTools) {
|
|
532
|
-
registry.register(wrapWorkItemTool(
|
|
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
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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(
|