Skip to content

feat(provider-utils): narrow tool() return when execute is provided#15466

Merged
lgrammel merged 1 commit into
mainfrom
hiroki/tool-execute-inference
May 26, 2026
Merged

feat(provider-utils): narrow tool() return when execute is provided#15466
lgrammel merged 1 commit into
mainfrom
hiroki/tool-execute-inference

Conversation

@privatenumber
Copy link
Copy Markdown
Contributor

Background

Consumers that define a tool with execute in their own codebase and then call .execute(...) (most often in tests) currently hit a typing gap. The inferred type of tool({ ...with execute... }) is the wide Tool<INPUT, OUTPUT, CONTEXT> union, where execute is ToolExecuteFunction<...> | undefined. That forces isExecutableTool narrowing, a ! assertion, or ?.() chaining at the call site even when the consumer statically knows execute is defined.

The SDK already ships the right primitive: ExecutableTool<TOOL> from @ai-sdk/provider-utils (added in #14516). It just isn't wired into tool()'s own return type. Current options to get a narrowed tool are:

  1. Call isExecutableTool(t) (runtime guard) at every consumption site.
  2. Cast at the definition site with as ExecutableTool<...>.
  3. Wrap tool() in a project-local factory that re-applies the cast.

Summary

Add a fifth overload to tool() that matches calls whose config includes an execute function. That overload returns ExecutableTool<Tool<INPUT, OUTPUT, CONTEXT>> instead of the wider Tool<INPUT, OUTPUT, CONTEXT>. Calls without execute fall through to the existing four overloads unchanged.

// New overload (added at the top so it wins for execute-bearing calls):
export function tool<INPUT, OUTPUT, CONTEXT extends Context>(
  tool: Tool<INPUT, OUTPUT, CONTEXT> & {
    execute: ToolExecuteFunction<INPUT, OUTPUT, CONTEXT>;
  },
): ExecutableTool<Tool<INPUT, OUTPUT, CONTEXT>>;

This is non-breaking:

  • Runtime: identical. The implementation body is still return tool;.
  • Value-level type: ExecutableTool<Tool<I, O, C>> is Tool<I, O, C> & { execute: NonNullable<...> }. Strictly assignable to the old return; anything that took a Tool<...> keeps compiling.
  • Property-level type: t.execute narrows from ToolExecuteFunction<...> | undefined to ToolExecuteFunction<...>. Existing runtime null checks become provably-true but still compile.

The existing type tests in tool.test-d.ts codified the wider contract. This PR updates the three affected assertions to expect the narrower one.

Effect at a call site:

const t = tool({
  inputSchema: z.object({ q: z.string() }),
  execute: async ({ q }) => ({ result: q.toUpperCase() }),
});

// Before: t.execute is `ToolExecuteFunction<...> | undefined`.
//   Needs isExecutableTool(t), or t.execute!(...), or t.execute?.(...).
// After:  t.execute is `ToolExecuteFunction<...>`. Direct call type-checks.
await t.execute({ q: 'hi' }, { toolCallId: 'x', messages: [], context: {} });

Manual Verification

  • pnpm type-check:full passes.
  • pnpm exec vitest --typecheck.only in packages/provider-utils: 26 type-test files, 184 assertions, all pass.
  • pnpm exec vitest --typecheck.only in packages/ai: 30 type-test files, 504 assertions, all pass.
  • pnpm --filter '@ai-sdk/provider-utils' test:node: 65 test files, 606 tests, all pass.
  • pnpm check (lint + format) clean.

Open question for maintainers

This is small enough to land standalone, but #14516 deliberately introduced ExecutableTool and isExecutableTool as additive primitives and chose not to wire them into tool() itself. Was that intentional?

If you'd rather keep tool()'s return wide and expose narrowing as an opt-in, happy to close this and instead propose:

  • A named executableTool() factory exported from @ai-sdk/provider-utils (symmetric with isExecutableTool/ExecutableTool).
  • A short doc paragraph in content/docs/03-ai-sdk-core/15-tools-and-tool-calling.mdx covering isExecutableTool for the runtime-discovered case (MCP etc).

Marking this as a draft until you've had a chance to weigh in.

Checklist

  • All commits are signed (PRs with unsigned commits cannot be merged)
  • Tests have been added / updated (for bug fixes / features)
  • Documentation has been added / updated (for bug fixes / features)
  • A patch changeset for relevant packages has been added
  • I have reviewed this pull request (self-review)

Related

@aayush-kapoor aayush-kapoor added the backport Admins only: add this label to a pull request in order to backport it to the prior version label May 20, 2026
@lgrammel lgrammel merged commit f617ac2 into main May 26, 2026
52 of 53 checks passed
@lgrammel lgrammel deleted the hiroki/tool-execute-inference branch May 26, 2026 10:25
@github-actions
Copy link
Copy Markdown
Contributor

🚀 Published in:

Package Version
ai 7.0.0-canary.153 github npm
@ai-sdk/alibaba 2.0.0-canary.55 github npm
@ai-sdk/amazon-bedrock 5.0.0-canary.70 github npm
@ai-sdk/angular 3.0.0-canary.153 github npm
@ai-sdk/anthropic 4.0.0-canary.60 github npm
@ai-sdk/anthropic-aws 1.0.0-canary.1 github npm
@ai-sdk/assemblyai 3.0.0-canary.46 github npm
@ai-sdk/azure 4.0.0-canary.64 github npm
@ai-sdk/baseten 2.0.0-canary.52 github npm
@ai-sdk/black-forest-labs 2.0.0-canary.46 github npm
@ai-sdk/bytedance 2.0.0-canary.46 github npm
@ai-sdk/cerebras 3.0.0-canary.52 github npm
@ai-sdk/cohere 4.0.0-canary.48 github npm
@ai-sdk/deepgram 3.0.0-canary.45 github npm
@ai-sdk/deepinfra 3.0.0-canary.52 github npm
@ai-sdk/deepseek 3.0.0-canary.49 github npm
@ai-sdk/elevenlabs 3.0.0-canary.45 github npm
@ai-sdk/fal 3.0.0-canary.45 github npm
@ai-sdk/fireworks 3.0.0-canary.52 github npm
@ai-sdk/gateway 4.0.0-canary.92 github npm
@ai-sdk/gladia 3.0.0-canary.45 github npm
@ai-sdk/google 4.0.0-canary.73 github npm
@ai-sdk/google-vertex 5.0.0-canary.95 github npm
@ai-sdk/groq 4.0.0-canary.48 github npm
@ai-sdk/huggingface 2.0.0-canary.52 github npm
@ai-sdk/hume 3.0.0-canary.45 github npm
@ai-sdk/klingai 4.0.0-canary.46 github npm
@ai-sdk/langchain 3.0.0-canary.153 github npm
@ai-sdk/llamaindex 3.0.0-canary.153 github npm
@ai-sdk/lmnt 3.0.0-canary.45 github npm
@ai-sdk/luma 3.0.0-canary.45 github npm
@ai-sdk/mcp 2.0.0-canary.54 github npm
@ai-sdk/mistral 4.0.0-canary.50 github npm
@ai-sdk/moonshotai 3.0.0-canary.52 github npm
@ai-sdk/open-responses 2.0.0-canary.48 github npm
@ai-sdk/openai 4.0.0-canary.64 github npm
@ai-sdk/openai-compatible 3.0.0-canary.52 github npm
@ai-sdk/otel 1.0.0-canary.99 github npm
@ai-sdk/perplexity 4.0.0-canary.48 github npm
@ai-sdk/prodia 2.0.0-canary.48 github npm
@ai-sdk/provider-utils 5.0.0-canary.44 github npm
@ai-sdk/quiverai 2.0.0-canary.1 github npm
@ai-sdk/react 4.0.0-canary.155 github npm
@ai-sdk/replicate 3.0.0-canary.46 github npm
@ai-sdk/revai 3.0.0-canary.46 github npm
@ai-sdk/rsc 3.0.0-canary.154 github npm
@ai-sdk/svelte 5.0.0-canary.153 github npm
@ai-sdk/togetherai 3.0.0-canary.52 github npm
@ai-sdk/valibot 3.0.0-canary.44 github npm
@ai-sdk/vercel 3.0.0-canary.52 github npm
@ai-sdk/voyage 2.0.0-canary.19 github npm
@ai-sdk/vue 4.0.0-canary.153 github npm
@ai-sdk/workflow 1.0.0-canary.70 github npm
@ai-sdk/xai 4.0.0-canary.68 github npm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport Admins only: add this label to a pull request in order to backport it to the prior version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants