@thegitai/cli 1.0.0-preview.5 → 1.0.0-preview.51
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 +93 -4
- package/dist/bin/ai.js +142 -112
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +32 -7
- package/dist/src/api/auth.js +10 -4
- package/dist/src/api/browser-login.js +149 -37
- package/dist/src/api/chat.js +680 -166
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +151 -7
- package/dist/src/api/models.js +29 -10
- package/dist/src/api/startup-handshake.js +36 -0
- package/dist/src/api/turn-recovery.js +189 -0
- package/dist/src/background-jobs.js +4 -2
- package/dist/src/browser/bridge.js +262 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +219 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +130 -0
- package/dist/src/cli-args.js +19 -5
- package/dist/src/client-environment.js +2 -0
- package/dist/src/core/clipboard.js +7 -13
- package/dist/src/core/image-limits.js +56 -0
- package/dist/src/core/image-path-extractor.js +70 -3
- package/dist/src/core/session-image-store.js +246 -0
- package/dist/src/edit-journal.js +33 -2
- package/dist/src/executor.js +25 -3
- package/dist/src/help-text.js +112 -21
- package/dist/src/patcher.js +43 -4
- package/dist/src/permissions.js +288 -0
- package/dist/src/preferences.js +39 -0
- package/dist/src/quota-notice.js +35 -0
- package/dist/src/session-safety.js +0 -12
- package/dist/src/session-store.js +121 -20
- package/dist/src/session.js +19 -4
- package/dist/src/signin.js +58 -0
- package/dist/src/tool-executor.js +13 -46
- package/dist/src/tools/browser.js +641 -0
- package/dist/src/tools/delete-file.js +20 -4
- package/dist/src/tools/index.js +32 -10
- package/dist/src/tools/patch-file.js +12 -26
- package/dist/src/tools/read-file.js +5 -0
- package/dist/src/tools/read-image-file.js +85 -0
- package/dist/src/tools/replace-document-text.js +28 -18
- package/dist/src/tools/restore-checkpoint.js +0 -1
- package/dist/src/tools/run-command.js +14 -71
- package/dist/src/tools/run-node-script.js +12 -81
- package/dist/src/tools/save-generated-image.js +120 -0
- package/dist/src/tools/str-replace.js +12 -26
- package/dist/src/tools/undo-edit.js +1 -6
- package/dist/src/tools/write-file.js +67 -11
- package/dist/src/ui/prompt-history-store.js +1 -1
- package/dist/src/ui/repl.js +949 -279
- package/dist/src/ui/slash-commands.js +121 -0
- package/dist/src/ui/tui/attention.js +25 -0
- package/dist/src/ui/tui/bridge.js +38 -5
- package/dist/src/ui/tui/build-frame.js +686 -223
- package/dist/src/ui/tui/composer-layout.js +76 -0
- package/dist/src/ui/tui/desktop-notification.js +118 -0
- package/dist/src/ui/tui/markdown-render.js +11 -2
- package/dist/src/ui/tui/shell-input.js +256 -73
- package/dist/src/ui/tui/shortcuts.js +61 -0
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/user-input.js +593 -0
- package/dist/src/update-check.js +205 -0
- package/dist/src/usage.js +5 -2
- package/dist/src/utils.js +13 -0
- package/dist/src/version.js +52 -2
- package/package.json +22 -9
- package/dist/src/project-index.js +0 -233
- package/dist/src/tools/code-intel.js +0 -472
- package/dist/src/tools/find-symbol.js +0 -70
- package/dist/src/tools/hover-symbol.js +0 -95
- package/dist/src/tools/list-symbols.js +0 -55
- package/dist/src/tools/search-code.js +0 -37
- package/dist/src/tools/signature-help.js +0 -118
package/dist/src/api/chat.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
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
|
-
import { applySessionSnapshot, snapshotFromSession, } from '../session-store.js';
|
|
5
|
+
import { applySessionSnapshot, saveSessionState, snapshotFromSession, } from '../session-store.js';
|
|
4
6
|
import { executeLocalToolCall } from '../tool-executor.js';
|
|
5
|
-
import {
|
|
7
|
+
import { saveGeneratedImage } from '../tools/save-generated-image.js';
|
|
8
|
+
import { isUserInputQuestionArray } from './contracts.js';
|
|
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';
|
|
6
11
|
import { collectClientEnvironment } from '../client-environment.js';
|
|
7
12
|
import { collectProjectOrientation } from '../project-orientation.js';
|
|
13
|
+
import { describeSessionImageStore } from '../core/session-image-store.js';
|
|
8
14
|
import { autoAttachImages } from '../core/image-path-extractor.js';
|
|
9
15
|
import { formatTurnFailureMarker } from '../turn-failure-marker.js';
|
|
16
|
+
import { browserExtensionSeen, releaseBrowserAfterTurn } from '../browser/session-bridge.js';
|
|
10
17
|
export class TurnCancelledError extends Error {
|
|
11
18
|
name = 'TurnCancelledError';
|
|
12
19
|
constructor(message = 'Turn cancelled.') {
|
|
@@ -18,11 +25,13 @@ export class ChatTurnFailedError extends Error {
|
|
|
18
25
|
category;
|
|
19
26
|
retryable;
|
|
20
27
|
traceId;
|
|
21
|
-
|
|
28
|
+
partialSnapshot;
|
|
29
|
+
constructor(message, category = 'unknown_error', retryable = false, traceId = '', partialSnapshot) {
|
|
22
30
|
super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
|
|
23
31
|
this.category = category;
|
|
24
32
|
this.retryable = retryable;
|
|
25
33
|
this.traceId = traceId;
|
|
34
|
+
this.partialSnapshot = partialSnapshot;
|
|
26
35
|
}
|
|
27
36
|
}
|
|
28
37
|
export function isTurnCancelledError(error) {
|
|
@@ -32,8 +41,13 @@ export function isTurnCancelledError(error) {
|
|
|
32
41
|
}
|
|
33
42
|
function parseSseBlock(block) {
|
|
34
43
|
let event = 'message';
|
|
44
|
+
let id = '';
|
|
35
45
|
const dataLines = [];
|
|
36
46
|
for (const line of block.split(/\r?\n/)) {
|
|
47
|
+
if (line.startsWith('id:')) {
|
|
48
|
+
id = line.slice(3).trim();
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
37
51
|
if (line.startsWith('event:')) {
|
|
38
52
|
event = line.slice('event:'.length).trim() || event;
|
|
39
53
|
continue;
|
|
@@ -46,10 +60,10 @@ function parseSseBlock(block) {
|
|
|
46
60
|
return null;
|
|
47
61
|
const text = dataLines.join('\n');
|
|
48
62
|
try {
|
|
49
|
-
return { event, data: JSON.parse(text) };
|
|
63
|
+
return { id, event, data: JSON.parse(text) };
|
|
50
64
|
}
|
|
51
65
|
catch {
|
|
52
|
-
|
|
66
|
+
throw new Error('Server emitted invalid event JSON.');
|
|
53
67
|
}
|
|
54
68
|
}
|
|
55
69
|
let toolStateSeqCounter = 0;
|
|
@@ -86,7 +100,7 @@ export function preserveCancelledTurnInput(session, input) {
|
|
|
86
100
|
return;
|
|
87
101
|
for (let i = session.history.length - 1; i >= 0; i--) {
|
|
88
102
|
const entry = session.history[i];
|
|
89
|
-
if (!entry || entry.role !== 'user')
|
|
103
|
+
if (!entry || entry.role !== 'user' || entry.kind === 'userInterjection')
|
|
90
104
|
continue;
|
|
91
105
|
const prior = userHistoryText(entry);
|
|
92
106
|
if (!prior)
|
|
@@ -95,24 +109,46 @@ export function preserveCancelledTurnInput(session, input) {
|
|
|
95
109
|
return;
|
|
96
110
|
break;
|
|
97
111
|
}
|
|
98
|
-
session.history.push({
|
|
112
|
+
session.history.push({
|
|
113
|
+
role: 'user',
|
|
114
|
+
parts: [{ text }],
|
|
115
|
+
kind: 'turnStart',
|
|
116
|
+
userInput: text,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function appendTurnFailureMarker(session, category) {
|
|
120
|
+
session.history.push({
|
|
121
|
+
role: 'model',
|
|
122
|
+
parts: [{ text: formatTurnFailureMarker(category) }],
|
|
123
|
+
});
|
|
99
124
|
}
|
|
100
125
|
function preserveFailedTurnInput(session, input, category) {
|
|
101
126
|
const text = input.trim();
|
|
102
127
|
if (!text)
|
|
103
128
|
return;
|
|
104
|
-
session.history.push({ role: 'user', parts: [{ text }], kind: 'turnStart' });
|
|
105
129
|
session.history.push({
|
|
106
|
-
role: '
|
|
107
|
-
parts: [{ text
|
|
130
|
+
role: 'user',
|
|
131
|
+
parts: [{ text }],
|
|
132
|
+
kind: 'turnStart',
|
|
133
|
+
userInput: text,
|
|
108
134
|
});
|
|
135
|
+
appendTurnFailureMarker(session, category);
|
|
109
136
|
}
|
|
110
|
-
function
|
|
111
|
-
return
|
|
137
|
+
function tagTurnPhase(phase, run) {
|
|
138
|
+
return run().catch((error) => {
|
|
139
|
+
if (error && typeof error === 'object' && !error.thegitaiPhase) {
|
|
140
|
+
error.thegitaiPhase = phase;
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function turnPhaseOf(error) {
|
|
146
|
+
const phase = error?.thegitaiPhase;
|
|
147
|
+
return typeof phase === 'string' ? phase : null;
|
|
112
148
|
}
|
|
113
149
|
function preserveCancelledTurnToolResult(session, input, event, result) {
|
|
114
150
|
const callId = String(event.call.id ?? '').trim();
|
|
115
|
-
if (!callId
|
|
151
|
+
if (!callId)
|
|
116
152
|
return;
|
|
117
153
|
preserveCancelledTurnInput(session, input);
|
|
118
154
|
session.history.push({
|
|
@@ -132,6 +168,22 @@ function preserveCancelledTurnToolResult(session, input, event, result) {
|
|
|
132
168
|
],
|
|
133
169
|
});
|
|
134
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
|
+
}
|
|
135
187
|
function publicStatusMessage(data) {
|
|
136
188
|
if (!data || typeof data !== 'object')
|
|
137
189
|
return null;
|
|
@@ -145,11 +197,19 @@ function publicStatusMessage(data) {
|
|
|
145
197
|
? event.toolName
|
|
146
198
|
: 'tool';
|
|
147
199
|
if (event.phase === 'thinking')
|
|
148
|
-
return '
|
|
200
|
+
return 'Exploring options...';
|
|
201
|
+
if (event.phase === 'analyzing_image') {
|
|
202
|
+
return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
|
|
203
|
+
}
|
|
204
|
+
if (event.phase === 'generating_image') {
|
|
205
|
+
return 'Generating image...';
|
|
206
|
+
}
|
|
149
207
|
if (event.phase === 'running_tool')
|
|
150
208
|
return `Running ${toolName}...`;
|
|
151
209
|
if (event.phase === 'waiting_for_tool')
|
|
152
210
|
return `Running ${toolName} locally...`;
|
|
211
|
+
if (event.phase === 'waiting_for_user_input')
|
|
212
|
+
return 'Waiting for your input...';
|
|
153
213
|
return null;
|
|
154
214
|
}
|
|
155
215
|
function normalizeShellJobToolCall(call) {
|
|
@@ -181,15 +241,33 @@ function normalizeShellJobToolCall(call) {
|
|
|
181
241
|
}
|
|
182
242
|
return changed ? { ...call, args } : call;
|
|
183
243
|
}
|
|
184
|
-
|
|
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, }) {
|
|
185
262
|
const payload = {
|
|
263
|
+
executionId: event.executionId,
|
|
186
264
|
toolCallId: event.call.id,
|
|
187
265
|
result,
|
|
188
266
|
toolState: toolStateFromSession(session),
|
|
189
267
|
toolStateSeq: ++toolStateSeqCounter,
|
|
190
268
|
};
|
|
191
269
|
const trace = createTraceContext(traceId);
|
|
192
|
-
const response = await
|
|
270
|
+
const response = await request('posting a tool result', turnUrl(config, turnId, 'tool-result'), {
|
|
193
271
|
method: 'POST',
|
|
194
272
|
headers: {
|
|
195
273
|
authorization: `Bearer ${config.token}`,
|
|
@@ -197,14 +275,88 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
197
275
|
...trace.headers,
|
|
198
276
|
},
|
|
199
277
|
body: JSON.stringify(payload),
|
|
278
|
+
signal: delivery,
|
|
200
279
|
});
|
|
201
|
-
if (response.status === 410)
|
|
280
|
+
if (response.status === 410)
|
|
202
281
|
return;
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
203
284
|
}
|
|
285
|
+
}
|
|
286
|
+
async function postUserInputResult({ config, turnId, requestId, result, request, traceId, delivery, }) {
|
|
287
|
+
const payload = {
|
|
288
|
+
requestId,
|
|
289
|
+
result,
|
|
290
|
+
};
|
|
291
|
+
const trace = createTraceContext(traceId);
|
|
292
|
+
const response = await request('posting your answer', turnUrl(config, turnId, 'user-input-result'), {
|
|
293
|
+
method: 'POST',
|
|
294
|
+
headers: {
|
|
295
|
+
authorization: `Bearer ${config.token}`,
|
|
296
|
+
'content-type': 'application/json',
|
|
297
|
+
...trace.headers,
|
|
298
|
+
},
|
|
299
|
+
body: JSON.stringify(payload),
|
|
300
|
+
signal: delivery,
|
|
301
|
+
});
|
|
302
|
+
if (response.status === 410)
|
|
303
|
+
return;
|
|
204
304
|
if (!response.ok) {
|
|
205
305
|
throw await readErrorResponse(response, trace.traceId);
|
|
206
306
|
}
|
|
207
307
|
}
|
|
308
|
+
const activeTurnRequesters = new Map();
|
|
309
|
+
export async function postInterjection({ config, turnId, text, messageId, imageAttachments = [], fetchImpl = globalThis.fetch, traceId, }) {
|
|
310
|
+
const payload = {
|
|
311
|
+
text,
|
|
312
|
+
messageId,
|
|
313
|
+
...(imageAttachments.length > 0 ? { imageAttachments } : {}),
|
|
314
|
+
};
|
|
315
|
+
const trace = createTraceContext(traceId);
|
|
316
|
+
const url = turnUrl(config, turnId, 'interject');
|
|
317
|
+
const init = {
|
|
318
|
+
method: 'POST',
|
|
319
|
+
headers: {
|
|
320
|
+
authorization: `Bearer ${config.token}`,
|
|
321
|
+
'content-type': 'application/json',
|
|
322
|
+
...trace.headers,
|
|
323
|
+
},
|
|
324
|
+
body: JSON.stringify(payload),
|
|
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
|
+
}
|
|
350
|
+
if (response.status === 410) {
|
|
351
|
+
forget();
|
|
352
|
+
return 'stale';
|
|
353
|
+
}
|
|
354
|
+
if (!response.ok) {
|
|
355
|
+
forget();
|
|
356
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
357
|
+
}
|
|
358
|
+
return 'delivered';
|
|
359
|
+
}
|
|
208
360
|
const turnIdOverrides = new WeakMap();
|
|
209
361
|
function enterServerTurnId(session, serverSessionTurnId) {
|
|
210
362
|
const active = turnIdOverrides.get(session);
|
|
@@ -228,14 +380,13 @@ function exitServerTurnId(session) {
|
|
|
228
380
|
turnIdOverrides.delete(session);
|
|
229
381
|
}
|
|
230
382
|
}
|
|
231
|
-
async function executeAndPostToolResult({ config,
|
|
383
|
+
async function executeAndPostToolResult({ config, session, event, input, request, signal, delivery, executed, traceId, }) {
|
|
232
384
|
const turnId = String(event?.turnId ?? '').trim();
|
|
233
|
-
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
385
|
+
if (!turnId || !event?.executionId || !event?.call?.id || !event.call.name) {
|
|
234
386
|
throw new Error('Server emitted an invalid tool-call event.');
|
|
235
387
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
388
|
+
signal.throwIfAborted();
|
|
389
|
+
delivery.throwIfAborted();
|
|
239
390
|
const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
|
|
240
391
|
if (serverSessionTurnId) {
|
|
241
392
|
enterServerTurnId(session, serverSessionTurnId);
|
|
@@ -245,19 +396,28 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
245
396
|
}
|
|
246
397
|
try {
|
|
247
398
|
const call = normalizeShellJobToolCall(event.call);
|
|
248
|
-
const rawResult =
|
|
399
|
+
const rawResult = event.generatedImage
|
|
400
|
+
? saveGeneratedImage({
|
|
401
|
+
base64Data: event.generatedImage.base64Data,
|
|
402
|
+
mimeType: event.generatedImage.mimeType,
|
|
403
|
+
suggestedFilename: event.generatedImage.suggestedFilename ||
|
|
404
|
+
String(call.args?.filename ?? call.args?.file_name ?? ''),
|
|
405
|
+
})
|
|
406
|
+
: await executeLocalToolCall(session, call);
|
|
249
407
|
preserveCancelledTurnToolResult(session, input, { ...event, call }, rawResult);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
408
|
+
executed();
|
|
409
|
+
signal.throwIfAborted();
|
|
410
|
+
if (delivery.aborted)
|
|
411
|
+
return;
|
|
253
412
|
await postToolResult({
|
|
254
413
|
config,
|
|
255
414
|
turnId,
|
|
256
415
|
event,
|
|
257
416
|
result: rawResult,
|
|
258
417
|
session,
|
|
259
|
-
|
|
418
|
+
request,
|
|
260
419
|
traceId,
|
|
420
|
+
delivery,
|
|
261
421
|
});
|
|
262
422
|
}
|
|
263
423
|
finally {
|
|
@@ -266,142 +426,312 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
266
426
|
}
|
|
267
427
|
}
|
|
268
428
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
});
|
|
272
461
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
},
|
|
278
474
|
};
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
function recordParallelFailure(error) {
|
|
287
|
-
const failure = error ?? new Error('Local tool execution failed.');
|
|
288
|
-
if (firstParallelFailure == null) {
|
|
289
|
-
firstParallelFailure = failure;
|
|
290
|
-
rejectOnParallelFailure?.(failure);
|
|
291
|
-
}
|
|
292
|
-
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);
|
|
293
482
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return;
|
|
297
|
-
const pending = pendingParallelTools.splice(0);
|
|
298
|
-
const outcomes = await Promise.all(pending);
|
|
299
|
-
for (const outcome of outcomes) {
|
|
300
|
-
if (outcome != null)
|
|
301
|
-
throw outcome;
|
|
302
|
-
}
|
|
483
|
+
if (data.code === QUOTA_EXCEEDED_CODE) {
|
|
484
|
+
return new ServerApiError(message, 429, eventTraceId, QUOTA_EXCEEDED_CODE, data.details ?? {});
|
|
303
485
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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)
|
|
309
498
|
return;
|
|
310
|
-
|
|
311
|
-
|
|
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)
|
|
312
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.');
|
|
313
545
|
}
|
|
314
|
-
if (event.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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;
|
|
561
|
+
}
|
|
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;
|
|
571
|
+
}
|
|
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;
|
|
590
|
+
}
|
|
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({
|
|
318
597
|
config,
|
|
319
|
-
projectIndex,
|
|
320
598
|
session,
|
|
321
|
-
event:
|
|
599
|
+
event: toolCall,
|
|
322
600
|
input,
|
|
323
|
-
|
|
601
|
+
request,
|
|
324
602
|
signal,
|
|
603
|
+
delivery,
|
|
604
|
+
executed,
|
|
325
605
|
traceId,
|
|
326
|
-
})
|
|
327
|
-
|
|
606
|
+
}));
|
|
607
|
+
break;
|
|
328
608
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
input,
|
|
336
|
-
fetchImpl,
|
|
337
|
-
signal,
|
|
338
|
-
traceId,
|
|
339
|
-
});
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
if (event.event === 'tool-result') {
|
|
343
|
-
const data = event.data;
|
|
344
|
-
if (data?.call?.name) {
|
|
345
|
-
session.onToolEvent?.({ call: data.call, result: data.result });
|
|
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;
|
|
346
615
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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;
|
|
644
|
+
}
|
|
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;
|
|
353
654
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
+
}
|
|
359
685
|
}
|
|
360
|
-
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
await reader.cancel().catch(() => { });
|
|
361
689
|
}
|
|
362
690
|
}
|
|
363
691
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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);
|
|
368
699
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const block = buffer.slice(0, separatorIndex);
|
|
376
|
-
buffer = buffer.slice(separatorIndex + 2);
|
|
377
|
-
const event = parseSseBlock(block);
|
|
378
|
-
if (event)
|
|
379
|
-
await handleEvent(event);
|
|
380
|
-
separatorIndex = buffer.indexOf('\n\n');
|
|
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;
|
|
381
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();
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
stopLocalWork();
|
|
716
|
+
await work.settled();
|
|
717
|
+
}
|
|
718
|
+
return outcome;
|
|
382
719
|
}
|
|
383
|
-
await drainParallelTools();
|
|
384
720
|
}
|
|
385
721
|
catch (error) {
|
|
386
|
-
|
|
387
|
-
|
|
722
|
+
const outcome = signal.aborted ? signal.reason : error;
|
|
723
|
+
stopLocalWork();
|
|
724
|
+
await work.settled();
|
|
725
|
+
throw outcome;
|
|
388
726
|
}
|
|
389
727
|
finally {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (tail) {
|
|
395
|
-
const event = parseSseBlock(tail);
|
|
396
|
-
if (event)
|
|
397
|
-
await handleEvent(event);
|
|
398
|
-
}
|
|
399
|
-
if (!finalResult.current?.snapshot) {
|
|
400
|
-
throw new Error('Server returned an invalid chat result.');
|
|
728
|
+
if (registeredTurnId) {
|
|
729
|
+
activeTurnRequesters.delete(registeredTurnId);
|
|
730
|
+
turnInterjections.delete(registeredTurnId);
|
|
731
|
+
}
|
|
401
732
|
}
|
|
402
|
-
return finalResult.current;
|
|
403
733
|
}
|
|
404
|
-
export async function sendServerUserMessage({ config,
|
|
734
|
+
export async function sendServerUserMessage({ config, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, recoverySleep, }) {
|
|
405
735
|
const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
|
|
406
736
|
const requestImageAttachments = autoAttach.attachments.length > 0
|
|
407
737
|
? [...imageAttachments, ...autoAttach.attachments]
|
|
@@ -414,57 +744,99 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
414
744
|
session.onStatus(`Image: ${err}`);
|
|
415
745
|
}
|
|
416
746
|
const request = {
|
|
747
|
+
clientTurnId: randomUUID(),
|
|
417
748
|
modelId: session.modelId,
|
|
418
749
|
session: snapshotForServer(session),
|
|
419
750
|
input: requestInputBase,
|
|
420
751
|
backgroundJobUpdate: backgroundJobUpdate || undefined,
|
|
421
752
|
clientEnvironment: collectClientEnvironment({ env: session.env }),
|
|
422
753
|
projectOrientation: collectProjectOrientation(session.rootDir) ?? undefined,
|
|
754
|
+
sessionImageStore: describeSessionImageStore(session.env) ?? undefined,
|
|
423
755
|
imageAttachments: imageAttachmentsForServer(requestImageAttachments),
|
|
424
|
-
|
|
756
|
+
...(session.maxToolStepsWasExplicit || !browserExtensionSeen(session.env)
|
|
757
|
+
? { maxToolSteps: session.maxToolSteps }
|
|
758
|
+
: {}),
|
|
425
759
|
autoYes: session.autoYes,
|
|
426
760
|
agentMode: session.agentMode,
|
|
427
761
|
};
|
|
428
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();
|
|
429
783
|
const preTurnHistoryLength = session.history.length;
|
|
430
784
|
const preserveOnAbort = () => preserveCancelledTurnInput(session, requestInputBase);
|
|
431
785
|
if (signal?.aborted) {
|
|
432
786
|
preserveOnAbort();
|
|
787
|
+
cancelTurn();
|
|
433
788
|
}
|
|
434
789
|
else {
|
|
435
790
|
signal?.addEventListener('abort', preserveOnAbort, { once: true });
|
|
791
|
+
signal?.addEventListener('abort', cancelTurn, { once: true });
|
|
436
792
|
}
|
|
437
793
|
try {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
accept: 'text/event-stream',
|
|
442
|
-
authorization: `Bearer ${config.token}`,
|
|
443
|
-
'content-type': 'application/json',
|
|
444
|
-
...trace.headers,
|
|
445
|
-
},
|
|
446
|
-
body: JSON.stringify(request),
|
|
447
|
-
signal,
|
|
448
|
-
});
|
|
449
|
-
if (!response.ok) {
|
|
450
|
-
throw await readErrorResponse(response, trace.traceId);
|
|
794
|
+
let outcome;
|
|
795
|
+
try {
|
|
796
|
+
outcome = await attempt.run();
|
|
451
797
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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;
|
|
463
835
|
return {
|
|
464
|
-
text: result.text,
|
|
465
|
-
waitingForApproval: result.waitingForApproval,
|
|
466
|
-
toolBudgetReached: result.toolBudgetReached,
|
|
467
|
-
usageSummary: result.usageSummary,
|
|
836
|
+
text: outcome.result.text,
|
|
837
|
+
waitingForApproval: outcome.result.waitingForApproval,
|
|
838
|
+
toolBudgetReached: outcome.result.toolBudgetReached,
|
|
839
|
+
usageSummary: outcome.result.usageSummary,
|
|
468
840
|
};
|
|
469
841
|
}
|
|
470
842
|
catch (error) {
|
|
@@ -474,11 +846,153 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
474
846
|
? error
|
|
475
847
|
: new TurnCancelledError();
|
|
476
848
|
}
|
|
477
|
-
|
|
478
|
-
|
|
849
|
+
const exhausted = isTurnRecoveryExhausted(error);
|
|
850
|
+
const carriesTrace = error instanceof ChatTurnFailedError || error instanceof ServerApiError;
|
|
851
|
+
const transportDetail = carriesTrace || exhausted ? null : transportFailureDetail(error);
|
|
852
|
+
if (transportDetail && error instanceof Error) {
|
|
853
|
+
const phase = turnPhaseOf(error) ?? 'reaching the server';
|
|
854
|
+
error.message = `connection lost while ${phase}: ${transportDetail} [trace ${trace.traceId}]`;
|
|
855
|
+
}
|
|
856
|
+
else if (!carriesTrace && error instanceof Error) {
|
|
857
|
+
error.message = `${error.message} [trace ${trace.traceId}]`;
|
|
858
|
+
}
|
|
859
|
+
const category = error instanceof ChatTurnFailedError
|
|
860
|
+
? error.category
|
|
861
|
+
: exhausted
|
|
862
|
+
? 'connection_error'
|
|
863
|
+
:
|
|
864
|
+
(gatewayFailureCategory(error) ?? 'unknown_error');
|
|
865
|
+
const partial = error instanceof ChatTurnFailedError ? error.partialSnapshot : undefined;
|
|
866
|
+
if (partial) {
|
|
867
|
+
applySessionSnapshot(session, partial, { preserveAgentMode: true });
|
|
868
|
+
}
|
|
869
|
+
else if (session.history.length === preTurnHistoryLength) {
|
|
870
|
+
preserveFailedTurnInput(session, requestInputBase, category);
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
appendTurnFailureMarker(session, category);
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
saveSessionState(session, session.env);
|
|
877
|
+
}
|
|
878
|
+
catch { }
|
|
479
879
|
throw error;
|
|
480
880
|
}
|
|
481
881
|
finally {
|
|
882
|
+
turnEnded = true;
|
|
482
883
|
signal?.removeEventListener('abort', preserveOnAbort);
|
|
884
|
+
signal?.removeEventListener('abort', cancelTurn);
|
|
885
|
+
attempt.close(completed);
|
|
886
|
+
releaseBrowserAfterTurn();
|
|
483
887
|
}
|
|
484
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
|
+
}
|