From 09ffd070d0eb4ec0ebf13397e6fe520ffc704692 Mon Sep 17 00:00:00 2001 From: "vercel-gh-bot-2[bot]" <282331341+vercel-gh-bot-2[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 07:07:30 +0000 Subject: [PATCH] fix(eve): preserve dynamic tool schema validation Co-authored-by: JonaCodes <11865013+JonaCodes@users.noreply.github.com> --- ...preserve-dynamic-tool-schema-validation.md | 5 + .../eve/src/context/build-dynamic-tools.ts | 9 +- .../context/dynamic-tool-lifecycle.test.ts | 187 ++++++++++++++++++ .../eve/src/context/dynamic-tool-lifecycle.ts | 68 +++++-- .../eve/src/context/dynamic-tool-metadata.ts | 20 +- .../src/context/dynamic-tool-schema-replay.ts | 130 ++++++++++++ packages/eve/src/tools/schema.ts | 5 + 7 files changed, 406 insertions(+), 18 deletions(-) create mode 100644 .changeset/preserve-dynamic-tool-schema-validation.md create mode 100644 packages/eve/src/context/dynamic-tool-schema-replay.ts diff --git a/.changeset/preserve-dynamic-tool-schema-validation.md b/.changeset/preserve-dynamic-tool-schema-validation.md new file mode 100644 index 0000000000..60c39103d5 --- /dev/null +++ b/.changeset/preserve-dynamic-tool-schema-validation.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Preserve live schema transformations and refinements when dynamic tools replay across workflow steps. diff --git a/packages/eve/src/context/build-dynamic-tools.ts b/packages/eve/src/context/build-dynamic-tools.ts index 4b40626bfe..a806464438 100644 --- a/packages/eve/src/context/build-dynamic-tools.ts +++ b/packages/eve/src/context/build-dynamic-tools.ts @@ -11,6 +11,10 @@ import { type CurrentDynamicToolMetadata, type PersistedDynamicToolMetadata, } from "#context/dynamic-tool-metadata.js"; +import { + replayDynamicToolInputSchema, + replayDynamicToolOutputSchema, +} from "#context/dynamic-tool-schema-replay.js"; import { createToolExecuteWithAuth } from "#execution/tool-auth.js"; import { createLogger } from "#internal/logging.js"; import type { @@ -24,7 +28,6 @@ import { lookupDurableDynamicCallback, type DurableDynamicCallbackPhase, } from "#tools/durable-callbacks.js"; -import { toInputSchema, toOutputSchema } from "#tools/schema.js"; const log = createLogger("dynamic-tools"); @@ -131,11 +134,11 @@ export function replayDynamicTools( ); }, }), - inputSchema: toInputSchema(entry.inputSchema), + inputSchema: replayDynamicToolInputSchema(entry), name: entry.name, execution: entry.execution, approval: buildReplayedApproval(entry), - outputSchema: toOutputSchema(entry.outputSchema), + outputSchema: replayDynamicToolOutputSchema(entry), ...(toModelOutputReference === undefined ? {} : { diff --git a/packages/eve/src/context/dynamic-tool-lifecycle.test.ts b/packages/eve/src/context/dynamic-tool-lifecycle.test.ts index 51904e8ffd..e04ed987b6 100644 --- a/packages/eve/src/context/dynamic-tool-lifecycle.test.ts +++ b/packages/eve/src/context/dynamic-tool-lifecycle.test.ts @@ -1,5 +1,6 @@ import { asSchema } from "ai"; import { describe, expect, it, vi } from "vitest"; +import { z as z3 } from "zod/v3"; import { z } from "#compiled/zod/index.js"; import type { DynamicToolEntry } from "#tools/dynamic.js"; @@ -9,6 +10,7 @@ import { type OldSourceOffsetDynamicToolMetadata, type OldStepFunctionDynamicToolMetadata, } from "#context/dynamic-tool-metadata.js"; +import { clearDynamicToolSchemas } from "#context/dynamic-tool-schema-replay.js"; import { resolveApprovalPolicy, type ApprovalContext } from "#approval/definition.js"; import { defineTool, type TaskExec, type ToolContext } from "#tools/definition.js"; import type { JsonObject } from "#shared/json.js"; @@ -386,6 +388,7 @@ const dynamicCallbackRegistry = getDynamicCallbackRegistry(); function simulateColdStart(ctx: ContextContainer): void { for (const metadata of ctx.get(SessionDynamicToolMetadataKey) ?? []) { dynamicCallbackRegistry.delete(metadata.name); + clearDynamicToolSchemas(metadata); } ctx.clearVirtualContext(); } @@ -1165,6 +1168,190 @@ describe("dispatchDynamicToolEvent", () => { }); }); + it("preserves live input transformations when replaying a session tool", async () => { + const ctx = createCtx(); + const handler = vi.fn(() => { + const entry = defineTool({ + description: "normalize input", + inputSchema: z.strictObject({ + value: z.string().trim().min(1).optional(), + }), + execute: async () => ({ ok: true }), + }); + stampDurableDynamicToolCallbacks(entry, { + execute: { callback: () => ({ ok: true }), closure: {} }, + }); + return { normalize: entry }; + }); + const resolver = createResolver("normalizer", ["session.started"], handler); + + await dispatchDynamicToolEvent({ + ctx, + resolvers: [resolver], + messages: [], + event: makeEvent("session.started"), + }); + ctx.set(SessionDynamicToolRuntimeRevisionKey, "deployment:dpl_current"); + simulateColdStart(ctx); + await refreshDynamicSessionToolsForRuntimeRevision({ + ctx, + resolvers: [resolver], + messages: [], + event: createSessionStartedEvent(), + runtimeRevision: "deployment:dpl_current", + }); + + const [tool] = buildDynamicTools(ctx); + const validate = asSchema(tool!.inputSchema).validate; + await expect(validate?.({ value: " " })).resolves.toMatchObject({ success: false }); + await expect(validate?.({ value: " kept " })).resolves.toEqual({ + success: true, + value: { value: "kept" }, + }); + expect(handler).toHaveBeenCalledTimes(2); + }); + + it("upgrades pre-fix metadata to live schema replay during a cold rebind", async () => { + const ctx = createCtx(); + const handler = vi.fn(() => { + const entry = defineTool({ + description: "normalize legacy metadata", + inputSchema: z.strictObject({ value: z.string().trim().min(1) }), + execute: async () => ({ ok: true }), + }); + stampDurableDynamicToolCallbacks(entry, { + execute: { callback: () => ({ ok: true }), closure: {} }, + }); + return { normalize_pre_fix: entry }; + }); + const resolver = createResolver("pre-fix", ["session.started"], handler); + + await dispatchDynamicToolEvent({ + ctx, + resolvers: [resolver], + messages: [], + event: makeEvent("session.started"), + }); + const [current] = ctx.get(SessionDynamicToolMetadataKey)!; + const { inputSchemaIsLive: _live, ...preFixMetadata } = current!; + ctx.set(SessionDynamicToolMetadataKey, [preFixMetadata]); + ctx.set(SessionDynamicToolRuntimeRevisionKey, "deployment:dpl_current"); + dynamicCallbackRegistry.delete(preFixMetadata.name); + + await refreshDynamicSessionToolsForRuntimeRevision({ + ctx, + resolvers: [resolver], + messages: [], + event: createSessionStartedEvent(), + runtimeRevision: "deployment:dpl_current", + }); + + const [tool] = buildDynamicTools(ctx); + const validate = asSchema(tool!.inputSchema).validate; + await expect(validate?.({ value: " " })).resolves.toMatchObject({ success: false }); + await expect(validate?.({ value: " kept " })).resolves.toEqual({ + success: true, + value: { value: "kept" }, + }); + expect(ctx.get(SessionDynamicToolMetadataKey)?.[0]?.inputSchemaIsLive).toBe(true); + expect(handler).toHaveBeenCalledTimes(2); + }); + + it("preserves Zod 3 input transformations during replay", async () => { + const ctx = createCtx(); + const resolver = createResolver("zod-three", ["session.started"], () => { + const entry = defineTool({ + description: "normalize legacy input", + inputSchema: z3.object({ value: z3.string().trim().min(1) }), + execute: async () => ({ ok: true }), + }); + stampDurableDynamicToolCallbacks(entry, { + execute: { callback: () => ({ ok: true }), closure: {} }, + }); + return { normalize_legacy: entry }; + }); + + await dispatchDynamicToolEvent({ + ctx, + resolvers: [resolver], + messages: [], + event: makeEvent("session.started"), + }); + + const [tool] = buildDynamicTools(ctx); + const validate = asSchema(tool!.inputSchema).validate; + await expect(validate?.({ value: " " })).resolves.toMatchObject({ success: false }); + await expect(validate?.({ value: " kept " })).resolves.toEqual({ + success: true, + value: { value: "kept" }, + }); + }); + + it("keeps same-named live schemas isolated across sessions", async () => { + const createNormalizer = (minimum: number) => { + const entry = defineTool({ + description: "normalize isolated input", + inputSchema: z.strictObject({ value: z.string().trim().min(minimum) }), + execute: async () => ({ ok: true }), + }); + stampDurableDynamicToolCallbacks(entry, { + execute: { callback: () => ({ ok: true }), closure: {} }, + }); + return { normalize_isolated: entry }; + }; + const shortCtx = createCtx(); + const longCtx = createCtx(); + + await dispatchDynamicToolEvent({ + ctx: shortCtx, + resolvers: [createResolver("short", ["session.started"], () => createNormalizer(2))], + messages: [], + event: makeEvent("session.started"), + }); + await dispatchDynamicToolEvent({ + ctx: longCtx, + resolvers: [createResolver("long", ["session.started"], () => createNormalizer(4))], + messages: [], + event: makeEvent("session.started"), + }); + + const shortValidate = asSchema(buildDynamicTools(shortCtx)[0]!.inputSchema).validate; + const longValidate = asSchema(buildDynamicTools(longCtx)[0]!.inputSchema).validate; + await expect(shortValidate?.({ value: " abc " })).resolves.toEqual({ + success: true, + value: { value: "abc" }, + }); + await expect(longValidate?.({ value: " abc " })).resolves.toMatchObject({ success: false }); + }); + + it("fails closed when a live schema has not been rebound", async () => { + const ctx = createCtx(); + const resolver = createResolver("unbound", ["session.started"], () => { + const entry = defineTool({ + description: "require live validation", + inputSchema: z.strictObject({ value: z.string().trim() }), + execute: async () => ({ ok: true }), + }); + stampDurableDynamicToolCallbacks(entry, { + execute: { callback: () => ({ ok: true }), closure: {} }, + }); + return { require_live_schema: entry }; + }); + + await dispatchDynamicToolEvent({ + ctx, + resolvers: [resolver], + messages: [], + event: makeEvent("session.started"), + }); + const [metadata] = ctx.get(SessionDynamicToolMetadataKey)!; + clearDynamicToolSchemas(metadata!); + + expect(() => buildDynamicTools(ctx)).toThrow( + 'Dynamic tool "require_live_schema" cannot replay its live input schema', + ); + }); + it("provides inline auth to a replayed session-scoped tool", async () => { const ctx = createCtx(); const getToken = vi.fn(async () => ({ token: "replayed-token" })); diff --git a/packages/eve/src/context/dynamic-tool-lifecycle.ts b/packages/eve/src/context/dynamic-tool-lifecycle.ts index 0a80a9a0bd..b660e08200 100644 --- a/packages/eve/src/context/dynamic-tool-lifecycle.ts +++ b/packages/eve/src/context/dynamic-tool-lifecycle.ts @@ -2,6 +2,11 @@ import type { ModelMessage } from "ai"; import { replayDynamicTools } from "#context/build-dynamic-tools.js"; import { contextStorage, type AlsContext } from "#context/container.js"; +import { + hasUnregisteredDynamicToolSchemas, + registerDynamicToolSchemas, + transferDynamicToolSchemas, +} from "#context/dynamic-tool-schema-replay.js"; import type { ContextKey } from "#context/key.js"; import { SessionDynamicToolMetadataKey, @@ -36,7 +41,7 @@ import { } from "#tools/durable-callbacks.js"; import { toErrorMessage } from "#shared/errors.js"; import { parseJsonObject } from "#shared/json.js"; -import { serializeInputSchema, serializeOutputSchema } from "#tools/schema.js"; +import { isStandardSchema, serializeInputSchema, serializeOutputSchema } from "#tools/schema.js"; import type { ResolvedDynamicToolResolver } from "#runtime/types.js"; const log = createLogger("dynamic-tools"); @@ -227,16 +232,40 @@ function createMetadata(input: { readonly name: string; readonly resolver: ResolvedDynamicToolResolver; }): CurrentDynamicToolMetadata { - return { - callbacks: validateDurableDynamicToolCallbacks(input.name, input.entry), + const inputSchema = serializeInputSchema(input.entry.inputSchema); + const outputSchema = serializeOutputSchema(input.entry.outputSchema); + const callbacks = validateDurableDynamicToolCallbacks(input.name, input.entry); + const inputSchemaIsLive = isStandardSchema(input.entry.inputSchema); + const outputSchemaIsLive = isStandardSchema(input.entry.outputSchema); + const metadata: CurrentDynamicToolMetadata = { + callbacks, description: input.entry.description, execution: input.entry.execution === "background" ? "background" : undefined, entryKey: input.entryKey, - inputSchema: serializeInputSchema(input.entry.inputSchema), + inputSchema, + ...(inputSchemaIsLive ? { inputSchemaIsLive: true } : {}), name: input.name, - outputSchema: serializeOutputSchema(input.entry.outputSchema), + outputSchema, + ...(outputSchemaIsLive ? { outputSchemaIsLive: true } : {}), resolverSlug: input.resolver.slug, }; + if (inputSchemaIsLive || outputSchemaIsLive) { + registerDynamicToolSchemas({ + inputSchema: input.entry.inputSchema, + metadata, + outputSchema: input.entry.outputSchema, + }); + } + return metadata; +} + +function mergePersistedDynamicToolMetadata( + persisted: readonly PersistedDynamicToolMetadata[], + resolved: readonly CurrentDynamicToolMetadata[], +): readonly CurrentDynamicToolMetadata[] { + const merged = toCurrentDynamicToolMetadataList(persisted, resolved); + transferDynamicToolSchemas(merged, resolved); + return merged; } interface ResolvedDynamicToolEvent { @@ -343,7 +372,11 @@ export async function preparePersistedStepDynamicToolMetadata(input: { }): Promise { const persisted = input.ctx.get(StepDynamicToolMetadataKey) ?? []; const current = persisted.filter(isCurrentDynamicToolMetadata); - if (current.length === persisted.length && !hasUnregisteredDurableDynamicCallbacks(current)) { + if ( + current.length === persisted.length && + !hasUnregisteredDurableDynamicCallbacks(current) && + !hasUnregisteredDynamicToolSchemas(current) + ) { if (current.length > 0) { storeResolvedStepTools({ ctx: input.ctx, event: input.event, metadata: current }); } @@ -355,7 +388,7 @@ export async function preparePersistedStepDynamicToolMetadata(input: { storeResolvedStepTools({ ctx: input.ctx, event: input.event, - metadata: toCurrentDynamicToolMetadataList( + metadata: mergePersistedDynamicToolMetadata( persisted, resolved.filter(isCurrentDynamicToolMetadata), ), @@ -412,7 +445,12 @@ export async function refreshDynamicSessionToolsForRuntimeRevision(input: { const hasOldMetadata = current.length !== persisted.length; const revisionChanged = input.ctx.get(SessionDynamicToolRuntimeRevisionKey) !== input.runtimeRevision; - if (!revisionChanged && !hasOldMetadata && !hasUnregisteredDurableDynamicCallbacks(current)) { + if ( + !revisionChanged && + !hasOldMetadata && + !hasUnregisteredDurableDynamicCallbacks(current) && + !hasUnregisteredDynamicToolSchemas(current) + ) { return; } const matching = input.resolvers.filter((resolver) => @@ -424,12 +462,12 @@ export async function refreshDynamicSessionToolsForRuntimeRevision(input: { : await resolveToolsFromEvent(input.ctx, matching, input.event, input.messages); input.ctx.set( SessionDynamicToolMetadataKey, - revisionChanged ? metadata : toCurrentDynamicToolMetadataList(persisted, metadata), + revisionChanged ? metadata : mergePersistedDynamicToolMetadata(persisted, metadata), ); input.ctx.set(SessionDynamicToolRuntimeRevisionKey, input.runtimeRevision); } -/** Re-registers callbacks for compiled resolvers that explicitly support cold replay. */ +/** Re-registers runtime bindings for compiled resolvers that explicitly support cold replay. */ export async function rebindMissingCompiledDynamicToolCallbacks(input: { readonly ctx: AlsContext; readonly event: UnstampedMessageStreamEvent; @@ -440,7 +478,9 @@ export async function rebindMissingCompiledDynamicToolCallbacks(input: { input.ctx.get(TurnDynamicToolMetadataKey) ?? []; const needsResolution = persisted.filter( (entry) => - !isCurrentDynamicToolMetadata(entry) || hasUnregisteredDurableDynamicCallbacks([entry]), + !isCurrentDynamicToolMetadata(entry) || + hasUnregisteredDurableDynamicCallbacks([entry]) || + hasUnregisteredDynamicToolSchemas([entry]), ); if (needsResolution.length === 0) return; const resolverSlugs = new Set(needsResolution.map((entry) => entry.resolverSlug)); @@ -463,7 +503,7 @@ export async function rebindMissingCompiledDynamicToolCallbacks(input: { input.ctx, async () => await resolveToolsFromEvent(input.ctx, matching, input.event, input.messages), ); - const updated = toCurrentDynamicToolMetadataList(persisted, resolved.metadata); + const updated = mergePersistedDynamicToolMetadata(persisted, resolved.metadata); input.ctx.set(TurnDynamicToolMetadataKey, updated); const unresolved = updated.filter( @@ -471,7 +511,9 @@ export async function rebindMissingCompiledDynamicToolCallbacks(input: { needsResolution.some( (candidate) => candidate.resolverSlug === entry.resolverSlug && candidate.name === entry.name, - ) && hasUnregisteredDurableDynamicCallbacks([entry]), + ) && + (hasUnregisteredDurableDynamicCallbacks([entry]) || + hasUnregisteredDynamicToolSchemas([entry])), ); if (unresolved.length > 0) { throw new Error( diff --git a/packages/eve/src/context/dynamic-tool-metadata.ts b/packages/eve/src/context/dynamic-tool-metadata.ts index a619d77f3c..d48b0ba90e 100644 --- a/packages/eve/src/context/dynamic-tool-metadata.ts +++ b/packages/eve/src/context/dynamic-tool-metadata.ts @@ -6,7 +6,9 @@ interface DynamicToolMetadataBase { readonly description: string; readonly execution?: "background"; readonly inputSchema: JsonObject; + readonly inputSchemaIsLive?: true; readonly outputSchema?: JsonObject; + readonly outputSchemaIsLive?: true; readonly resolverSlug: string; readonly entryKey: string; } @@ -88,12 +90,26 @@ function convertSourceOffsetMetadata( return { ...metadata, callbacks }; } +function preserveResolvedLiveSchemaMarkers( + metadata: CurrentDynamicToolMetadata, + resolved: CurrentDynamicToolMetadata | undefined, +): CurrentDynamicToolMetadata { + if (resolved === undefined) return metadata; + return { + ...metadata, + ...(resolved.inputSchemaIsLive === true ? { inputSchemaIsLive: true } : {}), + ...(resolved.outputSchemaIsLive === true ? { outputSchemaIsLive: true } : {}), + }; +} + /** Converts persisted metadata without changing already-persisted callback closures. */ export function toCurrentDynamicToolMetadata( persisted: PersistedDynamicToolMetadata, resolved?: CurrentDynamicToolMetadata, ): CurrentDynamicToolMetadata { - if (isCurrentDynamicToolMetadata(persisted)) return persisted; + if (isCurrentDynamicToolMetadata(persisted)) { + return preserveResolvedLiveSchemaMarkers(persisted, resolved); + } if (resolved === undefined) { throw new Error( `Dynamic tool "${persisted.name}" uses old persisted metadata, but its resolver did not return a current replacement.`, @@ -114,7 +130,7 @@ export function toCurrentDynamicToolMetadata( ); } } - return convertSourceOffsetMetadata(persisted); + return preserveResolvedLiveSchemaMarkers(convertSourceOffsetMetadata(persisted), resolved); } if ( persisted.approvalResponseStepFnName !== undefined && diff --git a/packages/eve/src/context/dynamic-tool-schema-replay.ts b/packages/eve/src/context/dynamic-tool-schema-replay.ts new file mode 100644 index 0000000000..08455899aa --- /dev/null +++ b/packages/eve/src/context/dynamic-tool-schema-replay.ts @@ -0,0 +1,130 @@ +import type { JsonObject } from "#shared/json.js"; +import type { ToolSchema, ToolSchemaSource } from "#tools/schema.js"; +import { isStandardSchema, isToolSchema, toInputSchema, toOutputSchema } from "#tools/schema.js"; + +const REGISTRY = Symbol.for("eve:dynamic-tool-schemas"); + +interface DynamicToolSchemas { + readonly input: ToolSchemaSource; + readonly output?: ToolSchemaSource; +} + +interface DynamicToolSchemaMetadata { + readonly inputSchema: JsonObject; + readonly inputSchemaIsLive?: true; + readonly name: string; + readonly outputSchema?: JsonObject; + readonly outputSchemaIsLive?: true; + readonly resolverSlug: string; +} + +type Registry = WeakMap; + +function getRegistry(): Registry { + const global = globalThis as Record; + const existing = global[REGISTRY]; + if (existing !== undefined) return existing; + + const registry: Registry = new WeakMap(); + global[REGISTRY] = registry; + return registry; +} + +/** Keeps live validators beside the callback bindings that make metadata replayable. */ +export function registerDynamicToolSchemas(input: { + readonly inputSchema: ToolSchemaSource; + readonly metadata: DynamicToolSchemaMetadata; + readonly outputSchema?: ToolSchemaSource; +}): void { + getRegistry().set(input.metadata, { + input: input.inputSchema, + output: input.outputSchema, + }); +} + +export function clearDynamicToolSchemas(metadata: DynamicToolSchemaMetadata): void { + getRegistry().delete(metadata); +} + +/** Carries newly resolved live schemas onto metadata whose durable closures must be preserved. */ +export function transferDynamicToolSchemas( + target: readonly DynamicToolSchemaMetadata[], + resolved: readonly DynamicToolSchemaMetadata[], +): void { + const registry = getRegistry(); + for (const entry of target) { + if (entry.inputSchemaIsLive !== true && entry.outputSchemaIsLive !== true) continue; + if (registry.has(entry)) continue; + const source = resolved.find( + (candidate) => candidate.name === entry.name && candidate.resolverSlug === entry.resolverSlug, + ); + if (source === undefined) continue; + const schemas = registry.get(source); + if (schemas !== undefined) registry.set(entry, schemas); + } +} + +export function hasUnregisteredDynamicToolSchemas( + metadata: readonly DynamicToolSchemaMetadata[], +): boolean { + const registry = getRegistry(); + return metadata.some( + (entry) => + (entry.inputSchemaIsLive === true || entry.outputSchemaIsLive === true) && + !registry.has(entry), + ); +} + +export function replayDynamicToolInputSchema(metadata: DynamicToolSchemaMetadata): ToolSchema { + if (metadata.inputSchemaIsLive !== true) return toInputSchema(metadata.inputSchema); + const registered = getRegistry().get(metadata); + if (registered !== undefined) { + return preserveLiveValidation(registered.input, metadata.inputSchema, "input"); + } + throw missingLiveSchemaError(metadata.name, "input"); +} + +export function replayDynamicToolOutputSchema( + metadata: DynamicToolSchemaMetadata, +): ToolSchema | undefined { + if (metadata.outputSchemaIsLive !== true) return toOutputSchema(metadata.outputSchema); + if (metadata.outputSchema === undefined) { + throw missingLiveSchemaError(metadata.name, "output"); + } + const registered = getRegistry().get(metadata); + if (registered !== undefined) { + if (registered.output === undefined) { + throw missingLiveSchemaError(metadata.name, "output"); + } + return preserveLiveValidation(registered.output, metadata.outputSchema, "output"); + } + throw missingLiveSchemaError(metadata.name, "output"); +} + +function preserveLiveValidation( + source: ToolSchemaSource, + fallback: JsonObject, + direction: "input" | "output", +): ToolSchema { + if (isToolSchema(source)) return source; + if (!isStandardSchema(source)) { + return direction === "input" ? toInputSchema(source) : toOutputSchema(source); + } + + const standard = source["~standard"]; + const emit = (): Record => structuredClone(fallback); + return { + "~standard": { + ...standard, + vendor: "eve", + jsonSchema: { input: emit, output: emit }, + }, + } as ToolSchema; +} + +function missingLiveSchemaError(toolName: string, direction: "input" | "output"): Error { + return new Error( + `Dynamic tool "${toolName}" cannot replay its live ${direction} schema because its resolver ` + + "is not registered in this process. Restore the tool definition or start a new session.", + ); +} diff --git a/packages/eve/src/tools/schema.ts b/packages/eve/src/tools/schema.ts index 8dc3f9a153..5f2bba19b8 100644 --- a/packages/eve/src/tools/schema.ts +++ b/packages/eve/src/tools/schema.ts @@ -104,6 +104,11 @@ export function isToolSchema(value: unknown): value is ToolSchema { ); } +/** Returns whether a value provides live Standard Schema validation. */ +export function isStandardSchema(value: unknown): value is StandardSchemaV1 { + return typeof getStandardSchemaProperties(value)?.validate === "function"; +} + /** * Permissive schema lowered onto model-visible tools whose definitions * declare no input schema. Accepts any input — an absent schema declares no