@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.294",
3
+ "version": "1.0.296",
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",
package/yeaft/engine.js CHANGED
@@ -1838,7 +1838,7 @@ export class Engine {
1838
1838
  * string-prompt shape (no regression for existing callers).
1839
1839
  * @yields {EngineEvent}
1840
1840
  */
1841
- async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, closePendingUserInput = null, collabToolPolicy = null } = {}) {
1841
+ async *query({ prompt, promptParts = null, messages = [], signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null } = {}) {
1842
1842
  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
1843
1843
  yield {
1844
1844
  type: 'error',
@@ -1915,7 +1915,7 @@ export class Engine {
1915
1915
  };
1916
1916
  try {
1917
1917
  this.#currentThreadId = threadId || MAIN_THREAD_ID;
1918
- yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle });
1918
+ yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, askUser, threadId: this.#currentThreadId, vpTurnId, drainPendingUserMessages, prepareProviderRequest, startProviderRequest, finishProviderRequest, failProviderRequest, closePendingUserInput, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName, retryLifecycle });
1919
1919
  } finally {
1920
1920
  // Closing the async generator at a visible retry boundary means the
1921
1921
  // continuation never reached a provider. Keep it out of history and
@@ -1963,7 +1963,7 @@ export class Engine {
1963
1963
  * in a try/finally without indenting the whole loop.
1964
1964
  * @private
1965
1965
  */
1966
- async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle }) {
1966
+ async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workCenterInstructions, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, askUser = null, threadId = MAIN_THREAD_ID, vpTurnId = null, drainPendingUserMessages = null, prepareProviderRequest = null, startProviderRequest = null, finishProviderRequest = null, failProviderRequest = null, closePendingUserInput = null, collabToolPolicy = null, explicitSkillName = null, retryLifecycle }) {
1967
1967
 
1968
1968
  const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
1969
1969
  ? collabToolPolicy
@@ -2367,6 +2367,7 @@ export class Engine {
2367
2367
  let currentModel = this.#config.model;
2368
2368
  let cumulativeInputTokens = 0;
2369
2369
  let cumulativeOutputTokens = 0;
2370
+ let activeProviderRequest = null;
2370
2371
  // task-707: tool-callable end-turn signal. Tools (currently only
2371
2372
  // `route_forward`) can set this via toolCtx.requestEndTurn(reason)
2372
2373
  // to break out of the tool-loop after the current batch finishes
@@ -2645,6 +2646,16 @@ export class Engine {
2645
2646
  retryLifecycle.pendingContinuation = null;
2646
2647
  }
2647
2648
 
2649
+ activeProviderRequest = typeof prepareProviderRequest === 'function'
2650
+ ? prepareProviderRequest({
2651
+ turnNumber,
2652
+ entries: appendedBeforeStream,
2653
+ system: systemPrompt,
2654
+ messages: wireMessages.map(mapDebugMessage),
2655
+ model: currentModel,
2656
+ }) || null
2657
+ : null;
2658
+
2648
2659
  // Snapshot task results carried by this exact request. Request start
2649
2660
  // is not delivery: fetch may remain pending and then be aborted before
2650
2661
  // the provider processes anything. Ack only after a normal stream end
@@ -2661,6 +2672,7 @@ export class Engine {
2661
2672
  effortSource: userEffort ? 'user' : 'auto',
2662
2673
  signal,
2663
2674
  onRawExchange: captureRawExchange,
2675
+ onRequestStart: () => startProviderRequest?.(activeProviderRequest),
2664
2676
  })) {
2665
2677
  // task-325a (abort-stop fix): per-event abort short-circuit.
2666
2678
  // The adapter is expected to throw AbortError when fetch's
@@ -2768,6 +2780,14 @@ export class Engine {
2768
2780
  // escrow so retry or final rescue can deliver the payload.
2769
2781
  if (sawProviderStop) {
2770
2782
  this.#confirmAsyncTaskResults(requestAsyncTaskIds);
2783
+ const completedProviderRequest = activeProviderRequest;
2784
+ activeProviderRequest = null;
2785
+ finishProviderRequest?.(completedProviderRequest, {
2786
+ responseText,
2787
+ stopReason,
2788
+ toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
2789
+ thinkingBlocks,
2790
+ });
2771
2791
  }
2772
2792
  traceRequest('llm.request_complete', {
2773
2793
  durationMs: perfNowMs() - requestPerfStart,
@@ -2785,6 +2805,10 @@ export class Engine {
2785
2805
  consecutiveRetryableErrors = 0;
2786
2806
  } catch (err) {
2787
2807
  const latencyMs = Date.now() - startTime;
2808
+ if (activeProviderRequest) {
2809
+ failProviderRequest?.(activeProviderRequest, err);
2810
+ activeProviderRequest = null;
2811
+ }
2788
2812
 
2789
2813
  const endAttemptTrace = (attemptStopReason) => {
2790
2814
  this.#trace.endTurn(turnId, {
@@ -20,7 +20,7 @@ let shutdownPromise = null;
20
20
  let serviceFactory = null;
21
21
 
22
22
  const BROWSER_DETAIL_OPS = new Set([
23
- 'get', 'create', 'update', 'start', 'cancel', 'resume', 'action_input', 'retry_action', 'guide', 'retry',
23
+ 'get', 'create', 'update', 'start', 'cancel', 'resume', 'post_work_item_message', 'action_input', 'retry_action', 'guide', 'retry',
24
24
  ]);
25
25
  const BROWSER_ACTION_DEBUG_OPS = new Set(['get_action_messages', 'get_action_requests', 'get_action_request']);
26
26
  // `files` is an internal server-to-Agent field. The browser relay rejects any
@@ -29,6 +29,10 @@ const BROWSER_FILE_FIELDS = Object.freeze({
29
29
  create: [
30
30
  'title', 'goal', 'acceptanceCriteria', 'workItemType', 'workDir', 'reuseMemory', 'files', 'start',
31
31
  ],
32
+ post_work_item_message: [
33
+ 'id', 'clientMessageId', 'text', 'target', 'revision', 'planRevision', 'ledgerRevision',
34
+ 'coordinatorRevision', 'files',
35
+ ],
32
36
  work_item_message: [
33
37
  'id', 'text', 'revision', 'planRevision', 'ledgerRevision', 'coordinatorRevision', 'files',
34
38
  ],
@@ -227,6 +227,8 @@ export class WorkflowController {
227
227
  }
228
228
 
229
229
  input(id, input = {}) {
230
+ const existingClientMessage = this.store.hasActionInputClientMessage(id, input.actionId, input.clientMessageId);
231
+ if (existingClientMessage) return this.store.getWorkItemDetail(id);
230
232
  const text = typeof input.text === 'string' ? input.text.trim().slice(0, 8_000) : '';
231
233
  const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
232
234
  if (!text && addedAttachmentCount === 0) throw new Error('Action input or attachments are required');
@@ -253,7 +255,7 @@ export class WorkflowController {
253
255
  actionId: input.actionId,
254
256
  generation: expectedGeneration,
255
257
  revision: input.revision,
256
- }, input.attachments, input.addedAttachments);
258
+ }, input.attachments, input.addedAttachments, input.clientMessageId);
257
259
  }
258
260
  if (!['waiting', 'failed'].includes(targetAction.status)) {
259
261
  throw new Error(`Action in ${targetAction.status} cannot accept input`);
@@ -264,7 +266,9 @@ export class WorkflowController {
264
266
  expected: { actionId: input.actionId, generation: input.generation, revision: input.revision },
265
267
  attachments: input.attachments,
266
268
  inputEvent: {
267
- inputId: randomUUID(),
269
+ inputId: input.clientMessageId || randomUUID(),
270
+ clientMessageId: input.clientMessageId || null,
271
+ targetActionId: input.actionId,
268
272
  text: text || `The user added ${addedAttachmentCount} attachment(s) as additional context for this Action.`,
269
273
  attachments: input.addedAttachments,
270
274
  },
@@ -558,8 +558,12 @@ export class WorkItemCoordinator {
558
558
  }, {
559
559
  attachments: input.attachments,
560
560
  addedAttachments,
561
+ clientMessageId: input.clientMessageId,
561
562
  });
562
563
  if (!started) throw new Error(`WorkItem not found: ${id}`);
564
+ if (started.duplicate) {
565
+ return { detail: started.detail, task: Promise.resolve(started.detail), duplicate: true };
566
+ }
563
567
  options.onUpdate?.('coordinator.turn_started', started.detail);
564
568
  return this.#scheduleTurn(started, {
565
569
  text: promptText,
@@ -569,6 +573,18 @@ export class WorkItemCoordinator {
569
573
  });
570
574
  }
571
575
 
576
+ resume(started, options = {}) {
577
+ if (!started?.turnId || !started?.detail || !started?.fence) {
578
+ throw new Error('Coordinator provider recovery target is invalid');
579
+ }
580
+ return this.#scheduleTurn(started, {
581
+ text: typeof options.text === 'string' ? options.text : '',
582
+ recovery: options.recovery === true,
583
+ addedAttachments: Array.isArray(options.addedAttachments) ? options.addedAttachments : [],
584
+ options,
585
+ });
586
+ }
587
+
572
588
  recover(id, options = {}) {
573
589
  if (this.shuttingDown) throw new Error('Work Center Coordinator is shutting down');
574
590
  const detail = this.store.getWorkItemDetail(id);
@@ -689,19 +705,42 @@ export class WorkItemCoordinator {
689
705
  const content = attachmentContext.promptParts.length > 0
690
706
  ? [{ type: 'text', text: latestMessage }, ...attachmentContext.promptParts]
691
707
  : latestMessage;
692
- result = await Promise.race([
708
+ const requestBody = {
709
+ model: resolved.model,
710
+ system: coordinatorSystemPrompt(language),
711
+ messages: [{ role: 'user', content }],
712
+ maxTokens: Math.min(
713
+ resolveMaxOutputTokens(resolved.model, runtime.config),
714
+ COORDINATOR_MAX_OUTPUT_TOKENS,
715
+ ),
716
+ effort: resolved.effort,
717
+ effortSource: resolved.source,
718
+ effortContext: { scenario: 'work-center-coordinator' },
719
+ };
720
+ const providerTurn = this.store.prepareCoordinatorProviderTurn(
721
+ started.detail.id, started.turnId, attemptCount, requestBody,
722
+ );
723
+ if (providerTurn.status === 'unknown') {
724
+ throw new Error('Coordinator provider dispatch outcome is unknown and requires review');
725
+ }
726
+ if (providerTurn.status === 'responded') {
727
+ result = providerTurn.response;
728
+ } else {
729
+ result = await Promise.race([
693
730
  runtime.adapter.call({
694
- model: resolved.model,
695
- system: coordinatorSystemPrompt(language),
696
- messages: [{ role: 'user', content }],
697
- maxTokens: Math.min(
698
- resolveMaxOutputTokens(resolved.model, runtime.config),
699
- COORDINATOR_MAX_OUTPUT_TOKENS,
700
- ),
701
- effort: resolved.effort,
702
- effortSource: resolved.source,
703
- effortContext: { scenario: 'work-center-coordinator' },
731
+ ...requestBody,
704
732
  signal: abortController.signal,
733
+ onRequestStart: () => {
734
+ if (!this.store.dispatchCoordinatorProviderTurn(providerTurn.id)) {
735
+ throw new Error('Coordinator provider turn lost its dispatch fence');
736
+ }
737
+ },
738
+ }).then(response => {
739
+ const persisted = this.store.respondCoordinatorProviderTurn(
740
+ providerTurn.id, providerTurn.requestHash, response,
741
+ );
742
+ if (!persisted) throw new Error('Coordinator provider response lost its CAS fence');
743
+ return response;
705
744
  }),
706
745
  new Promise((_, reject) => {
707
746
  abortController.signal.addEventListener('abort', () => {
@@ -709,6 +748,7 @@ export class WorkItemCoordinator {
709
748
  }, { once: true });
710
749
  }),
711
750
  ]);
751
+ }
712
752
  } catch (error) {
713
753
  if (abortController.signal.aborted || this.shuttingDown) throw error;
714
754
  throw coordinatorExecutionError(error, 'provider', language);