@coder/ai-sdk-agent 0.2.1 → 0.4.0
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 +210 -21
- package/dist/index.d.ts +280 -39
- package/dist/index.js +672 -31
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -43,10 +43,12 @@ pure text/JSON steps and the Agent only for the steps that touch tools.
|
|
|
43
43
|
## Install
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
pnpm add @coder/ai-sdk-agent ai zod
|
|
46
|
+
pnpm add @coder/ai-sdk-agent ai@^7 zod
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Requires Node ≥
|
|
49
|
+
Requires Node ≥ 22 and `ai` v7 — the constructors throw an actionable error when
|
|
50
|
+
another `ai` major is detected (the guard fails open when the installed version
|
|
51
|
+
can't be resolved), instead of failing cryptically mid‑generation.
|
|
50
52
|
|
|
51
53
|
## Quick start
|
|
52
54
|
|
|
@@ -114,6 +116,12 @@ AI SDK tool loop — your `execute` runs in your process.
|
|
|
114
116
|
appear in the transcript as `providerExecuted` tool calls/results — you observe them, you
|
|
115
117
|
don't execute them.
|
|
116
118
|
|
|
119
|
+
Migration note: since v0.2.1 server‑executed tools stream with `dynamic: true`
|
|
120
|
+
(they aren't in your `ToolSet`, and the AI SDK only accepts unknown tool names on
|
|
121
|
+
dynamic calls). In UI message streams they therefore surface as `dynamic-tool`
|
|
122
|
+
parts rather than `tool-{name}` parts — key off `toolName`, not `part.type`, when
|
|
123
|
+
rendering them.
|
|
124
|
+
|
|
117
125
|
## Files
|
|
118
126
|
|
|
119
127
|
There are two distinct ways to get a file to the agent, depending on whether the model should
|
|
@@ -203,13 +211,81 @@ created on the first turn and reused for subsequent `generate()`/`stream()` call
|
|
|
203
211
|
conversation with server‑side history). `agent.chatId` is the current chat id.
|
|
204
212
|
|
|
205
213
|
- `agent.resetSession()` — start a fresh chat on the next turn (reuse one instance for sequential turns; you don't need a new agent per turn).
|
|
206
|
-
- `agent.interrupt()` — interrupt an in‑flight generation.
|
|
207
|
-
- `agent.archive()` — archive the underlying chat (cleanup; see [Cleanup](#cleanup)).
|
|
214
|
+
- `agent.interrupt({ signal? })` — interrupt an in‑flight generation.
|
|
215
|
+
- `agent.archive({ signal? })` — archive the underlying chat (cleanup; see [Cleanup](#cleanup)).
|
|
208
216
|
- `agent.listModels()` — list the deployment's model configs, so you don't have to guess the `model` hint.
|
|
209
217
|
- Resume a prior chat: `new CoderAgent({ …, chatId: "…" })`.
|
|
210
218
|
|
|
219
|
+
Interrupting is asynchronous on the server: `interrupt()` resolves as soon as the
|
|
220
|
+
interrupt is acknowledged, and the run keeps winding down for a few seconds
|
|
221
|
+
afterwards. The client‑level `client.interruptChat(chatId, { wait: true })` sends
|
|
222
|
+
`?wait=true` to ask the server to hold the response until the run has stopped —
|
|
223
|
+
current Coder servers ignore the unknown parameter and still return immediately,
|
|
224
|
+
so confirm completion via the event stream (e.g. [`watchChats`](#watching-chats))
|
|
225
|
+
rather than relying on it.
|
|
226
|
+
|
|
211
227
|
A single instance is **single‑flight** — don't run concurrent generations against it. For concurrency, use one instance per session (and see [Workspaces & quota](#workspaces--quota)).
|
|
212
228
|
|
|
229
|
+
## Rehydrating chat history
|
|
230
|
+
|
|
231
|
+
Chat history lives on the server. To render an existing chat in a UI (e.g. after
|
|
232
|
+
a reload), fetch its messages with the `CoderChatClient` (`agent.client`, or one
|
|
233
|
+
you construct — see [Auth](#auth)) and convert them with
|
|
234
|
+
`chatMessagesToUIMessages` — the mapping mirrors what a live‑streamed transcript
|
|
235
|
+
of the same turn looks like:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
import { chatMessagesToUIMessages } from "@coder/ai-sdk-agent";
|
|
239
|
+
|
|
240
|
+
const { messages } = await client.getMessages(chatId);
|
|
241
|
+
const uiMessages = chatMessagesToUIMessages(messages);
|
|
242
|
+
// e.g. in React: useChat({ messages: uiMessages })
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The converter sorts by message id, so the endpoint's newest‑first default page
|
|
246
|
+
order (and any pagination order) is safe to pass straight in — `useChat` always
|
|
247
|
+
receives a chronological transcript.
|
|
248
|
+
|
|
249
|
+
Tool calls become `dynamic-tool` parts with their results folded in, `source`
|
|
250
|
+
parts become `source-url` parts, and unknown part kinds are skipped silently, so
|
|
251
|
+
history written by newer Coder servers degrades gracefully. One caveat: history
|
|
252
|
+
does not record which tool names were client (`ToolSet`) tools, so _every_ tool
|
|
253
|
+
call rehydrates as `dynamic-tool` — live, client tools stream as statically
|
|
254
|
+
typed `tool-{name}` parts. Render tools by name (ai's
|
|
255
|
+
`isToolOrDynamicToolUIPart` and `getToolOrDynamicToolName`) rather than by
|
|
256
|
+
exact `part.type` and the difference disappears. Persisted `file`
|
|
257
|
+
parts carry only a `file_id` (no bytes, usually no URL), so pass a `fileUrl`
|
|
258
|
+
resolver to keep attachments visible — download the bytes with
|
|
259
|
+
`client.getChatFile(fileId)` and return a data:/object/proxy URL; parts that end
|
|
260
|
+
up without a URL are skipped:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
chatMessagesToUIMessages(messages, {
|
|
264
|
+
fileUrl: (part) => (part.file_id ? `/api/files/${part.file_id}` : undefined),
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Watching chats
|
|
269
|
+
|
|
270
|
+
`client.watchChats({ signal })` yields lifecycle events (status/title changes,
|
|
271
|
+
creation, deletion, …) for **every chat visible to the authenticated user** as an
|
|
272
|
+
async iterable, backed by the `/api/experimental/chats/watch` WebSocket:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
for await (const event of client.watchChats({ signal })) {
|
|
276
|
+
if (event.kind === "status_change") console.log(event.chat.id, event.chat.status);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Unlike the per‑chat event stream, this is a long‑lived subscription: dropped
|
|
281
|
+
connections are redialed automatically with exponential backoff (1s doubling to
|
|
282
|
+
a 30s cap, reset once an event arrives). Iteration ends only when the signal
|
|
283
|
+
aborts, or with a terminal `CoderApiError` when the server rejects the upgrade
|
|
284
|
+
with a 4xx — bad/expired token, or an older Coder server without the endpoint
|
|
285
|
+
(404). For custom plumbing (own client, browser sockets), the standalone
|
|
286
|
+
`watchChatEvents({ baseUrl, token, signal, webSocketFactory })` export provides
|
|
287
|
+
the same stream without a `CoderChatClient`.
|
|
288
|
+
|
|
213
289
|
## Timeouts & cancellation
|
|
214
290
|
|
|
215
291
|
Pass an `abortSignal` to `generate()`/`stream()` to cancel a turn. Aborting
|
|
@@ -242,17 +318,28 @@ truncated result as if the turn had finished.
|
|
|
242
318
|
## Cleanup
|
|
243
319
|
|
|
244
320
|
`archive()` soft‑hides the chat (it stays in listings as `archived: true`; there
|
|
245
|
-
is no hard delete yet).
|
|
246
|
-
|
|
321
|
+
is no hard delete yet). A freshly interrupted chat keeps winding down server‑side
|
|
322
|
+
for a few seconds, during which archiving 409s — `archive()` retries those 409s
|
|
323
|
+
(~1s apart, up to ~15s overall; tune with `settleDeadlineMs` /
|
|
324
|
+
`settleRetryDelayMs`) and rethrows the last one if the chat never settles. Any
|
|
325
|
+
other failure, including your own abort, rethrows immediately.
|
|
326
|
+
|
|
327
|
+
To make cleanup ride scope exit instead of a `finally` you have to remember, the
|
|
328
|
+
agent is an **async disposable**:
|
|
247
329
|
|
|
248
330
|
```ts
|
|
249
331
|
await using agent = new CoderAgent({
|
|
250
332
|
/* … */
|
|
251
333
|
});
|
|
252
334
|
const { text } = await agent.generate({ prompt: "…" });
|
|
253
|
-
// agent.archive()
|
|
335
|
+
// agent.interrupt() + agent.archive() run automatically when the scope exits.
|
|
254
336
|
```
|
|
255
337
|
|
|
338
|
+
Disposal interrupts any in‑flight run, then archives. It is **bounded and never
|
|
339
|
+
throws** (~15s overall, best‑effort): disposal errors are swallowed so they can't
|
|
340
|
+
mask the scope's own error. Call `archive()` directly when you need guaranteed
|
|
341
|
+
cleanup.
|
|
342
|
+
|
|
256
343
|
In a request handler that returns before a fire‑and‑forget `archive()` settles, the
|
|
257
344
|
archive can be abandoned — `await using` (or an awaited `archive()` in `finally`)
|
|
258
345
|
avoids accumulating live chats.
|
|
@@ -284,6 +371,36 @@ try {
|
|
|
284
371
|
SDK‑level retry could duplicate a turn. Prefer catching `retryable` errors and
|
|
285
372
|
retrying the whole step deliberately.
|
|
286
373
|
|
|
374
|
+
## Usage & cost
|
|
375
|
+
|
|
376
|
+
Results carry normalized token usage in `usage`, plus the verbatim snake_case
|
|
377
|
+
wire usage in `usage.raw` for fields the normalized shape has no slot for
|
|
378
|
+
(`context_limit`, cost, runtime, …). When the server reports them,
|
|
379
|
+
`total_cost_micros` (micro‑USD) and `total_runtime_ms` are also mirrored under
|
|
380
|
+
`providerMetadata.coder` on finish. Both are **absence‑tolerant mirrors**:
|
|
381
|
+
today's Coder servers only expose cost on the aggregate cost endpoints
|
|
382
|
+
(`/api/experimental/chats/cost/*`), so expect these fields to be absent —
|
|
383
|
+
nothing is emitted when the server sends neither.
|
|
384
|
+
|
|
385
|
+
Forward usage to a UI via message metadata:
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
const result = await agent.stream({ prompt: "…" });
|
|
389
|
+
return result.toUIMessageStream({
|
|
390
|
+
messageMetadata: ({ part }) =>
|
|
391
|
+
part.type === "finish-step"
|
|
392
|
+
? { usage: part.usage, coder: part.providerMetadata?.coder }
|
|
393
|
+
: undefined,
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Sources
|
|
398
|
+
|
|
399
|
+
Model configs with web search enabled emit `source` parts. These flow through to
|
|
400
|
+
`result.sources` and, in UI message streams, `source-url` parts (pass
|
|
401
|
+
`sendSources: true` to `toUIMessageStream` — the AI SDK omits them by default).
|
|
402
|
+
Earlier releases dropped them.
|
|
403
|
+
|
|
287
404
|
## Structured output
|
|
288
405
|
|
|
289
406
|
Coder Agents has no server‑side `response_format`, so `CoderAgent` cannot
|
|
@@ -325,13 +442,21 @@ const agent = new CoderAgent({
|
|
|
325
442
|
});
|
|
326
443
|
|
|
327
444
|
const result = await agent.generate({ prompt: "…" });
|
|
328
|
-
// toolCalls only holds the LAST step's calls — scan all steps
|
|
329
|
-
|
|
445
|
+
// toolCalls only holds the LAST step's calls — scan all steps. Take the last call
|
|
446
|
+
// that VALIDATES: a schema-invalid re-file must not shadow a valid answer (rule 2).
|
|
447
|
+
const filed = result.steps
|
|
330
448
|
.flatMap((s) => s.toolCalls)
|
|
331
|
-
.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const
|
|
449
|
+
.filter((c) => c.toolName === "structured_output");
|
|
450
|
+
let answer: z.infer<typeof Answer> | undefined;
|
|
451
|
+
for (const call of filed.reverse()) {
|
|
452
|
+
const parsed = Answer.safeParse(call.input);
|
|
453
|
+
if (parsed.success) {
|
|
454
|
+
answer = parsed.data; // typed: { severity: "critical" | "major" | "minor"; summary: string }
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (answer === undefined)
|
|
459
|
+
throw new Error("no valid structured_output call — nudge once on an idle chat (rule 3)");
|
|
335
460
|
```
|
|
336
461
|
|
|
337
462
|
Rules that keep it robust — each guards against a failure mode observed live:
|
|
@@ -356,19 +481,73 @@ Rules that keep it robust — each guards against a failure mode observed live:
|
|
|
356
481
|
4. **Settle a turn that stopped on a tool call.** If the loop stops on a
|
|
357
482
|
tool‑call step — e.g. your `stopWhen` ceiling lands exactly on the
|
|
358
483
|
`structured_output` call (`finishReason: "tool-calls"`) — the tool results
|
|
359
|
-
ran locally but never reached the server.
|
|
360
|
-
|
|
361
|
-
`
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
the
|
|
365
|
-
|
|
366
|
-
|
|
484
|
+
ran locally but never reached the server. Guard on `agent.chatId` (it is
|
|
485
|
+
`undefined` until the first turn creates the chat), then submit the
|
|
486
|
+
stranded step's (`result.steps.at(-1)`) locally‑executed client outcomes
|
|
487
|
+
directly via
|
|
488
|
+
`agent.client.submitToolResults(chatId, { results: [{ tool_call_id, output, is_error }] }, AbortSignal.timeout(8_000))`
|
|
489
|
+
before touching the chat again, or it strands as in rule 1. Read the
|
|
490
|
+
outcomes off the step's **content parts**: a `tool-result` part is a
|
|
491
|
+
success, a `tool-error` part (the tool's `execute` threw) must be submitted
|
|
492
|
+
with `is_error: true` — mirroring what the resume path would have sent. If
|
|
493
|
+
a pending call has no local outcome (or the submit fails), end the stranded
|
|
494
|
+
turn with `agent.client.interruptChat(chatId, AbortSignal.timeout(8_000))`
|
|
495
|
+
instead. **Bound every one of these recovery requests with an
|
|
496
|
+
`AbortSignal`** — they target a server that may already be stalled, and the
|
|
497
|
+
bare `agent.interrupt()` / `agent.archive()` helpers carry no timeout. A
|
|
498
|
+
settled chat resumes its wind‑down server‑side for a few seconds, so retry
|
|
499
|
+
a 409ing archive (`agent.client.archiveChat(chatId, signal)`, per‑attempt
|
|
500
|
+
bound) under a short deadline instead of giving up.
|
|
367
501
|
|
|
368
502
|
[`examples/06-structured-output.ts`](./examples/06-structured-output.ts) packages
|
|
369
503
|
all four rules into a small copyable helper — `structuredOutput(schema)` returns
|
|
370
504
|
`agentOpts` to spread into the constructor plus a typed `ask(agent, prompt)`
|
|
371
505
|
that runs the settle + one‑nudge ladder and returns a `z.infer<typeof schema>`.
|
|
506
|
+
Compose additional client tools through the helper —
|
|
507
|
+
`structuredOutput(schema, { tools: { myTool } })` merges them into one ToolSet —
|
|
508
|
+
rather than passing `tools:` to the constructor next to the spread, where the
|
|
509
|
+
later key silently clobbers the other map.
|
|
510
|
+
|
|
511
|
+
## Workspace previews
|
|
512
|
+
|
|
513
|
+
When the agent is bound to a workspace (the `workspaceId` setting), you can
|
|
514
|
+
resolve — and share — the browser URL where a port on that workspace is served,
|
|
515
|
+
e.g. the dev server the agent just started:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
const { url } = await agent.getPreview({ port: 3000 });
|
|
519
|
+
// → https://3000--main--dev--alice.apps.example.com (private to the workspace owner)
|
|
520
|
+
|
|
521
|
+
const shared = await agent.sharePreview({ port: 3000, shareLevel: "authenticated" });
|
|
522
|
+
// shared.url is now reachable by any logged-in user; shared.shareLevel is the level in effect
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Both are built on the stable v2 workspace APIs (workspace lookup + the wildcard
|
|
526
|
+
apps host; `sharePreview` adds a port‑share upsert), so they work against old
|
|
527
|
+
Coder servers — no experimental endpoints.
|
|
528
|
+
|
|
529
|
+
- `getPreview({ port, agentName?, protocol?, signal? })` composes the subdomain
|
|
530
|
+
URL. The URL honors the port's current share level — private to the workspace
|
|
531
|
+
owner unless shared. `agentName` is optional when the workspace has exactly one
|
|
532
|
+
agent (with several, the error lists the candidates); `protocol: "https"` means
|
|
533
|
+
the app speaks TLS _inside the workspace_ (it adds the `s` label suffix,
|
|
534
|
+
`3000s--…`) and does not affect the browser scheme.
|
|
535
|
+
- `sharePreview({ port, shareLevel?, … })` additionally upserts the port's share
|
|
536
|
+
level (re‑invoking updates it in place) and returns the level in effect.
|
|
537
|
+
`shareLevel` is `"authenticated"` (any logged‑in user; the default),
|
|
538
|
+
`"organization"` (members of the workspace's organization; requires a newer
|
|
539
|
+
Coder server), or `"public"` (no auth at all — mind what the port serves).
|
|
540
|
+
Reverting to owner‑only means deleting the share; `"owner"` is not accepted on
|
|
541
|
+
upsert.
|
|
542
|
+
- Clear failures instead of broken URLs: a deployment without a wildcard access
|
|
543
|
+
URL (`--wildcard-access-url`) yields an explanatory error, and a server that
|
|
544
|
+
predates port sharing (< Coder v2.9) yields a 404 `CoderApiError` saying so.
|
|
545
|
+
Ports below 1000 are rejected up front for the same reason — Coder subdomain
|
|
546
|
+
URLs only encode 4–5 digit ports, so `80--agent--…` would be parsed as an app
|
|
547
|
+
named "80" and never resolve; serve the preview on a higher port.
|
|
548
|
+
|
|
549
|
+
The preview helpers call non‑chat endpoints, so they need `baseUrl` + `token`
|
|
550
|
+
credentials — pass them alongside `client` if you construct one yourself.
|
|
372
551
|
|
|
373
552
|
## Workspaces & quota
|
|
374
553
|
|
|
@@ -402,8 +581,18 @@ running many agents at once:
|
|
|
402
581
|
| `stopWhen` | AI SDK stop condition(s); default `stepCountIs(64)` |
|
|
403
582
|
| `maxRetries` | default `0` — SDK retries can duplicate server‑side turns; override with care |
|
|
404
583
|
| `requestTimeoutMs` | per‑turn time budget (ms); interrupts the run and rejects (`kind: "timeout"`) instead of hanging |
|
|
584
|
+
| `settleDeadlineMs` | overall deadline for bounded cleanup (`archive()` 409 retries, disposal); default 15 000 |
|
|
585
|
+
| `settleRetryDelayMs` | pause between `archive()` retries while the chat settles; default 1000 |
|
|
405
586
|
| `chatId` | resume an existing chat |
|
|
406
587
|
|
|
588
|
+
The `model` hint resolves against the deployment's model configs in order: a
|
|
589
|
+
config UUID is used as‑is, then an exact `provider:model` match, an exact model
|
|
590
|
+
id, a display‑name substring (case‑insensitive), and finally a model‑id
|
|
591
|
+
substring. Partial payloads from older/newer servers are tolerated (entries
|
|
592
|
+
match on the fields they carry), and an unresolvable hint falls back to the
|
|
593
|
+
server's default model instead of failing. Use `agent.listModels()` to see
|
|
594
|
+
what's available.
|
|
595
|
+
|
|
407
596
|
## How it works
|
|
408
597
|
|
|
409
598
|
```
|