ai-functions 2.1.3 → 2.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 (284) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +90 -1
  3. package/README.md +38 -0
  4. package/dist/ai-promise.d.ts +3 -3
  5. package/dist/ai-promise.d.ts.map +1 -1
  6. package/dist/ai-promise.js +135 -64
  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 +51 -858
  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.map +1 -1
  56. package/dist/budget.js +27 -14
  57. package/dist/budget.js.map +1 -1
  58. package/dist/cache.d.ts +23 -0
  59. package/dist/cache.d.ts.map +1 -1
  60. package/dist/cache.js +36 -15
  61. package/dist/cache.js.map +1 -1
  62. package/dist/context.d.ts +26 -8
  63. package/dist/context.d.ts.map +1 -1
  64. package/dist/context.js +64 -62
  65. package/dist/context.js.map +1 -1
  66. package/dist/digital-objects-registry.d.ts +229 -0
  67. package/dist/digital-objects-registry.d.ts.map +1 -0
  68. package/dist/digital-objects-registry.js +617 -0
  69. package/dist/digital-objects-registry.js.map +1 -0
  70. package/dist/embeddings.d.ts +2 -2
  71. package/dist/embeddings.d.ts.map +1 -1
  72. package/dist/errors.d.ts +22 -0
  73. package/dist/errors.d.ts.map +1 -0
  74. package/dist/errors.js +35 -0
  75. package/dist/errors.js.map +1 -0
  76. package/dist/eval/runner.d.ts +8 -0
  77. package/dist/eval/runner.d.ts.map +1 -1
  78. package/dist/eval/runner.js +41 -35
  79. package/dist/eval/runner.js.map +1 -1
  80. package/dist/eval-log/in-memory.d.ts +34 -0
  81. package/dist/eval-log/in-memory.d.ts.map +1 -0
  82. package/dist/eval-log/in-memory.js +84 -0
  83. package/dist/eval-log/in-memory.js.map +1 -0
  84. package/dist/eval-log/index.d.ts +29 -0
  85. package/dist/eval-log/index.d.ts.map +1 -0
  86. package/dist/eval-log/index.js +39 -0
  87. package/dist/eval-log/index.js.map +1 -0
  88. package/dist/eval-log/types.d.ts +101 -0
  89. package/dist/eval-log/types.d.ts.map +1 -0
  90. package/dist/eval-log/types.js +16 -0
  91. package/dist/eval-log/types.js.map +1 -0
  92. package/dist/function-registry.d.ts +176 -0
  93. package/dist/function-registry.d.ts.map +1 -0
  94. package/dist/function-registry.js +685 -0
  95. package/dist/function-registry.js.map +1 -0
  96. package/dist/generate.d.ts +9 -3
  97. package/dist/generate.d.ts.map +1 -1
  98. package/dist/generate.js +18 -18
  99. package/dist/generate.js.map +1 -1
  100. package/dist/index.d.ts +18 -11
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +35 -18
  103. package/dist/index.js.map +1 -1
  104. package/dist/logger.d.ts +118 -0
  105. package/dist/logger.d.ts.map +1 -0
  106. package/dist/logger.js +187 -0
  107. package/dist/logger.js.map +1 -0
  108. package/dist/middleware/budget.d.ts +84 -0
  109. package/dist/middleware/budget.d.ts.map +1 -0
  110. package/dist/middleware/budget.js +110 -0
  111. package/dist/middleware/budget.js.map +1 -0
  112. package/dist/middleware/cache.d.ts +103 -0
  113. package/dist/middleware/cache.d.ts.map +1 -0
  114. package/dist/middleware/cache.js +228 -0
  115. package/dist/middleware/cache.js.map +1 -0
  116. package/dist/middleware/embed-cache.d.ts +99 -0
  117. package/dist/middleware/embed-cache.d.ts.map +1 -0
  118. package/dist/middleware/embed-cache.js +128 -0
  119. package/dist/middleware/embed-cache.js.map +1 -0
  120. package/dist/middleware/index.d.ts +11 -0
  121. package/dist/middleware/index.d.ts.map +1 -0
  122. package/dist/middleware/index.js +11 -0
  123. package/dist/middleware/index.js.map +1 -0
  124. package/dist/middleware/trace.d.ts +103 -0
  125. package/dist/middleware/trace.d.ts.map +1 -0
  126. package/dist/middleware/trace.js +176 -0
  127. package/dist/middleware/trace.js.map +1 -0
  128. package/dist/primitives.d.ts +120 -1
  129. package/dist/primitives.d.ts.map +1 -1
  130. package/dist/primitives.js +398 -26
  131. package/dist/primitives.js.map +1 -1
  132. package/dist/retry.d.ts +66 -1
  133. package/dist/retry.d.ts.map +1 -1
  134. package/dist/retry.js +115 -8
  135. package/dist/retry.js.map +1 -1
  136. package/dist/sandbox.d.ts +36 -0
  137. package/dist/sandbox.d.ts.map +1 -0
  138. package/dist/sandbox.js +44 -0
  139. package/dist/sandbox.js.map +1 -0
  140. package/dist/schema.js +2 -2
  141. package/dist/schema.js.map +1 -1
  142. package/dist/telemetry.d.ts +128 -0
  143. package/dist/telemetry.d.ts.map +1 -0
  144. package/dist/telemetry.js +285 -0
  145. package/dist/telemetry.js.map +1 -0
  146. package/dist/template.d.ts.map +1 -1
  147. package/dist/template.js +6 -1
  148. package/dist/template.js.map +1 -1
  149. package/dist/tool-orchestration.d.ts +66 -4
  150. package/dist/tool-orchestration.d.ts.map +1 -1
  151. package/dist/tool-orchestration.js +123 -23
  152. package/dist/tool-orchestration.js.map +1 -1
  153. package/dist/type-guards.d.ts +28 -0
  154. package/dist/type-guards.d.ts.map +1 -0
  155. package/dist/type-guards.js +29 -0
  156. package/dist/type-guards.js.map +1 -0
  157. package/dist/types.d.ts +155 -19
  158. package/dist/types.d.ts.map +1 -1
  159. package/dist/types.js +36 -1
  160. package/dist/types.js.map +1 -1
  161. package/dist/wrap-for-v3.d.ts +80 -0
  162. package/dist/wrap-for-v3.d.ts.map +1 -0
  163. package/dist/wrap-for-v3.js +89 -0
  164. package/dist/wrap-for-v3.js.map +1 -0
  165. package/examples/00-quickstart.ts +232 -0
  166. package/examples/01-rag-chatbot.ts +212 -0
  167. package/examples/02-multi-agent-research.ts +290 -0
  168. package/examples/03-email-classification.ts +379 -0
  169. package/examples/04-content-moderation.ts +400 -0
  170. package/examples/05-document-extraction.ts +455 -0
  171. package/examples/06-streaming-chat-nextjs.ts +437 -0
  172. package/examples/07-cloudflare-worker.ts +483 -0
  173. package/examples/08-batch-processing.ts +491 -0
  174. package/examples/09-budget-constrained.ts +527 -0
  175. package/examples/10-tool-orchestration.ts +565 -0
  176. package/examples/11-retry-resilience.ts +403 -0
  177. package/examples/12-caching-strategies.ts +422 -0
  178. package/examples/README.md +145 -0
  179. package/package.json +29 -25
  180. package/src/ai-promise.ts +226 -140
  181. package/src/ai-schemas.ts +122 -0
  182. package/src/ai.ts +71 -1176
  183. package/src/batch/anthropic.ts +96 -161
  184. package/src/batch/bedrock.ts +203 -454
  185. package/src/batch/cloudflare.ts +99 -282
  186. package/src/batch/google.ts +91 -297
  187. package/src/batch/index.ts +4 -1
  188. package/src/batch/memory.ts +15 -10
  189. package/src/batch/openai.ts +65 -193
  190. package/src/batch/provider.ts +336 -0
  191. package/src/batch-map.ts +29 -24
  192. package/src/batch-queue.ts +200 -11
  193. package/src/budget.ts +31 -18
  194. package/src/cache.ts +45 -17
  195. package/src/context.ts +106 -77
  196. package/src/digital-objects-registry.ts +750 -0
  197. package/src/errors.ts +37 -0
  198. package/src/eval/runner.ts +60 -36
  199. package/src/eval-log/in-memory.ts +90 -0
  200. package/src/eval-log/index.ts +46 -0
  201. package/src/eval-log/types.ts +110 -0
  202. package/src/function-registry.ts +874 -0
  203. package/src/generate.ts +33 -28
  204. package/src/index.ts +122 -21
  205. package/src/logger.ts +232 -0
  206. package/src/middleware/budget.ts +171 -0
  207. package/src/middleware/cache.ts +299 -0
  208. package/src/middleware/embed-cache.ts +195 -0
  209. package/src/middleware/index.ts +23 -0
  210. package/src/middleware/trace.ts +248 -0
  211. package/src/primitives.ts +589 -62
  212. package/src/retry.ts +144 -18
  213. package/src/sandbox.ts +52 -0
  214. package/src/schema.ts +8 -8
  215. package/src/telemetry.ts +403 -0
  216. package/src/template.ts +8 -4
  217. package/src/tool-orchestration.ts +213 -48
  218. package/src/type-guards.ts +31 -0
  219. package/src/types.ts +186 -27
  220. package/src/wrap-for-v3.ts +105 -0
  221. package/test/ai-promise.test.ts +1080 -0
  222. package/test/ai-proxy.test.ts +1 -1
  223. package/test/batch-autosubmit-errors.test.ts +49 -37
  224. package/test/batch-blog-posts.test.ts +87 -129
  225. package/test/core-functions.test.ts +183 -579
  226. package/test/decide.test.ts +154 -322
  227. package/test/define.test.ts +211 -8
  228. package/test/digital-objects-registry.test.ts +760 -0
  229. package/test/embedding-cache-middleware.test.ts +140 -0
  230. package/test/fill-template.test.ts +89 -0
  231. package/test/generate-core.test.ts +140 -229
  232. package/test/implicit-batch.test.ts +22 -65
  233. package/test/retry-policy-integration.test.ts +117 -0
  234. package/test/sandbox-execution.test.ts +155 -0
  235. package/test/schema.test.ts +55 -19
  236. package/test/template.test.ts +1164 -0
  237. package/test/tool-orchestration.test.ts +270 -0
  238. package/test/wrap-for-v3.test.ts +612 -0
  239. package/vitest.config.js +6 -0
  240. package/vitest.config.ts +20 -0
  241. package/LICENSE +0 -21
  242. package/dist/rpc/auth.d.ts +0 -69
  243. package/dist/rpc/auth.d.ts.map +0 -1
  244. package/dist/rpc/auth.js +0 -136
  245. package/dist/rpc/auth.js.map +0 -1
  246. package/dist/rpc/client.d.ts +0 -62
  247. package/dist/rpc/client.d.ts.map +0 -1
  248. package/dist/rpc/client.js +0 -103
  249. package/dist/rpc/client.js.map +0 -1
  250. package/dist/rpc/deferred.d.ts +0 -60
  251. package/dist/rpc/deferred.d.ts.map +0 -1
  252. package/dist/rpc/deferred.js +0 -96
  253. package/dist/rpc/deferred.js.map +0 -1
  254. package/dist/rpc/index.d.ts +0 -22
  255. package/dist/rpc/index.d.ts.map +0 -1
  256. package/dist/rpc/index.js +0 -38
  257. package/dist/rpc/index.js.map +0 -1
  258. package/dist/rpc/local.d.ts +0 -42
  259. package/dist/rpc/local.d.ts.map +0 -1
  260. package/dist/rpc/local.js +0 -50
  261. package/dist/rpc/local.js.map +0 -1
  262. package/dist/rpc/server.d.ts +0 -165
  263. package/dist/rpc/server.d.ts.map +0 -1
  264. package/dist/rpc/server.js +0 -405
  265. package/dist/rpc/server.js.map +0 -1
  266. package/dist/rpc/session.d.ts +0 -32
  267. package/dist/rpc/session.d.ts.map +0 -1
  268. package/dist/rpc/session.js +0 -43
  269. package/dist/rpc/session.js.map +0 -1
  270. package/dist/rpc/transport.d.ts +0 -306
  271. package/dist/rpc/transport.d.ts.map +0 -1
  272. package/dist/rpc/transport.js +0 -731
  273. package/dist/rpc/transport.js.map +0 -1
  274. package/src/batch/anthropic.js +0 -256
  275. package/src/batch/bedrock.js +0 -584
  276. package/src/batch/cloudflare.js +0 -287
  277. package/src/batch/google.js +0 -359
  278. package/src/batch/index.js +0 -30
  279. package/src/batch/memory.js +0 -187
  280. package/src/batch/openai.js +0 -402
  281. package/src/eval/index.js +0 -7
  282. package/src/eval/models.js +0 -119
  283. package/src/eval/runner.js +0 -147
  284. package/test/schema.test.js +0 -96
