@coder/ai-sdk-agent 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -311,9 +311,27 @@ tools runs several segments, so it bounds each one, not the whole call. To cap t
311
311
  await agent.generate({ prompt: "…", abortSignal: AbortSignal.timeout(120_000) });
312
312
  ```
313
313
 
314
- If the event stream drops before the turn settles, the call rejects with
315
- `CoderChatError` (`kind: "stream_closed"`, retryable) rather than returning a
316
- truncated result as if the turn had finished.
314
+ If the event stream drops mid‑turn, the agent redials it automatically with
315
+ exponential backoff, replaying the turn's events from its starting cursor and
316
+ deduplicating them on receipt — the server keeps generating during the gap, so
317
+ a transient drop costs nothing and the run is **not** interrupted. Only when the stream cannot be re‑established (several
318
+ consecutive failed attempts, ~15s) is the server run interrupted and the call
319
+ rejected with a `CoderStreamError` — an AI SDK `APICallError`. When the failed
320
+ turn had just created its chat AND had no external effects a replay would
321
+ repeat — no `workspaceId`, no `mcpServerIds`, and no freshly uploaded inline
322
+ attachments (pre‑uploaded `fileId` references are fine) — the dead session is
323
+ discarded and the error is `isRetryable: true`, so `generate()` calls with
324
+ `maxRetries` set retry the whole turn on a fresh chat automatically. Otherwise
325
+ the error is `isRetryable: false` and retrying is the caller's deliberate
326
+ decision: on a chat with prior state (resumed sessions, later turns,
327
+ tool‑result segments) a re‑invocation would resubmit the same prompt as a new
328
+ user turn, workspace/MCP tools may already have executed side effects, and
329
+ inline attachments would upload again. (For `stream()`, a mid‑stream failure
330
+ surfaces on the stream itself, outside the SDK's retry wrapper — handle it in
331
+ your consumption loop.)
332
+ A non‑transient 4xx upgrade rejection (bad/expired token,
333
+ deleted chat) fails fast with a `CoderApiError` instead of retrying; 408/425/429
334
+ consume the redial budget like any other transient failure.
317
335
 
318
336
  ## Cleanup
319
337
 
@@ -344,10 +362,20 @@ avoids accumulating live chats.
344
362
 
345
363
  ## Handling errors
346
364
 
347
- All errors extend `CoderAgentError`. Two carry structured detail you can branch on:
365
+ All errors extend `CoderAgentError`, except `CoderStreamError` (below). Two
366
+ carry structured detail you can branch on:
348
367
 
349
368
  - **`CoderApiError`** — an HTTP request failed. Fields: `status`, `method`, `path`, `detail`.
350
369
  - **`CoderChatError`** — a turn ended in an error, timed out, or lost its stream. Fields: `kind`, `retryable`, `statusCode`, `provider`.
370
+ - **`CoderStreamError`** — the event stream dropped and could not be re‑established
371
+ within its redial budget. Extends the AI SDK's `APICallError` (not
372
+ `CoderAgentError`), so `generate()`'s `maxRetries` machinery recognizes it.
373
+ `isRetryable` is `true` only when the failed turn created its chat (the dead
374
+ session is discarded, so a retry starts fresh) and had no external effects to
375
+ repeat (workspace/MCP tooling, fresh attachment uploads); otherwise it is
376
+ `false` — a retry would resubmit the prompt as a new user turn and could
377
+ duplicate those effects. A failure mid‑`stream()` surfaces on the stream,
378
+ outside the retry wrapper. The last transport failure is in `cause`.
351
379
 
352
380
  ```ts
353
381
  import { CoderApiError, CoderChatError } from "@coder/ai-sdk-agent";
@@ -371,14 +399,31 @@ retrying the whole step deliberately.
371
399
 
372
400
  ## Usage & cost
373
401
 
