@yeaft/webchat-agent 1.0.266 → 1.0.268

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.
@@ -24,7 +24,16 @@ const COORDINATOR_MAX_WORK_ITEM_BYTES = 14 * 1024;
24
24
  const COORDINATOR_MAX_ACTIONS_BYTES = 34 * 1024;
25
25
  const COORDINATOR_MAX_CONVERSATION_BYTES = 10 * 1024;
26
26
  const COORDINATOR_MAX_STAGE_ID_BYTES = 256;
27
- const COORDINATOR_TEMPORARY_ERROR = 'Work Center Coordinator is temporarily unavailable; automatic recovery will retry';
27
+
28
+ function coordinatorLanguage(value) {
29
+ return String(value || '').toLowerCase().startsWith('zh') ? 'zh' : 'en';
30
+ }
31
+
32
+ function coordinatorTemporaryError(language) {
33
+ return coordinatorLanguage(language) === 'zh'
34
+ ? 'Work Center Coordinator 暂时不可用;系统会自动重试恢复'
35
+ : 'Work Center Coordinator is temporarily unavailable; automatic recovery will retry';
36
+ }
28
37
 
29
38
  function truncateUtf8(value, maxBytes) {
30
39
  const bytes = Buffer.from(String(value || ''), 'utf8');
@@ -172,6 +181,15 @@ Decision rules:
172
181
  - Stage ids in the snapshot may be bounded aliases. Echo them exactly; the runtime resolves them to durable identities.
173
182
  - Never return destructive cancellation. Tell the user to use the explicit cancel control instead.`;
174
183
 
184
+ function coordinatorSystemPrompt(language) {
185
+ const userLanguage = coordinatorLanguage(language) === 'zh'
186
+ ? 'Simplified Chinese (zh-CN)'
187
+ : 'English';
188
+ return `${COORDINATOR_SYSTEM_PROMPT}
189
+
190
+ User-facing language: ${userLanguage}. Write reply and question in that language. Keep JSON property names and decision enum values in English. Never expose deterministic validator errors as the user-facing reply; explain the underlying issue plainly.`;
191
+ }
192
+
175
193
  function parseJsonObject(value) {
176
194
  const source = String(value || '').trim();
177
195
  if (!source) throw new Error('Work Center Coordinator returned an empty response');
@@ -196,25 +214,40 @@ function cleanText(value, limit, name) {
196
214
  return text;
197
215
  }
198
216
 
199
- function permanentCoordinatorDiagnostic(cause, phase) {
217
+ function permanentCoordinatorDiagnostic(cause, phase, language) {
218
+ const zh = coordinatorLanguage(language) === 'zh';
200
219
  if (cause instanceof LLMAuthError) {
201
- return 'Work Center Coordinator authentication failed. Update the configured provider credentials before retrying this Action.';
220
+ return zh
221
+ ? 'Work Center Coordinator 认证失败。请更新 Provider 凭据后再重试。'
222
+ : 'Work Center Coordinator authentication failed. Update the configured provider credentials before retrying this Action.';
202
223
  }
203
224
  if (cause instanceof LLMContextError) {
204
- return 'Work Center Coordinator exceeded the model context limit. Reduce the WorkItem context or select a model with a larger context window before retrying this Action.';
225
+ return zh
226
+ ? 'Work Center Coordinator 超过模型上下文限制。请减少 WorkItem 上下文,或改用上下文窗口更大的模型后重试。'
227
+ : 'Work Center Coordinator exceeded the model context limit. Reduce the WorkItem context or select a model with a larger context window before retrying this Action.';
205
228
  }
206
229
  const detail = sanitizeDiagnosticText(cause?.message || String(cause || ''), 2_000);
207
- const label = phase === 'runtime'
208
- ? 'runtime could not be loaded'
209
- : phase === 'policy'
210
- ? 'settings could not be loaded'
211
- : phase === 'selection'
212
- ? 'executor or model selection failed'
213
- : 'provider request failed';
214
- return `Work Center Coordinator ${label}${detail ? `: ${detail}` : '.'}`;
230
+ const label = zh
231
+ ? (phase === 'runtime'
232
+ ? '无法加载运行时'
233
+ : phase === 'policy'
234
+ ? '无法加载设置'
235
+ : phase === 'selection'
236
+ ? '执行者或模型选择失败'
237
+ : 'Provider 请求失败')
238
+ : (phase === 'runtime'
239
+ ? 'runtime could not be loaded'
240
+ : phase === 'policy'
241
+ ? 'settings could not be loaded'
242
+ : phase === 'selection'
243
+ ? 'executor or model selection failed'
244
+ : 'provider request failed');
245
+ return zh
246
+ ? `Work Center Coordinator ${label}${detail ? `:${detail}` : '。'}`
247
+ : `Work Center Coordinator ${label}${detail ? `: ${detail}` : '.'}`;
215
248
  }
216
249
 
217
- function coordinatorExecutionError(cause, phase) {
250
+ function coordinatorExecutionError(cause, phase, language) {
218
251
  if (cause?.coordinatorClassified === true) return cause;
219
252
  const explicitlyPermanent = cause?.retryable === false;
220
253
  const retryable = !explicitlyPermanent && (
@@ -223,8 +256,8 @@ function coordinatorExecutionError(cause, phase) {
223
256
  || (['runtime', 'policy'].includes(phase) && cause?.retryable === true)
224
257
  );
225
258
  const error = new Error(retryable
226
- ? COORDINATOR_TEMPORARY_ERROR
227
- : permanentCoordinatorDiagnostic(cause, phase));
259
+ ? coordinatorTemporaryError(language)
260
+ : permanentCoordinatorDiagnostic(cause, phase, language));
228
261
  error.coordinatorClassified = true;
229
262
  error.coordinatorRetryable = retryable;
230
263
  error.coordinatorPhase = phase;
@@ -232,19 +265,34 @@ function coordinatorExecutionError(cause, phase) {
232
265
  return error;
233
266
  }
234
267
 
235
- function permanentRecoveryDecision(error) {
236
- const diagnostic = String(error?.message || 'Work Center Coordinator cannot recover this Action automatically');
237
- const resolution = error?.cause instanceof LLMAuthError
238
- ? 'Update the provider credentials, then tell Yeaft to retry or replan the failed Action.'
239
- : error?.cause instanceof LLMContextError
240
- ? 'Reduce the WorkItem context or choose a model with a larger context window, then tell Yeaft to retry or replan the failed Action.'
241
- : error?.coordinatorPhase === 'selection'
242
- ? 'Configure an available VP and model, then tell Yeaft to retry or replan the failed Action.'
243
- : error?.coordinatorPhase === 'policy'
244
- ? 'Correct the Work Center settings, then tell Yeaft to retry or replan the failed Action.'
245
- : 'Correct the Coordinator runtime or provider configuration, then tell Yeaft to retry or replan the failed Action.';
268
+ function permanentRecoveryDecision(error, language) {
269
+ const zh = coordinatorLanguage(language) === 'zh';
270
+ const diagnostic = String(error?.message || (zh
271
+ ? 'Work Center Coordinator 无法自动恢复这个 Action'
272
+ : 'Work Center Coordinator cannot recover this Action automatically'));
273
+ const resolution = zh
274
+ ? (error?.cause instanceof LLMAuthError
275
+ ? '请更新 Provider 凭据,然后让 Yeaft 重试或重新规划失败的 Action'
276
+ : error?.cause instanceof LLMContextError
277
+ ? '请减少 WorkItem 上下文,或选择上下文窗口更大的模型,然后让 Yeaft 重试或重新规划失败的 Action'
278
+ : error?.coordinatorPhase === 'selection'
279
+ ? '请配置可用的 VP 和模型,然后让 Yeaft 重试或重新规划失败的 Action。'
280
+ : error?.coordinatorPhase === 'policy'
281
+ ? '请修正 Work Center 设置,然后让 Yeaft 重试或重新规划失败的 Action。'
282
+ : '请修正 Coordinator 运行时或 Provider 配置,然后让 Yeaft 重试或重新规划失败的 Action。')
283
+ : (error?.cause instanceof LLMAuthError
284
+ ? 'Update the provider credentials, then tell Yeaft to retry or replan the failed Action.'
285
+ : error?.cause instanceof LLMContextError
286
+ ? 'Reduce the WorkItem context or choose a model with a larger context window, then tell Yeaft to retry or replan the failed Action.'
287
+ : error?.coordinatorPhase === 'selection'
288
+ ? 'Configure an available VP and model, then tell Yeaft to retry or replan the failed Action.'
289
+ : error?.coordinatorPhase === 'policy'
290
+ ? 'Correct the Work Center settings, then tell Yeaft to retry or replan the failed Action.'
291
+ : 'Correct the Coordinator runtime or provider configuration, then tell Yeaft to retry or replan the failed Action.');
246
292
  return {
247
- reply: `${diagnostic} Automatic recovery stopped to avoid repeated attempts.`,
293
+ reply: zh
294
+ ? `${diagnostic} 为避免重复尝试,自动恢复已停止。`
295
+ : `${diagnostic} Automatic recovery stopped to avoid repeated attempts.`,
248
296
  decision: {
249
297
  kind: 'request_human',
250
298
  reason: `Automatic recovery stopped after a non-retryable ${error?.coordinatorPhase || 'Coordinator'} error`,
@@ -256,6 +304,18 @@ function permanentRecoveryDecision(error) {
256
304
  };
257
305
  }
258
306
 
307
+ function coordinatorDecisionError(error, language) {
308
+ const diagnostic = sanitizeDiagnosticText(error?.message || String(error || ''), 2_000);
309
+ const wrapped = new Error(coordinatorLanguage(language) === 'zh'
310
+ ? 'Work Center Coordinator 未能生成有效回复。你的消息已经保留,请重试。'
311
+ : 'Work Center Coordinator could not produce a valid response. Your message was preserved; try again.');
312
+ wrapped.coordinatorClassified = true;
313
+ wrapped.coordinatorRetryable = false;
314
+ wrapped.coordinatorPhase = 'decision';
315
+ wrapped.cause = diagnostic ? new Error(diagnostic) : error;
316
+ return wrapped;
317
+ }
318
+
259
319
  function normalizeGuidance(value, detail) {
260
320
  if (!Array.isArray(value) || value.length < 1 || value.length > 8) {
261
321
  throw new Error('Work Center Coordinator guidance requires between 1 and 8 targets');
@@ -303,9 +363,7 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
303
363
  : {};
304
364
  const allowedKinds = options.recovery === true
305
365
  ? ['guide_actions', 'replan', 'request_human']
306
- : options.controlRequired === true
307
- ? ['guide_actions', 'replan']
308
- : ['answer', 'guide_actions', 'replan'];
366
+ : ['answer', 'guide_actions', 'replan'];
309
367
  const kind = allowedKinds.includes(source.kind) ? source.kind : '';
310
368
  if (!kind) throw new Error('Work Center Coordinator decision kind is invalid');
311
369
  const reason = cleanText(source.reason, 2_000, 'decision reason');
@@ -474,6 +532,9 @@ export class WorkItemCoordinator {
474
532
  this.policyProvider = typeof options.policyProvider === 'function' ? options.policyProvider : async () => ({});
475
533
  this.registry = options.registry;
476
534
  this.attachmentRoot = options.attachmentRoot || null;
535
+ this.languageProvider = typeof options.languageProvider === 'function'
536
+ ? options.languageProvider
537
+ : runtime => runtime?.config?.language || 'en';
477
538
  this.activeTurns = new Map();
478
539
  this.activeTasks = new Map();
479
540
  this.shuttingDown = false;
@@ -497,15 +558,12 @@ export class WorkItemCoordinator {
497
558
  }, {
498
559
  attachments: input.attachments,
499
560
  addedAttachments,
500
- recovery: input.recovery,
501
- requireWaitingRecovery: input.controlRequired === true,
502
561
  });
503
562
  if (!started) throw new Error(`WorkItem not found: ${id}`);
504
563
  options.onUpdate?.('coordinator.turn_started', started.detail);
505
564
  return this.#scheduleTurn(started, {
506
565
  text: promptText,
507
566
  recovery: false,
508
- controlRequired: input.controlRequired === true,
509
567
  addedAttachments,
510
568
  options,
511
569
  });
@@ -541,17 +599,17 @@ export class WorkItemCoordinator {
541
599
  const text = `Action stage "${action.stageId}" failed. Decide the next safe control transition. `
542
600
  + 'Failure is not a terminal WorkItem state: guide or replan executable work whenever possible. '
543
601
  + 'Request human input only when the snapshot lacks information required for a safe decision.';
544
- return this.#scheduleTurn(started, { text, recovery: true, controlRequired: false, options });
602
+ return this.#scheduleTurn(started, { text, recovery: true, options });
545
603
  }
546
604
 
547
605
  #scheduleTurn(started, {
548
- text, recovery, controlRequired = false, addedAttachments = [], options,
606
+ text, recovery, addedAttachments = [], options,
549
607
  }) {
550
608
  const abortController = new AbortController();
551
609
  this.activeTurns.set(started.turnId, abortController);
552
610
  const task = new Promise(resolve => setTimeout(resolve, 0))
553
611
  .then(() => this.#executeTurn(started, {
554
- text, recovery, controlRequired, addedAttachments, options, abortController,
612
+ text, recovery, addedAttachments, options, abortController,
555
613
  }))
556
614
  .finally(() => {
557
615
  this.activeTurns.delete(started.turnId);
@@ -562,7 +620,7 @@ export class WorkItemCoordinator {
562
620
  }
563
621
 
564
622
  async #executeTurn(started, {
565
- text, recovery, controlRequired, addedAttachments, options, abortController,
623
+ text, recovery, addedAttachments, options, abortController,
566
624
  }) {
567
625
  try {
568
626
  let normalized = null;
@@ -570,6 +628,7 @@ export class WorkItemCoordinator {
570
628
  let attemptCount = 0;
571
629
  let lastError = null;
572
630
  const snapshotText = coordinatorSnapshotText(started.detail);
631
+ let language = 'en';
573
632
  const attachmentContext = !recovery && this.attachmentRoot
574
633
  ? buildWorkItemAttachmentContext({ ...started.detail, attachments: addedAttachments }, {
575
634
  root: this.attachmentRoot,
@@ -581,13 +640,14 @@ export class WorkItemCoordinator {
581
640
  let settings;
582
641
  try {
583
642
  runtime = await this.runtimeProvider();
643
+ language = coordinatorLanguage(await this.languageProvider(runtime));
584
644
  } catch (error) {
585
- throw coordinatorExecutionError(error, 'runtime');
645
+ throw coordinatorExecutionError(error, 'runtime', language);
586
646
  }
587
647
  try {
588
648
  settings = await this.policyProvider();
589
649
  } catch (error) {
590
- throw coordinatorExecutionError(error, 'policy');
650
+ throw coordinatorExecutionError(error, 'policy', language);
591
651
  }
592
652
  if (this.shuttingDown) throw new Error('Work Center Coordinator is shutting down');
593
653
  let vps;
@@ -611,7 +671,7 @@ export class WorkItemCoordinator {
611
671
  };
612
672
  resolved = resolveWorkItemModel(runtime.config, assignment.vp, coordinatorPolicy);
613
673
  } catch (error) {
614
- throw coordinatorExecutionError(error, 'selection');
674
+ throw coordinatorExecutionError(error, 'selection', language);
615
675
  }
616
676
  const maxAttempts = recovery
617
677
  ? COORDINATOR_RECOVERY_DECISION_ATTEMPTS
@@ -632,7 +692,7 @@ export class WorkItemCoordinator {
632
692
  result = await Promise.race([
633
693
  runtime.adapter.call({
634
694
  model: resolved.model,
635
- system: COORDINATOR_SYSTEM_PROMPT,
695
+ system: coordinatorSystemPrompt(language),
636
696
  messages: [{ role: 'user', content }],
637
697
  maxTokens: Math.min(
638
698
  resolveMaxOutputTokens(resolved.model, runtime.config),
@@ -651,11 +711,10 @@ export class WorkItemCoordinator {
651
711
  ]);
652
712
  } catch (error) {
653
713
  if (abortController.signal.aborted || this.shuttingDown) throw error;
654
- throw coordinatorExecutionError(error, 'provider');
714
+ throw coordinatorExecutionError(error, 'provider', language);
655
715
  }
656
716
  normalized = normalizeCoordinatorResponse(result?.text, started.detail, {
657
717
  recovery,
658
- controlRequired,
659
718
  recoveryActionId: started.fence.recovery?.actionId || null,
660
719
  });
661
720
  if (normalized.decision.kind === 'replan') {
@@ -692,17 +751,27 @@ export class WorkItemCoordinator {
692
751
  throw lastError || new Error('Work Center Coordinator was interrupted');
693
752
  }
694
753
  if (!normalized) {
695
- if (!recovery || lastError?.coordinatorRetryable) {
696
- throw lastError || new Error('Work Center Coordinator did not produce a decision');
754
+ if (!recovery) {
755
+ throw lastError?.coordinatorClassified
756
+ ? lastError
757
+ : coordinatorDecisionError(
758
+ lastError || new Error('Work Center Coordinator did not produce a decision'),
759
+ language,
760
+ );
697
761
  }
762
+ if (lastError?.coordinatorRetryable) throw lastError;
698
763
  normalized = lastError?.coordinatorClassified
699
- ? permanentRecoveryDecision(lastError)
764
+ ? permanentRecoveryDecision(lastError, language)
700
765
  : {
701
- reply: 'Automatic recovery could not choose a safe executable next step. Human input is required.',
766
+ reply: language === 'zh'
767
+ ? '自动恢复无法确定安全、可执行的下一步,需要你补充信息。'
768
+ : 'Automatic recovery could not choose a safe executable next step. Human input is required.',
702
769
  decision: {
703
770
  kind: 'request_human',
704
771
  reason: 'Automatic recovery exhausted its bounded decision attempts',
705
- question: 'Review the failed Action and provide the missing decision or constraint needed to retry or replan it safely.',
772
+ question: language === 'zh'
773
+ ? '请查看失败的 Action,并补充安全重试或重新规划所需的决定或约束。'
774
+ : 'Review the failed Action and provide the missing decision or constraint needed to retry or replan it safely.',
706
775
  contractPatch: null,
707
776
  guidance: [],
708
777
  actions: [],
@@ -4,9 +4,12 @@ import {
4
4
  canonicalActionId,
5
5
  canonicalExplicitActionId,
6
6
  canonicalExplicitActionIds,
7
+ MAX_WORK_ITEM_ACTIONS,
7
8
  validateGeneratedCompletionGate,
8
9
  } from './workflow.js';
9
10
 
11
+ export const MAX_REPLAN_ADDED_ACTIONS = 8;
12
+
10
13
  function cleanProposalId(value) {
11
14
  const id = typeof value === 'string' ? value.trim().slice(0, 128) : '';
12
15
  if (!id) throw new Error('Work Center plan proposal requires proposalId');
@@ -185,7 +188,10 @@ export function applyAdditivePlanProposal({ workItem, actions, proposal, availab
185
188
  workItemType: workItem.workflowSnapshot.workItemType,
186
189
  actions: orderedActions,
187
190
  };
188
- const workflowSnapshot = applyGeneratedPlan(synthetic, rawPlan, { availableVpIds });
191
+ const workflowSnapshot = applyGeneratedPlan(synthetic, rawPlan, {
192
+ availableVpIds,
193
+ maxActions: MAX_WORK_ITEM_ACTIONS,
194
+ });
189
195
  const addedStages = workflowSnapshot.stages.filter(stage => addedIds.has(stage.id));
190
196
  return {
191
197
  proposalId,
@@ -269,7 +275,7 @@ export function applyCoordinatorReplan({ workItem, actions, proposal, availableV
269
275
  const workflowSnapshot = applyGeneratedPlan(synthetic, {
270
276
  workItemType: workItem.workflowSnapshot.workItemType,
271
277
  actions: [...completedInputs, ...normalizedFuture],
272
- }, { availableVpIds });
278
+ }, { availableVpIds, maxActions: MAX_WORK_ITEM_ACTIONS });
273
279
  const stageById = new Map(workflowSnapshot.stages.map(stage => [stage.id, stage]));
274
280
 
275
281
  return {
@@ -305,6 +311,14 @@ export function applyReplanMutation({ workItem, action, actions, proposal, avail
305
311
  throw new Error('Work Center replan Action is missing its frozen candidate set');
306
312
  }
307
313
  const candidateIds = barrier.candidateActionIds;
314
+ for (const field of ['retain', 'replace', 'remove', 'add']) {
315
+ if (!Array.isArray(proposal[field])) {
316
+ throw new Error(`Work Center replan mutation requires ${field} to be an array`);
317
+ }
318
+ }
319
+ if (proposal.add.length > MAX_REPLAN_ADDED_ACTIONS) {
320
+ throw new Error(`Work Center replan mutation can add at most ${MAX_REPLAN_ADDED_ACTIONS} new Actions`);
321
+ }
308
322
  const actionById = new Map(actions.map(candidate => [candidate.id, candidate]));
309
323
  const candidates = new Map(candidateIds.map(id => [id, actionById.get(id)]));
310
324
  for (const [id, candidate] of candidates) {
@@ -377,7 +391,7 @@ export function applyReplanMutation({ workItem, action, actions, proposal, avail
377
391
  const workflowSnapshot = applyGeneratedPlan(synthetic, {
378
392
  workItemType: workItem.workflowSnapshot.workItemType,
379
393
  actions: stableTopologicalActions([...completedInputs, ...retainedInputs, ...replacementInputs, ...addedInputs]),
380
- }, { availableVpIds });
394
+ }, { availableVpIds, maxActions: MAX_WORK_ITEM_ACTIONS });
381
395
  const stageById = new Map(workflowSnapshot.stages.map(stage => [stage.id, stage]));
382
396
  const context = (Array.isArray(action.context) ? action.context : [])
383
397
  .filter(entry => entry?.type !== 'replan-barrier' && entry?.type !== 'input');
@@ -32,7 +32,11 @@ 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
+ import {
36
+ applyAdditivePlanProposal,
37
+ applyReplanMutation,
38
+ MAX_REPLAN_ADDED_ACTIONS,
39
+ } from './plan-mutation.js';
36
40
  import { normalizeContractPatch, validateCompletedResult } from './completion-contract.js';
37
41
  import { normalizeEvidence } from './evidence.js';
38
42
  import {
@@ -468,7 +472,7 @@ export function createSubmitWorkItemReplanTool({ vps, workItem, action, actions,
468
472
  const candidateIdSchema = candidateIds.length > 0
469
473
  ? { type: 'string', enum: candidateIds }
470
474
  : { type: 'string' };
471
- const candidateLimit = Math.min(8, candidateIds.length);
475
+ const candidateLimit = candidateIds.length;
472
476
  const classification = { type: 'object', additionalProperties: false,
473
477
  required: ['actionId', 'action'], properties: {
474
478
  actionId: candidateIdSchema,
@@ -486,11 +490,19 @@ export function createSubmitWorkItemReplanTool({ vps, workItem, action, actions,
486
490
  retain: { type: 'array', maxItems: candidateLimit, items: classification },
487
491
  replace: { type: 'array', maxItems: candidateLimit, items: classification },
488
492
  remove: { type: 'array', maxItems: candidateLimit, uniqueItems: true, items: candidateIdSchema },
489
- add: { type: 'array', maxItems: 8, items: plannedActionSchema(vpIds) },
493
+ add: { type: 'array', maxItems: MAX_REPLAN_ADDED_ACTIONS, items: plannedActionSchema(vpIds) },
490
494
  } },
491
495
  async execute(input, ctx = {}) {
492
496
  if (!isRunActive()) throw new Error('Work Center Run is no longer active');
493
497
  if (collector.value) throw new Error('A WorkItem plan was already submitted for this Run');
498
+ applyReplanMutation({
499
+ workItem,
500
+ action,
501
+ actions,
502
+ proposal: input,
503
+ availableVpIds: vpIds,
504
+ });
505
+ if (!isRunActive()) throw new Error('Work Center Run is no longer active');
494
506
  collector.value = structuredClone(input);
495
507
  ctx.requestEndTurn?.({ kind: 'work_item_replan_submitted', proposalId: input.proposalId });
496
508
  return JSON.stringify({ submitted: true, proposalId: input.proposalId });
@@ -60,19 +60,6 @@ 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
-
76
63
  function listBoardItems(store, payload) {
77
64
  const limit = Math.min(Math.max(Number(payload.limit) || 100, 1), 200);
78
65
  const cursor = parseBoardCursor(payload.cursor);
@@ -387,49 +374,7 @@ export class WorkCenterService {
387
374
  throw new Error('generation must be a positive integer');
388
375
  }
389
376
  const workItem = this.#requiredItem(id);
390
- const targetAction = this.#requiredAction(workItem, payload.actionId);
391
- const humanRequest = coordinatorHumanRequest(workItem, targetAction, generation);
392
- if (humanRequest) {
393
- let addedAttachments = [];
394
- let turn;
395
- try {
396
- addedAttachments = appendWorkItemAttachments(workItem.attachments, payload.files, {
397
- root: this.attachmentRoot,
398
- workItemId: id,
399
- });
400
- turn = this.coordinator.message(id, {
401
- text: typeof payload.text === 'string' ? payload.text : '',
402
- revision: payload.revision,
403
- planRevision: workItem.planRevision,
404
- ledgerRevision: workItem.ledgerRevision,
405
- coordinatorRevision: workItem.coordinatorRevision,
406
- controlRequired: true,
407
- recovery: { ...humanRequest.recovery },
408
- addedAttachments,
409
- attachments: [...(workItem.attachments || []), ...addedAttachments],
410
- }, {
411
- onUpdate: (type, nextWorkItem) => {
412
- this.watcher.abortInvalidWorkItemRuns(id);
413
- this.#emit({ type, workItem: nextWorkItem });
414
- },
415
- });
416
- } catch (error) {
417
- try {
418
- if ((workItem.attachments || []).length === 0 && addedAttachments.length > 0) {
419
- removeWorkItemAttachments(this.attachmentRoot, id);
420
- } else {
421
- removeWorkItemAttachmentFiles(this.attachmentRoot, id, addedAttachments);
422
- }
423
- } catch {}
424
- throw error;
425
- }
426
- turn.task.catch(() => {});
427
- return {
428
- accepted: true,
429
- routedTo: 'coordinator',
430
- turnId: turn.detail.messages?.at(-1)?.turnId || null,
431
- };
432
- }
377
+ this.#requiredAction(workItem, payload.actionId);
433
378
  let addedAttachments = [];
434
379
  let detail;
435
380
  try {
@@ -464,6 +464,9 @@ export function validateGeneratedCompletionGate(stages) {
464
464
  }
465
465
  }
466
466
 
467
+ export const MAX_INITIAL_PLAN_ACTIONS = 8;
468
+ export const MAX_WORK_ITEM_ACTIONS = 64;
469
+
467
470
  export function applyGeneratedPlan(workItem, rawPlan, options = {}) {
468
471
  const source = workflowFrom(workItem);
469
472
  const forceGraph = options.forceGraph !== false;
@@ -481,8 +484,15 @@ export function applyGeneratedPlan(workItem, rawPlan, options = {}) {
481
484
  }
482
485
  const reservedStageIds = new Set((options.reservedStageIds || [])
483
486
  .map(id => String(id || '').trim()).filter(Boolean));
484
- if (!Array.isArray(rawPlan.actions) || rawPlan.actions.length < 1 || rawPlan.actions.length > 8) {
485
- throw new Error('AI-planned triage requires between 1 and 8 task-specific Actions');
487
+ const maxActions = Math.min(
488
+ Math.max(Number(options.maxActions) || MAX_INITIAL_PLAN_ACTIONS, 1),
489
+ MAX_WORK_ITEM_ACTIONS,
490
+ );
491
+ if (!Array.isArray(rawPlan.actions) || rawPlan.actions.length < 1
492
+ || rawPlan.actions.length > maxActions) {
493
+ throw new Error(maxActions === MAX_INITIAL_PLAN_ACTIONS
494
+ ? 'AI-planned triage requires between 1 and 8 task-specific Actions'
495
+ : `AI-planned graph requires between 1 and ${maxActions} task-specific Actions`);
486
496
  }
487
497
  const availableVpIds = Array.isArray(options.availableVpIds)
488
498
  ? new Set(options.availableVpIds.map(id => String(id || '').trim()).filter(Boolean))