@@ -0,0 +1,874 @@
1
+ /**
2
+ * Function Registry - Storage and management of defined AI functions
3
+ *
4
+ * This module provides the registry for storing and retrieving defined functions,
5
+ * including the global registry and factory for creating isolated registries.
6
+ */
7
+
8
+ import { generateObject } from './generate.js'
9
+ import type { SimpleSchema } from './schema.js'
10
+ import type {
11
+ AIFunctionDefinition,
12
+ JSONSchema,
13
+ FunctionDefinition,
14
+ DefinedFunction,
15
+ CodeFunctionDefinition,
16
+ CodeGenerationDefinition,
17
+ GenerativeFunctionDefinition,
18
+ AgenticFunctionDefinition,
19
+ HumanFunctionDefinition,
20
+ HumanFunctionPending,
21
+ FunctionRegistry,
22
+ } from './types.js'
23
+ import { PENDING_HUMAN_RESULT_SYMBOL } from './types.js'
24
+ import { schema as convertSchema, type SimpleSchema as SimpleSchemaType } from './schema.js'
25
+ import { getLogger } from './logger.js'
26
+ import { runInSandbox, type SandboxEnv } from './sandbox.js'
27
+
28
+ // ============================================================================
29
+ // JSON Schema Conversion
30
+ // ============================================================================
31
+
32
+ /**
33
+ * Convert args schema to JSON Schema
34
+ */
35
+ export function convertArgsToJSONSchema(args: unknown): JSONSchema {
36
+ // If it's already a JSON schema-like object
37
+ if (typeof args === 'object' && args !== null && 'type' in args) {
38
+ return args as JSONSchema
39
+ }
40
+
41
+ // Convert SimpleSchema to JSON Schema
42
+ const properties: Record<string, JSONSchema> = {}
43
+ const required: string[] = []
44
+
45
+ if (typeof args === 'object' && args !== null) {
46
+ for (const [key, value] of Object.entries(args as Record<string, unknown>)) {
47
+ required.push(key) // All properties required for cross-provider compatibility
48
+ properties[key] = convertValueToJSONSchema(value)
49
+ }
50
+ }
51
+
52
+ return {
53
+ type: 'object',
54
+ properties,
55
+ required,
56
+ additionalProperties: false, // Required for OpenAI compatibility
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Convert a single value to JSON Schema
62
+ */
63
+ function convertValueToJSONSchema(value: unknown): JSONSchema {
64
+ if (typeof value === 'string') {
65
+ // Check for type hints: 'description (number)', 'description (boolean)', etc.
66
+ const typeMatch = value.match(/^(.+?)\s*\((number|boolean|integer|date)\)$/i)
67
+ if (typeMatch) {
68
+ const description = typeMatch[1]!
69
+ const type = typeMatch[2]!
70
+ switch (type.toLowerCase()) {
71
+ case 'number':
72
+ return { type: 'number', description: description.trim() }
73
+ case 'integer':
74
+ return { type: 'integer', description: description.trim() }
75
+ case 'boolean':
76
+ return { type: 'boolean', description: description.trim() }
77
+ case 'date':
78
+ return { type: 'string', format: 'date-time', description: description.trim() }
79
+ }
80
+ }
81
+
82
+ // Check for enum: 'option1 | option2 | option3'
83
+ if (value.includes(' | ')) {
84
+ const options = value.split(' | ').map((s) => s.trim())
85
+ return { type: 'string', enum: options }
86
+ }
87
+
88
+ return { type: 'string', description: value }
89
+ }
90
+
91
+ if (Array.isArray(value) && value.length === 1) {
92
+ const [desc] = value
93
+ if (typeof desc === 'string') {
94
+ return { type: 'array', items: { type: 'string' }, description: desc }
95
+ }
96
+ if (typeof desc === 'number') {
97
+ return { type: 'array', items: { type: 'number' } }
98
+ }
99
+ return { type: 'array', items: convertValueToJSONSchema(desc) }
100
+ }
101
+
102
+ if (typeof value === 'object' && value !== null) {
103
+ return convertArgsToJSONSchema(value)
104
+ }
105
+
106
+ return { type: 'string' }
107
+ }
108
+
109
+ // ============================================================================
110
+ // Template Utilities
111
+ // ============================================================================
112
+
113
+ /**
114
+ * Fill template with values
115
+ */
116
+ export function fillTemplate(template: string, args: Record<string, unknown>): string {
117
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
118
+ const v = args[key] ?? ''
119
+ if (typeof v === 'object' && v !== null) {
120
+ return JSON.stringify(v)
121
+ }
122
+ return String(v)
123
+ })
124
+ }
125
+
126
+ // ============================================================================
127
+ // Function Executors
128
+ // ============================================================================
129
+
130
+ /**
131
+ * Execute a **deterministic** code function.
132
+ *
133
+ * `Code` is the deterministic kind: no model is consulted at call time. This
134
+ * invokes the definition's `handler` (canonical) or, if only an inline `code`
135
+ * body was supplied, deterministically compiles and runs it. The result is
136
+ * returned directly.
137
+ *
138
+ * This is a deliberate change from the previous behavior, where `type: 'code'`
139
+ * LLM-generated source at call time. That code-*authoring* behavior now lives
140
+ * in {@link generateAndRunCode} (and the `generate('code', …)` primitive), so
141
+ * that `Code` can carry the "deterministic handler" contract consumers depend
142
+ * on (ADR-0033). See the package README migration note.
143
+ *
144
+ * Inline `code` bodies are executed in ai-evaluate's V8-isolate sandbox — never
145
+ * via `new Function`/`eval`. Execution stays deterministic: no model is ever
146
+ * consulted on this path.
147
+ *
148
+ * @param env - Optional host Workers env (carrying `LOADER`) for the sandbox;
149
+ * when omitted the inline-code path falls back to the Miniflare-backed Node
150
+ * runtime. Ignored when a `handler` is supplied (direct call, no sandbox).
151
+ *
152
+ * @throws if neither `handler` nor `code` is provided, or if an inline `code`
153
+ * body is in a non-evaluable language.
154
+ */
155
+ async function executeCodeFunction<TOutput, TInput>(
156
+ definition: CodeFunctionDefinition<TOutput, TInput>,
157
+ args: TInput,
158
+ env?: SandboxEnv
159
+ ): Promise<TOutput> {
160
+ const { handler, code, language = 'typescript', name } = definition
161
+
162
+ if (typeof handler === 'function') {
163
+ return await handler(args)
164
+ }
165
+
166
+ if (typeof code === 'string' && code.length > 0) {
167
+ return await runInlineCode<TOutput, TInput>(code, args, language, name, env)
168
+ }
169
+
170
+ throw new Error(
171
+ `Code function '${name}' has no handler or inline code. ` +
172
+ `'code' functions are deterministic and require a handler: (input) => output ` +
173
+ `(or an inline 'code' body). To have a model *author* code instead, use ` +
174
+ `generateAndRunCode() / generateCode() or define a 'generative' function.`
175
+ )
176
+ }
177
+
178
+ /**
179
+ * Deterministically run an inline `code` body for a Code function in the
180
+ * ai-evaluate V8-isolate sandbox.
181
+ *
182
+ * The body is treated as a function whose `return` value is the result; the
183
+ * parsed `args` are exposed as a top-level `args` binding inside the sandbox.
184
+ * Only the JS/TS-compatible languages can be evaluated; other languages are
185
+ * carried as metadata for an external runtime and are rejected here.
186
+ *
187
+ * No model is involved — the same body always produces the same behavior. This
188
+ * replaces the former `new Function(...)` path: `new Function`/`eval` are
189
+ * banned in this package (broken under Workers, unsandboxed under Node).
190
+ *
191
+ * Limitation: `args` are injected by JSON-serializing them into the sandbox
192
+ * script (`JSON.parse(<json>)`), so only JSON-serializable inputs are
193
+ * supported on the inline-`code` path. Pass a `handler` for non-serializable
194
+ * inputs (functions, class instances, etc.).
195
+ *
196
+ * @param env - Optional host Workers env (carrying `LOADER`) for the sandbox;
197
+ * when omitted, runs against the Miniflare-backed Node runtime.
198
+ */
199
+ async function runInlineCode<TOutput, TInput>(
200
+ code: string,
201
+ args: TInput,
202
+ language: string,
203
+ name: string,
204
+ env?: SandboxEnv
205
+ ): Promise<TOutput> {
206
+ if (language !== 'typescript' && language !== 'javascript') {
207
+ throw new Error(
208
+ `Code function '${name}' has an inline 'code' body in language '${language}', ` +
209
+ `which cannot be evaluated in the sandbox. Pass a 'handler' instead, or run it ` +
210
+ `in an external deterministic runtime.`
211
+ )
212
+ }
213
+
214
+ const body = /\breturn\b/.test(code) ? code : `return (${code})`
215
+
216
+ // Inject args deterministically by serializing them into the sandbox script.
217
+ // (JSON-serializable inputs only — see the doc comment.)
218
+ let argsJson: string
219
+ try {
220
+ argsJson = JSON.stringify(args ?? null)
221
+ } catch (e) {
222
+ throw new Error(
223
+ `Code function '${name}' received non-JSON-serializable args for its inline 'code' ` +
224
+ `body: ${(e as Error).message}. Pass a 'handler' for non-serializable inputs.`
225
+ )
226
+ }
227
+
228
+ const script = `const args = JSON.parse(${JSON.stringify(argsJson)});\n${body}`
229
+
230
+ const result = await runInSandbox({ script }, env)
231
+
232
+ if (result.success === false) {
233
+ throw new Error(
234
+ `Code function '${name}' failed in the sandbox: ${result.error ?? 'unknown error'}`
235
+ )
236
+ }
237
+
238
+ return result.value as TOutput
239
+ }
240
+
241
+ /**
242
+ * Author code with a model — the explicit, opt-in code-*generation* path.
243
+ *
244
+ * This is the behavior `type: 'code'` used to have implicitly at call time.
245
+ * It has been split out so that `Code` functions can be deterministic. Calling
246
+ * this **does** consult a model and returns the generated source as a string;
247
+ * it does not produce a deterministic, repeatable handler.
248
+ *
249
+ * @param definition - The code-authoring spec ({@link CodeGenerationDefinition})
250
+ * @param args - Concrete inputs / refinements for the requested code
251
+ * @returns The generated source code as a string
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * import { generateCode } from 'ai-functions'
256
+ *
257
+ * const src = await generateCode(
258
+ * { name: 'calculateTax', args: { amount: '(number)', rate: '(number)' }, language: 'typescript' },
259
+ * { amount: 100, rate: 0.2 }
260
+ * )
261
+ * ```
262
+ */
263
+ export async function generateCode<TInput>(
264
+ definition: CodeGenerationDefinition<TInput>,
265
+ args?: TInput
266
+ ): Promise<string> {
267
+ const {
268
+ name,
269
+ description,
270
+ language = 'typescript',
271
+ instructions,
272
+ returnType,
273
+ model = 'sonnet',
274
+ } = definition
275
+
276
+ const argsDescription = JSON.stringify(args ?? definition.args, null, 2)
277
+
278
+ const result = await generateObject({
279
+ model,
280
+ schema: {
281
+ code: `The complete ${language} implementation code. Output ONLY the raw code without markdown formatting or code blocks.`,
282
+ },
283
+ system: `You are an expert ${language} developer. Generate clean, well-documented, production-ready code. Output ONLY the code itself, without any markdown code fences or language tags.`,
284
+ prompt: `Generate a ${language} function/query with the following specification:
285
+
286
+ Name: ${name}
287
+ Description: ${description || 'No description provided'}
288
+ Arguments: ${argsDescription}
289
+ Return Type: ${JSON.stringify(returnType)}
290
+
291
+ ${instructions ? `Additional Instructions: ${instructions}` : ''}
292
+
293
+ Requirements:
294
+ - Include appropriate comments/documentation
295
+ - Follow best practices for ${language}
296
+ - Handle edge cases appropriately
297
+ - Return ONLY the code without markdown formatting`,
298
+ })
299
+
300
+ return (result.object as { code: string }).code
301
+ }
302
+
303
+ /**
304
+ * Result of {@link generateAndRunCode}: the executed value plus the artifacts
305
+ * that produced it.
306
+ */
307
+ export interface GeneratedCodeRunResult<TOutput = unknown> {
308
+ /** The value returned by running the authored code against the inputs. */
309
+ value: TOutput
310
+ /** The model-authored module source that was executed. */
311
+ code: string
312
+ /** The model-authored test source, if tests were requested. */
313
+ tests?: string
314
+ /** Test results, if tests ran in the sandbox. */
315
+ testResults?: {
316
+ total: number
317
+ passed: number
318
+ failed: number
319
+ skipped: number
320
+ }
321
+ /** Console logs captured during execution. */
322
+ logs: { level: string; message: string }[]
323
+ }
324
+
325
+ /**
326
+ * The **non-deterministic** generate → run → test → return capability.
327
+ *
328
+ * This is the headline of the `generate('code', …)` primitive: a model
329
+ * **authors** code, that code is **run** in ai-evaluate's V8-isolate sandbox,
330
+ * optionally **tested** there, and the executed **result** is returned (not
331
+ * just the source). This is deliberately separate from `type: 'code'`, which is
332
+ * deterministic and never consults a model — so determinism is never blurred.
333
+ *
334
+ * Unlike {@link generateCode} (which only returns source text), this runs the
335
+ * authored code. The authored module is expected to `export function ${name}`
336
+ * (a NAMED export — the sandbox's module loader does not support `export
337
+ * default`); the sandbox script invokes `${name}(args)` and returns its result.
338
+ *
339
+ * @param definition - The code-authoring spec ({@link CodeGenerationDefinition}).
340
+ * Set `includeTests: false` to skip test authoring (default: tests included).
341
+ * @param args - Concrete inputs the authored code is invoked with.
342
+ * @param env - Optional host Workers env. When it carries `LOADER` **and**
343
+ * `TEST`, tests run on the real Dynamic Workers loader; otherwise execution
344
+ * falls back to the Miniflare-backed Node runtime (whose dev worker has its
345
+ * own embedded test runner and needs no live `TEST` binding).
346
+ * @returns The executed result plus authored artifacts.
347
+ *
348
+ * @example
349
+ * ```ts
350
+ * import { generateAndRunCode } from 'ai-functions'
351
+ *
352
+ * const { value } = await generateAndRunCode(
353
+ * { name: 'calculateTax', args: { amount: '(number)', rate: '(number)' } },
354
+ * { amount: 100, rate: 0.2 }
355
+ * )
356
+ * // value === 20 (the model authored the code, the sandbox ran it)
357
+ * ```
358
+ */
359
+ export async function generateAndRunCode<TOutput = unknown, TInput = unknown>(
360
+ definition: CodeGenerationDefinition<TInput>,
361
+ args?: TInput,
362
+ env?: SandboxEnv
363
+ ): Promise<GeneratedCodeRunResult<TOutput>> {
364
+ const {
365
+ name,
366
+ description,
367
+ language = 'typescript',
368
+ instructions,
369
+ returnType,
370
+ includeTests = true,
371
+ model = 'sonnet',
372
+ } = definition
373
+
374
+ const argsDescription = JSON.stringify(args ?? definition.args, null, 2)
375
+
376
+ // Step 1 — model AUTHORS the module (+ optional tests). Non-deterministic.
377
+ const codeSpec = `The complete ${language} module. It MUST contain a NAMED export 'export function ${name}(args) { ... }' (NOT a default export) that takes a single arguments object and returns the result. Output ONLY raw code, no markdown fences.`
378
+ const schema: SimpleSchemaType = includeTests
379
+ ? {
380
+ code: codeSpec,
381
+ tests: `vitest-style tests using global describe/it/expect. The function '${name}' is already in scope (do not import it). Output ONLY raw code, no markdown fences.`,
382
+ }
383
+ : {
384
+ code: codeSpec,
385
+ }
386
+
387
+ const authored = await generateObject({
388
+ model,
389
+ schema,
390
+ system: `You are an expert ${language} developer. Generate clean, production-ready code. The module MUST expose a NAMED export 'export function ${name}(args)' taking one arguments object — do NOT use 'export default'. Output ONLY raw code, no markdown code fences or language tags.`,
391
+ prompt: `Author a ${language} module with the following specification:
392
+
393
+ Name: ${name}
394
+ Description: ${description || 'No description provided'}
395
+ Arguments: ${argsDescription}
396
+ Return Type: ${JSON.stringify(returnType)}
397
+
398
+ ${instructions ? `Additional Instructions: ${instructions}` : ''}
399
+
400
+ Requirements:
401
+ - Expose 'export function ${name}(args) { ... }' (a NAMED export, not default), taking one arguments object.
402
+ - Handle edge cases appropriately.
403
+ - Return ONLY raw code without markdown formatting.`,
404
+ })
405
+
406
+ const authoredObj = authored.object as { code: string; tests?: string }
407
+ const code = authoredObj.code
408
+ const tests = includeTests ? authoredObj.tests : undefined
409
+
410
+ // Step 2 — RUN the authored code in the sandbox and capture its return value.
411
+ // The module's default export is invoked with the JSON-injected args; the
412
+ // result is returned by the sandbox script. Tests (if any) run in the same
413
+ // sandbox via the worker template's test runner.
414
+ let argsJson: string
415
+ try {
416
+ argsJson = JSON.stringify(args ?? null)
417
+ } catch (e) {
418
+ throw new Error(
419
+ `generateAndRunCode('${name}'): args are not JSON-serializable: ${(e as Error).message}`
420
+ )
421
+ }
422
+
423
+ // The named export `${name}` is exposed as a top-level binding by the worker
424
+ // template (`const { ${name} } = exports`). The script calls it with the
425
+ // JSON-injected args and returns the result.
426
+ const result = await runInSandbox(
427
+ {
428
+ module: code,
429
+ script: `const __args__ = JSON.parse(${JSON.stringify(
430
+ argsJson
431
+ )}); if (typeof ${name} !== 'function') { throw new Error("authored module did not export a callable '${name}'"); } return await ${name}(__args__);`,
432
+ ...(tests !== undefined && { tests }),
433
+ },
434
+ env
435
+ )
436
+
437
+ if (result.success === false) {
438
+ throw new Error(
439
+ `generateAndRunCode('${name}') failed in the sandbox: ${result.error ?? 'unknown error'}`
440
+ )
441
+ }
442
+
443
+ return {
444
+ value: result.value as TOutput,
445
+ code,
446
+ ...(tests !== undefined && { tests }),
447
+ ...(result.testResults && {
448
+ testResults: {
449
+ total: result.testResults.total,
450
+ passed: result.testResults.passed,
451
+ failed: result.testResults.failed,
452
+ skipped: result.testResults.skipped,
453
+ },
454
+ }),
455
+ logs: result.logs.map((l) => ({ level: l.level, message: l.message })),
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Execute a generative function - uses AI to generate content
461
+ */
462
+ async function executeGenerativeFunction<TOutput, TInput>(
463
+ definition: GenerativeFunctionDefinition<TOutput, TInput>,
464
+ args: TInput
465
+ ): Promise<TOutput> {
466
+ const { output, system, promptTemplate, model = 'sonnet', temperature, returnType } = definition
467
+
468
+ const prompt = promptTemplate
469
+ ? fillTemplate(promptTemplate, args as Record<string, unknown>)
470
+ : JSON.stringify(args)
471
+
472
+ switch (output) {
473
+ case 'string': {
474
+ const result = await generateObject({
475
+ model,
476
+ schema: { text: 'The generated text response' },
477
+ prompt,
478
+ ...(system !== undefined && { system }),
479
+ ...(temperature !== undefined && { temperature }),
480
+ })
481
+ return (result.object as { text: string }).text as TOutput
482
+ }
483
+
484
+ case 'object': {
485
+ const objectSchema = returnType || { result: 'The generated result' }
486
+ const result = await generateObject({
487
+ model,
488
+ schema: objectSchema as SimpleSchemaType,
489
+ prompt,
490
+ ...(system !== undefined && { system }),
491
+ ...(temperature !== undefined && { temperature }),
492
+ })
493
+ return result.object as TOutput
494
+ }
495
+
496
+ case 'image':
497
+ throw new Error(
498
+ 'Image generation via generative functions is not yet implemented. ' +
499
+ 'Use the image() primitive directly instead.'
500
+ )
501
+
502
+ case 'video':
503
+ throw new Error(
504
+ 'Video generation via generative functions is not yet implemented. ' +
505
+ 'Use the video() primitive directly instead.'
506
+ )
507
+
508
+ default:
509
+ throw new Error(`Unknown output type: ${output}`)
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Execute an agentic function - runs in a loop with tools
515
+ */
516
+ async function executeAgenticFunction<TOutput, TInput>(
517
+ definition: AgenticFunctionDefinition<TOutput, TInput>,
518
+ args: TInput
519
+ ): Promise<TOutput> {
520
+ const {
521
+ instructions,
522
+ promptTemplate,
523
+ tools = [],
524
+ maxIterations = 10,
525
+ model = 'sonnet',
526
+ returnType,
527
+ } = definition
528
+
529
+ const prompt = promptTemplate
530
+ ? fillTemplate(promptTemplate, args as Record<string, unknown>)
531
+ : JSON.stringify(args)
532
+
533
+ // Build system prompt with tool descriptions
534
+ const toolDescriptions = tools.map((t) => `- ${t.name}: ${t.description}`).join('\n')
535
+ const systemPrompt = `${instructions}
536
+
537
+ Available tools:
538
+ ${toolDescriptions || 'No tools available'}
539
+
540
+ Work step by step to accomplish the task. When you have completed the task, provide your final result.`
541
+
542
+ let iteration = 0
543
+ const toolResults: unknown[] = []
544
+
545
+ // Simple agent loop
546
+ while (iteration < maxIterations) {
547
+ iteration++
548
+
549
+ const result = await generateObject({
550
+ model,
551
+ schema: {
552
+ thinking: 'Your step-by-step reasoning',
553
+ toolCall: {
554
+ name: 'Tool to call (or "done" if finished)',
555
+ arguments: 'Arguments for the tool as JSON string',
556
+ },
557
+ finalResult: returnType || 'The final result if done',
558
+ },
559
+ system: systemPrompt,
560
+ prompt: `Task: ${prompt}
561
+
562
+ Previous tool results:
563
+ ${toolResults.map((r, i) => `Step ${i + 1}: ${JSON.stringify(r)}`).join('\n') || 'None yet'}
564
+
565
+ What is your next step?`,
566
+ })
567
+
568
+ const response = result.object as {
569
+ thinking: string
570
+ toolCall: { name: string; arguments: string }
571
+ finalResult: unknown
572
+ }
573
+
574
+ if (response.toolCall.name === 'done' || response.finalResult) {
575
+ return response.finalResult as TOutput
576
+ }
577
+
578
+ // Execute tool call
579
+ const tool = tools.find((t) => t.name === response.toolCall.name)
580
+ if (tool) {
581
+ let toolArgs: Record<string, unknown>
582
+ try {
583
+ toolArgs = JSON.parse(response.toolCall.arguments || '{}')
584
+ } catch (e) {
585
+ toolResults.push({
586
+ error: `Invalid tool arguments: ${(e as Error).message}`,
587
+ })
588
+ continue
589
+ }
590
+ const toolResult = await tool.handler(toolArgs)
591
+ toolResults.push({ tool: response.toolCall.name, result: toolResult })
592
+ } else {
593
+ toolResults.push({ error: `Tool not found: ${response.toolCall.name}` })
594
+ }
595
+ }
596
+
597
+ throw new Error(`Agent exceeded maximum iterations (${maxIterations})`)
598
+ }
599
+
600
+ /**
601
+ * Execute a human function - generates UI and waits for human input
602
+ *
603
+ * **Note: This function currently returns a pending placeholder.**
604
+ *
605
+ * In a complete implementation, this function would:
606
+ * 1. Generate channel-specific UI (Slack blocks, email templates, web forms, etc.)
607
+ * 2. Send the generated UI to the appropriate channel
608
+ * 3. Wait for human response with optional timeout
609
+ * 4. Validate and return the human's response
610
+ *
611
+ * The current implementation generates the UI artifacts but returns a pending
612
+ * placeholder instead of actually sending to the channel and waiting for response.
613
+ * This allows testing the UI generation without requiring actual channel integrations.
614
+ *
615
+ * **Important:** Use `isPendingHumanResult()` to check if the result is pending
616
+ * before attempting to use it as the expected output type.
617
+ *
618
+ * @param definition - The human function definition with channel and instructions
619
+ * @param args - Arguments to pass to the function
620
+ * @returns Either the actual TOutput from human input, or a HumanFunctionPending placeholder
621
+ *
622
+ * @example
623
+ * ```ts
624
+ * import { isPendingHumanResult } from 'ai-functions'
625
+ *
626
+ * const result = await approveRefund({ amount: 500 })
627
+ *
628
+ * if (isPendingHumanResult(result)) {
629
+ * // Handle pending state
630
+ * console.log('Awaiting human approval via:', result.channel)
631
+ * return { status: 'pending' }
632
+ * }
633
+ *
634
+ * // result is the actual approval response
635
+ * console.log('Approved:', result.approved)
636
+ * ```
637
+ */
638
+ async function executeHumanFunction<TOutput, TInput>(
639
+ definition: HumanFunctionDefinition<TOutput, TInput>,
640
+ args: TInput
641
+ ): Promise<TOutput | HumanFunctionPending<TOutput>> {
642
+ const { channel, instructions, promptTemplate, returnType } = definition
643
+
644
+ const prompt = promptTemplate
645
+ ? fillTemplate(promptTemplate, args as Record<string, unknown>)
646
+ : JSON.stringify(args)
647
+
648
+ // Generate channel-specific UI
649
+ const uiSchema: Record<string, SimpleSchemaType> = {
650
+ // New HumanChannel types
651
+ chat: {
652
+ message: 'Chat message to send',
653
+ options: ['Response options if applicable'],
654
+ },
655
+ email: {
656
+ subject: 'Email subject',
657
+ html: 'Email HTML body',
658
+ text: 'Plain text fallback',
659
+ },
660
+ phone: {
661
+ script: 'Phone call script',
662
+ keyPoints: ['Key points to cover'],
663
+ },
664
+ sms: {
665
+ text: 'SMS message text (max 160 chars)',
666
+ },
667
+ workspace: {
668
+ blocks: ['Workspace/Slack BlockKit blocks as JSON array'],
669
+ text: 'Plain text fallback',
670
+ },
671
+ web: {
672
+ component: 'React component code for the form',
673
+ schema: 'JSON schema for the form fields',
674
+ },
675
+ // Legacy fallback
676
+ custom: {
677
+ data: 'Structured data for custom implementation',
678
+ instructions: 'Instructions for the human',
679
+ },
680
+ }
681
+
682
+ const result = await generateObject({
683
+ model: 'sonnet',
684
+ schema: uiSchema[channel] ?? uiSchema['custom'],
685
+ system: `Generate ${channel} UI/content for a human-in-the-loop task.`,
686
+ prompt: `Task: ${instructions}
687
+
688
+ Input data:
689
+ ${prompt}
690
+
691
+ Expected response format:
692
+ ${JSON.stringify(returnType)}
693
+
694
+ Generate the appropriate ${channel} UI/content to collect this response from a human.`,
695
+ })
696
+
697
+ // Runtime warning for developers
698
+ getLogger().warn(
699
+ `[HumanFunction] Returning pending placeholder for channel '${channel}'. ` +
700
+ `Use isPendingHumanResult() to check before using the result. ` +
701
+ `Full channel integration is not yet implemented.`
702
+ )
703
+
704
+ // Return a properly typed pending result
705
+ // The symbol marker allows isPendingHumanResult() to reliably identify this
706
+ const pendingResult: HumanFunctionPending<TOutput> = {
707
+ [PENDING_HUMAN_RESULT_SYMBOL]: true,
708
+ _pending: true,
709
+ channel,
710
+ artifacts: result.object,
711
+ expectedResponseType: returnType as TOutput,
712
+ }
713
+
714
+ return pendingResult
715
+ }
716
+
717
+ // ============================================================================
718
+ // Defined Function Creation
719
+ // ============================================================================
720
+
721
+ /**
722
+ * Create a defined function from a function definition
723
+ */
724
+ export function createDefinedFunction<TOutput, TInput>(
725
+ definition: FunctionDefinition<TOutput, TInput>
726
+ ): DefinedFunction<TOutput, TInput> {
727
+ const call = async (args: TInput, env?: SandboxEnv): Promise<TOutput> => {
728
+ switch (definition.type) {
729
+ case 'code':
730
+ // Optional host Workers env threads through to the sandbox for inline
731
+ // `code` bodies; ignored for `handler` (direct call) and other types.
732
+ return executeCodeFunction(definition, args, env) as Promise<TOutput>
733
+ case 'generative':
734
+ return executeGenerativeFunction(definition, args) as Promise<TOutput>
735
+ case 'agentic':
736
+ return executeAgenticFunction(definition, args) as Promise<TOutput>
737
+ case 'human':
738
+ return executeHumanFunction(definition, args) as Promise<TOutput>
739
+ default:
740
+ throw new Error(`Unknown function type: ${(definition as FunctionDefinition).type}`)
741
+ }
742
+ }
743
+
744
+ const asTool = (): AIFunctionDefinition<TOutput, TInput> => {
745
+ return {
746
+ name: definition.name,
747
+ description: definition.description || `Execute ${definition.name}`,
748
+ parameters: convertArgsToJSONSchema(definition.args),
749
+ handler: call,
750
+ }
751
+ }
752
+
753
+ return { definition, call, asTool }
754
+ }
755
+
756
+ /**
757
+ * Standalone function for defining AI functions
758
+ *
759
+ * @example
760
+ * ```ts
761
+ * import { defineFunction } from 'ai-functions'
762
+ *
763
+ * const summarize = defineFunction({
764
+ * type: 'generative',
765
+ * name: 'summarize',
766
+ * args: { text: 'Text to summarize' },
767
+ * output: 'string',
768
+ * promptTemplate: 'Summarize: {{text}}',
769
+ * })
770
+ *
771
+ * const result = await summarize.call({ text: 'Long article...' })
772
+ * ```
773
+ */
774
+ export function defineFunction<TOutput, TInput>(
775
+ definition: FunctionDefinition<TOutput, TInput>
776
+ ): DefinedFunction<TOutput, TInput> {
777
+ return createDefinedFunction(definition)
778
+ }
779
+
780
+ // ============================================================================
781
+ // Function Registry Implementation
782
+ // ============================================================================
783
+
784
+ /**
785
+ * In-memory function registry
786
+ */
787
+ class InMemoryFunctionRegistry implements FunctionRegistry {
788
+ private functions = new Map<string, DefinedFunction>()
789
+
790
+ get(name: string): DefinedFunction | undefined {
791
+ return this.functions.get(name)
792
+ }
793
+
794
+ set(name: string, fn: DefinedFunction): void {
795
+ this.functions.set(name, fn)
796
+ }
797
+
798
+ has(name: string): boolean {
799
+ return this.functions.has(name)
800
+ }
801
+
802
+ list(): string[] {
803
+ return Array.from(this.functions.keys())
804
+ }
805
+
806
+ delete(name: string): boolean {
807
+ return this.functions.delete(name)
808
+ }
809
+
810
+ clear(): void {
811
+ this.functions.clear()
812
+ }
813
+ }
814
+
815
+ /**
816
+ * Factory function to create a new isolated function registry instance.
817
+ *
818
+ * Use this when you need:
819
+ * - Test isolation: Each test can have its own registry
820
+ * - Scoped registries: Different parts of an app can have separate registries
821
+ * - Custom lifecycle management: Control when registries are created/destroyed
822
+ *
823
+ * @example
824
+ * ```ts
825
+ * // Create isolated registry for tests
826
+ * const registry = createFunctionRegistry()
827
+ * const fn = defineFunction({ ... })
828
+ * registry.set('myFunc', fn)
829
+ *
830
+ * // Later, registry can be discarded without affecting global state
831
+ * ```
832
+ *
833
+ * @returns A new FunctionRegistry instance
834
+ */
835
+ export function createFunctionRegistry(): FunctionRegistry {
836
+ return new InMemoryFunctionRegistry()
837
+ }
838
+
839
+ /**
840
+ * Global function registry
841
+ *
842
+ * Note: This is in-memory only. For persistence, use mdxai or mdxdb packages.
843
+ *
844
+ * **Lifecycle:**
845
+ * - Created once at module load time
846
+ * - Shared across the entire application
847
+ * - Use `resetGlobalRegistry()` in tests to clear state between test runs
848
+ * - For isolated registries, use `createFunctionRegistry()` instead
849
+ */
850
+ export const functions: FunctionRegistry = new InMemoryFunctionRegistry()
851
+
852
+ /**
853
+ * Reset the global function registry to a clean state.
854
+ *
855
+ * **Important:** This is primarily intended for test cleanup to ensure
856
+ * test isolation. In production code, prefer using `createFunctionRegistry()`
857
+ * for isolated registries.
858
+ *
859
+ * @example
860
+ * ```ts
861
+ * // In test setup/teardown
862
+ * beforeEach(() => {
863
+ * resetGlobalRegistry()
864
+ * })
865
+ *
866
+ * // Or after each test
867
+ * afterEach(() => {
868
+ * resetGlobalRegistry()
869
+ * })
870
+ * ```
871
+ */
872
+ export function resetGlobalRegistry(): void {
873
+ functions.clear()
874
+ }