@assistant-ui/x-generative-compiler 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,14 +8,16 @@ colocates a tool's schema, its server-only `execute`, and its client-only
8
8
  `render`, and rewrites it for a single build target:
9
9
 
10
10
  - **client** — keeps `render` and any `"use client"` `execute` (frontend tools),
11
- drops backend `execute` and `hitl()` sentinels, and stamps each tool's inferred
12
- `type`.
11
+ drops backend `execute` and `humanTool()` sentinels, and stamps each tool's
12
+ inferred `type`.
13
13
  - **server** — keeps the backend `execute` (importing `server-only`), drops
14
- `render`.
14
+ `render`, and omits externally-defined backend tools marked with
15
+ `externalTool()`.
15
16
 
16
17
  The marker functions a `"use generative"` file imports — `defineToolkit` and
17
- `hitl` — live in `@assistant-ui/core/react` (re-exported from `@assistant-ui/react`).
18
- This package only recognizes them by name and strips them at build time.
18
+ `humanTool` — live in `@assistant-ui/core/react` (re-exported from
19
+ `@assistant-ui/react`). This package only recognizes them by name and strips them
20
+ at build time.
19
21
 
20
22
  ## API
21
23
 
package/dist/compile.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Target } from "./constants.js";
2
2
 
3
3
  //#region src/compile.d.ts
