@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 +1 -1
- package/yeaft/engine.js +128 -8
- package/yeaft/tools/agent.js +2 -1
- package/yeaft/tools/bash.js +2 -1
- package/yeaft/web-bridge.js +27 -0
package/package.json
CHANGED
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) =>
|
|
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.#
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
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);
|
package/yeaft/tools/agent.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/yeaft/tools/bash.js
CHANGED
|
@@ -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
|
-
|
|
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) });
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -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.
|