@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
package/yeaft/stdio-protocol.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
|
|
4
|
-
const TERMINAL_STOP_REASONS = new Set([
|
|
4
|
+
const TERMINAL_STOP_REASONS = new Set([
|
|
5
|
+
'end_turn', 'max_tokens', 'stop_sequence', 'aborted', 'error', 'tool_handoff', 'plan_recorded',
|
|
6
|
+
]);
|
|
5
7
|
|
|
6
8
|
function snakeUsage(usage) {
|
|
7
9
|
const inputTokens = usage.inputTokens || 0;
|
|
@@ -32,10 +34,52 @@ export function extractPrompt(message) {
|
|
|
32
34
|
.join('');
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Parse per-turn VP selectors without reading or mutating formal Session
|
|
39
|
+
* metadata. Membership is validated later by the Session runner against its
|
|
40
|
+
* canonical persisted roster.
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeStreamRoutingIntent(message) {
|
|
43
|
+
if (!message || typeof message !== 'object') return null;
|
|
44
|
+
const hasSelector = Object.hasOwn(message, 'targetVps')
|
|
45
|
+
|| Object.hasOwn(message, 'targets')
|
|
46
|
+
|| Object.hasOwn(message, 'targetVpId')
|
|
47
|
+
|| Object.hasOwn(message, 'broadcast');
|
|
48
|
+
if (!hasSelector) return null;
|
|
49
|
+
|
|
50
|
+
const rawTargets = [];
|
|
51
|
+
for (const key of ['targetVps', 'targets']) {
|
|
52
|
+
if (!Object.hasOwn(message, key)) continue;
|
|
53
|
+
if (!Array.isArray(message[key])) {
|
|
54
|
+
throw new Error(`stream-json ${key} must be an array`);
|
|
55
|
+
}
|
|
56
|
+
rawTargets.push(...message[key]);
|
|
57
|
+
}
|
|
58
|
+
if (Object.hasOwn(message, 'targetVpId')) rawTargets.push(message.targetVpId);
|
|
59
|
+
|
|
60
|
+
const targetVpIds = [];
|
|
61
|
+
let broadcast = message.broadcast === true;
|
|
62
|
+
for (const rawTarget of rawTargets) {
|
|
63
|
+
if (typeof rawTarget !== 'string' || !rawTarget.trim()) {
|
|
64
|
+
throw new Error('stream-json target VP must be a non-empty string');
|
|
65
|
+
}
|
|
66
|
+
const target = rawTarget.trim();
|
|
67
|
+
if (target === 'all' || target === 'everyone') {
|
|
68
|
+
broadcast = true;
|
|
69
|
+
} else if (!targetVpIds.includes(target)) {
|
|
70
|
+
targetVpIds.push(target);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
targetVpIds: Object.freeze(targetVpIds),
|
|
76
|
+
broadcast,
|
|
77
|
+
explicit: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
35
81
|
export function createJsonlWriter(output = process.stdout) {
|
|
36
|
-
return
|
|
37
|
-
output.write(`${JSON.stringify(event)}\n`);
|
|
38
|
-
};
|
|
82
|
+
return event => { output.write(`${JSON.stringify(event)}\n`); };
|
|
39
83
|
}
|
|
40
84
|
|
|
41
85
|
export class JsonlInput {
|
|
@@ -126,335 +170,414 @@ export class JsonlInput {
|
|
|
126
170
|
if (this.#answerRequestIds.has(requestId)) {
|
|
127
171
|
throw new Error(`Duplicate AskUser request_id: ${requestId}`);
|
|
128
172
|
}
|
|
129
|
-
if (this.#error) throw this.#error;
|
|
130
173
|
if (this.#closed) throw new Error('stdin closed before AskUser response');
|
|
131
174
|
this.#answerRequestIds.add(requestId);
|
|
132
175
|
return new Promise((resolve, reject) => this.#answerWaiters.set(requestId, { resolve, reject }));
|
|
133
176
|
}
|
|
134
177
|
|
|
135
|
-
close() {
|
|
136
|
-
this.#rl.close();
|
|
137
|
-
}
|
|
178
|
+
close() { this.#rl.close(); }
|
|
138
179
|
}
|
|
139
180
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
modelEffort = null,
|
|
148
|
-
input = null,
|
|
149
|
-
write,
|
|
150
|
-
getCurrentTodos = null,
|
|
151
|
-
setCurrentTodos = null,
|
|
152
|
-
}) {
|
|
153
|
-
const clientTurnId = randomUUID();
|
|
154
|
-
let engineTurnId = null;
|
|
155
|
-
let resultText = '';
|
|
156
|
-
let stopReason = 'end_turn';
|
|
157
|
-
let failed = null;
|
|
158
|
-
const usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, cacheInputDeltaTokens: 0 };
|
|
159
|
-
|
|
160
|
-
const askUser = async ({ question, options }) => {
|
|
161
|
-
const requestId = randomUUID();
|
|
162
|
-
write({
|
|
163
|
-
type: 'ask_user',
|
|
164
|
-
subtype: 'request',
|
|
165
|
-
request_id: requestId,
|
|
166
|
-
session_id: sessionId,
|
|
167
|
-
turn_id: engineTurnId || clientTurnId,
|
|
168
|
-
question,
|
|
169
|
-
options: Array.isArray(options) ? options : [],
|
|
170
|
-
});
|
|
171
|
-
if (!input) throw new Error('AskUser requires --input-format stream-json');
|
|
172
|
-
const answers = await input.waitForAnswer(requestId);
|
|
173
|
-
write({
|
|
174
|
-
type: 'ask_user',
|
|
175
|
-
subtype: 'response',
|
|
176
|
-
request_id: requestId,
|
|
177
|
-
session_id: sessionId,
|
|
178
|
-
turn_id: engineTurnId || clientTurnId,
|
|
179
|
-
answers,
|
|
180
|
-
});
|
|
181
|
-
return answers;
|
|
181
|
+
function makeUsageState() {
|
|
182
|
+
return {
|
|
183
|
+
inputTokens: 0,
|
|
184
|
+
outputTokens: 0,
|
|
185
|
+
cacheReadTokens: 0,
|
|
186
|
+
cacheWriteTokens: 0,
|
|
187
|
+
cacheInputDeltaTokens: 0,
|
|
182
188
|
};
|
|
189
|
+
}
|
|
183
190
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
getCurrentTodos,
|
|
193
|
-
setCurrentTodos,
|
|
194
|
-
})) {
|
|
195
|
-
if (event.type === 'turn_open') engineTurnId = event.turnId;
|
|
196
|
-
const turnId = engineTurnId || clientTurnId;
|
|
197
|
-
switch (event.type) {
|
|
198
|
-
case 'turn_open':
|
|
199
|
-
write({ ...event, type: 'turn', subtype: 'start', session_id: sessionId, turn_id: event.turnId, model, model_effort: modelEffort, cwd: workDir });
|
|
200
|
-
break;
|
|
201
|
-
case 'text_delta':
|
|
202
|
-
resultText += event.text || '';
|
|
203
|
-
write({ type: 'assistant', subtype: 'text_delta', session_id: sessionId, turn_id: turnId, delta: { type: 'text_delta', text: event.text || '' } });
|
|
204
|
-
break;
|
|
205
|
-
case 'thinking_delta':
|
|
206
|
-
write({ type: 'assistant', subtype: 'thinking_delta', session_id: sessionId, turn_id: turnId, delta: { type: 'thinking_delta', thinking: event.text || '' } });
|
|
207
|
-
break;
|
|
208
|
-
case 'skill_loaded':
|
|
209
|
-
write({ type: 'skill', subtype: 'loaded', session_id: sessionId, turn_id: turnId, skill: event.skill });
|
|
210
|
-
break;
|
|
211
|
-
case 'skill_error':
|
|
212
|
-
write({ type: 'skill', subtype: 'error', session_id: sessionId, turn_id: turnId, skill_name: event.skillName, error: event.message });
|
|
213
|
-
break;
|
|
214
|
-
case 'tool_call':
|
|
215
|
-
write({ type: 'assistant', subtype: 'tool_use', session_id: sessionId, turn_id: turnId, content: [{ type: 'tool_use', id: event.id, name: event.name, input: event.input }] });
|
|
216
|
-
if (event.name === 'TodoWrite') {
|
|
217
|
-
write({ type: 'todo', subtype: 'update', session_id: sessionId, turn_id: turnId, tool_use_id: event.id, todos: event.input?.todos || [] });
|
|
218
|
-
}
|
|
219
|
-
break;
|
|
220
|
-
case 'tool_start':
|
|
221
|
-
write({ type: 'tool', subtype: 'start', session_id: sessionId, turn_id: turnId, tool_use_id: event.id, name: event.name, input: event.input });
|
|
222
|
-
break;
|
|
223
|
-
case 'tool_end':
|
|
224
|
-
write({
|
|
225
|
-
type: 'tool',
|
|
226
|
-
subtype: 'result',
|
|
227
|
-
session_id: sessionId,
|
|
228
|
-
turn_id: turnId,
|
|
229
|
-
tool_use_id: event.id,
|
|
230
|
-
name: event.name,
|
|
231
|
-
content: event.output,
|
|
232
|
-
is_error: !!event.isError,
|
|
233
|
-
...(Array.isArray(event.displayImages) && event.displayImages.length > 0
|
|
234
|
-
? { display_images: event.displayImages }
|
|
235
|
-
: {}),
|
|
236
|
-
});
|
|
237
|
-
break;
|
|
238
|
-
case 'usage': {
|
|
239
|
-
const cacheDelta = event.cacheTokensAreIncludedInInput ? 0 : (event.cacheReadTokens || 0) + (event.cacheWriteTokens || 0);
|
|
240
|
-
usage.inputTokens += event.inputTokens || 0;
|
|
241
|
-
usage.outputTokens += event.outputTokens || 0;
|
|
242
|
-
usage.cacheReadTokens += event.cacheReadTokens || 0;
|
|
243
|
-
usage.cacheWriteTokens += event.cacheWriteTokens || 0;
|
|
244
|
-
usage.cacheInputDeltaTokens += cacheDelta;
|
|
245
|
-
write({ type: 'usage', session_id: sessionId, turn_id: turnId, usage: snakeUsage(event) });
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
248
|
-
case 'stop':
|
|
249
|
-
if (event.stopReason) stopReason = event.stopReason;
|
|
250
|
-
break;
|
|
251
|
-
case 'turn_end':
|
|
252
|
-
if (event.stopReason && TERMINAL_STOP_REASONS.has(event.stopReason)) stopReason = event.stopReason;
|
|
253
|
-
break;
|
|
254
|
-
case 'turn_close':
|
|
255
|
-
write({ type: 'turn', subtype: 'stop', session_id: sessionId, turn_id: turnId, duration_ms: event.totalMs, loop_count: event.loopCount, total_tokens: event.totalTokens });
|
|
256
|
-
break;
|
|
257
|
-
case 'error':
|
|
258
|
-
failed = event.error instanceof Error ? event.error : new Error(event.error?.message || String(event.error || 'Unknown error'));
|
|
259
|
-
write({ type: 'error', session_id: sessionId, turn_id: turnId, error: { name: failed.name, message: failed.message }, retryable: !!event.retryable });
|
|
260
|
-
break;
|
|
261
|
-
case 'fallback':
|
|
262
|
-
case 'llm_retry':
|
|
263
|
-
case 'memory_used':
|
|
264
|
-
case 'recall':
|
|
265
|
-
case 'consolidate':
|
|
266
|
-
case 'reflection':
|
|
267
|
-
write({ ...event, session_id: sessionId, turn_id: turnId });
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
} catch (err) {
|
|
272
|
-
failed = err;
|
|
273
|
-
write({ type: 'error', session_id: sessionId, turn_id: engineTurnId || clientTurnId, error: { name: err.name || 'Error', message: err.message || String(err) } });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const finalUsage = {
|
|
277
|
-
input_tokens: usage.inputTokens,
|
|
278
|
-
output_tokens: usage.outputTokens,
|
|
279
|
-
cache_read_input_tokens: usage.cacheReadTokens,
|
|
280
|
-
cache_creation_input_tokens: usage.cacheWriteTokens,
|
|
281
|
-
total_input_tokens: usage.inputTokens + usage.cacheInputDeltaTokens,
|
|
282
|
-
total_tokens: usage.inputTokens + usage.cacheInputDeltaTokens + usage.outputTokens,
|
|
283
|
-
};
|
|
284
|
-
const result = {
|
|
285
|
-
type: 'result',
|
|
286
|
-
subtype: failed ? 'error' : 'success',
|
|
287
|
-
session_id: sessionId,
|
|
288
|
-
turn_id: engineTurnId || clientTurnId,
|
|
289
|
-
model,
|
|
290
|
-
model_effort: modelEffort,
|
|
291
|
-
stop_reason: failed ? 'error' : stopReason,
|
|
292
|
-
is_error: !!failed,
|
|
293
|
-
result: resultText,
|
|
294
|
-
usage: finalUsage,
|
|
295
|
-
...(failed ? { error: failed.message || String(failed) } : {}),
|
|
191
|
+
function finalUsage(state) {
|
|
192
|
+
return {
|
|
193
|
+
input_tokens: state.inputTokens,
|
|
194
|
+
output_tokens: state.outputTokens,
|
|
195
|
+
cache_read_input_tokens: state.cacheReadTokens,
|
|
196
|
+
cache_creation_input_tokens: state.cacheWriteTokens,
|
|
197
|
+
total_input_tokens: state.inputTokens + state.cacheInputDeltaTokens,
|
|
198
|
+
total_tokens: state.inputTokens + state.cacheInputDeltaTokens + state.outputTokens,
|
|
296
199
|
};
|
|
297
|
-
write(result);
|
|
298
|
-
return result;
|
|
299
200
|
}
|
|
300
201
|
|
|
301
202
|
/**
|
|
302
|
-
*
|
|
303
|
-
* the stream-json shape used by runStreamTurn and add `vp_id`, allowing
|
|
304
|
-
* concurrent VP streams to be demultiplexed without imposing output order.
|
|
203
|
+
* One Engine-event projector shared by single- and multi-VP stream-json paths.
|
|
305
204
|
*/
|
|
306
|
-
export
|
|
307
|
-
runner,
|
|
308
|
-
prompt,
|
|
205
|
+
export function createStreamProjector({
|
|
309
206
|
sessionId,
|
|
207
|
+
write,
|
|
310
208
|
workDir = process.cwd(),
|
|
311
209
|
model = null,
|
|
312
210
|
modelEffort = null,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
211
|
+
defaultVpId = null,
|
|
212
|
+
defaultThreadId = 'main',
|
|
213
|
+
clientTurnId = randomUUID(),
|
|
214
|
+
} = {}) {
|
|
317
215
|
const states = new Map();
|
|
318
216
|
let failed = null;
|
|
319
|
-
|
|
320
|
-
const stateFor = (vpId) => {
|
|
321
|
-
|
|
217
|
+
const keyFor = vpId => vpId || '__single__';
|
|
218
|
+
const stateFor = (vpId = null) => {
|
|
219
|
+
const key = keyFor(vpId);
|
|
220
|
+
let state = states.get(key);
|
|
322
221
|
if (!state) {
|
|
323
222
|
state = {
|
|
223
|
+
vpId: vpId || defaultVpId || null,
|
|
224
|
+
threadId: defaultThreadId || 'main',
|
|
324
225
|
turnId: clientTurnId,
|
|
325
226
|
resultText: '',
|
|
326
227
|
stopReason: 'end_turn',
|
|
327
|
-
usage:
|
|
228
|
+
usage: makeUsageState(),
|
|
229
|
+
asyncWaitPendingByLoop: new Map(),
|
|
328
230
|
};
|
|
329
|
-
states.set(
|
|
231
|
+
states.set(key, state);
|
|
232
|
+
} else if (vpId && !state.vpId) {
|
|
233
|
+
state.vpId = vpId;
|
|
330
234
|
}
|
|
331
235
|
return state;
|
|
332
236
|
};
|
|
237
|
+
const scopeFor = state => ({
|
|
238
|
+
session_id: sessionId,
|
|
239
|
+
turn_id: state.turnId || clientTurnId,
|
|
240
|
+
...(state.vpId ? { vp_id: state.vpId, vpId: state.vpId } : {}),
|
|
241
|
+
thread_id: state.threadId || 'main',
|
|
242
|
+
threadId: state.threadId || 'main',
|
|
243
|
+
});
|
|
333
244
|
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
write({
|
|
337
|
-
type: 'ask_user', subtype: 'request', request_id: requestId,
|
|
338
|
-
session_id: sessionId, turn_id: turnId, vp_id: vpId,
|
|
339
|
-
question, options: Array.isArray(options) ? options : [],
|
|
340
|
-
});
|
|
341
|
-
if (!input) throw new Error('AskUser requires --input-format stream-json');
|
|
342
|
-
const answers = await input.waitForAnswer(requestId);
|
|
343
|
-
write({
|
|
344
|
-
type: 'ask_user', subtype: 'response', request_id: requestId,
|
|
345
|
-
session_id: sessionId, turn_id: turnId, vp_id: vpId, answers,
|
|
346
|
-
});
|
|
347
|
-
return answers;
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const onEvent = async ({ vpId, event, turnId: fallbackTurnId }) => {
|
|
245
|
+
const project = ({ vpId = null, event, turnId: fallbackTurnId = null } = {}) => {
|
|
246
|
+
if (!event || typeof event !== 'object') return;
|
|
351
247
|
const state = stateFor(vpId);
|
|
352
|
-
if (event.type === 'turn_open'
|
|
353
|
-
|
|
354
|
-
|
|
248
|
+
if (event.type === 'turn_open') {
|
|
249
|
+
if (event.turnId) state.turnId = event.turnId;
|
|
250
|
+
if (event.vpId) state.vpId = event.vpId;
|
|
251
|
+
if (event.threadId) state.threadId = event.threadId;
|
|
252
|
+
} else if (event.threadId) {
|
|
253
|
+
state.threadId = event.threadId;
|
|
254
|
+
}
|
|
255
|
+
if (!state.turnId && fallbackTurnId) state.turnId = fallbackTurnId;
|
|
256
|
+
const base = scopeFor(state);
|
|
355
257
|
switch (event.type) {
|
|
356
258
|
case 'turn_open':
|
|
357
|
-
write({ ...event, type: 'turn', subtype: 'start',
|
|
259
|
+
write({ ...event, ...base, type: 'turn', subtype: 'start', model, model_effort: modelEffort, cwd: workDir });
|
|
358
260
|
break;
|
|
359
261
|
case 'text_delta':
|
|
360
262
|
state.resultText += event.text || '';
|
|
361
|
-
write({ type: 'assistant', subtype: 'text_delta',
|
|
263
|
+
write({ ...base, type: 'assistant', subtype: 'text_delta', delta: { type: 'text_delta', text: event.text || '' } });
|
|
362
264
|
break;
|
|
363
265
|
case 'thinking_delta':
|
|
364
|
-
write({ type: 'assistant', subtype: 'thinking_delta',
|
|
266
|
+
write({ ...base, type: 'assistant', subtype: 'thinking_delta', delta: { type: 'thinking_delta', thinking: event.text || '' } });
|
|
365
267
|
break;
|
|
366
268
|
case 'skill_loaded':
|
|
367
|
-
write({ type: 'skill', subtype: 'loaded',
|
|
269
|
+
write({ ...base, type: 'skill', subtype: 'loaded', skill: event.skill });
|
|
368
270
|
break;
|
|
369
271
|
case 'skill_error':
|
|
370
|
-
write({ type: 'skill', subtype: 'error',
|
|
272
|
+
write({ ...base, type: 'skill', subtype: 'error', skill_name: event.skillName, error: event.message });
|
|
371
273
|
break;
|
|
372
274
|
case 'tool_call':
|
|
373
|
-
write({ type: 'assistant', subtype: 'tool_use',
|
|
275
|
+
write({ ...base, type: 'assistant', subtype: 'tool_use', content: [{ type: 'tool_use', id: event.id, name: event.name, input: event.input }] });
|
|
374
276
|
if (event.name === 'TodoWrite') {
|
|
375
|
-
write({ type: 'todo', subtype: 'update',
|
|
277
|
+
write({ ...base, type: 'todo', subtype: 'update', tool_use_id: event.id, todos: event.input?.todos || [] });
|
|
376
278
|
}
|
|
377
279
|
break;
|
|
378
280
|
case 'tool_start':
|
|
379
|
-
write({ type: 'tool', subtype: 'start',
|
|
281
|
+
write({ ...base, type: 'tool', subtype: 'start', tool_use_id: event.id, name: event.name, input: event.input });
|
|
380
282
|
break;
|
|
381
283
|
case 'tool_end':
|
|
382
|
-
write({
|
|
284
|
+
write({
|
|
285
|
+
...base,
|
|
286
|
+
type: 'tool',
|
|
287
|
+
subtype: 'result',
|
|
288
|
+
tool_use_id: event.id,
|
|
289
|
+
name: event.name,
|
|
290
|
+
content: event.output,
|
|
291
|
+
is_error: !!event.isError,
|
|
292
|
+
...(Array.isArray(event.displayImages) && event.displayImages.length > 0
|
|
293
|
+
? { display_images: event.displayImages }
|
|
294
|
+
: {}),
|
|
295
|
+
});
|
|
383
296
|
break;
|
|
384
297
|
case 'usage': {
|
|
385
|
-
const cacheDelta = event.cacheTokensAreIncludedInInput
|
|
298
|
+
const cacheDelta = event.cacheTokensAreIncludedInInput
|
|
299
|
+
? 0
|
|
300
|
+
: (event.cacheReadTokens || 0) + (event.cacheWriteTokens || 0);
|
|
386
301
|
state.usage.inputTokens += event.inputTokens || 0;
|
|
387
302
|
state.usage.outputTokens += event.outputTokens || 0;
|
|
388
303
|
state.usage.cacheReadTokens += event.cacheReadTokens || 0;
|
|
389
304
|
state.usage.cacheWriteTokens += event.cacheWriteTokens || 0;
|
|
390
305
|
state.usage.cacheInputDeltaTokens += cacheDelta;
|
|
391
|
-
write({ type: 'usage',
|
|
306
|
+
write({ ...base, type: 'usage', usage: snakeUsage(event) });
|
|
392
307
|
break;
|
|
393
308
|
}
|
|
309
|
+
case 'async_task_wait_start': {
|
|
310
|
+
const loopKey = event.loopNumber ?? state.turnId;
|
|
311
|
+
const pendingTaskIds = Array.isArray(event.pendingTaskIds) ? event.pendingTaskIds.slice() : [];
|
|
312
|
+
state.asyncWaitPendingByLoop.set(loopKey, pendingTaskIds);
|
|
313
|
+
write({
|
|
314
|
+
...base,
|
|
315
|
+
type: 'async_task',
|
|
316
|
+
subtype: 'wait_start',
|
|
317
|
+
loop_number: event.loopNumber,
|
|
318
|
+
pending_task_ids: pendingTaskIds,
|
|
319
|
+
});
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case 'async_task_wait_end': {
|
|
323
|
+
const loopKey = event.loopNumber ?? state.turnId;
|
|
324
|
+
const pendingTaskIds = state.asyncWaitPendingByLoop.get(loopKey) || [];
|
|
325
|
+
write({
|
|
326
|
+
...base,
|
|
327
|
+
type: 'async_task',
|
|
328
|
+
subtype: 'wait_end',
|
|
329
|
+
loop_number: event.loopNumber,
|
|
330
|
+
aborted: !!event.aborted,
|
|
331
|
+
timed_out: !!event.timedOut,
|
|
332
|
+
pending_task_ids: pendingTaskIds,
|
|
333
|
+
remaining_task_ids: Array.isArray(event.remainingTaskIds) ? event.remainingTaskIds.slice() : [],
|
|
334
|
+
deferred_task_ids: Array.isArray(event.deferredTaskIds) ? event.deferredTaskIds.slice() : [],
|
|
335
|
+
});
|
|
336
|
+
state.asyncWaitPendingByLoop.delete(loopKey);
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case 'aborted':
|
|
340
|
+
state.stopReason = 'aborted';
|
|
341
|
+
write({ ...base, type: 'aborted', reason: event.reason || 'external' });
|
|
342
|
+
break;
|
|
394
343
|
case 'stop':
|
|
344
|
+
if (event.stopReason) state.stopReason = event.stopReason;
|
|
345
|
+
break;
|
|
395
346
|
case 'turn_end':
|
|
396
|
-
if (event.stopReason && TERMINAL_STOP_REASONS.has(event.stopReason))
|
|
347
|
+
if (event.stopReason && (event.terminal || TERMINAL_STOP_REASONS.has(event.stopReason))) {
|
|
348
|
+
state.stopReason = event.stopReason;
|
|
349
|
+
}
|
|
397
350
|
break;
|
|
398
351
|
case 'turn_close':
|
|
399
|
-
write({ type: 'turn', subtype: 'stop',
|
|
352
|
+
write({ ...base, type: 'turn', subtype: 'stop', duration_ms: event.totalMs, loop_count: event.loopCount, total_tokens: event.totalTokens });
|
|
400
353
|
break;
|
|
401
|
-
case 'error':
|
|
402
|
-
|
|
403
|
-
|
|
354
|
+
case 'error': {
|
|
355
|
+
const error = event.error instanceof Error
|
|
356
|
+
? event.error
|
|
357
|
+
: new Error(event.error?.message || String(event.error || 'Unknown error'));
|
|
358
|
+
failed ||= error;
|
|
359
|
+
state.stopReason = 'error';
|
|
360
|
+
write({ ...base, type: 'error', error: { name: error.name, message: error.message }, retryable: !!event.retryable });
|
|
404
361
|
break;
|
|
362
|
+
}
|
|
405
363
|
case 'fallback':
|
|
406
364
|
case 'llm_retry':
|
|
407
365
|
case 'memory_used':
|
|
408
366
|
case 'recall':
|
|
409
367
|
case 'consolidate':
|
|
410
368
|
case 'reflection':
|
|
369
|
+
case 'tool_result_update':
|
|
370
|
+
case 'user_append':
|
|
411
371
|
write({ ...event, ...base });
|
|
412
372
|
break;
|
|
413
373
|
}
|
|
414
374
|
};
|
|
415
375
|
|
|
416
|
-
|
|
376
|
+
const recordFailure = (error, { vpId = null, threadId = null, turnId = null, writeError = true } = {}) => {
|
|
377
|
+
const normalized = error instanceof Error ? error : new Error(String(error || 'Unknown error'));
|
|
378
|
+
failed ||= normalized;
|
|
379
|
+
const state = stateFor(vpId);
|
|
380
|
+
if (threadId) state.threadId = threadId;
|
|
381
|
+
if (turnId) state.turnId = turnId;
|
|
382
|
+
state.stopReason = 'error';
|
|
383
|
+
if (writeError) {
|
|
384
|
+
write({
|
|
385
|
+
...scopeFor(state),
|
|
386
|
+
type: 'error',
|
|
387
|
+
error: { name: normalized.name || 'Error', message: normalized.message || String(normalized) },
|
|
388
|
+
retryable: false,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return normalized;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
const perVpResults = () => Array.from(states.values()).map(state => ({
|
|
395
|
+
...(state.vpId ? { vp_id: state.vpId } : {}),
|
|
396
|
+
turn_id: state.turnId,
|
|
397
|
+
thread_id: state.threadId || 'main',
|
|
398
|
+
stop_reason: state.stopReason,
|
|
399
|
+
result: state.resultText,
|
|
400
|
+
usage: finalUsage(state.usage),
|
|
401
|
+
}));
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
clientTurnId,
|
|
405
|
+
project,
|
|
406
|
+
recordFailure,
|
|
407
|
+
stateFor,
|
|
408
|
+
scopeFor,
|
|
409
|
+
get failure() { return failed; },
|
|
410
|
+
perVpResults,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function runStreamTurn({
|
|
415
|
+
engine,
|
|
416
|
+
prompt,
|
|
417
|
+
messages = [],
|
|
418
|
+
sessionId = null,
|
|
419
|
+
workDir = process.cwd(),
|
|
420
|
+
model = null,
|
|
421
|
+
modelEffort = null,
|
|
422
|
+
input = null,
|
|
423
|
+
write,
|
|
424
|
+
getCurrentTodos = null,
|
|
425
|
+
setCurrentTodos = null,
|
|
426
|
+
vpPersona = null,
|
|
427
|
+
sessionMembers = null,
|
|
428
|
+
router = null,
|
|
429
|
+
inboundEnvelope = null,
|
|
430
|
+
taskId = null,
|
|
431
|
+
taskMembers = null,
|
|
432
|
+
threadId = 'main',
|
|
433
|
+
vpTurnId = null,
|
|
434
|
+
userAlreadyPersisted = false,
|
|
435
|
+
}) {
|
|
436
|
+
const vpId = typeof vpPersona?.vpId === 'string' ? vpPersona.vpId : null;
|
|
437
|
+
const projector = createStreamProjector({
|
|
438
|
+
sessionId, write, workDir, model, modelEffort, defaultVpId: vpId, defaultThreadId: threadId,
|
|
439
|
+
});
|
|
440
|
+
const askUser = async ({ question, options }) => {
|
|
441
|
+
const requestId = randomUUID();
|
|
442
|
+
const state = projector.stateFor(vpId);
|
|
443
|
+
const scope = projector.scopeFor(state);
|
|
444
|
+
write({ ...scope, type: 'ask_user', subtype: 'request', request_id: requestId, question, options: Array.isArray(options) ? options : [] });
|
|
445
|
+
if (!input) throw new Error('AskUser requires --input-format stream-json');
|
|
446
|
+
const answers = await input.waitForAnswer(requestId);
|
|
447
|
+
write({ ...scope, type: 'ask_user', subtype: 'response', request_id: requestId, answers });
|
|
448
|
+
return answers;
|
|
449
|
+
};
|
|
450
|
+
try {
|
|
451
|
+
for await (const event of engine.query({
|
|
452
|
+
prompt,
|
|
453
|
+
messages,
|
|
454
|
+
sessionId,
|
|
455
|
+
workDir,
|
|
456
|
+
userEffort: modelEffort,
|
|
457
|
+
askUser,
|
|
458
|
+
getCurrentTodos,
|
|
459
|
+
setCurrentTodos,
|
|
460
|
+
vpPersona,
|
|
461
|
+
senderVpId: vpId,
|
|
462
|
+
sessionMembers,
|
|
463
|
+
router,
|
|
464
|
+
inboundEnvelope,
|
|
465
|
+
taskId,
|
|
466
|
+
taskMembers,
|
|
467
|
+
threadId,
|
|
468
|
+
vpTurnId,
|
|
469
|
+
userAlreadyPersisted,
|
|
470
|
+
})) {
|
|
471
|
+
projector.project({ vpId, event, turnId: vpTurnId });
|
|
472
|
+
}
|
|
473
|
+
} catch (error) {
|
|
474
|
+
projector.recordFailure(error, { vpId, threadId });
|
|
475
|
+
}
|
|
476
|
+
const perVp = projector.perVpResults();
|
|
477
|
+
const state = perVp[0] || { turn_id: projector.clientTurnId, thread_id: threadId, stop_reason: 'end_turn', result: '', usage: finalUsage(makeUsageState()) };
|
|
478
|
+
const failed = projector.failure;
|
|
479
|
+
const result = {
|
|
480
|
+
type: 'result',
|
|
481
|
+
subtype: failed ? 'error' : 'success',
|
|
482
|
+
session_id: sessionId,
|
|
483
|
+
turn_id: state.turn_id,
|
|
484
|
+
...(vpId ? { vp_id: vpId, vpId } : {}),
|
|
485
|
+
thread_id: state.thread_id || 'main',
|
|
486
|
+
threadId: state.thread_id || 'main',
|
|
487
|
+
model,
|
|
488
|
+
model_effort: modelEffort,
|
|
489
|
+
stop_reason: failed ? 'error' : state.stop_reason,
|
|
490
|
+
is_error: !!failed,
|
|
491
|
+
result: state.result,
|
|
492
|
+
usage: state.usage,
|
|
493
|
+
...(failed ? { error: failed.message || String(failed) } : {}),
|
|
494
|
+
};
|
|
495
|
+
write(result);
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
export async function runStreamSessionTurn({
|
|
500
|
+
runner,
|
|
501
|
+
prompt,
|
|
502
|
+
sessionId,
|
|
503
|
+
workDir = process.cwd(),
|
|
504
|
+
model = null,
|
|
505
|
+
modelEffort = null,
|
|
506
|
+
input = null,
|
|
507
|
+
write,
|
|
508
|
+
routingIntent = null,
|
|
509
|
+
internal = false,
|
|
510
|
+
taskId = null,
|
|
511
|
+
meta = null,
|
|
512
|
+
}) {
|
|
513
|
+
const projector = createStreamProjector({ sessionId, write, workDir, model, modelEffort });
|
|
514
|
+
const askUser = async ({ question, options }, vpId = null, turnId = projector.clientTurnId, threadId = 'main') => {
|
|
515
|
+
const requestId = randomUUID();
|
|
516
|
+
const state = projector.stateFor(vpId);
|
|
517
|
+
state.turnId = turnId || state.turnId;
|
|
518
|
+
state.threadId = threadId || state.threadId;
|
|
519
|
+
const scope = projector.scopeFor(state);
|
|
520
|
+
write({ ...scope, type: 'ask_user', subtype: 'request', request_id: requestId, question, options: Array.isArray(options) ? options : [] });
|
|
521
|
+
if (!input) throw new Error('AskUser requires --input-format stream-json');
|
|
522
|
+
const answers = await input.waitForAnswer(requestId);
|
|
523
|
+
write({ ...scope, type: 'ask_user', subtype: 'response', request_id: requestId, answers });
|
|
524
|
+
return answers;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
let outcome = { report: { dispatched: [] }, results: [] };
|
|
417
528
|
try {
|
|
418
529
|
outcome = await runner.run(prompt, {
|
|
419
530
|
modelEffort,
|
|
420
|
-
|
|
531
|
+
routingIntent,
|
|
532
|
+
internal,
|
|
533
|
+
taskId,
|
|
534
|
+
meta,
|
|
535
|
+
onEvent: event => projector.project(event),
|
|
421
536
|
askUser,
|
|
422
537
|
});
|
|
423
538
|
} catch (error) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
539
|
+
projector.recordFailure(error);
|
|
540
|
+
}
|
|
541
|
+
for (const row of outcome?.results || []) {
|
|
542
|
+
if (row?.error) projector.recordFailure(row.error, { vpId: row.vpId, writeError: false });
|
|
543
|
+
}
|
|
544
|
+
const dispatchedVpIds = Array.isArray(outcome?.report?.dispatched)
|
|
545
|
+
? outcome.report.dispatched
|
|
546
|
+
: [];
|
|
547
|
+
if (dispatchedVpIds.length === 0 && !projector.failure) {
|
|
548
|
+
const reportErrors = Array.isArray(outcome?.report?.errors)
|
|
549
|
+
? outcome.report.errors
|
|
550
|
+
.map(entry => entry?.error || entry?.message)
|
|
551
|
+
.filter(Boolean)
|
|
552
|
+
: [];
|
|
553
|
+
const detail = reportErrors.join(', ') || outcome?.report?.reason || 'no_vp_dispatched';
|
|
554
|
+
projector.recordFailure(new Error(`stream-json Session turn dispatched no VP: ${detail}`));
|
|
427
555
|
}
|
|
428
556
|
|
|
429
|
-
const perVp =
|
|
430
|
-
vp_id: vpId,
|
|
431
|
-
turn_id: state.turnId,
|
|
432
|
-
stop_reason: state.stopReason,
|
|
433
|
-
result: state.resultText,
|
|
434
|
-
usage: {
|
|
435
|
-
input_tokens: state.usage.inputTokens,
|
|
436
|
-
output_tokens: state.usage.outputTokens,
|
|
437
|
-
cache_read_input_tokens: state.usage.cacheReadTokens,
|
|
438
|
-
cache_creation_input_tokens: state.usage.cacheWriteTokens,
|
|
439
|
-
total_input_tokens: state.usage.inputTokens + state.usage.cacheInputDeltaTokens,
|
|
440
|
-
total_tokens: state.usage.inputTokens + state.usage.cacheInputDeltaTokens + state.usage.outputTokens,
|
|
441
|
-
},
|
|
442
|
-
}));
|
|
557
|
+
const perVp = projector.perVpResults();
|
|
443
558
|
const stopReasons = perVp.map(item => item.stop_reason);
|
|
444
|
-
|
|
559
|
+
if (stopReasons.includes('error') && !projector.failure) {
|
|
560
|
+
projector.recordFailure(new Error('stream-json VP turn ended with stop reason error'), { writeError: false });
|
|
561
|
+
}
|
|
562
|
+
const failed = projector.failure;
|
|
563
|
+
const aggregateStopReason = failed
|
|
445
564
|
? 'error'
|
|
446
565
|
: stopReasons.length > 0 && stopReasons.every(reason => reason === 'aborted')
|
|
447
566
|
? 'aborted'
|
|
448
|
-
: '
|
|
449
|
-
|
|
567
|
+
: stopReasons.length > 0 && stopReasons.every(reason => reason === 'tool_handoff')
|
|
568
|
+
? 'tool_handoff'
|
|
569
|
+
: 'end_turn';
|
|
450
570
|
const result = {
|
|
451
|
-
type: 'result',
|
|
452
|
-
|
|
453
|
-
|
|
571
|
+
type: 'result',
|
|
572
|
+
subtype: failed ? 'error' : 'success',
|
|
573
|
+
session_id: sessionId,
|
|
574
|
+
turn_id: projector.clientTurnId,
|
|
575
|
+
model,
|
|
576
|
+
model_effort: modelEffort,
|
|
454
577
|
stop_reason: aggregateStopReason,
|
|
455
|
-
is_error:
|
|
578
|
+
is_error: !!failed,
|
|
456
579
|
result: perVp.map(item => item.result).filter(Boolean).join('\n'),
|
|
457
|
-
dispatched_vp_ids:
|
|
580
|
+
dispatched_vp_ids: dispatchedVpIds,
|
|
458
581
|
vp_results: perVp,
|
|
459
582
|
...(failed ? { error: failed.message || String(failed) } : {}),
|
|
460
583
|
};
|