@tanstack/ai 0.29.0 → 0.32.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/adapter.d.ts +10 -0
- package/dist/esm/activities/chat/adapter.js +1 -0
- package/dist/esm/activities/chat/adapter.js.map +1 -1
- package/dist/esm/activities/chat/index.d.ts +3 -2
- package/dist/esm/activities/chat/index.js +13 -1
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/builder.d.ts +46 -0
- package/dist/esm/activities/chat/middleware/builder.js +17 -0
- package/dist/esm/activities/chat/middleware/builder.js.map +1 -0
- package/dist/esm/activities/chat/middleware/capabilities.d.ts +93 -0
- package/dist/esm/activities/chat/middleware/capabilities.js +45 -0
- package/dist/esm/activities/chat/middleware/capabilities.js.map +1 -0
- package/dist/esm/activities/chat/middleware/compose.d.ts +10 -0
- package/dist/esm/activities/chat/middleware/compose.js +47 -0
- package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
- package/dist/esm/activities/chat/middleware/define.d.ts +20 -0
- package/dist/esm/activities/chat/middleware/define.js +7 -0
- package/dist/esm/activities/chat/middleware/define.js.map +1 -0
- package/dist/esm/activities/chat/middleware/index.d.ts +8 -0
- package/dist/esm/activities/chat/middleware/types.d.ts +51 -0
- package/dist/esm/activities/chat/middleware/validate.d.ts +19 -0
- package/dist/esm/activities/chat/middleware/validate.js +30 -0
- package/dist/esm/activities/chat/middleware/validate.js.map +1 -0
- package/dist/esm/activities/generateImage/adapter.d.ts +8 -4
- package/dist/esm/activities/generateImage/adapter.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +19 -3
- package/dist/esm/activities/generateImage/index.js +12 -1
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/adapter.d.ts +65 -6
- package/dist/esm/activities/generateVideo/adapter.js +14 -0
- package/dist/esm/activities/generateVideo/adapter.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +31 -5
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/snap.d.ts +14 -0
- package/dist/esm/activities/generateVideo/snap.js +54 -0
- package/dist/esm/activities/generateVideo/snap.js.map +1 -0
- package/dist/esm/activities/index.d.ts +3 -2
- package/dist/esm/activities/index.js +2 -0
- package/dist/esm/activities/index.js.map +1 -1
- package/dist/esm/client.d.ts +1 -1
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/middlewares/otel.js +40 -12
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +96 -7
- package/dist/esm/utilities/media-prompt.d.ts +35 -0
- package/dist/esm/utilities/media-prompt.js +43 -0
- package/dist/esm/utilities/media-prompt.js.map +1 -0
- package/package.json +10 -2
- package/skills/ai-core/media-generation/SKILL.md +173 -3
- package/src/activities/chat/adapter.ts +11 -0
- package/src/activities/chat/index.ts +20 -1
- package/src/activities/chat/middleware/builder.ts +109 -0
- package/src/activities/chat/middleware/capabilities.ts +162 -0
- package/src/activities/chat/middleware/compose.ts +51 -0
- package/src/activities/chat/middleware/define.ts +34 -0
- package/src/activities/chat/middleware/index.ts +20 -0
- package/src/activities/chat/middleware/types.ts +60 -0
- package/src/activities/chat/middleware/validate.ts +55 -0
- package/src/activities/generateImage/adapter.ts +16 -3
- package/src/activities/generateImage/index.ts +48 -4
- package/src/activities/generateVideo/adapter.ts +80 -4
- package/src/activities/generateVideo/index.ts +53 -4
- package/src/activities/generateVideo/snap.ts +100 -0
- package/src/activities/index.ts +4 -0
- package/src/client.ts +4 -0
- package/src/index.ts +18 -0
- package/src/middlewares/otel.ts +57 -12
- package/src/types.ts +119 -6
- package/src/utilities/media-prompt.ts +86 -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
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ImageGenerationOptions,
|
|
3
|
+
ImageGenerationResult,
|
|
4
|
+
ModelInputModalitiesByName,
|
|
5
|
+
} from '../../types'
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Resolve the size type for a model from the model-size map.
|
|
@@ -29,6 +33,8 @@ export interface ImageAdapterConfig {
|
|
|
29
33
|
* - TProviderOptions: Base provider-specific options (already resolved)
|
|
30
34
|
* - TModelProviderOptionsByName: Map from model name to its specific provider options
|
|
31
35
|
* - TModelSizeByName: Map from model name to its supported sizes
|
|
36
|
+
* - TModelInputModalitiesByName: Map from model name to the non-text prompt
|
|
37
|
+
* modalities it accepts (constrains the `prompt` part types at compile time)
|
|
32
38
|
*/
|
|
33
39
|
export interface ImageAdapter<
|
|
34
40
|
TModel extends string = string,
|
|
@@ -38,6 +44,8 @@ export interface ImageAdapter<
|
|
|
38
44
|
string,
|
|
39
45
|
string
|
|
40
46
|
>,
|
|
47
|
+
TModelInputModalitiesByName extends ModelInputModalitiesByName =
|
|
48
|
+
ModelInputModalitiesByName,
|
|
41
49
|
> {
|
|
42
50
|
/** Discriminator for adapter kind - used by generate() to determine API shape */
|
|
43
51
|
readonly kind: 'image'
|
|
@@ -53,6 +61,7 @@ export interface ImageAdapter<
|
|
|
53
61
|
providerOptions: TProviderOptions
|
|
54
62
|
modelProviderOptionsByName: TModelProviderOptionsByName
|
|
55
63
|
modelSizeByName: TModelSizeByName
|
|
64
|
+
modelInputModalitiesByName: TModelInputModalitiesByName
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
/**
|
|
@@ -67,7 +76,7 @@ export interface ImageAdapter<
|
|
|
67
76
|
* An ImageAdapter with any/unknown type parameters.
|
|
68
77
|
* Useful as a constraint in generic functions and interfaces.
|
|
69
78
|
*/
|
|
70
|
-
export type AnyImageAdapter = ImageAdapter<any, any, any, any>
|
|
79
|
+
export type AnyImageAdapter = ImageAdapter<any, any, any, any, any>
|
|
71
80
|
|
|
72
81
|
/**
|
|
73
82
|
* Abstract base class for image generation adapters.
|
|
@@ -83,11 +92,14 @@ export abstract class BaseImageAdapter<
|
|
|
83
92
|
string,
|
|
84
93
|
string
|
|
85
94
|
>,
|
|
95
|
+
TModelInputModalitiesByName extends ModelInputModalitiesByName =
|
|
96
|
+
ModelInputModalitiesByName,
|
|
86
97
|
> implements ImageAdapter<
|
|
87
98
|
TModel,
|
|
88
99
|
TProviderOptions,
|
|
89
100
|
TModelProviderOptionsByName,
|
|
90
|
-
TModelSizeByName
|
|
101
|
+
TModelSizeByName,
|
|
102
|
+
TModelInputModalitiesByName
|
|
91
103
|
> {
|
|
92
104
|
readonly kind = 'image' as const
|
|
93
105
|
abstract readonly name: string
|
|
@@ -98,6 +110,7 @@ export abstract class BaseImageAdapter<
|
|
|
98
110
|
providerOptions: TProviderOptions
|
|
99
111
|
modelProviderOptionsByName: TModelProviderOptionsByName
|
|
100
112
|
modelSizeByName: TModelSizeByName
|
|
113
|
+
modelInputModalitiesByName: TModelInputModalitiesByName
|
|
101
114
|
}
|
|
102
115
|
|
|
103
116
|
protected config: ImageAdapterConfig
|
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
9
9
|
import { streamGenerationResult } from '../stream-generation-result.js'
|
|
10
10
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
11
|
+
import { resolveMediaPrompt } from '../../utilities/media-prompt'
|
|
11
12
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
12
13
|
import type { DebugOption } from '../../logger/types'
|
|
13
14
|
import type { ImageAdapter } from './adapter'
|
|
14
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
ImageGenerationResult,
|
|
17
|
+
MediaPrompt,
|
|
18
|
+
MediaPromptFor,
|
|
19
|
+
StreamChunk,
|
|
20
|
+
} from '../../types'
|
|
15
21
|
|
|
16
22
|
// ===========================
|
|
17
23
|
// Activity Kind
|
|
@@ -55,6 +61,23 @@ export type ImageSizeForModel<TAdapter, TModel extends string> =
|
|
|
55
61
|
: string
|
|
56
62
|
: string
|
|
57
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Extract the prompt type a model accepts from an ImageAdapter via ~types.
|
|
66
|
+
* Adapters declare a per-model input-modality map; models in the map get a
|
|
67
|
+
* `prompt` narrowed to text + their supported part types (text-only models
|
|
68
|
+
* accept `string | Array<TextPart>`), so unsupported media parts fail at
|
|
69
|
+
* compile time. Adapters without a map fall back to the full MediaPrompt.
|
|
70
|
+
*/
|
|
71
|
+
export type ImagePromptForModel<TAdapter, TModel extends string> =
|
|
72
|
+
TAdapter extends ImageAdapter<any, any, any, any, infer ModsByName>
|
|
73
|
+
? string extends keyof ModsByName
|
|
74
|
+
? // No explicit map - accept the full union
|
|
75
|
+
MediaPrompt
|
|
76
|
+
: TModel extends keyof ModsByName
|
|
77
|
+
? MediaPromptFor<ModsByName[TModel][number]>
|
|
78
|
+
: MediaPrompt
|
|
79
|
+
: MediaPrompt
|
|
80
|
+
|
|
58
81
|
// ===========================
|
|
59
82
|
// Activity Options Type
|
|
60
83
|
// ===========================
|
|
@@ -72,8 +95,16 @@ export type ImageActivityOptions<
|
|
|
72
95
|
> = {
|
|
73
96
|
/** The image adapter to use (must be created with a model) */
|
|
74
97
|
adapter: TAdapter & { kind: typeof kind }
|
|
75
|
-
/**
|
|
76
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Description of the desired image(s). Either a plain string, or — for
|
|
100
|
+
* models that support image-conditioned generation — an ordered array of
|
|
101
|
+
* content parts interleaving text with image inputs (image-to-image,
|
|
102
|
+
* reference-guided, edit, multi-reference). Media parts may carry
|
|
103
|
+
* `metadata.role` (`'reference' | 'mask' | 'control' | 'character'`) to
|
|
104
|
+
* disambiguate intent. The accepted part types are narrowed per model via
|
|
105
|
+
* the adapter's input-modality map.
|
|
106
|
+
*/
|
|
107
|
+
prompt: ImagePromptForModel<TAdapter, TAdapter['model']>
|
|
77
108
|
/** Number of images to generate (default: 1) */
|
|
78
109
|
numberOfImages?: number
|
|
79
110
|
/** Image size in WIDTHxHEIGHT format (e.g., "1024x1024") */
|
|
@@ -203,13 +234,26 @@ async function runGenerateImage<
|
|
|
203
234
|
const startTime = Date.now()
|
|
204
235
|
const logger: InternalLogger = resolveDebugOption(options.debug)
|
|
205
236
|
|
|
237
|
+
// Devtools events carry the flattened prompt text plus media-part counts —
|
|
238
|
+
// the wire payload stays `prompt: string` regardless of the prompt shape.
|
|
239
|
+
const resolved = resolveMediaPrompt(rest.prompt)
|
|
240
|
+
|
|
206
241
|
aiEventClient.emit('image:request:started', {
|
|
207
242
|
requestId,
|
|
208
243
|
provider: adapter.name,
|
|
209
244
|
model,
|
|
210
|
-
prompt:
|
|
245
|
+
prompt: resolved.text,
|
|
211
246
|
numberOfImages: rest.numberOfImages,
|
|
212
247
|
size: rest.size,
|
|
248
|
+
...(resolved.images.length > 0 && {
|
|
249
|
+
imageInputCount: resolved.images.length,
|
|
250
|
+
}),
|
|
251
|
+
...(resolved.videos.length > 0 && {
|
|
252
|
+
videoInputCount: resolved.videos.length,
|
|
253
|
+
}),
|
|
254
|
+
...(resolved.audios.length > 0 && {
|
|
255
|
+
audioInputCount: resolved.audios.length,
|
|
256
|
+
}),
|
|
213
257
|
modelOptions: rest.modelOptions,
|
|
214
258
|
timestamp: startTime,
|
|
215
259
|
})
|