@coder/ai-sdk-agent 0.4.1 → 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 +58 -13
- package/dist/index.d.ts +56 -5
- package/dist/index.js +458 -99
- 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";
|
|
@@ -883,11 +1017,29 @@ function dynamicToolNames(tools) {
|
|
|
883
1017
|
}
|
|
884
1018
|
|
|
885
1019
|
// src/model/translate.ts
|
|
1020
|
+
function addDefined(a, b) {
|
|
1021
|
+
if (b === void 0) return a;
|
|
1022
|
+
return (a ?? 0) + b;
|
|
1023
|
+
}
|
|
1024
|
+
var USAGE_FIELD_KINDS = {
|
|
1025
|
+
input_tokens: "sum",
|
|
1026
|
+
output_tokens: "sum",
|
|
1027
|
+
total_tokens: "sum",
|
|
1028
|
+
reasoning_tokens: "sum",
|
|
1029
|
+
cache_creation_tokens: "sum",
|
|
1030
|
+
cache_read_tokens: "sum",
|
|
1031
|
+
total_cost_micros: "sum",
|
|
1032
|
+
total_runtime_ms: "sum",
|
|
1033
|
+
context_limit: "newest"
|
|
1034
|
+
};
|
|
886
1035
|
function mapUsage(u) {
|
|
887
1036
|
return {
|
|
888
1037
|
inputTokens: {
|
|
889
|
-
total:
|
|
890
|
-
|
|
1038
|
+
total: addDefined(
|
|
1039
|
+
addDefined(u?.input_tokens, u?.cache_read_tokens),
|
|
1040
|
+
u?.cache_creation_tokens
|
|
1041
|
+
),
|
|
1042
|
+
noCache: u?.input_tokens,
|
|
891
1043
|
cacheRead: u?.cache_read_tokens,
|
|
892
1044
|
cacheWrite: u?.cache_creation_tokens
|
|
893
1045
|
},
|
|
@@ -896,11 +1048,28 @@ function mapUsage(u) {
|
|
|
896
1048
|
text: void 0,
|
|
897
1049
|
reasoning: u?.reasoning_tokens
|
|
898
1050
|
},
|
|
899
|
-
// Preserve the
|
|
1051
|
+
// Preserve the turn's wire usage (snake_case) so callers can reach fields
|
|
900
1052
|
// the normalized shape has no slot for (context_limit, cost, runtime, …).
|
|
901
1053
|
...u ? { raw: u } : {}
|
|
902
1054
|
};
|
|
903
1055
|
}
|
|
1056
|
+
function accumulateUsage(byMessageId) {
|
|
1057
|
+
if (byMessageId.size === 0) return void 0;
|
|
1058
|
+
const total = {};
|
|
1059
|
+
const newestIdByKey = /* @__PURE__ */ new Map();
|
|
1060
|
+
for (const [id, u] of byMessageId) {
|
|
1061
|
+
for (const [key, value] of Object.entries(u)) {
|
|
1062
|
+
if (value === void 0) continue;
|
|
1063
|
+
if (USAGE_FIELD_KINDS[key] === "sum" && typeof value === "number") {
|
|
1064
|
+
total[key] = (total[key] ?? 0) + value;
|
|
1065
|
+
} else if (id > (newestIdByKey.get(key) ?? -1)) {
|
|
1066
|
+
newestIdByKey.set(key, id);
|
|
1067
|
+
total[key] = value;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return total;
|
|
1072
|
+
}
|
|
904
1073
|
function jsonResult(value) {
|
|
905
1074
|
return value ?? {};
|
|
906
1075
|
}
|
|
@@ -919,12 +1088,29 @@ var TurnTranslator = class {
|
|
|
919
1088
|
#clientToolCalls = /* @__PURE__ */ new Set();
|
|
920
1089
|
#clientToolCallSeen = false;
|
|
921
1090
|
#sources = /* @__PURE__ */ new Set();
|
|
922
|
-
|
|
1091
|
+
// Per-message wire usage, keyed by message id so a re-streamed revision of
|
|
1092
|
+
// the same message replaces its earlier entry instead of double-counting
|
|
1093
|
+
// (chatd re-sends full snapshots on revision bumps and history resets).
|
|
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();
|
|
923
1100
|
#error;
|
|
924
1101
|
#terminalStatus;
|
|
925
1102
|
#maxMessageId = 0;
|
|
1103
|
+
/**
|
|
1104
|
+
* Messages with an id at or below this cursor belong to earlier turns (or
|
|
1105
|
+
* to a segment that already streamed) — they arrive only as replays: the
|
|
1106
|
+
* initial sync when resuming a chat, or a mid-turn `history_reset` re-send
|
|
1107
|
+
* of the full history. Ingest skips them entirely so a replay can neither
|
|
1108
|
+
* re-emit earlier turns' content nor inflate this turn's usage.
|
|
1109
|
+
*/
|
|
1110
|
+
#turnCursor;
|
|
926
1111
|
constructor(opts) {
|
|
927
1112
|
this.#dynamicToolNames = opts.dynamicToolNames;
|
|
1113
|
+
this.#turnCursor = opts.turnCursor ?? 0;
|
|
928
1114
|
}
|
|
929
1115
|
get terminalStatus() {
|
|
930
1116
|
return this.#terminalStatus;
|
|
@@ -1144,11 +1330,23 @@ var TurnTranslator = class {
|
|
|
1144
1330
|
}
|
|
1145
1331
|
#ingestMessage(out, message) {
|
|
1146
1332
|
if (message.id > this.#maxMessageId) this.#maxMessageId = message.id;
|
|
1147
|
-
if (message.
|
|
1148
|
-
|
|
1333
|
+
if (message.id <= this.#turnCursor) return;
|
|
1334
|
+
if (message.usage && message.role === "assistant") {
|
|
1335
|
+
this.#usageByMessageId.set(message.id, message.usage);
|
|
1149
1336
|
}
|
|
1150
1337
|
const content = message.content ?? [];
|
|
1151
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);
|
|
1152
1350
|
if (this.#currentAssistantId !== void 0 && this.#currentAssistantId !== message.id && !this.#deltasSinceSnapshot) {
|
|
1153
1351
|
this.#closeText(out);
|
|
1154
1352
|
this.#closeReasoning(out);
|
|
@@ -1156,13 +1354,24 @@ var TurnTranslator = class {
|
|
|
1156
1354
|
this.#reasoning.sawDelta = false;
|
|
1157
1355
|
}
|
|
1158
1356
|
this.#currentAssistantId = message.id;
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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();
|
|
1166
1375
|
}
|
|
1167
1376
|
for (const part of content) {
|
|
1168
1377
|
if (part.type === "tool-call" && !this.#isClientTool(part.tool_name))
|
|
@@ -1190,14 +1399,13 @@ var TurnTranslator = class {
|
|
|
1190
1399
|
else if (this.#clientToolCallSeen || this.#terminalStatus === "requires_action")
|
|
1191
1400
|
unified = "tool-calls";
|
|
1192
1401
|
else unified = "stop";
|
|
1402
|
+
const usage = accumulateUsage(this.#usageByMessageId);
|
|
1193
1403
|
const coder = {};
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
1196
|
-
if (this.#usage?.total_runtime_ms !== void 0)
|
|
1197
|
-
coder.total_runtime_ms = this.#usage.total_runtime_ms;
|
|
1404
|
+
if (usage?.total_cost_micros !== void 0) coder.total_cost_micros = usage.total_cost_micros;
|
|
1405
|
+
if (usage?.total_runtime_ms !== void 0) coder.total_runtime_ms = usage.total_runtime_ms;
|
|
1198
1406
|
out.push({
|
|
1199
1407
|
type: "finish",
|
|
1200
|
-
usage: mapUsage(
|
|
1408
|
+
usage: mapUsage(usage),
|
|
1201
1409
|
finishReason: { unified, raw: this.#terminalStatus },
|
|
1202
1410
|
...Object.keys(coder).length > 0 ? { providerMetadata: { coder } } : {}
|
|
1203
1411
|
});
|
|
@@ -1206,6 +1414,15 @@ var TurnTranslator = class {
|
|
|
1206
1414
|
};
|
|
1207
1415
|
|
|
1208
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
|
+
}
|
|
1209
1426
|
var EMPTY_USAGE = {
|
|
1210
1427
|
inputTokens: {
|
|
1211
1428
|
total: void 0,
|
|
@@ -1259,17 +1476,74 @@ var CoderLanguageModel = class {
|
|
|
1259
1476
|
* id, so this runs before the chat exists). Pre-uploaded files carried via
|
|
1260
1477
|
* `providerOptions.coder.fileId` are referenced without re-uploading.
|
|
1261
1478
|
*/
|
|
1262
|
-
#buildContent(content, signal) {
|
|
1479
|
+
#buildContent(content, signal, onUpload) {
|
|
1263
1480
|
const uploadFile = async (f) => {
|
|
1264
1481
|
const uploaded = await this.#config.client.uploadChatFile(
|
|
1265
1482
|
this.#config.organizationId,
|
|
1266
1483
|
{ content: dataContentToFileContent(f.data), mediaType: f.mediaType, name: f.filename },
|
|
1267
1484
|
signal
|
|
1268
1485
|
);
|
|
1486
|
+
onUpload?.();
|
|
1269
1487
|
return uploaded.id;
|
|
1270
1488
|
};
|
|
1271
1489
|
return userContentToInputParts(content, uploadFile);
|
|
1272
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
|
+
}
|
|
1273
1547
|
async *#runTurn(options) {
|
|
1274
1548
|
if (this.#inFlight) {
|
|
1275
1549
|
throw new CoderAgentError(
|
|
@@ -1284,7 +1558,8 @@ var CoderLanguageModel = class {
|
|
|
1284
1558
|
(s) => s !== void 0
|
|
1285
1559
|
);
|
|
1286
1560
|
const signal = sources.length > 0 ? AbortSignal.any(sources) : void 0;
|
|
1287
|
-
|
|
1561
|
+
let translator;
|
|
1562
|
+
let discardSession = false;
|
|
1288
1563
|
let interruptSent = false;
|
|
1289
1564
|
const interrupt = () => {
|
|
1290
1565
|
const id = this.#chatId;
|
|
@@ -1324,10 +1599,23 @@ var CoderLanguageModel = class {
|
|
|
1324
1599
|
);
|
|
1325
1600
|
}
|
|
1326
1601
|
let afterId;
|
|
1602
|
+
let turnCreatedChat = false;
|
|
1603
|
+
let uploadedAttachment = false;
|
|
1604
|
+
let chatHasWorkspace = Boolean(this.#config.workspaceId);
|
|
1605
|
+
if (this.#chatId && this.#lastSeenMessageId === 0) {
|
|
1606
|
+
const { messages } = await this.#config.client.getMessages(
|
|
1607
|
+
this.#chatId,
|
|
1608
|
+
{ limit: 1 },
|
|
1609
|
+
signal
|
|
1610
|
+
);
|
|
1611
|
+
this.#lastSeenMessageId = messages[0]?.id ?? 0;
|
|
1612
|
+
}
|
|
1327
1613
|
if (action.kind === "new-turn") {
|
|
1328
1614
|
const [modelConfigId, content] = await Promise.all([
|
|
1329
1615
|
this.#resolveModelConfigId(signal),
|
|
1330
|
-
this.#buildContent(action.content, signal)
|
|
1616
|
+
this.#buildContent(action.content, signal, () => {
|
|
1617
|
+
uploadedAttachment = true;
|
|
1618
|
+
})
|
|
1331
1619
|
]);
|
|
1332
1620
|
if (!this.#chatId) {
|
|
1333
1621
|
const req = {
|
|
@@ -1345,6 +1633,8 @@ var CoderLanguageModel = class {
|
|
|
1345
1633
|
if (this.#config.planMode) req.plan_mode = this.#config.planMode;
|
|
1346
1634
|
const chat = await this.#config.client.createChat(req, signal);
|
|
1347
1635
|
this.#chatId = chat.id;
|
|
1636
|
+
turnCreatedChat = true;
|
|
1637
|
+
if (chat.workspace_id) chatHasWorkspace = true;
|
|
1348
1638
|
afterId = this.#lastSeenMessageId > 0 ? this.#lastSeenMessageId : void 0;
|
|
1349
1639
|
} else {
|
|
1350
1640
|
const resp = await this.#config.client.createChatMessage(
|
|
@@ -1370,10 +1660,58 @@ var CoderLanguageModel = class {
|
|
|
1370
1660
|
afterId = this.#lastSeenMessageId;
|
|
1371
1661
|
}
|
|
1372
1662
|
const chatId = this.#chatId;
|
|
1663
|
+
const dynamicNames = dynamicToolNames(options.tools);
|
|
1664
|
+
translator = new TurnTranslator({
|
|
1665
|
+
dynamicToolNames: dynamicNames,
|
|
1666
|
+
turnCursor: afterId ?? 0
|
|
1667
|
+
});
|
|
1373
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 });
|
|
1374
1674
|
try {
|
|
1375
|
-
|
|
1376
|
-
|
|
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;
|
|
1377
1715
|
const status = translator.terminalStatus;
|
|
1378
1716
|
if (status) {
|
|
1379
1717
|
if (status !== "requires_action") break;
|
|
@@ -1383,6 +1721,19 @@ var CoderLanguageModel = class {
|
|
|
1383
1721
|
}
|
|
1384
1722
|
} catch (err) {
|
|
1385
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
|
+
}
|
|
1386
1737
|
if (err instanceof CoderAgentError && !(err instanceof CoderApiError) && !(err instanceof CoderChatError)) {
|
|
1387
1738
|
throw new CoderChatError({
|
|
1388
1739
|
message: `Coder chat stream failed mid-turn: ${err.message}`,
|
|
@@ -1391,6 +1742,11 @@ var CoderLanguageModel = class {
|
|
|
1391
1742
|
});
|
|
1392
1743
|
}
|
|
1393
1744
|
throw err;
|
|
1745
|
+
} finally {
|
|
1746
|
+
grace?.cancel();
|
|
1747
|
+
recoveryAbort?.abort();
|
|
1748
|
+
await stream.return(void 0).catch(() => {
|
|
1749
|
+
});
|
|
1394
1750
|
}
|
|
1395
1751
|
throwIfAborted();
|
|
1396
1752
|
if (!translator.terminalStatus && !translator.error) {
|
|
@@ -1405,10 +1761,11 @@ var CoderLanguageModel = class {
|
|
|
1405
1761
|
throwIfAborted();
|
|
1406
1762
|
throw err;
|
|
1407
1763
|
} finally {
|
|
1408
|
-
if (translator.maxMessageId > this.#lastSeenMessageId)
|
|
1764
|
+
if (translator && translator.maxMessageId > this.#lastSeenMessageId)
|
|
1409
1765
|
this.#lastSeenMessageId = translator.maxMessageId;
|
|
1410
1766
|
signal?.removeEventListener("abort", interrupt);
|
|
1411
|
-
if (!translator
|
|
1767
|
+
if (!translator?.terminalStatus) interrupt();
|
|
1768
|
+
if (discardSession) this.resetSession();
|
|
1412
1769
|
this.#inFlight = false;
|
|
1413
1770
|
}
|
|
1414
1771
|
}
|
|
@@ -1734,7 +2091,7 @@ var CoderAgent = class {
|
|
|
1734
2091
|
async uploadToWorkspace(file) {
|
|
1735
2092
|
if (!this.#workspaceFiles) {
|
|
1736
2093
|
throw new CoderAgentError(
|
|
1737
|
-
"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."
|
|
1738
2095
|
);
|
|
1739
2096
|
}
|
|
1740
2097
|
const { path } = await this.#workspaceFiles.write({
|
|
@@ -1905,7 +2262,9 @@ export {
|
|
|
1905
2262
|
CoderChatClient,
|
|
1906
2263
|
CoderChatError,
|
|
1907
2264
|
CoderLanguageModel,
|
|
2265
|
+
CoderStreamError,
|
|
1908
2266
|
MAX_CHAT_FILE_SIZE_BYTES,
|
|
2267
|
+
TERMINAL_STATUSES,
|
|
1909
2268
|
TurnTranslator,
|
|
1910
2269
|
chatMessagesToUIMessages,
|
|
1911
2270
|
classifyTurnAction,
|