@coder/ai-sdk-agent 0.2.0 → 0.3.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 +280 -14
- package/dist/index.d.ts +249 -16
- package/dist/index.js +672 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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@^6 zod
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Requires Node ≥ 20 and `ai` v6
|
|
49
|
+
Requires Node ≥ 20 and `ai` v6 — 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
|
|
|
@@ -96,6 +98,7 @@ pnpm example:stream # streaming via textStream
|
|
|
96
98
|
pnpm example:tool # custom (client-executed) tool round-trip
|
|
97
99
|
pnpm example:multi-turn # multi-turn session memory
|
|
98
100
|
pnpm example:file # attach a file to a chat (optional: pass a path)
|
|
101
|
+
pnpm example:structured # typed structured output via the structured_output tool
|
|
99
102
|
```
|
|
100
103
|
|
|
101
104
|
Each example creates a new chat and archives it when done — it never touches workspaces. See
|
|
@@ -113,6 +116,12 @@ AI SDK tool loop — your `execute` runs in your process.
|
|
|
113
116
|
appear in the transcript as `providerExecuted` tool calls/results — you observe them, you
|
|
114
117
|
don't execute them.
|
|
115
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
|
+
|
|
116
125
|
## Files
|
|
117
126
|
|
|
118
127
|
There are two distinct ways to get a file to the agent, depending on whether the model should
|
|
@@ -202,13 +211,81 @@ created on the first turn and reused for subsequent `generate()`/`stream()` call
|
|
|
202
211
|
conversation with server‑side history). `agent.chatId` is the current chat id.
|
|
203
212
|
|
|
204
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).
|
|
205
|
-
- `agent.interrupt()` — interrupt an in‑flight generation.
|
|
206
|
-
- `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)).
|
|
207
216
|
- `agent.listModels()` — list the deployment's model configs, so you don't have to guess the `model` hint.
|
|
208
217
|
- Resume a prior chat: `new CoderAgent({ …, chatId: "…" })`.
|
|
209
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
|
+
|
|
210
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)).
|
|
211
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
|
+
|
|
212
289
|
## Timeouts & cancellation
|
|
213
290
|
|
|
214
291
|
Pass an `abortSignal` to `generate()`/`stream()` to cancel a turn. Aborting
|
|
@@ -241,17 +318,28 @@ truncated result as if the turn had finished.
|
|
|
241
318
|
## Cleanup
|
|
242
319
|
|
|
243
320
|
`archive()` soft‑hides the chat (it stays in listings as `archived: true`; there
|
|
244
|
-
is no hard delete yet).
|
|
245
|
-
|
|
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**:
|
|
246
329
|
|
|
247
330
|
```ts
|
|
248
331
|
await using agent = new CoderAgent({
|
|
249
332
|
/* … */
|
|
250
333
|
});
|
|
251
334
|
const { text } = await agent.generate({ prompt: "…" });
|
|
252
|
-
// agent.archive()
|
|
335
|
+
// agent.interrupt() + agent.archive() run automatically when the scope exits.
|
|
253
336
|
```
|
|
254
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
|
+
|
|
255
343
|
In a request handler that returns before a fire‑and‑forget `archive()` settles, the
|
|
256
344
|
archive can be abandoned — `await using` (or an awaited `archive()` in `finally`)
|
|
257
345
|
avoids accumulating live chats.
|
|
@@ -283,15 +371,183 @@ try {
|
|
|
283
371
|
SDK‑level retry could duplicate a turn. Prefer catching `retryable` errors and
|
|
284
372
|
retrying the whole step deliberately.
|
|
285
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
|
+
|
|
286
404
|
## Structured output
|
|
287
405
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
request emits a warning and is best‑effort at most.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
provider
|
|
406
|
+
Coder Agents has no server‑side `response_format`, so `CoderAgent` cannot
|
|
407
|
+
constrain what the model **says** to a JSON schema — a `responseFormat` /
|
|
408
|
+
`experimental_output` request emits a warning and is best‑effort at most. Pick
|
|
409
|
+
by what the step needs:
|
|
410
|
+
|
|
411
|
+
- **Pure text‑in / JSON‑out, no server‑side tools** → use
|
|
412
|
+
**[`@coder/ai-sdk-provider`](../provider)** with `generateObject` /
|
|
413
|
+
`Output.object` (schema‑constrained; requires AI Gateway on the deployment).
|
|
414
|
+
- **The answer must come out of an agent run** (server‑side tools, MCP, a
|
|
415
|
+
workspace) → use the **`structured_output` tool pattern** below. What the
|
|
416
|
+
model _says_ isn't schema‑constrained, but what it passes **into a tool** is
|
|
417
|
+
typed — so have it submit its answer by _calling a tool_ whose `inputSchema`
|
|
418
|
+
is your Zod schema. The answer arrives as the tool call's typed `input`; no
|
|
419
|
+
fishing JSON out of prose.
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import { stepCountIs, tool } from "ai";
|
|
423
|
+
import { z } from "zod";
|
|
424
|
+
|
|
425
|
+
const Answer = z.object({ severity: z.enum(["critical", "major", "minor"]), summary: z.string() });
|
|
426
|
+
|
|
427
|
+
const agent = new CoderAgent({
|
|
428
|
+
/* … */
|
|
429
|
+
instructions: "… Submit your final answer by calling the structured_output tool exactly once.",
|
|
430
|
+
tools: {
|
|
431
|
+
structured_output: tool({
|
|
432
|
+
description:
|
|
433
|
+
"Submit your final structured answer as JSON. Call this exactly once, when your work is complete.",
|
|
434
|
+
inputSchema: Answer, // your schema IS the tool's input schema
|
|
435
|
+
// Ack instead of stopping the turn: the model finishes naturally and can
|
|
436
|
+
// wind down anything it still has running (dev servers, watchers, …).
|
|
437
|
+
execute: async () =>
|
|
438
|
+
"Output received. Wind down and end your turn. Do not call structured_output again.",
|
|
439
|
+
}),
|
|
440
|
+
},
|
|
441
|
+
stopWhen: stepCountIs(6), // happy path is 2 steps: file + ack, wind down
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const result = await agent.generate({ prompt: "…" });
|
|
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
|
|
448
|
+
.flatMap((s) => s.toolCalls)
|
|
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)");
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Rules that keep it robust — each guards against a failure mode observed live:
|
|
463
|
+
|
|
464
|
+
1. **Don't force `toolChoice`, don't stop on the call.** `toolChoice` is
|
|
465
|
+
construction‑time and applies to _every_ segment, so after the ack it would
|
|
466
|
+
force the tool again and again up to the step ceiling (and it blocks any
|
|
467
|
+
other tools the step needs). A `hasToolCall` stop is worse: the server only
|
|
468
|
+
receives a client tool result as a side effect of the _next_ loop segment,
|
|
469
|
+
so ending the loop on the call strands the chat in `requires_action` —
|
|
470
|
+
follow‑up messages queue forever and `archive()` 409s. Instructions plus the
|
|
471
|
+
tool's own description are enough; models file unprompted most of the time.
|
|
472
|
+
2. **Validate client‑side.** The schema is not enforced server‑side —
|
|
473
|
+
`schema.safeParse` on the tool input is the real gate. (Schema‑invalid calls
|
|
474
|
+
that the AI SDK catches in‑loop are automatically answered with a
|
|
475
|
+
`tool-error` result the model retries against.)
|
|
476
|
+
3. **Nudge at most once, and only an idle chat.** If the turn ends in prose
|
|
477
|
+
(`finishReason: "stop"`) without a valid call, send one typed re‑prompt
|
|
478
|
+
("Call the structured_output tool now …"), then fail into your normal error
|
|
479
|
+
handling. Never re‑prompt a chat that isn't idle — the message would queue
|
|
480
|
+
behind whatever the server is still doing.
|
|
481
|
+
4. **Settle a turn that stopped on a tool call.** If the loop stops on a
|
|
482
|
+
tool‑call step — e.g. your `stopWhen` ceiling lands exactly on the
|
|
483
|
+
`structured_output` call (`finishReason: "tool-calls"`) — the tool results
|
|
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.
|
|
501
|
+
|
|
502
|
+
[`examples/06-structured-output.ts`](./examples/06-structured-output.ts) packages
|
|
503
|
+
all four rules into a small copyable helper — `structuredOutput(schema)` returns
|
|
504
|
+
`agentOpts` to spread into the constructor plus a typed `ask(agent, prompt)`
|
|
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.
|
|
295
551
|
|
|
296
552
|
## Workspaces & quota
|
|
297
553
|
|
|
@@ -325,8 +581,18 @@ running many agents at once:
|
|
|
325
581
|
| `stopWhen` | AI SDK stop condition(s); default `stepCountIs(64)` |
|
|
326
582
|
| `maxRetries` | default `0` — SDK retries can duplicate server‑side turns; override with care |
|
|
327
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 |
|
|
328
586
|
| `chatId` | resume an existing chat |
|
|
329
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
|
+
|
|
330
596
|
## How it works
|
|
331
597
|
|
|
332
598
|
```
|