ai-functions 2.1.1 → 2.3.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 (286) hide show
  1. package/.turbo/turbo-build.log +1 -4
  2. package/CHANGELOG.md +68 -1
  3. package/README.md +397 -157
  4. package/dist/ai-promise.d.ts +50 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +410 -51
  7. package/dist/ai-promise.js.map +1 -1
  8. package/dist/ai-schemas.d.ts +56 -0
  9. package/dist/ai-schemas.d.ts.map +1 -0
  10. package/dist/ai-schemas.js +53 -0
  11. package/dist/ai-schemas.js.map +1 -0
  12. package/dist/ai.d.ts +16 -242
  13. package/dist/ai.d.ts.map +1 -1
  14. package/dist/ai.js +54 -837
  15. package/dist/ai.js.map +1 -1
  16. package/dist/batch/anthropic.d.ts +6 -4
  17. package/dist/batch/anthropic.d.ts.map +1 -1
  18. package/dist/batch/anthropic.js +83 -145
  19. package/dist/batch/anthropic.js.map +1 -1
  20. package/dist/batch/bedrock.d.ts +8 -30
  21. package/dist/batch/bedrock.d.ts.map +1 -1
  22. package/dist/batch/bedrock.js +155 -338
  23. package/dist/batch/bedrock.js.map +1 -1
  24. package/dist/batch/cloudflare.d.ts +8 -20
  25. package/dist/batch/cloudflare.d.ts.map +1 -1
  26. package/dist/batch/cloudflare.js +68 -189
  27. package/dist/batch/cloudflare.js.map +1 -1
  28. package/dist/batch/google.d.ts +6 -20
  29. package/dist/batch/google.d.ts.map +1 -1
  30. package/dist/batch/google.js +70 -238
  31. package/dist/batch/google.js.map +1 -1
  32. package/dist/batch/index.d.ts +4 -1
  33. package/dist/batch/index.d.ts.map +1 -1
  34. package/dist/batch/index.js +4 -1
  35. package/dist/batch/index.js.map +1 -1
  36. package/dist/batch/memory.d.ts +1 -1
  37. package/dist/batch/memory.d.ts.map +1 -1
  38. package/dist/batch/memory.js +14 -10
  39. package/dist/batch/memory.js.map +1 -1
  40. package/dist/batch/openai.d.ts +11 -14
  41. package/dist/batch/openai.d.ts.map +1 -1
  42. package/dist/batch/openai.js +52 -156
  43. package/dist/batch/openai.js.map +1 -1
  44. package/dist/batch/provider.d.ts +111 -0
  45. package/dist/batch/provider.d.ts.map +1 -0
  46. package/dist/batch/provider.js +233 -0
  47. package/dist/batch/provider.js.map +1 -0
  48. package/dist/batch-map.d.ts.map +1 -1
  49. package/dist/batch-map.js +23 -17
  50. package/dist/batch-map.js.map +1 -1
  51. package/dist/batch-queue.d.ts +65 -0
  52. package/dist/batch-queue.d.ts.map +1 -1
  53. package/dist/batch-queue.js +169 -14
  54. package/dist/batch-queue.js.map +1 -1
  55. package/dist/budget.d.ts +272 -0
  56. package/dist/budget.d.ts.map +1 -0
  57. package/dist/budget.js +513 -0
  58. package/dist/budget.js.map +1 -0
  59. package/dist/cache.d.ts +295 -0
  60. package/dist/cache.d.ts.map +1 -0
  61. package/dist/cache.js +433 -0
  62. package/dist/cache.js.map +1 -0
  63. package/dist/context.d.ts +42 -8
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +64 -62
  66. package/dist/context.js.map +1 -1
  67. package/dist/digital-objects-registry.d.ts +229 -0
  68. package/dist/digital-objects-registry.d.ts.map +1 -0
  69. package/dist/digital-objects-registry.js +617 -0
  70. package/dist/digital-objects-registry.js.map +1 -0
  71. package/dist/embeddings.d.ts +2 -2
  72. package/dist/embeddings.d.ts.map +1 -1
  73. package/dist/errors.d.ts +22 -0
  74. package/dist/errors.d.ts.map +1 -0
  75. package/dist/errors.js +35 -0
  76. package/dist/errors.js.map +1 -0
  77. package/dist/eval/runner.d.ts +10 -1
  78. package/dist/eval/runner.d.ts.map +1 -1
  79. package/dist/eval/runner.js +41 -35
  80. package/dist/eval/runner.js.map +1 -1
  81. package/dist/eval-log/in-memory.d.ts +34 -0
  82. package/dist/eval-log/in-memory.d.ts.map +1 -0
  83. package/dist/eval-log/in-memory.js +84 -0
  84. package/dist/eval-log/in-memory.js.map +1 -0
  85. package/dist/eval-log/index.d.ts +29 -0
  86. package/dist/eval-log/index.d.ts.map +1 -0
  87. package/dist/eval-log/index.js +39 -0
  88. package/dist/eval-log/index.js.map +1 -0
  89. package/dist/eval-log/types.d.ts +101 -0
  90. package/dist/eval-log/types.d.ts.map +1 -0
  91. package/dist/eval-log/types.js +16 -0
  92. package/dist/eval-log/types.js.map +1 -0
  93. package/dist/function-registry.d.ts +116 -0
  94. package/dist/function-registry.d.ts.map +1 -0
  95. package/dist/function-registry.js +546 -0
  96. package/dist/function-registry.js.map +1 -0
  97. package/dist/generate.d.ts +9 -3
  98. package/dist/generate.d.ts.map +1 -1
  99. package/dist/generate.js +18 -22
  100. package/dist/generate.js.map +1 -1
  101. package/dist/index.d.ts +35 -20
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/index.js +89 -42
  104. package/dist/index.js.map +1 -1
  105. package/dist/logger.d.ts +118 -0
  106. package/dist/logger.d.ts.map +1 -0
  107. package/dist/logger.js +187 -0
  108. package/dist/logger.js.map +1 -0
  109. package/dist/middleware/budget.d.ts +84 -0
  110. package/dist/middleware/budget.d.ts.map +1 -0
  111. package/dist/middleware/budget.js +110 -0
  112. package/dist/middleware/budget.js.map +1 -0
  113. package/dist/middleware/cache.d.ts +103 -0
  114. package/dist/middleware/cache.d.ts.map +1 -0
  115. package/dist/middleware/cache.js +228 -0
  116. package/dist/middleware/cache.js.map +1 -0
  117. package/dist/middleware/embed-cache.d.ts +99 -0
  118. package/dist/middleware/embed-cache.d.ts.map +1 -0
  119. package/dist/middleware/embed-cache.js +128 -0
  120. package/dist/middleware/embed-cache.js.map +1 -0
  121. package/dist/middleware/index.d.ts +11 -0
  122. package/dist/middleware/index.d.ts.map +1 -0
  123. package/dist/middleware/index.js +11 -0
  124. package/dist/middleware/index.js.map +1 -0
  125. package/dist/middleware/trace.d.ts +103 -0
  126. package/dist/middleware/trace.d.ts.map +1 -0
  127. package/dist/middleware/trace.js +176 -0
  128. package/dist/middleware/trace.js.map +1 -0
  129. package/dist/primitives.d.ts +120 -1
  130. package/dist/primitives.d.ts.map +1 -1
  131. package/dist/primitives.js +398 -26
  132. package/dist/primitives.js.map +1 -1
  133. package/dist/retry.d.ts +368 -0
  134. package/dist/retry.d.ts.map +1 -0
  135. package/dist/retry.js +646 -0
  136. package/dist/retry.js.map +1 -0
  137. package/dist/schema.d.ts.map +1 -1
  138. package/dist/schema.js +2 -10
  139. package/dist/schema.js.map +1 -1
  140. package/dist/telemetry.d.ts +128 -0
  141. package/dist/telemetry.d.ts.map +1 -0
  142. package/dist/telemetry.js +285 -0
  143. package/dist/telemetry.js.map +1 -0
  144. package/dist/template.d.ts.map +1 -1
  145. package/dist/template.js +6 -1
  146. package/dist/template.js.map +1 -1
  147. package/dist/tool-orchestration.d.ts +453 -0
  148. package/dist/tool-orchestration.d.ts.map +1 -0
  149. package/dist/tool-orchestration.js +763 -0
  150. package/dist/tool-orchestration.js.map +1 -0
  151. package/dist/type-guards.d.ts +28 -0
  152. package/dist/type-guards.d.ts.map +1 -0
  153. package/dist/type-guards.js +29 -0
  154. package/dist/type-guards.js.map +1 -0
  155. package/dist/types.d.ts +135 -17
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/types.js +36 -1
  158. package/dist/types.js.map +1 -1
  159. package/dist/wrap-for-v3.d.ts +80 -0
  160. package/dist/wrap-for-v3.d.ts.map +1 -0
  161. package/dist/wrap-for-v3.js +89 -0
  162. package/dist/wrap-for-v3.js.map +1 -0
  163. package/examples/00-quickstart.ts +232 -0
  164. package/examples/01-rag-chatbot.ts +212 -0
  165. package/examples/02-multi-agent-research.ts +290 -0
  166. package/examples/03-email-classification.ts +379 -0
  167. package/examples/04-content-moderation.ts +400 -0
  168. package/examples/05-document-extraction.ts +455 -0
  169. package/examples/06-streaming-chat-nextjs.ts +437 -0
  170. package/examples/07-cloudflare-worker.ts +483 -0
  171. package/examples/08-batch-processing.ts +491 -0
  172. package/examples/09-budget-constrained.ts +527 -0
  173. package/examples/10-tool-orchestration.ts +565 -0
  174. package/examples/11-retry-resilience.ts +403 -0
  175. package/examples/12-caching-strategies.ts +422 -0
  176. package/examples/README.md +145 -0
  177. package/package.json +10 -6
  178. package/src/ai-promise.ts +528 -99
  179. package/src/ai-schemas.ts +122 -0
  180. package/src/ai.ts +69 -1153
  181. package/src/batch/anthropic.ts +96 -161
  182. package/src/batch/bedrock.ts +203 -454
  183. package/src/batch/cloudflare.ts +99 -282
  184. package/src/batch/google.ts +91 -297
  185. package/src/batch/index.ts +4 -1
  186. package/src/batch/memory.ts +15 -10
  187. package/src/batch/openai.ts +65 -193
  188. package/src/batch/provider.ts +336 -0
  189. package/src/batch-map.ts +29 -24
  190. package/src/batch-queue.ts +200 -11
  191. package/src/budget.ts +740 -0
  192. package/src/cache.ts +681 -0
  193. package/src/context.ts +122 -76
  194. package/src/digital-objects-registry.ts +750 -0
  195. package/src/errors.ts +37 -0
  196. package/src/eval/runner.ts +63 -38
  197. package/src/eval-log/in-memory.ts +90 -0
  198. package/src/eval-log/index.ts +46 -0
  199. package/src/eval-log/types.ts +110 -0
  200. package/src/function-registry.ts +671 -0
  201. package/src/generate.ts +33 -33
  202. package/src/index.ts +325 -49
  203. package/src/logger.ts +232 -0
  204. package/src/middleware/budget.ts +171 -0
  205. package/src/middleware/cache.ts +299 -0
  206. package/src/middleware/embed-cache.ts +195 -0
  207. package/src/middleware/index.ts +23 -0
  208. package/src/middleware/trace.ts +248 -0
  209. package/src/primitives.ts +589 -62
  210. package/src/retry.ts +902 -0
  211. package/src/schema.ts +8 -17
  212. package/src/telemetry.ts +403 -0
  213. package/src/template.ts +8 -4
  214. package/src/tool-orchestration.ts +1173 -0
  215. package/src/type-guards.ts +31 -0
  216. package/src/types.ts +164 -25
  217. package/src/wrap-for-v3.ts +105 -0
  218. package/test/ai-promise.test.ts +1080 -0
  219. package/test/ai-proxy.test.ts +1 -1
  220. package/test/backward-compat.test.ts +147 -0
  221. package/test/batch-autosubmit-errors.test.ts +610 -0
  222. package/test/batch-blog-posts.test.ts +87 -129
  223. package/test/budget-tracking.test.ts +800 -0
  224. package/test/cache.test.ts +712 -0
  225. package/test/context-isolation.test.ts +687 -0
  226. package/test/core-functions.test.ts +183 -579
  227. package/test/decide.test.ts +154 -322
  228. package/test/define.test.ts +211 -8
  229. package/test/digital-objects-registry.test.ts +760 -0
  230. package/test/embedding-cache-middleware.test.ts +140 -0
  231. package/test/evals/deterministic.eval.test.ts +376 -0
  232. package/test/generate-core.test.ts +140 -229
  233. package/test/implicit-batch.test.ts +22 -65
  234. package/test/json-parse-error-handling.test.ts +463 -0
  235. package/test/retry-policy-integration.test.ts +117 -0
  236. package/test/retry.test.ts +1016 -0
  237. package/test/schema.test.ts +55 -19
  238. package/test/streaming.test.ts +316 -0
  239. package/test/template.test.ts +1164 -0
  240. package/test/tool-orchestration.test.ts +1040 -0
  241. package/test/wrap-for-v3.test.ts +612 -0
  242. package/vitest.config.js +6 -0
  243. package/vitest.config.ts +20 -0
  244. package/dist/rpc/auth.d.ts +0 -69
  245. package/dist/rpc/auth.d.ts.map +0 -1
  246. package/dist/rpc/auth.js +0 -136
  247. package/dist/rpc/auth.js.map +0 -1
  248. package/dist/rpc/client.d.ts +0 -62
  249. package/dist/rpc/client.d.ts.map +0 -1
  250. package/dist/rpc/client.js +0 -103
  251. package/dist/rpc/client.js.map +0 -1
  252. package/dist/rpc/deferred.d.ts +0 -60
  253. package/dist/rpc/deferred.d.ts.map +0 -1
  254. package/dist/rpc/deferred.js +0 -96
  255. package/dist/rpc/deferred.js.map +0 -1
  256. package/dist/rpc/index.d.ts +0 -22
  257. package/dist/rpc/index.d.ts.map +0 -1
  258. package/dist/rpc/index.js +0 -38
  259. package/dist/rpc/index.js.map +0 -1
  260. package/dist/rpc/local.d.ts +0 -42
  261. package/dist/rpc/local.d.ts.map +0 -1
  262. package/dist/rpc/local.js +0 -50
  263. package/dist/rpc/local.js.map +0 -1
  264. package/dist/rpc/server.d.ts +0 -165
  265. package/dist/rpc/server.d.ts.map +0 -1
  266. package/dist/rpc/server.js +0 -405
  267. package/dist/rpc/server.js.map +0 -1
  268. package/dist/rpc/session.d.ts +0 -32
  269. package/dist/rpc/session.d.ts.map +0 -1
  270. package/dist/rpc/session.js +0 -43
  271. package/dist/rpc/session.js.map +0 -1
  272. package/dist/rpc/transport.d.ts +0 -306
  273. package/dist/rpc/transport.d.ts.map +0 -1
  274. package/dist/rpc/transport.js +0 -731
  275. package/dist/rpc/transport.js.map +0 -1
  276. package/src/batch/anthropic.js +0 -256
  277. package/src/batch/bedrock.js +0 -584
  278. package/src/batch/cloudflare.js +0 -287
  279. package/src/batch/google.js +0 -359
  280. package/src/batch/index.js +0 -30
  281. package/src/batch/memory.js +0 -187
  282. package/src/batch/openai.js +0 -402
  283. package/src/eval/index.js +0 -7
  284. package/src/eval/models.js +0 -119
  285. package/src/eval/runner.js +0 -147
  286. package/test/schema.test.js +0 -96
