@yeaft/webchat-agent 1.0.384 → 1.0.385
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/local-runtime/version.json +1 -1
- package/package.json +1 -1
- package/yeaft/cli-session-runner.js +137 -21
- package/yeaft/cli.js +248 -16
- package/yeaft/conversation/persist.js +2 -0
- package/yeaft/engine.js +31 -4
- package/yeaft/routing/router.js +6 -5
- package/yeaft/sessions/coordinator.js +41 -16
- package/yeaft/stdio-protocol.js +365 -242
- package/yeaft/sub-agent/public-event.js +131 -0
- package/yeaft/sub-agent/runner.js +11 -1
- package/yeaft/tasks/result-delivery.js +122 -0
- package/yeaft/tasks/result-format.js +28 -0
- package/yeaft/web-bridge.js +1 -23
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
function cleanString(value) {
|
|
2
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function publicError(value) {
|
|
6
|
+
if (!value) return null;
|
|
7
|
+
if (typeof value === 'string') return { name: 'Error', message: value };
|
|
8
|
+
return {
|
|
9
|
+
name: cleanString(value.name) || 'Error',
|
|
10
|
+
message: cleanString(value.message) || String(value),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Project one sub-agent event onto the public lifecycle boundary.
|
|
16
|
+
* Provider envelopes, accumulated messages, prompts, tool input/output, and
|
|
17
|
+
* raw loop diagnostics are intentionally not part of this projection.
|
|
18
|
+
*/
|
|
19
|
+
export function projectPublicSubAgentEvent(event) {
|
|
20
|
+
if (!event || typeof event !== 'object') return null;
|
|
21
|
+
const agentId = cleanString(event.agentId);
|
|
22
|
+
const agentName = cleanString(event.agentName);
|
|
23
|
+
const source = {
|
|
24
|
+
sessionId: cleanString(event.parentSessionId) || cleanString(event.sessionId),
|
|
25
|
+
vpId: cleanString(event.parentVpId) || cleanString(event.ownerVpId) || cleanString(event.vpId),
|
|
26
|
+
threadId: cleanString(event.parentThreadId) || cleanString(event.threadId) || 'main',
|
|
27
|
+
};
|
|
28
|
+
const base = {
|
|
29
|
+
type: event.type,
|
|
30
|
+
...(agentId ? { agentId } : {}),
|
|
31
|
+
...(agentName ? { agentName } : {}),
|
|
32
|
+
parentSessionId: source.sessionId,
|
|
33
|
+
parentVpId: source.vpId,
|
|
34
|
+
parentThreadId: source.threadId,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
switch (event.type) {
|
|
38
|
+
case 'sub_agent_status':
|
|
39
|
+
return {
|
|
40
|
+
...base,
|
|
41
|
+
status: cleanString(event.status) || 'unknown',
|
|
42
|
+
...(event.error ? { error: publicError(event.error) } : {}),
|
|
43
|
+
};
|
|
44
|
+
case 'sub_agent_turn_end':
|
|
45
|
+
return {
|
|
46
|
+
...base,
|
|
47
|
+
status: cleanString(event.status) || 'idle',
|
|
48
|
+
content: typeof event.content === 'string' ? event.content : '',
|
|
49
|
+
};
|
|
50
|
+
case 'text_delta':
|
|
51
|
+
return {
|
|
52
|
+
...base,
|
|
53
|
+
text: typeof event.text === 'string' ? event.text : '',
|
|
54
|
+
};
|
|
55
|
+
case 'sub_agent_spawned':
|
|
56
|
+
return { ...base, status: 'running' };
|
|
57
|
+
case 'turn_open':
|
|
58
|
+
return {
|
|
59
|
+
...base,
|
|
60
|
+
turnId: cleanString(event.turnId),
|
|
61
|
+
...(event.at ? { at: event.at } : {}),
|
|
62
|
+
};
|
|
63
|
+
case 'turn_close':
|
|
64
|
+
return {
|
|
65
|
+
...base,
|
|
66
|
+
turnId: cleanString(event.turnId),
|
|
67
|
+
totalMs: Number.isFinite(event.totalMs) ? event.totalMs : null,
|
|
68
|
+
totalTokens: Number.isFinite(event.totalTokens) ? event.totalTokens : null,
|
|
69
|
+
loopCount: Number.isFinite(event.loopCount) ? event.loopCount : null,
|
|
70
|
+
};
|
|
71
|
+
case 'tool_start':
|
|
72
|
+
case 'tool_end':
|
|
73
|
+
return {
|
|
74
|
+
...base,
|
|
75
|
+
id: cleanString(event.id),
|
|
76
|
+
name: cleanString(event.name),
|
|
77
|
+
...(event.type === 'tool_end' ? { isError: event.isError === true } : {}),
|
|
78
|
+
};
|
|
79
|
+
case 'usage':
|
|
80
|
+
return {
|
|
81
|
+
...base,
|
|
82
|
+
inputTokens: Number(event.inputTokens) || 0,
|
|
83
|
+
outputTokens: Number(event.outputTokens) || 0,
|
|
84
|
+
cacheReadTokens: Number(event.cacheReadTokens) || 0,
|
|
85
|
+
cacheWriteTokens: Number(event.cacheWriteTokens) || 0,
|
|
86
|
+
};
|
|
87
|
+
case 'stop':
|
|
88
|
+
return { ...base, stopReason: cleanString(event.stopReason) || 'unknown' };
|
|
89
|
+
case 'error':
|
|
90
|
+
return { ...base, error: publicError(event.error), retryable: event.retryable === true };
|
|
91
|
+
case 'fallback':
|
|
92
|
+
return {
|
|
93
|
+
...base,
|
|
94
|
+
from: cleanString(event.from),
|
|
95
|
+
to: cleanString(event.to),
|
|
96
|
+
reason: cleanString(event.reason),
|
|
97
|
+
};
|
|
98
|
+
case 'llm_retry':
|
|
99
|
+
return {
|
|
100
|
+
...base,
|
|
101
|
+
attempt: Number(event.attempt) || 0,
|
|
102
|
+
maxRetries: Number(event.maxRetries) || 0,
|
|
103
|
+
delayMs: Number(event.delayMs) || 0,
|
|
104
|
+
errorClass: cleanString(event.errorClass),
|
|
105
|
+
};
|
|
106
|
+
default:
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function buildStreamSubAgentFrame({ event, sessionId, vpId = null, threadId = 'main', agentId = null } = {}) {
|
|
112
|
+
const payload = projectPublicSubAgentEvent({
|
|
113
|
+
...event,
|
|
114
|
+
...(agentId && !event?.agentId ? { agentId } : {}),
|
|
115
|
+
parentSessionId: event?.parentSessionId || sessionId || null,
|
|
116
|
+
parentVpId: event?.parentVpId || vpId || null,
|
|
117
|
+
parentThreadId: event?.parentThreadId || threadId || 'main',
|
|
118
|
+
});
|
|
119
|
+
if (!payload || !payload.agentId || !payload.parentSessionId) return null;
|
|
120
|
+
if (sessionId && payload.parentSessionId !== sessionId) return null;
|
|
121
|
+
return {
|
|
122
|
+
type: 'sub_agent',
|
|
123
|
+
subtype: payload.type === 'sub_agent_status' ? 'status' : 'event',
|
|
124
|
+
session_id: payload.parentSessionId,
|
|
125
|
+
agent_id: payload.agentId,
|
|
126
|
+
...(payload.parentVpId ? { vp_id: payload.parentVpId, vpId: payload.parentVpId } : {}),
|
|
127
|
+
thread_id: payload.parentThreadId || 'main',
|
|
128
|
+
threadId: payload.parentThreadId || 'main',
|
|
129
|
+
payload,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -297,7 +297,14 @@ async function driveSubAgent(agent, subEngine, vpPersona, deps) {
|
|
|
297
297
|
const idleAbandonMs = typeof deps.idleAbandonMs === 'number' && deps.idleAbandonMs > 0
|
|
298
298
|
? deps.idleAbandonMs : IDLE_ABANDON_MS;
|
|
299
299
|
|
|
300
|
-
const wrapEvt = (evt) => ({
|
|
300
|
+
const wrapEvt = (evt) => ({
|
|
301
|
+
...evt,
|
|
302
|
+
agentId: agent.id,
|
|
303
|
+
agentName: agent.name,
|
|
304
|
+
parentSessionId: agent.parentSessionId || deps.parentSessionId || null,
|
|
305
|
+
parentVpId: agent.parentVpId || deps.parentVpId || null,
|
|
306
|
+
parentThreadId: agent.parentThreadId || deps.parentThreadId || 'main',
|
|
307
|
+
});
|
|
301
308
|
let lastTaskLogRefreshAt = 0;
|
|
302
309
|
const refreshTaskLog = ({ force = false } = {}) => {
|
|
303
310
|
if (!agent.taskId || !deps.taskManager || !agent.parentSessionId) return;
|
|
@@ -599,6 +606,9 @@ function finalizeTerminal(agent, status, { error, deps } = {}) {
|
|
|
599
606
|
agentName: agent.name,
|
|
600
607
|
status,
|
|
601
608
|
error: error || agent.error || null,
|
|
609
|
+
parentSessionId: agent.parentSessionId || deps?.parentSessionId || null,
|
|
610
|
+
parentVpId: agent.parentVpId || deps?.parentVpId || null,
|
|
611
|
+
parentThreadId: agent.parentThreadId || deps?.parentThreadId || 'main',
|
|
602
612
|
};
|
|
603
613
|
try { agent.outputLog?.write(evt); } catch { /* ignore */ }
|
|
604
614
|
if (agent.taskId && deps?.taskManager && agent.parentSessionId) {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ownership checks and stream-json projection for asynchronous tasks.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { TASK_RESULT_DELIVERY, isTerminalTaskStatus, taskResultDeliveryFor } from './store.js';
|
|
6
|
+
import { formatTaskResultForVp } from './result-format.js';
|
|
7
|
+
|
|
8
|
+
function cleanString(value) {
|
|
9
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function taskThreadId(task) {
|
|
13
|
+
return cleanString(task?.source?.threadId) || cleanString(task?.runtime?.threadId) || 'main';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function publicTaskSnapshot(task) {
|
|
17
|
+
return {
|
|
18
|
+
id: task.id,
|
|
19
|
+
sessionId: task.sessionId,
|
|
20
|
+
ownerVpId: cleanString(task.ownerVpId),
|
|
21
|
+
kind: task.kind || 'tool',
|
|
22
|
+
status: task.status || 'unknown',
|
|
23
|
+
resultDelivery: taskResultDeliveryFor(task),
|
|
24
|
+
createdAt: task.createdAt || null,
|
|
25
|
+
startedAt: task.startedAt || null,
|
|
26
|
+
updatedAt: task.updatedAt || null,
|
|
27
|
+
endedAt: task.endedAt || null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Project one TaskManager event for an active stream-json Session.
|
|
33
|
+
* Returns null for sibling, malformed, or unowned Session events.
|
|
34
|
+
*/
|
|
35
|
+
export function projectStreamTaskEvent(event, { sessionId } = {}) {
|
|
36
|
+
const task = event?.task;
|
|
37
|
+
const activeSessionId = cleanString(sessionId);
|
|
38
|
+
const taskSessionId = cleanString(task?.sessionId);
|
|
39
|
+
if (!event || typeof event !== 'object' || !task?.id || !activeSessionId || taskSessionId !== activeSessionId) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const vpId = cleanString(task.ownerVpId);
|
|
43
|
+
const threadId = taskThreadId(task);
|
|
44
|
+
return {
|
|
45
|
+
type: 'task',
|
|
46
|
+
subtype: event.event || 'updated',
|
|
47
|
+
session_id: activeSessionId,
|
|
48
|
+
task_id: task.id,
|
|
49
|
+
taskId: task.id,
|
|
50
|
+
...(vpId ? { vp_id: vpId, vpId } : {}),
|
|
51
|
+
thread_id: threadId,
|
|
52
|
+
threadId,
|
|
53
|
+
task: publicTaskSnapshot(task),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build owner-scoped model context for a terminal model-reentry task.
|
|
59
|
+
* When an expected owner is supplied, Session, VP, and thread must match
|
|
60
|
+
* exactly before sensitive task details are formatted.
|
|
61
|
+
*/
|
|
62
|
+
export function taskResultReentryContext(event, { sessionId = null, owner = null } = {}) {
|
|
63
|
+
if (!event || event.event !== 'completed' || !event.task) return null;
|
|
64
|
+
const task = event.task;
|
|
65
|
+
if (!task.id
|
|
66
|
+
|| !isTerminalTaskStatus(task.status)
|
|
67
|
+
|| taskResultDeliveryFor(task) !== TASK_RESULT_DELIVERY.MODEL_REENTRY) return null;
|
|
68
|
+
const taskSessionId = cleanString(task.sessionId);
|
|
69
|
+
const activeSessionId = cleanString(sessionId);
|
|
70
|
+
if (!taskSessionId || (activeSessionId && taskSessionId !== activeSessionId)) return null;
|
|
71
|
+
const vpId = cleanString(task.ownerVpId);
|
|
72
|
+
const threadId = taskThreadId(task);
|
|
73
|
+
if (owner) {
|
|
74
|
+
if (cleanString(owner.sessionId) !== taskSessionId) return null;
|
|
75
|
+
if (cleanString(owner.vpId) !== vpId) return null;
|
|
76
|
+
if ((cleanString(owner.threadId) || 'main') !== threadId) return null;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
task,
|
|
80
|
+
sessionId: taskSessionId,
|
|
81
|
+
vpId,
|
|
82
|
+
threadId,
|
|
83
|
+
content: formatTaskResultForVp(task),
|
|
84
|
+
metadata: {
|
|
85
|
+
preview: `task ${task.kind || 'tool'} ${task.status || 'completed'}`,
|
|
86
|
+
sessionId: taskSessionId,
|
|
87
|
+
vpId,
|
|
88
|
+
threadId,
|
|
89
|
+
taskKind: task.kind,
|
|
90
|
+
taskStatus: task.status,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Deliver to a still-running exact owner Engine. */
|
|
96
|
+
export function notifyPendingTaskOwner(event, asyncTaskOwners, { sessionId = null } = {}) {
|
|
97
|
+
const taskId = event?.task?.id;
|
|
98
|
+
const owner = taskId ? asyncTaskOwners?.get?.(taskId) : null;
|
|
99
|
+
if (!owner?.engine) return false;
|
|
100
|
+
const context = taskResultReentryContext(event, { sessionId, owner });
|
|
101
|
+
if (!context) return false;
|
|
102
|
+
const engine = owner.engine;
|
|
103
|
+
if (typeof engine.ownsPendingAsyncTask !== 'function'
|
|
104
|
+
|| !engine.ownsPendingAsyncTask(taskId)
|
|
105
|
+
|| typeof engine.notifyAsyncTaskCompleted !== 'function') return false;
|
|
106
|
+
try {
|
|
107
|
+
return engine.notifyAsyncTaskCompleted(taskId, context.content, context.metadata) === true;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Project and, when ownership matches, deliver one TaskManager event. */
|
|
114
|
+
export function emitStreamTaskEvent({ event, asyncTaskOwners, write, sessionId }) {
|
|
115
|
+
const frame = projectStreamTaskEvent(event, { sessionId });
|
|
116
|
+
if (!frame) return { projected: false, delivered: false };
|
|
117
|
+
if (typeof write === 'function') write(frame);
|
|
118
|
+
return {
|
|
119
|
+
projected: true,
|
|
120
|
+
delivered: notifyPendingTaskOwner(event, asyncTaskOwners, { sessionId }),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-only rendering for asynchronous task results.
|
|
3
|
+
*
|
|
4
|
+
* This output may contain command, log, and provider result details. It is
|
|
5
|
+
* owner-scoped prompt context and must never be reused as a public wire
|
|
6
|
+
* projection.
|
|
7
|
+
*/
|
|
8
|
+
export function formatTaskResultForVp(task) {
|
|
9
|
+
const result = task?.result || {};
|
|
10
|
+
const log = task?.log || {};
|
|
11
|
+
const lines = [
|
|
12
|
+
`<task-result id="${task?.id || 'unknown'}" kind="${task?.kind || 'tool'}" status="${task?.status || 'unknown'}">`,
|
|
13
|
+
`title: ${task?.title || task?.kind || task?.id || 'task'}`,
|
|
14
|
+
];
|
|
15
|
+
if (task?.runtime?.command) lines.push(`command: ${task.runtime.command}`);
|
|
16
|
+
if (result.exitCode !== undefined && result.exitCode !== null) lines.push(`exitCode: ${result.exitCode}`);
|
|
17
|
+
if (result.signal) lines.push(`signal: ${result.signal}`);
|
|
18
|
+
if (result.error) lines.push(`error: ${result.error}`);
|
|
19
|
+
if (result.summary) lines.push(`summary: ${result.summary}`);
|
|
20
|
+
if (log.path) lines.push(`log: ${log.path}`);
|
|
21
|
+
if (log.preview) {
|
|
22
|
+
lines.push('logTail:');
|
|
23
|
+
lines.push(String(log.preview).slice(-4000).split('\n').map(line => ` ${line}`).join('\n'));
|
|
24
|
+
}
|
|
25
|
+
lines.push('</task-result>');
|
|
26
|
+
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.');
|
|
27
|
+
return lines.join('\n');
|
|
28
|
+
}
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -97,6 +97,7 @@ import { consumeNotificationForAgent } from './sub-agent/notifications.js';
|
|
|
97
97
|
import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
|
|
98
98
|
import { recordAgentSessionCreated, recordAgentTurn } from '../metrics.js';
|
|
99
99
|
import { TASK_RESULT_DELIVERY, isTerminalTaskStatus, taskResultDeliveryFor } from './tasks/store.js';
|
|
100
|
+
import { formatTaskResultForVp } from './tasks/result-format.js';
|
|
100
101
|
|
|
101
102
|
const LEGACY_SKILL_COMMAND_PREFIX = 'skill:';
|
|
102
103
|
const YEAFT_SKILL_COMMAND_PREFIX = 'yeaft-skills:';
|
|
@@ -1947,29 +1948,6 @@ function enqueueForVp(sessionId, vpId, envelope) {
|
|
|
1947
1948
|
registerRoutePromise(envelope?.msg?.id, routePromise);
|
|
1948
1949
|
}
|
|
1949
1950
|
|
|
1950
|
-
function formatTaskResultForVp(task) {
|
|
1951
|
-
const result = task?.result || {};
|
|
1952
|
-
const log = task?.log || {};
|
|
1953
|
-
const lines = [
|
|
1954
|
-
`<task-result id="${task.id}" kind="${task.kind}" status="${task.status}">`,
|
|
1955
|
-
`title: ${task.title || task.kind || task.id}`,
|
|
1956
|
-
];
|
|
1957
|
-
if (task?.runtime?.command) lines.push(`command: ${task.runtime.command}`);
|
|
1958
|
-
if (result.exitCode !== undefined && result.exitCode !== null) lines.push(`exitCode: ${result.exitCode}`);
|
|
1959
|
-
if (result.signal) lines.push(`signal: ${result.signal}`);
|
|
1960
|
-
if (result.error) lines.push(`error: ${result.error}`);
|
|
1961
|
-
if (result.summary) lines.push(`summary: ${result.summary}`);
|
|
1962
|
-
if (log.path) lines.push(`log: ${log.path}`);
|
|
1963
|
-
if (log.preview) {
|
|
1964
|
-
const preview = String(log.preview).slice(-4000);
|
|
1965
|
-
lines.push('logTail:');
|
|
1966
|
-
lines.push(preview.split('\n').map(line => ` ${line}`).join('\n'));
|
|
1967
|
-
}
|
|
1968
|
-
lines.push('</task-result>');
|
|
1969
|
-
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.');
|
|
1970
|
-
return lines.join('\n');
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
1951
|
function scheduleTaskResultRescue({ taskId, sessionId, vpId, threadId = 'main', content, taskKind, taskStatus }) {
|
|
1974
1952
|
if (!sessionId || !vpId || !taskId) return false;
|
|
1975
1953
|
const text = typeof content === 'string'
|