374
- Results carry normalized token usage in `usage`, plus the verbatim snake_case
375
- wire usage in `usage.raw` for fields the normalized shape has no slot for
376
- (`context_limit`, cost, runtime, …). When the server reports them,
377
- `total_cost_micros` (micro‑USD) and `total_runtime_ms` are also mirrored under
378
- `providerMetadata.coder` on finish. Both are **absence‑tolerant mirrors**:
379
- today's Coder servers only expose cost on the aggregate cost endpoints
380
- (`/api/experimental/chats/cost/*`), so expect these fields to be absent —
381
- nothing is emitted when the server sends neither.
402
+ Results carry normalized token usage in `usage`. A chatd turn runs several
403
+ model steps server‑side (one per server tool round), each reporting its own
404
+ usage — the SDK **sums every step**, and the AI SDK adds up the steps of a
405
+ turn that paused for client tools, so `result.usage` reflects what the whole
406
+ turn actually consumed. `inputTokens` is the full prompt size: Coder
407
+ normalizes the wire `input_tokens` to the _uncached_ count (cache reads/writes
408
+ are separate fields), so the SDK adds them back into the total and exposes the
409
+ split via `inputTokenDetails` (`noCacheTokens`, `cacheReadTokens`,
410
+ `cacheWriteTokens`).
411
+
412
+ The snake_case wire usage lives **per step** at `result.steps[i].usage.raw`
413
+ (the AI SDK does not carry `raw` onto the summed `result.usage`) for fields
414
+ the normalized shape has no slot for (`context_limit`, cost, runtime, and any
415
+ newer wire fields, which pass through newest‑value‑wins). `raw` keeps the wire
416
+ convention (`input_tokens` = uncached only) with counters summed over that
417
+ step's server‑side model steps and `context_limit` from the newest one — so
418
+ don't divide `raw`'s summed counters by `context_limit` to estimate context
419
+ fullness; they are turn consumption, not a prompt‑size snapshot. When the
420
+ server reports them, `total_cost_micros` (micro‑USD) and `total_runtime_ms`
421
+ are mirrored the same way under each step's `providerMetadata.coder`
422
+ (`result.providerMetadata` reflects only the final step — sum
423
+ `result.steps[*].providerMetadata.coder` for whole‑turn cost when client tools
424
+ ran). Both are **absence‑tolerant mirrors**: on servers that don't send them
425
+ (cost is otherwise only on the aggregate cost endpoints,
426
+ `/api/experimental/chats/cost/*`), nothing is emitted.
382
427
 
383
428
  Forward usage to a UI via message metadata:
384
429
 
@@ -596,7 +641,7 @@ what's available.
596
641
  ```
597
642
  CoderAgent (implements ai.Agent)
598
643
  └─ ToolLoopAgent (ai) ← inherits generate()/stream(), loop control
599
- └─ CoderLanguageModel ← implements @ai-sdk/provider LanguageModelV3
644
+ └─ CoderLanguageModel ← implements @ai-sdk/provider LanguageModelV4
600
645
  └─ CoderChatClient ← REST + WebSocket to /api/experimental/chats
601
646
  └─ Coder Agents ← runs the agent loop SERVER-side
602
647
  ```
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { FilePart, ToolSet, Agent, SystemModelMessage, ToolLoopAgentSettings, ToolChoice, StopCondition, ToolLoopAgent, UIMessage } from 'ai';
2
- import { SharedV4FileData, LanguageModelV4, LanguageModelV4CallOptions, LanguageModelV4StreamResult, LanguageModelV4GenerateResult, LanguageModelV4StreamPart, LanguageModelV4Message, LanguageModelV4Prompt } from '@ai-sdk/provider';
2
+ import { SharedV4FileData, LanguageModelV4, LanguageModelV4CallOptions, LanguageModelV4StreamResult, LanguageModelV4GenerateResult, LanguageModelV4StreamPart, LanguageModelV4Message, LanguageModelV4Prompt, APICallError } from '@ai-sdk/provider';
3
3
 
4
4
  /**
5
5
  * Bytes to upload, in any convenient shape.
@@ -365,6 +365,29 @@ interface StreamChatEventsOptions {
365
365
  * The chat stream is a live subscription that stays open after a turn settles,
366
366
  * so callers should `break` out of the loop once they observe a terminal status
367
367
  * — that triggers generator cleanup, which closes the socket.
368
+ *
369
+ * Dropped connections are redialed with exponential backoff (like
370
+ * {@link watchChatEvents}), replaying from the turn's ORIGINAL `after_id`. The
371
+ * cursor is deliberately NOT advanced past messages already yielded: chatd's
372
+ * initial sync filters out ids at or below `after_id`, so an advanced cursor
373
+ * would silently lose same-id REVISION snapshots (updated content, tool
374
+ * results, usage) committed while the connection was down. Consumers must
375
+ * dedupe repeated `message` snapshots — the wire contract demands that anyway,
376
+ * since chatd re-sends full snapshots on revision bumps even without a
377
+ * reconnect (TurnTranslator does). On reconnect chatd also replays the
378
+ * in-progress generation attempt's `message_part` deltas from `seq` 1, so
379
+ * parts of an episode
380
+ * (`history_version`, `generation_attempt`) at or below the last yielded `seq`
381
+ * are suppressed here rather than double-yielded. The iteration ends in five
382
+ * ways: `signal` aborting (clean return), a non-transient 4xx upgrade
383
+ * rejection (terminal {@link CoderApiError} — bad/expired token or a deleted
384
+ * chat cannot succeed on retry, while 408/425/429 consume the redial budget),
385
+ * an unparseable frame (terminal {@link CoderAgentError} — a redial would
386
+ * replay the same frame forever), a drop while deltas WITHOUT episode
387
+ * coordinates are outstanding (older chatd; a {@link CoderStreamError},
388
+ * because a delta replay could not be deduped and would double-emit — a NEW
389
+ * committed `message` finalizes them into dedupable snapshots and re-enables
390
+ * redial), or the redial budget running out (also a {@link CoderStreamError}).
368
391
  */
