Tool CallingSAFE
🤖 Type-safe, provider-agnostic TypeScript AI SDK for streaming chat, tool calling, agents, and multimodal apps across OpenAI, Anthropic, Gemini, React, Vue, Svelte, and Solid.
Overview
🤖 Type-safe, provider-agnostic TypeScript AI SDK for streaming chat, tool calling, agents, and multimodal apps across OpenAI, Anthropic, Gemini, React, Vue, Svelte, and Solid.
dafe8ab3bd88OBSERVED · 2026-10-08What it tells the agent
The instruction file, verbatim from the audited commit — this is the text the model reads, and the surface the audit's instruction layer examines. Quoted here so you can judge it without cloning anything.
---
name: ai-core/tool-calling
description: >
Isomorphic tool system: toolDefinition() with Zod schemas,
.server() and .client() implementations, passing tools to both
chat() on server and useChat/clientTools on client, tool approval
flows with needsApproval and bound interrupts (resolveInterrupt), generic
middleware interrupts with defineInterrupt(), lazy tool
discovery with lazy:true, rendering ToolCallPart and ToolResultPart
in UI.
type: sub-skill
library: tanstack-ai
library_version: '0.42.0'
sources:
- 'TanStack/ai:docs/tools/tools.md'
- 'TanStack/ai:docs/tools/server-tools.md'
- 'TanStack/ai:docs/tools/client-tools.md'
- 'TanStack/ai:docs/tools/tool-approval.md'
- 'TanStack/ai:docs/tools/lazy-tool-discovery.md'
---
# Tool Calling
This skill builds on ai-core. Read it first for critical rules.
## Setup
Complete end-to-end example: shared definition, server tool, client tool, server route, React client.
The four files below share one scope, so later files use the earlier exports directly.
```typescript group=product-catalog
// tools/definitions.ts
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
export const getProductsDef = toolDefinition({
name: 'get_products',
description: 'Search for products in the catalog',
inputSchema: z.object({
query: z.string().meta({ description: 'Search keyword' }),
limit: z.number().optional().meta({ description: 'Max results' }),
}),
outputSchema: z.object({
products: z.array(
z.object({ id: z.string(), name: z.string(), price: z.number() }),
),
}),
})
export const updateCartUIDef = toolDefinition({
name: 'update_cart_ui',
description: 'Update the shopping cart UI with item count',
inputSchema: z.object({ itemCount: z.number(), message: z.string() }),
outputSchema: z.object({ displayed: z.boolean() }),
})
```
```typescript group=product-catalog
// tools/server.ts (uses getProductsDef from tools/definitions.ts)
import { db } from './db'
export const getProducts = getProductsDef.server(async ({ query, limit }) => {
const results: Array<{ id: string; name: string; price: number }> =
await db.products.search(query, { limit: limit ?? 10 })
return {
products: results.map((p) => ({ id: p.id, name: p.name, price: p.price })),
}
})
```
```typescript group=product-catalog
// api/chat/route.ts (uses getProducts and updateCartUIDef from tools/)
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages,
tools: [getProducts, updateCartUIDef], // server tool + client definition
})
return toServerSentEventsResponse(stream)
}
```
```tsx group=product-catalog
// app/chat.tsx (uses updateCartUIDef from tools/definitions.ts)
import {
useChat,
fetchServerSentEvents,
createChatClientOptions,
type InferChatMessages,
} from '@tanstack/ai-react'
import { clientTools } from '@tanstack/ai-client'
import { useState } from 'react'
function ChatPage() {
const [cartCount, setCartCount] = useState(0)
const updateCartUI = updateCartUIDef.client((input) => {
setCartCount(input.itemCount)
return { displayed: true }
})
const tools = clientTools(updateCartUI)
const chatOptions = createChatClientOptions({
connection: fetchServerSentEvents('/api/chat'),
tools,
})
const { messages, sendMessage } = useChat(chatOptions)
// InferChatMessages ties part types to the configured tools when needed:
// type Messages = InferChatMessages<typeof chatOptions>
return (
<div>
<span>Cart: {cartCount}</span>
{messages.map((msg) => (
<div key={msg.id}>
{msg.parts.map((part) => {
if (part.type === 'text') return <p>{part.content}</p>
if (part.type === 'tool-call') {
return (
<div key={part.id}>
Tool: {part.name} ({part.state})
</div>
)
}
return null
})}
</div>
))}
</div>
)
}
```
## Core Patterns
### Generic middleware interrupts
Use `defineInterrupt()` when middleware needs typed data from the client. This
does not replace `needsApproval`. Tool approval asks whether a tool can run.
Generic interrupts ask for application data at a chat lifecycle boundary.
Define the interrupt once. Register it with both `chat({ interrupts })` and
`useChat({ interrupts })`. Emit it only from `onInterruptBoundary`, then read
the typed result in `onInterruptResolution`.
```typescript
import { defineInterrupt, type ChatMiddleware } from '@tanstack/ai'
import { z } from 'zod'
const reviewPlan = defineInterrupt({
id: 'review-plan',
payloadSchema: z.object({ title: z.string() }),
responseSchema: z.object({ approved: z.boolean() }),
})
const reviewMiddleware: ChatMiddleware<unknown, typeof reviewPlan> = {
onInterruptBoundary(ctx) {
if (ctx.phase !== 'beforeTools') return
return {
interrupts: [
reviewPlan.interrupt({
key: 'release-plan',
reason: 'review-required',
message: 'Approve this plan?',
payload: { title: 'Release plan' },
}),
],
}
},
onInterruptResolution(_ctx, resumedInterrupts) {
for (const result of resumedInterrupts.for(reviewPlan)) {
if (result.status === 'resolved' && !result.response.approved) {
return { toolResume: 'stop' }
}
}
},
}
```
Several middleware can request generic interrupts at one boundary. They share
one AG-UI interrupt batch with tool approvals. A continuation starts only after
the client resolves or cancels every bound item. `stop` is more restrictive than
`cancel`, which is more restrictive than `continue`.
Do not emit raw AG-UI interrupt events from middleware. Use the boundary hook
so the engine creates one tTrust audit
SAFEgrade B · trust 89/100 Nothing in the source contradicts what it says it does. Grade A is reserved for packages that have also passed the behavioural sandbox.
| Layer | What it checks | Result |
|---|---|---|
| L0 | Provenance & inventory | PASS |
| L1 | Static analysis of the code | NA |
| L2 | Instruction surface (what it tells the agent) | PASS |
| L3 | Class-specific surface | PASS |
| L4 | Behavioural (sandbox) | SKIPPED |
What the source does
- Filesystem
- none-observed
- Network
- none-observed
- Shell
- none-observed
- Dependencies
- pinned
- Secrets in source
- none-found
Findings (0)
No findings outside the package's declared scope.
Gates applied: no_behavioural_pass.
dafe8ab3bd88full audit observations/trust-audit/skill/tanstack__tool-calling.json · Report an issue / request a re-scanAudit history
Every audit this skill has had.
| Date | Source | Verdict | Grade | Score | Change |
|---|---|---|---|---|---|
| 2026-10-08 | dafe8ab3bd88 | SAFE | B | 89 | first audit |
Questions
What does the Tool Calling skill do?
🤖 Type-safe, provider-agnostic TypeScript AI SDK for streaming chat, tool calling, agents, and multimodal apps across OpenAI, Anthropic, Gemini, React, Vue, Svelte, and Solid.
Is Tool Calling safe to install?
The audit found nothing in the source that contradicts what it says it does, and graded it B (89/100). Grade A is held back for packages that have also passed a sandboxed behavioural run, which is why a clean skill reads B.
What can Tool Calling access on my machine?
The audit observed no filesystem, network or shell use at all in its source.
How current is this page?
The grade is for one exact copy of the source (dafe8ab3bd88), read on 2026-10-08. The repository is watched, and a new audit runs when it changes — this is the first audit.