@@ -2,6 +2,7 @@
2
2
  * Tests for schema conversion
3
3
  *
4
4
  * These are pure unit tests - no AI calls needed.
5
+ * Uses type-safe instanceof checks instead of accessing internal _def property.
5
6
  */
6
7
 
7
8
  import { describe, it, expect } from 'vitest'
@@ -12,54 +13,67 @@ describe('schema', () => {
12
13
  describe('string types', () => {
13
14
  it('converts simple string description to z.string()', () => {
14
15
  const result = schema('User name')
15
- expect(result._def.typeName).toBe('ZodString')
16
- expect(result._def.description).toBe('User name')
16
+ expect(result instanceof z.ZodString).toBe(true)
17
+ expect(result.description).toBe('User name')
17
18
  })
18
19
 
19
20
  it('converts (number) hint to z.number()', () => {
20
21
  const result = schema('User age (number)')
21
- expect(result._def.typeName).toBe('ZodNumber')
22
- expect(result._def.description).toBe('User age')
22
+ expect(result instanceof z.ZodNumber).toBe(true)
23
+ expect(result.description).toBe('User age')
23
24
  })
24
25
 
25
26
  it('converts (boolean) hint to z.boolean()', () => {
26
27
  const result = schema('Is active (boolean)')
27
- expect(result._def.typeName).toBe('ZodBoolean')
28
- expect(result._def.description).toBe('Is active')
28
+ expect(result instanceof z.ZodBoolean).toBe(true)
29
+ expect(result.description).toBe('Is active')
29
30
  })
30
31
 
31
32
  it('converts (integer) hint to z.number().int()', () => {
32
33
  const result = schema('Item count (integer)')
33
- expect(result._def.typeName).toBe('ZodNumber')
34
- expect(result._def.checks?.some((c: { kind: string }) => c.kind === 'int')).toBe(true)
34
+ expect(result instanceof z.ZodNumber).toBe(true)
35
+ // Verify it's an integer by testing validation behavior
36
+ expect(result.safeParse(5).success).toBe(true)
37
+ expect(result.safeParse(5.5).success).toBe(false)
35
38
  })
36
39
 
37
40
  it('converts (date) hint to z.string().datetime()', () => {
38
41
  const result = schema('Created at (date)')
39
- expect(result._def.typeName).toBe('ZodString')
40
- expect(result._def.checks?.some((c: { kind: string }) => c.kind === 'datetime')).toBe(true)
42
+ expect(result instanceof z.ZodString).toBe(true)
43
+ // Verify it validates datetime format
44
+ expect(result.safeParse('2024-01-15T10:30:00Z').success).toBe(true)
45
+ expect(result.safeParse('not-a-date').success).toBe(false)
41
46
  })
42
47
  })
43
48
 
44
49
  describe('enum types', () => {
45
50
  it('converts pipe-separated values to z.enum()', () => {
46
51
  const result = schema('pending | done | cancelled')
47
- expect(result._def.typeName).toBe('ZodEnum')
48
- expect(result._def.values).toEqual(['pending', 'done', 'cancelled'])
52
+ expect(result instanceof z.ZodEnum).toBe(true)
53
+ // Verify enum values through validation
54
+ expect(result.safeParse('pending').success).toBe(true)
55
+ expect(result.safeParse('done').success).toBe(true)
56
+ expect(result.safeParse('cancelled').success).toBe(true)
57
+ expect(result.safeParse('invalid').success).toBe(false)
49
58
  })
50
59
 
51
60
  it('handles spaces around pipe', () => {
52
61
  const result = schema('yes | no | maybe')
53
- expect(result._def.values).toEqual(['yes', 'no', 'maybe'])
62
+ expect(result instanceof z.ZodEnum).toBe(true)
63
+ expect(result.safeParse('yes').success).toBe(true)
64
+ expect(result.safeParse('no').success).toBe(true)
65
+ expect(result.safeParse('maybe').success).toBe(true)
54
66
  })
55
67
  })
56
68
 
57
69
  describe('array types', () => {
58
70
  it('converts [string] to z.array(z.string())', () => {
59
71
  const result = schema(['List of items'])
60
- expect(result._def.typeName).toBe('ZodArray')
61
- expect(result._def.type._def.typeName).toBe('ZodString')
62
- expect(result._def.description).toBe('List of items')
72
+ expect(result instanceof z.ZodArray).toBe(true)
73
+ expect(result.description).toBe('List of items')
74
+ // Verify array of strings validation
75
+ expect(result.safeParse(['a', 'b', 'c']).success).toBe(true)
76
+ expect(result.safeParse([1, 2, 3]).success).toBe(false)
63
77
  })
64
78
  })
65
79
 
@@ -69,7 +83,10 @@ describe('schema', () => {
69
83
  name: 'User name',
70
84
  age: 'Age (number)',
71
85
  })
72
- expect(result._def.typeName).toBe('ZodObject')
86
+ expect(result instanceof z.ZodObject).toBe(true)
87
+ // Verify object validation
88
+ expect(result.safeParse({ name: 'John', age: 30 }).success).toBe(true)
89
+ expect(result.safeParse({ name: 'John', age: 'thirty' }).success).toBe(false)
73
90
  })