369
392
  declare function streamChatEvents(options: StreamChatEventsOptions): AsyncGenerator<ChatStreamEvent, void, void>;
370
393
  interface WatchChatEventsOptions {
@@ -518,7 +541,9 @@ type PreviewShareLevel = Exclude<PortShareLevel, "owner">;
518
541
  * binaries, oversized files, or anything outside the narrow chat-attachment
519
542
  * allowlist. It is intentionally NOT bundled into the agent package: the agent
520
543
  * core stays dependency-free, and whoever already holds a workspace connection
521
- * (e.g. a `@coder/ai-sdk-eve-sandbox` session) supplies a few-line adapter.
544
+ * (e.g. a `CoderWorkspaceSession` from `@coder/ai-sdk-sandbox`) supplies a
545
+ * few-line adapter. Note the session does not report the workspace UUID —
546
+ * retain it from `ensureCoderWorkspace(...).id` (same package).
522
547
  *
523
548
  * The store does one thing — write bytes to a path. It deliberately does not
524
549
  * unpack archives or mutate the working tree: instruct the agent to `unzip`/
@@ -526,8 +551,10 @@ type PreviewShareLevel = Exclude<PortShareLevel, "owner">;
526
551
  *
527
552
  * @example
528
553
  * ```ts
554
+ * // UUID via `ensureCoderWorkspace`; bytes over a `CoderWorkspaceSession`.
555
+ * const ws = await ensureCoderWorkspace({ workspace: "my-ws" });
529
556
  * const store: WorkspaceFileStore = {
530
- * workspaceId: session.workspaceId,
557
+ * workspaceId: ws.id!, // workspace UUID (reported by the CLI/native transports)
531
558
  * write: async ({ content, path, signal }) => {
532
559
  * await session.writeFile({ path, content: toStream(content), abortSignal: signal });
533
560
  * return { path };
@@ -595,7 +622,7 @@ interface CoderAgentSettings<TOOLS extends ToolSet = {}> {
595
622
  /**
596
623
  * Adapter for writing files onto a workspace filesystem, enabling
597
624
  * {@link CoderAgent.uploadToWorkspace}. Supply one backed by a workspace
598
- * connection (e.g. a `@coder/ai-sdk-eve-sandbox` session).
625
+ * connection (e.g. a `CoderWorkspaceSession` from `@coder/ai-sdk-sandbox`).
599
626
  */
600
627
  workspaceFiles?: WorkspaceFileStore;
601
628
  /** chatd-side MCP servers to enable for this chat. */
@@ -883,6 +910,7 @@ declare class TurnTranslator {
883
910
  #private;
884
911
  constructor(opts: {
885
912
  dynamicToolNames: ReadonlySet<string>;
913
+ turnCursor?: number;
886
914
  });
887
915
  get terminalStatus(): ChatStatus | undefined;
888
916
  /** Whether a client (custom) tool call has been emitted this turn. */
@@ -1014,6 +1042,29 @@ declare class CoderApiError extends CoderAgentError {
1014
1042
  detail?: string;
1015
1043
  });
1016
1044
  }
1045
+ /**
1046
+ * The per-chat event stream dropped and could not be re-established within its
1047
+ * redial budget (or could not be safely resumed — see `streamChatEvents` on
1048
+ * servers without delta coordinates). Extends the AI SDK's {@link APICallError} with
1049
+ * `isRetryable: true` because the `ai` package's retry machinery (`maxRetries`)
1050
+ * only honors `APICallError`/`GatewayError`-branded errors — plain `Error`
1051
+ * subclasses are never retried no matter what they claim. Note the retry
1052
+ * wrapper only sees errors that reject the model call itself (`generate()`);
1053
+ * a failure mid-`stream()` surfaces on the stream, past that wrapper. The last
1054
+ * underlying transport failure is preserved as `cause`.
1055
+ *
1056
+ * NOTE: this is deliberately NOT a {@link CoderAgentError} (single
1057
+ * inheritance); match it with `APICallError.isInstance(err)` or `err.name`.
1058
+ */
1059
+ declare class CoderStreamError extends APICallError {
1060
+ name: string;
1061
+ constructor(args: {
1062
+ message: string;
1063
+ url: string;
1064
+ cause?: unknown;
1065
+ isRetryable?: boolean;
1066
+ });
1067
+ }
1017
1068
  /** A chat generation ended in an error status. */
1018
1069
  declare class CoderChatError extends CoderAgentError {
1019
1070
  name: string;
@@ -1024,4 +1075,4 @@ declare class CoderChatError extends CoderAgentError {
1024
1075
  constructor(payload: ChatErrorPayload);
1025
1076
  }
1026
1077
 
1027
- export { CHAT_ATTACHMENT_MEDIA_TYPES, CODER_PROVIDER_OPTIONS, type Chat, type ChatAttachment, type ChatAttachmentMediaType, type ChatBusyBehavior, type ChatClientType, type ChatErrorPayload, type ChatFileInput, type ChatInputPart, type ChatInputPartType, type ChatMessage, type ChatMessagePart, type ChatMessagePartType, type ChatMessageRole, type ChatMessageUsage, type ChatMessagesResponse, type ChatMessagesToUIMessagesOptions, type ChatModelConfig, type ChatPlanMode, type ChatQueuedMessage, type ChatStatus, type ChatStreamActionRequired, type ChatStreamEvent, type ChatStreamEventType, type ChatStreamMessagePart, type ChatStreamRetry, type ChatStreamStatus, type ChatStreamToolCall, type ChatWatchEvent, type ChatWatchEventKind, CoderAgent, CoderAgentError, type CoderAgentSettings, CoderApiError, CoderChatClient, type CoderChatClientOptions, CoderChatError, type CoderFileProviderOptions, CoderLanguageModel, type CoderLanguageModelConfig, type CreateChatMessageRequest, type CreateChatMessageResponse, type CreateChatRequest, type DynamicTool, type FileContent, type FilePartUploader, MAX_CHAT_FILE_SIZE_BYTES, type PreviewShareLevel, type ResolvedFile, type SharePreviewOptions, type SharedWorkspacePreview, type SubmitToolResultsRequest, TERMINAL_STATUSES, type ToolResult, type TurnAction, TurnTranslator, type UpdateChatRequest, type UploadChatFileResponse, type UploadedChatFile, type UserContent, type WatchChatEventsOptions, type WebSocketFactory, type WebSocketLike, type WorkspaceFileStore, type WorkspacePlacement, type WorkspacePreview, type WorkspacePreviewOptions, chatMessagesToUIMessages, classifyTurnAction, dataContentToFileContent, dynamicToolNames, extractSystemPrompt, resolveFileContent, streamChatEvents, toolsToDynamicTools, userContentToInputParts, watchChatEvents };
1078
+ export { CHAT_ATTACHMENT_MEDIA_TYPES, CODER_PROVIDER_OPTIONS, type Chat, type ChatAttachment, type ChatAttachmentMediaType, type ChatBusyBehavior, type ChatClientType, type ChatErrorPayload, type ChatFileInput, type ChatInputPart, type ChatInputPartType, type ChatMessage, type ChatMessagePart, type ChatMessagePartType, type ChatMessageRole, type ChatMessageUsage, type ChatMessagesResponse, type ChatMessagesToUIMessagesOptions, type ChatModelConfig, type ChatPlanMode, type ChatQueuedMessage, type ChatStatus, type ChatStreamActionRequired, type ChatStreamEvent, type ChatStreamEventType, type ChatStreamMessagePart, type ChatStreamRetry, type ChatStreamStatus, type ChatStreamToolCall, type ChatWatchEvent, type ChatWatchEventKind, CoderAgent, CoderAgentError, type CoderAgentSettings, CoderApiError, CoderChatClient, type CoderChatClientOptions, CoderChatError, type CoderFileProviderOptions, CoderLanguageModel, type CoderLanguageModelConfig, CoderStreamError, type CreateChatMessageRequest, type CreateChatMessageResponse, type CreateChatRequest, type DynamicTool, type FileContent, type FilePartUploader, MAX_CHAT_FILE_SIZE_BYTES, type PreviewShareLevel, type ResolvedFile, type SharePreviewOptions, type SharedWorkspacePreview, type SubmitToolResultsRequest, TERMINAL_STATUSES, type ToolResult, type TurnAction, TurnTranslator, type UpdateChatRequest, type UploadChatFileResponse, type UploadedChatFile, type UserContent, type WatchChatEventsOptions, type WebSocketFactory, type WebSocketLike, type WorkspaceFileStore, type WorkspacePlacement, type WorkspacePreview, type WorkspacePreviewOptions, chatMessagesToUIMessages, classifyTurnAction, dataContentToFileContent, dynamicToolNames, extractSystemPrompt, resolveFileContent, streamChatEvents, toolsToDynamicTools, userContentToInputParts, watchChatEvents };