@thegitai/cli 1.0.0-preview.4 → 1.0.0-preview.40
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 +49 -4
- package/dist/bin/ai.js +119 -111
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +11 -6
- package/dist/src/api/auth.js +6 -4
- package/dist/src/api/browser-login.js +152 -37
- package/dist/src/api/chat.js +286 -44
- package/dist/src/api/contracts.js +55 -1
- package/dist/src/api/default-host.js +1 -0
- package/dist/src/api/http.js +127 -7
- package/dist/src/api/models.js +29 -10
- package/dist/src/background-jobs.js +4 -2
- package/dist/src/browser/bridge.js +232 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +209 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +104 -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 +58 -17
- package/dist/src/patcher.js +43 -4
- package/dist/src/permissions.js +288 -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 +12 -46
- package/dist/src/tools/browser.js +628 -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-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 +802 -195
- package/dist/src/ui/tui/bridge.js +10 -0
- package/dist/src/ui/tui/build-frame.js +517 -165
- package/dist/src/ui/tui/composer-layout.js +76 -0
- package/dist/src/ui/tui/markdown-render.js +81 -73
- package/dist/src/ui/tui/shell-input.js +227 -67
- package/dist/src/ui/tui/terminal-theme.js +28 -0
- package/dist/src/ui/tui/terminal-title.js +3 -0
- package/dist/src/ui/tui/terminal-writes.js +48 -0
- package/dist/src/ui/tui/text.js +158 -4
- package/dist/src/ui/tui/user-input.js +568 -0
- package/dist/src/utils.js +9 -0
- package/package.json +20 -8
- package/dist/src/markdown-renderer.js +0 -112
- package/dist/src/project-index.js +0 -221
- 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,16 @@
|
|
|
1
1
|
import { drainBackgroundJobNotifications } from '../background-jobs.js';
|
|
2
2
|
import { createPromptCheckpoint, sanitizeSessionSafetyForServer, } from '../session-safety.js';
|
|
3
|
-
import { applySessionSnapshot, snapshotFromSession, } from '../session-store.js';
|
|
3
|
+
import { applySessionSnapshot, saveSessionState, snapshotFromSession, } from '../session-store.js';
|
|
4
4
|
import { executeLocalToolCall } from '../tool-executor.js';
|
|
5
|
-
import {
|
|
5
|
+
import { saveGeneratedImage } from '../tools/save-generated-image.js';
|
|
6
|
+
import { isUserInputQuestionArray } from './contracts.js';
|
|
7
|
+
import { createTraceContext, gatewayFailureCategory, transportFailureDetail, normalizeServerUrl, readErrorResponse, } from './http.js';
|
|
6
8
|
import { collectClientEnvironment } from '../client-environment.js';
|
|
7
9
|
import { collectProjectOrientation } from '../project-orientation.js';
|
|
10
|
+
import { describeSessionImageStore } from '../core/session-image-store.js';
|
|
8
11
|
import { autoAttachImages } from '../core/image-path-extractor.js';
|
|
9
12
|
import { formatTurnFailureMarker } from '../turn-failure-marker.js';
|
|
13
|
+
import { browserExtensionSeen, releaseBrowserAfterTurn } from '../browser/session-bridge.js';
|
|
10
14
|
export class TurnCancelledError extends Error {
|
|
11
15
|
name = 'TurnCancelledError';
|
|
12
16
|
constructor(message = 'Turn cancelled.') {
|
|
@@ -18,11 +22,13 @@ export class ChatTurnFailedError extends Error {
|
|
|
18
22
|
category;
|
|
19
23
|
retryable;
|
|
20
24
|
traceId;
|
|
21
|
-
|
|
25
|
+
partialSnapshot;
|
|
26
|
+
constructor(message, category = 'unknown_error', retryable = false, traceId = '', partialSnapshot) {
|
|
22
27
|
super(traceId ? `${message}\nTrace ID: ${traceId}` : message);
|
|
23
28
|
this.category = category;
|
|
24
29
|
this.retryable = retryable;
|
|
25
30
|
this.traceId = traceId;
|
|
31
|
+
this.partialSnapshot = partialSnapshot;
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
export function isTurnCancelledError(error) {
|
|
@@ -52,6 +58,7 @@ function parseSseBlock(block) {
|
|
|
52
58
|
return { event, data: text };
|
|
53
59
|
}
|
|
54
60
|
}
|
|
61
|
+
let toolStateSeqCounter = 0;
|
|
55
62
|
function toolStateFromSession(session) {
|
|
56
63
|
return {
|
|
57
64
|
autoYes: session.autoYes,
|
|
@@ -94,18 +101,43 @@ export function preserveCancelledTurnInput(session, input) {
|
|
|
94
101
|
return;
|
|
95
102
|
break;
|
|
96
103
|
}
|
|
97
|
-
session.history.push({
|
|
104
|
+
session.history.push({
|
|
105
|
+
role: 'user',
|
|
106
|
+
parts: [{ text }],
|
|
107
|
+
kind: 'turnStart',
|
|
108
|
+
userInput: text,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
function appendTurnFailureMarker(session, category) {
|
|
112
|
+
session.history.push({
|
|
113
|
+
role: 'model',
|
|
114
|
+
parts: [{ text: formatTurnFailureMarker(category) }],
|
|
115
|
+
});
|
|
98
116
|
}
|
|
99
117
|
function preserveFailedTurnInput(session, input, category) {
|
|
100
118
|
const text = input.trim();
|
|
101
119
|
if (!text)
|
|
102
120
|
return;
|
|
103
|
-
session.history.push({ role: 'user', parts: [{ text }], kind: 'turnStart' });
|
|
104
121
|
session.history.push({
|
|
105
|
-
role: '
|
|
106
|
-
parts: [{ text
|
|
122
|
+
role: 'user',
|
|
123
|
+
parts: [{ text }],
|
|
124
|
+
kind: 'turnStart',
|
|
125
|
+
userInput: text,
|
|
126
|
+
});
|
|
127
|
+
appendTurnFailureMarker(session, category);
|
|
128
|
+
}
|
|
129
|
+
function tagTurnPhase(phase, run) {
|
|
130
|
+
return run().catch((error) => {
|
|
131
|
+
if (error && typeof error === 'object' && !error.thegitaiPhase) {
|
|
132
|
+
error.thegitaiPhase = phase;
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
107
135
|
});
|
|
108
136
|
}
|
|
137
|
+
function turnPhaseOf(error) {
|
|
138
|
+
const phase = error?.thegitaiPhase;
|
|
139
|
+
return typeof phase === 'string' ? phase : null;
|
|
140
|
+
}
|
|
109
141
|
function historyHasToolCall(session, callId) {
|
|
110
142
|
return session.history.some((entry) => (entry.parts ?? []).some((part) => String(part?.functionCall?.id ?? '') === callId));
|
|
111
143
|
}
|
|
@@ -144,11 +176,19 @@ function publicStatusMessage(data) {
|
|
|
144
176
|
? event.toolName
|
|
145
177
|
: 'tool';
|
|
146
178
|
if (event.phase === 'thinking')
|
|
147
|
-
return '
|
|
179
|
+
return 'Exploring options...';
|
|
180
|
+
if (event.phase === 'analyzing_image') {
|
|
181
|
+
return (event.imageCount ?? 1) > 1 ? 'Analyzing images...' : 'Analyzing image...';
|
|
182
|
+
}
|
|
183
|
+
if (event.phase === 'generating_image') {
|
|
184
|
+
return 'Generating image...';
|
|
185
|
+
}
|
|
148
186
|
if (event.phase === 'running_tool')
|
|
149
187
|
return `Running ${toolName}...`;
|
|
150
188
|
if (event.phase === 'waiting_for_tool')
|
|
151
189
|
return `Running ${toolName} locally...`;
|
|
190
|
+
if (event.phase === 'waiting_for_user_input')
|
|
191
|
+
return 'Waiting for your input...';
|
|
152
192
|
return null;
|
|
153
193
|
}
|
|
154
194
|
function normalizeShellJobToolCall(call) {
|
|
@@ -185,9 +225,32 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
185
225
|
toolCallId: event.call.id,
|
|
186
226
|
result,
|
|
187
227
|
toolState: toolStateFromSession(session),
|
|
228
|
+
toolStateSeq: ++toolStateSeqCounter,
|
|
229
|
+
};
|
|
230
|
+
const trace = createTraceContext(traceId);
|
|
231
|
+
const response = await tagTurnPhase('posting a tool result', () => fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/tool-result`, {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: {
|
|
234
|
+
authorization: `Bearer ${config.token}`,
|
|
235
|
+
'content-type': 'application/json',
|
|
236
|
+
...trace.headers,
|
|
237
|
+
},
|
|
238
|
+
body: JSON.stringify(payload),
|
|
239
|
+
}));
|
|
240
|
+
if (response.status === 410) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function postUserInputResult({ config, turnId, requestId, result, fetchImpl, traceId, }) {
|
|
248
|
+
const payload = {
|
|
249
|
+
requestId,
|
|
250
|
+
result,
|
|
188
251
|
};
|
|
189
252
|
const trace = createTraceContext(traceId);
|
|
190
|
-
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/
|
|
253
|
+
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/user-input-result`, {
|
|
191
254
|
method: 'POST',
|
|
192
255
|
headers: {
|
|
193
256
|
authorization: `Bearer ${config.token}`,
|
|
@@ -203,7 +266,54 @@ async function postToolResult({ config, turnId, event, result, session, fetchImp
|
|
|
203
266
|
throw await readErrorResponse(response, trace.traceId);
|
|
204
267
|
}
|
|
205
268
|
}
|
|
206
|
-
async function
|
|
269
|
+
export async function postInterjection({ config, turnId, text, messageId, imageAttachments = [], fetchImpl = globalThis.fetch, traceId, }) {
|
|
270
|
+
const payload = {
|
|
271
|
+
text,
|
|
272
|
+
messageId,
|
|
273
|
+
...(imageAttachments.length > 0 ? { imageAttachments } : {}),
|
|
274
|
+
};
|
|
275
|
+
const trace = createTraceContext(traceId);
|
|
276
|
+
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn/${encodeURIComponent(turnId)}/interject`, {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
headers: {
|
|
279
|
+
authorization: `Bearer ${config.token}`,
|
|
280
|
+
'content-type': 'application/json',
|
|
281
|
+
...trace.headers,
|
|
282
|
+
},
|
|
283
|
+
body: JSON.stringify(payload),
|
|
284
|
+
});
|
|
285
|
+
if (response.status === 410) {
|
|
286
|
+
return 'stale';
|
|
287
|
+
}
|
|
288
|
+
if (!response.ok) {
|
|
289
|
+
throw await readErrorResponse(response, trace.traceId);
|
|
290
|
+
}
|
|
291
|
+
return 'delivered';
|
|
292
|
+
}
|
|
293
|
+
const turnIdOverrides = new WeakMap();
|
|
294
|
+
function enterServerTurnId(session, serverSessionTurnId) {
|
|
295
|
+
const active = turnIdOverrides.get(session);
|
|
296
|
+
if (active) {
|
|
297
|
+
active.depth += 1;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
turnIdOverrides.set(session, {
|
|
301
|
+
previousTurnId: session.turnState.id,
|
|
302
|
+
depth: 1,
|
|
303
|
+
});
|
|
304
|
+
session.turnState.id = serverSessionTurnId;
|
|
305
|
+
}
|
|
306
|
+
function exitServerTurnId(session) {
|
|
307
|
+
const active = turnIdOverrides.get(session);
|
|
308
|
+
if (!active)
|
|
309
|
+
return;
|
|
310
|
+
active.depth -= 1;
|
|
311
|
+
if (active.depth === 0) {
|
|
312
|
+
session.turnState.id = active.previousTurnId;
|
|
313
|
+
turnIdOverrides.delete(session);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
async function executeAndPostToolResult({ config, session, event, input, fetchImpl, signal, traceId, }) {
|
|
207
317
|
const turnId = String(event?.turnId ?? '').trim();
|
|
208
318
|
if (!turnId || !event?.call?.id || !event.call.name) {
|
|
209
319
|
throw new Error('Server emitted an invalid tool-call event.');
|
|
@@ -211,17 +321,23 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
211
321
|
if (signal?.aborted) {
|
|
212
322
|
throw new TurnCancelledError();
|
|
213
323
|
}
|
|
214
|
-
const previousTurnId = session.turnState.id;
|
|
215
324
|
const serverSessionTurnId = String(event.sessionTurnId ?? '').trim();
|
|
216
325
|
if (serverSessionTurnId) {
|
|
217
|
-
session
|
|
326
|
+
enterServerTurnId(session, serverSessionTurnId);
|
|
218
327
|
if (!session.clientState.safety.checkpoints.some((checkpoint) => checkpoint.turnId === serverSessionTurnId)) {
|
|
219
328
|
createPromptCheckpoint(session.clientState.safety, 'prompt boundary', serverSessionTurnId);
|
|
220
329
|
}
|
|
221
330
|
}
|
|
222
331
|
try {
|
|
223
332
|
const call = normalizeShellJobToolCall(event.call);
|
|
224
|
-
const rawResult =
|
|
333
|
+
const rawResult = event.generatedImage
|
|
334
|
+
? saveGeneratedImage({
|
|
335
|
+
base64Data: event.generatedImage.base64Data,
|
|
336
|
+
mimeType: event.generatedImage.mimeType,
|
|
337
|
+
suggestedFilename: event.generatedImage.suggestedFilename ||
|
|
338
|
+
String(call.args?.filename ?? call.args?.file_name ?? ''),
|
|
339
|
+
})
|
|
340
|
+
: await executeLocalToolCall(session, call);
|
|
225
341
|
preserveCancelledTurnToolResult(session, input, { ...event, call }, rawResult);
|
|
226
342
|
if (signal?.aborted) {
|
|
227
343
|
throw new TurnCancelledError();
|
|
@@ -237,10 +353,12 @@ async function executeAndPostToolResult({ config, projectIndex, session, event,
|
|
|
237
353
|
});
|
|
238
354
|
}
|
|
239
355
|
finally {
|
|
240
|
-
|
|
356
|
+
if (serverSessionTurnId) {
|
|
357
|
+
exitServerTurnId(session);
|
|
358
|
+
}
|
|
241
359
|
}
|
|
242
360
|
}
|
|
243
|
-
async function consumeTurnStream({ response, config,
|
|
361
|
+
async function consumeTurnStream({ response, config, session, input, fetchImpl, signal, traceId, onTurnStart, onInterjectionDelivered, }) {
|
|
244
362
|
if (!response.body) {
|
|
245
363
|
throw new Error('Server returned an empty chat stream.');
|
|
246
364
|
}
|
|
@@ -250,8 +368,56 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
250
368
|
const finalResult = {
|
|
251
369
|
current: null,
|
|
252
370
|
};
|
|
371
|
+
const pendingParallelTools = [];
|
|
372
|
+
let firstParallelFailure = null;
|
|
373
|
+
let rejectOnParallelFailure = null;
|
|
374
|
+
const parallelToolFailure = new Promise((_, reject) => {
|
|
375
|
+
rejectOnParallelFailure = reject;
|
|
376
|
+
});
|
|
377
|
+
parallelToolFailure.catch(() => { });
|
|
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;
|
|
385
|
+
}
|
|
386
|
+
async function drainParallelTools() {
|
|
387
|
+
if (!pendingParallelTools.length)
|
|
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
|
+
}
|
|
395
|
+
}
|
|
253
396
|
async function handleEvent(event) {
|
|
397
|
+
if (event.event === 'turn-start') {
|
|
398
|
+
const turnId = String(event.data?.turnId ?? '').trim();
|
|
399
|
+
if (turnId)
|
|
400
|
+
onTurnStart?.(turnId);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (event.event === 'interjection-delivered') {
|
|
404
|
+
onInterjectionDelivered?.(event.data);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
254
407
|
if (event.event === 'status') {
|
|
408
|
+
const data = event.data;
|
|
409
|
+
if (data?.phase === 'analyzing_image') {
|
|
410
|
+
session.onImageAnalysis?.(Math.max(1, Number(data.imageCount ?? 1) || 1));
|
|
411
|
+
session.onImageGeneration?.(false);
|
|
412
|
+
}
|
|
413
|
+
else if (data?.phase === 'generating_image') {
|
|
414
|
+
session.onImageGeneration?.(true);
|
|
415
|
+
session.onImageAnalysis?.(0);
|
|
416
|
+
}
|
|
417
|
+
else if (data?.phase) {
|
|
418
|
+
session.onImageAnalysis?.(0);
|
|
419
|
+
session.onImageGeneration?.(false);
|
|
420
|
+
}
|
|
255
421
|
const message = publicStatusMessage(event.data);
|
|
256
422
|
if (message)
|
|
257
423
|
session.onStatus(message);
|
|
@@ -261,11 +427,24 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
261
427
|
return;
|
|
262
428
|
}
|
|
263
429
|
if (event.event === 'tool-call') {
|
|
430
|
+
const data = event.data;
|
|
431
|
+
if (data?.parallelSafe === true) {
|
|
432
|
+
pendingParallelTools.push(executeAndPostToolResult({
|
|
433
|
+
config,
|
|
434
|
+
session,
|
|
435
|
+
event: data,
|
|
436
|
+
input,
|
|
437
|
+
fetchImpl,
|
|
438
|
+
signal,
|
|
439
|
+
traceId,
|
|
440
|
+
}).then(() => null, (error) => recordParallelFailure(error)));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
await drainParallelTools();
|
|
264
444
|
await executeAndPostToolResult({
|
|
265
445
|
config,
|
|
266
|
-
projectIndex,
|
|
267
446
|
session,
|
|
268
|
-
event:
|
|
447
|
+
event: data,
|
|
269
448
|
input,
|
|
270
449
|
fetchImpl,
|
|
271
450
|
signal,
|
|
@@ -280,36 +459,75 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
280
459
|
}
|
|
281
460
|
return;
|
|
282
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.');
|
|
471
|
+
}
|
|
472
|
+
if (!session.requestUserInput) {
|
|
473
|
+
throw new Error('Interactive user input is unavailable in this client.');
|
|
474
|
+
}
|
|
475
|
+
const result = await session.requestUserInput({ questions: data.questions }, signal);
|
|
476
|
+
if (signal?.aborted) {
|
|
477
|
+
throw new TurnCancelledError();
|
|
478
|
+
}
|
|
479
|
+
await postUserInputResult({
|
|
480
|
+
config,
|
|
481
|
+
turnId,
|
|
482
|
+
requestId,
|
|
483
|
+
result,
|
|
484
|
+
fetchImpl,
|
|
485
|
+
traceId,
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
283
489
|
if (event.event === 'result') {
|
|
490
|
+
await drainParallelTools();
|
|
284
491
|
finalResult.current = event.data;
|
|
285
492
|
return;
|
|
286
493
|
}
|
|
287
494
|
if (event.event === 'cancelled' || event.event === 'error') {
|
|
495
|
+
await drainParallelTools().catch(() => { });
|
|
288
496
|
const message = String(event.data?.message ?? 'Server chat failed.');
|
|
289
497
|
if (event.event === 'cancelled') {
|
|
290
498
|
throw new TurnCancelledError(message);
|
|
291
499
|
}
|
|
292
|
-
throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable), typeof event.data?.traceId === 'string' ? event.data.traceId : traceId);
|
|
500
|
+
throw new ChatTurnFailedError(message, typeof event.data?.category === 'string' ? event.data.category : 'unknown_error', Boolean(event.data?.retryable), typeof event.data?.traceId === 'string' ? event.data.traceId : traceId, event.data?.snapshot);
|
|
293
501
|
}
|
|
294
502
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
503
|
+
try {
|
|
504
|
+
while (true) {
|
|
505
|
+
if (signal?.aborted) {
|
|
506
|
+
await reader.cancel().catch(() => { });
|
|
507
|
+
throw new TurnCancelledError();
|
|
508
|
+
}
|
|
509
|
+
const read = await Promise.race([reader.read(), parallelToolFailure]);
|
|
510
|
+
if (read.done)
|
|
511
|
+
break;
|
|
512
|
+
buffer += decoder.decode(read.value, { stream: true });
|
|
513
|
+
let separatorIndex = buffer.indexOf('\n\n');
|
|
514
|
+
while (separatorIndex !== -1) {
|
|
515
|
+
const block = buffer.slice(0, separatorIndex);
|
|
516
|
+
buffer = buffer.slice(separatorIndex + 2);
|
|
517
|
+
const event = parseSseBlock(block);
|
|
518
|
+
if (event)
|
|
519
|
+
await handleEvent(event);
|
|
520
|
+
separatorIndex = buffer.indexOf('\n\n');
|
|
521
|
+
}
|
|
312
522
|
}
|
|
523
|
+
await drainParallelTools();
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
await reader.cancel().catch(() => { });
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
finally {
|
|
530
|
+
await drainParallelTools().catch(() => { });
|
|
313
531
|
}
|
|
314
532
|
buffer += decoder.decode();
|
|
315
533
|
const tail = buffer.trim();
|
|
@@ -323,7 +541,7 @@ async function consumeTurnStream({ response, config, projectIndex, session, inpu
|
|
|
323
541
|
}
|
|
324
542
|
return finalResult.current;
|
|
325
543
|
}
|
|
326
|
-
export async function sendServerUserMessage({ config,
|
|
544
|
+
export async function sendServerUserMessage({ config, session, input, imageAttachments = [], fetchImpl = globalThis.fetch, signal, onTurnStart, onInterjectionDelivered, }) {
|
|
327
545
|
const autoAttach = autoAttachImages(input, session.rootDir, imageAttachments);
|
|
328
546
|
const requestImageAttachments = autoAttach.attachments.length > 0
|
|
329
547
|
? [...imageAttachments, ...autoAttach.attachments]
|
|
@@ -342,8 +560,11 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
342
560
|
backgroundJobUpdate: backgroundJobUpdate || undefined,
|
|
343
561
|
clientEnvironment: collectClientEnvironment({ env: session.env }),
|
|
344
562
|
projectOrientation: collectProjectOrientation(session.rootDir) ?? undefined,
|
|
563
|
+
sessionImageStore: describeSessionImageStore(session.env) ?? undefined,
|
|
345
564
|
imageAttachments: imageAttachmentsForServer(requestImageAttachments),
|
|
346
|
-
|
|
565
|
+
...(session.maxToolStepsWasExplicit || !browserExtensionSeen(session.env)
|
|
566
|
+
? { maxToolSteps: session.maxToolSteps }
|
|
567
|
+
: {}),
|
|
347
568
|
autoYes: session.autoYes,
|
|
348
569
|
agentMode: session.agentMode,
|
|
349
570
|
};
|
|
@@ -357,7 +578,7 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
357
578
|
signal?.addEventListener('abort', preserveOnAbort, { once: true });
|
|
358
579
|
}
|
|
359
580
|
try {
|
|
360
|
-
const response = await fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn`, {
|
|
581
|
+
const response = await tagTurnPhase('opening the turn', () => fetchImpl(`${normalizeServerUrl(config.serverUrl)}/v1/chat/turn`, {
|
|
361
582
|
method: 'POST',
|
|
362
583
|
headers: {
|
|
363
584
|
accept: 'text/event-stream',
|
|
@@ -367,20 +588,21 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
367
588
|
},
|
|
368
589
|
body: JSON.stringify(request),
|
|
369
590
|
signal,
|
|
370
|
-
});
|
|
591
|
+
}));
|
|
371
592
|
if (!response.ok) {
|
|
372
593
|
throw await readErrorResponse(response, trace.traceId);
|
|
373
594
|
}
|
|
374
|
-
const result = await consumeTurnStream({
|
|
595
|
+
const result = await tagTurnPhase('reading the turn stream', () => consumeTurnStream({
|
|
375
596
|
response,
|
|
376
597
|
config,
|
|
377
|
-
projectIndex,
|
|
378
598
|
session,
|
|
379
599
|
input: requestInputBase,
|
|
380
600
|
fetchImpl,
|
|
381
601
|
signal,
|
|
382
602
|
traceId: trace.traceId,
|
|
383
|
-
|
|
603
|
+
onTurnStart,
|
|
604
|
+
onInterjectionDelivered,
|
|
605
|
+
}));
|
|
384
606
|
applySessionSnapshot(session, result.snapshot, { preserveAgentMode: true });
|
|
385
607
|
return {
|
|
386
608
|
text: result.text,
|
|
@@ -396,11 +618,31 @@ export async function sendServerUserMessage({ config, projectIndex, session, inp
|
|
|
396
618
|
? error
|
|
397
619
|
: new TurnCancelledError();
|
|
398
620
|
}
|
|
399
|
-
|
|
400
|
-
|
|
621
|
+
const transportDetail = error instanceof ChatTurnFailedError ? null : transportFailureDetail(error);
|
|
622
|
+
if (transportDetail && error instanceof Error) {
|
|
623
|
+
const phase = turnPhaseOf(error) ?? 'reaching the server';
|
|
624
|
+
error.message = `connection lost while ${phase}: ${transportDetail} [trace ${trace.traceId}]`;
|
|
625
|
+
}
|
|
626
|
+
const category = error instanceof ChatTurnFailedError
|
|
627
|
+
? error.category
|
|
628
|
+
:
|
|
629
|
+
gatewayFailureCategory(error) ?? 'unknown_error';
|
|
630
|
+
const partial = error instanceof ChatTurnFailedError ? error.partialSnapshot : undefined;
|
|
631
|
+
if (partial) {
|
|
632
|
+
applySessionSnapshot(session, partial, { preserveAgentMode: true });
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
session.history.length = preTurnHistoryLength;
|
|
636
|
+
preserveFailedTurnInput(session, requestInputBase, category);
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
saveSessionState(session, session.env);
|
|
640
|
+
}
|
|
641
|
+
catch { }
|
|
401
642
|
throw error;
|
|
402
643
|
}
|
|
403
644
|
finally {
|
|
404
645
|
signal?.removeEventListener('abort', preserveOnAbort);
|
|
646
|
+
releaseBrowserAfterTurn();
|
|
405
647
|
}
|
|
406
648
|
}
|
|
@@ -1 +1,55 @@
|
|
|
1
|
-
|
|
1
|
+
function nonEmptyString(value) {
|
|
2
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
3
|
+
}
|
|
4
|
+
const USER_INPUT_ID_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
5
|
+
export function isUserInputQuestionArray(value) {
|
|
6
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 4) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
const questionIds = new Set();
|
|
10
|
+
return value.every((question) => {
|
|
11
|
+
if (!question || typeof question !== 'object' || Array.isArray(question)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const candidate = question;
|
|
15
|
+
if (!nonEmptyString(candidate.id) ||
|
|
16
|
+
!USER_INPUT_ID_PATTERN.test(candidate.id) ||
|
|
17
|
+
questionIds.has(candidate.id) ||
|
|
18
|
+
!nonEmptyString(candidate.header) ||
|
|
19
|
+
Array.from(candidate.header).length > 12 ||
|
|
20
|
+
!nonEmptyString(candidate.question) ||
|
|
21
|
+
typeof candidate.multiSelect !== 'boolean' ||
|
|
22
|
+
!Array.isArray(candidate.options) ||
|
|
23
|
+
candidate.options.length < 2 ||
|
|
24
|
+
candidate.options.length > 4) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
questionIds.add(candidate.id);
|
|
28
|
+
const optionIds = new Set();
|
|
29
|
+
const optionsValid = candidate.options.every((option) => {
|
|
30
|
+
if (!option || typeof option !== 'object' || Array.isArray(option)) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const item = option;
|
|
34
|
+
if (!nonEmptyString(item.id) ||
|
|
35
|
+
!USER_INPUT_ID_PATTERN.test(item.id) ||
|
|
36
|
+
optionIds.has(item.id) ||
|
|
37
|
+
!nonEmptyString(item.label) ||
|
|
38
|
+
!nonEmptyString(item.description)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
optionIds.add(item.id);
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
if (!optionsValid) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
if (candidate.recommendedOptionId !== undefined &&
|
|
48
|
+
(!nonEmptyString(candidate.recommendedOptionId) ||
|
|
49
|
+
candidate.recommendedOptionId !==
|
|
50
|
+
candidate.options[0].id)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_THEGITAI_HOST = 'https://thegit.ai';
|