@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.
@@ -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) => ({ ...evt, agentId: agent.id, agentName: agent.name });
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
+ }
@@ -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'