ai 7.0.108 → 7.0.109
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 +8 -0
- package/dist/index.d.ts +12 -7
- package/dist/index.js +38 -21
- package/dist/index.js.map +1 -1
- package/dist/internal/index.d.ts +34 -2
- package/dist/internal/index.js +22 -21
- package/dist/internal/index.js.map +1 -1
- package/docs/03-ai-sdk-harnesses/02-harness-agent.mdx +7 -0
- package/docs/03-ai-sdk-harnesses/03-tools.mdx +46 -0
- package/package.json +2 -2
- package/src/generate-text/tools-context-parameter.ts +13 -7
- package/src/middleware/extract-reasoning-middleware.ts +29 -14
- package/src/ui/call-completion-api.ts +20 -7
|
@@ -362,6 +362,8 @@ When you only have raw continuation state from `suspendTurn()`, resume with
|
|
|
362
362
|
const session = await agent.createSession({
|
|
363
363
|
sessionId: chatId,
|
|
364
364
|
continueFrom: continuationState,
|
|
365
|
+
// Rebind this when the suspended turn used host-only tool context:
|
|
366
|
+
toolsContext,
|
|
365
367
|
});
|
|
366
368
|
|
|
367
369
|
const result = await agent.continueStream({ session });
|
|
@@ -370,6 +372,11 @@ const result = await agent.continueStream({ session });
|
|
|
370
372
|
Use `continueStream()` for incremental output, or `continueGenerate()` to drain
|
|
371
373
|
the continued turn and return a `GenerateTextResult`.
|
|
372
374
|
|
|
375
|
+
`toolsContext` is intentionally not stored in continuation state because it can
|
|
376
|
+
contain credentials or non-serializable host objects. When a suspended turn
|
|
377
|
+
used static or `prepareCall`-derived tool context, pass the same per-tool map to
|
|
378
|
+
`createSession` to rebind it in the process that continues the turn.
|
|
379
|
+
|
|
373
380
|
## Stop After a Harness Step
|
|
374
381
|
|
|
375
382
|
Use `stopWhen` to opt into semantic step boundaries. Predicates run after real
|
|
@@ -93,6 +93,52 @@ const agent = new HarnessAgent({
|
|
|
93
93
|
When the harness calls `weather`, `HarnessAgent` executes the tool in your host
|
|
94
94
|
process, then submits the result back to the harness runtime.
|
|
95
95
|
|
|
96
|
+
Host-executed tools can declare a `contextSchema` and receive turn-scoped
|
|
97
|
+
context through `toolsContext`:
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const lookupAccount = tool({
|
|
101
|
+
inputSchema: z.object({}),
|
|
102
|
+
contextSchema: z.object({ userId: z.string() }),
|
|
103
|
+
execute: async (_, { context }) => {
|
|
104
|
+
return loadAccount(context.userId);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const agent = new HarnessAgent({
|
|
109
|
+
harness: claudeCode,
|
|
110
|
+
tools: { lookupAccount },
|
|
111
|
+
toolsContext: {
|
|
112
|
+
lookupAccount: { userId: 'user-123' },
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use `prepareCall` to replace `toolsContext` when the value depends on custom
|
|
118
|
+
call options. The type of `toolsContext` follows the tool set: it is rejected
|
|
119
|
+
when no tool declares a context schema, required when a tool requires a
|
|
120
|
+
context object, and optional when every context object is optional.
|
|
121
|
+
`HarnessAgent` validates each entry against the tool's `contextSchema` before
|
|
122
|
+
execution and exposes the configured map in step results and lifecycle
|
|
123
|
+
callbacks.
|
|
124
|
+
|
|
125
|
+
Tool context remains host-only and is not serialized into suspended-turn state.
|
|
126
|
+
When recreating a session for an unfinished turn, rebind it explicitly:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const session = await agent.createSession({
|
|
130
|
+
sessionId,
|
|
131
|
+
continueFrom,
|
|
132
|
+
toolsContext: {
|
|
133
|
+
lookupAccount: { userId: 'user-123' },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Missing or invalid context fails validation before the host tool executes. The
|
|
139
|
+
validation error returned to the harness runtime is intentionally generic so
|
|
140
|
+
host-only context values and schema details are not disclosed to the model.
|
|
141
|
+
|
|
96
142
|
## Client-Side Tools
|
|
97
143
|
|
|
98
144
|
Omit `execute` when a browser, user interaction, or another external process
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.109",
|
|
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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@ai-sdk/provider-utils": "5.0.45"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
|
-
"@ai-sdk/amazon-bedrock": "5.0.
|
|
50
|
+
"@ai-sdk/amazon-bedrock": "5.0.90",
|
|
51
51
|
"@ai-sdk/deepseek": "3.0.50",
|
|
52
52
|
"@ai-sdk/google": "4.0.76",
|
|
53
53
|
"@ai-sdk/groq": "4.0.46",
|
|
@@ -10,13 +10,19 @@ import type {
|
|
|
10
10
|
type IsEmptyObject<OBJECT> = keyof OBJECT extends never ? true : false;
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Makes the toolsContext setting optional, required, or unavailable based on
|
|
14
|
+
* the tool set.
|
|
15
|
+
*/
|
|
16
|
+
export type ToolsContextSettings<TOOLS extends ToolSet> =
|
|
17
|
+
IsEmptyObject<InferToolSetContext<TOOLS>> extends true
|
|
18
|
+
? { toolsContext?: never }
|
|
19
|
+
: HasRequiredKey<InferToolSetContext<TOOLS>> extends true
|
|
20
|
+
? { toolsContext: InferToolSetContext<TOOLS> }
|
|
21
|
+
: { toolsContext?: InferToolSetContext<TOOLS> };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Helper type for request options that include both tools and their context.
|
|
15
25
|
*/
|
|
16
26
|
export type ToolsContextParameter<TOOLS extends ToolSet> = {
|
|
17
27
|
tools?: TOOLS;
|
|
18
|
-
} &
|
|
19
|
-
? { toolsContext?: never }
|
|
20
|
-
: HasRequiredKey<InferToolSetContext<TOOLS>> extends true
|
|
21
|
-
? { toolsContext: InferToolSetContext<TOOLS> }
|
|
22
|
-
: { toolsContext?: InferToolSetContext<TOOLS> });
|
|
28
|
+
} & ToolsContextSettings<TOOLS>;
|
|
@@ -90,12 +90,17 @@ export function extractReasoningMiddleware({
|
|
|
90
90
|
afterSwitch: boolean;
|
|
91
91
|
isReasoning: boolean;
|
|
92
92
|
buffer: string;
|
|
93
|
-
|
|
93
|
+
reasoningId: string | undefined;
|
|
94
94
|
textId: string;
|
|
95
95
|
}
|
|
96
96
|
> = createIdMap();
|
|
97
97
|
|
|
98
|
-
let
|
|
98
|
+
let reasoningIdCounter = 0;
|
|
99
|
+
|
|
100
|
+
const delayedTextStarts: Record<
|
|
101
|
+
string,
|
|
102
|
+
Extract<LanguageModelV4StreamPart, { type: 'text-start' }>
|
|
103
|
+
> = createIdMap();
|
|
99
104
|
|
|
100
105
|
return {
|
|
101
106
|
stream: stream.pipeThrough(
|
|
@@ -107,13 +112,16 @@ export function extractReasoningMiddleware({
|
|
|
107
112
|
// do not send `text-start` before `reasoning-start`
|
|
108
113
|
// https://github.com/vercel/ai/issues/7774
|
|
109
114
|
if (chunk.type === 'text-start') {
|
|
110
|
-
|
|
115
|
+
delayedTextStarts[chunk.id] = chunk;
|
|
111
116
|
return;
|
|
112
117
|
}
|
|
113
118
|
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
if (
|
|
120
|
+
chunk.type === 'text-end' &&
|
|
121
|
+
delayedTextStarts[chunk.id] != null
|
|
122
|
+
) {
|
|
123
|
+
controller.enqueue(delayedTextStarts[chunk.id]);
|
|
124
|
+
delete delayedTextStarts[chunk.id];
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
if (chunk.type !== 'text-delta') {
|
|
@@ -128,7 +136,7 @@ export function extractReasoningMiddleware({
|
|
|
128
136
|
afterSwitch: false,
|
|
129
137
|
isReasoning: startWithReasoning,
|
|
130
138
|
buffer: '',
|
|
131
|
-
|
|
139
|
+
reasoningId: undefined,
|
|
132
140
|
textId: chunk.id,
|
|
133
141
|
};
|
|
134
142
|
}
|
|
@@ -137,6 +145,10 @@ export function extractReasoningMiddleware({
|
|
|
137
145
|
|
|
138
146
|
activeExtraction.buffer += chunk.delta;
|
|
139
147
|
|
|
148
|
+
function getReasoningId() {
|
|
149
|
+
return (activeExtraction.reasoningId ??= `reasoning-${reasoningIdCounter++}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
140
152
|
function publish(text: string) {
|
|
141
153
|
if (text.length > 0) {
|
|
142
154
|
const prefix =
|
|
@@ -154,7 +166,7 @@ export function extractReasoningMiddleware({
|
|
|
154
166
|
) {
|
|
155
167
|
controller.enqueue({
|
|
156
168
|
type: 'reasoning-start',
|
|
157
|
-
id:
|
|
169
|
+
id: getReasoningId(),
|
|
158
170
|
});
|
|
159
171
|
}
|
|
160
172
|
|
|
@@ -162,12 +174,14 @@ export function extractReasoningMiddleware({
|
|
|
162
174
|
controller.enqueue({
|
|
163
175
|
type: 'reasoning-delta',
|
|
164
176
|
delta: prefix + text,
|
|
165
|
-
id:
|
|
177
|
+
id: getReasoningId(),
|
|
166
178
|
});
|
|
167
179
|
} else {
|
|
168
|
-
if (
|
|
169
|
-
controller.enqueue(
|
|
170
|
-
|
|
180
|
+
if (delayedTextStarts[activeExtraction.textId] != null) {
|
|
181
|
+
controller.enqueue(
|
|
182
|
+
delayedTextStarts[activeExtraction.textId],
|
|
183
|
+
);
|
|
184
|
+
delete delayedTextStarts[activeExtraction.textId];
|
|
171
185
|
}
|
|
172
186
|
controller.enqueue({
|
|
173
187
|
type: 'text-delta',
|
|
@@ -221,15 +235,16 @@ export function extractReasoningMiddleware({
|
|
|
221
235
|
if (activeExtraction.isFirstReasoning) {
|
|
222
236
|
controller.enqueue({
|
|
223
237
|
type: 'reasoning-start',
|
|
224
|
-
id:
|
|
238
|
+
id: getReasoningId(),
|
|
225
239
|
});
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
// reasoning part finished:
|
|
229
243
|
controller.enqueue({
|
|
230
244
|
type: 'reasoning-end',
|
|
231
|
-
id:
|
|
245
|
+
id: getReasoningId(),
|
|
232
246
|
});
|
|
247
|
+
activeExtraction.reasoningId = undefined;
|
|
233
248
|
}
|
|
234
249
|
|
|
235
250
|
activeExtraction.isReasoning = !activeExtraction.isReasoning;
|
|
@@ -30,6 +30,7 @@ export async function callCompletionApi({
|
|
|
30
30
|
setLoading,
|
|
31
31
|
setError,
|
|
32
32
|
setAbortController,
|
|
33
|
+
getAbortController,
|
|
33
34
|
onFinish,
|
|
34
35
|
onError,
|
|
35
36
|
fetch = getOriginalFetch(),
|
|
@@ -44,15 +45,19 @@ export async function callCompletionApi({
|
|
|
44
45
|
setLoading: (loading: boolean) => void;
|
|
45
46
|
setError: (error: Error | undefined) => void;
|
|
46
47
|
setAbortController: (abortController: AbortController | null) => void;
|
|
48
|
+
getAbortController?: () => AbortController | null | undefined;
|
|
47
49
|
onFinish: ((prompt: string, completion: string) => void) | undefined;
|
|
48
50
|
onError: ((error: Error) => void) | undefined;
|
|
49
51
|
fetch: ReturnType<typeof getOriginalFetch> | undefined;
|
|
50
52
|
}) {
|
|
53
|
+
const abortController = new AbortController();
|
|
54
|
+
const isCurrentRequest = () =>
|
|
55
|
+
getAbortController == null || getAbortController() === abortController;
|
|
56
|
+
|
|
51
57
|
try {
|
|
52
58
|
setLoading(true);
|
|
53
59
|
setError(undefined);
|
|
54
60
|
|
|
55
|
-
const abortController = new AbortController();
|
|
56
61
|
setAbortController(abortController);
|
|
57
62
|
|
|
58
63
|
// Empty the completion immediately.
|
|
@@ -100,7 +105,9 @@ export async function callCompletionApi({
|
|
|
100
105
|
stream: response.body,
|
|
101
106
|
onTextPart: chunk => {
|
|
102
107
|
result += chunk;
|
|
103
|
-
|
|
108
|
+
if (isCurrentRequest()) {
|
|
109
|
+
setCompletion(result);
|
|
110
|
+
}
|
|
104
111
|
},
|
|
105
112
|
});
|
|
106
113
|
break;
|
|
@@ -120,7 +127,9 @@ export async function callCompletionApi({
|
|
|
120
127
|
const streamPart = part.value;
|
|
121
128
|
if (streamPart.type === 'text-delta') {
|
|
122
129
|
result += streamPart.delta;
|
|
123
|
-
|
|
130
|
+
if (isCurrentRequest()) {
|
|
131
|
+
setCompletion(result);
|
|
132
|
+
}
|
|
124
133
|
} else if (streamPart.type === 'error') {
|
|
125
134
|
throw new UIMessageStreamError({
|
|
126
135
|
chunkType: 'error',
|
|
@@ -151,12 +160,10 @@ export async function callCompletionApi({
|
|
|
151
160
|
onFinish(prompt, result);
|
|
152
161
|
}
|
|
153
162
|
|
|
154
|
-
setAbortController(null);
|
|
155
163
|
return result;
|
|
156
164
|
} catch (err) {
|
|
157
165
|
// Ignore abort errors as they are expected.
|
|
158
166
|
if ((err as any).name === 'AbortError') {
|
|
159
|
-
setAbortController(null);
|
|
160
167
|
return null;
|
|
161
168
|
}
|
|
162
169
|
|
|
@@ -166,8 +173,14 @@ export async function callCompletionApi({
|
|
|
166
173
|
}
|
|
167
174
|
}
|
|
168
175
|
|
|
169
|
-
|
|
176
|
+
if (isCurrentRequest()) {
|
|
177
|
+
setError(err as Error);
|
|
178
|
+
}
|
|
170
179
|
} finally {
|
|
171
|
-
|
|
180
|
+
// A newer request may have started while this one was settling.
|
|
181
|
+
if (isCurrentRequest()) {
|
|
182
|
+
setAbortController(null);
|
|
183
|
+
setLoading(false);
|
|
184
|
+
}
|
|
172
185
|
}
|
|
173
186
|
}
|