@yeaft/webchat-agent 0.1.1094 → 0.1.1096

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": "0.1.1094",
3
+ "version": "0.1.1096",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/yeaft/engine.js CHANGED
@@ -487,6 +487,21 @@ export class Engine {
487
487
  */
488
488
  #pendingTaskResultMessages = [];
489
489
 
490
+ /**
491
+ * Terminal async task results that should be appended to the original
492
+ * tool_result message instead of injected as a synthetic user prompt.
493
+ * @type {Array<{taskId:string, toolCallId:string, content:string|Array, preview:string}>}
494
+ */
495
+ #pendingTaskResultUpdates = [];
496
+
497
+ /**
498
+ * Async task ownership metadata captured when a tool registers a
499
+ * background task. Keyed by taskId so terminal events can update the
500
+ * original tool_result instead of fabricating a separate turn.
501
+ * @type {Map<string, { toolCallId?: string, toolName?: string, threadId?: string }>}
502
+ */
503
+ #asyncTaskToolMeta = new Map();
504
+
490
505
  /**
491
506
  * Resolvers parked by the main loop while it waits for an async task to
492
507
  * terminate (or a fresh user append to arrive). Wake order is FIFO; every
@@ -1093,7 +1108,10 @@ export class Engine {
1093
1108
  // until the task terminates — its result is then spliced into the
1094
1109
  // next adapter loop in the SAME turn. Tools that don't produce
1095
1110
  // async tasks ignore it.
1096
- registerAsyncTask: (taskId) => this.#registerAsyncTask(taskId),
1111
+ registerAsyncTask: (taskId, meta = {}) => {
1112
+ const current = typeof vpCtx?.currentToolCall === 'function' ? vpCtx.currentToolCall() : null;
1113
+ this.#registerAsyncTask(taskId, { ...(current || {}), ...(meta || {}) });
1114
+ },
1097
1115
  // Sub-agent plumbing — Agent tool needs these to spawn a child
1098
1116
  // Engine that inherits the parent's adapter / stores / toolset.
1099
1117
  parentEngineDeps: {
@@ -1439,6 +1457,40 @@ export class Engine {
1439
1457
  }
1440
1458
  }
1441
1459
 
1460
+ #formatTaskResultUpdateContent(content) {
1461
+ if (typeof content === 'string') return content;
1462
+ try { return JSON.stringify(content); } catch { return String(content); }
1463
+ }
1464
+
1465
+ #drainPendingTaskResultUpdates(conversationMessages) {
1466
+ if (this.#pendingTaskResultUpdates.length === 0) return [];
1467
+ const updates = this.#pendingTaskResultUpdates.splice(0);
1468
+ const applied = [];
1469
+ for (const update of updates) {
1470
+ if (!update?.toolCallId) continue;
1471
+ const appendText = this.#formatTaskResultUpdateContent(update.content);
1472
+ if (!appendText.trim()) continue;
1473
+ const toolMsg = [...conversationMessages].reverse().find((msg) => (
1474
+ msg && msg.role === 'tool' && msg.toolCallId === update.toolCallId
1475
+ ));
1476
+ if (!toolMsg) {
1477
+ this.#pendingTaskResultMessages.push({
1478
+ content: update.content,
1479
+ preview: update.preview,
1480
+ internal: true,
1481
+ taskId: update.taskId,
1482
+ });
1483
+ continue;
1484
+ }
1485
+ const prior = typeof toolMsg.content === 'string'
1486
+ ? toolMsg.content
1487
+ : this.#formatTaskResultUpdateContent(toolMsg.content);
1488
+ toolMsg.content = `${prior}\n\n${appendText}`;
1489
+ applied.push(update);
1490
+ }
1491
+ return applied;
1492
+ }
1493
+
1442
1494
  #drainPendingUserMessages(drainPendingUserMessages) {
1443
1495
  const pending = [];
1444
1496
  if (typeof drainPendingUserMessages === 'function') {
@@ -1590,10 +1642,13 @@ export class Engine {
1590
1642
  const leftover = Array.from(this.#pendingAsyncTaskIds);
1591
1643
  this.#pendingAsyncTaskIds.clear();
1592
1644
  for (const tid of leftover) {
1645
+ this.#asyncTaskToolMeta.delete(tid);
1593
1646
  try { this.#asyncTaskCoordinator?.onUnregister?.(tid); } catch { /* ignore */ }
