@coder/ai-sdk-agent 0.9.14 → 0.10.1
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 +56 -25
- package/dist/index.d.ts +21 -17
- package/dist/index.js +96 -12
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -10,8 +10,9 @@ AI SDK's [`Agent`](https://ai-sdk.dev/docs/reference/ai-sdk-core/agent) interfac
|
|
|
10
10
|
(`generate()` / `stream()`). Script it, stream from it, and attach your own tools —
|
|
11
11
|
exactly like the SDK's own `ToolLoopAgent`.
|
|
12
12
|
|
|
13
|
-
> Status:
|
|
14
|
-
>
|
|
13
|
+
> Status: targets Coder's stable chat API (`/api/v2/chats`), available since
|
|
14
|
+
> Coder 2.37.0 (September 1, 2026). This package remains pre‑1.0.
|
|
15
|
+
> See [API compatibility](#api-compatibility) for older deployments.
|
|
15
16
|
|
|
16
17
|
## Why
|
|
17
18
|
|
|
@@ -58,7 +59,7 @@ import { tool } from "ai";
|
|
|
58
59
|
import { z } from "zod";
|
|
59
60
|
|
|
60
61
|
const agent = new CoderAgent({
|
|
61
|
-
baseUrl: "https://
|
|
62
|
+
baseUrl: "https://dogfood.cdr.dev",
|
|
62
63
|
token: process.env.CODER_SESSION_TOKEN!, // Coder API/session token
|
|
63
64
|
organizationId: "703f72a1-…", // your org UUID
|
|
64
65
|
model: "claude-sonnet-4-6", // hint: UUID, provider:model, model id, or display-name substring
|
|
@@ -90,7 +91,7 @@ of the AI SDK.
|
|
|
90
91
|
Runnable scripts live in [`examples/`](./examples) (run against a real deployment via `tsx`):
|
|
91
92
|
|
|
92
93
|
```bash
|
|
93
|
-
export CODER_URL=https://
|
|
94
|
+
export CODER_URL=https://dogfood.cdr.dev
|
|
94
95
|
export CODER_SESSION_TOKEN=$(coder tokens create --name coderagent-example)
|
|
95
96
|
|
|
96
97
|
pnpm example:generate # non-streaming generate()
|
|
@@ -213,6 +214,30 @@ const client = new CoderChatClient({ baseUrl, token });
|
|
|
213
214
|
const agent = new CoderAgent({ client, organizationId });
|
|
214
215
|
```
|
|
215
216
|
|
|
217
|
+
## API compatibility
|
|
218
|
+
|
|
219
|
+
Coder 2.37.0 promoted chat routes to `/api/v2/chats`
|
|
220
|
+
([server routes](https://github.com/coder/coder/pull/28496),
|
|
221
|
+
[Go SDK](https://github.com/coder/coder/pull/28497)). The promoted routes also
|
|
222
|
+
remain under `/api/experimental/chats` during the compatibility window
|
|
223
|
+
(CODAGT-921); legacy removal is tracked in CODAGT-922. Coder 2.36.4 has no stable
|
|
224
|
+
chat mount.
|
|
225
|
+
|
|
226
|
+
Each `CoderChatClient` selects its chat prefix from HTTP responses, not version
|
|
227
|
+
strings. Until selected, an actual REST request (including JSON requests and
|
|
228
|
+
`Blob` uploads) tries v2 first and retries once under the experimental prefix
|
|
229
|
+
only on 404. The first non-404 HTTP response
|
|
230
|
+
(including an error) locks that prefix for the client's lifetime; concurrent
|
|
231
|
+
unresolved calls may probe independently, with the first conclusive response
|
|
232
|
+
winning. Two 404s preserve the original v2 error without locking; non-HTTP
|
|
233
|
+
failures do not lock either. A locked prefix is never renegotiated, even on 404.
|
|
234
|
+
Before opening a per-chat `/stream` or global `/watch` WebSocket, an unresolved
|
|
235
|
+
client preflights with `GET /api/v2/chats` using the same selection rules and
|
|
236
|
+
cancellation signal.
|
|
237
|
+
`ReadableStream` uploads also preflight so their consumed bodies are never
|
|
238
|
+
replayed. Legacy model-config lookup remains independent of chat-prefix
|
|
239
|
+
selection (see [Configuration](#configuration)).
|
|
240
|
+
|
|
216
241
|
## Sessions
|
|
217
242
|
|
|
218
243
|
One `CoderAgent` instance maps to one chat ("session") on the Coder server. The chat is
|
|
@@ -282,7 +307,7 @@ chatMessagesToUIMessages(messages, {
|
|
|
282
307
|
|
|
283
308
|
`client.watchChats({ signal })` yields lifecycle events (status/title changes,
|
|
284
309
|
creation, deletion, …) for **every chat visible to the authenticated user** as an
|
|
285
|
-
async iterable, backed by the `/api/
|
|
310
|
+
async iterable, backed by the `/api/v2/chats/watch` WebSocket:
|
|
286
311
|
|
|
287
312
|
```ts
|
|
288
313
|
for await (const event of client.watchChats({ signal })) {
|
|
@@ -290,14 +315,16 @@ for await (const event of client.watchChats({ signal })) {
|
|
|
290
315
|
}
|
|
291
316
|
```
|
|
292
317
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
a 30s cap, reset once an event arrives).
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
(404). For custom plumbing (own client,
|
|
299
|
-
|
|
300
|
-
|
|
318
|
+
After [prefix selection](#api-compatibility) (whose preflight errors propagate),
|
|
319
|
+
this is a long‑lived subscription: dropped connections are redialed automatically
|
|
320
|
+
with exponential backoff (1s doubling to a 30s cap, reset once an event arrives).
|
|
321
|
+
Aborting the signal or ending iteration tears down the reader. A 4xx upgrade
|
|
322
|
+
rejection throws a terminal `CoderApiError` — bad/expired token, or an older
|
|
323
|
+
Coder server without the endpoint (404). For custom plumbing (own client,
|
|
324
|
+
browser sockets), the standalone
|
|
325
|
+
`watchChatEvents({ baseUrl, token, signal, webSocketFactory })` export defaults
|
|
326
|
+
to the stable prefix without preflighting; use `CoderChatClient` for automatic
|
|
327
|
+
prefix selection.
|
|
301
328
|
|
|
302
329
|
## Observability
|
|
303
330
|
|
|
@@ -365,7 +392,9 @@ Semantics worth knowing:
|
|
|
365
392
|
`"getMessages"`, `"submitToolResults"`, … — the `CoderClientOperation`
|
|
366
393
|
union), so per‑operation classification never has to reverse‑engineer
|
|
367
394
|
`path`. `method`/`path` stay for generic consumers; `archiveChat` stamps its
|
|
368
|
-
own `op` even though it issues the same `PATCH` as `updateChat`.
|
|
395
|
+
own `op` even though it issues the same `PATCH` as `updateChat`. WebSocket
|
|
396
|
+
prefix-selection preflights emit the existing `http:*` event kinds with
|
|
397
|
+
`op: "streamEvents"` or `op: "watchChats"` — no new event kinds.
|
|
369
398
|
- `ws:event` fires at arrival: after a redial, chatd's replay of the
|
|
370
399
|
in‑progress episode is visible here (correlate with `reader`/`attempt`),
|
|
371
400
|
stamped with the reader's own replay verdict — `forwarded: false` exactly on
|
|
@@ -559,7 +588,7 @@ are mirrored the same way under each step's `providerMetadata.coder`
|
|
|
559
588
|
`result.steps[*].providerMetadata.coder` for whole‑turn cost when client tools
|
|
560
589
|
ran). Both are **absence‑tolerant mirrors**: on servers that don't send them
|
|
561
590
|
(cost is otherwise only on the aggregate cost endpoints,
|
|
562
|
-
`/api/
|
|
591
|
+
`/api/v2/chats/cost/*`), nothing is emitted.
|
|
563
592
|
|
|
564
593
|
Forward usage to a UI via message metadata:
|
|
565
594
|
|
|
@@ -865,7 +894,7 @@ status indefinitely. Defend in this order:
|
|
|
865
894
|
cursor or replay), so a chat already stuck before the watcher started — or
|
|
866
895
|
one that transitioned during a gap — never emits an event to start your
|
|
867
896
|
timer from. Seed and periodically reconcile against a chat listing
|
|
868
|
-
(`GET /api/
|
|
897
|
+
(`GET /api/v2/chats`) instead of trusting the event stream alone.
|
|
869
898
|
|
|
870
899
|
### Troubleshooting: unschedulable & stuck chats
|
|
871
900
|
|
|
@@ -912,9 +941,10 @@ is used as‑is, then an exact `provider:model` match, an exact model id, a
|
|
|
912
941
|
display‑name substring (case‑insensitive), and finally a model‑id substring.
|
|
913
942
|
On older deployments where the organization‑scoped route does not exist yet
|
|
914
943
|
(404), resolution falls back once to the legacy deployment‑wide
|
|
915
|
-
`/model-configs` listing
|
|
916
|
-
|
|
917
|
-
|
|
944
|
+
`/api/experimental/chats/model-configs` listing, independently of chat-prefix
|
|
945
|
+
selection. Partial payloads from older/newer servers are tolerated (entries
|
|
946
|
+
match on the fields they carry), and an unresolvable hint falls back to the
|
|
947
|
+
server's default model instead of failing. Use
|
|
918
948
|
`agent.listModels()` to see what's available.
|
|
919
949
|
|
|
920
950
|
## How it works
|
|
@@ -923,7 +953,7 @@ falls back to the server's default model instead of failing. Use
|
|
|
923
953
|
CoderAgent (implements ai.Agent)
|
|
924
954
|
└─ ToolLoopAgent (ai) ← inherits generate()/stream(), loop control
|
|
925
955
|
└─ CoderLanguageModel ← implements @ai-sdk/provider LanguageModelV4
|
|
926
|
-
└─ CoderChatClient ← REST + WebSocket to /api/
|
|
956
|
+
└─ CoderChatClient ← REST + WebSocket to /api/v2/chats
|
|
927
957
|
└─ Coder Agents ← runs the agent loop SERVER-side
|
|
928
958
|
```
|
|
929
959
|
|
|
@@ -1734,13 +1764,13 @@ live chats.
|
|
|
1734
1764
|
|
|
1735
1765
|
```ts
|
|
1736
1766
|
export async function archiveWorkflowChat(workflowId: string): Promise<void> {
|
|
1737
|
-
const
|
|
1738
|
-
if (!
|
|
1767
|
+
const checkpoint = await checkpoints.get(workflowId);
|
|
1768
|
+
if (!checkpoint) return; // the workflow died before its first turn created a chat
|
|
1739
1769
|
const agent = new CoderAgent({
|
|
1740
1770
|
baseUrl: process.env.CODER_URL!,
|
|
1741
1771
|
token: process.env.CODER_SESSION_TOKEN!,
|
|
1742
1772
|
organizationId: process.env.CODER_ORG_ID!,
|
|
1743
|
-
chatId,
|
|
1773
|
+
chatId: checkpoint.chatId,
|
|
1744
1774
|
});
|
|
1745
1775
|
// A crashed step may have left a run live — stop it first, bounded. On an
|
|
1746
1776
|
// already-settled chat the interrupt rejects with a 409; ignore it.
|
|
@@ -1782,7 +1812,7 @@ pnpm build
|
|
|
1782
1812
|
End‑to‑end tests run against a live Coder deployment and are opt‑in via env:
|
|
1783
1813
|
|
|
1784
1814
|
```bash
|
|
1785
|
-
CODER_URL=https://
|
|
1815
|
+
CODER_URL=https://dogfood.cdr.dev \
|
|
1786
1816
|
CODER_SESSION_TOKEN=$(coder tokens create --name e2e) \
|
|
1787
1817
|
pnpm test:e2e
|
|
1788
1818
|
```
|
|
@@ -1791,7 +1821,8 @@ The e2e suite creates **new chats only** (no workspaces) and archives them after
|
|
|
1791
1821
|
|
|
1792
1822
|
## Limitations
|
|
1793
1823
|
|
|
1794
|
-
-
|
|
1824
|
+
- Older deployments use a temporary experimental-route fallback; it is not a
|
|
1825
|
+
guarantee that every chat feature is available (see [API compatibility](#api-compatibility)).
|
|
1795
1826
|
- Designed for Node (WebSocket via `ws`); a browser build can inject a `webSocketFactory`.
|
|
1796
1827
|
- A v7 `@ai-sdk/harness` adapter (the conceptually exact fit) is a future direction once that
|
|
1797
1828
|
experimental API stabilizes.
|
package/dist/index.d.ts
CHANGED
|
@@ -48,12 +48,10 @@ declare function resolveFileContent(content: FileContent, opts?: {
|
|
|
48
48
|
declare function dataContentToFileContent(data: SharedV4FileData): FileContent;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Wire types for Coder's
|
|
52
|
-
* (`/api/
|
|
51
|
+
* Wire types for Coder's stable `chatd` chat API
|
|
52
|
+
* (`/api/v2/chats`, promoted in Coder 2.37.0). These mirror the Go types in
|
|
53
53
|
* `coder/coder`'s `codersdk/chats.go` (JSON shapes), restricted to the subset
|
|
54
54
|
* this client uses. Field names use snake_case to match the wire format.
|
|
55
|
-
*
|
|
56
|
-
* NOTE: The chatd API is experimental and may change between Coder releases.
|
|
57
55
|
*/
|
|
58
56
|
type ChatInputPartType = "text" | "file" | "file-reference";
|
|
59
57
|
interface ChatInputPart {
|
|
@@ -67,7 +65,7 @@ interface ChatInputPart {
|
|
|
67
65
|
}
|
|
68
66
|
/**
|
|
69
67
|
* Media types accepted by the chat-file upload endpoint
|
|
70
|
-
* (`POST /api/
|
|
68
|
+
* (`POST /api/v2/chats/files`). The server allowlist is intentionally
|
|
71
69
|
* narrow — anything else (zip, tarball, binaries, …) is not a chat attachment
|
|
72
70
|
* and belongs on the workspace filesystem instead. Mirrors
|
|
73
71
|
* `codersdk.AllChatAttachmentMediaTypes`.
|
|
@@ -240,7 +238,7 @@ interface ChatMessagesResponse {
|
|
|
240
238
|
queued_messages: ChatQueuedMessage[];
|
|
241
239
|
has_more: boolean;
|
|
242
240
|
}
|
|
243
|
-
/** Response from `POST /api/
|
|
241
|
+
/** Response from `POST /api/v2/chats/files`: the uploaded file's id. */
|
|
244
242
|
interface UploadChatFileResponse {
|
|
245
243
|
id: string;
|
|
246
244
|
}
|
|
@@ -363,7 +361,7 @@ declare const TERMINAL_STATUSES: ReadonlySet<ChatStatus>;
|
|
|
363
361
|
/** Kinds of chat lifecycle events emitted on the per-user watch socket. */
|
|
364
362
|
type ChatWatchEventKind = "status_change" | "summary_change" | "title_change" | "created" | "deleted" | "diff_status_change" | "action_required" | "context_dirty";
|
|
365
363
|
/**
|
|
366
|
-
* An event from `GET /api/
|
|
364
|
+
* An event from `GET /api/v2/chats/watch`, which covers every chat
|
|
367
365
|
* visible to the authenticated user (not a single chat). Mirrors
|
|
368
366
|
* `codersdk.ChatWatchEvent`.
|
|
369
367
|
*/
|
|
@@ -409,7 +407,7 @@ interface TransportEventBase {
|
|
|
409
407
|
* extend this union; treat unrecognized values as "some other operation"
|
|
410
408
|
* rather than matching exhaustively.
|
|
411
409
|
*/
|
|
412
|
-
type CoderClientOperation = "listModelConfigs" | "resolveModelConfigId" | "createChat" | "getChat" | "createChatMessage" | "getMessages" | "submitToolResults" | "interruptChat" | "deleteQueuedMessage" | "updateChat" | "archiveChat" | "uploadChatFile" | "getChatFile";
|
|
410
|
+
type CoderClientOperation = "listModelConfigs" | "resolveModelConfigId" | "createChat" | "getChat" | "createChatMessage" | "getMessages" | "submitToolResults" | "interruptChat" | "deleteQueuedMessage" | "updateChat" | "archiveChat" | "uploadChatFile" | "getChatFile" | "streamEvents" | "watchChats";
|
|
413
411
|
interface HttpTransportEventBase extends TransportEventBase {
|
|
414
412
|
/**
|
|
415
413
|
* Correlates a request with its response/error. Monotonically increasing
|
|
@@ -426,7 +424,7 @@ interface HttpTransportEventBase extends TransportEventBase {
|
|
|
426
424
|
method: string;
|
|
427
425
|
/**
|
|
428
426
|
* Path + query relative to the client's `baseUrl`, e.g.
|
|
429
|
-
* `/api/
|
|
427
|
+
* `/api/v2/chats/{id}/tool-results`. Never carries credentials.
|
|
430
428
|
*/
|
|
431
429
|
path: string;
|
|
432
430
|
}
|
|
@@ -639,6 +637,8 @@ interface StreamChatEventsOptions {
|
|
|
639
637
|
afterId?: number;
|
|
640
638
|
signal?: AbortSignal;
|
|
641
639
|
webSocketFactory?: WebSocketFactory;
|
|
640
|
+
/** Resolve the client's route prefix before dialing, using the reader's cancellation signal. */
|
|
641
|
+
resolveApiPrefix?: (signal: AbortSignal) => Promise<string>;
|
|
642
642
|
/**
|
|
643
643
|
* Observability hook: receives the reader's `ws:*` transport events (dial,
|
|
644
644
|
* open, decoded events, close, error, redial). Exceptions it throws are
|
|
@@ -695,6 +695,8 @@ interface WatchChatEventsOptions {
|
|
|
695
695
|
token: string;
|
|
696
696
|
signal?: AbortSignal;
|
|
697
697
|
webSocketFactory?: WebSocketFactory;
|
|
698
|
+
/** Resolve the client's route prefix before dialing, using the reader's cancellation signal. */
|
|
699
|
+
resolveApiPrefix?: (signal: AbortSignal) => Promise<string>;
|
|
698
700
|
}
|
|
699
701
|
/**
|
|
700
702
|
* Opens the chatd `/watch` WebSocket — lifecycle events for every chat visible
|
|
@@ -731,7 +733,7 @@ interface UploadedChatFile {
|
|
|
731
733
|
name?: string;
|
|
732
734
|
}
|
|
733
735
|
interface CoderChatClientOptions {
|
|
734
|
-
/** Base URL of the Coder deployment, e.g. `https://
|
|
736
|
+
/** Base URL of the Coder deployment, e.g. `https://dogfood.cdr.dev`. */
|
|
735
737
|
baseUrl: string;
|
|
736
738
|
/** Coder API token or session token (sent as `Coder-Session-Token`). */
|
|
737
739
|
token: string;
|
|
@@ -748,8 +750,8 @@ interface CoderChatClientOptions {
|
|
|
748
750
|
onTransportEvent?: TransportEventHandler;
|
|
749
751
|
}
|
|
750
752
|
/**
|
|
751
|
-
* A thin, typed client for Coder's
|
|
752
|
-
*
|
|
753
|
+
* A thin, typed client for Coder's chat API, using stable routes with a
|
|
754
|
+
* per-instance experimental-prefix fallback for deployments before v2.37.0.
|
|
753
755
|
*/
|
|
754
756
|
declare class CoderChatClient {
|
|
755
757
|
#private;
|
|
@@ -830,10 +832,12 @@ declare class CoderChatClient {
|
|
|
830
832
|
/**
|
|
831
833
|
* Watch lifecycle events (status/title changes, creation, deletion, …) for
|
|
832
834
|
* every chat visible to the authenticated user via the `/chats/watch`
|
|
833
|
-
* WebSocket.
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
835
|
+
* WebSocket. An unresolved client first probes the REST chat listing;
|
|
836
|
+
* probe failures propagate unless the reader was cancelled. Once connected,
|
|
837
|
+
* dropped connections are redialed with exponential backoff. Aborting the
|
|
838
|
+
* signal or ending iteration tears down the reader; a 4xx upgrade rejection
|
|
839
|
+
* is a terminal {@link CoderApiError} (bad/expired token, or an older Coder
|
|
840
|
+
* server without the endpoint → 404).
|
|
837
841
|
*/
|
|
838
842
|
watchChats(opts?: {
|
|
839
843
|
signal?: AbortSignal;
|
|
@@ -974,7 +978,7 @@ interface CoderAgentSettings<TOOLS extends ToolSet = {}> {
|
|
|
974
978
|
* set `CODER_URL` + `CODER_SESSION_TOKEN`) to enable them.
|
|
975
979
|
*/
|
|
976
980
|
client?: CoderChatClient;
|
|
977
|
-
/** Coder deployment base URL, e.g. `https://
|
|
981
|
+
/** Coder deployment base URL, e.g. `https://dogfood.cdr.dev`. Defaults to `CODER_URL`. */
|
|
978
982
|
baseUrl?: string;
|
|
979
983
|
/** Coder API/session token (sent as `Coder-Session-Token`). Defaults to `CODER_SESSION_TOKEN`. */
|
|
980
984
|
token?: string;
|
package/dist/index.js
CHANGED
|
@@ -261,7 +261,15 @@ async function* streamChatEventsLoop(options) {
|
|
|
261
261
|
const emit = safeTransportEmitter(options.onTransportEvent);
|
|
262
262
|
let attempt = 0;
|
|
263
263
|
const wsBase = httpToWs(baseUrl);
|
|
264
|
-
|
|
264
|
+
if (signal.aborted) return;
|
|
265
|
+
let prefix = "/api/v2/chats";
|
|
266
|
+
try {
|
|
267
|
+
if (options.resolveApiPrefix) prefix = await options.resolveApiPrefix(signal);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (signal.aborted) return;
|
|
270
|
+
throw err;
|
|
271
|
+
}
|
|
272
|
+
const path = `${prefix}/${chatId}/stream`;
|
|
265
273
|
const query = options.afterId !== void 0 ? `?after_id=${options.afterId}` : "";
|
|
266
274
|
const url = `${wsBase}${path}${query}`;
|
|
267
275
|
let maxCommittedId = options.afterId;
|
|
@@ -480,7 +488,6 @@ async function* streamChatEventsLoop(options) {
|
|
|
480
488
|
backoffMs = Math.min(backoffMs * 2, STREAM_BACKOFF_CAP_MS);
|
|
481
489
|
}
|
|
482
490
|
}
|
|
483
|
-
var WATCH_PATH = "/api/experimental/chats/watch";
|
|
484
491
|
var WATCH_BACKOFF_INITIAL_MS = 1e3;
|
|
485
492
|
var WATCH_BACKOFF_CAP_MS = 3e4;
|
|
486
493
|
function sleep(ms, signal) {
|
|
@@ -558,7 +565,16 @@ function watchChatEvents(options) {
|
|
|
558
565
|
async function* watchChatEventsLoop(options) {
|
|
559
566
|
const { baseUrl, token, signal } = options;
|
|
560
567
|
const factory = options.webSocketFactory ?? defaultFactory;
|
|
561
|
-
|
|
568
|
+
if (signal.aborted) return;
|
|
569
|
+
let prefix = "/api/v2/chats";
|
|
570
|
+
try {
|
|
571
|
+
if (options.resolveApiPrefix) prefix = await options.resolveApiPrefix(signal);
|
|
572
|
+
} catch (err) {
|
|
573
|
+
if (signal.aborted) return;
|
|
574
|
+
throw err;
|
|
575
|
+
}
|
|
576
|
+
const path = `${prefix}/watch`;
|
|
577
|
+
const url = `${httpToWs(baseUrl)}${path}`;
|
|
562
578
|
let backoffMs = WATCH_BACKOFF_INITIAL_MS;
|
|
563
579
|
while (!signal?.aborted) {
|
|
564
580
|
const queue = [];
|
|
@@ -609,7 +625,7 @@ async function* watchChatEventsLoop(options) {
|
|
|
609
625
|
if (finished) return;
|
|
610
626
|
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "chat watch socket error";
|
|
611
627
|
const status = upgradeStatus(message);
|
|
612
|
-
failure = status !== void 0 && status >= 400 && status < 500 ? new CoderApiError({ status, method: "GET", path
|
|
628
|
+
failure = status !== void 0 && status >= 400 && status < 500 ? new CoderApiError({ status, method: "GET", path, message }) : new CoderAgentError(message);
|
|
613
629
|
finished = true;
|
|
614
630
|
wake();
|
|
615
631
|
};
|
|
@@ -652,7 +668,8 @@ async function* watchChatEventsLoop(options) {
|
|
|
652
668
|
}
|
|
653
669
|
|
|
654
670
|
// src/coder/client.ts
|
|
655
|
-
var API_PREFIX = "/api/
|
|
671
|
+
var API_PREFIX = "/api/v2/chats";
|
|
672
|
+
var LEGACY_API_PREFIX = "/api/experimental/chats";
|
|
656
673
|
var CoderChatClient = class {
|
|
657
674
|
baseUrl;
|
|
658
675
|
#token;
|
|
@@ -663,6 +680,7 @@ var CoderChatClient = class {
|
|
|
663
680
|
/** Exception-isolated emitter for this client's own HTTP events. */
|
|
664
681
|
#emitTransportEvent;
|
|
665
682
|
#httpSeq = 0;
|
|
683
|
+
#apiPrefix;
|
|
666
684
|
constructor(options) {
|
|
667
685
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
668
686
|
this.#token = options.token;
|
|
@@ -678,7 +696,7 @@ var CoderChatClient = class {
|
|
|
678
696
|
* public client method on the exchange's `http:*` events (issue #112) — the
|
|
679
697
|
* emit site is shared, so each call site threads its own literal.
|
|
680
698
|
*/
|
|
681
|
-
async #
|
|
699
|
+
async #sendOnce(op, method, path, opts) {
|
|
682
700
|
const headers = {
|
|
683
701
|
"Coder-Session-Token": this.#token,
|
|
684
702
|
...opts?.headers
|
|
@@ -734,6 +752,65 @@ var CoderChatClient = class {
|
|
|
734
752
|
}
|
|
735
753
|
return res;
|
|
736
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* Negotiate promoted chat routes from responses, not buildinfo versions:
|
|
757
|
+
* v2.37.0 introduced /api/v2/chats (coder/coder#28496), while patched older
|
|
758
|
+
* releases still only mount /api/experimental/chats. Retry a v2 404 once on
|
|
759
|
+
* experimental; the first non-404 HTTP response locks the prefix for this
|
|
760
|
+
* client. Network/abort failures cannot establish route support. Two 404s
|
|
761
|
+
* preserve the original v2 error (possibly a missing resource) and leave
|
|
762
|
+
* negotiation open for the next call. A locked client never falls back.
|
|
763
|
+
*
|
|
764
|
+
* Concurrent initial calls may probe in parallel; a late response cannot
|
|
765
|
+
* overwrite the first lock. Model listings use separate routes/fallbacks
|
|
766
|
+
* and must neither consult nor establish this chat-prefix lock.
|
|
767
|
+
*/
|
|
768
|
+
async #send(op, method, path, opts) {
|
|
769
|
+
if (path !== API_PREFIX && !path.startsWith(`${API_PREFIX}/`)) {
|
|
770
|
+
return this.#sendOnce(op, method, path, opts);
|
|
771
|
+
}
|
|
772
|
+
const suffix = path.slice(API_PREFIX.length);
|
|
773
|
+
if (this.#apiPrefix) {
|
|
774
|
+
return this.#sendOnce(op, method, `${this.#apiPrefix}${suffix}`, opts);
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const res = await this.#sendOnce(op, method, path, opts);
|
|
778
|
+
this.#apiPrefix ??= API_PREFIX;
|
|
779
|
+
return res;
|
|
780
|
+
} catch (err) {
|
|
781
|
+
if (!(err instanceof CoderApiError)) throw err;
|
|
782
|
+
if (err.status !== 404) {
|
|
783
|
+
this.#apiPrefix ??= API_PREFIX;
|
|
784
|
+
throw err;
|
|
785
|
+
}
|
|
786
|
+
if (this.#apiPrefix === API_PREFIX) throw err;
|
|
787
|
+
try {
|
|
788
|
+
const res = await this.#sendOnce(op, method, `${LEGACY_API_PREFIX}${suffix}`, opts);
|
|
789
|
+
this.#apiPrefix ??= LEGACY_API_PREFIX;
|
|
790
|
+
return res;
|
|
791
|
+
} catch (legacyErr) {
|
|
792
|
+
if (legacyErr instanceof CoderApiError) {
|
|
793
|
+
if (legacyErr.status === 404) throw err;
|
|
794
|
+
this.#apiPrefix ??= LEGACY_API_PREFIX;
|
|
795
|
+
}
|
|
796
|
+
throw legacyErr;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Resolve before a WebSocket dial or a non-replayable streaming upload. A
|
|
802
|
+
* read-only listing avoids consuming the upload body and works even without
|
|
803
|
+
* a chat id (/watch). The WS reader supplies its internal cancellation signal
|
|
804
|
+
* so return()/throw()/async disposal can also cancel an in-flight probe.
|
|
805
|
+
*/
|
|
806
|
+
async #resolveApiPrefix(op, signal) {
|
|
807
|
+
if (!this.#apiPrefix) {
|
|
808
|
+
const res = await this.#send(op, "GET", API_PREFIX, { signal });
|
|
809
|
+
await res.body?.cancel();
|
|
810
|
+
}
|
|
811
|
+
if (!this.#apiPrefix) throw new CoderAgentError("Chat route probe succeeded without a prefix");
|
|
812
|
+
return this.#apiPrefix;
|
|
813
|
+
}
|
|
737
814
|
/** Read a response body as JSON, tolerating an unreadable/empty/malformed body (→ undefined). */
|
|
738
815
|
async #json(res) {
|
|
739
816
|
const text = await res.text().catch(() => "");
|
|
@@ -884,6 +961,9 @@ var CoderChatClient = class {
|
|
|
884
961
|
);
|
|
885
962
|
headers["Content-Disposition"] = `attachment; filename="${ascii}"; filename*=UTF-8''${utf8}`;
|
|
886
963
|
}
|
|
964
|
+
if (resolved.body instanceof ReadableStream) {
|
|
965
|
+
await this.#resolveApiPrefix("uploadChatFile", signal);
|
|
966
|
+
}
|
|
887
967
|
const res = await this.#send(
|
|
888
968
|
"uploadChatFile",
|
|
889
969
|
"POST",
|
|
@@ -895,7 +975,7 @@ var CoderChatClient = class {
|
|
|
895
975
|
throw new CoderApiError({
|
|
896
976
|
status: res.status,
|
|
897
977
|
method: "POST",
|
|
898
|
-
path: `${
|
|
978
|
+
path: `${this.#apiPrefix}/files`,
|
|
899
979
|
message: "upload succeeded but the response contained no file id"
|
|
900
980
|
});
|
|
901
981
|
}
|
|
@@ -918,6 +998,7 @@ var CoderChatClient = class {
|
|
|
918
998
|
baseUrl: this.baseUrl,
|
|
919
999
|
token: this.#token,
|
|
920
1000
|
chatId,
|
|
1001
|
+
resolveApiPrefix: (signal) => this.#resolveApiPrefix("streamEvents", signal),
|
|
921
1002
|
afterId: opts?.afterId,
|
|
922
1003
|
signal: opts?.signal,
|
|
923
1004
|
reader: opts?.reader,
|
|
@@ -928,15 +1009,18 @@ var CoderChatClient = class {
|
|
|
928
1009
|
/**
|
|
929
1010
|
* Watch lifecycle events (status/title changes, creation, deletion, …) for
|
|
930
1011
|
* every chat visible to the authenticated user via the `/chats/watch`
|
|
931
|
-
* WebSocket.
|
|
932
|
-
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
1012
|
+
* WebSocket. An unresolved client first probes the REST chat listing;
|
|
1013
|
+
* probe failures propagate unless the reader was cancelled. Once connected,
|
|
1014
|
+
* dropped connections are redialed with exponential backoff. Aborting the
|
|
1015
|
+
* signal or ending iteration tears down the reader; a 4xx upgrade rejection
|
|
1016
|
+
* is a terminal {@link CoderApiError} (bad/expired token, or an older Coder
|
|
1017
|
+
* server without the endpoint → 404).
|
|
935
1018
|
*/
|
|
936
1019
|
watchChats(opts) {
|
|
937
1020
|
return watchChatEvents({
|
|
938
1021
|
baseUrl: this.baseUrl,
|
|
939
1022
|
token: this.#token,
|
|
1023
|
+
resolveApiPrefix: (signal) => this.#resolveApiPrefix("watchChats", signal),
|
|
940
1024
|
signal: opts?.signal,
|
|
941
1025
|
webSocketFactory: this.#webSocketFactory
|
|
942
1026
|
});
|
|
@@ -972,7 +1056,7 @@ var CoderChatClient = class {
|
|
|
972
1056
|
const raw = await this.#request(
|
|
973
1057
|
op,
|
|
974
1058
|
"GET",
|
|
975
|
-
`${
|
|
1059
|
+
`${LEGACY_API_PREFIX}/model-configs`,
|
|
976
1060
|
void 0,
|
|
977
1061
|
signal
|
|
978
1062
|
);
|