@coder/ai-sdk-agent 0.11.6 → 0.11.8

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.
@@ -0,0 +1,130 @@
1
+ # Observability: transport events
2
+
3
+ `onTransportEvent` receives typed transport events: HTTP exchanges, the per‑chat
4
+ stream's WebSocket lifecycle, and turn‑segment boundaries. Use it for timing and
5
+ tracing without wrapping `fetch`/`webSocketFactory` or re‑parsing stream frames.
6
+
7
+ Pass it on any of:
8
+
9
+ - `CoderAgentSettings` — reaches both the client and the model.
10
+ - `CoderChatClientOptions`
11
+ - `CoderLanguageModelConfig`
12
+
13
+ ## Example: where did the turn spend its time?
14
+
15
+ ```ts
16
+ import { CoderAgent, type CoderTransportEvent } from "@coder/ai-sdk-agent";
17
+
18
+ const events: CoderTransportEvent[] = [];
19
+ const agent = new CoderAgent({
20
+ baseUrl,
21
+ token,
22
+ organizationId,
23
+ onTransportEvent: (ev) => events.push(ev),
24
+ });
25
+
26
+ await agent.generate({ prompt: "…" });
27
+
28
+ for (const ev of events) {
29
+ if (ev.type === "http:response")
30
+ console.log(
31
+ `${ev.op}: ${ev.method} ${ev.path} → ${ev.status} in ${ev.durationMs.toFixed(0)}ms`,
32
+ );
33
+ if (ev.type === "ws:event" && ev.event.type === "action_required")
34
+ console.log(`tool calls arrived at +${ev.timestamp - events[0]!.timestamp}ms`);
35
+ if (ev.type === "segment:settle")
36
+ console.log(`segment ${ev.segment}: ${ev.status} in ${ev.durationMs.toFixed(0)}ms`);
37
+ }
38
+ ```
39
+
40
+ ## Event reference
41
+
42
+ `CoderTransportEvent` is a discriminated union on `type`. Every event carries
43
+ `timestamp`: `Date.now()` at observation. It is comparable to server‑side
44
+ timestamps such as a message's `created_at`, so you can measure delivery lag.
45
+
46
+ | event | when | payload (besides `timestamp`) |
47
+ | ---------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
48
+ | `http:request` | a REST request is sent | `id` (correlates the pair), `op` (the client operation, e.g. `"createChatMessage"`), `method`, `path` |
49
+ | `http:response` | response headers arrive (incl. non‑2xx, `ok:false`) | `id`, `op`, `method`, `path`, `status`, `ok`, `durationMs` |
50
+ | `http:error` | the fetch itself rejects (network failure, abort) | `id`, `op`, `method`, `path`, `message`, `durationMs` |
51
+ | `ws:dial` | a stream connection attempt starts | `chatId`, `reader` (identifies the `streamChatEvents` call), `attempt` (1‑based per reader, increments per redial), `url` |
52
+ | `ws:open` | the WebSocket handshake completes | `chatId`, `reader`, `attempt` |
53
+ | `ws:event` | a decoded stream event arrives | `chatId`, `reader`, `attempt`, `event` (the decoded `ChatStreamEvent`, by reference — don't mutate), `forwarded` (the reader's replay verdict) |
54
+ | `ws:close` | the connection ends (exactly one per dial) | `chatId`, `reader`, `attempt`, `code`/`reason` when the server/network closed it; absent when the reader closed it (settle, teardown, redial) |
55
+ | `ws:error` | a socket error or unparseable frame | `chatId`, `reader`, `attempt`, `message` |
56
+ | `ws:redial` | a dropped connection is about to be redialed | `chatId`, `reader`, `attempt` (the ended connection), `consecutiveFailures`, `maxConsecutiveFailures`, `backoffMs` |
57
+ | `segment:start` | a turn segment (one model round‑trip) starts | `segment` (1‑based per model instance), `chatId` (absent before the first turn creates the chat) |
58
+ | `segment:settle` | the segment ends (exactly one per start) | `segment`, `chatId`, `reader` (the reader that served the segment; absent if none was acquired), `durationMs`, and: `status` + `finishReason` on a clean settle, `error` (`{name, message}`, plus `status` if the run still settled terminally) on failure, neither on a teardown |
59
+
60
+ ## Guarantees
61
+
62
+ - **Isolation** — exceptions thrown by the handler are swallowed. They can never
63
+ alter transport behavior or a turn's outcome.
64
+ - **Zero overhead** — without a handler, no event objects are allocated and no
65
+ extra socket listeners are registered.
66
+ - **No secrets** — events carry no headers and no tokens. Auth travels in the
67
+ `Coder-Session-Token` header, which is deliberately excluded. `path`/`url`
68
+ never contain credentials.
69
+
70
+ ## Semantics
71
+
72
+ ### HTTP operation names
73
+
74
+ - `op` is the public `CoderChatClient` method performing the exchange
75
+ (`"createChat"`, `"createChatMessage"`, `"getMessages"`,
76
+ `"submitToolResults"`, … — the `CoderClientOperation` union). Classify per
77
+ operation without reverse‑engineering `path`.
78
+ - `method`/`path` stay for generic consumers.
79
+ - `archiveChat` stamps its own `op`, even though it issues the same `PATCH` as
80
+ `updateChat`.
81
+ - WebSocket prefix‑selection preflights emit the existing `http:*` event kinds
82
+ with `op: "streamEvents"` or `op: "watchChats"`. There are no new event kinds.
83
+
84
+ ### Replay and `forwarded`
85
+
86
+ - `ws:event` fires at arrival. After a redial, chatd's replay of the
87
+ in‑progress episode is visible here; correlate with `reader`/`attempt`.
88
+ - Each `ws:event` is stamped with the reader's own replay verdict:
89
+ `forwarded: false` exactly on the duplicate deltas the reader suppresses from
90
+ the turn. Subscribers never re‑derive the episode filter.
91
+ - `forwarded` does **not** reflect snapshot dedup. Repeated or revised
92
+ `message` snapshots are always `forwarded: true`: reconciling them is
93
+ deliberately the consumer's job past the transport layer. `TurnTranslator`'s
94
+ per‑message ledger decides what a revision re‑emits, and that disposition is
95
+ not stamped on transport frames.
96
+ - Use `ws:event` for span pairing and replay accounting. If you need content
97
+ fidelity, consume model output (or `TurnTranslator`), not transport frames.
98
+
99
+ ### Identifying a connection
100
+
101
+ Identify a connection as `(chatId, reader, attempt)`, never `(chatId, attempt)`
102
+ alone.
103
+
104
+ - `reader` is a monotonic id for the `streamChatEvents` call behind the
105
+ connection. It comes from one process‑wide counter, so it stays unique across
106
+ model and client instances. Every `ws:*` event carries it.
107
+ - `attempt` restarts at 1 per reader.
108
+ - Why it matters: a client‑tool pause the caller abandons is closed
109
+ fire‑and‑forget when the next turn dials its replacement. The superseded
110
+ reader's late `ws:close` (or a raced‑in frame) can emit after the new reader's
111
+ `ws:dial`. The reader id tells them apart.
112
+ - `segment:settle` names the reader that served the segment. `segment:start`
113
+ predates stream acquisition and carries none.
114
+
115
+ ### Multi‑step turns
116
+
117
+ A multi‑step turn that drives client tools emits one
118
+ `segment:start`/`segment:settle` pair per round‑trip, all riding **one**
119
+ `ws:dial`ed connection: the stream is retained across `requires_action` pauses.
120
+
121
+ - A pause settles with `status: "requires_action"`, `finishReason: "tool-calls"`.
122
+ - The final settle carries the terminal status (`waiting`/`completed`/`error`).
123
+
124
+ ### Coverage
125
+
126
+ - `ws:*` events cover the per‑chat `/stream` reader (turn transport). The
127
+ `watchChats` subscription is not instrumented.
128
+ - With a pre‑built `client` in `CoderAgentSettings`, HTTP/WS events come from the
129
+ hook given to **that client's** options. The agent‑level hook then only
130
+ receives `segment:*` events.
@@ -0,0 +1,134 @@
1
+ # Structured output from an agent run
2
+
3
+ Coder Agents has no server‑side `response_format`. `CoderAgent` cannot constrain
4
+ what the model **says** to a JSON schema: a `responseFormat` /
5
+ `experimental_output` request emits a warning and is best‑effort at most.
6
+
7
+ Pick by what the step needs:
8
+
9
+ | The step… | Use |
10
+ | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
11
+ | is pure text‑in / JSON‑out, with no server‑side tools | [`@coder/ai-sdk-provider`](https://github.com/coder/ai-sdk/tree/main/packages/provider) with `generateObject` / `Output.object` — schema‑constrained; requires AI Gateway on the deployment |
12
+ | must produce its answer from an agent run (server‑side tools, MCP, a workspace) | the **`structured_output` tool pattern** below |
13
+
14
+ ## The `structured_output` tool pattern
15
+
16
+ What the model _says_ isn't schema‑constrained, but what it passes **into a
17
+ tool** is typed. So have it submit its answer by calling a tool whose
18
+ `inputSchema` is your Zod schema. The answer arrives as the tool call's typed
19
+ `input` — no fishing JSON out of prose.
20
+
21
+ ```ts
22
+ import { CoderAgent } from "@coder/ai-sdk-agent";
23
+ import { stepCountIs, tool } from "ai";
24
+ import { z } from "zod";
25
+
26
+ const Answer = z.object({ severity: z.enum(["critical", "major", "minor"]), summary: z.string() });
27
+
28
+ const agent = new CoderAgent({
29
+ organizationId: "your-org-uuid", // connection defaults to CODER_URL + CODER_SESSION_TOKEN
30
+ instructions: "… Submit your final answer by calling the structured_output tool exactly once.",
31
+ tools: {
32
+ structured_output: tool({
33
+ description:
34
+ "Submit your final structured answer as JSON. Call this exactly once, when your work is complete.",
35
+ inputSchema: Answer, // your schema IS the tool's input schema
36
+ // Ack instead of stopping the turn: the model finishes naturally and can
37
+ // wind down anything it still has running (dev servers, watchers, …).
38
+ execute: async () =>
39
+ "Output received. Wind down and end your turn. Do not call structured_output again.",
40
+ }),
41
+ },
42
+ stopWhen: stepCountIs(6), // happy path is 2 steps: file + ack, wind down
43
+ });
44
+
45
+ const result = await agent.generate({ prompt: "…" });
46
+ // toolCalls only holds the LAST step's calls — scan all steps. Take the last call
47
+ // that VALIDATES: a schema-invalid re-file must not shadow a valid answer (rule 2).
48
+ const filed = result.steps
49
+ .flatMap((s) => s.toolCalls)
50
+ .filter((c) => c.toolName === "structured_output");
51
+ let answer: z.infer<typeof Answer> | undefined;
52
+ for (const call of filed.reverse()) {
53
+ const parsed = Answer.safeParse(call.input);
54
+ if (parsed.success) {
55
+ answer = parsed.data; // typed: { severity: "critical" | "major" | "minor"; summary: string }
56
+ break;
57
+ }
58
+ }
59
+ if (answer === undefined)
60
+ throw new Error("no valid structured_output call — nudge once on an idle chat (rule 3)");
61
+ ```
62
+
63
+ ## Rules that keep it robust
64
+
65
+ Each rule guards against a failure mode observed live.
66
+
67
+ ### 1. Don't force `toolChoice`, and don't stop on the call
68
+
69
+ - **`toolChoice`** is construction‑time and applies to _every_ segment. After the
70
+ ack it would force the tool again and again, up to the step ceiling. It also
71
+ blocks any other tools the step needs.
72
+ - **A `hasToolCall` stop is worse.** The server only receives a client tool
73
+ result as a side effect of the _next_ loop segment. Ending the loop on the
74
+ call strands the chat in `requires_action`: follow‑up messages queue forever
75
+ and `archive()` 409s.
76
+
77
+ Instructions plus the tool's own description are enough; models file unprompted
78
+ most of the time.
79
+
80
+ ### 2. Validate client‑side
81
+
82
+ The schema is not enforced server‑side. `schema.safeParse` on the tool input is
83
+ the real gate. Schema‑invalid calls that the AI SDK catches in‑loop are
84
+ automatically answered with a `tool-error` result the model retries against.
85
+
86
+ ### 3. Nudge at most once, and only an idle chat
87
+
88
+ If the turn ends in prose (`finishReason: "stop"`) without a valid call, send one
89
+ typed re‑prompt ("Call the structured_output tool now …"), then fail into your
90
+ normal error handling.
91
+
92
+ Never re‑prompt a chat that isn't idle: the message would queue behind whatever
93
+ the server is still doing.
94
+
95
+ ### 4. Settle a turn that stopped on a tool call
96
+
97
+ If the loop stops on a tool‑call step — e.g. your `stopWhen` ceiling lands
98
+ exactly on the `structured_output` call (`finishReason: "tool-calls"`) — the tool
99
+ results ran locally but never reached the server. Settle the chat before touching
100
+ it again, or it strands as in rule 1:
101
+
102
+ 1. Guard on `agent.chatId`. It is `undefined` until the first turn creates the
103
+ chat.
104
+ 2. Read the stranded step's (`result.steps.at(-1)`) locally‑executed client
105
+ outcomes off its **content parts**:
106
+ - a `tool-result` part is a success;
107
+ - a `tool-error` part (the tool's `execute` threw) must be submitted with
108
+ `is_error: true` — mirroring what the resume path would have sent.
109
+ 3. Submit them directly:
110
+ `agent.client.submitToolResults(chatId, { results: [{ tool_call_id, output, is_error }] }, AbortSignal.timeout(8_000))`.
111
+ 4. If a pending call has no local outcome (or the submit fails), end the
112
+ stranded turn instead with
113
+ `agent.client.interruptChat(chatId, AbortSignal.timeout(8_000))`.
114
+
115
+ **Bound every one of these recovery requests with an `AbortSignal`.** They
116
+ target a server that may already be stalled, and the bare `agent.interrupt()` /
117
+ `agent.archive()` helpers carry no timeout.
118
+
119
+ A settled chat resumes its wind‑down server‑side for a few seconds. Retry a
120
+ 409ing archive (`agent.client.archiveChat(chatId, signal)`, per‑attempt bound)
121
+ under a short deadline instead of giving up.
122
+
123
+ ## Copyable helper
124
+
125
+ [`examples/06-structured-output.ts`](https://github.com/coder/ai-sdk/blob/main/packages/agent/examples/06-structured-output.ts)
126
+ packages all four rules into a small helper:
127
+
128
+ - `structuredOutput(schema)` returns `agentOpts` to spread into the constructor,
129
+ plus a typed `ask(agent, prompt)` that runs the settle + one‑nudge ladder and
130
+ returns a `z.infer<typeof schema>`.
131
+ - Compose additional client tools through the helper:
132
+ `structuredOutput(schema, { tools: { myTool } })` merges them into one ToolSet.
133
+ Don't pass `tools:` to the constructor next to the spread — the later key
134
+ silently clobbers the other map.
@@ -0,0 +1,214 @@
1
+ # Workspaces & quota
2
+
3
+ How to run fleets of agents: how a chat binds to a Coder workspace, how to size
4
+ concurrency, what to clean up, and how to diagnose a stuck chat. Read it before
5
+ running many `CoderAgent`s at once. Back to the [package README](../README.md).
6
+
7
+ A chat may be backed by a **Coder workspace** that runs its tools, depending on
8
+ its configuration and the deployment. Workspaces are the scarce resource: a
9
+ deployment budgets how many an account may run at once, so **N agents running concurrently can need N schedulable workspaces.** Past that
10
+ bound, a turn can sit unscheduled and never settle.
11
+
12
+ ## How a chat binds to a workspace
13
+
14
+ - **One chat, at most one workspace, fixed at creation.** `workspaceId` is sent
15
+ as `workspace_id` on chat creation; message and update requests carry no
16
+ workspace field. To move work to another workspace, start a new agent/chat.
17
+ - **This SDK never provisions workspaces.** Pass an existing one. Provision it
18
+ with [`@coder/ai-sdk-sandbox`](https://github.com/coder/ai-sdk/tree/main/packages/sandbox)'s `ensureCoderWorkspace`, the
19
+ CLI, or the v2 API.
20
+ - **A chat created without `workspaceId` can still be workspace‑backed.**
21
+ Deployments may assign one server‑side. The SDK reads the created chat's
22
+ `workspace_id` and treats the chat as workspace‑backed from then on (this
23
+ affects [retry ownership](#preventing-stuck-turns)).
24
+ - **Chat cleanup does not release the workspace.** `archive()` soft‑hides the
25
+ chat only. The workspace keeps running until template autostop or an
26
+ explicit stop. Stopping releases only stop‑scoped quota; persistent resources
27
+ (disks, volumes) keep consuming their cost until the workspace is deleted.
28
+
29
+ ## Sizing a fleet
30
+
31
+ Rule: **workspaces that must be running concurrently ≤ schedulable
32
+ workspaces.**
33
+
34
+ - **One workspace per chat** (the common shape, and what auto‑assigning
35
+ deployments produce): concurrent chats ≤ schedulable workspaces.
36
+ - **Chats bound to a shared `workspaceId`** count it once, so their concurrency
37
+ is not quota‑bound. They share one filesystem and tool environment — only
38
+ acceptable within a single tenant / trust boundary.
39
+
40
+ "Schedulable" is a deployment property, not an SDK knob. Whichever binds first:
41
+
42
+ | Limit | How it works | When exceeded |
43
+ | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
44
+ | **Workspace quota** (premium deployments) | Templates declare per‑resource costs. A user's budget is the sum of their groups' quota allowances, enforced when a build starts or stops ([resource quotas](https://coder.com/docs/admin/users/quotas)). | The build **fails** (`INSUFFICIENT_QUOTA`, "insufficient quota"); the turn never gets its workspace. |
45
+ | **Infrastructure** | No per‑user workspace limit by default without quotas ([workspace lifecycle](https://coder.com/docs/user-guides/workspace-lifecycle)). The bound is provisioner throughput and cluster capacity. | Slow or failing builds, not a crisp quota error. |
46
+
47
+ Practical sizing:
48
+
49
+ 1. **Read headroom before fanning out.**
50
+ `GET /api/v2/organizations/{org}/members/{user}/workspace-quota` returns
51
+ `{ "credits_consumed": …, "budget": … }`. Admit another workspace only while
52
+ `budget − credits_consumed` covers _that workspace's_ cost (the sum of its
53
+ template's `daily_cost` declarations).
54
+ <details><summary>Credits are not slots</summary>
55
+
56
+ "Free slots = headroom ÷ cost" only holds for a homogeneous fleet on one
57
+ template. With mixed templates, size against each planned workspace's own
58
+ cost.
59
+
60
+ </details>
61
+
62
+ 2. **Queue the rest client‑side.** An unschedulable turn does not queue
63
+ usefully on the server ([Preventing stuck turns](#preventing-stuck-turns)).
64
+ 3. **Reuse one bound workspace** across sequential turns and sessions. The
65
+ workspace is the expensive part; the chat is cheap. Reuse only **within one
66
+ tenant / trust boundary**: agents have file and shell tools, so a reused
67
+ filesystem carries one session's artifacts (and secrets) into the next.
68
+ Provision per tenant, or securely reset a workspace before reassigning it.
69
+ 4. **Send steps that don't need server‑side tools to the
70
+ [provider](https://github.com/coder/ai-sdk/tree/main/packages/provider)** — it never touches a workspace.
71
+
72
+ ## Autostop & cleanup
73
+
74
+ Manage two lifetimes separately:
75
+
76
+ - **Chats:** `archive()` / `await using` every agent
77
+ ([Cleanup](../README.md#cleanup)), or finished chats keep holding server
78
+ resources.
79
+ - **Workspaces:** rely on template‑level scheduling, not manual hygiene.
80
+
81
+ | Template setting | What to do |
82
+ | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
83
+ | **Autostop TTL** | Long enough to survive a normal session (including idle gaps between turns), short enough that a leaked workspace stops burning running‑cost within hours. Without autostop, a leaked workspace pins its full quota until someone notices. [Sandbox](https://github.com/coder/ai-sdk/tree/main/packages/sandbox): `stopAfter: "8h"` sets `ttl_ms` at creation. |
84
+ | **Activity bump** (default 1 h) | Extends a running workspace's deadline when Coder detects sessions. Check [what counts as activity](https://coder.com/docs/user-guides/workspace-scheduling) before assuming server‑side tool use keeps a workspace alive. |
85
+ | **Dormancy / failure cleanup** | Reaps abandoned and repeatedly‑failing workspaces automatically ([template scheduling](https://coder.com/docs/admin/templates/managing-templates/schedule)). |
86
+
87
+ **Stopping is not enough.** A _stopped_ workspace typically still consumes its
88
+ persistent resources' `daily_cost` (disks, volumes), so a scratch fleet that
89
+ only ever stops converges on a full budget. Pair the TTL with dormancy
90
+ auto‑deletion or explicit deletion.
91
+
92
+ ## Preventing stuck turns
93
+
94
+ The signature failure of an over‑committed fleet: the chat is created, the
95
+ stream opens, then nothing. There is **no distinct "quota exceeded" error kind
96
+ on the chat stream.** An unschedulable chat surfaces a generic turn error or
97
+ sits in a non‑terminal status indefinitely.
98
+
99
+ Defend in this order:
100
+
101
+ 1. **Set `requestTimeoutMs` — always, in fleets.** It is unbounded by default.
102
+ On expiry the call rejects with a `CoderChatError` (`kind: "timeout"`,
103
+ `retryable: true`), so your dispatcher gets its slot back. Pair it with a
104
+ reconciliation sweep: periodically interrupt/archive non‑terminal chats
105
+ older than your budget.
106
+ <details><summary>Why the run is not guaranteed released on timeout</summary>
107
+
108
+ The timeout fires a server‑side interrupt **best‑effort**: fire‑and‑forget,
109
+ and unreachable when the timeout lands before chat creation returned an id.
110
+ The run usually stops, but is not guaranteed released
111
+ ([Timeouts](../README.md#timeouts--cancellation)).
112
+
113
+ </details>
114
+
115
+ 2. **Cap total wall‑clock** with `abortSignal: AbortSignal.timeout(…)`.
116
+ `requestTimeoutMs` bounds each segment, not a whole multi‑tool call.
117
+ 3. **Own the retries.** Workspace‑backed turns are **never auto‑retried**: a
118
+ stream‑loss error is `isRetryable` only when the failed turn created its
119
+ chat _and_ had no workspace, no MCP servers, and no fresh uploads
120
+ ([Timeouts](../README.md#timeouts--cancellation)). Re‑check quota headroom
121
+ before retrying, or you re‑queue into the same wall.
122
+ 4. **Watch the fleet.** [`watchChats`](../README.md#watching-chats) yields
123
+ status changes for every chat **visible to the authenticated user** — run
124
+ one watcher per identity for per‑tenant credentials, or use a credential
125
+ that sees them all. Alert on chats in a non‑terminal status (e.g. `pending`)
126
+ longer than your `requestTimeoutMs`. Seed and periodically reconcile against
127
+ `GET /api/v2/chats`; don't trust the event stream alone.
128
+ <details><summary>Why events alone miss stuck chats</summary>
129
+
130
+ Reconnects resubscribe fresh (no cursor or replay). A chat already stuck
131
+ before the watcher started — or one that transitioned during a gap — never
132
+ emits an event to start your timer from.
133
+
134
+ </details>
135
+
136
+ ## Troubleshooting: unschedulable & stuck chats
137
+
138
+ Wire [`onTransportEvent`](../README.md#observability) into fleet telemetry; the
139
+ event sequence pinpoints _where_ a turn died. Expand the symptom you see.
140
+
141
+ <details><summary><code>segment:start</code> and <code>ws:open</code> fired, initial status events, then silence</summary>
142
+
143
+ No `message_part`, no `segment:settle`.
144
+
145
+ - **Likely cause:** if the chat's status is `pending` (or its workspace build
146
+ is `pending`/`failed`), the workspace can't be scheduled — quota exhausted,
147
+ no free provisioner, failing template. If the status is `running` with a
148
+ healthy build, it may just be a slow model/tool step; silence alone doesn't
149
+ prove scheduling.
150
+ - **Fix:** check quota headroom (`workspace-quota` endpoint) and the
151
+ workspace's build — a quota failure logs `INSUFFICIENT_QUOTA`. Reclaim enough
152
+ _credits_ for the planned build: stopping helps only for stop‑scoped costs;
153
+ delete workspaces to reclaim persistent costs, or raise the group allowance.
154
+ Set `requestTimeoutMs` so this fails loudly next time.
155
+
156
+ </details>
157
+
158
+ <details><summary><code>segment:settle</code> carries a <code>CoderChatError</code> mentioning the <code>requestTimeoutMs</code> budget</summary>
159
+
160
+ The event looks like
161
+ `error: { name: "CoderChatError", message: "…requestTimeoutMs budget…" }`.
162
+
163
+ - **Likely cause:** the per‑segment bound expired — wedged server, slow model,
164
+ or an unschedulable workspace.
165
+ - **Fix:** inspect the workspace via the v2 API/UI. A `pending`/`failed` build
166
+ means the previous symptom; `running` means the turn was genuinely slow —
167
+ raise `requestTimeoutMs` for long tool work.
168
+
169
+ </details>
170
+
171
+ <details><summary>Repeated <code>ws:redial</code>, then a <code>CoderStreamError</code></summary>
172
+
173
+ `consecutiveFailures` climbs toward `maxConsecutiveFailures` (5; backoff
174
+ 1 s → 2 s → 4 s → 8 s, ≈15 s of redialing without forward progress).
175
+
176
+ - **Likely cause:** the network path to the deployment is failing — not
177
+ workspace scheduling (the server keeps generating through short gaps).
178
+ - **Fix:** fix connectivity. Mind [retry ownership](#preventing-stuck-turns):
179
+ on workspace‑backed chats the error is `isRetryable: false`, so the replay
180
+ decision is yours.
181
+
182
+ </details>
183
+
184
+ <details><summary><code>segment:settle</code> with <code>status: "error"</code> and an <code>error</code> payload</summary>
185
+
186
+ - **Likely cause:** the turn failed server‑side — a provider/model error, a
187
+ tool failure, or a scheduling/build failure that terminated the turn instead
188
+ of leaving it pending.
189
+ - **Fix:** the settle event's `error` carries only `{ name, message }`. To
190
+ branch on `kind` / `retryable` / `statusCode`, catch the thrown
191
+ `CoderChatError`: at the `generate()` call site, or — for `stream()` — around
192
+ stream consumption, since mid‑stream failures surface on the stream, not from
193
+ `await agent.stream()` ([Handling errors](../README.md#handling-errors)).
194
+ Check the workspace build state to rule scheduling in or out.
195
+
196
+ </details>
197
+
198
+ <details><summary>Turn settled <code>status: "requires_action"</code>; follow‑up messages queue forever</summary>
199
+
200
+ - **Likely cause:** the loop ended on an unanswered client tool call.
201
+ - **Fix:** submit the stranded results or interrupt — see [rule 4 of the structured-output guide](./structured-output.md#4-settle-a-turn-that-stopped-on-a-tool-call). If a crash left the
202
+ pause behind, reconcile effects first
203
+ ([Make client tools crash-safe](./durable-workflows.md#make-client-tools-crash-safe)).
204
+
205
+ </details>
206
+
207
+ <details><summary><code>archive()</code> keeps returning 409 and rethrows after ~15 s</summary>
208
+
209
+ - **Likely cause:** the chat never settled server‑side — usually a stuck run
210
+ still holding its workspace.
211
+ - **Fix:** `interrupt()` with a bounded signal, then re‑archive. If the run
212
+ stays wedged, stop the workspace itself.
213
+
214
+ </details>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coder/ai-sdk-agent",
3
- "version": "0.11.6",
3
+ "version": "0.11.8",
4
4
  "description": "A Vercel AI SDK-compliant agent backed by Coder Agents, Coder's server-side agent runtime. `new CoderAgent()` returns a real AI SDK Agent (generate/stream).",
5
5
  "keywords": [
6
6
  "agent",
@@ -25,6 +25,7 @@
25
25
  },
26
26
  "files": [
27
27
  "dist",
28
+ "docs",
28
29
  "README.md"
29
30
  ],
30
31
  "type": "module",
@@ -46,10 +47,10 @@
46
47
  "ws": "^8.21.3"
47
48
  },
48
49
  "devDependencies": {
49
- "@ai-sdk/react": "4.0.111",
50
+ "@ai-sdk/react": "4.0.114",
50
51
  "@types/node": "^26.6.2",
51
52
  "@types/ws": "^8.18.1",
52
- "ai": "^7.0.108",
53
+ "ai": "^7.0.111",
53
54
  "tsup": "^8.5.1",
54
55
  "tsx": "^4.23.15",
55
56
  "typescript": "^6.0.3",