ai 7.0.159 → 7.0.161
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/CHANGELOG.md +27 -0
- package/dist/index.d.ts +22 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +123 -67
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +33 -14
- package/dist/internal/index.js.map +1 -1
- package/docs/03-agents/04-loop-control.mdx +29 -1
- package/docs/03-ai-sdk-core/60-telemetry.mdx +32 -1
- package/docs/03-ai-sdk-harnesses/02-harness-agent.mdx +55 -0
- package/docs/07-reference/01-ai-sdk-core/01-generate-text.mdx +1 -1
- package/docs/07-reference/01-ai-sdk-core/02-stream-text.mdx +1 -1
- package/docs/07-reference/01-ai-sdk-core/16-tool-loop-agent.mdx +2 -2
- package/package.json +11 -11
- package/src/generate-text/generate-text.ts +9 -1
- package/src/generate-text/generated-file.ts +7 -4
- package/src/generate-text/smooth-stream.ts +1 -1
- package/src/generate-text/steering.ts +56 -3
- package/src/generate-text/stream-text.ts +9 -1
- package/src/prompt/clone-model-message.ts +53 -17
- package/src/prompt/content-part.ts +4 -2
- package/src/prompt/data-content.ts +10 -8
- package/src/prompt/file-part-data.ts +9 -5
- package/src/ui/process-ui-message-stream.ts +4 -2
|
@@ -12,7 +12,7 @@ You can control both the execution flow and the settings at each step of the age
|
|
|
12
12
|
- A tool call needs approval, or
|
|
13
13
|
- A stop condition is met
|
|
14
14
|
|
|
15
|
-
A [steering message](#steering) that is waiting when the model finishes its answer keeps the loop going for another step.
|
|
15
|
+
A [steering message](#steering) that is waiting when the model finishes its answer keeps the loop going for another step. So does a [queued message](#queueing-messages), unless the answer was cut off.
|
|
16
16
|
|
|
17
17
|
The AI SDK provides built-in loop control through two parameters: `stopWhen` for defining stopping conditions and `prepareStep` for modifying settings (model, tools, messages, and more) between steps.
|
|
18
18
|
|
|
@@ -477,6 +477,34 @@ prepareStep: ({ initialMessages, steps, experimental_steeringMessages }) => ({
|
|
|
477
477
|
}),
|
|
478
478
|
```
|
|
479
479
|
|
|
480
|
+
### Queueing Messages
|
|
481
|
+
|
|
482
|
+
`steer()` changes what the agent is doing right now. Use `queue()` for a follow-up that should wait until the agent has finished its current task:
|
|
483
|
+
|
|
484
|
+
```ts
|
|
485
|
+
const steering = experimental_createSteering();
|
|
486
|
+
|
|
487
|
+
const resultPromise = agent.generate({
|
|
488
|
+
prompt: 'Refactor the auth module.',
|
|
489
|
+
experimental_steering: steering,
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// while the agent is working:
|
|
493
|
+
steering.queue('Then add tests for it.');
|
|
494
|
+
steering.queue('Then update the readme.');
|
|
495
|
+
|
|
496
|
+
const result = await resultPromise;
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
A queued message is added once the model finishes its current task, i.e. when it gives a final answer without tool calls. The loop then continues with the message, so the agent works through the queue one task at a time:
|
|
500
|
+
|
|
501
|
+
- Queued messages are added one per task, in the order they are queued.
|
|
502
|
+
- Waiting steering messages are added first. A queued message waits for the next task that finishes.
|
|
503
|
+
- Messages that are queued before the loop starts wait until the model has answered the prompt.
|
|
504
|
+
- A response that is cut off (finish reason `length` or `content-filter`) does not finish the task, so the loop ends and the queued message is reported as not delivered.
|
|
505
|
+
- Stop conditions still apply, and a queued message that the loop does not reach is reported as not delivered, so you can send it as a new message instead.
|
|
506
|
+
- Queued messages are saved in `experimental_steeringMessages` like steering messages, and `queue()` resolves with the same outcome as `steer()`.
|
|
507
|
+
|
|
480
508
|
### Handling the Outcome
|
|
481
509
|
|
|
482
510
|
`steer()` resolves with the outcome of the message and never rejects:
|
|
@@ -16,7 +16,7 @@ to see providers that offer monitoring and tracing for AI SDK applications.
|
|
|
16
16
|
|
|
17
17
|
### Step 1: Register the OpenTelemetry integration
|
|
18
18
|
|
|
19
|
-
OpenTelemetry
|
|
19
|
+
OpenTelemetry collection requires the `@ai-sdk/otel` package. Install it and register the integration once at application startup:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
pnpm install @ai-sdk/otel
|
|
@@ -513,6 +513,37 @@ The `LegacyOpenTelemetry` integration emits legacy AI SDK-specific spans.
|
|
|
513
513
|
The `OpenTelemetry` emits spans that follow the [OpenTelemetry Semantic Conventions for GenAI](https://opentelemetry.io/docs/specs/semconv/gen-ai/).
|
|
514
514
|
By default, attributes use the `gen_ai.*` prefix. Provider names are mapped to well-known values (e.g. `openai`, `anthropic`, `gcp.vertex_ai`).
|
|
515
515
|
|
|
516
|
+
#### Inference duration metric
|
|
517
|
+
|
|
518
|
+
For `generateText` and `streamText`, `OpenTelemetry` also records the
|
|
519
|
+
[`gen_ai.client.inference.duration`](https://github.com/open-telemetry/semantic-conventions-genai/blob/3fe84604a581b3d50dd5b4bd29383b462d2a5018/docs/gen-ai/client-inference.md#metric-gen_aiclientinferenceduration)
|
|
520
|
+
histogram in seconds, following the development GenAI semantic conventions.
|
|
521
|
+
Configure a global OpenTelemetry `MeterProvider` with a metric reader and exporter
|
|
522
|
+
to collect it. A `TracerProvider` alone does not export metrics.
|
|
523
|
+
|
|
524
|
+
Each language model call contributes one observation, including calls that fail or
|
|
525
|
+
are cancelled. Streaming calls record the full response duration. In a workflow
|
|
526
|
+
with multiple model calls, each call is recorded separately; local tool execution
|
|
527
|
+
time is excluded. Each attempt of a retried model stream contributes its own
|
|
528
|
+
observation, including failed attempts that close without a finish chunk.
|
|
529
|
+
|
|
530
|
+
The histogram includes these attributes:
|
|
531
|
+
|
|
532
|
+
- `gen_ai.operation.name`: `"chat"`
|
|
533
|
+
- `gen_ai.provider.name`: the normalized provider name
|
|
534
|
+
- `gen_ai.request.model`: the requested model ID
|
|
535
|
+
- `gen_ai.response.model`: the response model ID, when available
|
|
536
|
+
- `error.type`: the error name for thrown errors, `"AbortError"` for cancellation,
|
|
537
|
+
or `"_OTHER"` when a call ends with an error finish reason or an unknown error
|
|
538
|
+
|
|
539
|
+
The default bucket boundary advice is `[0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64,
|
|
540
|
+
1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92]` seconds. Your OpenTelemetry SDK
|
|
541
|
+
configuration can override aggregation and bucket boundaries.
|
|
542
|
+
|
|
543
|
+
Setting `telemetry.isEnabled` to `false` disables these metrics for a call.
|
|
544
|
+
Setting `recordInputs` or `recordOutputs` to `false` does not disable duration
|
|
545
|
+
metrics. The `LegacyOpenTelemetry` integration does not emit this histogram.
|
|
546
|
+
|
|
516
547
|
#### generateText / streamText
|
|
517
548
|
|
|
518
549
|
For `generateText` and `streamText`, the integration records 3 types of spans:
|
|
@@ -579,6 +579,61 @@ to `'.'` to use the sandbox's default working directory itself. When omitted,
|
|
|
579
579
|
regular sessions use the default `<harnessId>-<sessionId>` directory, while
|
|
580
580
|
`onBootstrap` receives the sandbox's default working directory.
|
|
581
581
|
|
|
582
|
+
When the harness declares a sandbox bootstrap recipe, as Claude Code and Codex
|
|
583
|
+
do, `createSession()` runs the session's sandbox setup before the harness
|
|
584
|
+
starts: it runs `onBootstrap` when its marker is missing, creates the session
|
|
585
|
+
working directory, and calls `onSession`. For any other harness, such as Pi,
|
|
586
|
+
the setup runs on the first file, exec, or spawn operation that the harness or
|
|
587
|
+
a host tool performs on the sandbox. A session whose turns never touch the
|
|
588
|
+
sandbox then makes no setup calls. If the setup fails, the operation that
|
|
589
|
+
triggered it rejects, and the next operation runs the setup again. A sandbox
|
|
590
|
+
session without `defaultWorkingDirectory` still answers one `pwd` in
|
|
591
|
+
`createSession()`, because the session working directory is derived from it.
|
|
592
|
+
When the sandbox comes from the `sandbox` provider setting instead of
|
|
593
|
+
`sandboxSession`, the provider runs `onBootstrap` when it creates a new
|
|
594
|
+
sandbox, which happens in `createSession()`.
|
|
595
|
+
|
|
596
|
+
## Start the Sandbox on First Use
|
|
597
|
+
|
|
598
|
+
Deferring the setup does not defer the sandbox itself: a session you pass to
|
|
599
|
+
`createSession()` already exists. To defer the sandbox as well, pass
|
|
600
|
+
`defaultWorkingDirectory` and a `createSandboxSession` function instead. With
|
|
601
|
+
a harness that has no sandbox bootstrap recipe, such as Pi, `createSession()`
|
|
602
|
+
returns without calling `createSandboxSession`, and the first file, exec, or
|
|
603
|
+
spawn operation calls it, so a turn that only chats never starts the sandbox.
|
|
604
|
+
For any other harness, `createSession()` calls it right away.
|
|
605
|
+
|
|
606
|
+
```ts
|
|
607
|
+
import { HarnessAgent } from '@ai-sdk/harness/agent';
|
|
608
|
+
import { pi } from '@ai-sdk/harness-pi';
|
|
609
|
+
import { createVercelNetworkSandboxSessionFromNativeSandbox } from '@ai-sdk/sandbox-vercel';
|
|
610
|
+
import { Sandbox } from '@vercel/sandbox';
|
|
611
|
+
|
|
612
|
+
const agent = new HarnessAgent({ harness: pi });
|
|
613
|
+
|
|
614
|
+
const session = await agent.createSession({
|
|
615
|
+
sessionId: chatId,
|
|
616
|
+
resumeFrom: resumeState,
|
|
617
|
+
sandboxSession: {
|
|
618
|
+
defaultWorkingDirectory: '/vercel/sandbox',
|
|
619
|
+
createSandboxSession: async () =>
|
|
620
|
+
createVercelNetworkSandboxSessionFromNativeSandbox(
|
|
621
|
+
await Sandbox.getOrCreate({
|
|
622
|
+
name: `chat-${chatId}`,
|
|
623
|
+
runtime: 'node24',
|
|
624
|
+
}),
|
|
625
|
+
),
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
The harness needs the working directory before the sandbox exists, so
|
|
631
|
+
`defaultWorkingDirectory` must match the one the sandbox reports. Because the
|
|
632
|
+
sandbox is looked up by name, the same call works for the first turn and for
|
|
633
|
+
every later one. A failed `createSandboxSession` or setup is retried on the
|
|
634
|
+
next operation. You own the sandbox lifecycle, as with any session you pass to
|
|
635
|
+
`createSession()`.
|
|
636
|
+
|
|
582
637
|
## Prepare Reusable Sandboxes
|
|
583
638
|
|
|
584
639
|
Use `agent.getSandboxTemplate()` to resolve one harness's bootstrap recipe and
|
|
@@ -857,7 +857,7 @@ To see `generateText` in action, check out [these examples](#examples).
|
|
|
857
857
|
type: 'Experimental_Steering',
|
|
858
858
|
isOptional: true,
|
|
859
859
|
description:
|
|
860
|
-
"Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()`
|
|
860
|
+
"Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. Messages sent with `steering.queue()` wait until the model finishes its current task and are added one per task. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` and `queue()` resolve with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
|
|
861
861
|
},
|
|
862
862
|
{
|
|
863
863
|
name: 'experimental_download',
|
|
@@ -914,7 +914,7 @@ retain the content their contracts require.
|
|
|
914
914
|
type: 'Experimental_Steering',
|
|
915
915
|
isOptional: true,
|
|
916
916
|
description:
|
|
917
|
-
"Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()`
|
|
917
|
+
"Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. Messages sent with `steering.queue()` wait until the model finishes its current task and are added one per task. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` and `queue()` resolve with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
|
|
918
918
|
},
|
|
919
919
|
{
|
|
920
920
|
name: 'experimental_download',
|
|
@@ -768,7 +768,7 @@ const result = await agent.generate({
|
|
|
768
768
|
type: 'Experimental_Steering',
|
|
769
769
|
isOptional: true,
|
|
770
770
|
description:
|
|
771
|
-
"Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()`
|
|
771
|
+
"Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. Messages sent with `steering.queue()` wait until the model finishes its current task and are added one per task. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` and `queue()` resolve with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
|
|
772
772
|
},
|
|
773
773
|
{
|
|
774
774
|
name: 'options',
|
|
@@ -891,7 +891,7 @@ for await (const chunk of stream.textStream) {
|
|
|
891
891
|
type: 'Experimental_Steering',
|
|
892
892
|
isOptional: true,
|
|
893
893
|
description:
|
|
894
|
-
"Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()`
|
|
894
|
+
"Experimental steering object created with `experimental_createSteering()`. Messages sent with `steering.steer()` are added as user messages at the next step boundary, after the current step and its tool calls are done. Messages sent with `steering.queue()` wait until the model finishes its current task and are added one per task. A waiting message keeps the loop going for another step unless a stop condition is met. `steer()` and `queue()` resolve with `{ type: 'delivered', id, message, stepNumber }` or `{ type: 'not-delivered', id, message, reason }`. See [Steering](/docs/agents/loop-control#steering).",
|
|
895
895
|
},
|
|
896
896
|
{
|
|
897
897
|
name: 'options',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.161",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -42,20 +42,20 @@
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@ai-sdk/gateway": "4.0.
|
|
45
|
+
"@ai-sdk/gateway": "4.0.134",
|
|
46
46
|
"@ai-sdk/provider": "4.0.35",
|
|
47
|
-
"@ai-sdk/provider-utils": "5.0.
|
|
47
|
+
"@ai-sdk/provider-utils": "5.0.73"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@ai-sdk/amazon-bedrock": "5.0.
|
|
51
|
-
"@ai-sdk/deepseek": "3.0.
|
|
52
|
-
"@ai-sdk/google": "4.0.
|
|
53
|
-
"@ai-sdk/groq": "4.0.
|
|
54
|
-
"@ai-sdk/huggingface": "2.0.
|
|
55
|
-
"@ai-sdk/moonshotai": "3.0.
|
|
56
|
-
"@ai-sdk/openai": "4.0.
|
|
50
|
+
"@ai-sdk/amazon-bedrock": "5.0.131",
|
|
51
|
+
"@ai-sdk/deepseek": "3.0.78",
|
|
52
|
+
"@ai-sdk/google": "4.0.110",
|
|
53
|
+
"@ai-sdk/groq": "4.0.75",
|
|
54
|
+
"@ai-sdk/huggingface": "2.0.83",
|
|
55
|
+
"@ai-sdk/moonshotai": "3.0.83",
|
|
56
|
+
"@ai-sdk/openai": "4.0.107",
|
|
57
57
|
"@ai-sdk/test-server": "2.0.4",
|
|
58
|
-
"@ai-sdk/xai": "5.0.
|
|
58
|
+
"@ai-sdk/xai": "5.0.37",
|
|
59
59
|
"@edge-runtime/vm": "^5.0.0",
|
|
60
60
|
"@smithy/eventstream-codec": "^4.3.3",
|
|
61
61
|
"@smithy/util-utf8": "^4.3.3",
|
|
@@ -981,7 +981,15 @@ async function generateTextInternal<
|
|
|
981
981
|
clientToolCalls.length === 0 &&
|
|
982
982
|
pendingDeferredToolCalls.size === 0
|
|
983
983
|
) {
|
|
984
|
-
|
|
984
|
+
// queued messages wait for a task that was not cut off:
|
|
985
|
+
const isTruncated =
|
|
986
|
+
steps[steps.length - 1].finishReason === 'length' ||
|
|
987
|
+
steps[steps.length - 1].finishReason === 'content-filter';
|
|
988
|
+
|
|
989
|
+
if (
|
|
990
|
+
!steeringRun?.hasPendingMessages() &&
|
|
991
|
+
(isTruncated || !steeringRun?.promoteQueuedMessage())
|
|
992
|
+
) {
|
|
985
993
|
return false;
|
|
986
994
|
}
|
|
987
995
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { convertDataContentToUint8Array } from '../prompt/data-content';
|
|
1
2
|
import {
|
|
2
3
|
convertBase64ToUint8Array,
|
|
3
4
|
convertUint8ArrayToBase64,
|
|
@@ -48,13 +49,15 @@ export class DefaultGeneratedFile implements GeneratedFile {
|
|
|
48
49
|
mediaType,
|
|
49
50
|
providerMetadata,
|
|
50
51
|
}: {
|
|
51
|
-
data: string | Uint8Array;
|
|
52
|
+
data: string | Uint8Array | ArrayBuffer;
|
|
52
53
|
mediaType: string;
|
|
53
54
|
providerMetadata?: Record<string, JSONObject>;
|
|
54
55
|
}) {
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
|
|
56
|
+
this.base64Data = typeof data === 'string' ? data : undefined;
|
|
57
|
+
this.uint8ArrayData =
|
|
58
|
+
typeof data === 'string'
|
|
59
|
+
? undefined
|
|
60
|
+
: convertDataContentToUint8Array(data);
|
|
58
61
|
this.mediaType = mediaType;
|
|
59
62
|
this.providerMetadata = providerMetadata;
|
|
60
63
|
}
|
|
@@ -79,7 +79,7 @@ export function smoothStream<TOOLS extends ToolSet>({
|
|
|
79
79
|
|
|
80
80
|
if (!buffer.startsWith(match)) {
|
|
81
81
|
throw new Error(
|
|
82
|
-
`Chunking function must return a match that is a prefix of the buffer. Received: "${match}"
|
|
82
|
+
`Chunking function must return a match that is a prefix of the buffer. Received: "${match}" which is not a prefix of buffer "${buffer}"`,
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -81,6 +81,24 @@ export interface Steering {
|
|
|
81
81
|
message: string | UserModelMessage,
|
|
82
82
|
options?: { id?: string },
|
|
83
83
|
): Promise<SteeringOutcome>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Adds a user message to the run once the model has finished its current
|
|
87
|
+
* task, i.e. when it gives a final answer without tool calls. The run then
|
|
88
|
+
* continues with the message unless a stop condition is met. Queued
|
|
89
|
+
* messages are delivered one per task, in order, after any waiting steering
|
|
90
|
+
* messages.
|
|
91
|
+
*
|
|
92
|
+
* @param message - The user message, or its text.
|
|
93
|
+
* @param options.id - The id of the message. Sending a message again with
|
|
94
|
+
* the same id returns the outcome of the first message.
|
|
95
|
+
*
|
|
96
|
+
* @returns The outcome of the message. It never rejects.
|
|
97
|
+
*/
|
|
98
|
+
queue(
|
|
99
|
+
message: string | UserModelMessage,
|
|
100
|
+
options?: { id?: string },
|
|
101
|
+
): Promise<SteeringOutcome>;
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
/**
|
|
@@ -89,6 +107,12 @@ export interface Steering {
|
|
|
89
107
|
export type SteeringRun = {
|
|
90
108
|
hasPendingMessages(): boolean;
|
|
91
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Moves the oldest queued message to the waiting messages for the next
|
|
112
|
+
* step. Returns `false` when no message is queued.
|
|
113
|
+
*/
|
|
114
|
+
promoteQueuedMessage(): boolean;
|
|
115
|
+
|
|
92
116
|
/**
|
|
93
117
|
* Takes the waiting messages for the next step. They are resolved by
|
|
94
118
|
* `resolveTakenMessages` once the prompt of the step is known.
|
|
@@ -116,6 +140,7 @@ type PendingSteer = {
|
|
|
116
140
|
class DefaultSteering implements Steering {
|
|
117
141
|
private state: 'idle' | 'running' | 'ended' = 'idle';
|
|
118
142
|
private pending: Array<PendingSteer> = [];
|
|
143
|
+
private queued: Array<PendingSteer> = [];
|
|
119
144
|
private taken: Array<PendingSteer> = [];
|
|
120
145
|
private outcomes = new Map<string, Promise<SteeringOutcome>>();
|
|
121
146
|
|
|
@@ -123,8 +148,25 @@ class DefaultSteering implements Steering {
|
|
|
123
148
|
message: string | UserModelMessage,
|
|
124
149
|
options?: { id?: string },
|
|
125
150
|
): Promise<SteeringOutcome> {
|
|
126
|
-
|
|
151
|
+
return this.add({ message, id: options?.id, target: this.pending });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
queue(
|
|
155
|
+
message: string | UserModelMessage,
|
|
156
|
+
options?: { id?: string },
|
|
157
|
+
): Promise<SteeringOutcome> {
|
|
158
|
+
return this.add({ message, id: options?.id, target: this.queued });
|
|
159
|
+
}
|
|
127
160
|
|
|
161
|
+
private add({
|
|
162
|
+
message,
|
|
163
|
+
id = generateSteeringId(),
|
|
164
|
+
target,
|
|
165
|
+
}: {
|
|
166
|
+
message: string | UserModelMessage;
|
|
167
|
+
id?: string;
|
|
168
|
+
target: Array<PendingSteer>;
|
|
169
|
+
}): Promise<SteeringOutcome> {
|
|
128
170
|
const existingOutcome = this.outcomes.get(id);
|
|
129
171
|
if (existingOutcome != null) {
|
|
130
172
|
return existingOutcome;
|
|
@@ -144,7 +186,7 @@ class DefaultSteering implements Steering {
|
|
|
144
186
|
reason: 'finished',
|
|
145
187
|
})
|
|
146
188
|
: new Promise<SteeringOutcome>(resolve => {
|
|
147
|
-
|
|
189
|
+
target.push({ id, message: userMessage, resolve });
|
|
148
190
|
});
|
|
149
191
|
|
|
150
192
|
this.outcomes.set(id, outcome);
|
|
@@ -168,6 +210,16 @@ class DefaultSteering implements Steering {
|
|
|
168
210
|
hasPendingMessages: () =>
|
|
169
211
|
this.state === 'running' && this.pending.length > 0,
|
|
170
212
|
|
|
213
|
+
promoteQueuedMessage: () => {
|
|
214
|
+
const next = this.state === 'running' ? this.queued.shift() : undefined;
|
|
215
|
+
if (next == null) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.pending.push(next);
|
|
220
|
+
return true;
|
|
221
|
+
},
|
|
222
|
+
|
|
171
223
|
takeMessages: () => {
|
|
172
224
|
if (this.state !== 'running') {
|
|
173
225
|
return [];
|
|
@@ -207,9 +259,10 @@ class DefaultSteering implements Steering {
|
|
|
207
259
|
return;
|
|
208
260
|
}
|
|
209
261
|
|
|
210
|
-
const notDelivered = [...this.taken, ...this.pending];
|
|
262
|
+
const notDelivered = [...this.taken, ...this.pending, ...this.queued];
|
|
211
263
|
this.taken = [];
|
|
212
264
|
this.pending = [];
|
|
265
|
+
this.queued = [];
|
|
213
266
|
this.state = 'ended';
|
|
214
267
|
|
|
215
268
|
for (const { id, message, resolve } of notDelivered) {
|
|
@@ -3389,7 +3389,15 @@ class DefaultStreamTextResult<
|
|
|
3389
3389
|
clientToolCalls.length === 0 &&
|
|
3390
3390
|
pendingDeferredToolCalls.size === 0
|
|
3391
3391
|
) {
|
|
3392
|
-
|
|
3392
|
+
// queued messages wait for a task that was not cut off:
|
|
3393
|
+
const isTruncated =
|
|
3394
|
+
stepFinishReason === 'length' ||
|
|
3395
|
+
stepFinishReason === 'content-filter';
|
|
3396
|
+
|
|
3397
|
+
if (
|
|
3398
|
+
!steeringRun?.hasPendingMessages() &&
|
|
3399
|
+
(isTruncated || !steeringRun?.promoteQueuedMessage())
|
|
3400
|
+
) {
|
|
3393
3401
|
return false;
|
|
3394
3402
|
}
|
|
3395
3403
|
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
isArrayBuffer,
|
|
3
|
+
isUint8Array,
|
|
4
|
+
type ModelMessage,
|
|
5
|
+
} from '@ai-sdk/provider-utils';
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Clone model messages while preserving URL instances. Node's structuredClone
|
|
@@ -7,35 +11,67 @@ import type { ModelMessage } from '@ai-sdk/provider-utils';
|
|
|
7
11
|
export function cloneModelMessages<T extends ModelMessage>(
|
|
8
12
|
messages: Array<T>,
|
|
9
13
|
): Array<T> {
|
|
10
|
-
return messages
|
|
14
|
+
return cloneValue(messages, new Map()) as Array<T>;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
function cloneValue
|
|
17
|
+
function cloneValue(value: unknown, seen: Map<object, unknown>): unknown {
|
|
18
|
+
if (value == null || typeof value !== 'object') {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (seen.has(value)) {
|
|
23
|
+
return seen.get(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
if (value instanceof URL) {
|
|
15
|
-
|
|
27
|
+
const copy = new URL(value.href);
|
|
28
|
+
seen.set(value, copy);
|
|
29
|
+
return copy;
|
|
16
30
|
}
|
|
17
31
|
|
|
18
32
|
if (Array.isArray(value)) {
|
|
19
|
-
|
|
33
|
+
const copy: unknown[] = new Array(value.length);
|
|
34
|
+
seen.set(value, copy);
|
|
35
|
+
value.forEach((item, index) => {
|
|
36
|
+
copy[index] = cloneValue(item, seen);
|
|
37
|
+
});
|
|
38
|
+
return copy;
|
|
20
39
|
}
|
|
21
40
|
|
|
22
|
-
if (value
|
|
23
|
-
|
|
41
|
+
if (isUint8Array(value)) {
|
|
42
|
+
const copy = new Uint8Array(value);
|
|
43
|
+
seen.set(value, copy);
|
|
44
|
+
return copy;
|
|
24
45
|
}
|
|
25
46
|
|
|
26
|
-
if (value
|
|
27
|
-
|
|
47
|
+
if (isArrayBuffer(value)) {
|
|
48
|
+
const copy = new Uint8Array(value).slice().buffer;
|
|
49
|
+
seen.set(value, copy);
|
|
50
|
+
return copy;
|
|
28
51
|
}
|
|
29
52
|
|
|
30
|
-
if (
|
|
31
|
-
|
|
53
|
+
if (
|
|
54
|
+
value instanceof Date ||
|
|
55
|
+
Object.prototype.toString.call(value) === '[object Date]'
|
|
56
|
+
) {
|
|
57
|
+
try {
|
|
58
|
+
const copy = new Date(Date.prototype.getTime.call(value));
|
|
59
|
+
seen.set(value, copy);
|
|
60
|
+
return copy;
|
|
61
|
+
} catch {
|
|
62
|
+
// A string tag alone does not make a plain object a Date.
|
|
63
|
+
}
|
|
32
64
|
}
|
|
33
65
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
const copy: Record<string, unknown> = {};
|
|
67
|
+
seen.set(value, copy);
|
|
68
|
+
for (const [key, val] of Object.entries(value)) {
|
|
69
|
+
Object.defineProperty(copy, key, {
|
|
70
|
+
value: cloneValue(val, seen),
|
|
71
|
+
writable: true,
|
|
72
|
+
enumerable: true,
|
|
73
|
+
configurable: true,
|
|
74
|
+
});
|
|
38
75
|
}
|
|
39
|
-
|
|
40
|
-
return value;
|
|
76
|
+
return copy;
|
|
41
77
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
isBuffer,
|
|
3
|
+
isUint8Array,
|
|
4
|
+
isArrayBuffer,
|
|
3
5
|
type CustomPart,
|
|
4
6
|
type DataContent,
|
|
5
7
|
type FilePart,
|
|
@@ -19,8 +21,8 @@ import { z, type ZodType } from '../util/zod';
|
|
|
19
21
|
|
|
20
22
|
const fileInlineDataSchema: ZodType<DataContent> = z.union([
|
|
21
23
|
z.string(),
|
|
22
|
-
z.
|
|
23
|
-
z.
|
|
24
|
+
z.custom<Uint8Array>(isUint8Array, { message: 'Must be a Uint8Array' }),
|
|
25
|
+
z.custom<ArrayBuffer>(isArrayBuffer, { message: 'Must be an ArrayBuffer' }),
|
|
24
26
|
z.custom<Buffer>(isBuffer, { message: 'Must be a Buffer' }),
|
|
25
27
|
]);
|
|
26
28
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
isUint8Array,
|
|
3
|
+
isArrayBuffer,
|
|
2
4
|
convertBase64ToUint8Array,
|
|
3
5
|
convertUint8ArrayToBase64,
|
|
4
6
|
type DataContent,
|
|
@@ -16,11 +18,7 @@ export function convertDataContentToBase64String(content: DataContent): string {
|
|
|
16
18
|
return content;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
return convertUint8ArrayToBase64(new Uint8Array(content));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return convertUint8ArrayToBase64(content);
|
|
21
|
+
return convertUint8ArrayToBase64(convertDataContentToUint8Array(content));
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
/**
|
|
@@ -32,8 +30,12 @@ export function convertDataContentToBase64String(content: DataContent): string {
|
|
|
32
30
|
export function convertDataContentToUint8Array(
|
|
33
31
|
content: DataContent,
|
|
34
32
|
): Uint8Array {
|
|
35
|
-
if (content
|
|
36
|
-
|
|
33
|
+
if (isUint8Array(content)) {
|
|
34
|
+
if (content instanceof Uint8Array) {
|
|
35
|
+
return content;
|
|
36
|
+
}
|
|
37
|
+
const { buffer, byteOffset, byteLength } = content;
|
|
38
|
+
return new Uint8Array(buffer, byteOffset, byteLength);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
if (typeof content === 'string') {
|
|
@@ -49,7 +51,7 @@ export function convertDataContentToUint8Array(
|
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
if (content
|
|
54
|
+
if (isArrayBuffer(content)) {
|
|
53
55
|
return new Uint8Array(content);
|
|
54
56
|
}
|
|
55
57
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { convertDataContentToUint8Array } from './data-content';
|
|
1
2
|
import type { LanguageModelV4FilePart } from '@ai-sdk/provider';
|
|
2
3
|
import {
|
|
4
|
+
isUint8Array,
|
|
5
|
+
isArrayBuffer,
|
|
3
6
|
isBuffer,
|
|
4
7
|
isProviderReference,
|
|
5
8
|
type DataContent,
|
|
@@ -58,12 +61,9 @@ function convertUrlStringToFilePartData(content: string): ConvertResult {
|
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
function convertInlineDataToFilePartData(content: DataContent): ConvertResult {
|
|
61
|
-
if (content
|
|
62
|
-
return { data: { type: 'data', data: content }, mediaType: undefined };
|
|
63
|
-
}
|
|
64
|
-
if (content instanceof ArrayBuffer) {
|
|
64
|
+
if (isUint8Array(content) || isArrayBuffer(content)) {
|
|
65
65
|
return {
|
|
66
|
-
data: { type: 'data', data:
|
|
66
|
+
data: { type: 'data', data: convertDataContentToUint8Array(content) },
|
|
67
67
|
mediaType: undefined,
|
|
68
68
|
};
|
|
69
69
|
}
|
|
@@ -130,6 +130,10 @@ export function convertToLanguageModelV4FilePart(
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
if (isUint8Array(content) || isArrayBuffer(content)) {
|
|
134
|
+
return convertInlineDataToFilePartData(content);
|
|
135
|
+
}
|
|
136
|
+
|
|
133
137
|
if (isProviderReference(content)) {
|
|
134
138
|
return {
|
|
135
139
|
data: { type: 'reference', reference: content as ProviderReference },
|
|
@@ -1056,7 +1056,9 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
1056
1056
|
default: {
|
|
1057
1057
|
if (isDataUIMessageChunk(chunk)) {
|
|
1058
1058
|
// validate data chunk if dataPartSchemas is provided
|
|
1059
|
-
|
|
1059
|
+
// (schemas are keyed by the part name without the 'data-' prefix)
|
|
1060
|
+
const dataPartSchema = dataPartSchemas?.[chunk.type.slice(5)];
|
|
1061
|
+
if (dataPartSchema != null) {
|
|
1060
1062
|
const partIdx = state.message.parts.findIndex(
|
|
1061
1063
|
p =>
|
|
1062
1064
|
'id' in p &&
|
|
@@ -1069,7 +1071,7 @@ export function processUIMessageStream<UI_MESSAGE extends UIMessage>({
|
|
|
1069
1071
|
|
|
1070
1072
|
await validateTypes({
|
|
1071
1073
|
value: chunk.data,
|
|
1072
|
-
schema:
|
|
1074
|
+
schema: dataPartSchema,
|
|
1073
1075
|
context: {
|
|
1074
1076
|
field: `message.parts[${actualPartIdx}].data`,
|
|
1075
1077
|
entityName: chunk.type,
|