@thegitai/cli 1.0.0-preview.47 → 1.0.0-preview.49
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/README.md +25 -0
- package/dist/bin/ai.js +21 -11
- package/dist/src/api/chat.js +564 -214
- package/dist/src/api/startup-handshake.js +36 -0
- package/dist/src/api/turn-recovery.js +189 -0
- package/dist/src/browser/session-bridge.js +21 -7
- package/dist/src/help-text.js +7 -0
- package/dist/src/tool-executor.js +1 -0
- package/dist/src/tools/browser.js +19 -36
- package/dist/src/ui/repl.js +69 -38
- package/dist/src/ui/tui/build-frame.js +17 -0
- package/package.json +6 -6
package/dist/src/api/chat.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { drainBackgroundJobNotifications } from '../background-jobs.js';
|
|
3
|
+
import { cancelActiveCommand } from '../executor.js';
|
|
2
4
|
import { createPromptCheckpoint, sanitizeSessionSafetyForServer, } from '../session-safety.js';
|
|
3
5
|
import { applySessionSnapshot, saveSessionState, snapshotFromSession, } from '../session-store.js';
|
|
4
6
|
import { executeLocalToolCall } from '../tool-executor.js';
|
|
5
7
|
import { saveGeneratedImage } from '../tools/save-generated-image.js';
|
|
6
8
|
import { isUserInputQuestionArray } from './contracts.js';
|
|
7
|
-
import { createTraceContext, gatewayFailureCategory,
|
|
9
|
+
import { createTraceContext, gatewayFailureCategory, normalizeServerUrl, QUOTA_EXCEEDED_CODE, readErrorResponse, ServerApiError, transportFailureDetail, } from './http.js';
|
|
10
|
+
import { createTurnRecovery, fetchTurnAttempt, interruptedStream, isRetryableTurnStatus, isTurnRecoveryExhausted, readTurnChunk, } from './turn-recovery.js';
|
|
8
11
|
import { collectClientEnvironment } from '../client-environment.js';
|
|
9
12
|
import { collectProjectOrientation } from '../project-orientation.js';
|
|
10
13
|
import { describeSessionImageStore } from '../core/session-image-store.js';
|
|
@@ -38,8 +41,13 @@ export function isTurnCancelledError(error) {
|
|
|
38
41
|
}
|
|
39
42
|
function parseSseBlock(block) {
|
|
40
43
|
let event = 'message';
|
|
44
|
+
let id = '';
|
|
41
45
|
const dataLines = [];
|
|
42
46
|
for (const line of block.split(/\r?\n/)) {
|
|
47
|
+
if (line.startsWith('id:')) {
|
|
48
|
+
id = line.slice(3).trim();
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
43
51
|
if (line.startsWith('event:')) {
|
|
44
52
|
event = line.slice('event:'.length).trim() || event;
|
|
45
53
|
continue;
|
|
@@ -52,10 +60,10 @@ function parseSseBlock(block) {
|
|
|
52
60
|
return null;
|
|
53
61
|
const text = dataLines.join('\n');
|
|
54
62
|
try {
|
|
55
|
-
return { event, data: JSON.parse(text) };
|
|
63
|
+
return { id, event, data: JSON.parse(text) };
|
|
56
64
|
}
|
|
57
65
|
catch {
|
|
58
|
-
|
|
66
|
+
throw new Error('Server emitted invalid event JSON.');
|
|
59
67
|
}
|
|
60
68
|
}
|
|
61
69
|
let toolStateSeqCounter = 0;
|
|
@@ -92,7 +100,7 @@ export function preserveCancelledTurnInput(session, input) {
|
|
|
92
100
|
return;
|
|
93
101
|
for (let i = session.history.length - 1; i >= 0; i--) {
|
|
94
102
|
const entry = session.history[i];
|
|
95
|
-
if (!entry || entry.role !== 'user')
|
|
103
|
+
if (!entry || entry.role !== 'user' || entry.kind === 'userInterjection')
|
|
96
104
|
continue;
|
|
97
105
|
const prior = userHistoryText(entry);
|
|
98
106
|
if (!prior)
|
|
@@ -138,12 +146,9 @@ function turnPhaseOf(error) {
|
|
|
138
146
|
const phase = error?.thegitaiPhase;
|
|
139
147
|
return typeof phase === 'string' ? phase : null;
|
|
140
148
|
}
|
|
141
|
-
function historyHasToolCall(session, callId) {
|
|
142
|
-
return session.history.some((entry) => (entry.parts ?? []).some((part) => String(part?.functionCall?.id ?? '') === callId));
|
|
143
|
-
}
|
|
144
149
|
function preserveCancelledTurnToolResult(session, input, event, result) {
|
|
145
150
|
const callId = String(event.call.id ?? '').trim();
|
|
146
|
-
if (!callId
|
|
151
|
+
if (!callId)
|
|
147
152
|
return;
|
|
148
153
|
preserveCancelledTurnInput(session, input);
|
|
149
154
|
session.history.push({
|
|
@@ -163,6 +168,22 @@ function preserveCancelledTurnToolResult(session, input, event, result) {
|
|
|
163
168
|
],
|
|
164
169
|
});
|
|
165
170
|
}
|
|
171
|
+
const turnInterjections = new Map();
|
|
172
|
+
function preserveTurnInterjection(session, input, sent) {
|
|
173
|
+
sent.delivered = true;
|
|
174
|
+
preserveCancelledTurnInput(session, input);
|
|
175
|
+
session.history.push({
|
|
176
|
+
role: 'user',
|
|
177
|
+
parts: [{ text: sent.text }],
|
|
178
|
+
kind: 'userInterjection',
|
|
179
|
+
userInput: sent.text,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function preserveTurnAnswer(session, input, requestId, questions, result) {
|
|
183
|
+
preserveCancelledTurnInput(session, input);
|
|
184
|
+
session.history.push({ role: 'model', parts: [{ userInputRequest: { requestId, questions } }] });
|
|
185
|
+
session.history.push({ role: 'user', parts: [{ userInputResult: { requestId, result } }] });
|
|
186
|
+
}
|
|
166
187
|
function publicStatusMessage(data) {
|
|
167
188
|
if (!data || typeof data !== 'object')
|
|
168
189
|
return null;
|
|
@@ -220,15 +241,33 @@ function normalizeShellJobToolCall(call) {
|
|
|
220
241
|
}
|
|
221
242
|
return changed ? { ...call, args } : call;
|
|
222
243
|
}
|
|
223
|
-
|
|
244
|
+
function createTurnRequester({ fetchImpl, recovery, signal, traceId, }) {
|
|
245
|
+
return (operation, url, init) => {
|
|
246
|
+
const caller = init.signal ? AbortSignal.any([signal, init.signal]) : signal;
|
|
247
|
+
return tagTurnPhase(operation, () => recovery.run(operation, async () => {
|
|
248
|
+
const response = await fetchTurnAttempt(fetchImpl, url, init, caller, {
|
|
249
|
+
readBody: true,
|
|
250
|
+
});
|
|
251
|
+
if (isRetryableTurnStatus(response.status)) {
|
|
252
|
+
throw await readErrorResponse(response, traceId);
|
|
253
|
+
}
|
|
254
|
+
return response;
|
|
255
|
+
}, caller));
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function turnUrl(config, turnId, action) {
|
|
259
|
+
return `${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/${action}`;
|
|
260
|
+
}
|
|
261
|
+
async function postToolResult({ config, turnId, event, result, session, request, traceId, delivery, }) {
|
|
224
262
|
const payload = {
|
|
263
|
+
executionId: event.executionId,
|
|
225
264
|
toolCallId: event.call.id,
|
|
226
265
|
result,
|
|
227
266
|
toolState: toolStateFromSession(session),
|
|
228
267
|
toolStateSeq: ++toolStateSeqCounter,
|
|
229
268
|
};
|
|
230
269
|
const trace = createTraceContext(traceId);
|
|
231
|
-
const response = await
|
|
270
|
+
const response = await request('posting a tool result', turnUrl(config, turnId, 'tool-result'), {
|
|
232
271
|
method: 'POST',
|
|
233
272
|
headers: {
|
|
234
273
|
authorization: `Bearer ${config.token}`,
|
|
@@ -236,21 +275,21 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
236
275
|
...trace.headers,
|
|
237
276
|
},
|
|
238
277
|
body: JSON.stringify(payload),
|
|
239
|
-
|
|
240
|
-
|
|
278
|
+
signal: delivery,
|
|
279
|
+
});
|
|
280
|
+
if (response.status === 410)
|
|
241
281
|
return;
|
|
242
|
-
}
|
|
243
282
|
if (!response.ok) {
|
|
244
283
|
throw await readErrorResponse(response, trace.traceId);
|
|
245
284
|
}
|
|
246
285
|
}
|
|
247
|
-
async function postUserInputResult({ config, turnId, requestId, result,
|
|
286
|
+
async function postUserInputResult({ config, turnId, requestId, result, request, traceId, delivery, }) {
|
|
248
287
|
const payload = {
|
|
249
288
|
requestId,
|
|
250
289
|
result,
|
|
251
290
|
};
|
|
252
291
|
const trace = createTraceContext(traceId);
|
|
253
|
-
const response = await
|
|
292
|
+
const response = await request('posting your answer', turnUrl(config, turnId, 'user-input-result'), {
|
|
254
293
|
method: 'POST',
|
|
255
294
|
headers: {
|
|
256
295
|
authorization: `Bearer ${config.token}`,
|
|
@@ -258,14 +297,15 @@ async function postUserInputResult({ config, turnId, requestId, result, fetchImp
|
|
|
258
297
|
...trace.headers,
|
|
259
298
|
},
|
|
260
299
|
body: JSON.stringify(payload),
|
|
300
|
+
signal: delivery,
|
|
261
301
|
});
|
|
262
|
-
if (response.status === 410)
|
|
302
|
+
if (response.status === 410)
|
|
263
303
|
return;
|
|
264
|
-
}
|
|
265
304
|
if (!response.ok) {
|
|
266
305
|
throw await readErrorResponse(response, trace.traceId);
|
|
267
306
|
}
|
|
268
307
|
}
|
|
308
|
+
const activeTurnRequesters = new Map();
|
|
269
309
|
export async function postInterjection({ config, turnId, text, messageId, imageAttachments = [], fetchImpl = globalThis.fetch, traceId, }) {
|
|
270
310
|
const payload = {
|
|
271
311
|
text,
|
|
@@ -273,7 +313,8 @@ export async function postInterjection({ config, turnId, text, messageId, imageA
|
|
|
273
313
|
...(imageAttachments.length > 0 ? { imageAttachments } : {}),
|
|
274
314
|
};
|
|
275
315
|
const trace = createTraceContext(traceId);
|
|
276
|
-
const
|
|
316
|
+
const url = turnUrl(config, turnId, 'interject');
|
|
317
|
+
const init = {
|
|
277
318
|
method: 'POST',
|
|
278
319
|
headers: {
|
|
279
320
|
authorization: `Bearer ${config.token}`,
|
|
@@ -281,11 +322,37 @@ export async function postInterjection({ config, turnId, text, messageId, imageA
|
|
|
281
322
|
...trace.headers,
|
|
282
323
|
},
|
|
283
324
|
body: JSON.stringify(payload),
|
|
284
|
-
}
|
|
325
|
+
};
|
|
326
|
+
const request = activeTurnRequesters.get(turnId);
|
|
327
|
+
const sent = turnInterjections.get(turnId);
|
|
328
|
+
const record = {
|
|
329
|
+
messageId,
|
|
330
|
+
text,
|
|
331
|
+
hasImages: imageAttachments.length > 0,
|
|
332
|
+
delivered: false,
|
|
333
|
+
};
|
|
334
|
+
sent?.push(record);
|
|
335
|
+
const forget = () => {
|
|
336
|
+
const index = sent?.indexOf(record) ?? -1;
|
|
337
|
+
if (index !== -1)
|
|
338
|
+
sent.splice(index, 1);
|
|
339
|
+
};
|
|
340
|
+
let response;
|
|
341
|
+
try {
|
|
342
|
+
response = request
|
|
343
|
+
? await request('posting a message into the turn', url, init)
|
|
344
|
+
: await fetchImpl(url, init);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
forget();
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
285
350
|
if (response.status === 410) {
|
|
351
|
+
forget();
|
|
286
352
|
return 'stale';
|
|
287
353
|
}
|
|
288
354
|
if (!response.ok) {
|
|
355
|
+
forget();
|
|
289
356
|
throw await readErrorResponse(response, trace.traceId);
|
|
290
357
|
}
|
|
291
358
|
return 'delivered';
|
|
@@ -313,14 +380,13 @@ function exitServerTurnId(session) {
|
|
|
313
380
|
turnIdOverrides.delete(session);
|
|
314
381
|
}
|
|
315
382
|
}
|
|
316
|
-
async function executeAndPostToolResult({ config, session, event, input,
|
|
383
|
+
async function executeAndPostToolResult({ config, session, event, input, request, signal, delivery, executed, traceId, }) {
|
|
317
384
|
const turnId = String(event?.turnId ?? '').trim();
|
|
318
|
-
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
385
|
+
if (!turnId || !event?.executionId || !event?.call?.id || !event.call.name) {
|
|
319
386
|
throw new Error('Server emitted an invalid tool-call event.');
|
|
320
387
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
388
|
+
signal.throwIfAborted();
|
|
389
|
+
delivery.throwIfAborted();
|
|
324
390
|
const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
|
|
325
391
|
if (serverSessionTurnId) {
|
|
326
392
|
enterServerTurnId(session, serverSessionTurnId);
|
|
@@ -339,17 +405,19 @@ async function executeAndPostToolResult({ config, session, event, input, fetchIm
|
|
|
339
405
|
})
|
|
340
406
|
: await executeLocalToolCall(session, call);
|
|
341
407
|
preserveCancelledTurnToolResult(session, input, { ...event, call }, rawResult);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
408
|
+
executed();
|
|
409
|
+
signal.throwIfAborted();
|
|
410
|
+
if (delivery.aborted)
|
|
411
|
+
return;
|
|
345
412
|
await postToolResult({
|
|
346
413
|
config,
|
|
347
414
|
turnId,
|
|
348
415
|
event,
|
|
349
416
|
result: rawResult,
|
|
350
417
|
session,
|
|
351
|
-
|
|
418
|
+
request,
|
|
352
419
|
traceId,
|
|
420
|
+
delivery,
|
|
353
421
|
});
|
|
354
422
|
}
|
|
355
423
|
finally {
|
|
@@ -358,190 +426,312 @@ async function executeAndPostToolResult({ config, session, event, input, fetchIm
|
|
|
358
426
|
}
|
|
359
427
|
}
|
|
360
428
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
429
|
+
function createTurnWork() {
|
|
430
|
+
const executions = new Map();
|
|
431
|
+
const inFlight = new Set();
|
|
432
|
+
const running = new Set();
|
|
433
|
+
let lastSerial = Promise.resolve();
|
|
434
|
+
let hasFailed = false;
|
|
435
|
+
let rejectFailed;
|
|
436
|
+
const failed = new Promise((_, reject) => {
|
|
437
|
+
rejectFailed = reject;
|
|
438
|
+
});
|
|
439
|
+
failed.catch(() => { });
|
|
440
|
+
function dispatch(key, parallel, run) {
|
|
441
|
+
if (executions.has(key))
|
|
442
|
+
return;
|
|
443
|
+
const prior = parallel ? lastSerial : Promise.all([lastSerial, ...inFlight]);
|
|
444
|
+
let executed;
|
|
445
|
+
const local = new Promise((resolve) => {
|
|
446
|
+
executed = resolve;
|
|
447
|
+
});
|
|
448
|
+
running.add(local);
|
|
449
|
+
void local.then(() => running.delete(local));
|
|
450
|
+
const execution = prior.then(() => run(executed));
|
|
451
|
+
execution.then(executed, executed);
|
|
452
|
+
executions.set(key, execution);
|
|
453
|
+
inFlight.add(execution);
|
|
454
|
+
if (!parallel)
|
|
455
|
+
lastSerial = execution;
|
|
456
|
+
execution.then(() => inFlight.delete(execution), (error) => {
|
|
457
|
+
inFlight.delete(execution);
|
|
458
|
+
hasFailed = true;
|
|
459
|
+
rejectFailed(error);
|
|
460
|
+
});
|
|
364
461
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
462
|
+
return {
|
|
463
|
+
dispatch,
|
|
464
|
+
failed,
|
|
465
|
+
get hasFailed() {
|
|
466
|
+
return hasFailed;
|
|
467
|
+
},
|
|
468
|
+
async executed() {
|
|
469
|
+
await Promise.all([...running]);
|
|
470
|
+
},
|
|
471
|
+
async settled() {
|
|
472
|
+
await Promise.allSettled([...inFlight]);
|
|
473
|
+
},
|
|
370
474
|
};
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
function recordParallelFailure(error) {
|
|
379
|
-
const failure = error ?? new Error('Local tool execution failed.');
|
|
380
|
-
if (firstParallelFailure == null) {
|
|
381
|
-
firstParallelFailure = failure;
|
|
382
|
-
rejectOnParallelFailure?.(failure);
|
|
383
|
-
}
|
|
384
|
-
return failure;
|
|
475
|
+
}
|
|
476
|
+
function terminalFailure(event, traceId) {
|
|
477
|
+
const data = (event.data ?? {});
|
|
478
|
+
const message = String(data.message ?? 'The turn failed before completion.');
|
|
479
|
+
const eventTraceId = typeof data.traceId === 'string' ? data.traceId : traceId;
|
|
480
|
+
if (event.event === 'cancelled') {
|
|
481
|
+
return new TurnCancelledError(message);
|
|
385
482
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return;
|
|
389
|
-
const pending = pendingParallelTools.splice(0);
|
|
390
|
-
const outcomes = await Promise.all(pending);
|
|
391
|
-
for (const outcome of outcomes) {
|
|
392
|
-
if (outcome != null)
|
|
393
|
-
throw outcome;
|
|
394
|
-
}
|
|
483
|
+
if (data.code === QUOTA_EXCEEDED_CODE) {
|
|
484
|
+
return new ServerApiError(message, 429, eventTraceId, QUOTA_EXCEEDED_CODE, data.details ?? {});
|
|
395
485
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
486
|
+
return new ChatTurnFailedError(message, typeof data.category === 'string' ? data.category : 'unknown_error', Boolean(data.retryable), eventTraceId, data.snapshot);
|
|
487
|
+
}
|
|
488
|
+
const TURN_EVENT_ACK_INTERVAL_MS = 5_000;
|
|
489
|
+
const MAX_TURN_EVENT_BYTES = 64 * 1024 * 1024;
|
|
490
|
+
function createEventAcknowledger(send) {
|
|
491
|
+
let latest = '';
|
|
492
|
+
let confirmed = '';
|
|
493
|
+
let inFlight = false;
|
|
494
|
+
let stopped = false;
|
|
495
|
+
let timer = null;
|
|
496
|
+
const schedule = () => {
|
|
497
|
+
if (timer || stopped)
|
|
401
498
|
return;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
499
|
+
timer = setTimeout(flush, TURN_EVENT_ACK_INTERVAL_MS);
|
|
500
|
+
timer.unref?.();
|
|
501
|
+
};
|
|
502
|
+
const flush = () => {
|
|
503
|
+
if (timer)
|
|
504
|
+
clearTimeout(timer);
|
|
505
|
+
timer = null;
|
|
506
|
+
if (stopped || inFlight || !latest || latest === confirmed)
|
|
405
507
|
return;
|
|
508
|
+
const eventId = latest;
|
|
509
|
+
inFlight = true;
|
|
510
|
+
send(eventId)
|
|
511
|
+
.then(() => {
|
|
512
|
+
confirmed = eventId;
|
|
513
|
+
}, () => { })
|
|
514
|
+
.finally(() => {
|
|
515
|
+
inFlight = false;
|
|
516
|
+
if (latest !== confirmed)
|
|
517
|
+
schedule();
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
return {
|
|
521
|
+
note(eventId) {
|
|
522
|
+
latest = eventId;
|
|
523
|
+
schedule();
|
|
524
|
+
},
|
|
525
|
+
flush,
|
|
526
|
+
stop() {
|
|
527
|
+
stopped = true;
|
|
528
|
+
if (timer)
|
|
529
|
+
clearTimeout(timer);
|
|
530
|
+
timer = null;
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
async function runTurnStream({ openStream, config, session, input, request, recovery, signal, traceId, acknowledge, stopLocalWork, interjections, onTurnStart, onInterjectionDelivered, }) {
|
|
535
|
+
const seen = new Set();
|
|
536
|
+
const work = createTurnWork();
|
|
537
|
+
const deliveries = new AbortController();
|
|
538
|
+
const delivery = AbortSignal.any([signal, deliveries.signal]);
|
|
539
|
+
const streamToken = {};
|
|
540
|
+
let lastEventId = '';
|
|
541
|
+
let registeredTurnId = '';
|
|
542
|
+
function handleEvent(event) {
|
|
543
|
+
if (!event.id) {
|
|
544
|
+
throw new Error('The server sent a turn event without an id.');
|
|
406
545
|
}
|
|
407
|
-
if (event.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
546
|
+
if (seen.has(event.id))
|
|
547
|
+
return null;
|
|
548
|
+
seen.add(event.id);
|
|
549
|
+
lastEventId = event.id;
|
|
550
|
+
const data = event.data;
|
|
551
|
+
switch (event.event) {
|
|
552
|
+
case 'turn-start': {
|
|
553
|
+
const turnId = String(data?.turnId ?? '').trim();
|
|
554
|
+
if (!turnId)
|
|
555
|
+
throw new Error('The server sent a turn without an id.');
|
|
556
|
+
registeredTurnId = turnId;
|
|
557
|
+
activeTurnRequesters.set(turnId, request);
|
|
558
|
+
turnInterjections.set(turnId, interjections);
|
|
559
|
+
onTurnStart?.(turnId);
|
|
560
|
+
break;
|
|
412
561
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
562
|
+
case 'interjection-delivered': {
|
|
563
|
+
const delivered = data;
|
|
564
|
+
for (const messageId of delivered?.messageIds ?? []) {
|
|
565
|
+
const sent = interjections.find((item) => item.messageId === messageId);
|
|
566
|
+
if (sent && !sent.delivered)
|
|
567
|
+
preserveTurnInterjection(session, input, sent);
|
|
568
|
+
}
|
|
569
|
+
onInterjectionDelivered?.(delivered);
|
|
570
|
+
break;
|
|
416
571
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
572
|
+
case 'status': {
|
|
573
|
+
const status = (data ?? {});
|
|
574
|
+
if (status.phase === 'analyzing_image') {
|
|
575
|
+
session.onImageAnalysis?.(Math.max(1, Number(status.imageCount ?? 1) || 1));
|
|
576
|
+
session.onImageGeneration?.(false);
|
|
577
|
+
}
|
|
578
|
+
else if (status.phase === 'generating_image') {
|
|
579
|
+
session.onImageGeneration?.(true);
|
|
580
|
+
session.onImageAnalysis?.(0);
|
|
581
|
+
}
|
|
582
|
+
else if (status.phase) {
|
|
583
|
+
session.onImageAnalysis?.(0);
|
|
584
|
+
session.onImageGeneration?.(false);
|
|
585
|
+
}
|
|
586
|
+
const message = publicStatusMessage(data);
|
|
587
|
+
if (message)
|
|
588
|
+
session.onStatus(message);
|
|
589
|
+
break;
|
|
420
590
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (event.event === 'tool-call') {
|
|
430
|
-
const data = event.data;
|
|
431
|
-
if (data?.parallelSafe === true) {
|
|
432
|
-
pendingParallelTools.push(executeAndPostToolResult({
|
|
591
|
+
case 'tool-call': {
|
|
592
|
+
const toolCall = data;
|
|
593
|
+
if (!toolCall?.executionId || !toolCall.call?.name) {
|
|
594
|
+
throw new Error('Server emitted an invalid tool-call event.');
|
|
595
|
+
}
|
|
596
|
+
work.dispatch(`tool:${toolCall.executionId}`, toolCall.parallelSafe === true, (executed) => executeAndPostToolResult({
|
|
433
597
|
config,
|
|
434
598
|
session,
|
|
435
|
-
event:
|
|
599
|
+
event: toolCall,
|
|
436
600
|
input,
|
|
437
|
-
|
|
601
|
+
request,
|
|
438
602
|
signal,
|
|
603
|
+
delivery,
|
|
604
|
+
executed,
|
|
439
605
|
traceId,
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
await drainParallelTools();
|
|
444
|
-
await executeAndPostToolResult({
|
|
445
|
-
config,
|
|
446
|
-
session,
|
|
447
|
-
event: data,
|
|
448
|
-
input,
|
|
449
|
-
fetchImpl,
|
|
450
|
-
signal,
|
|
451
|
-
traceId,
|
|
452
|
-
});
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
if (event.event === 'tool-result') {
|
|
456
|
-
const data = event.data;
|
|
457
|
-
if (data?.call?.name) {
|
|
458
|
-
session.onToolEvent?.({ call: data.call, result: data.result });
|
|
459
|
-
}
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
if (event.event === 'user-input-request') {
|
|
463
|
-
await drainParallelTools();
|
|
464
|
-
const data = event.data;
|
|
465
|
-
const turnId = String(data?.turnId ?? '').trim();
|
|
466
|
-
const requestId = String(data?.requestId ?? '').trim();
|
|
467
|
-
if (!turnId ||
|
|
468
|
-
!requestId ||
|
|
469
|
-
!isUserInputQuestionArray(data?.questions)) {
|
|
470
|
-
throw new Error('Server emitted an invalid user-input request.');
|
|
606
|
+
}));
|
|
607
|
+
break;
|
|
471
608
|
}
|
|
472
|
-
|
|
473
|
-
|
|
609
|
+
case 'tool-result': {
|
|
610
|
+
const toolResult = data;
|
|
611
|
+
if (toolResult?.call?.name) {
|
|
612
|
+
session.onToolEvent?.({ call: toolResult.call, result: toolResult.result });
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
474
615
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
616
|
+
case 'user-input-request': {
|
|
617
|
+
const question = data;
|
|
618
|
+
const turnId = String(question?.turnId ?? '').trim();
|
|
619
|
+
const requestId = String(question?.requestId ?? '').trim();
|
|
620
|
+
if (!turnId || !requestId || !isUserInputQuestionArray(question?.questions)) {
|
|
621
|
+
throw new Error('Server emitted an invalid user-input request.');
|
|
622
|
+
}
|
|
623
|
+
work.dispatch(`question:${requestId}`, false, async (executed) => {
|
|
624
|
+
if (!session.requestUserInput) {
|
|
625
|
+
throw new Error('Interactive user input is unavailable in this client.');
|
|
626
|
+
}
|
|
627
|
+
const result = await session.requestUserInput({ questions: question.questions }, signal);
|
|
628
|
+
executed();
|
|
629
|
+
signal.throwIfAborted();
|
|
630
|
+
preserveTurnAnswer(session, input, requestId, question.questions, result);
|
|
631
|
+
if (delivery.aborted)
|
|
632
|
+
return;
|
|
633
|
+
await postUserInputResult({
|
|
634
|
+
config,
|
|
635
|
+
turnId,
|
|
636
|
+
requestId,
|
|
637
|
+
result,
|
|
638
|
+
request,
|
|
639
|
+
traceId,
|
|
640
|
+
delivery,
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
break;
|
|
478
644
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
traceId
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
if (event.event === 'result') {
|
|
490
|
-
await drainParallelTools();
|
|
491
|
-
finalResult.current = event.data;
|
|
492
|
-
return;
|
|
645
|
+
case 'result':
|
|
646
|
+
if (!data?.snapshot)
|
|
647
|
+
throw new Error('Server returned an invalid chat result.');
|
|
648
|
+
return { kind: 'result', result: data };
|
|
649
|
+
case 'error':
|
|
650
|
+
case 'cancelled':
|
|
651
|
+
return { kind: 'failed', error: terminalFailure(event, traceId) };
|
|
652
|
+
default:
|
|
653
|
+
break;
|
|
493
654
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
655
|
+
acknowledge.note(event.id);
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
async function readUntilOutcome(response) {
|
|
659
|
+
if (!response.body)
|
|
660
|
+
throw interruptedStream('The server returned an empty turn stream.');
|
|
661
|
+
const reader = response.body.getReader();
|
|
662
|
+
const decoder = new TextDecoder();
|
|
663
|
+
let buffer = '';
|
|
664
|
+
try {
|
|
665
|
+
for (;;) {
|
|
666
|
+
const read = await Promise.race([readTurnChunk(reader, signal), work.failed]);
|
|
667
|
+
if (read.done)
|
|
668
|
+
throw interruptedStream();
|
|
669
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
670
|
+
for (let match = /\r?\n\r?\n/.exec(buffer); match; match = /\r?\n\r?\n/.exec(buffer)) {
|
|
671
|
+
const block = buffer.slice(0, match.index);
|
|
672
|
+
buffer = buffer.slice(match.index + match[0].length);
|
|
673
|
+
signal.throwIfAborted();
|
|
674
|
+
const event = parseSseBlock(block);
|
|
675
|
+
if (!event)
|
|
676
|
+
continue;
|
|
677
|
+
recovery.restored(streamToken);
|
|
678
|
+
const outcome = handleEvent(event);
|
|
679
|
+
if (outcome)
|
|
680
|
+
return outcome;
|
|
681
|
+
}
|
|
682
|
+
if (buffer.length > MAX_TURN_EVENT_BYTES) {
|
|
683
|
+
throw new Error('The server sent a turn event larger than the protocol allows.');
|
|
684
|
+
}
|
|
499
685
|
}
|
|
500
|
-
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
await reader.cancel().catch(() => { });
|
|
501
689
|
}
|
|
502
690
|
}
|
|
503
691
|
try {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
692
|
+
for (;;) {
|
|
693
|
+
let outcome;
|
|
694
|
+
try {
|
|
695
|
+
signal.throwIfAborted();
|
|
696
|
+
const response = await openStream(lastEventId);
|
|
697
|
+
recovery.reconnected(streamToken);
|
|
698
|
+
outcome = await readUntilOutcome(response);
|
|
508
699
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
700
|
+
catch (error) {
|
|
701
|
+
signal.throwIfAborted();
|
|
702
|
+
if (work.hasFailed)
|
|
703
|
+
throw error;
|
|
704
|
+
await recovery.retryAfter(streamToken, lastEventId ? 'reading the turn stream' : 'opening the turn', error);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
acknowledge.note(lastEventId);
|
|
708
|
+
acknowledge.flush();
|
|
709
|
+
if (outcome.kind === 'result') {
|
|
710
|
+
deliveries.abort(new Error('The turn already finished.'));
|
|
711
|
+
await work.executed();
|
|
712
|
+
signal.throwIfAborted();
|
|
521
713
|
}
|
|
714
|
+
else {
|
|
715
|
+
stopLocalWork();
|
|
716
|
+
await work.settled();
|
|
717
|
+
}
|
|
718
|
+
return outcome;
|
|
522
719
|
}
|
|
523
|
-
await drainParallelTools();
|
|
524
720
|
}
|
|
525
721
|
catch (error) {
|
|
526
|
-
|
|
527
|
-
|
|
722
|
+
const outcome = signal.aborted ? signal.reason : error;
|
|
723
|
+
stopLocalWork();
|
|
724
|
+
await work.settled();
|
|
725
|
+
throw outcome;
|
|
528
726
|
}
|
|
529
727
|
finally {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
if (tail) {
|
|
535
|
-
const event = parseSseBlock(tail);
|
|
536
|
-
if (event)
|
|
537
|
-
await handleEvent(event);
|
|
538
|
-
}
|
|
539
|
-
if (!finalResult.current?.snapshot) {
|
|
540
|
-
throw new Error('Server returned an invalid chat result.');
|
|
728
|
+
if (registeredTurnId) {
|
|
729
|
+
activeTurnRequesters.delete(registeredTurnId);
|
|
730
|
+
turnInterjections.delete(registeredTurnId);
|
|
731
|
+
}
|
|
541
732
|
}
|
|
542
|
-
return finalResult.current;
|
|
543
733
|
}
|
|
544
|
-
export async function sendServerUserMessage({ config, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, }) {
|
|
734
|
+
export async function sendServerUserMessage({ config, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, recoverySleep, }) {
|
|
545
735
|
const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
|
|
546
736
|
const requestImageAttachments = autoAttach.attachments.length > 0
|
|
547
737
|
? [...imageAttachments, ...autoAttach.attachments]
|
|
@@ -554,6 +744,7 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
|
|
|
554
744
|
session.onStatus(`Image: ${err}`);
|
|
555
745
|
}
|
|
556
746
|
const request = {
|
|
747
|
+
clientTurnId: randomUUID(),
|
|
557
748
|
modelId: session.modelId,
|
|
558
749
|
session: snapshotForServer(session),
|
|
559
750
|
input: requestInputBase,
|
|
@@ -569,46 +760,83 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
|
|
|
569
760
|
agentMode: session.agentMode,
|
|
570
761
|
};
|
|
571
762
|
const trace = createTraceContext();
|
|
763
|
+
let completed = false;
|
|
764
|
+
let turnEnded = false;
|
|
765
|
+
const reportRecovery = (status) => {
|
|
766
|
+
if (!turnEnded)
|
|
767
|
+
session.onRecovery?.(status);
|
|
768
|
+
};
|
|
769
|
+
const startAttempt = (turnRequest, onStart) => startTurnAttempt({
|
|
770
|
+
config,
|
|
771
|
+
session,
|
|
772
|
+
turnRequest,
|
|
773
|
+
input: requestInputBase,
|
|
774
|
+
trace,
|
|
775
|
+
fetchImpl,
|
|
776
|
+
recoverySleep,
|
|
777
|
+
reportRecovery,
|
|
778
|
+
onTurnStart: onStart,
|
|
779
|
+
onInterjectionDelivered,
|
|
780
|
+
});
|
|
781
|
+
let attempt = startAttempt(request, onTurnStart);
|
|
782
|
+
const cancelTurn = () => attempt.cancel();
|
|
572
783
|
const preTurnHistoryLength = session.history.length;
|
|
573
784
|
const preserveOnAbort = () => preserveCancelledTurnInput(session, requestInputBase);
|
|
574
785
|
if (signal?.aborted) {
|
|
575
786
|
preserveOnAbort();
|
|
787
|
+
cancelTurn();
|
|
576
788
|
}
|
|
577
789
|
else {
|
|
578
790
|
signal?.addEventListener('abort', preserveOnAbort, { once: true });
|
|
791
|
+
signal?.addEventListener('abort', cancelTurn, { once: true });
|
|
579
792
|
}
|
|
580
793
|
try {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
accept: 'text/event-stream',
|
|
585
|
-
authorization: `Bearer ${config.token}`,
|
|
586
|
-
'content-type': 'application/json',
|
|
587
|
-
...trace.headers,
|
|
588
|
-
},
|
|
589
|
-
body: JSON.stringify(request),
|
|
590
|
-
signal,
|
|
591
|
-
}));
|
|
592
|
-
if (!response.ok) {
|
|
593
|
-
throw await readErrorResponse(response, trace.traceId);
|
|
794
|
+
let outcome;
|
|
795
|
+
try {
|
|
796
|
+
outcome = await attempt.run();
|
|
594
797
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
798
|
+
catch (error) {
|
|
799
|
+
const sent = attempt.interjections;
|
|
800
|
+
if (!isTurnLostByServer(error) || sent.some((item) => item.hasImages))
|
|
801
|
+
throw error;
|
|
802
|
+
attempt.close(false);
|
|
803
|
+
if (signal?.aborted)
|
|
804
|
+
throw new TurnCancelledError();
|
|
805
|
+
const carried = sent.filter((item) => !item.delivered);
|
|
806
|
+
for (const item of carried)
|
|
807
|
+
preserveTurnInterjection(session, requestInputBase, item);
|
|
808
|
+
reportRecovery({ phase: 'continuing' });
|
|
809
|
+
attempt = startAttempt({
|
|
810
|
+
...request,
|
|
811
|
+
clientTurnId: randomUUID(),
|
|
812
|
+
session: snapshotForServer(session),
|
|
813
|
+
continuation: {
|
|
814
|
+
clientTurnId: request.clientTurnId,
|
|
815
|
+
historyIndex: preTurnHistoryLength,
|
|
816
|
+
},
|
|
817
|
+
}, (turnId) => {
|
|
818
|
+
reportRecovery({ phase: 'continued' });
|
|
819
|
+
onTurnStart?.(turnId);
|
|
820
|
+
if (carried.length > 0) {
|
|
821
|
+
onInterjectionDelivered?.({
|
|
822
|
+
turnId,
|
|
823
|
+
step: 0,
|
|
824
|
+
deliveredAt: 'tool_round_boundary',
|
|
825
|
+
messageIds: carried.map((item) => item.messageId),
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
outcome = await attempt.run();
|
|
830
|
+
}
|
|
831
|
+
if (outcome.kind === 'failed')
|
|
832
|
+
throw outcome.error;
|
|
833
|
+
applySessionSnapshot(session, outcome.result.snapshot, { preserveAgentMode: true });
|
|
834
|
+
completed = true;
|
|
607
835
|
return {
|
|
608
|
-
text: result.text,
|
|
609
|
-
waitingForApproval: result.waitingForApproval,
|
|
610
|
-
toolBudgetReached: result.toolBudgetReached,
|
|
611
|
-
usageSummary: result.usageSummary,
|
|
836
|
+
text: outcome.result.text,
|
|
837
|
+
waitingForApproval: outcome.result.waitingForApproval,
|
|
838
|
+
toolBudgetReached: outcome.result.toolBudgetReached,
|
|
839
|
+
usageSummary: outcome.result.usageSummary,
|
|
612
840
|
};
|
|
613
841
|
}
|
|
614
842
|
catch (error) {
|
|
@@ -618,23 +846,32 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
|
|
|
618
846
|
? error
|
|
619
847
|
: new TurnCancelledError();
|
|
620
848
|
}
|
|
621
|
-
const
|
|
849
|
+
const exhausted = isTurnRecoveryExhausted(error);
|
|
850
|
+
const carriesTrace = error instanceof ChatTurnFailedError || error instanceof ServerApiError;
|
|
851
|
+
const transportDetail = carriesTrace || exhausted ? null : transportFailureDetail(error);
|
|
622
852
|
if (transportDetail && error instanceof Error) {
|
|
623
853
|
const phase = turnPhaseOf(error) ?? 'reaching the server';
|
|
624
854
|
error.message = `connection lost while ${phase}: ${transportDetail} [trace ${trace.traceId}]`;
|
|
625
855
|
}
|
|
856
|
+
else if (!carriesTrace && error instanceof Error) {
|
|
857
|
+
error.message = `${error.message} [trace ${trace.traceId}]`;
|
|
858
|
+
}
|
|
626
859
|
const category = error instanceof ChatTurnFailedError
|
|
627
860
|
? error.category
|
|
628
|
-
:
|
|
629
|
-
|
|
861
|
+
: exhausted
|
|
862
|
+
? 'connection_error'
|
|
863
|
+
:
|
|
864
|
+
(gatewayFailureCategory(error) ?? 'unknown_error');
|
|
630
865
|
const partial = error instanceof ChatTurnFailedError ? error.partialSnapshot : undefined;
|
|
631
866
|
if (partial) {
|
|
632
867
|
applySessionSnapshot(session, partial, { preserveAgentMode: true });
|
|
633
868
|
}
|
|
634
|
-
else {
|
|
635
|
-
session.history.length = preTurnHistoryLength;
|
|
869
|
+
else if (session.history.length === preTurnHistoryLength) {
|
|
636
870
|
preserveFailedTurnInput(session, requestInputBase, category);
|
|
637
871
|
}
|
|
872
|
+
else {
|
|
873
|
+
appendTurnFailureMarker(session, category);
|
|
874
|
+
}
|
|
638
875
|
try {
|
|
639
876
|
saveSessionState(session, session.env);
|
|
640
877
|
}
|
|
@@ -642,7 +879,120 @@ export async function sendServerUserMessage({ config, session, input, imageAttac
|
|
|
642
879
|
throw error;
|
|
643
880
|
}
|
|
644
881
|
finally {
|
|
882
|
+
turnEnded = true;
|
|
645
883
|
signal?.removeEventListener('abort', preserveOnAbort);
|
|
884
|
+
signal?.removeEventListener('abort', cancelTurn);
|
|
885
|
+
attempt.close(completed);
|
|
646
886
|
releaseBrowserAfterTurn();
|
|
647
887
|
}
|
|
648
888
|
}
|
|
889
|
+
function isTurnLostByServer(error) {
|
|
890
|
+
return error instanceof ServerApiError && error.code === 'RECOVERY_UNAVAILABLE';
|
|
891
|
+
}
|
|
892
|
+
function startTurnAttempt({ config, session, turnRequest, input, trace, fetchImpl, recoverySleep, reportRecovery, onTurnStart, onInterjectionDelivered, }) {
|
|
893
|
+
const requestBody = JSON.stringify(turnRequest);
|
|
894
|
+
const interjections = [];
|
|
895
|
+
const turnController = new AbortController();
|
|
896
|
+
const turnSignal = turnController.signal;
|
|
897
|
+
session.turnAbortSignal = turnSignal;
|
|
898
|
+
let serverSettled = false;
|
|
899
|
+
let cancellationSent = false;
|
|
900
|
+
const sendCancellation = () => {
|
|
901
|
+
if (cancellationSent)
|
|
902
|
+
return;
|
|
903
|
+
cancellationSent = true;
|
|
904
|
+
const cancelController = new AbortController();
|
|
905
|
+
void fetchTurnAttempt(fetchImpl, turnUrl(config, turnRequest.clientTurnId, 'cancel'), {
|
|
906
|
+
method: 'POST',
|
|
907
|
+
headers: {
|
|
908
|
+
authorization: `Bearer ${config.token}`,
|
|
909
|
+
'content-type': 'application/json',
|
|
910
|
+
...trace.headers,
|
|
911
|
+
},
|
|
912
|
+
body: JSON.stringify({ sessionId: turnRequest.session.id }),
|
|
913
|
+
}, cancelController.signal, { readBody: true }).catch(() => { });
|
|
914
|
+
};
|
|
915
|
+
const stopLocalWork = () => {
|
|
916
|
+
turnController.abort(new TurnCancelledError());
|
|
917
|
+
cancelActiveCommand();
|
|
918
|
+
};
|
|
919
|
+
const recovery = createTurnRecovery({
|
|
920
|
+
signal: turnSignal,
|
|
921
|
+
sleep: recoverySleep,
|
|
922
|
+
onStatus: reportRecovery,
|
|
923
|
+
});
|
|
924
|
+
const requester = createTurnRequester({
|
|
925
|
+
fetchImpl,
|
|
926
|
+
recovery,
|
|
927
|
+
signal: turnSignal,
|
|
928
|
+
traceId: trace.traceId,
|
|
929
|
+
});
|
|
930
|
+
const acknowledge = createEventAcknowledger(async (eventId) => {
|
|
931
|
+
const response = await fetchTurnAttempt(fetchImpl, turnUrl(config, turnRequest.clientTurnId, 'ack'), {
|
|
932
|
+
method: 'POST',
|
|
933
|
+
headers: {
|
|
934
|
+
authorization: `Bearer ${config.token}`,
|
|
935
|
+
'content-type': 'application/json',
|
|
936
|
+
...trace.headers,
|
|
937
|
+
},
|
|
938
|
+
body: JSON.stringify({ eventId }),
|
|
939
|
+
}, new AbortController().signal, { readBody: true });
|
|
940
|
+
if (!response.ok)
|
|
941
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
942
|
+
});
|
|
943
|
+
const openStream = async (lastEventId) => {
|
|
944
|
+
const response = await fetchTurnAttempt(fetchImpl, `${normalizeServerUrl(config.serverUrl)}/v1/chat/turn`, {
|
|
945
|
+
method: 'POST',
|
|
946
|
+
headers: {
|
|
947
|
+
accept: 'text/event-stream',
|
|
948
|
+
authorization: `Bearer ${config.token}`,
|
|
949
|
+
'content-type': 'application/json',
|
|
950
|
+
...trace.headers,
|
|
951
|
+
...(lastEventId ? { 'last-event-id': lastEventId } : {}),
|
|
952
|
+
},
|
|
953
|
+
body: requestBody,
|
|
954
|
+
}, turnSignal);
|
|
955
|
+
if (!response.ok) {
|
|
956
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
957
|
+
}
|
|
958
|
+
return response;
|
|
959
|
+
};
|
|
960
|
+
return {
|
|
961
|
+
interjections,
|
|
962
|
+
cancel() {
|
|
963
|
+
stopLocalWork();
|
|
964
|
+
sendCancellation();
|
|
965
|
+
},
|
|
966
|
+
async run() {
|
|
967
|
+
const outcome = await runTurnStream({
|
|
968
|
+
openStream,
|
|
969
|
+
config,
|
|
970
|
+
session,
|
|
971
|
+
input,
|
|
972
|
+
request: requester,
|
|
973
|
+
recovery,
|
|
974
|
+
signal: turnSignal,
|
|
975
|
+
traceId: trace.traceId,
|
|
976
|
+
acknowledge,
|
|
977
|
+
stopLocalWork,
|
|
978
|
+
interjections,
|
|
979
|
+
onTurnStart,
|
|
980
|
+
onInterjectionDelivered,
|
|
981
|
+
});
|
|
982
|
+
serverSettled = true;
|
|
983
|
+
return outcome;
|
|
984
|
+
},
|
|
985
|
+
close(completed) {
|
|
986
|
+
acknowledge.stop();
|
|
987
|
+
if (!serverSettled)
|
|
988
|
+
sendCancellation();
|
|
989
|
+
if (!completed)
|
|
990
|
+
turnController.abort(new TurnCancelledError());
|
|
991
|
+
if (session.turnAbortSignal === turnSignal)
|
|
992
|
+
session.turnAbortSignal = undefined;
|
|
993
|
+
},
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
export function isTurnOutcomeKnown(error) {
|
|
997
|
+
return error instanceof ChatTurnFailedError;
|
|
998
|
+
}
|