@tanstack/ai 0.28.0 → 0.31.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.
Files changed (67) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +10 -0
  2. package/dist/esm/activities/chat/adapter.js +1 -0
  3. package/dist/esm/activities/chat/adapter.js.map +1 -1
  4. package/dist/esm/activities/chat/index.d.ts +3 -2
  5. package/dist/esm/activities/chat/index.js +23 -11
  6. package/dist/esm/activities/chat/index.js.map +1 -1
  7. package/dist/esm/activities/chat/messages.js +1 -1
  8. package/dist/esm/activities/chat/messages.js.map +1 -1
  9. package/dist/esm/activities/chat/middleware/builder.d.ts +46 -0
  10. package/dist/esm/activities/chat/middleware/builder.js +17 -0
  11. package/dist/esm/activities/chat/middleware/builder.js.map +1 -0
  12. package/dist/esm/activities/chat/middleware/capabilities.d.ts +93 -0
  13. package/dist/esm/activities/chat/middleware/capabilities.js +45 -0
  14. package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -0
  15. package/dist/esm/activities/chat/middleware/compose.d.ts +10 -0
  16. package/dist/esm/activities/chat/middleware/compose.js +47 -0
  17. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  18. package/dist/esm/activities/chat/middleware/define.d.ts +20 -0
  19. package/dist/esm/activities/chat/middleware/define.js +7 -0
  20. package/dist/esm/activities/chat/middleware/define.js.map +1 -0
  21. package/dist/esm/activities/chat/middleware/index.d.ts +8 -0
  22. package/dist/esm/activities/chat/middleware/types.d.ts +51 -0
  23. package/dist/esm/activities/chat/middleware/validate.d.ts +19 -0
  24. package/dist/esm/activities/chat/middleware/validate.js +30 -0
  25. package/dist/esm/activities/chat/middleware/validate.js.map +1 -0
  26. package/dist/esm/activities/chat/stream/message-updaters.js +1 -1
  27. package/dist/esm/activities/chat/stream/message-updaters.js.map +1 -1
  28. package/dist/esm/activities/chat/stream/processor.d.ts +6 -0
  29. package/dist/esm/activities/chat/stream/processor.js +18 -3
  30. package/dist/esm/activities/chat/stream/processor.js.map +1 -1
  31. package/dist/esm/activities/generateVideo/index.d.ts +2 -1
  32. package/dist/esm/activities/generateVideo/index.js +12 -2
  33. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  34. package/dist/esm/index.d.ts +2 -0
  35. package/dist/esm/index.js +6 -0
  36. package/dist/esm/index.js.map +1 -1
  37. package/dist/esm/logger/console-logger.d.ts +18 -0
  38. package/dist/esm/logger/console-logger.js +64 -8
  39. package/dist/esm/logger/console-logger.js.map +1 -1
  40. package/dist/esm/logger/types.d.ts +4 -4
  41. package/dist/esm/middlewares/otel.js +40 -12
  42. package/dist/esm/middlewares/otel.js.map +1 -1
  43. package/dist/esm/realtime/index.d.ts +1 -3
  44. package/dist/esm/realtime/index.js.map +1 -1
  45. package/dist/esm/types.d.ts +7 -1
  46. package/package.json +10 -2
  47. package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +2 -2
  48. package/skills/ai-core/media-generation/SKILL.md +29 -1
  49. package/src/activities/chat/adapter.ts +11 -0
  50. package/src/activities/chat/index.ts +34 -14
  51. package/src/activities/chat/messages.ts +1 -0
  52. package/src/activities/chat/middleware/builder.ts +109 -0
  53. package/src/activities/chat/middleware/capabilities.ts +162 -0
  54. package/src/activities/chat/middleware/compose.ts +51 -0
  55. package/src/activities/chat/middleware/define.ts +34 -0
  56. package/src/activities/chat/middleware/index.ts +20 -0
  57. package/src/activities/chat/middleware/types.ts +60 -0
  58. package/src/activities/chat/middleware/validate.ts +55 -0
  59. package/src/activities/chat/stream/message-updaters.ts +1 -1
  60. package/src/activities/chat/stream/processor.ts +30 -3
  61. package/src/activities/generateVideo/index.ts +12 -0
  62. package/src/index.ts +14 -0
  63. package/src/logger/console-logger.ts +112 -17
  64. package/src/logger/types.ts +4 -4
  65. package/src/middlewares/otel.ts +57 -12
  66. package/src/realtime/index.ts +1 -3
  67. package/src/types.ts +7 -0
