@yeaft/webchat-agent 1.0.255 → 1.0.256
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +6 -5
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/work-center/bridge.js +3 -1
- package/yeaft/work-center/coordinator.js +17 -6
- package/yeaft/work-center/projection.js +5 -0
- package/yeaft/work-center/runner.js +16 -3
- package/yeaft/work-center/service.js +56 -0
- package/yeaft/work-center/store.js +27 -21
|
Binary file
|
package/package.json
CHANGED
|
@@ -220,7 +220,9 @@ export async function handleWorkCenterRequest(msg) {
|
|
|
220
220
|
: (BROWSER_ACTION_DEBUG_OPS.has(op) ? browserFilePayload(op, msg.payload) : (msg.payload || {}));
|
|
221
221
|
data = await workCenter.handle(op, payload);
|
|
222
222
|
}
|
|
223
|
-
if (BROWSER_DETAIL_OPS.has(op)
|
|
223
|
+
if (BROWSER_DETAIL_OPS.has(op) && data?.accepted !== true) {
|
|
224
|
+
data = workCenter.projectBrowserDetail(data);
|
|
225
|
+
}
|
|
224
226
|
send({
|
|
225
227
|
type: 'work_center_response',
|
|
226
228
|
requestId,
|
|
@@ -16,6 +16,7 @@ const COORDINATOR_MAX_REPLY_CHARS = 8_000;
|
|
|
16
16
|
const COORDINATOR_MAX_INSTRUCTION_CHARS = 8_000;
|
|
17
17
|
const COORDINATOR_MAX_OUTPUT_TOKENS = 8_192;
|
|
18
18
|
const COORDINATOR_MAX_SNAPSHOT_BYTES = 64 * 1024;
|
|
19
|
+
const COORDINATOR_DECISION_ATTEMPTS = 2;
|
|
19
20
|
const COORDINATOR_RECOVERY_DECISION_ATTEMPTS = 2;
|
|
20
21
|
const COORDINATOR_MAX_CONVERSATION_MESSAGES = 20;
|
|
21
22
|
const COORDINATOR_MAX_ACTIONS = 64;
|
|
@@ -302,7 +303,9 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
302
303
|
: {};
|
|
303
304
|
const allowedKinds = options.recovery === true
|
|
304
305
|
? ['guide_actions', 'replan', 'request_human']
|
|
305
|
-
:
|
|
306
|
+
: options.controlRequired === true
|
|
307
|
+
? ['guide_actions', 'replan']
|
|
308
|
+
: ['answer', 'guide_actions', 'replan'];
|
|
306
309
|
const kind = allowedKinds.includes(source.kind) ? source.kind : '';
|
|
307
310
|
if (!kind) throw new Error('Work Center Coordinator decision kind is invalid');
|
|
308
311
|
const reason = cleanText(source.reason, 2_000, 'decision reason');
|
|
@@ -494,12 +497,15 @@ export class WorkItemCoordinator {
|
|
|
494
497
|
}, {
|
|
495
498
|
attachments: input.attachments,
|
|
496
499
|
addedAttachments,
|
|
500
|
+
recovery: input.recovery,
|
|
501
|
+
requireWaitingRecovery: input.controlRequired === true,
|
|
497
502
|
});
|
|
498
503
|
if (!started) throw new Error(`WorkItem not found: ${id}`);
|
|
499
504
|
options.onUpdate?.('coordinator.turn_started', started.detail);
|
|
500
505
|
return this.#scheduleTurn(started, {
|
|
501
506
|
text: promptText,
|
|
502
507
|
recovery: false,
|
|
508
|
+
controlRequired: input.controlRequired === true,
|
|
503
509
|
addedAttachments,
|
|
504
510
|
options,
|
|
505
511
|
});
|
|
@@ -535,15 +541,17 @@ export class WorkItemCoordinator {
|
|
|
535
541
|
const text = `Action stage "${action.stageId}" failed. Decide the next safe control transition. `
|
|
536
542
|
+ 'Failure is not a terminal WorkItem state: guide or replan executable work whenever possible. '
|
|
537
543
|
+ 'Request human input only when the snapshot lacks information required for a safe decision.';
|
|
538
|
-
return this.#scheduleTurn(started, { text, recovery: true, options });
|
|
544
|
+
return this.#scheduleTurn(started, { text, recovery: true, controlRequired: false, options });
|
|
539
545
|
}
|
|
540
546
|
|
|
541
|
-
#scheduleTurn(started, {
|
|
547
|
+
#scheduleTurn(started, {
|
|
548
|
+
text, recovery, controlRequired = false, addedAttachments = [], options,
|
|
549
|
+
}) {
|
|
542
550
|
const abortController = new AbortController();
|
|
543
551
|
this.activeTurns.set(started.turnId, abortController);
|
|
544
552
|
const task = new Promise(resolve => setTimeout(resolve, 0))
|
|
545
553
|
.then(() => this.#executeTurn(started, {
|
|
546
|
-
text, recovery, addedAttachments, options, abortController,
|
|
554
|
+
text, recovery, controlRequired, addedAttachments, options, abortController,
|
|
547
555
|
}))
|
|
548
556
|
.finally(() => {
|
|
549
557
|
this.activeTurns.delete(started.turnId);
|
|
@@ -554,7 +562,7 @@ export class WorkItemCoordinator {
|
|
|
554
562
|
}
|
|
555
563
|
|
|
556
564
|
async #executeTurn(started, {
|
|
557
|
-
text, recovery, addedAttachments, options, abortController,
|
|
565
|
+
text, recovery, controlRequired, addedAttachments, options, abortController,
|
|
558
566
|
}) {
|
|
559
567
|
try {
|
|
560
568
|
let normalized = null;
|
|
@@ -605,7 +613,9 @@ export class WorkItemCoordinator {
|
|
|
605
613
|
} catch (error) {
|
|
606
614
|
throw coordinatorExecutionError(error, 'selection');
|
|
607
615
|
}
|
|
608
|
-
const maxAttempts = recovery
|
|
616
|
+
const maxAttempts = recovery
|
|
617
|
+
? COORDINATOR_RECOVERY_DECISION_ATTEMPTS
|
|
618
|
+
: COORDINATOR_DECISION_ATTEMPTS;
|
|
609
619
|
for (let index = 0; index < maxAttempts; index += 1) {
|
|
610
620
|
attemptCount = index + 1;
|
|
611
621
|
mutation = null;
|
|
@@ -645,6 +655,7 @@ export class WorkItemCoordinator {
|
|
|
645
655
|
}
|
|
646
656
|
normalized = normalizeCoordinatorResponse(result?.text, started.detail, {
|
|
647
657
|
recovery,
|
|
658
|
+
controlRequired,
|
|
648
659
|
recoveryActionId: started.fence.recovery?.actionId || null,
|
|
649
660
|
});
|
|
650
661
|
if (normalized.decision.kind === 'replan') {
|
|
@@ -901,6 +901,11 @@ export function projectWorkItemDetail(detail, options = {}) {
|
|
|
901
901
|
affectedActionIds: Array.isArray(message.decision.affectedActionIds)
|
|
902
902
|
? message.decision.affectedActionIds.map(id => String(id)).slice(0, 8) : [],
|
|
903
903
|
} : null,
|
|
904
|
+
recovery: message.recovery && typeof message.recovery === 'object' ? {
|
|
905
|
+
actionId: truncateUtf8(String(message.recovery.actionId || ''), 256),
|
|
906
|
+
actionGeneration: Math.max(0, count(message.recovery.actionGeneration)),
|
|
907
|
+
stageId: truncateUtf8(String(message.recovery.stageId || ''), 256),
|
|
908
|
+
} : null,
|
|
904
909
|
createdAt: count(message.createdAt),
|
|
905
910
|
updatedAt: count(message.updatedAt || message.createdAt),
|
|
906
911
|
})),
|
|
@@ -32,6 +32,7 @@ import { MCPManager } from '../mcp.js';
|
|
|
32
32
|
import { buildMcpFlattenedTools } from '../tools/mcp-tools.js';
|
|
33
33
|
import { recallWorkspaceSessionContext } from './workspace-context.js';
|
|
34
34
|
import { applyGeneratedPlan, BUILT_IN_ACTION_TYPES } from './workflow.js';
|
|
35
|
+
import { applyAdditivePlanProposal } from './plan-mutation.js';
|
|
35
36
|
import { normalizeContractPatch, validateCompletedResult } from './completion-contract.js';
|
|
36
37
|
import { normalizeEvidence } from './evidence.js';
|
|
37
38
|
import {
|
|
@@ -395,13 +396,18 @@ function plannedActionSchema(vpIds, { requireCandidates = true } = {}) {
|
|
|
395
396
|
} };
|
|
396
397
|
}
|
|
397
398
|
|
|
398
|
-
export function createProposeWorkItemActionsTool({
|
|
399
|
+
export function createProposeWorkItemActionsTool({
|
|
400
|
+
vps, workItem, actions, collector, isRunActive, currentAction = null,
|
|
401
|
+
}) {
|
|
399
402
|
const vpCatalog = planningVpCatalog(vps);
|
|
400
403
|
const vpIds = vpCatalog.map(vp => vp.id);
|
|
401
404
|
const existing = actions.filter(action => !['superseded', 'cancelled'].includes(action.status));
|
|
405
|
+
const currentIdentity = currentAction
|
|
406
|
+
? ` Current Action: stageId=${currentAction.stageId}; internalActionId=${currentAction.id}. Its graph references must use stageId.`
|
|
407
|
+
: '';
|
|
402
408
|
return defineTool({
|
|
403
409
|
name: 'ProposeWorkItemActions',
|
|
404
|
-
description: `Propose an additive change to the current WorkItem DAG. It is applied only if this Action completes and its Run lease plus basePlanRevision remain valid. Existing Actions: ${existing.map(action =>
|
|
410
|
+
description: `Propose an additive change to the current WorkItem DAG. It is applied only if this Action completes and its Run lease plus basePlanRevision remain valid. Use stable stageId values in dependsOnActionIds, changesRequestedActionId, and dependencyPatches[].addDependsOnActionIds. The only internal id field is dependencyPatches[].actionId, which must use the displayed internalActionId of an eligible ready attempt=0 target.${currentIdentity} Existing Actions: ${existing.map(action => `stageId=${action.stageId} (internalActionId=${action.id}, ${action.status}, attempt ${action.attempt})`).join('; ')}. Available VPs: ${vpCatalog.map(vp => `${vp.id} (${vp.role || vp.area || 'VP'})`).join('; ')}. Only add new Actions and optionally add dependencies to attempt=0 ready Actions. This tool validates the complete additive DAG immediately; if validation fails, correct the proposal in the same turn.`,
|
|
405
411
|
parameters: { type: 'object', additionalProperties: false,
|
|
406
412
|
required: ['summary', 'evidence', 'acceptanceChecks', 'proposalId', 'basePlanRevision', 'actions'],
|
|
407
413
|
properties: {
|
|
@@ -413,6 +419,13 @@ export function createProposeWorkItemActionsTool({ vps, workItem, actions, colle
|
|
|
413
419
|
async execute(input, ctx = {}) {
|
|
414
420
|
if (!isRunActive()) throw new Error('Work Center Run is no longer active');
|
|
415
421
|
if (collector.value) throw new Error('A WorkItem plan mutation was already submitted for this Run');
|
|
422
|
+
applyAdditivePlanProposal({
|
|
423
|
+
workItem,
|
|
424
|
+
actions,
|
|
425
|
+
proposal: input,
|
|
426
|
+
availableVpIds: vpIds,
|
|
427
|
+
});
|
|
428
|
+
if (!isRunActive()) throw new Error('Work Center Run is no longer active');
|
|
416
429
|
collector.value = { kind: 'expand', input: structuredClone(input) };
|
|
417
430
|
ctx.requestEndTurn?.({ kind: 'work_item_actions_proposed', proposalId: input.proposalId });
|
|
418
431
|
return JSON.stringify({ submitted: true, proposalId: input.proposalId, actionCount: input.actions.length });
|
|
@@ -981,7 +994,7 @@ export class WorkItemRunner {
|
|
|
981
994
|
runTools.push(createProposeWorkItemActionsTool({
|
|
982
995
|
vps: this.registry.listVps(), workItem,
|
|
983
996
|
actions: this.store.getWorkItemDetail(workItem.id).actions,
|
|
984
|
-
collector: mutationCollector, isRunActive,
|
|
997
|
+
collector: mutationCollector, isRunActive, currentAction: executionAction,
|
|
985
998
|
}));
|
|
986
999
|
runTools.push(createRequestWorkItemReplanTool({ workItem, collector: mutationCollector, isRunActive }));
|
|
987
1000
|
}
|
|
@@ -60,6 +60,19 @@ function parseBoardCursor(value) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function coordinatorHumanRequest(detail, action, generation) {
|
|
64
|
+
if (detail?.status !== 'waiting'
|
|
65
|
+
|| action?.status !== 'waiting'
|
|
66
|
+
|| Number(action.generation) !== Number(generation)) return null;
|
|
67
|
+
return [...(Array.isArray(detail.messages) ? detail.messages : [])].reverse().find(message => (
|
|
68
|
+
message?.role === 'assistant'
|
|
69
|
+
&& message.status === 'completed'
|
|
70
|
+
&& message.decision?.kind === 'request_human'
|
|
71
|
+
&& message.recovery?.actionId === action.id
|
|
72
|
+
&& message.recovery?.actionGeneration === action.generation
|
|
73
|
+
)) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
function listBoardItems(store, payload) {
|
|
64
77
|
const limit = Math.min(Math.max(Number(payload.limit) || 100, 1), 200);
|
|
65
78
|
const cursor = parseBoardCursor(payload.cursor);
|
|
@@ -367,6 +380,49 @@ export class WorkCenterService {
|
|
|
367
380
|
throw new Error('generation must be a positive integer');
|
|
368
381
|
}
|
|
369
382
|
const workItem = this.#requiredItem(id);
|
|
383
|
+
const targetAction = this.#requiredAction(workItem, payload.actionId);
|
|
384
|
+
const humanRequest = coordinatorHumanRequest(workItem, targetAction, generation);
|
|
385
|
+
if (humanRequest) {
|
|
386
|
+
let addedAttachments = [];
|
|
387
|
+
let turn;
|
|
388
|
+
try {
|
|
389
|
+
addedAttachments = appendWorkItemAttachments(workItem.attachments, payload.files, {
|
|
390
|
+
root: this.attachmentRoot,
|
|
391
|
+
workItemId: id,
|
|
392
|
+
});
|
|
393
|
+
turn = this.coordinator.message(id, {
|
|
394
|
+
text: typeof payload.text === 'string' ? payload.text : '',
|
|
395
|
+
revision: payload.revision,
|
|
396
|
+
planRevision: workItem.planRevision,
|
|
397
|
+
ledgerRevision: workItem.ledgerRevision,
|
|
398
|
+
coordinatorRevision: workItem.coordinatorRevision,
|
|
399
|
+
controlRequired: true,
|
|
400
|
+
recovery: { ...humanRequest.recovery },
|
|
401
|
+
addedAttachments,
|
|
402
|
+
attachments: [...(workItem.attachments || []), ...addedAttachments],
|
|
403
|
+
}, {
|
|
404
|
+
onUpdate: (type, nextWorkItem) => {
|
|
405
|
+
this.watcher.abortInvalidWorkItemRuns(id);
|
|
406
|
+
this.#emit({ type, workItem: nextWorkItem });
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
} catch (error) {
|
|
410
|
+
try {
|
|
411
|
+
if ((workItem.attachments || []).length === 0 && addedAttachments.length > 0) {
|
|
412
|
+
removeWorkItemAttachments(this.attachmentRoot, id);
|
|
413
|
+
} else {
|
|
414
|
+
removeWorkItemAttachmentFiles(this.attachmentRoot, id, addedAttachments);
|
|
415
|
+
}
|
|
416
|
+
} catch {}
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
turn.task.catch(() => {});
|
|
420
|
+
return {
|
|
421
|
+
accepted: true,
|
|
422
|
+
routedTo: 'coordinator',
|
|
423
|
+
turnId: turn.detail.messages?.at(-1)?.turnId || null,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
370
426
|
let addedAttachments = [];
|
|
371
427
|
let detail;
|
|
372
428
|
try {
|
|
@@ -2207,27 +2207,33 @@ export class WorkItemStore {
|
|
|
2207
2207
|
let recovery = options.recovery && typeof options.recovery === 'object'
|
|
2208
2208
|
? { ...options.recovery } : null;
|
|
2209
2209
|
if (recovery) {
|
|
2210
|
-
const
|
|
2210
|
+
const recoveryAction = activeActions.find(action => action.id === recovery.actionId);
|
|
2211
|
+
const requiredStatus = options.requireWaitingRecovery === true ? 'waiting' : 'failed';
|
|
2211
2212
|
if (['done', 'cancelled'].includes(workItem.status)
|
|
2212
|
-
||
|
|
2213
|
-
||
|
|
2214
|
-
||
|
|
2215
|
-
throw new Error(
|
|
2213
|
+
|| recoveryAction?.status !== requiredStatus
|
|
2214
|
+
|| recoveryAction.generation !== recovery.actionGeneration
|
|
2215
|
+
|| recoveryAction.stageId !== recovery.stageId) {
|
|
2216
|
+
throw new Error(options.requireWaitingRecovery === true
|
|
2217
|
+
? 'WorkItem human input target changed before the Coordinator turn started'
|
|
2218
|
+
: 'WorkItem failure changed before Coordinator recovery started');
|
|
2219
|
+
}
|
|
2220
|
+
if (options.requireWaitingRecovery !== true) {
|
|
2221
|
+
const priorAttempts = Number(this.db.prepare(`SELECT COUNT(*) AS count FROM events
|
|
2222
|
+
WHERE work_item_id = ? AND type = 'coordinator.recovery_started'
|
|
2223
|
+
AND action_id = ? AND action_generation = ?`).get(
|
|
2224
|
+
id,
|
|
2225
|
+
recoveryAction.id,
|
|
2226
|
+
recoveryAction.generation,
|
|
2227
|
+
)?.count) || 0;
|
|
2228
|
+
recovery = { ...recovery, attempt: priorAttempts + 1 };
|
|
2216
2229
|
}
|
|
2217
|
-
const priorAttempts = Number(this.db.prepare(`SELECT COUNT(*) AS count FROM events
|
|
2218
|
-
WHERE work_item_id = ? AND type = 'coordinator.recovery_started'
|
|
2219
|
-
AND action_id = ? AND action_generation = ?`).get(
|
|
2220
|
-
id,
|
|
2221
|
-
failedAction.id,
|
|
2222
|
-
failedAction.generation,
|
|
2223
|
-
)?.count) || 0;
|
|
2224
|
-
recovery = { ...recovery, attempt: priorAttempts + 1 };
|
|
2225
2230
|
}
|
|
2226
|
-
|
|
2231
|
+
const automaticRecovery = recovery && options.requireWaitingRecovery !== true;
|
|
2232
|
+
if (!automaticRecovery && !String(text || '').trim() && projectedAttachments.length === 0) {
|
|
2227
2233
|
throw new Error('WorkItem Coordinator message or attachments are required');
|
|
2228
2234
|
}
|
|
2229
2235
|
const turnId = randomUUID();
|
|
2230
|
-
const userMessage =
|
|
2236
|
+
const userMessage = automaticRecovery ? null : {
|
|
2231
2237
|
id: randomUUID(), turnId, role: 'user', text, attachments: projectedAttachments,
|
|
2232
2238
|
status: 'completed', createdAt: now,
|
|
2233
2239
|
};
|
|
@@ -2250,7 +2256,7 @@ export class WorkItemStore {
|
|
|
2250
2256
|
id, workItem.coordinatorRevision, workItem.revision, workItem.planRevision, workItem.ledgerRevision,
|
|
2251
2257
|
);
|
|
2252
2258
|
if (Number(changed.changes) !== 1) throw new Error('Coordinator turn lost its revision fence');
|
|
2253
|
-
this.appendEvent(id,
|
|
2259
|
+
this.appendEvent(id, automaticRecovery ? 'coordinator.recovery_started' : 'coordinator.turn_started', {
|
|
2254
2260
|
turnId,
|
|
2255
2261
|
status: 'thinking',
|
|
2256
2262
|
coordinatorRevision,
|
|
@@ -2313,17 +2319,17 @@ export class WorkItemStore {
|
|
|
2313
2319
|
throw new Error('Coordinator replan requires an AI-planned Action graph');
|
|
2314
2320
|
}
|
|
2315
2321
|
if (recovery && decision.kind === 'guide_actions') {
|
|
2316
|
-
const
|
|
2322
|
+
const recoveryAction = activeActions.find(action => (
|
|
2317
2323
|
action.id === recovery.actionId
|
|
2318
2324
|
&& action.generation === recovery.actionGeneration
|
|
2319
2325
|
&& action.stageId === recovery.stageId
|
|
2320
|
-
&& action.status
|
|
2326
|
+
&& ['failed', 'waiting'].includes(action.status)
|
|
2321
2327
|
));
|
|
2322
|
-
if (!
|
|
2328
|
+
if (!recoveryAction
|
|
2323
2329
|
|| !Array.isArray(decision.guidance)
|
|
2324
2330
|
|| decision.guidance.length !== 1
|
|
2325
|
-
|| decision.guidance[0]?.stageId !==
|
|
2326
|
-
throw new Error('Coordinator recovery guidance must target only the
|
|
2331
|
+
|| decision.guidance[0]?.stageId !== recoveryAction.stageId) {
|
|
2332
|
+
throw new Error('Coordinator recovery guidance must target only the fenced Action identity');
|
|
2327
2333
|
}
|
|
2328
2334
|
}
|
|
2329
2335
|
let nextWorkItem = workItem;
|