@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.
Binary file
@@ -16,6 +16,6 @@
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
19
- <script type="module" src="app.bundle.js?v=d010a7c7"></script>
19
+ <script type="module" src="app.bundle.js?v=b3f82a98"></script>
20
20
  </body>
21
21
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.255",
3
+ "version": "1.0.256",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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)) data = workCenter.projectBrowserDetail(data);
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
- : ['answer', 'guide_actions', 'replan'];
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, { text, recovery, addedAttachments = [], options }) {
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 ? COORDINATOR_RECOVERY_DECISION_ATTEMPTS : 1;
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({ vps, workItem, actions, collector, isRunActive }) {
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 => `${action.id}/${action.stageId} (${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.`,
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 failedAction = activeActions.find(action => action.id === recovery.actionId);
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
- || failedAction?.status !== 'failed'
2213
- || failedAction.generation !== recovery.actionGeneration
2214
- || failedAction.stageId !== recovery.stageId) {
2215
- throw new Error('WorkItem failure changed before Coordinator recovery started');
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
- if (!recovery && !String(text || '').trim() && projectedAttachments.length === 0) {
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 = recovery ? null : {
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, recovery ? 'coordinator.recovery_started' : 'coordinator.turn_started', {
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 failedAction = activeActions.find(action => (
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 === 'failed'
2326
+ && ['failed', 'waiting'].includes(action.status)
2321
2327
  ));
2322
- if (!failedAction
2328
+ if (!recoveryAction
2323
2329
  || !Array.isArray(decision.guidance)
2324
2330
  || decision.guidance.length !== 1
2325
- || decision.guidance[0]?.stageId !== failedAction.stageId) {
2326
- throw new Error('Coordinator recovery guidance must target only the failed Action identity');
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;