ai-functions 0.2.19 → 0.4.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 (227) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/.turbo/turbo-test.log +105 -0
  3. package/README.md +232 -37
  4. package/TODO.md +138 -0
  5. package/dist/ai-promise.d.ts +219 -0
  6. package/dist/ai-promise.d.ts.map +1 -0
  7. package/dist/ai-promise.js +610 -0
  8. package/dist/ai-promise.js.map +1 -0
  9. package/dist/ai.d.ts +285 -0
  10. package/dist/ai.d.ts.map +1 -0
  11. package/dist/ai.js +842 -0
  12. package/dist/ai.js.map +1 -0
  13. package/dist/batch/anthropic.d.ts +23 -0
  14. package/dist/batch/anthropic.d.ts.map +1 -0
  15. package/dist/batch/anthropic.js +257 -0
  16. package/dist/batch/anthropic.js.map +1 -0
  17. package/dist/batch/bedrock.d.ts +64 -0
  18. package/dist/batch/bedrock.d.ts.map +1 -0
  19. package/dist/batch/bedrock.js +586 -0
  20. package/dist/batch/bedrock.js.map +1 -0
  21. package/dist/batch/cloudflare.d.ts +37 -0
  22. package/dist/batch/cloudflare.d.ts.map +1 -0
  23. package/dist/batch/cloudflare.js +289 -0
  24. package/dist/batch/cloudflare.js.map +1 -0
  25. package/dist/batch/google.d.ts +41 -0
  26. package/dist/batch/google.d.ts.map +1 -0
  27. package/dist/batch/google.js +360 -0
  28. package/dist/batch/google.js.map +1 -0
  29. package/dist/batch/index.d.ts +31 -0
  30. package/dist/batch/index.d.ts.map +1 -0
  31. package/dist/batch/index.js +31 -0
  32. package/dist/batch/index.js.map +1 -0
  33. package/dist/batch/memory.d.ts +44 -0
  34. package/dist/batch/memory.d.ts.map +1 -0
  35. package/dist/batch/memory.js +188 -0
  36. package/dist/batch/memory.js.map +1 -0
  37. package/dist/batch/openai.d.ts +37 -0
  38. package/dist/batch/openai.d.ts.map +1 -0
  39. package/dist/batch/openai.js +403 -0
  40. package/dist/batch/openai.js.map +1 -0
  41. package/dist/batch-map.d.ts +125 -0
  42. package/dist/batch-map.d.ts.map +1 -0
  43. package/dist/batch-map.js +406 -0
  44. package/dist/batch-map.js.map +1 -0
  45. package/dist/batch-queue.d.ts +273 -0
  46. package/dist/batch-queue.d.ts.map +1 -0
  47. package/dist/batch-queue.js +271 -0
  48. package/dist/batch-queue.js.map +1 -0
  49. package/dist/context.d.ts +133 -0
  50. package/dist/context.d.ts.map +1 -0
  51. package/dist/context.js +267 -0
  52. package/dist/context.js.map +1 -0
  53. package/dist/embeddings.d.ts +123 -0
  54. package/dist/embeddings.d.ts.map +1 -0
  55. package/dist/embeddings.js +170 -0
  56. package/dist/embeddings.js.map +1 -0
  57. package/dist/eval/index.d.ts +8 -0
  58. package/dist/eval/index.d.ts.map +1 -0
  59. package/dist/eval/index.js +8 -0
  60. package/dist/eval/index.js.map +1 -0
  61. package/dist/eval/models.d.ts +66 -0
  62. package/dist/eval/models.d.ts.map +1 -0
  63. package/dist/eval/models.js +120 -0
  64. package/dist/eval/models.js.map +1 -0
  65. package/dist/eval/runner.d.ts +64 -0
  66. package/dist/eval/runner.d.ts.map +1 -0
  67. package/dist/eval/runner.js +148 -0
  68. package/dist/eval/runner.js.map +1 -0
  69. package/dist/generate.d.ts +168 -0
  70. package/dist/generate.d.ts.map +1 -0
  71. package/dist/generate.js +174 -0
  72. package/dist/generate.js.map +1 -0
  73. package/dist/index.d.ts +30 -0
  74. package/dist/index.d.ts.map +1 -0
  75. package/dist/index.js +54 -0
  76. package/dist/index.js.map +1 -0
  77. package/dist/primitives.d.ts +292 -0
  78. package/dist/primitives.d.ts.map +1 -0
  79. package/dist/primitives.js +471 -0
  80. package/dist/primitives.js.map +1 -0
  81. package/dist/providers/cloudflare.d.ts +9 -0
  82. package/dist/providers/cloudflare.d.ts.map +1 -0
  83. package/dist/providers/cloudflare.js +9 -0
  84. package/dist/providers/cloudflare.js.map +1 -0
  85. package/dist/providers/index.d.ts +9 -0
  86. package/dist/providers/index.d.ts.map +1 -0
  87. package/dist/providers/index.js +9 -0
  88. package/dist/providers/index.js.map +1 -0
  89. package/dist/schema.d.ts +54 -0
  90. package/dist/schema.d.ts.map +1 -0
  91. package/dist/schema.js +109 -0
  92. package/dist/schema.js.map +1 -0
  93. package/dist/template.d.ts +73 -0
  94. package/dist/template.d.ts.map +1 -0
  95. package/dist/template.js +129 -0
  96. package/dist/template.js.map +1 -0
  97. package/dist/types.d.ts +481 -0
  98. package/dist/types.d.ts.map +1 -0
  99. package/dist/types.js +5 -0
  100. package/dist/types.js.map +1 -0
  101. package/evalite.config.ts +19 -0
  102. package/evals/README.md +212 -0
  103. package/evals/classification.eval.ts +108 -0
  104. package/evals/marketing.eval.ts +370 -0
  105. package/evals/math.eval.ts +94 -0
  106. package/evals/run-evals.ts +166 -0
  107. package/evals/structured-output.eval.ts +143 -0
  108. package/evals/writing.eval.ts +117 -0
  109. package/examples/batch-blog-posts.ts +160 -0
  110. package/package.json +59 -43
  111. package/src/ai-promise.ts +784 -0
  112. package/src/ai.ts +1183 -0
  113. package/src/batch/anthropic.ts +375 -0
  114. package/src/batch/bedrock.ts +801 -0
  115. package/src/batch/cloudflare.ts +421 -0
  116. package/src/batch/google.ts +491 -0
  117. package/src/batch/index.ts +31 -0
  118. package/src/batch/memory.ts +253 -0
  119. package/src/batch/openai.ts +557 -0
  120. package/src/batch-map.ts +534 -0
  121. package/src/batch-queue.ts +493 -0
  122. package/src/context.ts +332 -0
  123. package/src/embeddings.ts +244 -0
  124. package/src/eval/index.ts +8 -0
  125. package/src/eval/models.ts +158 -0
  126. package/src/eval/runner.ts +217 -0
  127. package/src/generate.ts +245 -0
  128. package/src/index.ts +154 -0
  129. package/src/primitives.ts +612 -0
  130. package/src/providers/cloudflare.ts +15 -0
  131. package/src/providers/index.ts +14 -0
  132. package/src/schema.ts +147 -0
  133. package/src/template.ts +209 -0
  134. package/src/types.ts +540 -0
  135. package/test/README.md +105 -0
  136. package/test/ai-proxy.test.ts +192 -0
  137. package/test/async-iterators.test.ts +327 -0
  138. package/test/batch-background.test.ts +482 -0
  139. package/test/batch-blog-posts.test.ts +387 -0
  140. package/test/blog-generation.test.ts +510 -0
  141. package/test/browse-read.test.ts +611 -0
  142. package/test/core-functions.test.ts +694 -0
  143. package/test/decide.test.ts +393 -0
  144. package/test/define.test.ts +274 -0
  145. package/test/e2e-bedrock-manual.ts +163 -0
  146. package/test/e2e-bedrock.test.ts +191 -0
  147. package/test/e2e-flex-gateway.ts +157 -0
  148. package/test/e2e-flex-manual.ts +183 -0
  149. package/test/e2e-flex.test.ts +209 -0
  150. package/test/e2e-google-manual.ts +178 -0
  151. package/test/e2e-google.test.ts +216 -0
  152. package/test/embeddings.test.ts +284 -0
  153. package/test/evals/define-function.eval.test.ts +379 -0
  154. package/test/evals/primitives.eval.test.ts +384 -0
  155. package/test/function-types.test.ts +492 -0
  156. package/test/generate-core.test.ts +319 -0
  157. package/test/generate.test.ts +163 -0
  158. package/test/implicit-batch.test.ts +422 -0
  159. package/test/schema.test.ts +109 -0
  160. package/test/tagged-templates.test.ts +302 -0
  161. package/tsconfig.json +8 -6
  162. package/vitest.config.ts +42 -0
  163. package/LICENSE +0 -21
  164. package/db/cache.ts +0 -6
  165. package/db/mongo.ts +0 -75
  166. package/dist/mjs/db/cache.d.ts +0 -1
  167. package/dist/mjs/db/cache.js +0 -5
  168. package/dist/mjs/db/mongo.d.ts +0 -31
  169. package/dist/mjs/db/mongo.js +0 -48
  170. package/dist/mjs/examples/data.d.ts +0 -1105
  171. package/dist/mjs/examples/data.js +0 -1105
  172. package/dist/mjs/functions/ai.d.ts +0 -20
  173. package/dist/mjs/functions/ai.js +0 -83
  174. package/dist/mjs/functions/ai.test.d.ts +0 -1
  175. package/dist/mjs/functions/ai.test.js +0 -29
  176. package/dist/mjs/functions/gpt.d.ts +0 -4
  177. package/dist/mjs/functions/gpt.js +0 -10
  178. package/dist/mjs/functions/list.d.ts +0 -7
  179. package/dist/mjs/functions/list.js +0 -72
  180. package/dist/mjs/index.d.ts +0 -3
  181. package/dist/mjs/index.js +0 -3
  182. package/dist/mjs/queue/kafka.d.ts +0 -0
  183. package/dist/mjs/queue/kafka.js +0 -1
  184. package/dist/mjs/queue/memory.d.ts +0 -0
  185. package/dist/mjs/queue/memory.js +0 -1
  186. package/dist/mjs/queue/mongo.d.ts +0 -30
  187. package/dist/mjs/queue/mongo.js +0 -42
  188. package/dist/mjs/streams/kafka.d.ts +0 -0
  189. package/dist/mjs/streams/kafka.js +0 -1
  190. package/dist/mjs/streams/memory.d.ts +0 -0
  191. package/dist/mjs/streams/memory.js +0 -1
  192. package/dist/mjs/streams/mongo.d.ts +0 -0
  193. package/dist/mjs/streams/mongo.js +0 -1
  194. package/dist/mjs/streams/types.d.ts +0 -0
  195. package/dist/mjs/streams/types.js +0 -1
  196. package/dist/mjs/types.d.ts +0 -11
  197. package/dist/mjs/types.js +0 -1
  198. package/dist/mjs/utils/completion.d.ts +0 -9
  199. package/dist/mjs/utils/completion.js +0 -20
  200. package/dist/mjs/utils/schema.d.ts +0 -10
  201. package/dist/mjs/utils/schema.js +0 -72
  202. package/dist/mjs/utils/schema.test.d.ts +0 -1
  203. package/dist/mjs/utils/schema.test.js +0 -60
  204. package/dist/mjs/utils/state.d.ts +0 -1
  205. package/dist/mjs/utils/state.js +0 -19
  206. package/examples/data.ts +0 -1105
  207. package/fixup +0 -11
  208. package/functions/ai.test.ts +0 -41
  209. package/functions/ai.ts +0 -115
  210. package/functions/gpt.ts +0 -12
  211. package/functions/list.ts +0 -84
  212. package/index.ts +0 -3
  213. package/queue/kafka.ts +0 -0
  214. package/queue/memory.ts +0 -0
  215. package/queue/mongo.ts +0 -88
  216. package/streams/kafka.ts +0 -0
  217. package/streams/memory.ts +0 -0
  218. package/streams/mongo.ts +0 -0
  219. package/streams/types.ts +0 -0
  220. package/tsconfig-backup.json +0 -105
  221. package/tsconfig-base.json +0 -26
  222. package/tsconfig-cjs.json +0 -8
  223. package/types.ts +0 -12
  224. package/utils/completion.ts +0 -28
  225. package/utils/schema.test.ts +0 -69
  226. package/utils/schema.ts +0 -74
  227. package/utils/state.ts +0 -23
