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.
@@ -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.108",
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.89",
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
- * Helper type to make the toolsContext parameter optional, required, or
14
- * unavailable based on the tool set.
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
- } & (IsEmptyObject<InferToolSetContext<TOOLS>> extends true
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
- idCounter: number;
93
+ reasoningId: string | undefined;
94
94
  textId: string;
95
95
  }
96
96
  > = createIdMap();
97
97
 
98
- let delayedTextStart: LanguageModelV4StreamPart | undefined;
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
- delayedTextStart = chunk;
115
+ delayedTextStarts[chunk.id] = chunk;
111
116
  return;
112
117
  }
113
118
 
114
- if (chunk.type === 'text-end' && delayedTextStart) {
115
- controller.enqueue(delayedTextStart);
116
- delayedTextStart = undefined;
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
- idCounter: 0,
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: `reasoning-${activeExtraction.idCounter}`,
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: `reasoning-${activeExtraction.idCounter}`,
177
+ id: getReasoningId(),
166
178
  });
167
179
  } else {
168
- if (delayedTextStart) {
169
- controller.enqueue(delayedTextStart);
170
- delayedTextStart = undefined;
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: `reasoning-${activeExtraction.idCounter}`,
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: `reasoning-${activeExtraction.idCounter++}`,
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
- setCompletion(result);
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
- setCompletion(result);
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
- setError(err as Error);
176
+ if (isCurrentRequest()) {
177
+ setError(err as Error);
178
+ }
170
179
  } finally {
171
- setLoading(false);
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
  }