4
- type ToolType = "frontend" | "backend" | "human";
4
+ type ToolType = "frontend" | "backend" | "human" | "provider";
5
5
  interface CompileOptions {
6
6
  /** Which build target to emit. */
7
7
  target: Target;
@@ -1 +1 @@
1
- {"version":3,"file":"compile.d.ts","names":[],"sources":["../src/compile.ts"],"mappings":";;;KAeY,QAAA;AAAA,UAEK,cAAA;EAFL;EAIV,MAAA,EAAQ,MAAM;;EAEd,QAAA;EANkB;EAQlB,UAAA;EAN6B;;;;;;;;EAe7B,gBAAA;AAAA;AAAA,UAGe,aAAA;EACf,IAAA;EACA,GAAG;AAAA;AAAA;AAAA,cAIQ,sBAAA,SAA+B,KAAK;cACnC,OAAA,UAAiB,QAAA;AAAA;;iBAOf,kBAAA,CAAmB,IAAY;;;;;iBA4B/B,iBAAA,CACd,IAAA,UACA,OAAA,EAAS,cAAA,GACR,aAAa"}
1
+ {"version":3,"file":"compile.d.ts","names":[],"sources":["../src/compile.ts"],"mappings":";;;KAmBY,QAAA;AAAA,UAkCK,cAAA;EAlCL;EAoCV,MAAA,EAAQ,MAAM;;EAEd,QAAA;EAtCkB;EAwClB,UAAA;EAN6B;;;;;;;;EAe7B,gBAAA;AAAA;AAAA,UAGe,aAAA;EACf,IAAA;EACA,GAAG;AAAA;AAAA;AAAA,cAIQ,sBAAA,SAA+B,KAAK;cACnC,OAAA,UAAiB,QAAA;AAAA;;iBAOf,kBAAA,CAAmB,IAAY;;;;;iBA4B/B,iBAAA,CACd,IAAA,UACA,OAAA,EAAS,cAAA,GACR,aAAa"}
package/dist/compile.js CHANGED
@@ -1,11 +1,38 @@
1
1
  import { DIRECTIVE } from "./constants.js";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import * as nodePath from "node:path";
2
5
  import { parse } from "@babel/parser";
3
6
  import _traverse from "@babel/traverse";
4
7
  import _generate from "@babel/generator";
5
8
  import * as t from "@babel/types";
9
+ import { satisfies } from "semver";
6
10
  //#region src/compile.ts
7
11
  const traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
8
12
  const generate = typeof _generate === "function" ? _generate : _generate.default;
13
+ /** The required wrapper around a toolkit's tools (stripped at build time). */
14
+ const TOOLKIT_WRAPPER = "defineToolkit";
15
+ /** The helper that produces MCP-only toolkit fragments. */
16
+ const MCP_TOOLKIT_WRAPPER = "defineMcpToolkit";
17
+ /** The required wrapper around a generative-UI library (stripped at build time). */
18
+ const COMPONENTS_WRAPPER = "defineGenerativeComponents";
19
+ /** The core package whose metadata declares supported compiler versions. */
20
+ const CORE_PACKAGE = "@assistant-ui/core";
21
+ /** This package, checked against core's compatibility range. */
22
+ const COMPILER_PACKAGE = "@assistant-ui/x-generative-compiler";
23
+ /** Packages that re-export core's generative markers. */
24
+ const DISTRIBUTION_PACKAGES = [
25
+ CORE_PACKAGE,
26
+ "@assistant-ui/react",
27
+ "@assistant-ui/react-native",
28
+ "@assistant-ui/react-ink"
29
+ ];
30
+ /**
31
+ * The class whose instances expose split-by-condition tools (`present()`,
32
+ * `promptUser()`). A toolkit entry that calls a method on one of these passes
33
+ * through untouched — the library, not this compiler, routes its halves.
34
+ */
35
+ const GENERATIVE_FACTORY = "JSONGenerativeUI";
9
36
  /** Thrown when a `"use generative"` file violates an authoring constraint. */
10
37
  var GenerativeCompileError = class extends Error {
11
38
  constructor(message, filename) {
@@ -45,27 +72,32 @@ function compileGenerative(code, options) {
45
72
  ]
46
73
  });
47
74
  if (!ast.program.directives.some((d) => d.value.value === "use generative")) throw new GenerativeCompileError(`missing "${DIRECTIVE}" directive`, filename);
48
- const object = findDefaultExportObject(ast, filename);
49
- let keptRender = false;
50
- let keptBackendExecute = false;
51
- for (const entry of object.properties) {
52
- const value = entryValue(entry);
53
- if (!value) throw new GenerativeCompileError("each tool must be an inline object literal (`name: { ... }`) so its `execute` can be routed", filename);
54
- const type = inferToolType(value, filename);
55
- const hasRender = !!findMember(value, "render");
56
- const execute = findMember(value, "execute");
57
- if ((type === "frontend" || type === "human") && !hasRender) throw new GenerativeCompileError(`a ${type} tool must declare a \`render\` (it has no server execute to show otherwise)`, filename);
58
- if (target === "client") {
59
- if (execute && type === "frontend") stripUseClient(execute);
60
- else if (execute) removeMember(value, "execute");
61
- if (hasRender) keptRender = true;
62
- } else {
63
- if (hasRender) removeMember(value, "render");
64
- if (execute && type !== "backend") removeMember(value, "execute");
65
- if (execute && type === "backend") keptBackendExecute = true;
75
+ ensureCompilerCompatibleWithCore(ast, filename);
76
+ ensureDefaultExport(ast, filename);
77
+ const generativeInstances = collectGenerativeInstances(ast);
78
+ const safeToolkitSpreads = collectSafeToolkitSpreads(ast);
79
+ const flags = {
80
+ keptRender: false,
81
+ keptBackendExecute: false
82
+ };
83
+ traverse(ast, { CallExpression(path) {
84
+ const callee = path.node.callee;
85
+ const object = t.isObjectExpression(path.node.arguments[0]) ? path.node.arguments[0] : null;
86
+ if (t.isIdentifier(callee, { name: COMPONENTS_WRAPPER })) {
87
+ if (!object) throw new GenerativeCompileError(`${COMPONENTS_WRAPPER}() takes an inline object literal of components`, filename);
88
+ compileComponents(object, target, flags, filename);
89
+ path.replaceWith(object);
90
+ path.skip();
91
+ return;
66
92
  }
67
- setToolType(value, type);
68
- }
93
+ if (t.isIdentifier(callee, { name: TOOLKIT_WRAPPER })) {
94
+ if (!object) throw new GenerativeCompileError(`${TOOLKIT_WRAPPER}() takes an inline object literal of tools`, filename);
95
+ compileToolkit(object, target, generativeInstances, safeToolkitSpreads, flags, filename);
96
+ path.replaceWith(object);
97
+ path.skip();
98
+ }
99
+ } });
100
+ const { keptRender, keptBackendExecute } = flags;
69
101
  pruneUnused(ast);
70
102
  ast.program.directives = ast.program.directives.filter((d) => d.value.value !== "use generative" && d.value.value !== "use client");
71
103
  if (target === "client" && keptRender) ast.program.directives.unshift(t.directive(t.directiveLiteral("use client")));
@@ -80,29 +112,268 @@ function compileGenerative(code, options) {
80
112
  map: result.map
81
113
  };
82
114
  }
83
- function findDefaultExportObject(ast, filename) {
84
- let object = null;
85
- let sawDefault = false;
86
- for (const stmt of ast.program.body) {
87
- if (!t.isExportDefaultDeclaration(stmt)) continue;
88
- sawDefault = true;
89
- object = unwrapDefineToolkit(stmt.declaration);
90
- if (object) stmt.declaration = object;
115
+ const checkedCorePackageJsonPaths = /* @__PURE__ */ new Set();
116
+ let compilerPackageVersion;
117
+ function ensureCompilerCompatibleWithCore(ast, filename) {
118
+ const corePackageJsonPath = resolveCorePackageJson(ast, filename);
119
+ if (!corePackageJsonPath || checkedCorePackageJsonPaths.has(corePackageJsonPath)) return;
120
+ const corePackageJson = readPackageJson(corePackageJsonPath);
121
+ const range = corePackageJson?.optionalDevDependencies?.[COMPILER_PACKAGE];
122
+ if (!range) {
123
+ checkedCorePackageJsonPaths.add(corePackageJsonPath);
124
+ return;
125
+ }
126
+ const compilerVersion = getCompilerPackageVersion();
127
+ let compatible = false;
128
+ try {
129
+ compatible = satisfies(compilerVersion, range, { includePrerelease: true });
130
+ } catch {
131
+ throw new GenerativeCompileError(`${CORE_PACKAGE}@${corePackageJson.version ?? "unknown"} declares an invalid optionalDevDependencies range for ${COMPILER_PACKAGE}: ` + JSON.stringify(range), filename);
132
+ }
133
+ if (!compatible) throw new GenerativeCompileError(`${CORE_PACKAGE}@${corePackageJson.version ?? "unknown"} requires ${COMPILER_PACKAGE} ${range}, but the current compiler is ${compilerVersion}. Update @assistant-ui/next or @assistant-ui/vite so their compiler satisfies the core package's optionalDevDependencies range.`, filename);
134
+ checkedCorePackageJsonPaths.add(corePackageJsonPath);
135
+ }
136
+ function getCompilerPackageVersion() {
137
+ if (compilerPackageVersion) return compilerPackageVersion;
138
+ const packageJsonPath = findPackageJson(import.meta.url, COMPILER_PACKAGE);
139
+ const packageJson = packageJsonPath ? readPackageJson(packageJsonPath) : null;
140
+ if (!packageJson?.version) throw new GenerativeCompileError(`could not determine ${COMPILER_PACKAGE}'s package version`);
141
+ compilerPackageVersion = packageJson.version;
142
+ return compilerPackageVersion;
143
+ }
144
+ function resolveCorePackageJson(ast, filename) {
145
+ for (const packageName of collectImportedDistributionPackages(ast)) {
146
+ const packageJsonPath = resolvePackageJson(packageName, filename);
147
+ if (!packageJsonPath) continue;
148
+ if (packageName === CORE_PACKAGE) return packageJsonPath;
149
+ const corePackageJsonPath = resolvePackageJson(CORE_PACKAGE, packageJsonPath);
150
+ if (corePackageJsonPath) return corePackageJsonPath;
151
+ }
152
+ return null;
153
+ }
154
+ function collectImportedDistributionPackages(ast) {
155
+ const packages = /* @__PURE__ */ new Set();
156
+ for (const statement of ast.program.body) {
157
+ const source = (t.isImportDeclaration(statement) || t.isExportNamedDeclaration(statement) || t.isExportAllDeclaration(statement)) && statement.source ? statement.source.value : null;
158
+ if (!source) continue;
159
+ const packageName = packageNameFromSpecifier(source);
160
+ if (packageName) packages.add(packageName);
161
+ }
162
+ return packages;
163
+ }
164
+ function packageNameFromSpecifier(specifier) {
165
+ for (const packageName of DISTRIBUTION_PACKAGES) if (specifier === packageName || specifier.startsWith(`${packageName}/`)) return packageName;
166
+ return null;
167
+ }
168
+ function resolvePackageJson(packageName, filename) {
169
+ const requirePath = normalizeRequirePath(filename);
170
+ const require = createRequire(requirePath);
171
+ try {
172
+ return findPackageJson(require.resolve(packageName), packageName);
173
+ } catch {
174
+ return findPackageJsonFromNodeModules(packageName, requirePath);
175
+ }
176
+ }
177
+ function normalizeRequirePath(filename) {
178
+ if (filename) {
179
+ const cleanFilename = filename.split(/[?#]/, 1)[0];
180
+ if (nodePath.isAbsolute(cleanFilename)) return cleanFilename;
181
+ }
182
+ return import.meta.url;
183
+ }
184
+ function findPackageJson(fromPathOrUrl, packageName) {
185
+ let current = nodePath.dirname(fromPathOrUrl.startsWith("file:") ? new URL(fromPathOrUrl).pathname : nodePath.resolve(fromPathOrUrl));
186
+ for (;;) {
187
+ const packageJsonPath = nodePath.join(current, "package.json");
188
+ if (readPackageJson(packageJsonPath)?.name === packageName) return packageJsonPath;
189
+ const parent = nodePath.dirname(current);
190
+ if (parent === current) return null;
191
+ current = parent;
192
+ }
193
+ }
194
+ function findPackageJsonFromNodeModules(packageName, fromPathOrUrl) {
195
+ const parts = packageName.split("/");
196
+ let current = nodePath.dirname(fromPathOrUrl.startsWith("file:") ? new URL(fromPathOrUrl).pathname : nodePath.resolve(fromPathOrUrl));
197
+ for (;;) {
198
+ const packageJsonPath = nodePath.join(current, "node_modules", ...parts, "package.json");
199
+ if (readPackageJson(packageJsonPath)?.name === packageName) return packageJsonPath;
200
+ const parent = nodePath.dirname(current);
201
+ if (parent === current) return null;
202
+ current = parent;
203
+ }
204
+ }
205
+ function readPackageJson(packageJsonPath) {
206
+ if (!existsSync(packageJsonPath)) return null;
207
+ try {
208
+ return JSON.parse(readFileSync(packageJsonPath, "utf8"));
209
+ } catch (error) {
210
+ throw new GenerativeCompileError(`could not parse package metadata at ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
211
+ }
212
+ }
213
+ /**
214
+ * Errors unless the module's default export is the toolkit — a `defineToolkit(...)`
215
+ * call (through `satisfies`/`as`/parens). This is the security boundary: the
216
+ * default export is what the runtime registers, so it must be wrapped (and thus
217
+ * split). A bare `export default { ... }` would ship a backend `execute` to the
218
+ * client even if some *other* `defineToolkit(...)` exists elsewhere in the file.
219
+ */
220
+ function ensureDefaultExport(ast, filename) {
221
+ const def = ast.program.body.find((stmt) => t.isExportDefaultDeclaration(stmt));
222
+ if (!def) throw new GenerativeCompileError("missing a default export", filename);
223
+ if (!unwrapToCall(def.declaration, TOOLKIT_WRAPPER)) throw new GenerativeCompileError(`the default export must be ${TOOLKIT_WRAPPER}({ ... }) (imported from "@assistant-ui/react"); wrapping is required so a backend \`execute\` can't be authored in a way that reaches the client`, filename);
224
+ }
225
+ /**
226
+ * Unwraps a node through `satisfies`/`as`/parens to a call of the named function,
227
+ * or returns `null`.
228
+ */
229
+ function unwrapToCall(node, name) {
230
+ if (t.isTSSatisfiesExpression(node) || t.isTSAsExpression(node)) return unwrapToCall(node.expression, name);
231
+ if (t.isParenthesizedExpression(node)) return unwrapToCall(node.expression, name);
232
+ if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name })) return node;
233
+ return null;
234
+ }
235
+ /**
236
+ * Collects the names bound to `new JSONGenerativeUI(...)` (e.g.
237
+ * `const generative = new JSONGenerativeUI({ library })`). A toolkit entry that
238
+ * calls a method on one of these is a generative tool whose halves the library
239
+ * routes by export condition, so it passes through the toolkit pass untouched.
240
+ */
241
+ function collectGenerativeInstances(ast) {
242
+ const names = /* @__PURE__ */ new Set();
243
+ for (const statement of ast.program.body) {
244
+ if (!t.isVariableDeclaration(statement)) continue;
245
+ for (const declaration of statement.declarations) {
246
+ const { id, init } = declaration;
247
+ if (t.isIdentifier(id) && t.isNewExpression(init) && t.isIdentifier(init.callee, { name: GENERATIVE_FACTORY })) names.add(id.name);
248
+ }
249
+ }
250
+ return names;
251
+ }
252
+ /**
253
+ * Local toolkit variables whose initializer is visible to this compiler pass
254
+ * are safe to spread. `defineToolkit(...)` initializers are compiled in-place
255
+ * before a later spread reads them; `defineMcpToolkit(...)` entries cannot
256
+ * contain executable code.
257
+ */
258
+ function collectSafeToolkitSpreads(ast) {
259
+ const names = /* @__PURE__ */ new Set();
260
+ for (const statement of ast.program.body) {
261
+ if (!t.isVariableDeclaration(statement)) continue;
262
+ for (const declaration of statement.declarations) {
263
+ const { id, init } = declaration;
264
+ if (!t.isIdentifier(id) || !init) continue;
265
+ if (unwrapToToolkitCall(init)) names.add(id.name);
266
+ }
267
+ }
268
+ return names;
269
+ }
270
+ function unwrapToToolkitCall(node) {
271
+ return unwrapToCall(node, TOOLKIT_WRAPPER) ?? unwrapToCall(node, MCP_TOOLKIT_WRAPPER);
272
+ }
273
+ function isSafeToolkitSpread(entry, safeToolkitSpreads) {
274
+ if (t.isIdentifier(entry.argument)) return safeToolkitSpreads.has(entry.argument.name);
275
+ const directMcpToolkit = unwrapToCall(entry.argument, MCP_TOOLKIT_WRAPPER);
276
+ return !!directMcpToolkit && t.isObjectExpression(directMcpToolkit.arguments[0]);
277
+ }
278
+ /** The `JSONGenerativeUI` methods that produce a split-by-condition tool. */
279
+ const GENERATIVE_TOOL_METHODS = new Set(["present", "promptUser"]);
280
+ /**
281
+ * Whether a toolkit entry's value is a call to a tool-producing method on a
282
+ * collected `JSONGenerativeUI` instance (`generative.present()`), which passes
283
+ * through. The method name is checked too, so a typo like `generative.presnt()`
284
+ * is a compile error here rather than a pass-through that fails at runtime.
285
+ */
286
+ function isGenerativeToolEntry(value, instances) {
287
+ return t.isCallExpression(value) && t.isMemberExpression(value.callee) && !value.callee.computed && t.isIdentifier(value.callee.object) && instances.has(value.callee.object.name) && t.isIdentifier(value.callee.property) && GENERATIVE_TOOL_METHODS.has(value.callee.property.name);
288
+ }
289
+ /**
290
+ * Splits a `defineGenerativeComponents({ ... })` library for a build target:
291
+ * a component's `render` (and the client imports it alone uses) is dropped on
292
+ * the server; `properties`/`description` stay on both, since they drive the tool
293
+ * schema either way. Mutates the object in place.
294
+ */
295
+ function compileComponents(object, target, flags, filename) {
296
+ for (const entry of object.properties) {
297
+ const value = entryValue(entry);
298
+ if (!value) throw new GenerativeCompileError(`each component in ${COMPONENTS_WRAPPER}() must be an inline object literal (\`name: { ... }\`) so its \`render\` can be routed`, filename);
299
+ if (!findMember(value, "render")) continue;
300
+ if (target === "client") flags.keptRender = true;
301
+ else removeMember(value, "render");
91
302
  }
92
- if (!sawDefault) throw new GenerativeCompileError("missing a default export", filename);
93
- if (!object) throw new GenerativeCompileError("the default export must be `defineToolkit({ ... })` (imported from \"@assistant-ui/react\"); wrapping is required so a backend `execute` can't be authored in a way that reaches the client", filename);
94
- return object;
95
303
  }
96
304
  /**
97
- * Unwraps the required `defineToolkit({ ... })` wrapper (through `satisfies`/`as`
98
- * and parens) to the underlying object literal. Anything else — a bare object, a
99
- * `satisfies Toolkit` without the wrapper, some other call — yields `null` so the
100
- * caller errors.
305
+ * Splits a `defineToolkit({ ... })` for a build target. Each inline tool is
306
+ * routed by inferred type (see the per-entry logic); a generative entry like
307
+ * `generative.present()` passes through, the library having already split it.
308
+ * Mutates the object in place and records outcomes in {@link TargetFlags}.
101
309
  */
102
- function unwrapDefineToolkit(node) {
103
- if (t.isTSSatisfiesExpression(node) || t.isTSAsExpression(node)) return unwrapDefineToolkit(node.expression);
104
- if (t.isParenthesizedExpression(node)) return unwrapDefineToolkit(node.expression);
105
- if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "defineToolkit" }) && t.isObjectExpression(node.arguments[0])) return node.arguments[0];
310
+ function compileToolkit(object, target, instances, safeToolkitSpreads, flags, filename) {
311
+ const nextProperties = [];
312
+ for (const entry of object.properties) {
313
+ const value = entryValue(entry);
314
+ if (!value) {
315
+ if (t.isSpreadElement(entry) && isSafeToolkitSpread(entry, safeToolkitSpreads)) {
316
+ nextProperties.push(entry);
317
+ continue;
318
+ }
319
+ const raw = entryRawValue(entry);
320
+ if (raw && isGenerativeToolEntry(raw, instances)) {
321
+ nextProperties.push(entry);
322
+ continue;
323
+ }
324
+ throw new GenerativeCompileError("each tool must be an inline object literal (`name: { ... }`) or a compiler-visible toolkit spread / generative tool (e.g. `...defineMcpToolkit(...)`, `...baseToolkit`, or `generative.present()`) so its `execute` can be routed", filename);
325
+ }
326
+ const execute = findMember(value, "execute");
327
+ const isStub = execute ? executeIsStubTool(execute) : false;
328
+ const isExternal = execute ? executeIsExternalTool(execute) : false;
329
+ const type = inferToolType(value, filename);
330
+ const hasRender = !!findMember(value, "render");
331
+ const hasRenderText = !!findMember(value, "renderText");
332
+ if (type === "frontend" && !hasRender && !hasRenderText) throw new GenerativeCompileError("a frontend tool must declare a `render` or `renderText` (it has no server execute to show otherwise)", filename);
333
+ if (type === "human" && !hasRender) throw new GenerativeCompileError("a human tool must declare a `render` so it can collect input", filename);
334
+ if (type === "provider" && execute) applyProviderToolConfig(value, execute, filename);
335
+ if (isExternal) {
336
+ if (!hasRender && !hasRenderText) throw new GenerativeCompileError("an external tool must declare a `render` or `renderText` (assistant-ui only renders calls for tools defined elsewhere)", filename);
337
+ if (target === "server") continue;
338
+ stripExternalToolMetadata(value);
339
+ }
340
+ if (target === "client") {
341
+ if (execute && type === "frontend" && !isStub) stripUseClient(execute);
342
+ else if (execute) removeMember(value, "execute");
343
+ if (hasRender || hasRenderText) flags.keptRender = true;
344
+ } else {
345
+ if (hasRender) removeMember(value, "render");
346
+ if (hasRenderText) removeMember(value, "renderText");
347
+ if (execute) if (type === "backend" && !isExternal) flags.keptBackendExecute = true;
348
+ else removeMember(value, "execute");
349
+ }
350
+ setToolType(value, type);
351
+ setBackendDefault(value, target, type);
352
+ nextProperties.push(entry);
353
+ }
354
+ object.properties = nextProperties;
355
+ }
356
+ function applyProviderToolConfig(object, execute, filename) {
357
+ if (!t.isObjectProperty(execute) || !t.isCallExpression(execute.value) || execute.value.arguments.length !== 1 || !t.isObjectExpression(execute.value.arguments[0])) throw new GenerativeCompileError("`providerTool(...)` must receive an inline object literal", filename);
358
+ const existingNames = new Set(object.properties.flatMap((prop) => {
359
+ if (!t.isObjectProperty(prop) && !t.isObjectMethod(prop)) return [];
360
+ const name = memberName(prop.key, prop.computed);
361
+ return name ? [name] : [];
362
+ }));
363
+ const configNames = /* @__PURE__ */ new Set();
364
+ for (const prop of execute.value.arguments[0].properties) {
365
+ if (!t.isObjectProperty(prop)) throw new GenerativeCompileError("`providerTool(...)` config can only contain object properties", filename);
366
+ const name = memberName(prop.key, prop.computed);
367
+ if (!name) throw new GenerativeCompileError("`providerTool(...)` config can only contain static property names", filename);
368
+ if (t.isFunctionExpression(prop.value) || t.isArrowFunctionExpression(prop.value)) throw new GenerativeCompileError("`providerTool(...)` config cannot contain function-valued properties", filename);
369
+ if (existingNames.has(name) || configNames.has(name)) throw new GenerativeCompileError("`providerTool(...)` config cannot duplicate tool properties", filename);
370
+ configNames.add(name);
371
+ object.properties.push(prop);
372
+ }
373
+ }
374
+ /** The raw AST value of an entry (any expression), or null for spreads/methods. */
375
+ function entryRawValue(entry) {
376
+ if (t.isObjectProperty(entry) && t.isExpression(entry.value)) return entry.value;
106
377
  return null;
107
378
  }
108
379
  function entryValue(entry) {
@@ -126,9 +397,24 @@ function executeBody(member) {
126
397
  function executeIsClient(member) {
127
398
  return !!executeBody(member)?.directives.some((d) => d.value.value === "use client");
128
399
  }
129
- /** Whether an `execute` is the `hitl()` human-in-the-loop sentinel. */
130
- function executeIsHitl(member) {
131
- return t.isObjectProperty(member) && t.isCallExpression(member.value) && t.isIdentifier(member.value.callee, { name: "hitl" });
400
+ function executeIsSentinel(member, name) {
401
+ return t.isObjectProperty(member) && t.isCallExpression(member.value) && t.isIdentifier(member.value.callee, { name });
402
+ }
403
+ /** Whether an `execute` is the human-in-the-loop sentinel. */
404
+ function executeIsHumanTool(member) {
405
+ return executeIsSentinel(member, "humanTool") || executeIsSentinel(member, "hitlTool") || executeIsSentinel(member, "hitl");
406
+ }
407
+ /** Whether an `execute` is the provider-tool sentinel. */
408
+ function executeIsProviderTool(member) {
409
+ return executeIsSentinel(member, "providerTool");
410
+ }
411
+ /** Whether an `execute` is the local override sentinel. */
412
+ function executeIsStubTool(member) {
413
+ return executeIsSentinel(member, "stubTool");
414
+ }
415
+ /** Whether an `execute` is the externally-defined backend tool sentinel. */
416
+ function executeIsExternalTool(member) {
417
+ return executeIsSentinel(member, "externalTool");
132
418
  }
133
419
  /** Drops the `"use client"` directive from an `execute` body (kept frontend). */
134
420
  function stripUseClient(member) {
@@ -137,21 +423,40 @@ function stripUseClient(member) {
137
423
  }
138
424
  /**
139
425
  * The tool's nature, inferred from its (mandatory) `execute` rather than an
140
- * authored `type`: `hitl()` → `human`; `"use client"` → `frontend`; otherwise
141
- * `backend`. The loader writes the result back as a `type` field (see
142
- * {@link setToolType}) so the runtime keeps it.
426
+ * authored `type`: `humanTool()` → `human`; `providerTool(...)` → `provider`;
427
+ * `stubTool()` → `frontend`; `externalTool()` → `backend`; `"use client"` →
428
+ * `frontend`; otherwise `backend`.
429
+ * The loader writes the result back as a `type` field (see {@link setToolType})
430
+ * so the runtime keeps it.
143
431
  */
144
432
  function inferToolType(object, filename) {
145
433
  const execute = findMember(object, "execute");
146
- if (!execute) throw new GenerativeCompileError("every tool must declare an `execute`; use `hitl()` for a human-in-the-loop tool", filename);
147
- if (executeIsHitl(execute)) return "human";
434
+ if (!execute) throw new GenerativeCompileError("every tool must declare an `execute`; use `humanTool()` for a human-in-the-loop tool", filename);
435
+ if (executeIsHumanTool(execute)) return "human";
436
+ if (executeIsProviderTool(execute)) return "provider";
437
+ if (executeIsStubTool(execute)) return "frontend";
438
+ if (executeIsExternalTool(execute)) return "backend";
148
439
  return executeIsClient(execute) ? "frontend" : "backend";
149
440
  }
441
+ function stripExternalToolMetadata(object) {
442
+ removeMember(object, "description");
443
+ removeMember(object, "parameters");
444
+ removeMember(object, "disabled");
445
+ removeMember(object, "toModelOutput");
446
+ removeMember(object, "experimental_onSchemaValidationError");
447
+ removeMember(object, "providerOptions");
448
+ removeMember(object, "streamCall");
449
+ }
150
450
  /** Writes the resolved `type` back onto the tool object (replacing any author's). */
151
451
  function setToolType(object, type) {
152
452
  removeMember(object, "type");
153
453
  object.properties.push(t.objectProperty(t.identifier("type"), t.stringLiteral(type)));
154
454
  }
455
+ function setBackendDefault(object, target, type) {
456
+ removeMember(object, "unstable_backendDefault");
457
+ if (target !== "client" || type !== "frontend" && type !== "human") return;
458
+ object.properties.push(t.objectProperty(t.identifier("unstable_backendDefault"), t.objectExpression([t.objectProperty(t.identifier("parameters"), t.booleanLiteral(true))])));
459
+ }
155
460
  function memberName(key, computed) {
156
461
  if (computed) return void 0;
157
462
  if (t.isIdentifier(key)) return key.name;