1594
1647
  }
1595
1648
  }
1649
+ this.#asyncTaskToolMeta.clear();
1596
1650
  this.#pendingTaskResultMessages.length = 0;
1651
+ this.#pendingTaskResultUpdates.length = 0;
1597
1652
  // Release any parked waiters so they don't pin a microtask after
1598
1653
  // query() returns. The loop has already exited so they're harmless,
1599
1654
  // but cleanup keeps the promise graph tight.
@@ -2031,6 +2086,21 @@ export class Engine {
2031
2086
  };
2032
2087
  }
2033
2088
  }
2089
+ const taskResultUpdatesBeforeStream = this.#drainPendingTaskResultUpdates(conversationMessages);
2090
+ if (taskResultUpdatesBeforeStream.length > 0) {
2091
+ for (const update of taskResultUpdatesBeforeStream) {
2092
+ yield {
2093
+ type: 'tool_result_update',
2094
+ turnId: queryTurnId,
2095
+ loopNumber: turnNumber,
2096
+ threadId,
2097
+ taskId: update.taskId,
2098
+ toolCallId: update.toolCallId,
2099
+ content: update.content,
2100
+ preview: update.preview,
2101
+ };
2102
+ }
2103
+ }
2034
2104
 
2035
2105
  try {
2036
2106
  // task-327b: resolve effort per-turn so the long-loop auto-bump
@@ -2597,6 +2667,7 @@ export class Engine {
2597
2667
  // never "fall through" with content sitting in either queue.
2598
2668
  while (!signal?.aborted
2599
2669
  && this.#pendingTaskResultMessages.length === 0
2670
+ && this.#pendingTaskResultUpdates.length === 0
2600
2671
  && this.#pendingUserMessages.length === 0) {
2601
2672
  if (this.#pendingAsyncTaskIds.size === 0) break;
2602
2673
  await this.#waitForAsyncWake(signal);
@@ -2610,6 +2681,23 @@ export class Engine {
2610
2681
  remainingTaskIds: Array.from(this.#pendingAsyncTaskIds),
2611
2682
  };
2612
2683
  if (!signal?.aborted) {
2684
+ const taskResultUpdatesAfterAsyncWait = this.#drainPendingTaskResultUpdates(conversationMessages);
2685
+ if (taskResultUpdatesAfterAsyncWait.length > 0) {
2686
+ for (const update of taskResultUpdatesAfterAsyncWait) {
2687
+ yield {
2688
+ type: 'tool_result_update',
2689
+ turnId: queryTurnId,
2690
+ loopNumber: turnNumber,
2691
+ threadId,
2692
+ taskId: update.taskId,
2693
+ toolCallId: update.toolCallId,
2694
+ content: update.content,
2695
+ preview: update.preview,
2696
+ };
2697
+ }
2698
+ yield { type: 'turn_end', turnNumber, stopReason: 'async_task_continue', threadId };
2699
+ continue;
2700
+ }
2613
2701
  const appendedAfterAsyncWait = this.#drainPendingUserMessages(drainPendingUserMessages);
2614
2702
  if (appendedAfterAsyncWait.length > 0) {
2615
2703
  for (const item of appendedAfterAsyncWait) {
@@ -2781,6 +2869,7 @@ export class Engine {
2781
2869
  }
2782
2870
 
2783
2871
  // Execute tool calls and feed results back
2872
+ let currentToolCallForAsyncTask = null;
2784
2873
  // task-707: requestEndTurn is a per-batch closure that lets a tool
2785
2874
  // signal "end this turn after the current batch — no adapter retry".
2786
2875
  // We re-create the closure each iteration because endTurnRequested
@@ -2798,6 +2887,7 @@ export class Engine {
2798
2887
  getCurrentTodos,
2799
2888
  setCurrentTodos,
2800
2889
  workDir,
2890
+ currentToolCall: () => currentToolCallForAsyncTask ? { ...currentToolCallForAsyncTask } : null,
2801
2891
  requestEndTurn: (reason) => {
2802
2892
  // First call wins — preserve the kind/reason of the first tool
2803
2893
  // that asked to end the turn. Late callers (a second
@@ -2859,6 +2949,11 @@ export class Engine {
2859
2949
 
2860
2950
  let output;
2861
2951
  let isError = false;
2952
+ currentToolCallForAsyncTask = {
2953
+ id: tc.id,
2954
+ name: tc.name,
2955
+ threadId: runtimeThreadId,
2956
+ };
2862
2957
 
2863
2958
  // Resolve tool: prefer ToolRegistry, fallback to legacy #tools Map
2864
2959
  const hasTool = this.#toolRegistry
@@ -2894,6 +2989,8 @@ export class Engine {
2894
2989
  }
2895
2990
  }
2896
2991
 
2992
+ currentToolCallForAsyncTask = null;
2993
+
2897
2994
  const toolDurationMs = Date.now() - toolStartTime;
2898
2995
 
2899
2996
  // feat-6af5f9f1 PR B: emit a structured `tool_exec` event for the
@@ -3351,12 +3448,23 @@ export class Engine {
3351
3448
  const preview = typeof opts.preview === 'string'
3352
3449
  ? opts.preview
3353
3450
  : (typeof content === 'string' ? content.slice(0, 200) : '[task result]');
3354
- this.#pendingTaskResultMessages.push({
3355
- content,
3356
- preview,
3357
- internal: true,
3358
- taskId,
3359
- });
3451
+ const meta = this.#asyncTaskToolMeta.get(taskId) || {};
3452
+ this.#asyncTaskToolMeta.delete(taskId);
3453
+ if (typeof meta.toolCallId === 'string' && meta.toolCallId) {
3454
+ this.#pendingTaskResultUpdates.push({
3455
+ taskId,
3456
+ toolCallId: meta.toolCallId,
3457
+ content,
3458
+ preview,
3459
+ });
3460
+ } else {
3461
+ this.#pendingTaskResultMessages.push({
3462
+ content,
3463
+ preview,
3464
+ internal: true,
3465
+ taskId,
3466
+ });
3467
+ }
3360
3468
  this.#wakeAsyncTaskWaiters();
3361
3469
  return true;
3362
3470
  }
@@ -3365,11 +3473,22 @@ export class Engine {
3365
3473
  * Register a background task as belonging to the current query. Called
3366
3474
  * from tools (bash background, agent spawn) via `toolCtx.registerAsyncTask`.
3367
3475
  * @param {string} taskId
3476
+ * @param {{ id?: string, name?: string, threadId?: string, toolCallId?: string, toolName?: string }} [meta]
3368
3477
  * @returns {void}
3369
3478
  */
3370
- #registerAsyncTask(taskId) {
3479
+ #registerAsyncTask(taskId, meta = {}) {
3371
3480
  if (typeof taskId !== 'string' || !taskId) return;
3372
3481
  this.#pendingAsyncTaskIds.add(taskId);
3482
+ const toolCallId = typeof meta.toolCallId === 'string' && meta.toolCallId
3483
+ ? meta.toolCallId
3484
+ : (typeof meta.id === 'string' && meta.id ? meta.id : null);
3485
+ if (toolCallId) {
3486
+ this.#asyncTaskToolMeta.set(taskId, {
3487
+ toolCallId,
3488
+ toolName: typeof meta.toolName === 'string' && meta.toolName ? meta.toolName : (typeof meta.name === 'string' ? meta.name : undefined),
3489
+ threadId: typeof meta.threadId === 'string' && meta.threadId ? meta.threadId : undefined,
3490
+ });
3491
+ }
3373
3492
  try { this.#asyncTaskCoordinator?.onRegister?.(taskId, this); } catch { /* coord must not throw into tools */ }
3374
3493
  }
3375
3494
 
@@ -3395,6 +3514,7 @@ export class Engine {
3395
3514
  // Fast paths — anything already pending releases instantly. This is
3396
3515
  // the common case when a task finished between adapter loops.
3397
3516
  if (this.#pendingTaskResultMessages.length > 0) return resolve();
3517
+ if (this.#pendingTaskResultUpdates.length > 0) return resolve();
3398
3518
  if (this.#pendingUserMessages.length > 0) return resolve();
3399
3519
  if (signal?.aborted) return resolve();
3400
3520
  this.#asyncTaskWaiters.push(resolve);
@@ -433,7 +433,8 @@ use it as the default workflow or call it repeatedly in a loop.`,
433
433
  // sub-agent runs in its own engine but reports completion
434
434
  // through the same TaskManager event, so the spawning turn
435
435
  // stays parked until the sub-agent finishes.
436
- try { ctx.registerAsyncTask?.(task.id); } catch { /* coord errors must not block spawn */ }
436
+ const currentToolCall = typeof ctx.currentToolCall === 'function' ? ctx.currentToolCall() : null;
437
+ try { ctx.registerAsyncTask?.(task.id, currentToolCall || {}); } catch { /* coord errors must not block spawn */ }
437
438
  }
438
439
  startSubAgent(agent, deps);
439
440
  } catch (err) {
@@ -251,7 +251,8 @@ Guidelines:
251
251
  // while the set is non-empty and will splice the task result
252
252
  // into the next adapter loop when it terminates. No-op when
253
253
  // the engine didn't wire the hook (legacy callers / tests).
254
- try { ctx.registerAsyncTask?.(task.id); } catch { /* never block tool return on coord errors */ }
254
+ const currentToolCall = typeof ctx.currentToolCall === 'function' ? ctx.currentToolCall() : null;
255
+ try { ctx.registerAsyncTask?.(task.id, currentToolCall || {}); } catch { /* never block tool return on coord errors */ }
255
256
  return `Started background task ${task.id}.\nStatus: ${task.status}\nLog: ${task.log?.path || ''}\nUse ListTasks, ReadTaskLog, or CancelTask to inspect or control it.`;
256
257
  } catch (err) {
257
258
  return JSON.stringify({ error: err?.message || String(err) });
@@ -2411,6 +2411,33 @@ function handleEngineEvent(event, hctx) {
2411
2411
  // until the next real event arrives.
2412
2412
  break;
2413
2413
 
2414
+ case 'tool_result_update': {
2415
+ const content = typeof event.content === 'string'
2416
+ ? event.content
2417
+ : JSON.stringify(event.content ?? '');
2418
+ if (hctx.toolResultsAccum && event.toolCallId) {
2419
+ const idx = hctx.toolResultsAccum.findIndex((tr) => tr.toolCallId === event.toolCallId);
2420
+ if (idx >= 0) {
2421
+ const prior = hctx.toolResultsAccum[idx].content || '';
2422
+ hctx.toolResultsAccum[idx] = {
2423
+ ...hctx.toolResultsAccum[idx],
2424
+ content: `${prior}\n\n${content}`,
2425
+ };
2426
+ }
2427
+ }
2428
+ sendSessionOutputFrame({
2429
+ type: 'user',
2430
+ tool_use_result: [{
2431
+ type: 'tool_result',
2432
+ tool_use_id: event.toolCallId,
2433
+ content,
2434
+ is_update: true,
2435
+ task_id: event.taskId || null,
2436
+ }],
2437
+ }, envelope);
2438
+ break;
2439
+ }
2440
+
2414
2441
  case 'turn_start':
2415
2442
  case 'stop':
2416
2443
  // No UI action needed; outer loop sends the final result.