@tanstack/ai 0.32.0 → 0.34.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.js +47 -20
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +7 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.d.ts +25 -1
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +26 -2
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.d.ts +13 -0
- package/dist/esm/activities/chat/tools/schema-converter.js +61 -33
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +7 -0
- package/dist/esm/activities/generateAudio/index.js +26 -1
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +7 -0
- package/dist/esm/activities/generateImage/index.js +26 -1
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +7 -0
- package/dist/esm/activities/generateSpeech/index.js +26 -1
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +7 -0
- package/dist/esm/activities/generateTranscription/index.js +26 -1
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +9 -0
- package/dist/esm/activities/generateVideo/index.js +52 -2
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/middleware/index.d.ts +2 -0
- package/dist/esm/activities/middleware/run.d.ts +20 -0
- package/dist/esm/activities/middleware/run.js +42 -0
- package/dist/esm/activities/middleware/run.js.map +1 -0
- package/dist/esm/activities/middleware/types.d.ts +118 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/middlewares/otel.d.ts +8 -2
- package/dist/esm/middlewares/otel.js +145 -95
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.d.ts +24 -0
- package/dist/esm/middlewares/usage-attributes.js +43 -0
- package/dist/esm/middlewares/usage-attributes.js.map +1 -0
- package/dist/esm/types.d.ts +7 -7
- package/dist/esm/utilities/errors.d.ts +13 -0
- package/dist/esm/utilities/errors.js +22 -0
- package/dist/esm/utilities/errors.js.map +1 -0
- package/dist/esm/utilities/numbers.d.ts +8 -0
- package/dist/esm/utilities/numbers.js +12 -0
- package/dist/esm/utilities/numbers.js.map +1 -0
- package/package.json +3 -2
- package/src/activities/chat/index.ts +125 -35
- package/src/activities/chat/middleware/types.ts +7 -0
- package/src/activities/chat/tools/lazy-tool-manager.ts +46 -4
- package/src/activities/chat/tools/schema-converter.ts +146 -93
- package/src/activities/generateAudio/index.ts +42 -1
- package/src/activities/generateImage/index.ts +42 -1
- package/src/activities/generateSpeech/index.ts +42 -1
- package/src/activities/generateTranscription/index.ts +42 -1
- package/src/activities/generateVideo/index.ts +88 -2
- package/src/activities/middleware/index.ts +20 -0
- package/src/activities/middleware/run.ts +88 -0
- package/src/activities/middleware/types.ts +173 -0
- package/src/index.ts +19 -0
- package/src/middlewares/otel.ts +195 -120
- package/src/middlewares/usage-attributes.ts +65 -0
- package/src/types.ts +7 -7
- package/src/utilities/errors.ts +29 -0
- package/src/utilities/numbers.ts +15 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { devtoolsMiddleware } from '@tanstack/ai-event-client'
|
|
9
|
+
import { undoNullWidening } from '@tanstack/ai-utils'
|
|
9
10
|
import { stripToSpecMiddleware } from '../../strip-to-spec-middleware'
|
|
10
11
|
import { streamToText } from '../../stream-to-response.js'
|
|
11
12
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
executeToolCalls,
|
|
19
20
|
} from './tools/tool-calls'
|
|
20
21
|
import {
|
|
22
|
+
convertSchemaForStructuredOutput,
|
|
21
23
|
convertSchemaToJsonSchema,
|
|
22
24
|
isStandardSchema,
|
|
23
25
|
parseWithStandardSchema,
|
|
@@ -33,7 +35,11 @@ import type {
|
|
|
33
35
|
ClientToolRequest,
|
|
34
36
|
ToolResult,
|
|
35
37
|
} from './tools/tool-calls'
|
|
36
|
-
import type {
|
|
38
|
+
import type {
|
|
39
|
+
AnyTextAdapter,
|
|
40
|
+
StructuredOutputOptions,
|
|
41
|
+
StructuredOutputResult,
|
|
42
|
+
} from './adapter'
|
|
37
43
|
import type {
|
|
38
44
|
AgentLoopStrategy,
|
|
39
45
|
AnyTool,
|
|
@@ -418,11 +424,21 @@ interface TextEngineConfig<
|
|
|
418
424
|
* (used by runStreamingStructuredOutput). When false, chunks are
|
|
419
425
|
* consumed internally for middleware visibility but not yielded
|
|
420
426
|
* (used by runAgenticStructuredOutput).
|
|
421
|
-
* -
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* `
|
|
425
|
-
*
|
|
427
|
+
* - normalize: optional schema-aware transform applied to the captured
|
|
428
|
+
* structured-output object the moment it enters the engine — BEFORE it is
|
|
429
|
+
* stored, validated, or yielded. Used to undo strict-mode null-widening
|
|
430
|
+
* (`undoNullWidening`): strict schemas widen optional fields to
|
|
431
|
+
* `required` + nullable so the provider returns `null` for an absent
|
|
432
|
+
* optional, and this strips exactly those synthesized nulls while keeping
|
|
433
|
+
* the ones a `.nullable()` field genuinely allows. Applied here (not in
|
|
434
|
+
* the adapter) because the engine is the only layer holding the original
|
|
435
|
+
* schema's null-widening map, and applying it at capture fixes BOTH the
|
|
436
|
+
* streaming chunk and the Promise<T> result with one transform.
|
|
437
|
+
* - validate: optional callback invoked AFTER `normalize` and AFTER the
|
|
438
|
+
* structured-output result is captured, but BEFORE the terminal hook
|
|
439
|
+
* fires. If it throws, the engine records a `finalizationError` and fires
|
|
440
|
+
* `onError` instead of `onFinish` (per spec §7.3). On success, the
|
|
441
|
+
* returned value is stored as the validated result and retrievable via
|
|
426
442
|
* `getValidatedStructuredOutput()`. Used by `runAgenticStructuredOutput`
|
|
427
443
|
* to perform Standard Schema validation inside the engine.
|
|
428
444
|
* - nativeCombined: when true, the adapter declared
|
|
@@ -437,6 +453,7 @@ interface TextEngineConfig<
|
|
|
437
453
|
finalStructuredOutput?: {
|
|
438
454
|
jsonSchema: JSONSchema
|
|
439
455
|
yieldChunks: boolean
|
|
456
|
+
normalize?: (data: unknown) => unknown
|
|
440
457
|
validate?: (data: unknown) => unknown
|
|
441
458
|
nativeCombined?: boolean
|
|
442
459
|
}
|
|
@@ -542,8 +559,8 @@ class TextEngine<
|
|
|
542
559
|
// to carry, so the client matches it to the streaming text deltas.
|
|
543
560
|
private combinedStructuredMessageId: string | null = null
|
|
544
561
|
// Holds the validated value when `finalStructuredOutput.validate` is provided
|
|
545
|
-
// and succeeds. Distinct from `structuredOutputResult.data` (the
|
|
546
|
-
// unvalidated payload from the structured-output.complete chunk).
|
|
562
|
+
// and succeeds. Distinct from `structuredOutputResult.data` (the normalized
|
|
563
|
+
// but unvalidated payload from the structured-output.complete chunk).
|
|
547
564
|
private validatedStructuredOutput: unknown = undefined
|
|
548
565
|
private hasValidatedStructuredOutput = false
|
|
549
566
|
private finalizationError: {
|
|
@@ -554,6 +571,7 @@ class TextEngine<
|
|
|
554
571
|
private readonly finalStructuredOutput?: {
|
|
555
572
|
jsonSchema: JSONSchema
|
|
556
573
|
yieldChunks: boolean
|
|
574
|
+
normalize?: (data: unknown) => unknown
|
|
557
575
|
validate?: (data: unknown) => unknown
|
|
558
576
|
nativeCombined?: boolean
|
|
559
577
|
}
|
|
@@ -646,6 +664,7 @@ class TextEngine<
|
|
|
646
664
|
this.deferredPromises.push(promise)
|
|
647
665
|
},
|
|
648
666
|
// Provider / adapter info
|
|
667
|
+
activity: 'chat',
|
|
649
668
|
provider: config.adapter.name,
|
|
650
669
|
model: config.params.model,
|
|
651
670
|
source: 'server',
|
|
@@ -1197,6 +1216,23 @@ class TextEngine<
|
|
|
1197
1216
|
}
|
|
1198
1217
|
}
|
|
1199
1218
|
|
|
1219
|
+
/**
|
|
1220
|
+
* Tools available for execution this turn. The discovery tool is dropped
|
|
1221
|
+
* from the advertised set (`this.tools`) once every lazy tool is discovered,
|
|
1222
|
+
* but a model may still re-request discovery; this widens execution lookup
|
|
1223
|
+
* to include it so such calls don't fail with "Unknown tool". Centralised so
|
|
1224
|
+
* both execution sites (`processToolCalls` and `checkForPendingToolCalls`)
|
|
1225
|
+
* stay in sync.
|
|
1226
|
+
*/
|
|
1227
|
+
private resolveExecutableTools(
|
|
1228
|
+
toolCalls: ReadonlyArray<ToolCall>,
|
|
1229
|
+
): ReadonlyArray<AnyTool> {
|
|
1230
|
+
return this.lazyToolManager.getExecutableTools(
|
|
1231
|
+
this.tools,
|
|
1232
|
+
toolCalls.map((tc) => tc.function.name),
|
|
1233
|
+
)
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1200
1236
|
private async *checkForPendingToolCalls(): AsyncGenerator<
|
|
1201
1237
|
StreamChunk,
|
|
1202
1238
|
ToolPhaseResult,
|
|
@@ -1245,7 +1281,7 @@ class TextEngine<
|
|
|
1245
1281
|
|
|
1246
1282
|
const generator = executeToolCalls(
|
|
1247
1283
|
executablePendingCalls,
|
|
1248
|
-
this.
|
|
1284
|
+
this.resolveExecutableTools(executablePendingCalls),
|
|
1249
1285
|
approvals,
|
|
1250
1286
|
clientToolResults,
|
|
1251
1287
|
(eventName, data) => this.createCustomEventChunk(eventName, data),
|
|
@@ -1407,7 +1443,7 @@ class TextEngine<
|
|
|
1407
1443
|
|
|
1408
1444
|
const generator = executeToolCalls(
|
|
1409
1445
|
executableToolCalls,
|
|
1410
|
-
this.
|
|
1446
|
+
this.resolveExecutableTools(executableToolCalls),
|
|
1411
1447
|
approvals,
|
|
1412
1448
|
clientToolResults,
|
|
1413
1449
|
(eventName, data) => this.createCustomEventChunk(eventName, data),
|
|
@@ -2065,15 +2101,29 @@ class TextEngine<
|
|
|
2065
2101
|
// All narrowing below is via the discriminated-union `chunk.type`
|
|
2066
2102
|
// — no `as` casts.
|
|
2067
2103
|
|
|
2104
|
+
// The chunk forwarded to middleware/consumers. Replaced below only for
|
|
2105
|
+
// the structured-output.complete event, whose `object` we normalize
|
|
2106
|
+
// (un-widen) so streaming consumers see the same cleaned payload the
|
|
2107
|
+
// Promise<T> path validates and returns.
|
|
2108
|
+
let outboundChunk: StreamChunk = chunk
|
|
2109
|
+
|
|
2068
2110
|
if (
|
|
2069
2111
|
chunk.type === EventType.CUSTOM &&
|
|
2070
2112
|
chunk.name === 'structured-output.complete'
|
|
2071
2113
|
) {
|
|
2072
2114
|
const parsed = readStructuredOutputCompleteValue(chunk.value)
|
|
2073
2115
|
if (parsed) {
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2116
|
+
const object = this.finalStructuredOutput.normalize
|
|
2117
|
+
? this.finalStructuredOutput.normalize(parsed.object)
|
|
2118
|
+
: parsed.object
|
|
2119
|
+
this.structuredOutputResult = { data: object, rawText: parsed.raw }
|
|
2120
|
+
// Rewrite the outbound event so the yielded chunk carries the
|
|
2121
|
+
// normalized object (the original `chunk.value` still holds the
|
|
2122
|
+
// widened one). Preserve every other field — `raw`, `reasoning` —
|
|
2123
|
+
// by spreading the original value.
|
|
2124
|
+
const value = chunk.value
|
|
2125
|
+
if (object !== parsed.object && value && typeof value === 'object') {
|
|
2126
|
+
outboundChunk = { ...chunk, value: { ...value, object } }
|
|
2077
2127
|
}
|
|
2078
2128
|
}
|
|
2079
2129
|
}
|
|
@@ -2097,7 +2147,7 @@ class TextEngine<
|
|
|
2097
2147
|
// 7b. Pipe through middleware
|
|
2098
2148
|
const outputChunks = await this.middlewareRunner.runOnChunk(
|
|
2099
2149
|
this.middlewareCtx,
|
|
2100
|
-
|
|
2150
|
+
outboundChunk,
|
|
2101
2151
|
)
|
|
2102
2152
|
|
|
2103
2153
|
// 7c. Decide consumer visibility — only yieldChunks=true callers get them.
|
|
@@ -2254,7 +2304,14 @@ class TextEngine<
|
|
|
2254
2304
|
} else {
|
|
2255
2305
|
try {
|
|
2256
2306
|
const parsed: unknown = JSON.parse(rawText)
|
|
2257
|
-
|
|
2307
|
+
// Normalize (un-widen) before storing so the synthesized
|
|
2308
|
+
// structured-output.complete chunk and the Promise<T> result both
|
|
2309
|
+
// carry the cleaned payload. JSON.parse preserves provider nulls, so
|
|
2310
|
+
// this is where native-combined output gets its widening undone.
|
|
2311
|
+
const data = this.finalStructuredOutput.normalize
|
|
2312
|
+
? this.finalStructuredOutput.normalize(parsed)
|
|
2313
|
+
: parsed
|
|
2314
|
+
this.structuredOutputResult = { data, rawText }
|
|
2258
2315
|
} catch (err: unknown) {
|
|
2259
2316
|
const detail =
|
|
2260
2317
|
rawText.slice(0, 200) + (rawText.length > 200 ? '...' : '')
|
|
@@ -2711,18 +2768,31 @@ async function runAgenticStructuredOutput<
|
|
|
2711
2768
|
|
|
2712
2769
|
// Same strict-conversion as the streaming path (`forStructuredOutput: true`)
|
|
2713
2770
|
// so the same Zod schema produces the same JSON Schema regardless of
|
|
2714
|
-
// stream mode — Promise<T> and stream:true must not diverge here.
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2771
|
+
// stream mode — Promise<T> and stream:true must not diverge here. The same
|
|
2772
|
+
// pass also records a `nullWideningMap`: optional fields are widened to
|
|
2773
|
+
// `required` + nullable for the provider, which then returns `null` for an
|
|
2774
|
+
// absent optional — a `null` the original `.optional()` (`T | undefined`)
|
|
2775
|
+
// schema would otherwise reject. The map pinpoints exactly those synthesized
|
|
2776
|
+
// nulls so `undoNullWidening` can drop them while preserving the ones a
|
|
2777
|
+
// `.nullable()` field genuinely allows.
|
|
2778
|
+
const { jsonSchema, nullWideningMap } =
|
|
2779
|
+
convertSchemaForStructuredOutput(outputSchema)
|
|
2718
2780
|
if (!jsonSchema) {
|
|
2719
2781
|
throw new Error('Failed to convert output schema to JSON Schema')
|
|
2720
2782
|
}
|
|
2721
2783
|
|
|
2784
|
+
// Un-widening runs in the engine the moment the structured output is
|
|
2785
|
+
// captured (`finalStructuredOutput.normalize`), so it applies uniformly to
|
|
2786
|
+
// every adapter and to both stream modes — the engine is the only layer
|
|
2787
|
+
// holding the schema's `nullWideningMap`. Validation then runs on the
|
|
2788
|
+
// already-normalized data, so `validate` is a plain Standard Schema parse.
|
|
2789
|
+
const normalize = (data: unknown): unknown =>
|
|
2790
|
+
undoNullWidening(data, nullWideningMap)
|
|
2791
|
+
|
|
2722
2792
|
// Validation runs INSIDE the engine (per spec §7.3) so validation failures
|
|
2723
2793
|
// route through the engine's terminal-hook chooser as `onError`. We pass a
|
|
2724
2794
|
// `validate` callback when the schema is a Standard Schema; otherwise we
|
|
2725
|
-
// pass through the
|
|
2795
|
+
// pass through the (normalized) data and the engine returns it unchanged.
|
|
2726
2796
|
const validate = isStandardSchema(outputSchema)
|
|
2727
2797
|
? (data: unknown): unknown =>
|
|
2728
2798
|
parseWithStandardSchema<InferSchemaType<TSchema>>(outputSchema, data)
|
|
@@ -2754,6 +2824,7 @@ async function runAgenticStructuredOutput<
|
|
|
2754
2824
|
finalStructuredOutput: {
|
|
2755
2825
|
jsonSchema,
|
|
2756
2826
|
yieldChunks: false,
|
|
2827
|
+
normalize,
|
|
2757
2828
|
...(validate ? { validate } : {}),
|
|
2758
2829
|
...(nativeCombined ? { nativeCombined: true } : {}),
|
|
2759
2830
|
},
|
|
@@ -2861,7 +2932,7 @@ async function* fallbackStructuredOutputStream(
|
|
|
2861
2932
|
timestamp,
|
|
2862
2933
|
}
|
|
2863
2934
|
|
|
2864
|
-
let result:
|
|
2935
|
+
let result: StructuredOutputResult<unknown>
|
|
2865
2936
|
try {
|
|
2866
2937
|
result = await adapter.structuredOutput(options)
|
|
2867
2938
|
} catch (error) {
|
|
@@ -2917,6 +2988,12 @@ async function* fallbackStructuredOutputStream(
|
|
|
2917
2988
|
model,
|
|
2918
2989
|
timestamp,
|
|
2919
2990
|
finishReason: 'stop',
|
|
2991
|
+
// Forward adapter-reported token usage so consumers reading
|
|
2992
|
+
// `RUN_FINISHED.usage` (and the engine's `runOnUsage` middleware hook) see
|
|
2993
|
+
// it on the fallback path, mirroring the native streaming path. The
|
|
2994
|
+
// conditional spread avoids emitting `usage: undefined` for adapters that
|
|
2995
|
+
// don't report it. See #758.
|
|
2996
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
2920
2997
|
}
|
|
2921
2998
|
}
|
|
2922
2999
|
|
|
@@ -2926,17 +3003,23 @@ async function* fallbackStructuredOutputStream(
|
|
|
2926
3003
|
* RUN_STARTED/RUN_FINISHED are suppressed; the structured-output finalization
|
|
2927
3004
|
* step's pair brackets the run for the consumer.
|
|
2928
3005
|
*
|
|
2929
|
-
* Schema validation is intentionally NOT run on this path — it is
|
|
2930
|
-
* consumer's responsibility.
|
|
2931
|
-
*
|
|
2932
|
-
*
|
|
2933
|
-
* which DOES run Standard Schema validation inside the engine and routes
|
|
2934
|
-
* validation failures through `onError`. The reason for the asymmetry:
|
|
3006
|
+
* Standard Schema *validation* is intentionally NOT run on this path — it is
|
|
3007
|
+
* the consumer's responsibility. This is a deliberate asymmetry vs.
|
|
3008
|
+
* `runAgenticStructuredOutput` (Promise<T> path), which DOES validate inside
|
|
3009
|
+
* the engine and routes validation failures through `onError`. The reason:
|
|
2935
3010
|
* streaming consumers typically render partial JSON progressively (via
|
|
2936
3011
|
* `parsePartialJSON` or `useChat`'s `partial` slot) and validate downstream
|
|
2937
3012
|
* after assembly. Running validation server-side would force a hard error
|
|
2938
3013
|
* on partial-by-design payloads. See `docs/structured-outputs/overview.md`.
|
|
2939
3014
|
*
|
|
3015
|
+
* Null-widening normalization, however, IS run on both paths: the
|
|
3016
|
+
* `structured-output.complete` CUSTOM event is forwarded with its `value.object`
|
|
3017
|
+
* already un-widened (synthesized strict-mode nulls dropped, genuine
|
|
3018
|
+
* `.nullable()` nulls kept), so a consumer validating the assembled object
|
|
3019
|
+
* against the original schema doesn't choke on a `null` for an `.optional()`
|
|
3020
|
+
* field. Same `convertSchemaForStructuredOutput` pass and same
|
|
3021
|
+
* `undoNullWidening` map as the Promise<T> path — the two must not diverge.
|
|
3022
|
+
*
|
|
2940
3023
|
* Pre-flight validation (missing schema, unconvertible schema) throws
|
|
2941
3024
|
* synchronously at call time rather than as a yielded RUN_ERROR mid-stream —
|
|
2942
3025
|
* those are programmer errors, not runtime conditions.
|
|
@@ -2954,14 +3037,17 @@ function runStreamingStructuredOutput<
|
|
|
2954
3037
|
}
|
|
2955
3038
|
|
|
2956
3039
|
// forStructuredOutput strict-converts the schema once at the activity
|
|
2957
|
-
// boundary
|
|
2958
|
-
//
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
}
|
|
3040
|
+
// boundary, capturing the null-widening map so the engine can un-widen the
|
|
3041
|
+
// provider's response before it reaches the consumer. Adapters can re-convert
|
|
3042
|
+
// if their wire format diverges, but the default flow hands them a
|
|
3043
|
+
// strict-ready schema.
|
|
3044
|
+
const { jsonSchema, nullWideningMap } =
|
|
3045
|
+
convertSchemaForStructuredOutput(outputSchema)
|
|
2962
3046
|
if (!jsonSchema) {
|
|
2963
3047
|
throw new Error('Failed to convert output schema to JSON Schema')
|
|
2964
3048
|
}
|
|
3049
|
+
const normalize = (data: unknown): unknown =>
|
|
3050
|
+
undoNullWidening(data, nullWideningMap)
|
|
2965
3051
|
|
|
2966
3052
|
// The implementation generator yields the broader internal type
|
|
2967
3053
|
// (`StreamChunk | StructuredOutputCompleteEvent<T>`) so agent-loop
|
|
@@ -2972,6 +3058,7 @@ function runStreamingStructuredOutput<
|
|
|
2972
3058
|
return runStreamingStructuredOutputImpl(
|
|
2973
3059
|
options,
|
|
2974
3060
|
jsonSchema,
|
|
3061
|
+
normalize,
|
|
2975
3062
|
) as StructuredOutputStream<InferSchemaType<TSchema>>
|
|
2976
3063
|
}
|
|
2977
3064
|
|
|
@@ -2997,6 +3084,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
2997
3084
|
>(
|
|
2998
3085
|
options: TextActivityOptions<AnyTextAdapter, TSchema, true, TContext>,
|
|
2999
3086
|
jsonSchema: NonNullable<ReturnType<typeof convertSchemaToJsonSchema>>,
|
|
3087
|
+
normalize: (data: unknown) => unknown,
|
|
3000
3088
|
): StructuredOutputStreamInternal<InferSchemaType<TSchema>> {
|
|
3001
3089
|
const {
|
|
3002
3090
|
adapter,
|
|
@@ -3041,6 +3129,7 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
3041
3129
|
finalStructuredOutput: {
|
|
3042
3130
|
jsonSchema,
|
|
3043
3131
|
yieldChunks: true,
|
|
3132
|
+
normalize,
|
|
3044
3133
|
...(nativeCombined ? { nativeCombined: true } : {}),
|
|
3045
3134
|
},
|
|
3046
3135
|
},
|
|
@@ -3055,9 +3144,10 @@ async function* runStreamingStructuredOutputImpl<
|
|
|
3055
3144
|
await mcpManager.dispose()
|
|
3056
3145
|
}
|
|
3057
3146
|
|
|
3058
|
-
// Schema validation for the streaming variant remains the
|
|
3059
|
-
// responsibility — they read the CUSTOM 'structured-output.complete'
|
|
3060
|
-
// the yielded stream.
|
|
3147
|
+
// Standard Schema validation for the streaming variant remains the
|
|
3148
|
+
// consumer's responsibility — they read the CUSTOM 'structured-output.complete'
|
|
3149
|
+
// from the yielded stream. (Null-widening normalization, by contrast, already
|
|
3150
|
+
// ran inside the engine via `normalize`, so the object they read is un-widened.)
|
|
3061
3151
|
void outputSchema
|
|
3062
3152
|
}
|
|
3063
3153
|
|
|
@@ -80,6 +80,13 @@ export interface ChatMiddlewareContext<TContext = unknown> {
|
|
|
80
80
|
|
|
81
81
|
// --- Provider / adapter info (immutable for the lifetime of the request) ---
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Which activity this context describes — always `'chat'`. Present so the
|
|
85
|
+
* chat context structurally satisfies the base `GenerationMiddlewareContext`,
|
|
86
|
+
* letting an observe-only middleware authored against the base (e.g.
|
|
87
|
+
* `otelMiddleware`) run on both chat and media activities.
|
|
88
|
+
*/
|
|
89
|
+
activity: 'chat'
|
|
83
90
|
/** Provider name (e.g., 'openai', 'anthropic') */
|
|
84
91
|
provider: string
|
|
85
92
|
/** Model identifier (e.g., 'gpt-4o') */
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { convertSchemaToJsonSchema } from './schema-converter'
|
|
2
|
-
import type { Tool } from '../../../types'
|
|
2
|
+
import type { AnyTool, Tool } from '../../../types'
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Name of the synthetic tool the LLM calls to discover lazy tools.
|
|
6
|
+
*
|
|
7
|
+
* Exported so callers building custom message-compaction / history-trimming
|
|
8
|
+
* logic can reference the discovery tool by constant instead of hard-coding
|
|
9
|
+
* the string (which is an internal contract that could change).
|
|
10
|
+
*/
|
|
11
|
+
export const DISCOVERY_TOOL_NAME = '__lazy__tool__discovery__'
|
|
5
12
|
|
|
6
13
|
/**
|
|
7
14
|
* Manages lazy tool discovery for the chat agent loop.
|
|
@@ -87,6 +94,35 @@ export class LazyToolManager {
|
|
|
87
94
|
return active
|
|
88
95
|
}
|
|
89
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Returns the tools that should be available for *execution* this turn.
|
|
99
|
+
*
|
|
100
|
+
* This is the advertised set (`getActiveTools()`, passed in as `activeTools`)
|
|
101
|
+
* plus the discovery tool when a pending call references it but it is no
|
|
102
|
+
* longer advertised. Once every lazy tool has been discovered the discovery
|
|
103
|
+
* tool is dropped from the advertised set, but a model may still re-request
|
|
104
|
+
* discovery (long context / hallucination); keeping it executable lets that
|
|
105
|
+
* call return the schemas again instead of failing with "Unknown tool".
|
|
106
|
+
*
|
|
107
|
+
* The advertised set is intentionally left unchanged — only execution lookup
|
|
108
|
+
* is widened. Operates on the already-built `activeTools`: it must NOT call
|
|
109
|
+
* `getActiveTools()`, which would reset `hasNewDiscoveries` before the
|
|
110
|
+
* post-execution refresh check in the agent loop.
|
|
111
|
+
*/
|
|
112
|
+
getExecutableTools(
|
|
113
|
+
activeTools: ReadonlyArray<AnyTool>,
|
|
114
|
+
pendingToolCallNames: ReadonlyArray<string>,
|
|
115
|
+
): ReadonlyArray<AnyTool> {
|
|
116
|
+
if (
|
|
117
|
+
this.discoveryTool &&
|
|
118
|
+
pendingToolCallNames.includes(DISCOVERY_TOOL_NAME) &&
|
|
119
|
+
!activeTools.some((t) => t.name === DISCOVERY_TOOL_NAME)
|
|
120
|
+
) {
|
|
121
|
+
return [...activeTools, this.discoveryTool]
|
|
122
|
+
}
|
|
123
|
+
return activeTools
|
|
124
|
+
}
|
|
125
|
+
|
|
90
126
|
/**
|
|
91
127
|
* Returns whether new tools have been discovered since the last getActiveTools() call.
|
|
92
128
|
*/
|
|
@@ -221,8 +257,14 @@ export class LazyToolManager {
|
|
|
221
257
|
for (const name of args.toolNames) {
|
|
222
258
|
const tool = lazyToolMap.get(name)
|
|
223
259
|
if (tool) {
|
|
224
|
-
|
|
225
|
-
|
|
260
|
+
// Only flag a refresh for genuinely new discoveries. Re-requesting
|
|
261
|
+
// an already-discovered tool still returns its schema below (the
|
|
262
|
+
// model asked for it), but must not trigger a redundant tool-list
|
|
263
|
+
// refresh + continue in the agent loop.
|
|
264
|
+
if (!manager.discoveredTools.has(name)) {
|
|
265
|
+
manager.discoveredTools.add(name)
|
|
266
|
+
manager.hasNewDiscoveries = true
|
|
267
|
+
}
|
|
226
268
|
const jsonSchema = tool.inputSchema
|
|
227
269
|
? convertSchemaToJsonSchema(tool.inputSchema)
|
|
228
270
|
: undefined
|