@@ -0,0 +1,784 @@
1
+ /**
2
+ * AIPromise - RPC-style promise pipelining for AI functions
3
+ *
4
+ * Inspired by capnweb's RpcPromise, this enables:
5
+ * - Property access tracking for dynamic schema inference
6
+ * - Promise pipelining without await
7
+ * - Magical .map() for batch processing
8
+ * - Dependency graph resolution
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // Dynamic schema from destructuring
13
+ * const { summary, keyPoints, conclusion } = ai`write about ${topic}`
14
+ *
15
+ * // Pipeline without await
16
+ * const isValid = is`${conclusion} is solid given ${keyPoints}`
17
+ *
18
+ * // Batch process with map
19
+ * const ideas = list`startup ideas`
20
+ * const evaluated = await ideas.map(idea => ({
21
+ * idea,
22
+ * viable: is`${idea} is viable`,
23
+ * market: ai`market size for ${idea}`,
24
+ * }))
25
+ *
26
+ * // Only await at the end
27
+ * if (await isValid) { ... }
28
+ * ```
29
+ *
30
+ * @packageDocumentation
31
+ */
32
+
33
+ import { generateObject } from './generate.js'
34
+ import type { SimpleSchema } from './schema.js'
35
+ import type { FunctionOptions } from './template.js'
36
+ import {
37
+ isInRecordingMode,
38
+ getCurrentItemPlaceholder,
39
+ captureOperation,
40
+ createBatchMap,
41
+ BatchMapPromise,
42
+ } from './batch-map.js'
43
+ import { getModel } from './context.js'
44
+
45
+ // ============================================================================
46
+ // Types
47
+ // ============================================================================
48
+
49
+ /** Symbol to identify AIPromise instances */
50
+ export const AI_PROMISE_SYMBOL = Symbol.for('ai-promise')
51
+
52
+ /** Symbol to get the raw AIPromise from a proxy */
53
+ export const RAW_PROMISE_SYMBOL = Symbol.for('ai-promise-raw')
54
+
55
+ /** Recording mode for map() */
56
+ export const RECORDING_MODE = Symbol.for('ai-promise-recording')
57
+
58
+ /** Dependency tracking */
59
+ interface Dependency {
60
+ promise: AIPromise<unknown>
61
+ path: string[]
62
+ }
63
+
64
+ /** Map callback recording */
65
+ interface MapRecording {
66
+ operations: Array<{
67
+ type: 'ai' | 'is' | 'list' | 'lists' | 'extract' | 'other'
68
+ prompt: string
69
+ dependencies: Dependency[]
70
+ }>
71
+ capturedStubs: AIPromise<unknown>[]
72
+ }
73
+
74
+ /** Options for AIPromise creation */
75
+ export interface AIPromiseOptions extends FunctionOptions {
76
+ /** The type of generation */
77
+ type?: 'text' | 'object' | 'list' | 'lists' | 'boolean' | 'extract'
78
+ /** Base schema (can be extended by property access) */
79
+ baseSchema?: SimpleSchema
80
+ /** Parent promise this was derived from */
81
+ parent?: AIPromise<unknown>
82
+ /** Property path from parent */
83
+ propertyPath?: string[]
84
+ }
85
+
86
+ // ============================================================================
87
+ // Global State
88
+ // ============================================================================
89
+
90
+ /** Current recording context for map() */
91
+ let currentRecording: MapRecording | null = null
92
+
93
+ /** Pending promises for batch resolution */
94
+ const pendingPromises = new Set<AIPromise<unknown>>()
95
+
96
+ /** Promise resolution queue */
97
+ let resolutionScheduled = false
98
+
99
+ // ============================================================================
100
+ // AIPromise Implementation
101
+ // ============================================================================
102
+
103
+ /**
104
+ * AIPromise - Like capnweb's RpcPromise but for AI functions
105
+ *
106
+ * Acts as both a Promise AND a stub that:
107
+ * - Tracks property accesses for dynamic schema inference
108
+ * - Records dependencies for promise pipelining
109
+ * - Supports .map() for batch processing
110
+ */
111
+ export class AIPromise<T> implements PromiseLike<T> {
112
+ /** Marker to identify AIPromise instances */
113
+ readonly [AI_PROMISE_SYMBOL] = true
114
+
115
+ /** The prompt that will generate this value */
116
+ private _prompt: string
117
+
118
+ /** Options for generation */
119
+ private _options: AIPromiseOptions
120
+
121
+ /** Properties accessed on this promise (for schema inference) */
122
+ private _accessedProps = new Set<string>()
123
+
124
+ /** Property path from parent (for nested access) */
125
+ private _propertyPath: string[]
126
+
127
+ /** Parent promise (if this is a property access) */
128
+ private _parent: AIPromise<unknown> | null
129
+
130
+ /** Dependencies (other AIPromises used in our prompt) */
131
+ private _dependencies: Dependency[] = []
132
+
133
+ /** Cached resolver promise */
134
+ private _resolver: Promise<T> | null = null
135
+
136
+ /** Resolved value (cached after first resolution) */
137
+ private _resolvedValue: T | undefined
138
+
139
+ /** Whether this promise has been resolved */
140
+ private _isResolved = false
141
+
142
+ /** Whether we're in recording mode */
143
+ private _isRecording = false
144
+
145
+ constructor(prompt: string, options: AIPromiseOptions = {}) {
146
+ this._prompt = prompt
147
+ this._options = options
148
+ this._propertyPath = options.propertyPath || []
149
+ this._parent = options.parent || null
150
+
151
+ // Track this promise for batch resolution
152
+ pendingPromises.add(this)
153
+
154
+ // Return a proxy that intercepts property access
155
+ return new Proxy(this, PROXY_HANDLERS) as AIPromise<T>
156
+ }
157
+
158
+ /** Get the prompt */
159
+ get prompt(): string {
160
+ return this._prompt
161
+ }
162
+
163
+ /** Get the property path */
164
+ get path(): string[] {
165
+ return this._propertyPath
166
+ }
167
+
168
+ /** Check if resolved */
169
+ get isResolved(): boolean {
170
+ return this._isResolved
171
+ }
172
+
173
+ /** Get accessed properties */
174
+ get accessedProps(): Set<string> {
175
+ return this._accessedProps
176
+ }
177
+
178
+ /**
179
+ * Add a dependency (another AIPromise used in this one's prompt)
180
+ */
181
+ addDependency(promise: AIPromise<unknown>, path: string[] = []): void {
182
+ this._dependencies.push({ promise, path })
183
+ }
184
+
185
+ /**
186
+ * Resolve this promise
187
+ */
188
+ async resolve(): Promise<T> {
189
+ if (this._isResolved) {
190
+ return this._resolvedValue as T
191
+ }
192
+
193
+ // If this is a property access on a parent, resolve the parent first
194
+ if (this._parent) {
195
+ const parentValue = await this._parent.resolve()
196
+ const value = getNestedValue(parentValue, this._propertyPath)
197
+ this._resolvedValue = value as T
198
+ this._isResolved = true
199
+ return this._resolvedValue
200
+ }
201
+
202
+ // Resolve dependencies first
203
+ const resolvedDeps: Record<string, unknown> = {}
204
+ for (const dep of this._dependencies) {
205
+ const value = await dep.promise.resolve()
206
+ const key = dep.path.length > 0 ? dep.path.join('.') : `dep_${this._dependencies.indexOf(dep)}`
207
+ resolvedDeps[key] = value
208
+ }
209
+
210
+ // Substitute resolved dependencies into prompt
211
+ let finalPrompt = this._prompt
212
+ for (const [key, value] of Object.entries(resolvedDeps)) {
213
+ finalPrompt = finalPrompt.replace(
214
+ new RegExp(`\\$\\{${key}\\}`, 'g'),
215
+ String(value)
216
+ )
217
+ }
218
+
219
+ // Build schema from accessed properties
220
+ const schema = this._buildSchema()
221
+
222
+ // Generate the result
223
+ const result = await generateObject({
224
+ model: this._options.model || 'sonnet',
225
+ schema,
226
+ prompt: finalPrompt,
227
+ system: this._options.system,
228
+ temperature: this._options.temperature,
229
+ maxTokens: this._options.maxTokens,
230
+ })
231
+
232
+ this._resolvedValue = result.object as T
233
+ this._isResolved = true
234
+ pendingPromises.delete(this)
235
+
236
+ return this._resolvedValue
237
+ }
238
+
239
+ /**
240
+ * Build schema from accessed properties and base schema
241
+ */
242
+ private _buildSchema(): SimpleSchema {
243
+ const baseSchema = this._options.baseSchema || {}
244
+
245
+ // If no properties accessed, use base schema or infer from type
246
+ if (this._accessedProps.size === 0) {
247
+ if (typeof baseSchema === 'object' && Object.keys(baseSchema).length > 0) {
248
+ return baseSchema
249
+ }
250
+
251
+ // Infer from type
252
+ switch (this._options.type) {
253
+ case 'list':
254
+ return { items: ['List items'] }
255
+ case 'lists':
256
+ return { categories: ['Category names'], data: 'JSON object with categorized lists' }
257
+ case 'boolean':
258
+ return { answer: 'true | false' }
259
+ case 'text':
260
+ return { text: 'The generated text' }
261
+ default:
262
+ return { result: 'The result' }
263
+ }
264
+ }
265
+
266
+ // Build schema from accessed properties
267
+ const schema: { [key: string]: SimpleSchema } = {}
268
+
269
+ for (const prop of this._accessedProps) {
270
+ // Check if base schema has this property
271
+ if (typeof baseSchema === 'object' && !Array.isArray(baseSchema) && prop in baseSchema) {
272
+ const propSchema = (baseSchema as { [key: string]: SimpleSchema })[prop]
273
+ if (propSchema !== undefined) {
274
+ schema[prop] = propSchema
275
+ continue
276
+ }
277
+ }
278
+
279
+ // Infer type from property name patterns
280
+ const lowerProp = prop.toLowerCase()
281
+ if (
282
+ lowerProp.endsWith('s') ||
283
+ lowerProp.includes('list') ||
284
+ lowerProp.includes('items') ||
285
+ lowerProp.includes('array')
286
+ ) {
287
+ schema[prop] = [`List of ${prop}`]
288
+ } else if (
289
+ lowerProp.includes('is') ||
290
+ lowerProp.includes('has') ||
291
+ lowerProp.includes('can') ||
292
+ lowerProp.includes('should')
293
+ ) {
294
+ schema[prop] = `Whether ${prop} (true/false)`
295
+ } else if (
296
+ lowerProp.includes('count') ||
297
+ lowerProp.includes('number') ||
298
+ lowerProp.includes('total') ||
299
+ lowerProp.includes('amount')
300
+ ) {
301
+ schema[prop] = `The ${prop} (number)`
302
+ } else {
303
+ schema[prop] = `The ${prop}`
304
+ }
305
+ }
306
+
307
+ return schema
308
+ }
309
+
310
+ /**
311
+ * Map over array results - automatically batches operations!
312
+ *
313
+ * When you map over a list, the operations are captured and
314
+ * automatically batched when resolved. Uses provider batch APIs
315
+ * for cost savings (50% discount) when beneficial.
316
+ *
317
+ * @example
318
+ * ```ts
319
+ * // Simple map - each title becomes a blog post
320
+ * const titles = await list`10 blog post titles`
321
+ * const posts = titles.map(title => write`blog post: # ${title}`)
322
+ * console.log(await posts) // 10 blog posts via batch API
323
+ *
324
+ * // Complex map - multiple operations per item
325
+ * const ideas = await list`startup ideas`
326
+ * const evaluated = await ideas.map(idea => ({
327
+ * idea,
328
+ * viable: is`${idea} is viable`,
329
+ * market: ai`market size for ${idea}`,
330
+ * }))
331
+ * ```
332
+ */
333
+ map<U>(
334
+ callback: (item: T extends (infer I)[] ? I : T, index: number) => U
335
+ ): BatchMapPromise<U> {
336
+ // Create a wrapper that resolves this promise first, then maps
337
+ const mapPromise = new BatchMapPromise<U>([], [], {})
338
+
339
+ // Override the resolve to first get the list items
340
+ const originalResolve = mapPromise.resolve.bind(mapPromise)
341
+ ;(mapPromise as any).resolve = async () => {
342
+ // First, resolve the list
343
+ const items = await this.resolve()
344
+
345
+ if (!Array.isArray(items)) {
346
+ throw new Error('Cannot map over non-array result')
347
+ }
348
+
349
+ // Now create the actual batch map with the resolved items
350
+ const actualBatchMap = createBatchMap(
351
+ items as (T extends (infer I)[] ? I : T)[],
352
+ callback
353
+ )
354
+
355
+ return actualBatchMap.resolve()
356
+ }
357
+
358
+ return mapPromise
359
+ }
360
+
361
+ /**
362
+ * Map with explicit batch options
363
+ *
364
+ * @example
365
+ * ```ts
366
+ * // Force immediate execution (no batch API)
367
+ * const posts = titles.mapImmediate(title => write`blog post: ${title}`)
368
+ *
369
+ * // Force batch API (even for small lists)
370
+ * const posts = titles.mapDeferred(title => write`blog post: ${title}`)
371
+ * ```
372
+ */
373
+ mapImmediate<U>(
374
+ callback: (item: T extends (infer I)[] ? I : T, index: number) => U
375
+ ): BatchMapPromise<U> {
376
+ const mapPromise = new BatchMapPromise<U>([], [], { immediate: true })
377
+
378
+ ;(mapPromise as any).resolve = async () => {
379
+ const items = await this.resolve()
380
+ if (!Array.isArray(items)) {
381
+ throw new Error('Cannot map over non-array result')
382
+ }
383
+ const actualBatchMap = createBatchMap(
384
+ items as (T extends (infer I)[] ? I : T)[],
385
+ callback,
386
+ { immediate: true }
387
+ )
388
+ return actualBatchMap.resolve()
389
+ }
390
+
391
+ return mapPromise
392
+ }
393
+
394
+ mapDeferred<U>(
395
+ callback: (item: T extends (infer I)[] ? I : T, index: number) => U
396
+ ): BatchMapPromise<U> {
397
+ const mapPromise = new BatchMapPromise<U>([], [], { deferred: true })
398
+
399
+ ;(mapPromise as any).resolve = async () => {
400
+ const items = await this.resolve()
401
+ if (!Array.isArray(items)) {
402
+ throw new Error('Cannot map over non-array result')
403
+ }
404
+ const actualBatchMap = createBatchMap(
405
+ items as (T extends (infer I)[] ? I : T)[],
406
+ callback,
407
+ { deferred: true }
408
+ )
409
+ return actualBatchMap.resolve()
410
+ }
411
+
412
+ return mapPromise
413
+ }
414
+
415
+ /**
416
+ * ForEach with automatic batching
417
+ *
418
+ * @example
419
+ * ```ts
420
+ * await list`startup ideas`.forEach(async idea => {
421
+ * console.log(await is`${idea} is viable`)
422
+ * })
423
+ * ```
424
+ */
425
+ async forEach(
426
+ callback: (item: T extends (infer I)[] ? I : T, index: number) => void | Promise<void>
427
+ ): Promise<void> {
428
+ const items = await this.resolve()
429
+ if (Array.isArray(items)) {
430
+ for (let i = 0; i < items.length; i++) {
431
+ await callback(items[i], i)
432
+ }
433
+ } else {
434
+ await callback(items as any, 0)
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Async iterator support with smart batching
440
+ */
441
+ async *[Symbol.asyncIterator](): AsyncIterator<T extends (infer I)[] ? I : T> {
442
+ const items = await this.resolve()
443
+ if (Array.isArray(items)) {
444
+ for (const item of items) {
445
+ yield item as any
446
+ }
447
+ } else {
448
+ yield items as any
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Promise interface - then()
454
+ */
455
+ then<TResult1 = T, TResult2 = never>(
456
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
457
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
458
+ ): Promise<TResult1 | TResult2> {
459
+ if (!this._resolver) {
460
+ // Schedule batch resolution on next microtask
461
+ this._resolver = new Promise<T>((resolve, reject) => {
462
+ queueMicrotask(async () => {
463
+ try {
464
+ const value = await this.resolve()
465
+ resolve(value)
466
+ } catch (error) {
467
+ reject(error)
468
+ }
469
+ })
470
+ })
471
+ }
472
+
473
+ return this._resolver.then(onfulfilled, onrejected)
474
+ }
475
+
476
+ /**
477
+ * Promise interface - catch()
478
+ */
479
+ catch<TResult = never>(
480
+ onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
481
+ ): Promise<T | TResult> {
482
+ return this.then(null, onrejected)
483
+ }
484
+
485
+ /**
486
+ * Promise interface - finally()
487
+ */
488
+ finally(onfinally?: (() => void) | null): Promise<T> {
489
+ return this.then(
490
+ (value) => {
491
+ onfinally?.()
492
+ return value
493
+ },
494
+ (reason) => {
495
+ onfinally?.()
496
+ throw reason
497
+ }
498
+ )
499
+ }
500
+ }
501
+
502
+ // ============================================================================
503
+ // Proxy Handlers
504
+ // ============================================================================
505
+
506
+ const PROXY_HANDLERS: ProxyHandler<AIPromise<unknown>> = {
507
+ get(target, prop: string | symbol, receiver) {
508
+ // Handle symbols
509
+ if (typeof prop === 'symbol') {
510
+ if (prop === AI_PROMISE_SYMBOL) return true
511
+ if (prop === RAW_PROMISE_SYMBOL) return target
512
+ if (prop === Symbol.asyncIterator) return target[Symbol.asyncIterator].bind(target)
513
+ return (target as any)[prop]
514
+ }
515
+
516
+ // Handle promise methods
517
+ if (prop === 'then' || prop === 'catch' || prop === 'finally') {
518
+ return (target as any)[prop].bind(target)
519
+ }
520
+
521
+ // Handle AIPromise methods
522
+ if (prop === 'map' || prop === 'forEach' || prop === 'resolve') {
523
+ return (target as any)[prop].bind(target)
524
+ }
525
+
526
+ // Handle internal properties
527
+ if (prop.startsWith('_') || prop === 'prompt' || prop === 'path' || prop === 'isResolved' || prop === 'accessedProps') {
528
+ return (target as any)[prop]
529
+ }
530
+
531
+ // Track property access for schema inference
532
+ target.accessedProps.add(prop)
533
+
534
+ // If we're in recording mode, record this access
535
+ if (currentRecording) {
536
+ // Just track the access, don't create new promise
537
+ }
538
+
539
+ // Return a new AIPromise for the property path
540
+ return new AIPromise<unknown>(
541
+ target.prompt,
542
+ {
543
+ ...target['_options'],
544
+ parent: target,
545
+ propertyPath: [...target.path, prop],
546
+ }
547
+ )
548
+ },
549
+
550
+ // Prevent mutation
551
+ set() {
552
+ throw new Error('AIPromise properties are read-only')
553
+ },
554
+
555
+ deleteProperty() {
556
+ throw new Error('AIPromise properties cannot be deleted')
557
+ },
558
+
559
+ // Handle function calls (for chained methods)
560
+ apply(target, thisArg, args) {
561
+ // If the target is callable (e.g., from a template function), call it
562
+ if (typeof (target as any)._call === 'function') {
563
+ return (target as any)._call(...args)
564
+ }
565
+ throw new Error('AIPromise is not callable')
566
+ },
567
+ }
568
+
569
+ // ============================================================================
570
+ // Helper Functions
571
+ // ============================================================================
572
+
573
+ /**
574
+ * Get a nested value from an object by path
575
+ */
576
+ function getNestedValue(obj: unknown, path: string[]): unknown {
577
+ let current = obj
578
+ for (const key of path) {
579
+ if (current === null || current === undefined) return undefined
580
+ if (key === '__item__') continue // Skip internal markers
581
+ current = (current as Record<string, unknown>)[key]
582
+ }
583
+ return current
584
+ }
585
+
586
+ /**
587
+ * Analyze the result of a map callback to build batch schema
588
+ */
589
+ function analyzeRecordingResult(result: unknown, recording: MapRecording): SimpleSchema {
590
+ if (result === null || result === undefined) {
591
+ return { result: 'The result' }
592
+ }
593
+
594
+ if (typeof result !== 'object') {
595
+ return { result: 'The result' }
596
+ }
597
+
598
+ // Build schema from the result structure
599
+ const schema: Record<string, SimpleSchema> = {}
600
+
601
+ for (const [key, value] of Object.entries(result)) {
602
+ if (isAIPromise(value)) {
603
+ // This is a reference to an AI operation
604
+ const aiPromise = getRawPromise(value as AIPromise<unknown>)
605
+
606
+ // Infer schema from the promise's accessed properties or type
607
+ if (aiPromise.accessedProps.size > 0) {
608
+ schema[key] = Object.fromEntries(
609
+ Array.from(aiPromise.accessedProps).map(p => [p, `The ${p}`])
610
+ )
611
+ } else {
612
+ const type = (aiPromise as any)._options?.type
613
+ if (type === 'boolean') {
614
+ schema[key] = 'true | false'
615
+ } else if (type === 'list') {
616
+ schema[key] = ['List items']
617
+ } else {
618
+ schema[key] = `The ${key}`
619
+ }
620
+ }
621
+ } else if (typeof value === 'object' && value !== null) {
622
+ // Recursively analyze nested objects
623
+ schema[key] = analyzeRecordingResult(value, recording) as SimpleSchema
624
+ } else {
625
+ // Literal value - include as-is
626
+ schema[key] = `Value: ${JSON.stringify(value)}`
627
+ }
628
+ }
629
+
630
+ return schema
631
+ }
632
+
633
+ /**
634
+ * Check if a value is an AIPromise
635
+ */
636
+ export function isAIPromise(value: unknown): value is AIPromise<unknown> {
637
+ return (
638
+ value !== null &&
639
+ typeof value === 'object' &&
640
+ AI_PROMISE_SYMBOL in value &&
641
+ (value as any)[AI_PROMISE_SYMBOL] === true
642
+ )
643
+ }
644
+
645
+ /**
646
+ * Get the raw AIPromise from a proxied value
647
+ */
648
+ export function getRawPromise<T>(value: AIPromise<T>): AIPromise<T> {
649
+ if (RAW_PROMISE_SYMBOL in value) {
650
+ return (value as any)[RAW_PROMISE_SYMBOL]
651
+ }
652
+ return value
653
+ }
654
+
655
+ // ============================================================================
656
+ // Factory Functions
657
+ // ============================================================================
658
+
659
+ /**
660
+ * Create an AIPromise for text generation
661
+ */
662
+ export function createTextPromise(prompt: string, options?: FunctionOptions): AIPromise<string> {
663
+ return new AIPromise<string>(prompt, { ...options, type: 'text' })
664
+ }
665
+
666
+ /**
667
+ * Create an AIPromise for object generation with dynamic schema
668
+ */
669
+ export function createObjectPromise<T = unknown>(prompt: string, options?: FunctionOptions): AIPromise<T> {
670
+ return new AIPromise<T>(prompt, { ...options, type: 'object' })
671
+ }
672
+
673
+ /**
674
+ * Create an AIPromise for list generation
675
+ */
676
+ export function createListPromise(prompt: string, options?: FunctionOptions): AIPromise<string[]> {
677
+ return new AIPromise<string[]>(prompt, { ...options, type: 'list' })
678
+ }
679
+
680
+ /**
681
+ * Create an AIPromise for multiple lists generation
682
+ */
683
+ export function createListsPromise(prompt: string, options?: FunctionOptions): AIPromise<Record<string, string[]>> {
684
+ return new AIPromise<Record<string, string[]>>(prompt, { ...options, type: 'lists' })
685
+ }
686
+
687
+ /**
688
+ * Create an AIPromise for boolean/is check
689
+ */
690
+ export function createBooleanPromise(prompt: string, options?: FunctionOptions): AIPromise<boolean> {
691
+ return new AIPromise<boolean>(prompt, { ...options, type: 'boolean' })
692
+ }
693
+
694
+ /**
695
+ * Create an AIPromise for extraction
696
+ */
697
+ export function createExtractPromise<T = unknown>(prompt: string, options?: FunctionOptions): AIPromise<T[]> {
698
+ return new AIPromise<T[]>(prompt, { ...options, type: 'extract' })
699
+ }
700
+
701
+ // ============================================================================
702
+ // Template Tag Helpers
703
+ // ============================================================================
704
+
705
+ /**
706
+ * Parse template literals and track AIPromise dependencies
707
+ */
708
+ export function parseTemplateWithDependencies(
709
+ strings: TemplateStringsArray,
710
+ ...values: unknown[]
711
+ ): { prompt: string; dependencies: Dependency[] } {
712
+ const dependencies: Dependency[] = []
713
+ let prompt = ''
714
+
715
+ for (let i = 0; i < strings.length; i++) {
716
+ prompt += strings[i]
717
+
718
+ if (i < values.length) {
719
+ const value = values[i]
720
+
721
+ if (isAIPromise(value)) {
722
+ // Track as dependency
723
+ const rawPromise = getRawPromise(value)
724
+ const depKey = `dep_${dependencies.length}`
725
+ dependencies.push({ promise: rawPromise, path: rawPromise.path })
726
+ prompt += `\${${depKey}}`
727
+ } else {
728
+ // Inline the value
729
+ prompt += String(value)
730
+ }
731
+ }
732
+ }
733
+
734
+ return { prompt, dependencies }
735
+ }
736
+
737
+ /**
738
+ * Create a template function that returns AIPromise
739
+ */
740
+ export function createAITemplateFunction<T>(
741
+ type: AIPromiseOptions['type'],
742
+ baseOptions?: FunctionOptions
743
+ ): ((strings: TemplateStringsArray, ...values: unknown[]) => AIPromise<T>) &
744
+ ((prompt: string, options?: FunctionOptions) => AIPromise<T>) {
745
+
746
+ function templateFn(
747
+ promptOrStrings: string | TemplateStringsArray,
748
+ ...args: unknown[]
749
+ ): AIPromise<T> {
750
+ let prompt: string
751
+ let dependencies: Dependency[] = []
752
+ let options: FunctionOptions = { ...baseOptions }
753
+
754
+ if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) {
755
+ // Tagged template literal
756
+ const parsed = parseTemplateWithDependencies(promptOrStrings, ...args)
757
+ prompt = parsed.prompt
758
+ dependencies = parsed.dependencies
759
+ } else {
760
+ // Regular function call
761
+ prompt = promptOrStrings as string
762
+ if (args.length > 0 && typeof args[0] === 'object') {
763
+ options = { ...options, ...(args[0] as FunctionOptions) }
764
+ }
765
+ }
766
+
767
+ // If we're in recording mode (inside a .map() callback), capture this operation
768
+ if (isInRecordingMode()) {
769
+ const batchType = type === 'text' ? 'text' : type === 'boolean' ? 'boolean' : type === 'list' ? 'list' : 'object'
770
+ captureOperation(prompt, batchType, (options as any).baseSchema, options.system)
771
+ }
772
+
773
+ const promise = new AIPromise<T>(prompt, { ...options, type })
774
+
775
+ // Add dependencies
776
+ for (const dep of dependencies) {
777
+ promise.addDependency(dep.promise, dep.path)
778
+ }
779
+
780
+ return promise
781
+ }
782
+
783
+ return templateFn as any
784
+ }