@tanstack/ai 0.50.0 → 0.52.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 +94 -23
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/compose.js +4 -2
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/index.d.ts +2 -0
- package/dist/esm/activities/chat/middleware/metadata.d.ts +16 -0
- package/dist/esm/activities/chat/middleware/metadata.js +8 -0
- package/dist/esm/activities/chat/middleware/metadata.js.map +1 -0
- package/dist/esm/activities/chat/middleware/types.d.ts +9 -0
- package/dist/esm/activities/chat/middleware/types.js.map +1 -1
- package/dist/esm/activities/chat/stream/processor.d.ts +2 -0
- package/dist/esm/activities/chat/stream/processor.js +39 -12
- package/dist/esm/activities/chat/stream/processor.js.map +1 -1
- package/dist/esm/activities/chat/tools/schema-converter.js +21 -10
- package/dist/esm/activities/chat/tools/schema-converter.js.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +3 -1
- package/dist/esm/utilities/errors.d.ts +27 -0
- package/dist/esm/utilities/errors.js +19 -1
- package/dist/esm/utilities/errors.js.map +1 -1
- package/package.json +2 -2
- package/skills/ai-core/SKILL.md +17 -0
- package/src/activities/chat/index.ts +139 -44
- package/src/activities/chat/middleware/compose.ts +14 -2
- package/src/activities/chat/middleware/index.ts +3 -0
- package/src/activities/chat/middleware/metadata.ts +20 -0
- package/src/activities/chat/middleware/types.ts +9 -0
- package/src/activities/chat/stream/processor.ts +51 -6
- package/src/activities/chat/tools/schema-converter.ts +30 -17
- package/src/index.ts +6 -0
- package/src/utilities/errors.ts +42 -0
|
@@ -788,6 +788,7 @@ class TextEngine<
|
|
|
788
788
|
private readonly effectiveSignal?: AbortSignal
|
|
789
789
|
|
|
790
790
|
private messages: Array<ModelMessage>
|
|
791
|
+
private providerMessages: Array<ModelMessage>
|
|
791
792
|
private iterationCount = 0
|
|
792
793
|
/** Cumulative tool calls counted in this run (emitted + pending resume). */
|
|
793
794
|
private toolCallCount = 0
|
|
@@ -845,6 +846,9 @@ class TextEngine<
|
|
|
845
846
|
>
|
|
846
847
|
private readonly middlewareCtx: ChatMiddlewareContext<TContext>
|
|
847
848
|
private readonly sandboxFileQueue: Array<StreamChunk> = []
|
|
849
|
+
private readonly middlewareCustomQueue: Array<StreamChunk> = []
|
|
850
|
+
private middlewareCustomWaiters: Array<() => void> = []
|
|
851
|
+
private drainingMiddlewareCustom = false
|
|
848
852
|
private readonly deferredPromises: Array<Promise<unknown>> = []
|
|
849
853
|
private abortReason?: string
|
|
850
854
|
private readonly middlewareAbortController?: AbortController
|
|
@@ -935,6 +939,7 @@ class TextEngine<
|
|
|
935
939
|
// Convert messages to ModelMessage format (handles both UIMessage and ModelMessage input)
|
|
936
940
|
// This ensures consistent internal format regardless of what the client sends
|
|
937
941
|
this.messages = convertMessagesToModelMessages(config.params.messages)
|
|
942
|
+
this.providerMessages = this.messages
|
|
938
943
|
|
|
939
944
|
// Initialize lazy tool manager after messages are converted (needs message history for scanning)
|
|
940
945
|
assertUniqueToolNames(config.params.tools || [])
|
|
@@ -993,6 +998,14 @@ class TextEngine<
|
|
|
993
998
|
this.abortReason = reason
|
|
994
999
|
this.middlewareAbortController?.abort(reason)
|
|
995
1000
|
},
|
|
1001
|
+
emitCustomEvent: (name, value) => {
|
|
1002
|
+
this.middlewareCustomQueue.push(
|
|
1003
|
+
this.createCustomEventChunk(name, value),
|
|
1004
|
+
)
|
|
1005
|
+
const waiters = this.middlewareCustomWaiters
|
|
1006
|
+
this.middlewareCustomWaiters = []
|
|
1007
|
+
for (const waiter of waiters) waiter()
|
|
1008
|
+
},
|
|
996
1009
|
context: config.context as TContext,
|
|
997
1010
|
defer: (promise: Promise<unknown>) => {
|
|
998
1011
|
this.deferredPromises.push(promise)
|
|
@@ -1128,21 +1141,24 @@ class TextEngine<
|
|
|
1128
1141
|
|
|
1129
1142
|
try {
|
|
1130
1143
|
// Provision capabilities before any consumer (onConfig onward) can read them
|
|
1131
|
-
|
|
1144
|
+
yield* this.runWhileYielding(
|
|
1145
|
+
this.middlewareRunner.runSetup(this.middlewareCtx),
|
|
1146
|
+
)
|
|
1132
1147
|
|
|
1133
1148
|
// Run initial onConfig (phase = init)
|
|
1134
1149
|
this.middlewareCtx.phase = 'init'
|
|
1135
1150
|
const initialConfig = this.buildMiddlewareConfig()
|
|
1136
|
-
const transformedConfig =
|
|
1137
|
-
this.middlewareCtx,
|
|
1138
|
-
initialConfig,
|
|
1151
|
+
const transformedConfig = yield* this.runWhileYielding(
|
|
1152
|
+
this.middlewareRunner.runOnConfig(this.middlewareCtx, initialConfig),
|
|
1139
1153
|
)
|
|
1140
1154
|
this.applyMiddlewareConfig(transformedConfig)
|
|
1141
1155
|
await this.applyEphemeralInterruptResume(transformedConfig)
|
|
1142
1156
|
await this.applyDurableGenericInterruptResolution()
|
|
1143
1157
|
|
|
1144
1158
|
// Run onStart (devtools middleware emits text:request:started and initial messages here)
|
|
1145
|
-
|
|
1159
|
+
yield* this.runWhileYielding(
|
|
1160
|
+
this.middlewareRunner.runOnStart(this.middlewareCtx),
|
|
1161
|
+
)
|
|
1146
1162
|
|
|
1147
1163
|
if (this.earlyTermination) {
|
|
1148
1164
|
yield* this.emitSuccessfulEarlyTermination()
|
|
@@ -1189,18 +1205,16 @@ class TextEngine<
|
|
|
1189
1205
|
iteration: this.middlewareCtx.iteration,
|
|
1190
1206
|
})
|
|
1191
1207
|
|
|
1192
|
-
|
|
1208
|
+
yield* this.runWhileYielding(this.beginCycle())
|
|
1193
1209
|
|
|
1194
1210
|
if (this.cyclePhase === 'processText') {
|
|
1195
1211
|
// Run onConfig before each model call (phase = beforeModel)
|
|
1196
1212
|
this.middlewareCtx.phase = 'beforeModel'
|
|
1197
1213
|
this.middlewareCtx.iteration = this.iterationCount
|
|
1198
1214
|
const iterConfig = this.buildMiddlewareConfig()
|
|
1199
|
-
const iterTransformedConfig =
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
iterConfig,
|
|
1203
|
-
)
|
|
1215
|
+
const iterTransformedConfig = yield* this.runWhileYielding(
|
|
1216
|
+
this.middlewareRunner.runOnConfig(this.middlewareCtx, iterConfig),
|
|
1217
|
+
)
|
|
1204
1218
|
this.applyMiddlewareConfig(iterTransformedConfig)
|
|
1205
1219
|
|
|
1206
1220
|
if (
|
|
@@ -1239,7 +1253,7 @@ class TextEngine<
|
|
|
1239
1253
|
}
|
|
1240
1254
|
|
|
1241
1255
|
this.endCycle()
|
|
1242
|
-
} while (
|
|
1256
|
+
} while (yield* this.runWhileYielding(this.shouldContinue()))
|
|
1243
1257
|
}
|
|
1244
1258
|
|
|
1245
1259
|
this.logger.agentLoop('run finished', {
|
|
@@ -1497,7 +1511,7 @@ class TextEngine<
|
|
|
1497
1511
|
|
|
1498
1512
|
for await (const raw of this.adapter.chatStream({
|
|
1499
1513
|
model: this.params.model,
|
|
1500
|
-
messages: this.
|
|
1514
|
+
messages: this.providerMessages,
|
|
1501
1515
|
tools: toolsWithJsonSchemas,
|
|
1502
1516
|
metadata,
|
|
1503
1517
|
request: this.effectiveRequest,
|
|
@@ -1632,6 +1646,7 @@ class TextEngine<
|
|
|
1632
1646
|
continue
|
|
1633
1647
|
}
|
|
1634
1648
|
if (spec.type === EventType.RUN_STARTED) {
|
|
1649
|
+
if (this.hasPublicRunStarted) continue
|
|
1635
1650
|
this.hasPublicRunStarted = true
|
|
1636
1651
|
}
|
|
1637
1652
|
this.logger.output(`type=${spec.type}`, { chunk: spec })
|
|
@@ -1646,6 +1661,7 @@ class TextEngine<
|
|
|
1646
1661
|
|
|
1647
1662
|
// Drain any sandbox.file events emitted while processing this chunk.
|
|
1648
1663
|
yield* this.drainSandboxFileQueue()
|
|
1664
|
+
yield* this.drainMiddlewareCustomQueue()
|
|
1649
1665
|
|
|
1650
1666
|
if (this.earlyTermination) {
|
|
1651
1667
|
break
|
|
@@ -1654,6 +1670,7 @@ class TextEngine<
|
|
|
1654
1670
|
|
|
1655
1671
|
// Drain any remaining sandbox.file events emitted after the stream ended.
|
|
1656
1672
|
yield* this.drainSandboxFileQueue()
|
|
1673
|
+
yield* this.drainMiddlewareCustomQueue()
|
|
1657
1674
|
}
|
|
1658
1675
|
|
|
1659
1676
|
private handleStreamChunk(chunk: AdapterYieldChunk): void {
|
|
@@ -2047,12 +2064,14 @@ class TextEngine<
|
|
|
2047
2064
|
const allResults = [...executionResult.results, ...deferredErrorResults]
|
|
2048
2065
|
|
|
2049
2066
|
// Notify middleware of tool phase completion (devtools emits aggregate events here)
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2067
|
+
yield* this.runWhileYielding(
|
|
2068
|
+
this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
2069
|
+
toolCalls: pendingToolCalls,
|
|
2070
|
+
results: allResults,
|
|
2071
|
+
needsApproval: executionResult.needsApproval,
|
|
2072
|
+
needsClientExecution: executionResult.needsClientExecution,
|
|
2073
|
+
}),
|
|
2074
|
+
)
|
|
2056
2075
|
|
|
2057
2076
|
if (
|
|
2058
2077
|
executionResult.needsApproval.length > 0 ||
|
|
@@ -2228,23 +2247,26 @@ class TextEngine<
|
|
|
2228
2247
|
const allResults = [...executionResult.results, ...deferredErrorResults]
|
|
2229
2248
|
|
|
2230
2249
|
// Notify middleware of tool phase completion (devtools emits aggregate events here)
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2250
|
+
yield* this.runWhileYielding(
|
|
2251
|
+
this.middlewareRunner.runOnToolPhaseComplete(this.middlewareCtx, {
|
|
2252
|
+
toolCalls,
|
|
2253
|
+
results: allResults,
|
|
2254
|
+
needsApproval: executionResult.needsApproval,
|
|
2255
|
+
needsClientExecution: executionResult.needsClientExecution,
|
|
2256
|
+
}),
|
|
2257
|
+
)
|
|
2237
2258
|
|
|
2238
2259
|
const afterToolBoundaryChunks = this.buildToolResultChunks(
|
|
2239
2260
|
allResults,
|
|
2240
2261
|
finishEvent,
|
|
2241
2262
|
)
|
|
2242
|
-
const afterToolRequests =
|
|
2243
|
-
|
|
2263
|
+
const afterToolRequests = yield* this.runWhileYielding(
|
|
2264
|
+
this.middlewareRunner.runOnInterruptBoundary(
|
|
2244
2265
|
this.middlewareCtx as ChatMiddlewareContext<TContext> & {
|
|
2245
2266
|
phase: 'afterTools'
|
|
2246
2267
|
},
|
|
2247
|
-
)
|
|
2268
|
+
),
|
|
2269
|
+
)
|
|
2248
2270
|
if (afterToolRequests.length > 0) {
|
|
2249
2271
|
for (const chunk of afterToolBoundaryChunks) {
|
|
2250
2272
|
yield* this.pipeThroughMiddleware(chunk)
|
|
@@ -2924,10 +2946,12 @@ class TextEngine<
|
|
|
2924
2946
|
this.middlewareCtx.phase = phase
|
|
2925
2947
|
const boundaryRequests =
|
|
2926
2948
|
requests ??
|
|
2927
|
-
(
|
|
2928
|
-
this.
|
|
2929
|
-
|
|
2930
|
-
|
|
2949
|
+
(yield* this.runWhileYielding(
|
|
2950
|
+
this.middlewareRunner.runOnInterruptBoundary(
|
|
2951
|
+
this.middlewareCtx as ChatMiddlewareContext<TContext> & {
|
|
2952
|
+
phase: typeof phase
|
|
2953
|
+
},
|
|
2954
|
+
),
|
|
2931
2955
|
))
|
|
2932
2956
|
if (boundaryRequests.length === 0) return false
|
|
2933
2957
|
for (const request of boundaryRequests) {
|
|
@@ -3445,9 +3469,11 @@ class TextEngine<
|
|
|
3445
3469
|
}
|
|
3446
3470
|
|
|
3447
3471
|
// 1) onStructuredOutputConfig — middleware can transform messages, options, outputSchema
|
|
3448
|
-
structuredConfig =
|
|
3449
|
-
this.
|
|
3450
|
-
|
|
3472
|
+
structuredConfig = yield* this.runWhileYielding(
|
|
3473
|
+
this.middlewareRunner.runOnStructuredOutputConfig(
|
|
3474
|
+
this.middlewareCtx,
|
|
3475
|
+
structuredConfig,
|
|
3476
|
+
),
|
|
3451
3477
|
)
|
|
3452
3478
|
|
|
3453
3479
|
// 2) onConfig — phase-aware general-purpose middleware re-runs at the
|
|
@@ -3456,9 +3482,11 @@ class TextEngine<
|
|
|
3456
3482
|
// call — same constraint applies — but the view is consistent with the
|
|
3457
3483
|
// ChatMiddlewareConfig shape).
|
|
3458
3484
|
const { outputSchema: pinnedSchema, ...chatConfigSlice } = structuredConfig
|
|
3459
|
-
const postOnConfig =
|
|
3460
|
-
this.middlewareCtx,
|
|
3461
|
-
|
|
3485
|
+
const postOnConfig = yield* this.runWhileYielding(
|
|
3486
|
+
this.middlewareRunner.runOnConfig(this.middlewareCtx, {
|
|
3487
|
+
...chatConfigSlice,
|
|
3488
|
+
tools: baseConfig.tools,
|
|
3489
|
+
}),
|
|
3462
3490
|
)
|
|
3463
3491
|
|
|
3464
3492
|
// Apply merged config back to engine state
|
|
@@ -3470,7 +3498,7 @@ class TextEngine<
|
|
|
3470
3498
|
const structuredCallOptions = {
|
|
3471
3499
|
chatOptions: {
|
|
3472
3500
|
model: this.params.model,
|
|
3473
|
-
messages: this.
|
|
3501
|
+
messages: this.providerMessages,
|
|
3474
3502
|
metadata: postOnConfig.metadata,
|
|
3475
3503
|
modelOptions: postOnConfig.modelOptions,
|
|
3476
3504
|
systemPrompts: postOnConfig.systemPrompts,
|
|
@@ -3949,6 +3977,7 @@ class TextEngine<
|
|
|
3949
3977
|
private buildMiddlewareConfig(): ChatMiddlewareConfig {
|
|
3950
3978
|
return {
|
|
3951
3979
|
messages: this.messages,
|
|
3980
|
+
providerMessages: this.messages,
|
|
3952
3981
|
systemPrompts: [...this.systemPrompts],
|
|
3953
3982
|
tools: [...this.tools],
|
|
3954
3983
|
resume: this.params.resume,
|
|
@@ -4367,6 +4396,7 @@ class TextEngine<
|
|
|
4367
4396
|
private applyMiddlewareConfig(config: ChatMiddlewareConfig): void {
|
|
4368
4397
|
this.applyResumeToolState(config.resumeToolState)
|
|
4369
4398
|
this.messages = config.messages
|
|
4399
|
+
this.providerMessages = config.providerMessages ?? config.messages
|
|
4370
4400
|
this.systemPrompts = config.systemPrompts
|
|
4371
4401
|
assertUniqueToolNames(config.tools)
|
|
4372
4402
|
this.tools = config.tools
|
|
@@ -4399,6 +4429,7 @@ class TextEngine<
|
|
|
4399
4429
|
for (const spec of normalizeStreamChunk(output as AdapterYieldChunk)) {
|
|
4400
4430
|
restorePublicUsage(spec)
|
|
4401
4431
|
if (spec.type === EventType.RUN_STARTED) {
|
|
4432
|
+
if (this.hasPublicRunStarted) continue
|
|
4402
4433
|
this.hasPublicRunStarted = true
|
|
4403
4434
|
}
|
|
4404
4435
|
yield spec
|
|
@@ -4419,6 +4450,69 @@ class TextEngine<
|
|
|
4419
4450
|
chunk,
|
|
4420
4451
|
)
|
|
4421
4452
|
yield* this.emitPublicChunks(afterMw)
|
|
4453
|
+
if (!this.drainingMiddlewareCustom) {
|
|
4454
|
+
yield* this.drainMiddlewareCustomQueue()
|
|
4455
|
+
}
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4458
|
+
/**
|
|
4459
|
+
* Drain CUSTOM chunks pushed by `ctx.emitCustomEvent` through middleware
|
|
4460
|
+
* and into the public stream. If the run has not yet sent `RUN_STARTED`,
|
|
4461
|
+
* emit that first so CUSTOM events are not the first wire event.
|
|
4462
|
+
*/
|
|
4463
|
+
private async *drainMiddlewareCustomQueue(): AsyncGenerator<StreamChunk> {
|
|
4464
|
+
if (this.drainingMiddlewareCustom) return
|
|
4465
|
+
if (this.middlewareCustomQueue.length === 0) return
|
|
4466
|
+
this.drainingMiddlewareCustom = true
|
|
4467
|
+
try {
|
|
4468
|
+
yield* this.emitSyntheticRunStarted(this.createSyntheticFinishedEvent())
|
|
4469
|
+
while (this.middlewareCustomQueue.length > 0) {
|
|
4470
|
+
const chunk = this.middlewareCustomQueue.shift()
|
|
4471
|
+
if (chunk) yield* this.pipeThroughMiddleware(chunk)
|
|
4472
|
+
}
|
|
4473
|
+
} finally {
|
|
4474
|
+
this.drainingMiddlewareCustom = false
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
/**
|
|
4479
|
+
* Await `work` while yielding any `emitCustomEvent` chunks as they arrive.
|
|
4480
|
+
*/
|
|
4481
|
+
private async *runWhileYielding<T>(
|
|
4482
|
+
work: Promise<T>,
|
|
4483
|
+
): AsyncGenerator<StreamChunk, T> {
|
|
4484
|
+
let settled = false
|
|
4485
|
+
let result: T | undefined
|
|
4486
|
+
let error: unknown
|
|
4487
|
+
const done = work.then(
|
|
4488
|
+
(value) => {
|
|
4489
|
+
settled = true
|
|
4490
|
+
result = value
|
|
4491
|
+
},
|
|
4492
|
+
(err: unknown) => {
|
|
4493
|
+
settled = true
|
|
4494
|
+
error = err
|
|
4495
|
+
},
|
|
4496
|
+
)
|
|
4497
|
+
|
|
4498
|
+
while (!settled) {
|
|
4499
|
+
yield* this.drainMiddlewareCustomQueue()
|
|
4500
|
+
if (settled) break
|
|
4501
|
+
await Promise.race([
|
|
4502
|
+
done,
|
|
4503
|
+
new Promise<void>((resolve) => {
|
|
4504
|
+
if (this.middlewareCustomQueue.length > 0) {
|
|
4505
|
+
resolve()
|
|
4506
|
+
return
|
|
4507
|
+
}
|
|
4508
|
+
this.middlewareCustomWaiters.push(resolve)
|
|
4509
|
+
}),
|
|
4510
|
+
])
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
yield* this.drainMiddlewareCustomQueue()
|
|
4514
|
+
if (error !== undefined) throw error
|
|
4515
|
+
return result as T
|
|
4422
4516
|
}
|
|
4423
4517
|
|
|
4424
4518
|
/**
|
|
@@ -4455,17 +4549,18 @@ class TextEngine<
|
|
|
4455
4549
|
},
|
|
4456
4550
|
void
|
|
4457
4551
|
> {
|
|
4458
|
-
let
|
|
4459
|
-
while (
|
|
4552
|
+
let pending = generator.next()
|
|
4553
|
+
while (true) {
|
|
4554
|
+
const next = yield* this.runWhileYielding(pending)
|
|
4555
|
+
if (next.done) return next.value
|
|
4460
4556
|
yield* this.pipeThroughMiddleware(next.value)
|
|
4461
|
-
|
|
4557
|
+
pending = generator.next()
|
|
4462
4558
|
}
|
|
4463
|
-
return next.value
|
|
4464
4559
|
}
|
|
4465
4560
|
|
|
4466
4561
|
private createCustomEventChunk(
|
|
4467
4562
|
eventName: string,
|
|
4468
|
-
value: Record<string,
|
|
4563
|
+
value: Record<string, unknown>,
|
|
4469
4564
|
): CustomEvent {
|
|
4470
4565
|
return {
|
|
4471
4566
|
type: EventType.CUSTOM,
|
|
@@ -166,7 +166,13 @@ export class MiddlewareRunner<
|
|
|
166
166
|
const result = await mw.onConfig(ctx, current)
|
|
167
167
|
const hasTransform = result !== undefined && result !== null
|
|
168
168
|
if (hasTransform) {
|
|
169
|
-
current = {
|
|
169
|
+
current = {
|
|
170
|
+
...current,
|
|
171
|
+
...result,
|
|
172
|
+
...('messages' in result && !('providerMessages' in result)
|
|
173
|
+
? { providerMessages: result.messages }
|
|
174
|
+
: {}),
|
|
175
|
+
}
|
|
170
176
|
if (!skip) {
|
|
171
177
|
this.logger.config(
|
|
172
178
|
`middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result).join(',')}`,
|
|
@@ -221,7 +227,13 @@ export class MiddlewareRunner<
|
|
|
221
227
|
const result = await mw.onStructuredOutputConfig(ctx, current)
|
|
222
228
|
const hasTransform = result !== undefined && result !== null
|
|
223
229
|
if (hasTransform) {
|
|
224
|
-
current = {
|
|
230
|
+
current = {
|
|
231
|
+
...current,
|
|
232
|
+
...result,
|
|
233
|
+
...('messages' in result && !('providerMessages' in result)
|
|
234
|
+
? { providerMessages: result.messages }
|
|
235
|
+
: {}),
|
|
236
|
+
}
|
|
225
237
|
if (!skip) {
|
|
226
238
|
this.logger.config(
|
|
227
239
|
`middleware=${mw.name ?? 'unnamed'} keys=${Object.keys(result).join(',')}`,
|
|
@@ -67,6 +67,9 @@ export {
|
|
|
67
67
|
} from './locks'
|
|
68
68
|
export type { LockStore } from './locks'
|
|
69
69
|
|
|
70
|
+
export { MetadataCapability, getMetadata, provideMetadata } from './metadata'
|
|
71
|
+
export type { MetadataStore } from './metadata'
|
|
72
|
+
|
|
70
73
|
export {
|
|
71
74
|
isRunStatus,
|
|
72
75
|
isTerminalRunStatus,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createCapability } from './capabilities'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Namespaced key/value store for app and middleware metadata.
|
|
5
|
+
*
|
|
6
|
+
* `(namespace, key)` is the composite identity. Keep both values separate;
|
|
7
|
+
* joining them with a delimiter can create collisions.
|
|
8
|
+
*/
|
|
9
|
+
export interface MetadataStore {
|
|
10
|
+
/** Return the value for `(namespace, key)`, or `null` when it is absent. */
|
|
11
|
+
get: (namespace: string, key: string) => Promise<unknown | null>
|
|
12
|
+
/** Insert or replace the value for `(namespace, key)`. */
|
|
13
|
+
set: (namespace: string, key: string, value: unknown) => Promise<void>
|
|
14
|
+
/** Delete `(namespace, key)`. Do nothing when it is absent. */
|
|
15
|
+
delete: (namespace: string, key: string) => Promise<void>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const MetadataCapability = createCapability<MetadataStore>()('metadata')
|
|
19
|
+
|
|
20
|
+
export const [getMetadata, provideMetadata] = MetadataCapability
|
|
@@ -213,6 +213,12 @@ export interface ChatMiddlewareContext<TContext = unknown> {
|
|
|
213
213
|
signal?: AbortSignal
|
|
214
214
|
/** Abort the chat run with a reason */
|
|
215
215
|
abort: (reason?: string) => void
|
|
216
|
+
/**
|
|
217
|
+
* Push a `CUSTOM` chunk onto the chat stream immediately.
|
|
218
|
+
* The engine yields it as soon as it can (including while `onConfig`
|
|
219
|
+
* is still awaiting work such as a summarize call).
|
|
220
|
+
*/
|
|
221
|
+
emitCustomEvent: (name: string, value: Record<string, any>) => void
|
|
216
222
|
/** Runtime context provided by chat() options */
|
|
217
223
|
context: TContext
|
|
218
224
|
/**
|
|
@@ -305,7 +311,10 @@ export interface ChatMiddlewareContext<TContext = unknown> {
|
|
|
305
311
|
* that middleware is allowed to modify.
|
|
306
312
|
*/
|
|
307
313
|
export interface ChatMiddlewareConfig {
|
|
314
|
+
/** Canonical conversation history. Middleware and persistence read this. */
|
|
308
315
|
messages: Array<ModelMessage>
|
|
316
|
+
/** Provider-only context. Defaults to `messages` when it is not set. */
|
|
317
|
+
providerMessages?: Array<ModelMessage> | undefined
|
|
309
318
|
systemPrompts: Array<SystemPrompt>
|
|
310
319
|
tools: Array<Tool>
|
|
311
320
|
resume?: Array<RunAgentResumeItem> | undefined
|
|
@@ -212,6 +212,7 @@ export class StreamProcessor {
|
|
|
212
212
|
private finishReason: string | null = null
|
|
213
213
|
private hasError = false
|
|
214
214
|
private isDone = false
|
|
215
|
+
private streamEndEmitted = false
|
|
215
216
|
|
|
216
217
|
// Recording
|
|
217
218
|
private recording: ChunkRecording | null = null
|
|
@@ -729,9 +730,26 @@ export class StreamProcessor {
|
|
|
729
730
|
return id
|
|
730
731
|
}
|
|
731
732
|
}
|
|
733
|
+
// finalizeStream() clears activeMessageIds but keeps messageStates.
|
|
734
|
+
// Leftover reasoning after an early RUN_FINISHED must resume that
|
|
735
|
+
// assistant. A new user turn calls prepareAssistantMessage(), which
|
|
736
|
+
// clears messageStates first.
|
|
737
|
+
for (const [id, state] of [...this.messageStates].reverse()) {
|
|
738
|
+
if (state.role === 'assistant') {
|
|
739
|
+
return id
|
|
740
|
+
}
|
|
741
|
+
}
|
|
732
742
|
return null
|
|
733
743
|
}
|
|
734
744
|
|
|
745
|
+
private resumeAssistantState(id: string, state: MessageStreamState): void {
|
|
746
|
+
this.activeMessageIds.add(id)
|
|
747
|
+
if (state.isComplete || this.isDone) {
|
|
748
|
+
state.isComplete = false
|
|
749
|
+
this.isDone = false
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
735
753
|
/**
|
|
736
754
|
* Ensure an active assistant message exists, creating one if needed.
|
|
737
755
|
* Used for backward compat when events arrive without prior TEXT_MESSAGE_START.
|
|
@@ -748,14 +766,20 @@ export class StreamProcessor {
|
|
|
748
766
|
// Try to find state by preferred ID
|
|
749
767
|
if (preferredId) {
|
|
750
768
|
const state = this.getMessageState(preferredId)
|
|
751
|
-
if (state)
|
|
769
|
+
if (state) {
|
|
770
|
+
this.resumeAssistantState(preferredId, state)
|
|
771
|
+
return { messageId: preferredId, state }
|
|
772
|
+
}
|
|
752
773
|
}
|
|
753
774
|
|
|
754
775
|
// Try active assistant message
|
|
755
776
|
const activeId = this.getActiveAssistantMessageId()
|
|
756
777
|
if (activeId) {
|
|
757
778
|
const state = this.getMessageState(activeId)
|
|
758
|
-
if (state)
|
|
779
|
+
if (state) {
|
|
780
|
+
this.resumeAssistantState(activeId, state)
|
|
781
|
+
return { messageId: activeId, state }
|
|
782
|
+
}
|
|
759
783
|
}
|
|
760
784
|
|
|
761
785
|
// Check if a message with preferredId already exists (reconnect/resume case).
|
|
@@ -900,9 +924,21 @@ export class StreamProcessor {
|
|
|
900
924
|
}
|
|
901
925
|
|
|
902
926
|
// Ensure state exists
|
|
903
|
-
|
|
904
|
-
|
|
927
|
+
let pendingState = this.messageStates.get(messageId)
|
|
928
|
+
if (!pendingState) {
|
|
929
|
+
pendingState = this.createMessageState(messageId, uiRole)
|
|
905
930
|
this.activeMessageIds.add(messageId)
|
|
931
|
+
} else if (pendingState.hasToolCallsSinceTextStart) {
|
|
932
|
+
// A tool call (e.g. TOOL_CALL_START with parentMessageId) marked
|
|
933
|
+
// this message before its "real" TEXT_MESSAGE_START arrived — same
|
|
934
|
+
// reset Case 2 performs, so the segment accumulator doesn't carry
|
|
935
|
+
// stale tool-call state into the text that follows.
|
|
936
|
+
if (pendingState.currentSegmentText !== pendingState.lastEmittedText) {
|
|
937
|
+
this.emitTextUpdateForMessage(messageId)
|
|
938
|
+
}
|
|
939
|
+
pendingState.currentSegmentText = ''
|
|
940
|
+
pendingState.lastEmittedText = ''
|
|
941
|
+
pendingState.hasToolCallsSinceTextStart = false
|
|
906
942
|
}
|
|
907
943
|
|
|
908
944
|
this.mergeMessageMetadata(messageId, chunk.metadata)
|
|
@@ -1647,8 +1683,14 @@ export class StreamProcessor {
|
|
|
1647
1683
|
}
|
|
1648
1684
|
|
|
1649
1685
|
if (this.activeRuns.size === 0) {
|
|
1650
|
-
this.isDone = true
|
|
1651
1686
|
this.completeAllToolCalls()
|
|
1687
|
+
const isIntermediateToolTurn =
|
|
1688
|
+
this.finishReason === 'tool_calls' &&
|
|
1689
|
+
chunk.outcome?.type !== 'interrupt'
|
|
1690
|
+
if (isIntermediateToolTurn) {
|
|
1691
|
+
return
|
|
1692
|
+
}
|
|
1693
|
+
this.isDone = true
|
|
1652
1694
|
this.finalizeStream()
|
|
1653
1695
|
}
|
|
1654
1696
|
}
|
|
@@ -2344,6 +2386,7 @@ export class StreamProcessor {
|
|
|
2344
2386
|
* @see docs/chat-architecture.md#single-shot-text-response — Finalization step
|
|
2345
2387
|
*/
|
|
2346
2388
|
finalizeStream(): void {
|
|
2389
|
+
this.isDone = true
|
|
2347
2390
|
let lastAssistantMessage: UIMessage | undefined
|
|
2348
2391
|
|
|
2349
2392
|
// Finalize ALL active messages
|
|
@@ -2407,7 +2450,8 @@ export class StreamProcessor {
|
|
|
2407
2450
|
}
|
|
2408
2451
|
|
|
2409
2452
|
// Emit stream end for the last assistant message
|
|
2410
|
-
if (lastAssistantMessage) {
|
|
2453
|
+
if (lastAssistantMessage && !this.streamEndEmitted) {
|
|
2454
|
+
this.streamEndEmitted = true
|
|
2411
2455
|
this.events.onStreamEnd?.(lastAssistantMessage)
|
|
2412
2456
|
}
|
|
2413
2457
|
}
|
|
@@ -2526,6 +2570,7 @@ export class StreamProcessor {
|
|
|
2526
2570
|
this.finishReason = null
|
|
2527
2571
|
this.hasError = false
|
|
2528
2572
|
this.isDone = false
|
|
2573
|
+
this.streamEndEmitted = false
|
|
2529
2574
|
this.chunkStrategy.reset?.()
|
|
2530
2575
|
}
|
|
2531
2576
|
|
|
@@ -99,6 +99,26 @@ function pruneMap(map: NullWideningMap): NullWideningMap | undefined {
|
|
|
99
99
|
return Object.keys(map).length > 0 ? map : undefined
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function coerceArrayItems(items: JSONSchema | Array<JSONSchema>): {
|
|
103
|
+
schema: JSONSchema | Array<JSONSchema>
|
|
104
|
+
itemMap: NullWideningMap | Array<NullWideningMap> | undefined
|
|
105
|
+
} {
|
|
106
|
+
if (Array.isArray(items)) {
|
|
107
|
+
const nested = items.map((item) =>
|
|
108
|
+
makeStructuredOutputCompatible(item, item.required || []),
|
|
109
|
+
)
|
|
110
|
+
const itemMaps = nested.map((entry) => entry.nullWidening ?? {})
|
|
111
|
+
return {
|
|
112
|
+
schema: nested.map((entry) => entry.schema),
|
|
113
|
+
itemMap: itemMaps.some((entry) => Object.keys(entry).length > 0)
|
|
114
|
+
? itemMaps
|
|
115
|
+
: undefined,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const nested = makeStructuredOutputCompatible(items, items.required || [])
|
|
119
|
+
return { schema: nested.schema, itemMap: nested.nullWidening }
|
|
120
|
+
}
|
|
121
|
+
|
|
102
122
|
/**
|
|
103
123
|
* Transform a JSON schema to be compatible with OpenAI's structured output requirements.
|
|
104
124
|
* OpenAI requires:
|
|
@@ -146,18 +166,15 @@ function makeStructuredOutputCompatible(
|
|
|
146
166
|
widenedHere = wasOptional
|
|
147
167
|
childMap = nested.nullWidening
|
|
148
168
|
} else if (prop.type === 'array' && prop.items) {
|
|
149
|
-
const
|
|
150
|
-
const nestedItems = items
|
|
151
|
-
? makeStructuredOutputCompatible(items, items.required || [])
|
|
152
|
-
: undefined
|
|
169
|
+
const nestedItems = coerceArrayItems(prop.items)
|
|
153
170
|
properties[propName] = {
|
|
154
171
|
...prop,
|
|
155
|
-
items: nestedItems
|
|
172
|
+
items: nestedItems.schema,
|
|
156
173
|
...(wasOptional ? { type: ['array', 'null'] } : {}),
|
|
157
174
|
}
|
|
158
175
|
widenedHere = wasOptional
|
|
159
|
-
childMap = nestedItems
|
|
160
|
-
? { items: nestedItems.
|
|
176
|
+
childMap = nestedItems.itemMap
|
|
177
|
+
? { items: nestedItems.itemMap }
|
|
161
178
|
: undefined
|
|
162
179
|
} else if (wasOptional) {
|
|
163
180
|
// Make optional fields nullable by adding null to the type. Mark
|
|
@@ -188,17 +205,13 @@ function makeStructuredOutputCompatible(
|
|
|
188
205
|
if (Object.keys(propertyMaps).length > 0) map.properties = propertyMaps
|
|
189
206
|
}
|
|
190
207
|
|
|
191
|
-
// Handle array
|
|
208
|
+
// Handle array item schemas. A tuple (`items: [a, b, …]`) keeps every
|
|
209
|
+
// position. A homogeneous schema stays a single items map so
|
|
210
|
+
// `undoNullWidening` applies it to every element.
|
|
192
211
|
if (result.type === 'array' && result.items) {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
items,
|
|
197
|
-
items.required || [],
|
|
198
|
-
)
|
|
199
|
-
result.items = nestedItems.schema
|
|
200
|
-
if (nestedItems.nullWidening) map.items = nestedItems.nullWidening
|
|
201
|
-
}
|
|
212
|
+
const nestedItems = coerceArrayItems(result.items)
|
|
213
|
+
result.items = nestedItems.schema
|
|
214
|
+
if (nestedItems.itemMap) map.items = nestedItems.itemMap
|
|
202
215
|
}
|
|
203
216
|
|
|
204
217
|
return { schema: result, nullWidening: pruneMap(map) }
|
package/src/index.ts
CHANGED
|
@@ -104,6 +104,8 @@ export type {
|
|
|
104
104
|
// MCP error classes (value exports — usable with instanceof)
|
|
105
105
|
export { MCPDuplicateToolNameError } from './activities/chat/mcp/manager'
|
|
106
106
|
export { DuplicateToolNameError } from './activities/chat/tools/unique-tool-names'
|
|
107
|
+
export { SkillLimitError } from './utilities/errors'
|
|
108
|
+
export type { SkillLimitErrorInit } from './utilities/errors'
|
|
107
109
|
|
|
108
110
|
// Schema conversion (Standard JSON Schema compliant)
|
|
109
111
|
export {
|
|
@@ -281,6 +283,9 @@ export {
|
|
|
281
283
|
createCapability,
|
|
282
284
|
defineChatMiddleware,
|
|
283
285
|
createChatMiddleware,
|
|
286
|
+
MetadataCapability,
|
|
287
|
+
getMetadata,
|
|
288
|
+
provideMetadata,
|
|
284
289
|
} from './activities/chat/middleware/index'
|
|
285
290
|
export type {
|
|
286
291
|
Capability,
|
|
@@ -290,6 +295,7 @@ export type {
|
|
|
290
295
|
CapabilityProvider,
|
|
291
296
|
DefinedChatMiddleware,
|
|
292
297
|
AnyChatMiddleware,
|
|
298
|
+
MetadataStore,
|
|
293
299
|
} from './activities/chat/middleware/index'
|
|
294
300
|
// Locks are a distributed-mutex primitive — coordination, not chat state — and
|
|
295
301
|
// live behind their own subpath: `@tanstack/ai/locks` (see ./locks.ts).
|