@@ -0,0 +1,162 @@
1
+ /** Options accepted by a capability getter. */
2
+ export interface CapabilityGetOptions {
3
+ /** When true, return undefined instead of throwing if the capability is absent. */
4
+ optional?: boolean
5
+ }
6
+
7
+ /**
8
+ * The minimal context shape a capability accessor needs. The full
9
+ * `ChatMiddlewareContext` satisfies this (it has `capabilities`), so accessors
10
+ * accept any middleware context without referencing `any`.
11
+ */
12
+ export interface CapabilityContext {
13
+ capabilities: CapabilityRegistry
14
+ }
15
+
16
+ /** Reads a capability value off a context. Overloaded so the flag narrows the return. */
17
+ export interface CapabilityGetter<TValue> {
18
+ (ctx: CapabilityContext): TValue
19
+ (ctx: CapabilityContext, opts: { optional: true }): TValue | undefined
20
+ }
21
+
22
+ /** Writes a capability value onto a context. */
23
+ export type CapabilityProvider<TValue> = (
24
+ ctx: CapabilityContext,
25
+ value: TValue,
26
+ ) => void
27
+
28
+ /**
29
+ * A capability handle. It is BOTH a `[get, provide]` tuple (array-destructurable)
30
+ * AND the identity used in middleware `requires`/`provides` declarations.
31
+ *
32
+ * Runtime identity is this object's reference. The `capabilityName` literal is
33
+ * used for diagnostics and COMPILE-TIME tracking only — capability names MUST be
34
+ * unique across an app or the type-level coverage check conflates them.
35
+ */
36
+ export type Capability<
37
+ TValue = unknown,
38
+ TName extends string = string,
39
+ > = readonly [
40
+ get: CapabilityGetter<TValue>,
41
+ provide: CapabilityProvider<TValue>,
42
+ ] & {
43
+ readonly capabilityName: TName
44
+ /** @internal Presence check for the post-setup assertion. */
45
+ has: (ctx: CapabilityContext) => boolean
46
+ }
47
+
48
+ /**
49
+ * A capability handle with permissive value/name — for use as a constraint in
50
+ * `requires`/`provides` arrays. Concentrates `any` in one named alias (same
51
+ * convention as `AnyTextAdapter`/`AnyTool`); needed so `Capability<SpecificT>`
52
+ * is assignable to the handle-array element type.
53
+ */
54
+ export type CapabilityHandle = Capability<any, string>
55
+
56
+ /**
57
+ * Per-request bookkeeping: which capabilities were provided, plus the
58
+ * duplicate-provide notification. Capability VALUES live in per-capability
59
+ * WeakMaps (see `createCapability`), not here — this only tracks presence.
60
+ */
61
+ export class CapabilityRegistry {
62
+ private readonly provided = new Set<CapabilityHandle>()
63
+ private onDuplicate?: (name: string) => void
64
+
65
+ /** Register a callback fired when a handle is provided more than once. */
66
+ setOnDuplicate(cb: (name: string) => void): void {
67
+ this.onDuplicate = cb
68
+ }
69
+
70
+ /** Record that `handle` was provided; fire the duplicate callback on repeats. */
71
+ markProvided(handle: CapabilityHandle): void {
72
+ if (this.provided.has(handle)) this.onDuplicate?.(handle.capabilityName)
73
+ this.provided.add(handle)
74
+ }
75
+
76
+ has(handle: CapabilityHandle): boolean {
77
+ return this.provided.has(handle)
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Create a capability. Returns a hybrid handle that destructures to
83
+ * `[get, provide]` and is itself the identity for `requires`/`provides`.
84
+ *
85
+ * Curried so the value type is supplied explicitly while the name literal is
86
+ * INFERRED from the argument: `createCapability<T>()('name')`. (A single call
87
+ * `createCapability<T>('name')` cannot work — supplying `T` explicitly stops
88
+ * TypeScript inferring the name, collapsing it to `string` and defeating the
89
+ * compile-time coverage check that keys on the literal name.)
90
+ *
91
+ * @example Provider + consumer middleware
92
+ * ```ts
93
+ * const counterCapability = createCapability<{ value: number }>()('counter')
94
+ * const [getCounter, provideCounter] = counterCapability
95
+ *
96
+ * const withCounter = defineChatMiddleware({
97
+ * name: 'counter',
98
+ * provides: [counterCapability],
99
+ * setup(ctx) { provideCounter(ctx, { value: 0 }) },
100
+ * })
101
+ *
102
+ * const readsCounter = defineChatMiddleware({
103
+ * name: 'reads-counter',
104
+ * requires: [counterCapability],
105
+ * onChunk(ctx) { getCounter(ctx).value++ },
106
+ * })
107
+ *
108
+ * chat({ adapter, messages, middleware: [withCounter, readsCounter] })
109
+ * ```
110
+ *
111
+ * @remarks Capability `name`s must be unique across your app: compile-time
112
+ * coverage tracking keys on the name literal (runtime keys on reference).
113
+ */
114
+ export function createCapability<TValue = unknown>(): <
115
+ const TName extends string,
116
+ >(
117
+ name: TName,
118
+ ) => Capability<TValue, TName> {
119
+ return <const TName extends string>(
120
+ name: TName,
121
+ ): Capability<TValue, TName> => {
122
+ // Each capability owns a typed WeakMap keyed by the context object. Because
123
+ // the value type is TValue, reads are typed with no assertion.
124
+ const values = new WeakMap<CapabilityContext, TValue>()
125
+
126
+ function get(ctx: CapabilityContext): TValue
127
+ function get(
128
+ ctx: CapabilityContext,
129
+ opts: { optional: true },
130
+ ): TValue | undefined
131
+ function get(
132
+ ctx: CapabilityContext,
133
+ opts?: CapabilityGetOptions,
134
+ ): TValue | undefined {
135
+ if (!values.has(ctx)) {
136
+ if (opts?.optional) return undefined
137
+ throw new Error(
138
+ `Capability "${name}" was requested but never provided. Ensure a ` +
139
+ `middleware provides it in setup(), ordered before this consumer.`,
140
+ )
141
+ }
142
+ return values.get(ctx)
143
+ }
144
+
145
+ const provide: CapabilityProvider<TValue> = (ctx, value) => {
146
+ values.set(ctx, value)
147
+ ctx.capabilities.markProvided(handle)
148
+ }
149
+
150
+ const pair: readonly [
151
+ CapabilityGetter<TValue>,
152
+ CapabilityProvider<TValue>,
153
+ ] = [get, provide]
154
+ // Object.assign's return type is the intersection of the tuple and the
155
+ // props, which IS Capability<TValue, TName> — no cast needed.
156
+ const handle = Object.assign(pair, {
157
+ capabilityName: name,
158
+ has: (ctx: CapabilityContext) => values.has(ctx),
159
+ })
160
+ return handle
161
+ }
162
+ }
@@ -163,6 +163,57 @@ export class MiddlewareRunner<TContext = unknown> {
163
163
  return current
164
164
  }
165
165
 
166
+ /**
167
+ * Run all `setup` hooks in array order, then assert every declared `provides`
168
+ * capability was actually provided. Wires the last-wins duplicate-provide
169
+ * warning into the registry. Runs before init `onConfig`.
170
+ *
171
+ * Takes the full `ChatMiddlewareContext` — the same stable context the engine
172
+ * threads through every other hook — because it both forwards `ctx` to each
173
+ * `setup` hook and emits instrumentation events from it.
174
+ */
175
+ async runSetup(ctx: ChatMiddlewareContext<TContext>): Promise<void> {
176
+ ctx.capabilities.setOnDuplicate((name) => {
177
+ this.logger.warn(
178
+ `capability "${name}" was provided more than once; last provider wins`,
179
+ { capability: name },
180
+ )
181
+ })
182
+
183
+ for (const mw of this.middlewares) {
184
+ if (mw.setup) {
185
+ const skip = shouldSkipInstrumentation(mw)
186
+ const start = Date.now()
187
+ await mw.setup(ctx)
188
+ if (!skip) {
189
+ this.logger.middleware(
190
+ `hook=setup middleware=${mw.name ?? 'unnamed'}`,
191
+ { middleware: mw.name ?? 'unnamed', hook: 'setup' },
192
+ )
193
+ aiEventClient.emit('middleware:hook:executed', {
194
+ ...instrumentCtx(ctx),
195
+ middlewareName: mw.name || 'unnamed',
196
+ hookName: 'setup',
197
+ iteration: ctx.iteration,
198
+ duration: Date.now() - start,
199
+ hasTransform: false,
200
+ })
201
+ }
202
+ }
203
+ }
204
+
205
+ for (const mw of this.middlewares) {
206
+ for (const handle of mw.provides ?? []) {
207
+ if (!ctx.capabilities.has(handle)) {
208
+ throw new Error(
209
+ `Middleware "${mw.name ?? 'unnamed'}" declares it provides ` +
210
+ `"${handle.capabilityName}" but never called provide() in setup().`,
211
+ )
212
+ }
213
+ }
214
+ }
215
+ }
216
+
166
217
  /**
167
218
  * Call onStart on all middleware in order.
168
219
  */
@@ -0,0 +1,34 @@
1
+ import type { CapabilityHandle } from './capabilities'
2
+ import type { ChatMiddleware } from './types'
3
+
4
+ /**
5
+ * A middleware whose `requires`/`provides` tuple types are captured precisely
6
+ * (via `const` inference) for the array coverage check and the builder.
7
+ */
8
+ export interface DefinedChatMiddleware<
9
+ TContext,
10
+ TRequires extends ReadonlyArray<CapabilityHandle>,
11
+ TProvides extends ReadonlyArray<CapabilityHandle>,
12
+ > extends ChatMiddleware<TContext> {
13
+ requires?: TRequires
14
+ provides?: TProvides
15
+ }
16
+
17
+ /**
18
+ * Identity helper for authoring middleware with precise capability inference.
19
+ * Returns the middleware unchanged at runtime; only sharpens its type so the
20
+ * `chat()` array coverage check and `createChatMiddleware` builder can read the
21
+ * exact `requires`/`provides`.
22
+ */
23
+ export function defineChatMiddleware<
24
+ TContext = unknown,
25
+ const TRequires extends ReadonlyArray<CapabilityHandle> = readonly [],
26
+ const TProvides extends ReadonlyArray<CapabilityHandle> = readonly [],
27
+ >(
28
+ middleware: ChatMiddleware<TContext> & {
29
+ requires?: TRequires
30
+ provides?: TProvides
31
+ },
32
+ ): DefinedChatMiddleware<TContext, TRequires, TProvides> {
33
+ return middleware
34
+ }
@@ -16,3 +16,23 @@ export type {
16
16
  } from './types'
17
17
 
18
18
  export { MiddlewareRunner } from './compose'
19
+
20
+ export { createCapability, CapabilityRegistry } from './capabilities'
21
+ export type {
22
+ Capability,
23
+ CapabilityHandle,
24
+ CapabilityContext,
25
+ CapabilityGetter,
26
+ CapabilityProvider,
27
+ CapabilityGetOptions,
28
+ } from './capabilities'
29
+ export { defineChatMiddleware } from './define'
30
+ export type { DefinedChatMiddleware } from './define'
31
+ export { createChatMiddleware } from './builder'
32
+ export type {
33
+ ChatMiddlewareBuilder,
34
+ MissingCapabilities,
35
+ NamesOf,
36
+ } from './builder'
37
+ export { validateCapabilities } from './validate'
38
+ export type { AnyChatMiddleware } from './types'
@@ -7,6 +7,11 @@ import type {
7
7
  ToolCall,
8
8
  } from '../../../types'
9
9
  import type { SystemPrompt } from '../../../system-prompts'
10
+ import type {
11
+ Capability,
12
+ CapabilityHandle,
13
+ CapabilityRegistry,
14
+ } from './capabilities'
10
15
 
11
16
  // ===========================
12
17
  // Middleware Context
@@ -115,6 +120,28 @@ export interface ChatMiddlewareContext<TContext = unknown> {
115
120
  messages: ReadonlyArray<ModelMessage>
116
121
  /** Generate a unique ID with the given prefix */
117
122
  createId: (prefix: string) => string
123
+ /**
124
+ * Capability bookkeeping for this request. Populated by middleware `setup`
125
+ * hooks (via `provide` accessors) and read by later middleware (via `get`
126
+ * accessors). Prefer the accessors returned by `createCapability` over using
127
+ * this directly. Orthogonal to `context` (the user runtime context).
128
+ */
129
+ capabilities: CapabilityRegistry
130
+ /**
131
+ * Read a provided capability by its handle. Equivalent to the handle's own
132
+ * `get` accessor (`getX(ctx)`); throws if the capability was never provided.
133
+ */
134
+ get: <TValue>(capability: Capability<TValue>) => TValue
135
+ /**
136
+ * Read a capability by its handle, returning `undefined` if it was never
137
+ * provided (never throws).
138
+ */
139
+ getOptional: <TValue>(capability: Capability<TValue>) => TValue | undefined
140
+ /**
141
+ * Provide a capability value. Equivalent to the handle's own `provide`
142
+ * accessor (`provideX(ctx, value)`). Typically called from `setup`.
143
+ */
144
+ provide: <TValue>(capability: Capability<TValue>, value: TValue) => void
118
145
  }
119
146
 
120
147
  // ===========================
@@ -344,6 +371,36 @@ export interface ChatMiddleware<TContext = unknown> {
344
371
  /** Optional name for debugging and identification */
345
372
  name?: string
346
373
 
374
+ /**
375
+ * Capabilities this middleware requires. `chat()` validates that some
376
+ * middleware (or the adapter) provides each one; unsatisfied requirements are
377
+ * a compile-time error (array coverage / builder) and a runtime error before
378
+ * the adapter runs.
379
+ */
380
+ requires?: ReadonlyArray<CapabilityHandle>
381
+
382
+ /**
383
+ * Capabilities this middleware provides. Each declared capability MUST be
384
+ * provided (via its `provide` accessor) inside `setup`, or `chat()` throws
385
+ * after the setup phase.
386
+ */
387
+ provides?: ReadonlyArray<CapabilityHandle>
388
+
389
+ /**
390
+ * Capabilities this middleware uses if present but does not require.
391
+ * Non-gating: never causes a validation error. Read with
392
+ * `getX(ctx, { optional: true })`.
393
+ */
394
+ optionalRequires?: ReadonlyArray<CapabilityHandle>
395
+
396
+ /**
397
+ * Provisioning hook. Runs FIRST — before `onConfig` (init) — across all
398
+ * middleware in array order. Use it to call `provide` accessors so later
399
+ * middleware (`onConfig` onward) can consume the capabilities. Receives the
400
+ * stable context; does NOT receive the mutable config.
401
+ */
402
+ setup?: (ctx: ChatMiddlewareContext<TContext>) => void | Promise<void>
403
+
347
404
  /**
348
405
  * Called to observe or transform the chat configuration.
349
406
  * Called at init and at the beginning of each agent iteration.
@@ -476,3 +533,6 @@ export interface ChatMiddleware<TContext = unknown> {
476
533
  info: ErrorInfo,
477
534
  ) => void | Promise<void>
478
535
  }
536
+
537
+ /** A `ChatMiddleware` with a permissive context — for use as a constraint. */
538
+ export type AnyChatMiddleware = ChatMiddleware<any>
@@ -0,0 +1,55 @@
1
+ import type { CapabilityHandle } from './capabilities'
2
+ import type { AnyChatMiddleware } from './types'
3
+
4
+ /** Minimal adapter shape needed for capability validation. */
5
+ interface CapabilityRequiringAdapter {
6
+ name: string
7
+ requires?: ReadonlyArray<CapabilityHandle>
8
+ }
9
+
10
+ /**
11
+ * Runtime validation: every required capability (from middleware `requires` and
12
+ * the adapter's `requires`) must be provided by some middleware's `provides`.
13
+ * `optionalRequires` is never gating. Throws a clear error otherwise.
14
+ *
15
+ * Presence only — ORDER is not validated here (a provider may appear after its
16
+ * consumer in the array and still pass). Use the `createChatMiddleware()`
17
+ * builder for compile-time order enforcement; at runtime, a consumer that reads
18
+ * a not-yet-provided capability during `setup` fails loud via its getter.
19
+ */
20
+ export function validateCapabilities(
21
+ middlewares: ReadonlyArray<AnyChatMiddleware>,
22
+ adapter: CapabilityRequiringAdapter,
23
+ ): void {
24
+ const provided = new Set<CapabilityHandle>()
25
+ for (const mw of middlewares) {
26
+ for (const handle of mw.provides ?? []) provided.add(handle)
27
+ }
28
+
29
+ const providedNames = (): string => {
30
+ const names = [...provided].map((h) => h.capabilityName)
31
+ return names.length ? names.join(', ') : 'none'
32
+ }
33
+
34
+ for (const handle of adapter.requires ?? []) {
35
+ if (!provided.has(handle)) {
36
+ throw new Error(
37
+ `Adapter "${adapter.name}" requires capability "${handle.capabilityName}". ` +
38
+ `Provided capabilities: ${providedNames()}. ` +
39
+ `Add a middleware that provides "${handle.capabilityName}".`,
40
+ )
41
+ }
42
+ }
43
+
44
+ for (const mw of middlewares) {
45
+ for (const handle of mw.requires ?? []) {
46
+ if (!provided.has(handle)) {
47
+ throw new Error(
48
+ `Middleware "${mw.name ?? 'unnamed'}" requires capability ` +
49
+ `"${handle.capabilityName}". Provided capabilities: ${providedNames()}. ` +
50
+ `Add a middleware that provides "${handle.capabilityName}".`,
51
+ )
52
+ }
53
+ }
54
+ }
55
+ }
@@ -226,7 +226,7 @@ export function updateToolCallWithOutput(
226
226
  parts[index] = {
227
227
  ...toolCallPart,
228
228
  output: errorText ? { error: errorText } : output,
229
- state: state ?? (errorText ? 'input-complete' : 'complete'),
229
+ state: state ?? (errorText ? 'error' : 'complete'),
230
230
  }
231
231
  }
232
232
 
@@ -317,7 +317,7 @@ export class StreamProcessor {
317
317
  this.messages,
318
318
  toolCallId,
319
319
  output,
320
- error ? 'input-complete' : undefined,
320
+ error ? 'error' : undefined,
321
321
  error,
322
322
  )
323
323
 
@@ -1184,7 +1184,7 @@ export class StreamProcessor {
1184
1184
  this.messages,
1185
1185
  chunk.toolCallId,
1186
1186
  output,
1187
- chunk.state === 'output-error' ? 'input-complete' : undefined,
1187
+ chunk.state === 'output-error' ? 'error' : undefined,
1188
1188
  )
1189
1189
 
1190
1190
  // Step 2: Create/update the tool-result part (for LLM conversation history)
@@ -1240,7 +1240,7 @@ export class StreamProcessor {
1240
1240
  this.messages,
1241
1241
  chunk.toolCallId,
1242
1242
  output,
1243
- chunk.state === 'output-error' ? 'input-complete' : undefined,
1243
+ chunk.state === 'output-error' ? 'error' : undefined,
1244
1244
  )
1245
1245
 
1246
1246
  // Step 2: Create/update the tool-result part
@@ -1690,11 +1690,22 @@ export class StreamProcessor {
1690
1690
  _index: number,
1691
1691
  toolCall: InternalToolCallState,
1692
1692
  ): void {
1693
+ // Finalize the internal bookkeeping: the call's input arguments ARE
1694
+ // complete regardless of whether execution later failed, so the call still
1695
+ // counts as a completed tool call in getCompletedToolCalls()/getState().
1693
1696
  toolCall.state = 'input-complete'
1694
1697
 
1695
1698
  // Try final parse
1696
1699
  toolCall.parsedArguments = this.jsonParser.parse(toolCall.arguments)
1697
1700
 
1701
+ // Don't downgrade the rendered part of a call that already reached the
1702
+ // terminal 'error' state (e.g. an output-error TOOL_CALL_RESULT arrived
1703
+ // without a preceding TOOL_CALL_END). The RUN_FINISHED / finalizeStream
1704
+ // safety net must not clobber a failed call back to 'input-complete'.
1705
+ if (this.isToolCallPartErrored(toolCall.id)) {
1706
+ return
1707
+ }
1708
+
1698
1709
  // Update UIMessage
1699
1710
  this.messages = updateToolCallPart(this.messages, messageId, {
1700
1711
  id: toolCall.id,
@@ -1714,6 +1725,22 @@ export class StreamProcessor {
1714
1725
  )
1715
1726
  }
1716
1727
 
1728
+ /**
1729
+ * Whether the rendered tool-call part for the given id has reached the
1730
+ * terminal 'error' state. Used to prevent the completion safety net from
1731
+ * downgrading a failed call back to 'input-complete'.
1732
+ */
1733
+ private isToolCallPartErrored(toolCallId: string): boolean {
1734
+ return this.messages.some((msg) =>
1735
+ msg.parts.some(
1736
+ (part) =>
1737
+ part.type === 'tool-call' &&
1738
+ part.id === toolCallId &&
1739
+ part.state === 'error',
1740
+ ),
1741
+ )
1742
+ }
1743
+
1717
1744
  /**
1718
1745
  * Emit pending text update for a specific message.
1719
1746
  *
@@ -15,6 +15,7 @@ import type { DebugOption } from '../../logger/types'
15
15
  import type { VideoAdapter } from './adapter'
16
16
  import type {
17
17
  StreamChunk,
18
+ TokenUsage,
18
19
  VideoJobResult,
19
20
  VideoStatusResult,
20
21
  VideoUrlResult,
@@ -380,6 +381,7 @@ async function* runStreamingVideoGeneration<
380
381
  status: 'completed',
381
382
  url: urlResult.url,
382
383
  expiresAt: urlResult.expiresAt,
384
+ ...(urlResult.usage ? { usage: urlResult.usage } : {}),
383
385
  },
384
386
  timestamp: Date.now(),
385
387
  } as StreamChunk
@@ -454,6 +456,7 @@ export async function getVideoJobStatus<
454
456
  progress?: number
455
457
  url?: string
456
458
  error?: string
459
+ usage?: TokenUsage
457
460
  }> {
458
461
  const { adapter, jobId } = options
459
462
  const requestId = createId('video-status')
@@ -487,10 +490,19 @@ export async function getVideoJobStatus<
487
490
  duration: Date.now() - startTime,
488
491
  timestamp: Date.now(),
489
492
  })
493
+ if (urlResult.usage) {
494
+ aiEventClient.emit('video:usage', {
495
+ requestId,
496
+ model: adapter.model,
497
+ usage: urlResult.usage,
498
+ timestamp: Date.now(),
499
+ })
500
+ }
490
501
  return {
491
502
  status: statusResult.status,
492
503
  progress: statusResult.progress,
493
504
  url: urlResult.url,
505
+ ...(urlResult.usage ? { usage: urlResult.usage } : {}),
494
506
  }
495
507
  } catch (error) {
496
508
  const errorMessage =
package/src/index.ts CHANGED
@@ -118,6 +118,20 @@ export type {
118
118
  ErrorInfo,
119
119
  } from './activities/chat/middleware/index'
120
120
 
121
+ // Capability primitives + middleware builder
122
+ export {
123
+ createCapability,
124
+ defineChatMiddleware,
125
+ createChatMiddleware,
126
+ } from './activities/chat/middleware/index'
127
+ export type {
128
+ Capability,
129
+ CapabilityHandle,
130
+ CapabilityContext,
131
+ CapabilityGetter,
132
+ CapabilityProvider,
133
+ } from './activities/chat/middleware/index'
134
+
121
135
  // All types
122
136
  export * from './types'
123
137