74
91
 
75
92
  it('handles nested objects', () => {
@@ -81,7 +98,16 @@ describe('schema', () => {
81
98
  },
82
99
  },
83
100
  })
84
- expect(result._def.typeName).toBe('ZodObject')
101
+ expect(result instanceof z.ZodObject).toBe(true)
102
+ // Verify nested object validation
103
+ expect(
104
+ result.safeParse({
105
+ user: {
106
+ name: 'John',
107
+ profile: { bio: 'A developer' },
108
+ },
109
+ }).success
110
+ ).toBe(true)
85
111
  })
86
112
 
87
113
  it('handles mixed types in object', () => {
@@ -92,7 +118,17 @@ describe('schema', () => {
92
118
  status: 'pending | done',
93
119
  tags: ['Tags'],
94
120
  })
95
- expect(result._def.typeName).toBe('ZodObject')
121
+ expect(result instanceof z.ZodObject).toBe(true)
122
+ // Verify mixed type validation
123
+ expect(
124
+ result.safeParse({
125
+ name: 'Test',
126
+ count: 5,
127
+ active: true,
128
+ status: 'pending',
129
+ tags: ['tag1', 'tag2'],
130
+ }).success
131
+ ).toBe(true)
96
132
  })
97
133
  })
98
134
 
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Tests for streaming integration with AIPromise
3
+ *
4
+ * These tests verify the streaming API for AI functions:
5
+ * - ai().stream() returns AsyncIterable of chunks
6
+ * - Streaming with property access
7
+ * - Streaming with dependencies
8
+ * - Backpressure handling
9
+ * - Stream transformation (map/filter)
10
+ */
11
+
12
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
13
+ import { AIPromise, createTextPromise, createObjectPromise, createListPromise } from '../src/ai-promise.js'
14
+ import { ai, list, write } from '../src/primitives.js'
15
+
16
+ // Skip integration tests if no gateway configured
17
+ const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
18
+
19
+ // ============================================================================
20
+ // Unit Tests (Mock-based)
21
+ // ============================================================================
22
+
23
+ describe('AIPromise.stream() - Unit Tests', () => {
24
+ describe('Basic Streaming', () => {
25
+ it('should have a stream() method on AIPromise', () => {
26
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
27
+ expect(typeof promise.stream).toBe('function')
28
+ })
29
+
30
+ it('stream() should return an object with AsyncIterable interface', async () => {
31
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
32
+ const stream = promise.stream()
33
+
34
+ // Should have Symbol.asyncIterator
35
+ expect(typeof stream[Symbol.asyncIterator]).toBe('function')
36
+ })
37
+
38
+ it('stream() should return an object with textStream property', async () => {
39
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
40
+ const stream = promise.stream()
41
+
42
+ // Should have textStream for text generation
43
+ expect(stream.textStream).toBeDefined()
44
+ })
45
+
46
+ it('stream() should return an object with partialObjectStream property', async () => {
47
+ const promise = new AIPromise<{ name: string }>('test prompt', { type: 'object' })
48
+ const stream = promise.stream()
49
+
50
+ // Should have partialObjectStream for object generation
51
+ expect(stream.partialObjectStream).toBeDefined()
52
+ })
53
+ })
54
+
55
+ describe('StreamingAIPromise Interface', () => {
56
+ it('should be awaitable to get final result', async () => {
57
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
58
+ const stream = promise.stream()
59
+
60
+ // Should be thenable (Promise-like)
61
+ expect(typeof stream.then).toBe('function')
62
+ })
63
+
64
+ it('should have result property for final value', async () => {
65
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
66
+ const stream = promise.stream()
67
+
68
+ // result should be a Promise
69
+ expect(stream.result).toBeDefined()
70
+ expect(typeof stream.result.then).toBe('function')
71
+ })
72
+
73
+ it('should support abort controller', async () => {
74
+ const promise = new AIPromise<string>('test prompt', { type: 'text' })
75
+ const controller = new AbortController()
76
+ const stream = promise.stream({ abortSignal: controller.signal })
77
+
78
+ expect(stream).toBeDefined()
79
+ })
80
+ })
81
+
82
+ describe('Property Access on Streaming Results', () => {
83
+ it('should track property access for schema inference on stream', () => {
84
+ const promise = new AIPromise<{ summary: string; points: string[] }>('test', { type: 'object' })
85
+
86
+ // Access properties
87
+ const { summary, points } = promise
88
+
89
+ // The stream should include these properties
90
+ const stream = promise.stream()
91
+ expect(stream).toBeDefined()
92
+ })
93
+ })
94
+ })
95
+
96
+ // ============================================================================
97
+ // Integration Tests (Real AI calls)
98
+ // ============================================================================
99
+
100
+ describe.skipIf(!hasGateway)('AIPromise.stream() - Integration Tests', () => {
101
+ describe('Text Streaming', () => {
102
+ it('should stream text chunks from ai template', async () => {
103
+ const promise = write`Say hello in 3 words`
104
+ const stream = promise.stream()
105
+
106
+ const chunks: string[] = []
107
+ for await (const chunk of stream.textStream) {
108
+ chunks.push(chunk)
109
+ }
110
+
111
+ expect(chunks.length).toBeGreaterThan(0)
112
+ const fullText = chunks.join('')
113
+ expect(fullText.length).toBeGreaterThan(0)
114
+ }, 60000)
115
+
116
+ it('should allow awaiting final result after streaming', async () => {
117
+ const promise = write`Say hello`
118
+ const stream = promise.stream()
119
+
120
+ // Consume stream
121
+ for await (const _ of stream.textStream) {
122
+ // consume
123
+ }
124
+
125
+ // Get final result
126
+ const result = await stream.result
127
+ expect(typeof result).toBe('string')
128
+ expect(result.length).toBeGreaterThan(0)
129
+ }, 60000)
130
+ })
131
+
132
+ describe('Object Streaming', () => {
133
+ it('should stream partial objects', async () => {
134
+ const promise = ai`Generate a person with name and age`
135
+
136
+ // Access properties to set schema
137
+ const { name, age } = promise
138
+
139
+ const stream = promise.stream()
140
+
141
+ const partials: unknown[] = []
142
+ for await (const partial of stream.partialObjectStream) {
143
+ partials.push(partial)
144
+ }
145
+
146
+ expect(partials.length).toBeGreaterThan(0)
147
+
148
+ // Last partial should have complete object
149
+ const final = partials[partials.length - 1] as { name?: string; age?: number }
150
+ expect(final.name).toBeDefined()
151
+ })
152
+
153
+ it('should support property access on stream result', async () => {
154
+ const promise = ai`Generate recipe: name, ingredients list, steps list`
155
+ const { name, ingredients, steps } = promise
156
+
157
+ const stream = promise.stream()
158
+ const result = await stream.result
159
+
160
+ expect(result).toHaveProperty('name')
161
+ expect(result).toHaveProperty('ingredients')
162
+ expect(result).toHaveProperty('steps')
163
+ }, 120000)
164
+ })
165
+
166
+ describe('List Streaming', () => {
167
+ it('should stream list items one by one', async () => {
168
+ const promise = list`3 colors`
169
+ const stream = promise.stream()
170
+
171
+ const items: string[] = []
172
+ for await (const item of stream) {
173
+ items.push(item as string)
174
+ }
175
+
176
+ expect(items.length).toBeGreaterThanOrEqual(3)
177
+ })
178
+ })
179
+
180
+ describe('Streaming with Dependencies', () => {
181
+ it('should resolve dependencies before streaming', async () => {
182
+ const topic = ai`Pick a random topic: science, art, or music`
183
+ const essay = write`Write a paragraph about ${topic}`
184
+
185
+ const stream = essay.stream()
186
+
187
+ const chunks: string[] = []
188
+ for await (const chunk of stream.textStream) {
189
+ chunks.push(chunk)
190
+ }
191
+
192
+ const fullText = chunks.join('')
193
+ expect(fullText.length).toBeGreaterThan(50)
194
+ })
195
+
196
+ it('should handle mixed streaming and non-streaming in pipeline', async () => {
197
+ // First get a topic (non-streaming)
198
+ const topic = await ai`Pick: TypeScript or Python`
199
+
200
+ // Then stream based on that result
201
+ const explanation = write`Explain why ${topic} is great`
202
+ const stream = explanation.stream()
203
+
204
+ const result = await stream.result
205
+ expect(result.length).toBeGreaterThan(0)
206
+ }, 120000)
207
+ })
208
+
209
+ describe('Backpressure Handling', () => {
210
+ it('should handle slow consumers without memory issues', async () => {
211
+ const promise = write`Generate a short story about a dragon in 2 sentences`
212
+ const stream = promise.stream()
213
+
214
+ const chunks: string[] = []
215
+ for await (const chunk of stream.textStream) {
216
+ // Simulate slow consumer
217
+ await new Promise(resolve => setTimeout(resolve, 5))
218
+ chunks.push(chunk)
219
+ }
220
+
221
+ expect(chunks.length).toBeGreaterThan(0)
222
+ }, 120000)
223
+
224
+ it('should support early termination/cancellation', async () => {
225
+ const controller = new AbortController()
226
+ const promise = write`Generate a very long story`
227
+ const stream = promise.stream({ abortSignal: controller.signal })
228
+
229
+ const chunks: string[] = []
230
+ let count = 0
231
+
232
+ try {
233
+ for await (const chunk of stream.textStream) {
234
+ chunks.push(chunk)
235
+ count++
236
+ if (count >= 5) {
237
+ controller.abort()
238
+ break
239
+ }
240
+ }
241
+ } catch (error) {
242
+ // AbortError is expected
243
+ if (!(error instanceof Error && error.name === 'AbortError')) {
244
+ throw error
245
+ }
246
+ }
247
+
248
+ // Should have stopped early
249
+ expect(count).toBeLessThanOrEqual(10)
250
+ })
251
+ })
252
+
253
+ describe('Stream Transformation', () => {
254
+ it('should support map on streaming list', async () => {
255
+ const colors = list`3 colors`
256
+
257
+ // Map should work on stream results
258
+ const result = await colors.map(color => ({
259
+ color,
260
+ hex: ai`hex code for ${color}`,
261
+ }))
262
+
263
+ expect(Array.isArray(result)).toBe(true)
264
+ expect(result.length).toBeGreaterThanOrEqual(3)
265
+ })
266
+
267
+ it('should support filter-like operations via streaming', async () => {
268
+ const numbers = list`5 random numbers between 1-100`
269
+ const stream = numbers.stream()
270
+
271
+ const allItems: string[] = []
272
+ for await (const item of stream) {
273
+ allItems.push(item as string)
274
+ }
275
+
276
+ expect(allItems.length).toBe(5)
277
+ })
278
+ })
279
+ })
280
+
281
+ // ============================================================================
282
+ // Stream Batching Tests
283
+ // ============================================================================
284
+
285
+ describe('Stream Batching', () => {
286
+ it('should support streaming mode in batch operations', async () => {
287
+ const promise = new AIPromise<string[]>('test', { type: 'list' })
288
+ const stream = promise.stream()
289
+
290
+ // Stream batching should be supported
291
+ expect(stream).toBeDefined()
292
+ })
293
+ })
294
+
295
+ // ============================================================================
296
+ // Type Safety Tests
297
+ // ============================================================================
298
+
299
+ describe('Streaming Type Safety', () => {
300
+ it('should preserve types through streaming', () => {
301
+ // Text streaming should yield strings
302
+ const textPromise = createTextPromise('test')
303
+ const textStream = textPromise.stream()
304
+ expect(textStream.textStream).toBeDefined()
305
+
306
+ // Object streaming should yield partial objects
307
+ const objectPromise = createObjectPromise<{ name: string }>('test')
308
+ const objectStream = objectPromise.stream()
309
+ expect(objectStream.partialObjectStream).toBeDefined()
310
+
311
+ // List streaming should yield items
312
+ const listPromise = createListPromise('test')
313
+ const listStream = listPromise.stream()
314
+ expect(listStream).toBeDefined()
315
+ })
316
+ })