@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.385"}
|
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { Engine } from './engine.js';
|
|
4
4
|
import { createRouter } from './routing/router.js';
|
|
5
5
|
import { createCoordinator } from './sessions/coordinator.js';
|
|
6
|
+
import { resolveMemberId } from './sessions/roster.js';
|
|
6
7
|
import { sessionsRoot } from './sessions/session-crud.js';
|
|
7
8
|
import { openSession, loadSessionMeta } from './sessions/session-store.js';
|
|
8
9
|
import { loadSessionConfig, resolveSessionConfig } from './sessions/session-config.js';
|
|
@@ -61,6 +62,7 @@ export function createCliSessionRunner({
|
|
|
61
62
|
workDir = process.cwd(),
|
|
62
63
|
engineFactory = createCliVpEngine,
|
|
63
64
|
personaFactory = buildVpPersona,
|
|
65
|
+
configureEngine = null,
|
|
64
66
|
} = {}) {
|
|
65
67
|
if (!loaded || !sessionId) return null;
|
|
66
68
|
const sessionDir = join(sessionsRoot(loaded.yeaftDir), sessionId);
|
|
@@ -70,12 +72,21 @@ export function createCliSessionRunner({
|
|
|
70
72
|
const engines = new Map();
|
|
71
73
|
const tails = new Map();
|
|
72
74
|
const pending = new Set();
|
|
75
|
+
// Root turns are accepted synchronously but execute on per-VP promise tails.
|
|
76
|
+
// Durable rows therefore cannot use append order as their causal boundary: a
|
|
77
|
+
// later root user row may be written before an earlier VP starts, while the
|
|
78
|
+
// earlier VP's assistant rows may be written after that later user row. Every
|
|
79
|
+
// new row carries one durable causalRootId; the legacy ids remain fallbacks
|
|
80
|
+
// for rows produced before that field existed.
|
|
81
|
+
const rootOrderByIdentity = new Map();
|
|
82
|
+
let nextRootOrder = 0;
|
|
73
83
|
let closed = false;
|
|
74
84
|
|
|
75
85
|
const engineFor = (vpId) => {
|
|
76
86
|
let engine = engines.get(vpId);
|
|
77
87
|
if (!engine) {
|
|
78
88
|
engine = engineFactory(loaded, sessionId, vpId);
|
|
89
|
+
if (typeof configureEngine === 'function') configureEngine(engine, vpId);
|
|
79
90
|
engines.set(vpId, engine);
|
|
80
91
|
}
|
|
81
92
|
return engine;
|
|
@@ -86,16 +97,56 @@ export function createCliSessionRunner({
|
|
|
86
97
|
const runEnvelope = async (vpId, envelope, options) => {
|
|
87
98
|
const meta = handle.getMeta();
|
|
88
99
|
const engine = engineFor(vpId);
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
|
|
100
|
+
const prompt = envelope?.msg?.text || '';
|
|
101
|
+
const persistedUserClientMessageId = typeof envelope?._persistedUserClientMessageId === 'string'
|
|
102
|
+
? envelope._persistedUserClientMessageId
|
|
103
|
+
: null;
|
|
104
|
+
const causalRootId = typeof envelope?._cliCausalRootId === 'string' && envelope._cliCausalRootId
|
|
105
|
+
? envelope._cliCausalRootId
|
|
106
|
+
: null;
|
|
107
|
+
const rootOrder = Number.isInteger(envelope?._cliRootOrder)
|
|
108
|
+
? envelope._cliRootOrder
|
|
109
|
+
: null;
|
|
110
|
+
// Engine.query() appends `prompt` itself. Exclude this root's durable user
|
|
111
|
+
// row and every later root turn, regardless of where their assistant/tool
|
|
112
|
+
// rows landed in the globally sequenced transcript. This preserves rows
|
|
113
|
+
// completed by earlier accepted roots while preventing future prompts from
|
|
114
|
+
// entering an earlier provider request.
|
|
115
|
+
const messages = loaded.conversationStore
|
|
116
|
+
.loadSessionHistoryForVp(sessionId, vpId)
|
|
117
|
+
.filter((message) => {
|
|
118
|
+
if (persistedUserClientMessageId
|
|
119
|
+
&& message?.role === 'user'
|
|
120
|
+
&& message.clientMessageId === persistedUserClientMessageId) return false;
|
|
121
|
+
if (rootOrder === null) return true;
|
|
122
|
+
let messageRootOrder = null;
|
|
123
|
+
if (typeof message?.causalRootId === 'string' && message.causalRootId) {
|
|
124
|
+
// A durable causal root is authoritative. Do not reinterpret a row by
|
|
125
|
+
// its role-specific legacy ids when this field is present.
|
|
126
|
+
messageRootOrder = rootOrderByIdentity.get(message.causalRootId);
|
|
127
|
+
} else if (message?.role === 'user' && typeof message.clientMessageId === 'string') {
|
|
128
|
+
messageRootOrder = rootOrderByIdentity.get(message.clientMessageId);
|
|
129
|
+
} else if (typeof message?.turnId === 'string') {
|
|
130
|
+
messageRootOrder = rootOrderByIdentity.get(message.turnId);
|
|
131
|
+
}
|
|
132
|
+
return !Number.isInteger(messageRootOrder) || messageRootOrder < rootOrder;
|
|
133
|
+
});
|
|
92
134
|
const todos = [];
|
|
93
135
|
let resultText = '';
|
|
94
136
|
let failed = null;
|
|
95
137
|
const scopedCoordinator = {
|
|
96
138
|
group: coordinator.group,
|
|
97
139
|
ingest(input, opts) {
|
|
98
|
-
|
|
140
|
+
const report = coordinator.ingest({
|
|
141
|
+
...input,
|
|
142
|
+
_cliRootOrder: envelope._cliRootOrder,
|
|
143
|
+
_cliCausalRootId: envelope._cliCausalRootId,
|
|
144
|
+
_cliTurnContext: envelope._cliTurnContext,
|
|
145
|
+
}, opts);
|
|
146
|
+
if (rootOrder !== null && typeof report?.message?.id === 'string') {
|
|
147
|
+
rootOrderByIdentity.set(report.message.id, rootOrder);
|
|
148
|
+
}
|
|
149
|
+
return report;
|
|
99
150
|
},
|
|
100
151
|
};
|
|
101
152
|
const queryOptions = {
|
|
@@ -110,6 +161,7 @@ export function createCliSessionRunner({
|
|
|
110
161
|
router: createRouter({ coordinator: scopedCoordinator }),
|
|
111
162
|
inboundEnvelope: envelope,
|
|
112
163
|
userAlreadyPersisted: true,
|
|
164
|
+
causalRootId,
|
|
113
165
|
threadId: 'main',
|
|
114
166
|
vpTurnId: envelope?.msg?.id || randomUUID(),
|
|
115
167
|
collabToolPolicy: meta.roster.length > 1
|
|
@@ -120,7 +172,7 @@ export function createCliSessionRunner({
|
|
|
120
172
|
todos.splice(0, todos.length, ...(Array.isArray(next) ? next : []));
|
|
121
173
|
},
|
|
122
174
|
askUser: options.askUser
|
|
123
|
-
? request => options.askUser(request, vpId, queryOptions.vpTurnId)
|
|
175
|
+
? request => options.askUser(request, vpId, queryOptions.vpTurnId, queryOptions.threadId)
|
|
124
176
|
: null,
|
|
125
177
|
userEffort: options.modelEffort || null,
|
|
126
178
|
};
|
|
@@ -151,14 +203,17 @@ export function createCliSessionRunner({
|
|
|
151
203
|
if (closed) throw new Error('CLI Session runner is closed');
|
|
152
204
|
const turnContext = envelope?._cliTurnContext;
|
|
153
205
|
if (!turnContext) throw new Error('CLI Session envelope is missing its turn context');
|
|
206
|
+
if (turnContext.claimedVpIds.has(vpId)) {
|
|
207
|
+
return { ok: false, error: 'target_already_claimed' };
|
|
208
|
+
}
|
|
209
|
+
turnContext.claimedVpIds.add(vpId);
|
|
154
210
|
const previous = tails.get(vpId) || Promise.resolve();
|
|
155
211
|
const task = previous.catch(() => {}).then(() => runEnvelope(vpId, envelope, turnContext.options));
|
|
156
212
|
tails.set(vpId, task);
|
|
157
213
|
pending.add(task);
|
|
158
|
-
turnContext.
|
|
214
|
+
turnContext.tasks.push(task);
|
|
159
215
|
task.finally(() => {
|
|
160
216
|
pending.delete(task);
|
|
161
|
-
turnContext.pending.delete(task);
|
|
162
217
|
if (tails.get(vpId) === task) tails.delete(vpId);
|
|
163
218
|
}).catch(() => {});
|
|
164
219
|
return task;
|
|
@@ -181,26 +236,87 @@ export function createCliSessionRunner({
|
|
|
181
236
|
get meta() { return handle.getMeta(); },
|
|
182
237
|
async run(prompt, options = {}) {
|
|
183
238
|
if (closed) throw new Error('CLI Session runner is closed');
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
239
|
+
let routingIntent = options.routingIntent && typeof options.routingIntent === 'object'
|
|
240
|
+
? options.routingIntent
|
|
241
|
+
: null;
|
|
242
|
+
if (routingIntent) {
|
|
243
|
+
const meta = handle.getMeta();
|
|
244
|
+
const rawTargets = Array.isArray(routingIntent.targetVpIds)
|
|
245
|
+
? routingIntent.targetVpIds
|
|
246
|
+
: [];
|
|
247
|
+
const targetVpIds = [];
|
|
248
|
+
for (const rawTarget of rawTargets) {
|
|
249
|
+
const target = typeof rawTarget === 'string' ? rawTarget.trim() : '';
|
|
250
|
+
const resolved = resolveMemberId(meta, target);
|
|
251
|
+
if (!resolved) throw new Error(`Unknown stream-json target VP ${target || String(rawTarget)}: not in roster`);
|
|
252
|
+
if (!targetVpIds.includes(resolved)) targetVpIds.push(resolved);
|
|
253
|
+
}
|
|
254
|
+
if (routingIntent.broadcast === true) {
|
|
255
|
+
for (const vpId of meta.roster) {
|
|
256
|
+
if (!targetVpIds.includes(vpId)) targetVpIds.push(vpId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (targetVpIds.length === 0) throw new Error('stream-json routing intent requires at least one target VP');
|
|
260
|
+
routingIntent = Object.freeze({
|
|
261
|
+
targetVpIds: Object.freeze(targetVpIds),
|
|
262
|
+
broadcast: routingIntent.broadcast === true,
|
|
263
|
+
explicit: routingIntent.explicit === true,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
const turnContext = Object.freeze({
|
|
267
|
+
options: Object.freeze({ ...options }),
|
|
268
|
+
tasks: [],
|
|
269
|
+
claimedVpIds: new Set(),
|
|
195
270
|
});
|
|
271
|
+
const messageId = randomUUID();
|
|
272
|
+
const rootOrder = nextRootOrder++;
|
|
273
|
+
rootOrderByIdentity.set(messageId, rootOrder);
|
|
274
|
+
// The shared user row is the durability boundary. Validate structured
|
|
275
|
+
// routing above before this append so malformed machine selectors never
|
|
276
|
+
// enter the transcript or reach a provider.
|
|
277
|
+
let persistedUserClientMessageId = null;
|
|
278
|
+
if (!options.internal) {
|
|
279
|
+
const persistedUser = loaded.conversationStore.append({
|
|
280
|
+
role: 'user',
|
|
281
|
+
content: prompt,
|
|
282
|
+
sessionId,
|
|
283
|
+
threadId: 'main',
|
|
284
|
+
clientMessageId: messageId,
|
|
285
|
+
causalRootId: messageId,
|
|
286
|
+
userAuthored: true,
|
|
287
|
+
});
|
|
288
|
+
persistedUserClientMessageId = persistedUser?.clientMessageId || messageId;
|
|
289
|
+
}
|
|
196
290
|
const report = coordinator.ingest({
|
|
197
291
|
id: messageId,
|
|
198
|
-
from: 'user',
|
|
199
|
-
role: 'user',
|
|
292
|
+
from: options.internal ? 'tool' : 'user',
|
|
293
|
+
role: options.internal ? 'assistant' : 'user',
|
|
200
294
|
text: prompt,
|
|
295
|
+
...(options.internal ? {
|
|
296
|
+
internal: true,
|
|
297
|
+
taskId: options.taskId || null,
|
|
298
|
+
meta: {
|
|
299
|
+
...(options.meta || {}),
|
|
300
|
+
injectedBy: 'task_result',
|
|
301
|
+
...(routingIntent?.targetVpIds?.[0]
|
|
302
|
+
? { routeTargetVpId: routingIntent.targetVpIds[0] }
|
|
303
|
+
: {}),
|
|
304
|
+
},
|
|
305
|
+
} : {}),
|
|
306
|
+
...(routingIntent ? { _routingIntent: routingIntent } : {}),
|
|
307
|
+
...(persistedUserClientMessageId ? { _persistedUserClientMessageId: persistedUserClientMessageId } : {}),
|
|
308
|
+
_cliRootOrder: rootOrder,
|
|
309
|
+
_cliCausalRootId: messageId,
|
|
201
310
|
_cliTurnContext: turnContext,
|
|
202
311
|
});
|
|
203
|
-
const results =
|
|
312
|
+
const results = [];
|
|
313
|
+
let cursor = 0;
|
|
314
|
+
while (cursor < turnContext.tasks.length) {
|
|
315
|
+
const batch = turnContext.tasks.slice(cursor);
|
|
316
|
+
cursor += batch.length;
|
|
317
|
+
results.push(...await Promise.all(batch));
|
|
318
|
+
await Promise.resolve();
|
|
319
|
+
}
|
|
204
320
|
return { report, results };
|
|
205
321
|
},
|
|
206
322
|
abort(reason = 'user') {
|
package/yeaft/cli.js
CHANGED
|
@@ -38,8 +38,17 @@ import { ConversationStore } from './conversation/persist.js';
|
|
|
38
38
|
import { snapshotSessions } from './sessions/session-crud.js';
|
|
39
39
|
import { loadSessionConfig, resolveSessionConfig } from './sessions/session-config.js';
|
|
40
40
|
import { validateSessionId } from './sessions/ids.js';
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
createJsonlWriter,
|
|
43
|
+
JsonlInput,
|
|
44
|
+
normalizeStreamRoutingIntent,
|
|
45
|
+
runStreamTurn,
|
|
46
|
+
runStreamSessionTurn,
|
|
47
|
+
} from './stdio-protocol.js';
|
|
42
48
|
import { createCliSessionRunner } from './cli-session-runner.js';
|
|
49
|
+
import { emitStreamTaskEvent, taskResultReentryContext } from './tasks/result-delivery.js';
|
|
50
|
+
import { TASK_RESULT_DELIVERY, taskResultDeliveryFor } from './tasks/store.js';
|
|
51
|
+
import { buildStreamSubAgentFrame } from './sub-agent/public-event.js';
|
|
43
52
|
import {
|
|
44
53
|
cleanupManagedCliRuntimePaths,
|
|
45
54
|
ensureManagedCliTools,
|
|
@@ -816,6 +825,34 @@ async function runREPL(config, args) {
|
|
|
816
825
|
|
|
817
826
|
// ─── Structured stdio handler ──────────────────────────────────
|
|
818
827
|
|
|
828
|
+
function writeStreamProtocolError({ write, sessionId, error }) {
|
|
829
|
+
const turnId = randomUUID();
|
|
830
|
+
const normalized = error instanceof Error ? error : new Error(String(error || 'Unknown error'));
|
|
831
|
+
write({
|
|
832
|
+
type: 'error',
|
|
833
|
+
session_id: sessionId,
|
|
834
|
+
turn_id: turnId,
|
|
835
|
+
thread_id: 'main',
|
|
836
|
+
threadId: 'main',
|
|
837
|
+
error: { name: normalized.name || 'Error', message: normalized.message || String(normalized) },
|
|
838
|
+
retryable: false,
|
|
839
|
+
});
|
|
840
|
+
const result = {
|
|
841
|
+
type: 'result',
|
|
842
|
+
subtype: 'error',
|
|
843
|
+
session_id: sessionId,
|
|
844
|
+
turn_id: turnId,
|
|
845
|
+
thread_id: 'main',
|
|
846
|
+
threadId: 'main',
|
|
847
|
+
stop_reason: 'error',
|
|
848
|
+
is_error: true,
|
|
849
|
+
result: '',
|
|
850
|
+
error: normalized.message || String(normalized),
|
|
851
|
+
};
|
|
852
|
+
write(result);
|
|
853
|
+
return result;
|
|
854
|
+
}
|
|
855
|
+
|
|
819
856
|
async function runStreamJson(config, args) {
|
|
820
857
|
const sessionId = args.sessionId || `session_cli_${randomUUID()}`;
|
|
821
858
|
const validation = validateSessionId(sessionId);
|
|
@@ -854,8 +891,148 @@ async function runStreamJson(config, args) {
|
|
|
854
891
|
managedCliReady: args.managedCliReady,
|
|
855
892
|
});
|
|
856
893
|
const { engine, conversationStore, skillManager, toolRegistry } = loaded;
|
|
857
|
-
const sessionRunner = createCliSessionRunner({ loaded, sessionId, workDir });
|
|
858
894
|
const todoState = { value: [] };
|
|
895
|
+
const asyncTaskOwners = new Map();
|
|
896
|
+
const rescuedTaskIds = new Set();
|
|
897
|
+
const pendingTaskRescues = new Set();
|
|
898
|
+
const taskLifecycleWaiters = new Set();
|
|
899
|
+
let sessionRunner = null;
|
|
900
|
+
let taskEventIntakeOpen = true;
|
|
901
|
+
let hadError = false;
|
|
902
|
+
let singleEngineTail = Promise.resolve();
|
|
903
|
+
|
|
904
|
+
const loadStreamHistory = () => conversationStore.loadRecentBySession(sessionId, 20).map(message => ({
|
|
905
|
+
role: message.role,
|
|
906
|
+
content: message.content,
|
|
907
|
+
...(message.toolCallId && { toolCallId: message.toolCallId }),
|
|
908
|
+
...(message.toolCalls && { toolCalls: message.toolCalls }),
|
|
909
|
+
}));
|
|
910
|
+
|
|
911
|
+
// An ad-hoc stream-json conversation has exactly one Engine and no VP roster.
|
|
912
|
+
// Serialize both root turns and late task-result rescues on that same stable
|
|
913
|
+
// process owner so a completion cannot race another query or invent a VP id.
|
|
914
|
+
const runSingleEngineTurn = (turnOptions) => {
|
|
915
|
+
const turn = singleEngineTail.catch(() => {}).then(() => runStreamTurn({
|
|
916
|
+
engine,
|
|
917
|
+
...turnOptions,
|
|
918
|
+
}));
|
|
919
|
+
singleEngineTail = turn;
|
|
920
|
+
return turn;
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const wakeTaskLifecycleWaiters = () => {
|
|
924
|
+
if (taskLifecycleWaiters.size === 0) return;
|
|
925
|
+
const waiters = Array.from(taskLifecycleWaiters);
|
|
926
|
+
taskLifecycleWaiters.clear();
|
|
927
|
+
for (const resolveWaiter of waiters) resolveWaiter();
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
const rescueTaskResult = async (context) => {
|
|
931
|
+
const taskId = context?.task?.id;
|
|
932
|
+
if (!taskId || !context?.content || rescuedTaskIds.has(taskId)) return false;
|
|
933
|
+
if (sessionRunner && !context.vpId) return false;
|
|
934
|
+
rescuedTaskIds.add(taskId);
|
|
935
|
+
try {
|
|
936
|
+
const common = {
|
|
937
|
+
prompt: context.content,
|
|
938
|
+
sessionId,
|
|
939
|
+
workDir,
|
|
940
|
+
model: loaded.config.model,
|
|
941
|
+
modelEffort: loaded.config.modelEffort || null,
|
|
942
|
+
input,
|
|
943
|
+
write,
|
|
944
|
+
taskId,
|
|
945
|
+
};
|
|
946
|
+
const result = sessionRunner
|
|
947
|
+
? await runStreamSessionTurn({
|
|
948
|
+
runner: sessionRunner,
|
|
949
|
+
...common,
|
|
950
|
+
internal: true,
|
|
951
|
+
meta: {
|
|
952
|
+
injectedBy: 'task_result',
|
|
953
|
+
routeTargetVpId: context.vpId,
|
|
954
|
+
threadId: context.threadId || 'main',
|
|
955
|
+
},
|
|
956
|
+
routingIntent: { targetVpIds: [context.vpId], explicit: true },
|
|
957
|
+
})
|
|
958
|
+
: await runSingleEngineTurn({
|
|
959
|
+
...common,
|
|
960
|
+
messages: loadStreamHistory(),
|
|
961
|
+
threadId: context.threadId || 'main',
|
|
962
|
+
userAlreadyPersisted: true,
|
|
963
|
+
});
|
|
964
|
+
hadError ||= result?.is_error === true;
|
|
965
|
+
return true;
|
|
966
|
+
} catch (error) {
|
|
967
|
+
hadError = true;
|
|
968
|
+
writeStreamProtocolError({ write, sessionId, error });
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
const queueTaskRescue = (context) => {
|
|
974
|
+
if (!taskEventIntakeOpen) return null;
|
|
975
|
+
let rescue;
|
|
976
|
+
rescue = rescueTaskResult(context).finally(() => {
|
|
977
|
+
pendingTaskRescues.delete(rescue);
|
|
978
|
+
wakeTaskLifecycleWaiters();
|
|
979
|
+
});
|
|
980
|
+
pendingTaskRescues.add(rescue);
|
|
981
|
+
wakeTaskLifecycleWaiters();
|
|
982
|
+
return rescue;
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
const configureStreamEngine = (targetEngine, vpId = null) => {
|
|
986
|
+
if (!targetEngine) return;
|
|
987
|
+
if (typeof targetEngine.setAsyncTaskCoordinator === 'function') {
|
|
988
|
+
const removeOwner = (taskId, ownerEngine) => {
|
|
989
|
+
const owner = asyncTaskOwners.get(taskId);
|
|
990
|
+
if (!owner || owner.engine !== ownerEngine) return null;
|
|
991
|
+
asyncTaskOwners.delete(taskId);
|
|
992
|
+
return owner;
|
|
993
|
+
};
|
|
994
|
+
targetEngine.setAsyncTaskCoordinator({
|
|
995
|
+
onRegister(taskId, ownerEngine) {
|
|
996
|
+
if (!taskId) return;
|
|
997
|
+
asyncTaskOwners.set(taskId, {
|
|
998
|
+
engine: ownerEngine,
|
|
999
|
+
sessionId,
|
|
1000
|
+
vpId: vpId || null,
|
|
1001
|
+
threadId: ownerEngine.currentThreadId || 'main',
|
|
1002
|
+
});
|
|
1003
|
+
},
|
|
1004
|
+
onUnregister: removeOwner,
|
|
1005
|
+
onConsumed: removeOwner,
|
|
1006
|
+
onDeferred(taskId, ownerEngine) {
|
|
1007
|
+
const owner = removeOwner(taskId, ownerEngine);
|
|
1008
|
+
const task = loaded.taskManager?.getTask?.(sessionId, taskId);
|
|
1009
|
+
if (!owner || !task || !['succeeded', 'failed', 'cancelled', 'orphaned'].includes(task.status)) return;
|
|
1010
|
+
const context = taskResultReentryContext({ event: 'completed', task }, { sessionId, owner });
|
|
1011
|
+
if (context) queueTaskRescue(context);
|
|
1012
|
+
},
|
|
1013
|
+
onUndelivered(taskId, delivery, ownerEngine) {
|
|
1014
|
+
const owner = removeOwner(taskId, ownerEngine);
|
|
1015
|
+
if (!owner || rescuedTaskIds.has(taskId)) return;
|
|
1016
|
+
queueTaskRescue({
|
|
1017
|
+
task: { id: taskId, kind: delivery?.taskKind, status: delivery?.taskStatus },
|
|
1018
|
+
sessionId: delivery?.sessionId || owner.sessionId,
|
|
1019
|
+
vpId: delivery?.vpId || owner.vpId,
|
|
1020
|
+
threadId: delivery?.threadId || owner.threadId,
|
|
1021
|
+
content: delivery?.content,
|
|
1022
|
+
});
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
if (typeof targetEngine.setSubAgentEventSink === 'function') {
|
|
1027
|
+
targetEngine.setSubAgentEventSink((agentId, event) => {
|
|
1028
|
+
const frame = buildStreamSubAgentFrame({ event, agentId, sessionId, vpId, threadId: targetEngine.currentThreadId });
|
|
1029
|
+
if (frame) write(frame);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
|
|
1034
|
+
configureStreamEngine(engine, null);
|
|
1035
|
+
sessionRunner = createCliSessionRunner({ loaded, sessionId, workDir, configureEngine: configureStreamEngine });
|
|
859
1036
|
|
|
860
1037
|
write({
|
|
861
1038
|
type: 'system',
|
|
@@ -870,16 +1047,57 @@ async function runStreamJson(config, args) {
|
|
|
870
1047
|
output_format: 'stream-json',
|
|
871
1048
|
});
|
|
872
1049
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1050
|
+
if (loaded.taskManager && typeof loaded.taskManager.setEventSink === 'function') {
|
|
1051
|
+
loaded.taskManager.setEventSink((event) => {
|
|
1052
|
+
if (!taskEventIntakeOpen) return;
|
|
1053
|
+
const hadExactOwner = asyncTaskOwners.has(event?.task?.id);
|
|
1054
|
+
const delivery = emitStreamTaskEvent({ event, asyncTaskOwners, write, sessionId });
|
|
1055
|
+
if (delivery.projected && !delivery.delivered && !hadExactOwner && event?.event === 'completed') {
|
|
1056
|
+
const context = taskResultReentryContext(event, { sessionId });
|
|
1057
|
+
if (context) queueTaskRescue(context);
|
|
1058
|
+
}
|
|
1059
|
+
wakeTaskLifecycleWaiters();
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const activeModelReentryTasks = () => {
|
|
1064
|
+
if (!loaded.taskManager || typeof loaded.taskManager.listActiveTasks !== 'function') return [];
|
|
1065
|
+
return loaded.taskManager.listActiveTasks(sessionId).filter(task => (
|
|
1066
|
+
taskResultDeliveryFor(task) === TASK_RESULT_DELIVERY.MODEL_REENTRY
|
|
1067
|
+
));
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
const drainTaskLifecycle = async () => {
|
|
1071
|
+
if (!loaded.taskManager || typeof loaded.taskManager.setEventSink !== 'function') {
|
|
1072
|
+
taskEventIntakeOpen = false;
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
// EOF is a lifecycle fence, not permission to abandon model-reentry work.
|
|
1076
|
+
// Keep the Session runner and task event sink alive until every owned
|
|
1077
|
+
// result-producing task is terminal and every resulting rescue turn has
|
|
1078
|
+
// settled. `status_only` tasks remain detached and never hold CLI exit.
|
|
1079
|
+
for (;;) {
|
|
1080
|
+
if (activeModelReentryTasks().length === 0 && pendingTaskRescues.size === 0) {
|
|
1081
|
+
// JavaScript runs this check + close synchronously. No TaskManager
|
|
1082
|
+
// completion can interleave between observing the empty sets and
|
|
1083
|
+
// detaching the sink, so no late rescue can target a closed runner.
|
|
1084
|
+
taskEventIntakeOpen = false;
|
|
1085
|
+
loaded.taskManager.setEventSink(null);
|
|
1086
|
+
wakeTaskLifecycleWaiters();
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
await new Promise(resolveWaiter => taskLifecycleWaiters.add(resolveWaiter));
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
const runPrompt = async (prompt, message = null) => {
|
|
1094
|
+
const routingIntent = normalizeStreamRoutingIntent(message);
|
|
1095
|
+
if (routingIntent && !sessionRunner) {
|
|
1096
|
+
throw new Error('stream-json VP selectors require an existing formal Session with a persisted roster');
|
|
1097
|
+
}
|
|
880
1098
|
const turnOptions = {
|
|
881
1099
|
prompt,
|
|
882
|
-
messages:
|
|
1100
|
+
messages: loadStreamHistory(),
|
|
883
1101
|
sessionId,
|
|
884
1102
|
workDir,
|
|
885
1103
|
model: loaded.config.model,
|
|
@@ -890,31 +1108,45 @@ async function runStreamJson(config, args) {
|
|
|
890
1108
|
setCurrentTodos: todos => { todoState.value = Array.isArray(todos) ? todos.slice() : []; },
|
|
891
1109
|
};
|
|
892
1110
|
return sessionRunner
|
|
893
|
-
? runStreamSessionTurn({ runner: sessionRunner, ...turnOptions })
|
|
894
|
-
:
|
|
1111
|
+
? runStreamSessionTurn({ runner: sessionRunner, routingIntent, ...turnOptions })
|
|
1112
|
+
: runSingleEngineTurn(turnOptions);
|
|
895
1113
|
};
|
|
896
1114
|
|
|
897
|
-
let hadError = false;
|
|
898
1115
|
const recordResult = (result) => {
|
|
899
1116
|
hadError ||= result?.is_error === true;
|
|
900
1117
|
return result;
|
|
901
1118
|
};
|
|
902
1119
|
try {
|
|
903
1120
|
if (args.prompt) {
|
|
904
|
-
|
|
1121
|
+
try {
|
|
1122
|
+
recordResult(await runPrompt(args.prompt));
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
recordResult(writeStreamProtocolError({ write, sessionId, error }));
|
|
1125
|
+
}
|
|
905
1126
|
} else if (input) {
|
|
906
1127
|
for (;;) {
|
|
907
1128
|
const item = await input.nextPrompt();
|
|
908
1129
|
if (!item) break;
|
|
909
|
-
|
|
1130
|
+
try {
|
|
1131
|
+
recordResult(await runPrompt(item.prompt, item.message));
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
recordResult(writeStreamProtocolError({ write, sessionId, error }));
|
|
1134
|
+
}
|
|
910
1135
|
}
|
|
911
1136
|
} else {
|
|
912
1137
|
let prompt = '';
|
|
913
1138
|
for await (const chunk of process.stdin) prompt += chunk;
|
|
914
|
-
if (prompt.trim())
|
|
1139
|
+
if (prompt.trim()) {
|
|
1140
|
+
try {
|
|
1141
|
+
recordResult(await runPrompt(prompt.trim()));
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
recordResult(writeStreamProtocolError({ write, sessionId, error }));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
915
1146
|
}
|
|
916
1147
|
} finally {
|
|
917
1148
|
input?.close();
|
|
1149
|
+
await drainTaskLifecycle();
|
|
918
1150
|
await sessionRunner?.close();
|
|
919
1151
|
await loaded.shutdown();
|
|
920
1152
|
console.log = originalConsole.log;
|
|
@@ -434,6 +434,7 @@ function serializeMessage(msg) {
|
|
|
434
434
|
// Defaults to 'main' for legacy messages (see migrate-messages-threadid.js).
|
|
435
435
|
fm.push(`threadId: ${msg.threadId || 'main'}`);
|
|
436
436
|
if (msg.turnId) fm.push(`turnId: ${msg.turnId}`);
|
|
437
|
+
if (msg.causalRootId) fm.push(`causalRootId: ${msg.causalRootId}`);
|
|
437
438
|
if (msg.executionOrigin === 'route_forward') fm.push('executionOrigin: route_forward');
|
|
438
439
|
if (msg.imageAssetAnchor) fm.push('imageAssetAnchor: true');
|
|
439
440
|
// task-313: when a thread is merged into another, the messages keep
|
|
@@ -581,6 +582,7 @@ export function parseMessage(raw) {
|
|
|
581
582
|
case 'tokens_est': msg.tokens_est = parseInt(value, 10); break;
|
|
582
583
|
case 'threadId': msg.threadId = value; break;
|
|
583
584
|
case 'turnId': msg.turnId = value; break;
|
|
585
|
+
case 'causalRootId': msg.causalRootId = value; break;
|
|
584
586
|
case 'executionOrigin':
|
|
585
587
|
if (value === 'route_forward') msg.executionOrigin = value;
|
|
586
588
|
break;
|