@coder/ai-sdk-agent 0.8.0 → 0.9.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 +128 -50
- package/dist/index.d.ts +270 -26
- package/dist/index.js +464 -90
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -223,9 +223,11 @@ conversation with server‑side history). `agent.chatId` is the current chat id.
|
|
|
223
223
|
- `agent.interrupt({ signal? })` — interrupt an in‑flight generation.
|
|
224
224
|
- `agent.archive({ signal? })` — archive the underlying chat (cleanup; see [Cleanup](#cleanup)).
|
|
225
225
|
- `agent.listModels()` — list the deployment's model configs, so you don't have to guess the `model` hint.
|
|
226
|
-
- Resume a prior chat: `new CoderAgent({ …, chatId: "…" })` —
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
- Resume a prior chat: `new CoderAgent({ …, chatId: "…" })` — optionally with
|
|
227
|
+
`lastSeenMessageId` (the persisted resume cursor, read from
|
|
228
|
+
`agent.lastSeenMessageId`) to skip the resumed turn's pre‑prompt history
|
|
229
|
+
probe. See [Durable workflows](#durable-workflows-persist-resume-recover)
|
|
230
|
+
for the full resumption how‑to.
|
|
229
231
|
|
|
230
232
|
Interrupting is asynchronous on the server: `interrupt()` resolves as soon as the
|
|
231
233
|
interrupt is acknowledged, and the run keeps winding down for a few seconds
|
|
@@ -321,7 +323,9 @@ await agent.generate({ prompt: "…" });
|
|
|
321
323
|
// e.g. attribute where the turn spent its time:
|
|
322
324
|
for (const ev of events) {
|
|
323
325
|
if (ev.type === "http:response")
|
|
324
|
-
console.log(
|
|
326
|
+
console.log(
|
|
327
|
+
`${ev.op}: ${ev.method} ${ev.path} → ${ev.status} in ${ev.durationMs.toFixed(0)}ms`,
|
|
328
|
+
);
|
|
325
329
|
if (ev.type === "ws:event" && ev.event.type === "action_required")
|
|
326
330
|
console.log(`tool calls arrived at +${ev.timestamp - events[0]!.timestamp}ms`);
|
|
327
331
|
if (ev.type === "segment:settle")
|
|
@@ -335,12 +339,12 @@ such as a message's `created_at`, for delivery‑lag measurements):
|
|
|
335
339
|
|
|
336
340
|
| event | when | payload (besides `timestamp`) |
|
|
337
341
|
| ---------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
338
|
-
| `http:request` | a REST request is sent | `id` (correlates the pair), `method`, `path`
|
|
339
|
-
| `http:response` | response headers arrive (incl. non‑2xx, `ok:false`) | `id`, `method`, `path`, `status`, `ok`, `durationMs`
|
|
340
|
-
| `http:error` | the fetch itself rejects (network failure, abort) | `id`, `method`, `path`, `message`, `durationMs`
|
|
342
|
+
| `http:request` | a REST request is sent | `id` (correlates the pair), `op` (the client operation, e.g. `"createChatMessage"`), `method`, `path` |
|
|
343
|
+
| `http:response` | response headers arrive (incl. non‑2xx, `ok:false`) | `id`, `op`, `method`, `path`, `status`, `ok`, `durationMs` |
|
|
344
|
+
| `http:error` | the fetch itself rejects (network failure, abort) | `id`, `op`, `method`, `path`, `message`, `durationMs` |
|
|
341
345
|
| `ws:dial` | a stream connection attempt starts | `chatId`, `reader` (identifies the `streamChatEvents` call), `attempt` (1‑based per reader, increments per redial), `url` |
|
|
342
346
|
| `ws:open` | the WebSocket handshake completes | `chatId`, `reader`, `attempt` |
|
|
343
|
-
| `ws:event` | a decoded stream event arrives | `chatId`, `reader`, `attempt`, `event` (the decoded `ChatStreamEvent`, by reference — don't mutate)
|
|
347
|
+
| `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) |
|
|
344
348
|
| `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) |
|
|
345
349
|
| `ws:error` | a socket error or unparseable frame | `chatId`, `reader`, `attempt`, `message` |
|
|
346
350
|
| `ws:redial` | a dropped connection is about to be redialed | `chatId`, `reader`, `attempt` (the ended connection), `consecutiveFailures`, `maxConsecutiveFailures`, `backoffMs` |
|
|
@@ -356,9 +360,24 @@ Semantics worth knowing:
|
|
|
356
360
|
- **No secrets** — events carry no headers and no tokens (auth travels in the
|
|
357
361
|
`Coder-Session-Token` header, which is deliberately excluded); `path`/`url`
|
|
358
362
|
never contain credentials.
|
|
359
|
-
- `
|
|
360
|
-
|
|
361
|
-
|
|
363
|
+
- `http:*` events name their operation: `op` is the public `CoderChatClient`
|
|
364
|
+
method performing the exchange (`"createChat"`, `"createChatMessage"`,
|
|
365
|
+
`"getMessages"`, `"submitToolResults"`, … — the `CoderClientOperation`
|
|
366
|
+
union), so per‑operation classification never has to reverse‑engineer
|
|
367
|
+
`path`. `method`/`path` stay for generic consumers; `archiveChat` stamps its
|
|
368
|
+
own `op` even though it issues the same `PATCH` as `updateChat`.
|
|
369
|
+
- `ws:event` fires at arrival: after a redial, chatd's replay of the
|
|
370
|
+
in‑progress episode is visible here (correlate with `reader`/`attempt`),
|
|
371
|
+
stamped with the reader's own replay verdict — `forwarded: false` exactly on
|
|
372
|
+
the duplicate deltas the reader suppresses from the turn, so subscribers
|
|
373
|
+
never re‑derive the episode filter.
|
|
374
|
+
- `forwarded` does **not** reflect snapshot dedup: repeated or revised
|
|
375
|
+
`message` snapshots are always `forwarded: true`, because reconciling them
|
|
376
|
+
is deliberately the consumer's job past the transport layer —
|
|
377
|
+
`TurnTranslator`'s per‑message ledger decides what a revision re‑emits, and
|
|
378
|
+
that disposition is not stamped on transport frames. Use `ws:event` for
|
|
379
|
+
span pairing and replay accounting; subscribers needing content fidelity
|
|
380
|
+
must consume model output (or `TurnTranslator`), not transport frames.
|
|
362
381
|
- Every `ws:*` event carries `reader` — a monotonic id for the
|
|
363
382
|
`streamChatEvents` call (reader) behind the connection, allocated from one
|
|
364
383
|
process‑wide counter so it stays unique across model and client instances.
|
|
@@ -436,6 +455,25 @@ for a few seconds, during which archiving 409s — `archive()` retries those 409
|
|
|
436
455
|
`settleRetryDelayMs`) and rethrows the last one if the chat never settles. Any
|
|
437
456
|
other failure, including your own abort, rethrows immediately.
|
|
438
457
|
|
|
458
|
+
`archive()` and `interrupt()` target the session's chat — or, after the session
|
|
459
|
+
was dropped (`resetSession()`, or the automatic discard after a fresh‑chat
|
|
460
|
+
stream failure — see [Handling errors](#handling-errors)), the **last‑known
|
|
461
|
+
chat id** (`agent.lastKnownChatId`), so a stranded chat is still cleaned up
|
|
462
|
+
instead of leaking. Generation never uses the last‑known id: a turn after a
|
|
463
|
+
drop creates a fresh chat as always. Every chat stranded by an _automatic_
|
|
464
|
+
discard is also recorded on a ledger (`agent.strandedChatIds`, oldest first) —
|
|
465
|
+
with `maxRetries` several failed attempts can strand one chat each while only
|
|
466
|
+
the final attempt's error surfaces — and one `archive()` retires them all,
|
|
467
|
+
oldest first, alongside its primary target. Both methods report what they
|
|
468
|
+
acted on instead of silently no‑oping: `archive()` resolves
|
|
469
|
+
`{ archived: true, chatId, archivedChatIds }` (each archived id is cleared as
|
|
470
|
+
a cleanup target) or `{ archived: false }` when no chat exists at all;
|
|
471
|
+
`interrupt()` resolves `{ interrupted: true, chatId }` /
|
|
472
|
+
`{ interrupted: false }` the same way. Deliberate abandonment is different:
|
|
473
|
+
`resetSession()` does **not** add to the ledger (you may want that chat kept),
|
|
474
|
+
so after a manual reset the old chat is targetable only until a new chat
|
|
475
|
+
supersedes `lastKnownChatId`.
|
|
476
|
+
|
|
439
477
|
To make cleanup ride scope exit instead of a `finally` you have to remember, the
|
|
440
478
|
agent is an **async disposable**:
|
|
441
479
|
|
|
@@ -469,7 +507,11 @@ carry structured detail you can branch on:
|
|
|
469
507
|
repeat (workspace/MCP tooling, fresh attachment uploads); otherwise it is
|
|
470
508
|
`false` — a retry would resubmit the prompt as a new user turn and could
|
|
471
509
|
duplicate those effects. A failure mid‑`stream()` surfaces on the stream,
|
|
472
|
-
outside the retry wrapper. The last transport failure is in `cause
|
|
510
|
+
outside the retry wrapper. The last transport failure is in `cause`, and
|
|
511
|
+
`chatId` names the chat the failed turn had created or attached to (absent
|
|
512
|
+
when it failed before a chat existed) — after the fresh‑chat discard the
|
|
513
|
+
chat still exists server‑side, and `archive()` keeps targeting it via
|
|
514
|
+
`agent.lastKnownChatId` ([Cleanup](#cleanup)).
|
|
473
515
|
|
|
474
516
|
```ts
|
|
475
517
|
import { CoderApiError, CoderChatError } from "@coder/ai-sdk-agent";
|
|
@@ -843,24 +885,25 @@ sequence pinpoints _where_ a turn died:
|
|
|
843
885
|
|
|
844
886
|
`CoderAgentSettings`:
|
|
845
887
|
|
|
846
|
-
| field | description
|
|
847
|
-
| --------------------------------- |
|
|
848
|
-
| `client` \| (`baseUrl` + `token`) | connection (one or the other; `baseUrl`/`token` default from `CODER_URL`/`CODER_SESSION_TOKEN`)
|
|
849
|
-
| `organizationId` | org UUID that owns the chat (required)
|
|
850
|
-
| `model` | model hint: UUID, `provider:model`, model id, or display‑name substring
|
|
851
|
-
| `instructions` | system prompt
|
|
852
|
-
| `tools` | AI SDK `ToolSet` (client‑executed)
|
|
853
|
-
| `workspaceId` | bind the chat to a Coder workspace (enables workspace‑scoped tools)
|
|
854
|
-
| `workspaceFiles` | adapter enabling `uploadToWorkspace()` (write files to the workspace FS)
|
|
855
|
-
| `mcpServerIds` | server‑side MCP servers to enable
|
|
856
|
-
| `planMode` | enable plan mode (`"plan"`)
|
|
857
|
-
| `stopWhen` | AI SDK stop condition(s); default `stepCountIs(64)`
|
|
858
|
-
| `maxRetries` | default `0` — SDK retries can duplicate server‑side turns; override with care
|
|
859
|
-
| `requestTimeoutMs` | per‑turn time budget (ms); interrupts the run and rejects (`kind: "timeout"`) instead of hanging
|
|
860
|
-
| `onTransportEvent` | observability hook for typed transport events (see [Observability](#observability))
|
|
861
|
-
| `settleDeadlineMs` | overall deadline for bounded cleanup (`archive()` 409 retries, disposal); default 15 000
|
|
862
|
-
| `settleRetryDelayMs` | pause between `archive()` retries while the chat settles; default 1000
|
|
863
|
-
| `chatId` | resume an existing chat
|
|
888
|
+
| field | description |
|
|
889
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
890
|
+
| `client` \| (`baseUrl` + `token`) | connection (one or the other; `baseUrl`/`token` default from `CODER_URL`/`CODER_SESSION_TOKEN`) |
|
|
891
|
+
| `organizationId` | org UUID that owns the chat (required) |
|
|
892
|
+
| `model` | model hint: UUID, `provider:model`, model id, or display‑name substring |
|
|
893
|
+
| `instructions` | system prompt |
|
|
894
|
+
| `tools` | AI SDK `ToolSet` (client‑executed) |
|
|
895
|
+
| `workspaceId` | bind the chat to a Coder workspace (enables workspace‑scoped tools) |
|
|
896
|
+
| `workspaceFiles` | adapter enabling `uploadToWorkspace()` (write files to the workspace FS) |
|
|
897
|
+
| `mcpServerIds` | server‑side MCP servers to enable |
|
|
898
|
+
| `planMode` | enable plan mode (`"plan"`) |
|
|
899
|
+
| `stopWhen` | AI SDK stop condition(s); default `stepCountIs(64)` |
|
|
900
|
+
| `maxRetries` | default `0` — SDK retries can duplicate server‑side turns; override with care |
|
|
901
|
+
| `requestTimeoutMs` | per‑turn time budget (ms); interrupts the run and rejects (`kind: "timeout"`) instead of hanging |
|
|
902
|
+
| `onTransportEvent` | observability hook for typed transport events (see [Observability](#observability)) |
|
|
903
|
+
| `settleDeadlineMs` | overall deadline for bounded cleanup (`archive()` 409 retries, disposal); default 15 000 |
|
|
904
|
+
| `settleRetryDelayMs` | pause between `archive()` retries while the chat settles; default 1000 |
|
|
905
|
+
| `chatId` | resume an existing chat |
|
|
906
|
+
| `lastSeenMessageId` | resume cursor for `chatId` (persist `agent.lastSeenMessageId`) — skips the resumed turn's pre‑prompt history probe |
|
|
864
907
|
|
|
865
908
|
The `model` hint resolves against the deployment's model configs in order: a
|
|
866
909
|
config UUID is used as‑is, then an exact `provider:model` match, an exact model
|
|
@@ -903,7 +946,9 @@ Two facts make the pattern work:
|
|
|
903
946
|
|
|
904
947
|
- **All chat state lives on the Coder server.** Messages, tool activity, the
|
|
905
948
|
run itself — none of it is in your process. The only durable thing a
|
|
906
|
-
workflow has to carry between steps is **`agent.chatId`: a string
|
|
949
|
+
workflow has to carry between steps is **`agent.chatId`: a string** — plus,
|
|
950
|
+
optionally, **`agent.lastSeenMessageId`: the resume cursor**, which saves the
|
|
951
|
+
resumed turn one serial round‑trip (below).
|
|
907
952
|
- **`CoderAgent` can't ride a `fetch`‑shim durability layer** (it talks REST +
|
|
908
953
|
WebSocket through its own client), so each turn runs **inside** a durable
|
|
909
954
|
step — and the checkpointed chat id is the thread between steps.
|
|
@@ -916,18 +961,29 @@ on another machine, hours apart, retried after failures.
|
|
|
916
961
|
```ts
|
|
917
962
|
import { CoderAgent, type CoderTransportEvent } from "@coder/ai-sdk-agent";
|
|
918
963
|
|
|
964
|
+
interface WorkflowCheckpoint {
|
|
965
|
+
chatId: string;
|
|
966
|
+
/** The resume cursor — pairs with chatId, never persisted without it. */
|
|
967
|
+
lastSeenMessageId?: number;
|
|
968
|
+
}
|
|
969
|
+
|
|
919
970
|
// Any durable KV your engine gives you: step state, a job row, a DB table.
|
|
920
971
|
declare const checkpoints: {
|
|
921
|
-
get(workflowId: string): Promise<
|
|
922
|
-
set(workflowId: string,
|
|
972
|
+
get(workflowId: string): Promise<WorkflowCheckpoint | undefined>;
|
|
973
|
+
set(workflowId: string, checkpoint: WorkflowCheckpoint): Promise<void>;
|
|
923
974
|
};
|
|
924
975
|
|
|
925
976
|
export async function runTurn(workflowId: string, prompt: string): Promise<string> {
|
|
977
|
+
const checkpoint = await checkpoints.get(workflowId); // undefined on the first step → the turn creates the chat
|
|
926
978
|
const agent = new CoderAgent({
|
|
927
979
|
baseUrl: process.env.CODER_URL!,
|
|
928
980
|
token: process.env.CODER_SESSION_TOKEN!, // read per step — never checkpoint or log it
|
|
929
981
|
organizationId: process.env.CODER_ORG_ID!,
|
|
930
|
-
chatId:
|
|
982
|
+
chatId: checkpoint?.chatId,
|
|
983
|
+
// The previous step's cursor lets this step skip its pre-prompt history
|
|
984
|
+
// probe; absent (first resume, older checkpoints), the turn seeds it from
|
|
985
|
+
// the chat's newest message instead — one extra GET, same result.
|
|
986
|
+
lastSeenMessageId: checkpoint?.lastSeenMessageId,
|
|
931
987
|
requestTimeoutMs: 300_000, // always bound workflow steps — see below
|
|
932
988
|
onTransportEvent: observe, // per-step telemetry — see below
|
|
933
989
|
});
|
|
@@ -946,9 +1002,19 @@ export async function runTurn(workflowId: string, prompt: string): Promise<strin
|
|
|
946
1002
|
// still persist the id, or the retried step orphans a live chat and its
|
|
947
1003
|
// partial effects. Exception: after an exhausted stream failure on a
|
|
948
1004
|
// chat this very call created, the SDK has already discarded the dead
|
|
949
|
-
// session (agent.chatId is undefined) and nothing is written —
|
|
1005
|
+
// session (agent.chatId is undefined) and nothing is written — the thrown
|
|
1006
|
+
// CoderStreamError's chatId still names the stranded chat, and
|
|
1007
|
+
// agent.archive() retires it via agent.lastKnownChatId — see the
|
|
950
1008
|
// CoderStreamError notes below for what retrying means then.
|
|
951
|
-
if (agent.chatId)
|
|
1009
|
+
if (agent.chatId) {
|
|
1010
|
+
const next: WorkflowCheckpoint = { chatId: agent.chatId };
|
|
1011
|
+
// The cursor is optional sugar: persist it when a turn established one
|
|
1012
|
+
// (a turn that failed before streaming may not have), skip it otherwise.
|
|
1013
|
+
if (agent.lastSeenMessageId !== undefined) {
|
|
1014
|
+
next.lastSeenMessageId = agent.lastSeenMessageId;
|
|
1015
|
+
}
|
|
1016
|
+
await checkpoints.set(workflowId, next);
|
|
1017
|
+
}
|
|
952
1018
|
}
|
|
953
1019
|
}
|
|
954
1020
|
```
|
|
@@ -981,12 +1047,16 @@ await archiveWorkflowChat("wf-1042");
|
|
|
981
1047
|
|
|
982
1048
|
What resuming does (and doesn't do):
|
|
983
1049
|
|
|
984
|
-
- **Construction is free.** `new CoderAgent({ …, chatId })` performs no I/O
|
|
985
|
-
|
|
986
|
-
|
|
1050
|
+
- **Construction is free.** `new CoderAgent({ …, chatId })` performs no I/O —
|
|
1051
|
+
a bad or archived id fails the _turn_, not the constructor. (The one
|
|
1052
|
+
exception is local validation of `lastSeenMessageId`: a cursor that isn't a
|
|
1053
|
+
positive integer, or arrives without its `chatId`, throws immediately.)
|
|
987
1054
|
- **The first resumed turn seeds its cursor** from the chat's newest message
|
|
988
1055
|
(one single‑message history probe), so it streams only its own events —
|
|
989
|
-
it never re‑absorbs earlier turns' content or usage.
|
|
1056
|
+
it never re‑absorbs earlier turns' content or usage. Supplying the
|
|
1057
|
+
checkpointed `lastSeenMessageId` skips that probe: the previous step already
|
|
1058
|
+
knew the cursor, so the resumed prompt goes out without any pre‑prompt
|
|
1059
|
+
round‑trip.
|
|
990
1060
|
- **Re‑supply process‑local configuration.** Client `tools` live in your
|
|
991
1061
|
process, not on the server, so each step that expects tool calls must pass
|
|
992
1062
|
them again. The workspace binding is the opposite: fixed at creation,
|
|
@@ -994,7 +1064,10 @@ What resuming does (and doesn't do):
|
|
|
994
1064
|
- **Sessions stay single‑flight across the whole system**, not just within one
|
|
995
1065
|
process: run steps strictly sequentially per chat. A second job posting to
|
|
996
1066
|
the same chat queues behind the live run and burns its own
|
|
997
|
-
`requestTimeoutMs` waiting
|
|
1067
|
+
`requestTimeoutMs` waiting — it attributes nothing of the live run's output
|
|
1068
|
+
to itself (its window opens only when its own queued prompt materializes),
|
|
1069
|
+
and if it times out first, it withdraws its queued prompt rather than
|
|
1070
|
+
leaving it to run unattended.
|
|
998
1071
|
|
|
999
1072
|
### Let the SDK absorb drops — and handle what it re‑throws
|
|
1000
1073
|
|
|
@@ -1063,12 +1136,15 @@ try {
|
|
|
1063
1136
|
the dead session so a retry — `maxRetries` or a re‑run step — starts clean
|
|
1064
1137
|
on a fresh chat. The discard happens for _every_ chat the failed turn
|
|
1065
1138
|
itself created — **including the effectful, `isRetryable: false` case**:
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1139
|
+
`agent.chatId` is `undefined` and the `finally` checkpoint writes nothing —
|
|
1140
|
+
but the stranded chat is not nameless: the error's **`chatId`** field names
|
|
1141
|
+
it, and `interrupt()`/`archive()` keep targeting it via
|
|
1142
|
+
`agent.lastKnownChatId` ([Cleanup](#cleanup)), so the step's error path can
|
|
1143
|
+
retire the orphan directly — or checkpoint `err.chatId` for the
|
|
1144
|
+
reconciliation sweep (below) when its workspace/MCP effects need
|
|
1145
|
+
reconciling first. A retried step then starts a fresh chat that has _not_
|
|
1146
|
+
seen the orphan's effects: gate that retry on the same idempotency judgment
|
|
1147
|
+
as any re‑submission. **On a resumed chat it is always `isRetryable: false`:**
|
|
1072
1148
|
re‑running the step resubmits the same prompt as a _new user turn_, with the
|
|
1073
1149
|
failed attempt's partial output still in history (usually fine — the model
|
|
1074
1150
|
sees its own aborted attempt), and any workspace/MCP tool effects that ran
|
|
@@ -1117,9 +1193,11 @@ checkpoint:
|
|
|
1117
1193
|
turns into resuming the dead chat.)
|
|
1118
1194
|
- **Crashed after the checkpoint** — the chat is resumable, but don't just
|
|
1119
1195
|
resubmit. The crashed attempt's run may still be live server‑side, and a
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1196
|
+
resubmission then queues behind it: nothing the old run commits is
|
|
1197
|
+
attributed to the resumed turn (its window opens only at its own queued
|
|
1198
|
+
prompt's materialization), but the step burns its `requestTimeoutMs`
|
|
1199
|
+
waiting for the old run to finish first — and a timeout withdraws the
|
|
1200
|
+
queued prompt again. A crash _inside one of your client tools_ is worse: the
|
|
1123
1201
|
chat is paused in `requires_action`, where new messages wait forever — and
|
|
1124
1202
|
if the tool's external effect committed before the crash, that pause is the
|
|
1125
1203
|
only pending record of it, so reconcile the tool ledger **before** any
|