@yeaft/webchat-agent 0.1.1071 → 0.1.1073
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/sub-agent/runner.js +45 -30
- package/yeaft/tasks/manager.js +3 -3
- package/yeaft/web-bridge.js +84 -17
package/package.json
CHANGED
|
@@ -449,10 +449,19 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
|
|
|
449
449
|
return;
|
|
450
450
|
}
|
|
451
451
|
|
|
452
|
-
// Turn complete cleanly.
|
|
453
|
-
//
|
|
454
|
-
//
|
|
452
|
+
// Turn complete cleanly. Task-backed sub-agents are one-shot
|
|
453
|
+
// background tasks: once they produce their mission result, complete
|
|
454
|
+
// the agent/task instead of letting the idle watchdog later mark the
|
|
455
|
+
// already-delivered work as abandoned. Legacy in-process callers with
|
|
456
|
+
// no TaskManager keep the old idle/PromptAgent continuation flow.
|
|
455
457
|
emit({ type: 'sub_agent_turn_end', content: assistantText });
|
|
458
|
+
if (agent.taskId && deps.taskManager && agent.parentSessionId) {
|
|
459
|
+
transitionTerminal(agent, STATUS.COMPLETED, {
|
|
460
|
+
diagnostic: 'task_turn_complete',
|
|
461
|
+
deps,
|
|
462
|
+
});
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
456
465
|
}
|
|
457
466
|
} finally {
|
|
458
467
|
if (wallTimeWatchdog) clearTimeout(wallTimeWatchdog);
|
|
@@ -512,6 +521,9 @@ function finalizeTerminal(agent, status, { error, deps } = {}) {
|
|
|
512
521
|
deps.taskManager.completeTask(agent.parentSessionId, agent.taskId, {
|
|
513
522
|
status: taskStatus,
|
|
514
523
|
error: error || agent.error || null,
|
|
524
|
+
summary: status === STATUS.COMPLETED
|
|
525
|
+
? (typeof agent.result === 'string' ? agent.result : (agent.lastResult || null))
|
|
526
|
+
: null,
|
|
515
527
|
});
|
|
516
528
|
} catch { /* ignore */ }
|
|
517
529
|
}
|
|
@@ -519,34 +531,37 @@ function finalizeTerminal(agent, status, { error, deps } = {}) {
|
|
|
519
531
|
try { deps.onEvent(agent.id, evt); } catch { /* ignore */ }
|
|
520
532
|
}
|
|
521
533
|
|
|
522
|
-
// Push the
|
|
523
|
-
// even if it forgot to call WaitAgent.
|
|
524
|
-
//
|
|
525
|
-
//
|
|
534
|
+
// Push the legacy sub-agent notification so the parent learns about this
|
|
535
|
+
// even if it forgot to call WaitAgent. Task-backed sub-agents use the
|
|
536
|
+
// generic TaskManager async tool-result re-entry instead; queueing both
|
|
537
|
+
// would make the owner VP see duplicate results.
|
|
526
538
|
try { consumeNotificationForAgent(agent.id); } catch { /* ignore */ }
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
539
|
+
const taskBacked = !!(agent.taskId && deps?.taskManager && agent.parentSessionId);
|
|
540
|
+
if (!taskBacked) {
|
|
541
|
+
try {
|
|
542
|
+
const budgetResult = agent.result && typeof agent.result === 'object'
|
|
543
|
+
&& agent.result.status === 'budget_exceeded'
|
|
544
|
+
? agent.result
|
|
545
|
+
: null;
|
|
546
|
+
enqueueTerminalNotification({
|
|
547
|
+
agentId: agent.id,
|
|
548
|
+
agentName: agent.name,
|
|
549
|
+
status,
|
|
550
|
+
result: budgetResult
|
|
551
|
+
? (budgetResult.partial_output || '')
|
|
552
|
+
: (typeof agent.result === 'string' ? agent.result : (agent.lastResult || '')),
|
|
553
|
+
error: error || agent.error || null,
|
|
554
|
+
outputFile: agent.outputFile || null,
|
|
555
|
+
turns: agent.usage?.turns || 0,
|
|
556
|
+
parentVpId: agent.parentVpId || null,
|
|
557
|
+
parentSessionId: agent.parentSessionId || null,
|
|
558
|
+
parentThreadId: agent.parentThreadId || 'main',
|
|
559
|
+
budgetExceeded: !!budgetResult,
|
|
560
|
+
budgetReason: budgetResult?.reason || null,
|
|
561
|
+
budgetUsage: budgetResult?.usage || null,
|
|
562
|
+
});
|
|
563
|
+
} catch { /* never let the notification queue throw kill the driver */ }
|
|
564
|
+
}
|
|
550
565
|
}
|
|
551
566
|
|
|
552
567
|
/**
|
package/yeaft/tasks/manager.js
CHANGED
|
@@ -182,7 +182,7 @@ export class TaskManager {
|
|
|
182
182
|
return publicSnapshot(task);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
|
-
#completeTask(sessionId, taskId, { status, exitCode = null, signal = null, error = null } = {}) {
|
|
185
|
+
#completeTask(sessionId, taskId, { status, exitCode = null, signal = null, error = null, summary = null } = {}) {
|
|
186
186
|
const key = this.#key(sessionId, taskId);
|
|
187
187
|
const task = this.active.get(key) || this.store.readTask(sessionId, taskId);
|
|
188
188
|
if (!task || isTerminalTaskStatus(task.status)) return publicSnapshot(task);
|
|
@@ -192,9 +192,9 @@ export class TaskManager {
|
|
|
192
192
|
task.updatedAt = nowIso();
|
|
193
193
|
task.endedAt = nowIso();
|
|
194
194
|
task.log = { ...(task.log || {}), path: tail.path, bytes: tail.bytes, preview: tail.text };
|
|
195
|
-
task.result = { ...(task.result || {}), exitCode, signal, error };
|
|
195
|
+
task.result = { ...(task.result || {}), exitCode, signal, error, summary };
|
|
196
196
|
this.store.writeTask(task);
|
|
197
|
-
this.store.appendEvent(sessionId, { event: 'completed', taskId, status: task.status, exitCode, signal, error });
|
|
197
|
+
this.store.appendEvent(sessionId, { event: 'completed', taskId, status: task.status, exitCode, signal, error, summary });
|
|
198
198
|
this.active.delete(key);
|
|
199
199
|
this.processes.delete(key);
|
|
200
200
|
this.#emit('completed', task);
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -396,7 +396,7 @@ function buildVpPromptPayload(vpId, envelope) {
|
|
|
396
396
|
export function visibleInboundThreadId(envelope, fallbackThreadId = 'main') {
|
|
397
397
|
const meta = envelope?.msg?.meta || {};
|
|
398
398
|
if (
|
|
399
|
-
meta.injectedBy === 'route_forward'
|
|
399
|
+
(meta.injectedBy === 'route_forward' || meta.injectedBy === 'task_result')
|
|
400
400
|
&& typeof meta.sourceThreadId === 'string'
|
|
401
401
|
&& meta.sourceThreadId.trim()
|
|
402
402
|
) {
|
|
@@ -1024,13 +1024,74 @@ function enqueueForVp(sessionId, vpId, envelope) {
|
|
|
1024
1024
|
registerRoutePromise(envelope?.msg?.id, routePromise);
|
|
1025
1025
|
}
|
|
1026
1026
|
|
|
1027
|
+
function formatTaskResultForVp(task) {
|
|
1028
|
+
const result = task?.result || {};
|
|
1029
|
+
const log = task?.log || {};
|
|
1030
|
+
const lines = [
|
|
1031
|
+
`<task-result id="${task.id}" kind="${task.kind}" status="${task.status}">`,
|
|
1032
|
+
`title: ${task.title || task.kind || task.id}`,
|
|
1033
|
+
];
|
|
1034
|
+
if (result.exitCode !== undefined && result.exitCode !== null) lines.push(`exitCode: ${result.exitCode}`);
|
|
1035
|
+
if (result.signal) lines.push(`signal: ${result.signal}`);
|
|
1036
|
+
if (result.error) lines.push(`error: ${result.error}`);
|
|
1037
|
+
if (result.summary) lines.push(`summary: ${result.summary}`);
|
|
1038
|
+
if (log.path) lines.push(`log: ${log.path}`);
|
|
1039
|
+
if (log.preview) {
|
|
1040
|
+
const preview = String(log.preview).slice(-4000);
|
|
1041
|
+
lines.push('logTail:');
|
|
1042
|
+
lines.push(preview.split('\n').map(line => ` ${line}`).join('\n'));
|
|
1043
|
+
}
|
|
1044
|
+
lines.push('</task-result>');
|
|
1045
|
+
lines.push('This is an asynchronous tool result from a background task, not a user message. Consume it now: tell the user the outcome or continue the work. Do not wait for another user turn.');
|
|
1046
|
+
return lines.join('\n');
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function scheduleTaskResultReentry(event) {
|
|
1050
|
+
if (!event || event.event !== 'completed' || !event.task) return;
|
|
1051
|
+
const task = event.task;
|
|
1052
|
+
const sessionId = task.sessionId || event.sessionId || null;
|
|
1053
|
+
const vpId = task.ownerVpId || null;
|
|
1054
|
+
if (!sessionId || !vpId) return;
|
|
1055
|
+
const threadId = task.source?.threadId || task.runtime?.threadId || 'main';
|
|
1056
|
+
const msgId = `task_result_${Date.now().toString(36)}_${randomUUID().slice(0, 8)}`;
|
|
1057
|
+
queueMicrotask(() => {
|
|
1058
|
+
enqueueForVp(sessionId, vpId, {
|
|
1059
|
+
sessionId,
|
|
1060
|
+
taskId: task.id,
|
|
1061
|
+
trigger: 'task_result',
|
|
1062
|
+
_promptSuffix: '',
|
|
1063
|
+
msg: {
|
|
1064
|
+
id: msgId,
|
|
1065
|
+
from: 'tool',
|
|
1066
|
+
role: 'assistant',
|
|
1067
|
+
text: formatTaskResultForVp(task),
|
|
1068
|
+
meta: {
|
|
1069
|
+
injectedBy: 'task_result',
|
|
1070
|
+
taskId: task.id,
|
|
1071
|
+
taskKind: task.kind,
|
|
1072
|
+
taskStatus: task.status,
|
|
1073
|
+
sourceThreadId: threadId,
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1027
1080
|
async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
1028
1081
|
const { text, prompt, promptParts } = buildVpPromptPayload(vpId, envelope);
|
|
1029
1082
|
const runningThreads = getRunningThreads(sessionId, vpId);
|
|
1030
1083
|
let thread = null;
|
|
1031
1084
|
let related = false;
|
|
1032
1085
|
|
|
1033
|
-
|
|
1086
|
+
const meta = envelope?.msg?.meta || {};
|
|
1087
|
+
const isTaskResult = meta.injectedBy === 'task_result';
|
|
1088
|
+
const sourceThreadId = typeof meta.sourceThreadId === 'string' && meta.sourceThreadId.trim()
|
|
1089
|
+
? meta.sourceThreadId.trim()
|
|
1090
|
+
: null;
|
|
1091
|
+
|
|
1092
|
+
if (isTaskResult && sourceThreadId) {
|
|
1093
|
+
thread = getOrCreateVpThread({ sessionId, vpId, threadId: sourceThreadId, title: fallbackTitle(text) });
|
|
1094
|
+
} else if (runningThreads.length === 0) {
|
|
1034
1095
|
thread = getOrCreateVpThread({ sessionId, vpId, title: fallbackTitle(text) });
|
|
1035
1096
|
} else {
|
|
1036
1097
|
const decision = await threadClassifier({
|
|
@@ -1064,23 +1125,24 @@ async function routeEnvelopeToVpThread(sessionId, vpId, envelope) {
|
|
|
1064
1125
|
|
|
1065
1126
|
if (related) {
|
|
1066
1127
|
const content = promptParts || prompt;
|
|
1067
|
-
const
|
|
1128
|
+
const injectedBy = envelope?.msg?.meta?.injectedBy;
|
|
1129
|
+
const isInternalAppend = injectedBy === 'route_forward' || injectedBy === 'task_result';
|
|
1068
1130
|
thread.pendingQueries.push({
|
|
1069
1131
|
content,
|
|
1070
1132
|
preview: prompt,
|
|
1071
1133
|
originalText: text,
|
|
1072
1134
|
originalParts: Array.isArray(envelope?._promptParts) ? envelope._promptParts : null,
|
|
1073
|
-
internal:
|
|
1135
|
+
internal: isInternalAppend,
|
|
1074
1136
|
});
|
|
1075
1137
|
persistInboundMessageOnceByMsgId({
|
|
1076
1138
|
msgId: envelope?.msg?.id,
|
|
1077
1139
|
text,
|
|
1078
1140
|
sessionId,
|
|
1079
1141
|
threadId: visibleInboundThreadId(envelope, thread.threadId),
|
|
1080
|
-
role:
|
|
1142
|
+
role: isInternalAppend ? 'assistant' : 'user',
|
|
1081
1143
|
speakerVpId: envelope?.msg?.meta?.senderVpId || envelope?.msg?.from || null,
|
|
1082
1144
|
attachments: Array.isArray(envelope?.msg?.meta?.attachments) ? envelope.msg.meta.attachments : [],
|
|
1083
|
-
internal:
|
|
1145
|
+
internal: isInternalAppend,
|
|
1084
1146
|
});
|
|
1085
1147
|
thread.updatedAt = Date.now();
|
|
1086
1148
|
try {
|
|
@@ -1165,17 +1227,18 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
|
|
|
1165
1227
|
const envMsgId = envelope?.msg?.id;
|
|
1166
1228
|
if (envMsgId && text) {
|
|
1167
1229
|
const meta = envelope?.msg?.meta || {};
|
|
1168
|
-
const
|
|
1169
|
-
const
|
|
1230
|
+
const injectedBy = meta.injectedBy;
|
|
1231
|
+
const isInternal = injectedBy === 'route_forward' || injectedBy === 'task_result';
|
|
1232
|
+
const senderVpId = isInternal ? (meta.senderVpId || envelope?.msg?.from || null) : null;
|
|
1170
1233
|
persistInboundMessageOnceByMsgId({
|
|
1171
1234
|
msgId: envMsgId,
|
|
1172
1235
|
text,
|
|
1173
1236
|
sessionId,
|
|
1174
1237
|
threadId: visibleInboundThreadId(envelope, thread.threadId),
|
|
1175
|
-
role:
|
|
1238
|
+
role: isInternal ? 'assistant' : 'user',
|
|
1176
1239
|
speakerVpId: senderVpId,
|
|
1177
1240
|
attachments: Array.isArray(meta.attachments) ? meta.attachments : [],
|
|
1178
|
-
internal:
|
|
1241
|
+
internal: isInternal,
|
|
1179
1242
|
});
|
|
1180
1243
|
}
|
|
1181
1244
|
} catch { /* never crash WS pipeline */ }
|
|
@@ -1211,7 +1274,9 @@ function ensureDriverRunning(sessionId, vpId, threadId = 'main') {
|
|
|
1211
1274
|
} catch { /* never crash WS pipeline */ }
|
|
1212
1275
|
}
|
|
1213
1276
|
try {
|
|
1214
|
-
|
|
1277
|
+
const injectedBy = envelope?.msg?.meta?.injectedBy;
|
|
1278
|
+
const isInternalMessage = injectedBy === 'route_forward' || injectedBy === 'task_result';
|
|
1279
|
+
if (text && envelope?.msg && !isInternalMessage) {
|
|
1215
1280
|
sendSessionEvent({
|
|
1216
1281
|
type: 'session_message',
|
|
1217
1282
|
sessionId,
|
|
@@ -1949,6 +2014,7 @@ export function installYeaftRuntimeBridge(s) {
|
|
|
1949
2014
|
try {
|
|
1950
2015
|
const sessionId = event?.task?.sessionId || event?.sessionId || null;
|
|
1951
2016
|
sendSessionEvent(event, { sessionId });
|
|
2017
|
+
scheduleTaskResultReentry(event);
|
|
1952
2018
|
} catch { /* never let task event delivery throw */ }
|
|
1953
2019
|
});
|
|
1954
2020
|
}
|
|
@@ -3182,8 +3248,9 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
3182
3248
|
// the source VP's tool action. Do not append it as a visible prompt for
|
|
3183
3249
|
// the target VP turn; otherwise UI replay can show a trailing handoff
|
|
3184
3250
|
// block after the target response.
|
|
3185
|
-
const
|
|
3186
|
-
const
|
|
3251
|
+
const inboundInjectedBy = inboundEnvelope?.msg?.meta?.injectedBy;
|
|
3252
|
+
const inboundIsInternal = inboundInjectedBy === 'route_forward' || inboundInjectedBy === 'task_result';
|
|
3253
|
+
const visiblePrompts = inboundIsInternal ? appendedUserPrompts : [prompt, ...appendedUserPrompts];
|
|
3187
3254
|
appendTurnToSessionHistory(sessionId, threadId, vpId, visiblePrompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum);
|
|
3188
3255
|
|
|
3189
3256
|
sendSessionOutputFrame({
|
|
@@ -3368,7 +3435,7 @@ function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistan
|
|
|
3368
3435
|
* Persist an inbound message row to disk EXACTLY ONCE per
|
|
3369
3436
|
* coordinator-ingest call, keyed by the coordinator-assigned `msgId`.
|
|
3370
3437
|
* Both `handleYeaftSessionSend` (real user input, persists as
|
|
3371
|
-
* role='user') and `enqueueForVp`'s driver loop (route_forward
|
|
3438
|
+
* role='user') and `enqueueForVp`'s driver loop (route_forward / task_result
|
|
3372
3439
|
* synthetic injections, persists as role='assistant' attributed via
|
|
3373
3440
|
* `speakerVpId`) call this — the Set guard makes either path the
|
|
3374
3441
|
* writer, whichever runs first, while the other becomes a no-op.
|
|
@@ -3427,9 +3494,9 @@ function persistInboundMessageOnceByMsgId({ msgId, text, sessionId, threadId = '
|
|
|
3427
3494
|
try {
|
|
3428
3495
|
// role defaults to 'user' for back-compat: handleYeaftSessionSend's
|
|
3429
3496
|
// real-user call site passes no role and gets a user row. The driver
|
|
3430
|
-
// loop passes role='assistant' + speakerVpId for route_forward
|
|
3431
|
-
// injections so the on-disk record correctly attributes
|
|
3432
|
-
// the
|
|
3497
|
+
// loop passes role='assistant' + speakerVpId for route_forward /
|
|
3498
|
+
// task_result injections so the on-disk record correctly attributes
|
|
3499
|
+
// the internal trigger text.
|
|
3433
3500
|
const persistRole = role === 'assistant' ? 'assistant' : 'user';
|
|
3434
3501
|
const record = {
|
|
3435
3502
|
role: persistRole,
|