@coder/ai-sdk-agent 0.4.2 → 0.4.3
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 +33 -5
- package/dist/index.d.ts +55 -5
- package/dist/index.js +386 -86
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "ai";
|
|
6
6
|
|
|
7
7
|
// src/errors.ts
|
|
8
|
+
import { APICallError } from "@ai-sdk/provider";
|
|
8
9
|
var CoderAgentError = class extends Error {
|
|
9
10
|
name = "CoderAgentError";
|
|
10
11
|
constructor(message, options) {
|
|
@@ -27,6 +28,22 @@ var CoderApiError = class extends CoderAgentError {
|
|
|
27
28
|
this.path = args.path;
|
|
28
29
|
}
|
|
29
30
|
};
|
|
31
|
+
var CoderStreamError = class extends APICallError {
|
|
32
|
+
name = "CoderStreamError";
|
|
33
|
+
constructor(args) {
|
|
34
|
+
super({
|
|
35
|
+
message: args.message,
|
|
36
|
+
url: args.url,
|
|
37
|
+
requestBodyValues: void 0,
|
|
38
|
+
// Retries re-invoke the model with the SAME prompt, so the model layer
|
|
39
|
+
// downgrades this to false whenever the failed chat has prior state a
|
|
40
|
+
// re-submission would corrupt, or server-side tooling (workspace/MCP)
|
|
41
|
+
// whose effects a replay would duplicate (see #runTurn).
|
|
42
|
+
isRetryable: args.isRetryable ?? true,
|
|
43
|
+
cause: args.cause
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
};
|
|
30
47
|
var CoderChatError = class extends CoderAgentError {
|
|
31
48
|
name = "CoderChatError";
|
|
32
49
|
kind;
|
|
@@ -154,92 +171,209 @@ function httpToWs(baseUrl) {
|
|
|
154
171
|
if (baseUrl.startsWith("http://")) return `ws://${baseUrl.slice("http://".length)}`;
|
|
155
172
|
return baseUrl;
|
|
156
173
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
var STREAM_BACKOFF_INITIAL_MS = 1e3;
|
|
175
|
+
var STREAM_BACKOFF_CAP_MS = 3e4;
|
|
176
|
+
var STREAM_MAX_CONSECUTIVE_FAILURES = 5;
|
|
177
|
+
var TRANSIENT_UPGRADE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429]);
|
|
178
|
+
function streamChatEvents(options) {
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
const external = options.signal;
|
|
181
|
+
const chain = () => controller.abort(external?.reason);
|
|
182
|
+
if (external) {
|
|
183
|
+
if (external.aborted) chain();
|
|
184
|
+
else external.addEventListener("abort", chain, { once: true });
|
|
185
|
+
}
|
|
186
|
+
const inner = streamChatEventsLoop({ ...options, signal: controller.signal });
|
|
187
|
+
const detach = () => external?.removeEventListener("abort", chain);
|
|
188
|
+
const finish = async () => {
|
|
189
|
+
controller.abort();
|
|
190
|
+
detach();
|
|
174
191
|
try {
|
|
175
|
-
|
|
192
|
+
await inner.return();
|
|
176
193
|
} catch {
|
|
177
194
|
}
|
|
178
|
-
wake();
|
|
179
195
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
return {
|
|
197
|
+
// Detach the signal chain once the loop settles for good (done, or a
|
|
198
|
+
// terminal failure) — the iteration protocol never calls return() on a
|
|
199
|
+
// failed iterator, and the {once} listener would otherwise pile up on a
|
|
200
|
+
// long-lived signal across re-created readers.
|
|
201
|
+
async next() {
|
|
202
|
+
try {
|
|
203
|
+
const result = await inner.next();
|
|
204
|
+
if (result.done) detach();
|
|
205
|
+
return result;
|
|
206
|
+
} catch (err) {
|
|
207
|
+
detach();
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
async return() {
|
|
212
|
+
await finish();
|
|
213
|
+
return { done: true, value: void 0 };
|
|
214
|
+
},
|
|
215
|
+
async throw(err) {
|
|
216
|
+
await finish();
|
|
217
|
+
throw err;
|
|
218
|
+
},
|
|
219
|
+
[Symbol.asyncIterator]() {
|
|
220
|
+
return this;
|
|
221
|
+
},
|
|
222
|
+
// Native async generators are async-disposable; the wrapper must be too.
|
|
223
|
+
async [Symbol.asyncDispose]() {
|
|
224
|
+
await finish();
|
|
186
225
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async function* streamChatEventsLoop(options) {
|
|
229
|
+
const { baseUrl, token, chatId, signal } = options;
|
|
230
|
+
const factory = options.webSocketFactory ?? defaultFactory;
|
|
231
|
+
const wsBase = httpToWs(baseUrl);
|
|
232
|
+
const path = `/api/experimental/chats/${chatId}/stream`;
|
|
233
|
+
const query = options.afterId !== void 0 ? `?after_id=${options.afterId}` : "";
|
|
234
|
+
const url = `${wsBase}${path}${query}`;
|
|
235
|
+
let maxCommittedId = options.afterId;
|
|
236
|
+
let lastHistoryVersion;
|
|
237
|
+
let lastGenerationAttempt;
|
|
238
|
+
let lastSeq;
|
|
239
|
+
let sawUntrackableDelta = false;
|
|
240
|
+
let backoffMs = STREAM_BACKOFF_INITIAL_MS;
|
|
241
|
+
let failures = 0;
|
|
242
|
+
let lastFailure;
|
|
243
|
+
while (!signal?.aborted) {
|
|
244
|
+
const queue = [];
|
|
245
|
+
let resolveNext;
|
|
246
|
+
let finished = false;
|
|
247
|
+
let failure;
|
|
248
|
+
let parseFailure = false;
|
|
249
|
+
const wake = () => {
|
|
250
|
+
resolveNext?.();
|
|
251
|
+
resolveNext = void 0;
|
|
252
|
+
};
|
|
253
|
+
const ws = factory(url, { headers: { "Coder-Session-Token": token } });
|
|
254
|
+
const onAbort = () => {
|
|
196
255
|
finished = true;
|
|
256
|
+
try {
|
|
257
|
+
ws.close(1e3);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
197
260
|
wake();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
261
|
+
};
|
|
262
|
+
let abortListenerAdded = false;
|
|
263
|
+
if (signal) {
|
|
264
|
+
if (signal.aborted) onAbort();
|
|
265
|
+
else {
|
|
266
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
267
|
+
abortListenerAdded = true;
|
|
268
|
+
}
|
|
204
269
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
while (true) {
|
|
223
|
-
while (queue.length > 0) {
|
|
224
|
-
const next = queue.shift();
|
|
225
|
-
yield next;
|
|
270
|
+
const onMessage = (ev) => {
|
|
271
|
+
if (finished) return;
|
|
272
|
+
let batch;
|
|
273
|
+
try {
|
|
274
|
+
const data = typeof ev.data === "string" ? ev.data : String(ev.data);
|
|
275
|
+
batch = JSON.parse(data);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
failure = new CoderAgentError("failed to parse chat stream frame", { cause: err });
|
|
278
|
+
parseFailure = true;
|
|
279
|
+
finished = true;
|
|
280
|
+
wake();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (Array.isArray(batch)) {
|
|
284
|
+
for (const e of batch) queue.push(e);
|
|
285
|
+
} else if (batch && typeof batch === "object") {
|
|
286
|
+
queue.push(batch);
|
|
226
287
|
}
|
|
227
|
-
|
|
288
|
+
wake();
|
|
289
|
+
};
|
|
290
|
+
const onError = (ev) => {
|
|
228
291
|
if (finished) return;
|
|
229
|
-
|
|
230
|
-
|
|
292
|
+
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "chat stream socket error";
|
|
293
|
+
const status = upgradeStatus(message);
|
|
294
|
+
failure = status !== void 0 && status >= 400 && status < 500 && !TRANSIENT_UPGRADE_STATUSES.has(status) ? new CoderApiError({ status, method: "GET", path, message }) : new CoderAgentError(message);
|
|
295
|
+
finished = true;
|
|
296
|
+
wake();
|
|
297
|
+
};
|
|
298
|
+
const onClose = () => {
|
|
299
|
+
finished = true;
|
|
300
|
+
wake();
|
|
301
|
+
};
|
|
302
|
+
ws.addEventListener("message", onMessage);
|
|
303
|
+
ws.addEventListener("error", onError);
|
|
304
|
+
ws.addEventListener("close", onClose);
|
|
305
|
+
try {
|
|
306
|
+
while (true) {
|
|
307
|
+
while (queue.length > 0) {
|
|
308
|
+
const next = queue.shift();
|
|
309
|
+
let progress = false;
|
|
310
|
+
if (next.type === "message_part") {
|
|
311
|
+
const mp = next.message_part;
|
|
312
|
+
const hv = mp?.history_version;
|
|
313
|
+
const ga = mp?.generation_attempt;
|
|
314
|
+
const seq = mp?.seq;
|
|
315
|
+
if (hv !== void 0 && ga !== void 0 && seq !== void 0) {
|
|
316
|
+
if (hv === lastHistoryVersion && ga === lastGenerationAttempt && lastSeq !== void 0 && seq <= lastSeq) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
lastHistoryVersion = hv;
|
|
320
|
+
lastGenerationAttempt = ga;
|
|
321
|
+
lastSeq = seq;
|
|
322
|
+
progress = true;
|
|
323
|
+
} else {
|
|
324
|
+
sawUntrackableDelta = true;
|
|
325
|
+
}
|
|
326
|
+
} else if (next.type === "message" && next.message) {
|
|
327
|
+
if (maxCommittedId === void 0 || next.message.id > maxCommittedId) {
|
|
328
|
+
maxCommittedId = next.message.id;
|
|
329
|
+
progress = true;
|
|
330
|
+
sawUntrackableDelta = false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (progress) {
|
|
334
|
+
failures = 0;
|
|
335
|
+
backoffMs = STREAM_BACKOFF_INITIAL_MS;
|
|
336
|
+
}
|
|
337
|
+
yield next;
|
|
338
|
+
}
|
|
339
|
+
if (finished) break;
|
|
340
|
+
await new Promise((resolve) => {
|
|
341
|
+
resolveNext = resolve;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
} finally {
|
|
345
|
+
finished = true;
|
|
346
|
+
if (signal && abortListenerAdded) signal.removeEventListener("abort", onAbort);
|
|
347
|
+
ws.removeEventListener("message", onMessage);
|
|
348
|
+
ws.removeEventListener("error", onError);
|
|
349
|
+
ws.removeEventListener("close", onClose);
|
|
350
|
+
try {
|
|
351
|
+
ws.close(1e3);
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (signal?.aborted) return;
|
|
356
|
+
if (failure instanceof CoderApiError) throw failure;
|
|
357
|
+
if (parseFailure && failure) throw failure;
|
|
358
|
+
if (sawUntrackableDelta) {
|
|
359
|
+
throw new CoderStreamError({
|
|
360
|
+
message: "chat stream dropped mid-turn and cannot be resumed: this server does not stamp deltas with history_version/generation_attempt/seq, so a redial would replay them",
|
|
361
|
+
url,
|
|
362
|
+
cause: failure
|
|
231
363
|
});
|
|
232
364
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
} catch {
|
|
365
|
+
if (failure) lastFailure = failure;
|
|
366
|
+
failures += 1;
|
|
367
|
+
if (failures >= STREAM_MAX_CONSECUTIVE_FAILURES) {
|
|
368
|
+
throw new CoderStreamError({
|
|
369
|
+
message: `Coder chat stream ${path} could not be (re-)established after ${failures} consecutive connection failures without progress`,
|
|
370
|
+
url,
|
|
371
|
+
cause: lastFailure
|
|
372
|
+
});
|
|
242
373
|
}
|
|
374
|
+
await sleep(backoffMs, signal);
|
|
375
|
+
if (signal?.aborted) return;
|
|
376
|
+
backoffMs = Math.min(backoffMs * 2, STREAM_BACKOFF_CAP_MS);
|
|
243
377
|
}
|
|
244
378
|
}
|
|
245
379
|
var WATCH_PATH = "/api/experimental/chats/watch";
|
|
@@ -958,6 +1092,11 @@ var TurnTranslator = class {
|
|
|
958
1092
|
// the same message replaces its earlier entry instead of double-counting
|
|
959
1093
|
// (chatd re-sends full snapshots on revision bumps and history resets).
|
|
960
1094
|
#usageByMessageId = /* @__PURE__ */ new Map();
|
|
1095
|
+
// Assistant snapshots already processed once. A redial replays the turn
|
|
1096
|
+
// from its original cursor, so earlier messages arrive again — without this,
|
|
1097
|
+
// re-running the boundary logic on them would reset the emitted-length
|
|
1098
|
+
// cursors and re-emit their entire text (see #ingestMessage).
|
|
1099
|
+
#snapshotSeen = /* @__PURE__ */ new Set();
|
|
961
1100
|
#error;
|
|
962
1101
|
#terminalStatus;
|
|
963
1102
|
#maxMessageId = 0;
|
|
@@ -1197,6 +1336,17 @@ var TurnTranslator = class {
|
|
|
1197
1336
|
}
|
|
1198
1337
|
const content = message.content ?? [];
|
|
1199
1338
|
if (message.role === "assistant") {
|
|
1339
|
+
if (this.#snapshotSeen.has(message.id) && (message.id !== this.#currentAssistantId || this.#deltasSinceSnapshot)) {
|
|
1340
|
+
for (const part of content) {
|
|
1341
|
+
if (part.type === "tool-call" && !this.#isClientTool(part.tool_name))
|
|
1342
|
+
this.#emitServerToolCall(out, part);
|
|
1343
|
+
else if (part.type === "tool-result" && !this.#isClientTool(part.tool_name))
|
|
1344
|
+
this.#emitServerToolResult(out, part);
|
|
1345
|
+
else if (part.type === "source") this.#emitSource(out, part);
|
|
1346
|
+
}
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
this.#snapshotSeen.add(message.id);
|
|
1200
1350
|
if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id && !this.#deltasSinceSnapshot) {
|
|
1201
1351
|
this.#closeText(out);
|
|
1202
1352
|
this.#closeReasoning(out);
|
|
@@ -1204,13 +1354,24 @@ var TurnTranslator = class {
|
|
|
1204
1354
|
this.#reasoning.sawDelta = false;
|
|
1205
1355
|
}
|
|
1206
1356
|
this.#currentAssistantId = message.id;
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1357
|
+
const emitSnapshotText = () => {
|
|
1358
|
+
if (!this.#text.sawDelta || this.#text.id) {
|
|
1359
|
+
const full = content.filter((p) => p.type === "text").map((p) => p.text ?? "").join("");
|
|
1360
|
+
if (full.length > 0) this.#emitTextUpTo(out, full);
|
|
1361
|
+
}
|
|
1362
|
+
};
|
|
1363
|
+
const emitSnapshotReasoning = () => {
|
|
1364
|
+
if (!this.#reasoning.sawDelta || this.#reasoning.id) {
|
|
1365
|
+
const full = content.filter((p) => p.type === "reasoning").map((p) => p.text ?? "").join("");
|
|
1366
|
+
if (full.length > 0) this.#emitReasoningUpTo(out, full);
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
if (this.#reasoning.id) {
|
|
1370
|
+
emitSnapshotReasoning();
|
|
1371
|
+
emitSnapshotText();
|
|
1372
|
+
} else {
|
|
1373
|
+
emitSnapshotText();
|
|
1374
|
+
emitSnapshotReasoning();
|
|
1214
1375
|
}
|
|
1215
1376
|
for (const part of content) {
|
|
1216
1377
|
if (part.type === "tool-call" && !this.#isClientTool(part.tool_name))
|
|
@@ -1253,6 +1414,15 @@ var TurnTranslator = class {
|
|
|
1253
1414
|
};
|
|
1254
1415
|
|
|
1255
1416
|
// src/model/language-model.ts
|
|
1417
|
+
var ACTION_REQUIRED_GRACE_MS = 2e3;
|
|
1418
|
+
var GRACE_EXPIRED = /* @__PURE__ */ Symbol("action-required-grace-expired");
|
|
1419
|
+
function startGraceTimer(ms) {
|
|
1420
|
+
let timer;
|
|
1421
|
+
const expired = new Promise((resolve) => {
|
|
1422
|
+
timer = setTimeout(() => resolve(GRACE_EXPIRED), ms);
|
|
1423
|
+
});
|
|
1424
|
+
return { expired, cancel: () => clearTimeout(timer) };
|
|
1425
|
+
}
|
|
1256
1426
|
var EMPTY_USAGE = {
|
|
1257
1427
|
inputTokens: {
|
|
1258
1428
|
total: void 0,
|
|
@@ -1306,17 +1476,74 @@ var CoderLanguageModel = class {
|
|
|
1306
1476
|
* id, so this runs before the chat exists). Pre-uploaded files carried via
|
|
1307
1477
|
* `providerOptions.coder.fileId` are referenced without re-uploading.
|
|
1308
1478
|
*/
|
|
1309
|
-
#buildContent(content, signal) {
|
|
1479
|
+
#buildContent(content, signal, onUpload) {
|
|
1310
1480
|
const uploadFile = async (f) => {
|
|
1311
1481
|
const uploaded = await this.#config.client.uploadChatFile(
|
|
1312
1482
|
this.#config.organizationId,
|
|
1313
1483
|
{ content: dataContentToFileContent(f.data), mediaType: f.mediaType, name: f.filename },
|
|
1314
1484
|
signal
|
|
1315
1485
|
);
|
|
1486
|
+
onUpload?.();
|
|
1316
1487
|
return uploaded.id;
|
|
1317
1488
|
};
|
|
1318
1489
|
return userContentToInputParts(content, uploadFile);
|
|
1319
1490
|
}
|
|
1491
|
+
/**
|
|
1492
|
+
* REST fallback for a `requires_action` segment whose `action_required`
|
|
1493
|
+
* event never arrived: recover the turn's pending client tool calls from
|
|
1494
|
+
* committed chat history (`GET /chats/{id}/messages`) — the same history
|
|
1495
|
+
* chatd itself derives `action_required` from server-side. Returns synthetic
|
|
1496
|
+
* stream events for the translator: the turn's message snapshots first (so
|
|
1497
|
+
* text, usage, and the message cursor stay consistent even if the stream
|
|
1498
|
+
* also missed a commit — the translator's id-keyed dedup makes re-ingesting
|
|
1499
|
+
* already-delivered ones a no-op), then one `action_required` event carrying
|
|
1500
|
+
* the unresolved dynamic tool calls. Returns `[]` when history shows no
|
|
1501
|
+
* pending calls — nothing to recover, the caller keeps waiting on the
|
|
1502
|
+
* stream, bounded by `requestTimeoutMs`/abort.
|
|
1503
|
+
*/
|
|
1504
|
+
async #recoverRequiresAction(chatId, dynamicNames, afterId, signal) {
|
|
1505
|
+
const { messages } = await this.#config.client.getMessages(chatId, { limit: 200 }, signal);
|
|
1506
|
+
const turnMessages = messages.filter((m) => m.id > (afterId ?? 0)).sort((a, b) => a.id - b.id);
|
|
1507
|
+
const lastAssistant = turnMessages.findLast((m) => m.role === "assistant");
|
|
1508
|
+
if (!lastAssistant) return [];
|
|
1509
|
+
const handled = /* @__PURE__ */ new Set();
|
|
1510
|
+
for (const message of turnMessages) {
|
|
1511
|
+
if (message.id <= lastAssistant.id) continue;
|
|
1512
|
+
for (const part of message.content ?? []) {
|
|
1513
|
+
if (part.type === "tool-result" && part.tool_call_id) handled.add(part.tool_call_id);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const toolCalls = [];
|
|
1517
|
+
for (const part of lastAssistant.content ?? []) {
|
|
1518
|
+
if (part.type !== "tool-call" || part.provider_executed) continue;
|
|
1519
|
+
if (!part.tool_call_id || !part.tool_name) continue;
|
|
1520
|
+
if (!dynamicNames.has(part.tool_name)) continue;
|
|
1521
|
+
if (handled.has(part.tool_call_id)) continue;
|
|
1522
|
+
if (this.#submittedToolCallIds.has(part.tool_call_id)) continue;
|
|
1523
|
+
toolCalls.push({
|
|
1524
|
+
tool_call_id: part.tool_call_id,
|
|
1525
|
+
tool_name: part.tool_name,
|
|
1526
|
+
// `action_required` carries args as their raw JSON text (chatd sends
|
|
1527
|
+
// `string(part.Args)` of the history part's json.RawMessage);
|
|
1528
|
+
// re-encoding the snapshot's parsed JSON value reconstructs it. A
|
|
1529
|
+
// JSON `null` is a legitimate argument value and must survive; only
|
|
1530
|
+
// an ABSENT args field falls back to empty-object args.
|
|
1531
|
+
args: JSON.stringify(part.args === void 0 ? {} : part.args)
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
if (toolCalls.length === 0) return [];
|
|
1535
|
+
const events = turnMessages.map((message) => ({
|
|
1536
|
+
type: "message",
|
|
1537
|
+
chat_id: chatId,
|
|
1538
|
+
message
|
|
1539
|
+
}));
|
|
1540
|
+
events.push({
|
|
1541
|
+
type: "action_required",
|
|
1542
|
+
chat_id: chatId,
|
|
1543
|
+
action_required: { tool_calls: toolCalls }
|
|
1544
|
+
});
|
|
1545
|
+
return events;
|
|
1546
|
+
}
|
|
1320
1547
|
async *#runTurn(options) {
|
|
1321
1548
|
if (this.#inFlight) {
|
|
1322
1549
|
throw new CoderAgentError(
|
|
@@ -1332,6 +1559,7 @@ var CoderLanguageModel = class {
|
|
|
1332
1559
|
);
|
|
1333
1560
|
const signal = sources.length > 0 ? AbortSignal.any(sources) : void 0;
|
|
1334
1561
|
let translator;
|
|
1562
|
+
let discardSession = false;
|
|
1335
1563
|
let interruptSent = false;
|
|
1336
1564
|
const interrupt = () => {
|
|
1337
1565
|
const id = this.#chatId;
|
|
@@ -1371,6 +1599,9 @@ var CoderLanguageModel = class {
|
|
|
1371
1599
|
);
|
|
1372
1600
|
}
|
|
1373
1601
|
let afterId;
|
|
1602
|
+
let turnCreatedChat = false;
|
|
1603
|
+
let uploadedAttachment = false;
|
|
1604
|
+
let chatHasWorkspace = Boolean(this.#config.workspaceId);
|
|
1374
1605
|
if (this.#chatId && this.#lastSeenMessageId === 0) {
|
|
1375
1606
|
const { messages } = await this.#config.client.getMessages(
|
|
1376
1607
|
this.#chatId,
|
|
@@ -1382,7 +1613,9 @@ var CoderLanguageModel = class {
|
|
|
1382
1613
|
if (action.kind === "new-turn") {
|
|
1383
1614
|
const [modelConfigId, content] = await Promise.all([
|
|
1384
1615
|
this.#resolveModelConfigId(signal),
|
|
1385
|
-
this.#buildContent(action.content, signal)
|
|
1616
|
+
this.#buildContent(action.content, signal, () => {
|
|
1617
|
+
uploadedAttachment = true;
|
|
1618
|
+
})
|
|
1386
1619
|
]);
|
|
1387
1620
|
if (!this.#chatId) {
|
|
1388
1621
|
const req = {
|
|
@@ -1400,6 +1633,8 @@ var CoderLanguageModel = class {
|
|
|
1400
1633
|
if (this.#config.planMode) req.plan_mode = this.#config.planMode;
|
|
1401
1634
|
const chat = await this.#config.client.createChat(req, signal);
|
|
1402
1635
|
this.#chatId = chat.id;
|
|
1636
|
+
turnCreatedChat = true;
|
|
1637
|
+
if (chat.workspace_id) chatHasWorkspace = true;
|
|
1403
1638
|
afterId = this.#lastSeenMessageId > 0 ? this.#lastSeenMessageId : void 0;
|
|
1404
1639
|
} else {
|
|
1405
1640
|
const resp = await this.#config.client.createChatMessage(
|
|
@@ -1425,14 +1660,58 @@ var CoderLanguageModel = class {
|
|
|
1425
1660
|
afterId = this.#lastSeenMessageId;
|
|
1426
1661
|
}
|
|
1427
1662
|
const chatId = this.#chatId;
|
|
1663
|
+
const dynamicNames = dynamicToolNames(options.tools);
|
|
1428
1664
|
translator = new TurnTranslator({
|
|
1429
|
-
dynamicToolNames:
|
|
1665
|
+
dynamicToolNames: dynamicNames,
|
|
1430
1666
|
turnCursor: afterId ?? 0
|
|
1431
1667
|
});
|
|
1432
1668
|
let sinceRequiresAction = 0;
|
|
1669
|
+
let grace;
|
|
1670
|
+
let recovery;
|
|
1671
|
+
let recoveryAbort;
|
|
1672
|
+
let recoveryAttempted = false;
|
|
1673
|
+
const stream = this.#config.client.streamEvents(chatId, { afterId, signal });
|
|
1433
1674
|
try {
|
|
1434
|
-
|
|
1435
|
-
|
|
1675
|
+
let next = stream.next();
|
|
1676
|
+
for (; ; ) {
|
|
1677
|
+
let result;
|
|
1678
|
+
const awaitingActionRequired = translator.terminalStatus === "requires_action" && !translator.clientToolCallSeen;
|
|
1679
|
+
if (awaitingActionRequired && !recoveryAttempted) {
|
|
1680
|
+
grace ??= startGraceTimer(ACTION_REQUIRED_GRACE_MS);
|
|
1681
|
+
const raced = await Promise.race([next, grace.expired]);
|
|
1682
|
+
if (raced === GRACE_EXPIRED) {
|
|
1683
|
+
recoveryAttempted = true;
|
|
1684
|
+
throwIfAborted();
|
|
1685
|
+
recoveryAbort = new AbortController();
|
|
1686
|
+
recovery = this.#recoverRequiresAction(
|
|
1687
|
+
chatId,
|
|
1688
|
+
dynamicNames,
|
|
1689
|
+
afterId,
|
|
1690
|
+
signal ? AbortSignal.any([signal, recoveryAbort.signal]) : recoveryAbort.signal
|
|
1691
|
+
).catch(() => []);
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
result = raced;
|
|
1695
|
+
} else if (awaitingActionRequired && recovery) {
|
|
1696
|
+
const raced = await Promise.race([next, recovery]);
|
|
1697
|
+
if (Array.isArray(raced)) {
|
|
1698
|
+
recovery = void 0;
|
|
1699
|
+
throwIfAborted();
|
|
1700
|
+
for (const ev of raced) for (const part of translator.ingest(ev)) yield part;
|
|
1701
|
+
if (translator.clientToolCallSeen) {
|
|
1702
|
+
void next.catch(() => {
|
|
1703
|
+
});
|
|
1704
|
+
break;
|
|
1705
|
+
}
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
result = raced;
|
|
1709
|
+
} else {
|
|
1710
|
+
result = await next;
|
|
1711
|
+
}
|
|
1712
|
+
if (result.done) break;
|
|
1713
|
+
next = stream.next();
|
|
1714
|
+
for (const part of translator.ingest(result.value)) yield part;
|
|
1436
1715
|
const status = translator.terminalStatus;
|
|
1437
1716
|
if (status) {
|
|
1438
1717
|
if (status !== "requires_action") break;
|
|
@@ -1442,6 +1721,19 @@ var CoderLanguageModel = class {
|
|
|
1442
1721
|
}
|
|
1443
1722
|
} catch (err) {
|
|
1444
1723
|
throwIfAborted();
|
|
1724
|
+
if (err instanceof CoderStreamError) {
|
|
1725
|
+
if (turnCreatedChat) {
|
|
1726
|
+
discardSession = true;
|
|
1727
|
+
}
|
|
1728
|
+
const effectful = chatHasWorkspace || Boolean(this.#config.mcpServerIds?.length) || uploadedAttachment;
|
|
1729
|
+
if (turnCreatedChat && !effectful) throw err;
|
|
1730
|
+
throw new CoderStreamError({
|
|
1731
|
+
message: `${err.message}; automatic retry is disabled because ${turnCreatedChat ? "the turn has server-side effects a replay would repeat (workspace/MCP tools that may have executed, or freshly uploaded attachments)" : "the chat has prior state (a retry would resubmit this turn's prompt as a new user turn)"}`,
|
|
1732
|
+
url: err.url,
|
|
1733
|
+
cause: err,
|
|
1734
|
+
isRetryable: false
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1445
1737
|
if (err instanceof CoderAgentError && !(err instanceof CoderApiError) && !(err instanceof CoderChatError)) {
|
|
1446
1738
|
throw new CoderChatError({
|
|
1447
1739
|
message: `Coder chat stream failed mid-turn: ${err.message}`,
|
|
@@ -1450,6 +1742,11 @@ var CoderLanguageModel = class {
|
|
|
1450
1742
|
});
|
|
1451
1743
|
}
|
|
1452
1744
|
throw err;
|
|
1745
|
+
} finally {
|
|
1746
|
+
grace?.cancel();
|
|
1747
|
+
recoveryAbort?.abort();
|
|
1748
|
+
await stream.return(void 0).catch(() => {
|
|
1749
|
+
});
|
|
1453
1750
|
}
|
|
1454
1751
|
throwIfAborted();
|
|
1455
1752
|
if (!translator.terminalStatus && !translator.error) {
|
|
@@ -1468,6 +1765,7 @@ var CoderLanguageModel = class {
|
|
|
1468
1765
|
this.#lastSeenMessageId = translator.maxMessageId;
|
|
1469
1766
|
signal?.removeEventListener("abort", interrupt);
|
|
1470
1767
|
if (!translator?.terminalStatus) interrupt();
|
|
1768
|
+
if (discardSession) this.resetSession();
|
|
1471
1769
|
this.#inFlight = false;
|
|
1472
1770
|
}
|
|
1473
1771
|
}
|
|
@@ -1793,7 +2091,7 @@ var CoderAgent = class {
|
|
|
1793
2091
|
async uploadToWorkspace(file) {
|
|
1794
2092
|
if (!this.#workspaceFiles) {
|
|
1795
2093
|
throw new CoderAgentError(
|
|
1796
|
-
"uploadToWorkspace requires a `workspaceFiles` adapter. Construct the agent with `workspaceFiles` (e.g. backed by a @coder/ai-sdk-
|
|
2094
|
+
"uploadToWorkspace requires a `workspaceFiles` adapter. Construct the agent with `workspaceFiles` (e.g. backed by a @coder/ai-sdk-sandbox workspace session) to enable it."
|
|
1797
2095
|
);
|
|
1798
2096
|
}
|
|
1799
2097
|
const { path } = await this.#workspaceFiles.write({
|
|
@@ -1964,7 +2262,9 @@ export {
|
|
|
1964
2262
|
CoderChatClient,
|
|
1965
2263
|
CoderChatError,
|
|
1966
2264
|
CoderLanguageModel,
|
|
2265
|
+
CoderStreamError,
|
|
1967
2266
|
MAX_CHAT_FILE_SIZE_BYTES,
|
|
2267
|
+
TERMINAL_STATUSES,
|
|
1968
2268
|
TurnTranslator,
|
|
1969
2269
|
chatMessagesToUIMessages,
|
|
1970
2270
|
classifyTurnAction,
|