@tanstack/ai 0.23.1 → 0.24.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/dist/esm/activities/chat/index.d.ts +33 -9
- package/dist/esm/activities/chat/index.js +19 -9
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/messages.js +2 -1
- package/dist/esm/activities/chat/messages.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.d.ts +14 -14
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +16 -16
- package/dist/esm/activities/chat/runtime-context-types.d.ts +43 -0
- package/dist/esm/activities/chat/stream/message-updaters.d.ts +2 -2
- package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +1 -0
- package/dist/esm/activities/chat/stream/processor.js +35 -12
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-calls.d.ts +15 -5
- package/dist/esm/activities/chat/tools/tool-calls.js +59 -19
- package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
- package/dist/esm/activities/chat/tools/tool-definition.d.ts +12 -8
- package/dist/esm/activities/chat/tools/tool-definition.js.map +1 -1
- package/dist/esm/activities/error-payload.d.ts +26 -0
- package/dist/esm/activities/error-payload.js +12 -1
- package/dist/esm/activities/error-payload.js.map +1 -1
- package/dist/esm/adapter-internals.d.ts +1 -1
- package/dist/esm/adapter-internals.js +3 -2
- package/dist/esm/client.d.ts +1 -1
- package/dist/esm/client.js +3 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.js +7 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tool-registry.d.ts +7 -7
- package/dist/esm/tool-registry.js +1 -1
- package/dist/esm/tool-registry.js.map +1 -1
- package/dist/esm/types.d.ts +40 -8
- package/dist/esm/utilities/ag-ui-wire.js +1 -1
- package/dist/esm/utilities/ag-ui-wire.js.map +1 -1
- package/dist/esm/utilities/chat-params.d.ts +8 -3
- package/dist/esm/utilities/chat-params.js +6 -2
- package/dist/esm/utilities/chat-params.js.map +1 -1
- package/dist/esm/utilities/tool-result.d.ts +21 -0
- package/dist/esm/utilities/tool-result.js +37 -0
- package/dist/esm/utilities/tool-result.js.map +1 -0
- package/package.json +2 -2
- package/src/activities/chat/index.ts +219 -47
- package/src/activities/chat/messages.ts +2 -1
- package/src/activities/chat/middleware/compose.ts +23 -17
- package/src/activities/chat/middleware/types.ts +16 -16
- package/src/activities/chat/runtime-context-types.ts +68 -0
- package/src/activities/chat/stream/message-updaters.ts +2 -1
- package/src/activities/chat/stream/processor.ts +48 -8
- package/src/activities/chat/tools/tool-calls.ts +138 -43
- package/src/activities/chat/tools/tool-definition.ts +25 -31
- package/src/activities/error-payload.ts +44 -0
- package/src/adapter-internals.ts +4 -1
- package/src/client.ts +5 -1
- package/src/index.ts +7 -0
- package/src/tool-registry.ts +16 -14
- package/src/types.ts +86 -29
- package/src/utilities/ag-ui-wire.ts +4 -1
- package/src/utilities/chat-params.ts +22 -7
- package/src/utilities/tool-result.ts +60 -0
|
@@ -34,7 +34,7 @@ export type ChatMiddlewarePhase =
|
|
|
34
34
|
* Stable context object passed to all middleware hooks.
|
|
35
35
|
* Created once per chat() invocation and shared across all hooks.
|
|
36
36
|
*/
|
|
37
|
-
export interface ChatMiddlewareContext {
|
|
37
|
+
export interface ChatMiddlewareContext<TContext = unknown> {
|
|
38
38
|
/** Unique identifier for this chat request */
|
|
39
39
|
requestId: string
|
|
40
40
|
/** Unique identifier for this stream */
|
|
@@ -64,8 +64,8 @@ export interface ChatMiddlewareContext {
|
|
|
64
64
|
signal?: AbortSignal
|
|
65
65
|
/** Abort the chat run with a reason */
|
|
66
66
|
abort: (reason?: string) => void
|
|
67
|
-
/**
|
|
68
|
-
context:
|
|
67
|
+
/** Runtime context provided by chat() options */
|
|
68
|
+
context: TContext
|
|
69
69
|
/**
|
|
70
70
|
* Defer a non-blocking side-effect promise.
|
|
71
71
|
* Deferred promises do not block streaming and are awaited
|
|
@@ -343,7 +343,7 @@ export interface ErrorInfo {
|
|
|
343
343
|
* }
|
|
344
344
|
* ```
|
|
345
345
|
*/
|
|
346
|
-
export interface ChatMiddleware {
|
|
346
|
+
export interface ChatMiddleware<TContext = unknown> {
|
|
347
347
|
/** Optional name for debugging and identification */
|
|
348
348
|
name?: string
|
|
349
349
|
|
|
@@ -355,7 +355,7 @@ export interface ChatMiddleware {
|
|
|
355
355
|
* Only the fields you return are overwritten — everything else is preserved.
|
|
356
356
|
*/
|
|
357
357
|
onConfig?: (
|
|
358
|
-
ctx: ChatMiddlewareContext
|
|
358
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
359
359
|
config: ChatMiddlewareConfig,
|
|
360
360
|
) =>
|
|
361
361
|
| void
|
|
@@ -379,7 +379,7 @@ export interface ChatMiddleware {
|
|
|
379
379
|
* outputSchema or apply structured-output-specific behavior.
|
|
380
380
|
*/
|
|
381
381
|
onStructuredOutputConfig?: (
|
|
382
|
-
ctx: ChatMiddlewareContext
|
|
382
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
383
383
|
config: StructuredOutputMiddlewareConfig,
|
|
384
384
|
) =>
|
|
385
385
|
| void
|
|
@@ -390,14 +390,14 @@ export interface ChatMiddleware {
|
|
|
390
390
|
/**
|
|
391
391
|
* Called when the chat run starts (after initial onConfig).
|
|
392
392
|
*/
|
|
393
|
-
onStart?: (ctx: ChatMiddlewareContext) => void | Promise<void>
|
|
393
|
+
onStart?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>
|
|
394
394
|
|
|
395
395
|
/**
|
|
396
396
|
* Called at the start of each agent loop iteration, after a new assistant message ID
|
|
397
397
|
* is created. Use this to observe iteration boundaries.
|
|
398
398
|
*/
|
|
399
399
|
onIteration?: (
|
|
400
|
-
ctx: ChatMiddlewareContext
|
|
400
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
401
401
|
info: IterationInfo,
|
|
402
402
|
) => void | Promise<void>
|
|
403
403
|
|
|
@@ -408,7 +408,7 @@ export interface ChatMiddleware {
|
|
|
408
408
|
* @returns void (pass through), chunk (replace), chunk[] (expand), null (drop)
|
|
409
409
|
*/
|
|
410
410
|
onChunk?: (
|
|
411
|
-
ctx: ChatMiddlewareContext
|
|
411
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
412
412
|
chunk: StreamChunk,
|
|
413
413
|
) =>
|
|
414
414
|
| void
|
|
@@ -422,7 +422,7 @@ export interface ChatMiddleware {
|
|
|
422
422
|
* Can observe, transform args, skip execution, or abort the run.
|
|
423
423
|
*/
|
|
424
424
|
onBeforeToolCall?: (
|
|
425
|
-
ctx: ChatMiddlewareContext
|
|
425
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
426
426
|
hookCtx: ToolCallHookContext,
|
|
427
427
|
) => BeforeToolCallDecision | Promise<BeforeToolCallDecision>
|
|
428
428
|
|
|
@@ -430,7 +430,7 @@ export interface ChatMiddleware {
|
|
|
430
430
|
* Called after a tool execution completes (success or failure).
|
|
431
431
|
*/
|
|
432
432
|
onAfterToolCall?: (
|
|
433
|
-
ctx: ChatMiddlewareContext
|
|
433
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
434
434
|
info: AfterToolCallInfo,
|
|
435
435
|
) => void | Promise<void>
|
|
436
436
|
|
|
@@ -439,7 +439,7 @@ export interface ChatMiddleware {
|
|
|
439
439
|
* Provides aggregate data about tool execution results, approvals, and client tools.
|
|
440
440
|
*/
|
|
441
441
|
onToolPhaseComplete?: (
|
|
442
|
-
ctx: ChatMiddlewareContext
|
|
442
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
443
443
|
info: ToolPhaseCompleteInfo,
|
|
444
444
|
) => void | Promise<void>
|
|
445
445
|
|
|
@@ -448,7 +448,7 @@ export interface ChatMiddleware {
|
|
|
448
448
|
* Called once per model iteration that reports usage.
|
|
449
449
|
*/
|
|
450
450
|
onUsage?: (
|
|
451
|
-
ctx: ChatMiddlewareContext
|
|
451
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
452
452
|
usage: UsageInfo,
|
|
453
453
|
) => void | Promise<void>
|
|
454
454
|
|
|
@@ -457,7 +457,7 @@ export interface ChatMiddleware {
|
|
|
457
457
|
* Exactly one of onFinish/onAbort/onError will be called per run.
|
|
458
458
|
*/
|
|
459
459
|
onFinish?: (
|
|
460
|
-
ctx: ChatMiddlewareContext
|
|
460
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
461
461
|
info: FinishInfo,
|
|
462
462
|
) => void | Promise<void>
|
|
463
463
|
|
|
@@ -466,7 +466,7 @@ export interface ChatMiddleware {
|
|
|
466
466
|
* Exactly one of onFinish/onAbort/onError will be called per run.
|
|
467
467
|
*/
|
|
468
468
|
onAbort?: (
|
|
469
|
-
ctx: ChatMiddlewareContext
|
|
469
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
470
470
|
info: AbortInfo,
|
|
471
471
|
) => void | Promise<void>
|
|
472
472
|
|
|
@@ -475,7 +475,7 @@ export interface ChatMiddleware {
|
|
|
475
475
|
* Exactly one of onFinish/onAbort/onError will be called per run.
|
|
476
476
|
*/
|
|
477
477
|
onError?: (
|
|
478
|
-
ctx: ChatMiddlewareContext
|
|
478
|
+
ctx: ChatMiddlewareContext<TContext>,
|
|
479
479
|
info: ErrorInfo,
|
|
480
480
|
) => void | Promise<void>
|
|
481
481
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ChatMiddleware } from './middleware/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared type-level helpers for inferring the runtime `context` requirement
|
|
5
|
+
* from typed tools and middleware.
|
|
6
|
+
*
|
|
7
|
+
* These primitives are consumed by both the chat activity options
|
|
8
|
+
* (`./index.ts`, which merges tool + middleware requirements) and the tool
|
|
9
|
+
* execution layer (`./tools/tool-calls.ts`, which only sees tools). They live
|
|
10
|
+
* here so the two call sites share one definition instead of maintaining
|
|
11
|
+
* divergent copies.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** True only when `T` is exactly `unknown`. */
|
|
15
|
+
type IsUnknown<T> = unknown extends T
|
|
16
|
+
? [T] extends [unknown]
|
|
17
|
+
? true
|
|
18
|
+
: false
|
|
19
|
+
: false
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Drops an `unknown` context requirement to `never` so that untyped tools and
|
|
23
|
+
* middleware (which default `TContext` to `unknown`) contribute no requirement
|
|
24
|
+
* to the merged context.
|
|
25
|
+
*/
|
|
26
|
+
type KnownContext<T> = IsUnknown<T> extends true ? never : T
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merge two inferred context requirements, treating `never` as "no
|
|
30
|
+
* requirement". Using this instead of a raw intersection keeps a `never`
|
|
31
|
+
* (untyped) contributor from collapsing the whole merge to `never`.
|
|
32
|
+
*/
|
|
33
|
+
export type MergeContext<TLeft, TRight> = [TLeft] extends [never]
|
|
34
|
+
? TRight
|
|
35
|
+
: [TRight] extends [never]
|
|
36
|
+
? TLeft
|
|
37
|
+
: TLeft & TRight
|
|
38
|
+
|
|
39
|
+
/** Collapse a union of context requirements into their intersection. */
|
|
40
|
+
export type UnionToIntersection<T> = [T] extends [never]
|
|
41
|
+
? never
|
|
42
|
+
: (T extends unknown ? (value: T) => void : never) extends (
|
|
43
|
+
value: infer TIntersection,
|
|
44
|
+
) => void
|
|
45
|
+
? TIntersection
|
|
46
|
+
: never
|
|
47
|
+
|
|
48
|
+
/** Strip `undefined` from a context requirement. */
|
|
49
|
+
export type DefinedContext<T> = Exclude<T, undefined>
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extract the `context` requirement declared by a tool execute function's
|
|
53
|
+
* second argument, dropping `unknown` (untyped) contexts to `never`.
|
|
54
|
+
*/
|
|
55
|
+
type ContextFromExecute<T> = T extends (...args: any) => any
|
|
56
|
+
? NonNullable<Parameters<T>[1]> extends { context: infer TUserContext }
|
|
57
|
+
? KnownContext<TUserContext>
|
|
58
|
+
: never
|
|
59
|
+
: never
|
|
60
|
+
|
|
61
|
+
/** Extract the context requirement declared by a single tool. */
|
|
62
|
+
export type ContextFromTool<T> = T extends { execute?: infer TExecute }
|
|
63
|
+
? ContextFromExecute<TExecute>
|
|
64
|
+
: never
|
|
65
|
+
|
|
66
|
+
/** Extract the context requirement declared by a single middleware. */
|
|
67
|
+
export type ContextFromMiddleware<T> =
|
|
68
|
+
T extends ChatMiddleware<infer TContext> ? KnownContext<TContext> : never
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { parsePartialJSON } from './json-parser'
|
|
9
9
|
import type {
|
|
10
|
+
ContentPart,
|
|
10
11
|
StructuredOutputPart,
|
|
11
12
|
ThinkingPart,
|
|
12
13
|
ToolCallPart,
|
|
@@ -107,7 +108,7 @@ export function updateToolResultPart(
|
|
|
107
108
|
messages: Array<UIMessage>,
|
|
108
109
|
messageId: string,
|
|
109
110
|
toolCallId: string,
|
|
110
|
-
content: string
|
|
111
|
+
content: string | Array<ContentPart>,
|
|
111
112
|
state: ToolResultState,
|
|
112
113
|
error?: string,
|
|
113
114
|
): Array<UIMessage> {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* adapter contract, single-shot flows, and expected UIMessage output.
|
|
19
19
|
*/
|
|
20
20
|
import { generateMessageId, uiMessageToModelMessages } from '../messages.js'
|
|
21
|
+
import { normalizeToolResult } from '../../../utilities/tool-result'
|
|
21
22
|
import { defaultJSONParser } from './json-parser'
|
|
22
23
|
import {
|
|
23
24
|
appendStructuredOutputDelta,
|
|
@@ -321,7 +322,7 @@ export class StreamProcessor {
|
|
|
321
322
|
)
|
|
322
323
|
|
|
323
324
|
// Step 2: Create a tool-result part (for LLM conversation history)
|
|
324
|
-
const content =
|
|
325
|
+
const content = normalizeToolResult(output)
|
|
325
326
|
const toolResultState: ToolResultState = error ? 'error' : 'complete'
|
|
326
327
|
|
|
327
328
|
updatedMessages = updateToolResultPart(
|
|
@@ -1170,30 +1171,51 @@ export class StreamProcessor {
|
|
|
1170
1171
|
// Step 1: Update the tool-call part's output field (for UI consistency
|
|
1171
1172
|
// with client tools — see GitHub issue #176)
|
|
1172
1173
|
let output: unknown
|
|
1173
|
-
|
|
1174
|
-
output = JSON.parse(chunk.result)
|
|
1175
|
-
} catch {
|
|
1174
|
+
if (Array.isArray(chunk.result)) {
|
|
1176
1175
|
output = chunk.result
|
|
1176
|
+
} else {
|
|
1177
|
+
try {
|
|
1178
|
+
output = JSON.parse(chunk.result)
|
|
1179
|
+
} catch {
|
|
1180
|
+
output = chunk.result
|
|
1181
|
+
}
|
|
1177
1182
|
}
|
|
1178
1183
|
this.messages = updateToolCallWithOutput(
|
|
1179
1184
|
this.messages,
|
|
1180
1185
|
chunk.toolCallId,
|
|
1181
1186
|
output,
|
|
1187
|
+
chunk.state === 'output-error' ? 'input-complete' : undefined,
|
|
1182
1188
|
)
|
|
1183
1189
|
|
|
1184
1190
|
// Step 2: Create/update the tool-result part (for LLM conversation history)
|
|
1185
|
-
const resultState: ToolResultState =
|
|
1191
|
+
const resultState: ToolResultState =
|
|
1192
|
+
chunk.state === 'output-error' ? 'error' : 'complete'
|
|
1186
1193
|
this.messages = updateToolResultPart(
|
|
1187
1194
|
this.messages,
|
|
1188
1195
|
messageId,
|
|
1189
1196
|
chunk.toolCallId,
|
|
1190
1197
|
chunk.result,
|
|
1191
1198
|
resultState,
|
|
1199
|
+
resultState === 'error'
|
|
1200
|
+
? this.extractToolResultError(output)
|
|
1201
|
+
: undefined,
|
|
1192
1202
|
)
|
|
1193
1203
|
this.emitMessagesChange()
|
|
1194
1204
|
}
|
|
1195
1205
|
}
|
|
1196
1206
|
|
|
1207
|
+
private extractToolResultError(output: unknown): string {
|
|
1208
|
+
if (
|
|
1209
|
+
output &&
|
|
1210
|
+
typeof output === 'object' &&
|
|
1211
|
+
'error' in output &&
|
|
1212
|
+
typeof output.error === 'string'
|
|
1213
|
+
) {
|
|
1214
|
+
return output.error
|
|
1215
|
+
}
|
|
1216
|
+
return typeof output === 'string' ? output : 'Tool execution failed'
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1197
1219
|
/**
|
|
1198
1220
|
* Handle TOOL_CALL_RESULT event (AG-UI spec).
|
|
1199
1221
|
*
|
|
@@ -1218,16 +1240,19 @@ export class StreamProcessor {
|
|
|
1218
1240
|
this.messages,
|
|
1219
1241
|
chunk.toolCallId,
|
|
1220
1242
|
output,
|
|
1243
|
+
chunk.state === 'output-error' ? 'input-complete' : undefined,
|
|
1221
1244
|
)
|
|
1222
1245
|
|
|
1223
1246
|
// Step 2: Create/update the tool-result part
|
|
1224
|
-
const resultState: ToolResultState =
|
|
1247
|
+
const resultState: ToolResultState =
|
|
1248
|
+
chunk.state === 'output-error' ? 'error' : 'complete'
|
|
1225
1249
|
this.messages = updateToolResultPart(
|
|
1226
1250
|
this.messages,
|
|
1227
1251
|
messageId,
|
|
1228
1252
|
chunk.toolCallId,
|
|
1229
1253
|
chunk.content,
|
|
1230
1254
|
resultState,
|
|
1255
|
+
resultState === 'error' ? this.extractToolResultError(output) : undefined,
|
|
1231
1256
|
)
|
|
1232
1257
|
this.emitMessagesChange()
|
|
1233
1258
|
}
|
|
@@ -1274,7 +1299,10 @@ export class StreamProcessor {
|
|
|
1274
1299
|
chunk: Extract<StreamChunk, { type: 'RUN_ERROR' }>,
|
|
1275
1300
|
): void {
|
|
1276
1301
|
this.hasError = true
|
|
1277
|
-
const runId =
|
|
1302
|
+
const runId =
|
|
1303
|
+
'runId' in chunk && typeof chunk.runId === 'string'
|
|
1304
|
+
? chunk.runId
|
|
1305
|
+
: undefined
|
|
1278
1306
|
if (runId) {
|
|
1279
1307
|
this.activeRuns.delete(runId)
|
|
1280
1308
|
} else {
|
|
@@ -1305,7 +1333,19 @@ export class StreamProcessor {
|
|
|
1305
1333
|
this.emitMessagesChange()
|
|
1306
1334
|
}
|
|
1307
1335
|
|
|
1308
|
-
|
|
1336
|
+
// Attach the provider's structured error body (`rawEvent`) and `code` to
|
|
1337
|
+
// the surfaced Error so consumers can recover the upstream detail that the
|
|
1338
|
+
// RUN_ERROR's `message` alone discards. Both are optional and added only
|
|
1339
|
+
// when present, keeping the Error backward compatible.
|
|
1340
|
+
const error = new Error(errorMessage)
|
|
1341
|
+
const code = chunk.code ?? chunk.error?.code
|
|
1342
|
+
if (code !== undefined) {
|
|
1343
|
+
Object.assign(error, { code })
|
|
1344
|
+
}
|
|
1345
|
+
if (chunk.rawEvent !== undefined) {
|
|
1346
|
+
Object.assign(error, { rawEvent: chunk.rawEvent })
|
|
1347
|
+
}
|
|
1348
|
+
this.events.onError?.(error)
|
|
1309
1349
|
}
|
|
1310
1350
|
|
|
1311
1351
|
/**
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { normalizeToolResult } from '../../../utilities/tool-result'
|
|
1
2
|
import { isStandardSchema, parseWithStandardSchema } from './schema-converter'
|
|
2
3
|
import type {
|
|
4
|
+
AnyTool,
|
|
5
|
+
ContentPart,
|
|
3
6
|
CustomEvent,
|
|
4
7
|
ModelMessage,
|
|
5
8
|
RunFinishedEvent,
|
|
@@ -9,11 +12,18 @@ import type {
|
|
|
9
12
|
ToolCallEndEvent,
|
|
10
13
|
ToolCallStartEvent,
|
|
11
14
|
ToolExecutionContext,
|
|
15
|
+
ToolOutputState,
|
|
12
16
|
} from '../../../types'
|
|
13
17
|
import type {
|
|
14
18
|
AfterToolCallInfo,
|
|
15
19
|
BeforeToolCallDecision,
|
|
16
20
|
} from '../middleware/types'
|
|
21
|
+
import type {
|
|
22
|
+
ContextFromTool,
|
|
23
|
+
DefinedContext,
|
|
24
|
+
MergeContext,
|
|
25
|
+
UnionToIntersection,
|
|
26
|
+
} from '../runtime-context-types'
|
|
17
27
|
|
|
18
28
|
function safeJsonParse(value: string): unknown {
|
|
19
29
|
try {
|
|
@@ -46,6 +56,36 @@ export class MiddlewareAbortError extends Error {
|
|
|
46
56
|
}
|
|
47
57
|
}
|
|
48
58
|
|
|
59
|
+
// The leaf context-inference primitives (ContextFromTool, MergeContext,
|
|
60
|
+
// UnionToIntersection, DefinedContext) are shared with the chat activity
|
|
61
|
+
// options layer — see ../runtime-context-types.
|
|
62
|
+
type RequiredContextFromToolUnion<T> = T extends unknown
|
|
63
|
+
? undefined extends ContextFromTool<T>
|
|
64
|
+
? never
|
|
65
|
+
: ContextFromTool<T>
|
|
66
|
+
: never
|
|
67
|
+
|
|
68
|
+
type ContextFromToolUnion<T> = [
|
|
69
|
+
UnionToIntersection<DefinedContext<ContextFromTool<T>>>,
|
|
70
|
+
] extends [never]
|
|
71
|
+
? unknown
|
|
72
|
+
: [RequiredContextFromToolUnion<T>] extends [never]
|
|
73
|
+
? UnionToIntersection<DefinedContext<ContextFromTool<T>>> | undefined
|
|
74
|
+
: UnionToIntersection<DefinedContext<ContextFromTool<T>>>
|
|
75
|
+
|
|
76
|
+
type ContextFromTools<TTools> = TTools extends readonly [
|
|
77
|
+
infer THead,
|
|
78
|
+
...infer TTail,
|
|
79
|
+
]
|
|
80
|
+
? MergeContext<ContextFromTool<THead>, ContextFromTools<TTail>>
|
|
81
|
+
: TTools extends ReadonlyArray<infer TTool>
|
|
82
|
+
? ContextFromToolUnion<TTool>
|
|
83
|
+
: unknown
|
|
84
|
+
|
|
85
|
+
type ExecuteToolsContextArgs<TContext> = undefined extends TContext
|
|
86
|
+
? [userContext?: TContext]
|
|
87
|
+
: [userContext: TContext]
|
|
88
|
+
|
|
49
89
|
/**
|
|
50
90
|
* Manages tool call accumulation and execution for the chat() method's automatic tool execution loop.
|
|
51
91
|
*
|
|
@@ -80,11 +120,22 @@ export class MiddlewareAbortError extends Error {
|
|
|
80
120
|
* }
|
|
81
121
|
* ```
|
|
82
122
|
*/
|
|
83
|
-
export class ToolCallManager
|
|
123
|
+
export class ToolCallManager<
|
|
124
|
+
TToolsOrContext = ReadonlyArray<AnyTool>,
|
|
125
|
+
TContext = TToolsOrContext extends ReadonlyArray<AnyTool>
|
|
126
|
+
? ContextFromTools<TToolsOrContext>
|
|
127
|
+
: TToolsOrContext,
|
|
128
|
+
> {
|
|
84
129
|
private readonly toolCallsMap = new Map<number, ToolCall>()
|
|
85
|
-
private readonly tools: ReadonlyArray<
|
|
86
|
-
|
|
87
|
-
|
|
130
|
+
private readonly tools: TToolsOrContext extends ReadonlyArray<AnyTool>
|
|
131
|
+
? TToolsOrContext
|
|
132
|
+
: ReadonlyArray<AnyTool>
|
|
133
|
+
|
|
134
|
+
constructor(
|
|
135
|
+
tools: TToolsOrContext extends ReadonlyArray<AnyTool>
|
|
136
|
+
? TToolsOrContext
|
|
137
|
+
: ReadonlyArray<AnyTool>,
|
|
138
|
+
) {
|
|
88
139
|
this.tools = tools
|
|
89
140
|
}
|
|
90
141
|
|
|
@@ -161,14 +212,18 @@ export class ToolCallManager {
|
|
|
161
212
|
*/
|
|
162
213
|
async *executeTools(
|
|
163
214
|
finishEvent: RunFinishedEvent,
|
|
215
|
+
...contextArgs: ExecuteToolsContextArgs<TContext>
|
|
164
216
|
): AsyncGenerator<ToolCallEndEvent, Array<ModelMessage>, void> {
|
|
165
217
|
const toolCallsArray = this.getToolCalls()
|
|
166
218
|
const toolResults: Array<ModelMessage> = []
|
|
219
|
+
const hasRuntimeContext = contextArgs.length > 0
|
|
220
|
+
const userContext = contextArgs[0]
|
|
167
221
|
|
|
168
222
|
for (const toolCall of toolCallsArray) {
|
|
169
223
|
const tool = this.tools.find((t) => t.name === toolCall.function.name)
|
|
170
224
|
|
|
171
|
-
let toolResultContent: string
|
|
225
|
+
let toolResultContent: string | Array<ContentPart>
|
|
226
|
+
let toolResultState: ToolOutputState | undefined
|
|
172
227
|
if (tool?.execute) {
|
|
173
228
|
try {
|
|
174
229
|
// Parse arguments (normalize null/non-object to {} for empty tool_use blocks)
|
|
@@ -199,15 +254,21 @@ export class ToolCallManager {
|
|
|
199
254
|
}
|
|
200
255
|
|
|
201
256
|
// Execute the tool
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
257
|
+
const executionContext = {
|
|
258
|
+
toolCallId: toolCall.id,
|
|
259
|
+
context: userContext,
|
|
260
|
+
emitCustomEvent: () => {},
|
|
261
|
+
} as ToolExecutionContext<TContext>
|
|
262
|
+
let result = hasRuntimeContext
|
|
263
|
+
? await tool.execute(args, executionContext)
|
|
264
|
+
: await tool.execute(args)
|
|
265
|
+
|
|
266
|
+
// Validate output against outputSchema if provided (for Standard
|
|
267
|
+
// Schema compliant schemas). Unlike the previous implementation we
|
|
268
|
+
// intentionally validate `undefined`/`null` results too, so a tool
|
|
269
|
+
// whose schema forbids them surfaces a validation error instead of
|
|
270
|
+
// silently passing — the schema itself decides whether they're valid.
|
|
271
|
+
if (tool.outputSchema && isStandardSchema(tool.outputSchema)) {
|
|
211
272
|
try {
|
|
212
273
|
result = parseWithStandardSchema(tool.outputSchema, result)
|
|
213
274
|
} catch (validationError: unknown) {
|
|
@@ -221,13 +282,13 @@ export class ToolCallManager {
|
|
|
221
282
|
}
|
|
222
283
|
}
|
|
223
284
|
|
|
224
|
-
toolResultContent =
|
|
225
|
-
typeof result === 'string' ? result : JSON.stringify(result)
|
|
285
|
+
toolResultContent = normalizeToolResult(result)
|
|
226
286
|
} catch (error: unknown) {
|
|
227
287
|
// If tool execution fails, add error message
|
|
228
288
|
const message =
|
|
229
289
|
error instanceof Error ? error.message : 'Unknown error'
|
|
230
290
|
toolResultContent = `Error executing tool: ${message}`
|
|
291
|
+
toolResultState = 'output-error'
|
|
231
292
|
}
|
|
232
293
|
} else {
|
|
233
294
|
// Tool doesn't have execute function, add placeholder
|
|
@@ -243,6 +304,7 @@ export class ToolCallManager {
|
|
|
243
304
|
model: finishEvent.model,
|
|
244
305
|
timestamp: Date.now(),
|
|
245
306
|
result: toolResultContent,
|
|
307
|
+
...(toolResultState !== undefined && { state: toolResultState }),
|
|
246
308
|
} as ToolCallEndEvent
|
|
247
309
|
|
|
248
310
|
// Add tool result message
|
|
@@ -370,7 +432,7 @@ async function applyBeforeToolCallDecision(
|
|
|
370
432
|
result:
|
|
371
433
|
typeof skipResult === 'string'
|
|
372
434
|
? safeJsonParse(skipResult)
|
|
373
|
-
: skipResult
|
|
435
|
+
: (skipResult ?? null),
|
|
374
436
|
duration: 0,
|
|
375
437
|
})
|
|
376
438
|
if (middlewareHooks.onAfterToolCall) {
|
|
@@ -394,12 +456,12 @@ async function applyBeforeToolCallDecision(
|
|
|
394
456
|
* Execute a server-side tool with event polling, output validation, and middleware hooks.
|
|
395
457
|
* Yields CustomEvent chunks during execution and pushes the result to the results array.
|
|
396
458
|
*/
|
|
397
|
-
async function* executeServerTool(
|
|
459
|
+
async function* executeServerTool<TContext = unknown>(
|
|
398
460
|
toolCall: ToolCall,
|
|
399
|
-
tool:
|
|
461
|
+
tool: AnyTool,
|
|
400
462
|
toolName: string,
|
|
401
463
|
input: unknown,
|
|
402
|
-
context: ToolExecutionContext
|
|
464
|
+
context: ToolExecutionContext<TContext>,
|
|
403
465
|
pendingEvents: Array<CustomEvent>,
|
|
404
466
|
results: Array<ToolResult>,
|
|
405
467
|
middlewareHooks?: ToolExecutionMiddlewareHooks,
|
|
@@ -419,18 +481,14 @@ async function* executeServerTool(
|
|
|
419
481
|
yield pendingEvent
|
|
420
482
|
}
|
|
421
483
|
|
|
422
|
-
// Validate output against outputSchema if provided
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
isStandardSchema(tool.outputSchema) &&
|
|
426
|
-
result !== undefined &&
|
|
427
|
-
result !== null
|
|
428
|
-
) {
|
|
484
|
+
// Validate output against outputSchema if provided. Validates
|
|
485
|
+
// `undefined`/`null` too — the schema decides whether they're valid.
|
|
486
|
+
if (tool.outputSchema && isStandardSchema(tool.outputSchema)) {
|
|
429
487
|
result = parseWithStandardSchema(tool.outputSchema, result)
|
|
430
488
|
}
|
|
431
489
|
|
|
432
490
|
const finalResult =
|
|
433
|
-
typeof result === 'string' ? safeJsonParse(result) : result
|
|
491
|
+
typeof result === 'string' ? safeJsonParse(result) : (result ?? null)
|
|
434
492
|
|
|
435
493
|
results.push({
|
|
436
494
|
toolCallId: toolCall.id,
|
|
@@ -486,6 +544,35 @@ async function* executeServerTool(
|
|
|
486
544
|
}
|
|
487
545
|
}
|
|
488
546
|
|
|
547
|
+
function buildClientToolResult(
|
|
548
|
+
toolCallId: string,
|
|
549
|
+
toolName: string,
|
|
550
|
+
tool: AnyTool,
|
|
551
|
+
rawResult: unknown,
|
|
552
|
+
): ToolResult {
|
|
553
|
+
try {
|
|
554
|
+
let result = rawResult
|
|
555
|
+
if (tool.outputSchema && isStandardSchema(tool.outputSchema)) {
|
|
556
|
+
result = parseWithStandardSchema(tool.outputSchema, result)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return {
|
|
560
|
+
toolCallId,
|
|
561
|
+
toolName,
|
|
562
|
+
result:
|
|
563
|
+
typeof result === 'string' ? safeJsonParse(result) : (result ?? null),
|
|
564
|
+
}
|
|
565
|
+
} catch (error: unknown) {
|
|
566
|
+
const message = error instanceof Error ? error.message : 'Validation failed'
|
|
567
|
+
return {
|
|
568
|
+
toolCallId,
|
|
569
|
+
toolName,
|
|
570
|
+
result: { error: message },
|
|
571
|
+
state: 'output-error',
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
489
576
|
/**
|
|
490
577
|
* Execute tool calls based on their configuration.
|
|
491
578
|
* Yields CustomEvent chunks during tool execution for real-time progress updates.
|
|
@@ -501,9 +588,9 @@ async function* executeServerTool(
|
|
|
501
588
|
* @param clientResults - Map of client-side execution results (toolCallId -> result)
|
|
502
589
|
* @param createCustomEventChunk - Factory to create CustomEvent chunks (optional)
|
|
503
590
|
*/
|
|
504
|
-
export async function* executeToolCalls(
|
|
591
|
+
export async function* executeToolCalls<TContext = unknown>(
|
|
505
592
|
toolCalls: Array<ToolCall>,
|
|
506
|
-
tools: ReadonlyArray<
|
|
593
|
+
tools: ReadonlyArray<AnyTool>,
|
|
507
594
|
approvals: Map<string, boolean> = new Map(),
|
|
508
595
|
clientResults: Map<string, any> = new Map(),
|
|
509
596
|
createCustomEventChunk?: (
|
|
@@ -511,13 +598,14 @@ export async function* executeToolCalls(
|
|
|
511
598
|
value: Record<string, any>,
|
|
512
599
|
) => CustomEvent,
|
|
513
600
|
middlewareHooks?: ToolExecutionMiddlewareHooks,
|
|
601
|
+
userContext?: TContext,
|
|
514
602
|
): AsyncGenerator<CustomEvent, ExecuteToolCallsResult, void> {
|
|
515
603
|
const results: Array<ToolResult> = []
|
|
516
604
|
const needsApproval: Array<ApprovalRequest> = []
|
|
517
605
|
const needsClientExecution: Array<ClientToolRequest> = []
|
|
518
606
|
|
|
519
607
|
// Create tool lookup map
|
|
520
|
-
const toolMap = new Map<string,
|
|
608
|
+
const toolMap = new Map<string, AnyTool>()
|
|
521
609
|
for (const tool of tools) {
|
|
522
610
|
toolMap.set(tool.name, tool)
|
|
523
611
|
}
|
|
@@ -588,8 +676,9 @@ export async function* executeToolCalls(
|
|
|
588
676
|
|
|
589
677
|
// Create a ToolExecutionContext for this tool call with event emission
|
|
590
678
|
const pendingEvents: Array<CustomEvent> = []
|
|
591
|
-
const context
|
|
679
|
+
const context = {
|
|
592
680
|
toolCallId: toolCall.id,
|
|
681
|
+
context: userContext,
|
|
593
682
|
emitCustomEvent: (eventName: string, value: Record<string, any>) => {
|
|
594
683
|
if (createCustomEventChunk) {
|
|
595
684
|
pendingEvents.push(
|
|
@@ -600,7 +689,7 @@ export async function* executeToolCalls(
|
|
|
600
689
|
)
|
|
601
690
|
}
|
|
602
691
|
},
|
|
603
|
-
}
|
|
692
|
+
} as ToolExecutionContext<TContext>
|
|
604
693
|
|
|
605
694
|
// CASE 1: Client-side tool (no execute function)
|
|
606
695
|
if (!tool.execute) {
|
|
@@ -615,11 +704,14 @@ export async function* executeToolCalls(
|
|
|
615
704
|
if (approved) {
|
|
616
705
|
// Approved - check if client has executed
|
|
617
706
|
if (clientResults.has(toolCall.id)) {
|
|
618
|
-
results.push(
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
707
|
+
results.push(
|
|
708
|
+
buildClientToolResult(
|
|
709
|
+
toolCall.id,
|
|
710
|
+
toolName,
|
|
711
|
+
tool,
|
|
712
|
+
clientResults.get(toolCall.id),
|
|
713
|
+
),
|
|
714
|
+
)
|
|
623
715
|
} else {
|
|
624
716
|
// Approved but not executed yet - request client execution
|
|
625
717
|
needsClientExecution.push({
|
|
@@ -649,11 +741,14 @@ export async function* executeToolCalls(
|
|
|
649
741
|
} else {
|
|
650
742
|
// No approval needed - check if client has executed
|
|
651
743
|
if (clientResults.has(toolCall.id)) {
|
|
652
|
-
results.push(
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
744
|
+
results.push(
|
|
745
|
+
buildClientToolResult(
|
|
746
|
+
toolCall.id,
|
|
747
|
+
toolName,
|
|
748
|
+
tool,
|
|
749
|
+
clientResults.get(toolCall.id),
|
|
750
|
+
),
|
|
751
|
+
)
|
|
657
752
|
} else {
|
|
658
753
|
// Request client execution
|
|
659
754
|
needsClientExecution.push({
|