needsApproval client tools can't be approved without a server (custom connection adapters)

Cerrado
#1,945 0 comentarios 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@jherr ya está trabajando en esto.

Desde el 30/10/2026.

  • #1969 de @AlemTuzlak — fusionado

Evaluación

Este issue todavía no se ha evaluado.

Descripción

waiting-on: maintainer
TanStack AI version

@tanstack/ai 1.0.0, @tanstack/ai-client 1.0.0, @tanstack/ai-react 1.0.0 (same code on main @ caaa97b)

Framework/Library version

React 19.3.1, Vite 8

Describe the bug and the steps to reproduce it

The model runs entirely in the browser (Transformers.js in a Web Worker, or an on-device model behind a local endpoint). There is no server and no chat(). Each model is a custom ConnectConnectionAdapter passed to useChat({ connection, tools }). The adapter yields AG-UI events and ends a tool-calling run with RUN_FINISHED and outcome: { type: 'success', pendingToolCallIds }. useChat then runs the client tools and continues. That works.

A client tool with needsApproval: true never runs and never prompts:

import { toolDefinition, EventType } from '@tanstack/ai'
import { useChat, type ConnectConnectionAdapter } from '@tanstack/ai-react'
import { z } from 'zod'

const addCards = toolDefinition({
  name: 'add_cards',
  description: 'Add cards to the deck',
  inputSchema: z.object({ names: z.array(z.string()) }),
  outputSchema: z.object({ added: z.number() }),
  needsApproval: true,
}).client(({ names }) => ({ added: names.length }))

const localModel: ConnectConnectionAdapter = {
  async *connect(_messages, _data, _signal, { threadId, runId }) {
    // ...the in-browser model decides to call add_cards
    const toolCallId = 'call_1'
    yield { type: EventType.RUN_STARTED, threadId, runId }
    yield { type: EventType.TOOL_CALL_START, toolCallId, toolCallName: 'add_cards' }
    yield { type: EventType.TOOL_CALL_ARGS, toolCallId, delta: '{"names":["Sol Ring"]}' }
    yield { type: EventType.TOOL_CALL_END, toolCallId }
    yield {
      type: EventType.RUN_FINISHED,
      threadId,
      runId,
      outcome: { type: 'success', pendingToolCallIds: [toolCallId] },
    }
  },
}

function Chat() {
  const { interrupts } = useChat({ connection: localModel, tools: [addCards] })
  // ...
}

Expected: a kind: 'tool-approval' item in useChat().interrupts, so the same Approve/Deny UI from the Tool Approval docs works, and resolveInterrupt(true) runs the .client() function.

Actual: interrupts stays empty, the tool never runs, and the console shows:

[ChatClient] Did not run tool call call_1: add_cards needs approval, and the server did not ask for it

This comes from onToolCall in chat-client (source):

if (
  this.handingOverPendingToolCalls &&
  clientTool?.needsApproval === true
) {
  console.warn(`[ChatClient] Did not run tool call ${args.toolCallId}: ${args.toolName} needs approval, and the server did not ask for it`)
  return
}

The Client Tools docs describe this as intended for AG-UI servers. The gap is that a tool-approval interrupt can only come from a server. The ChatClient only creates one from a RUN_FINISHED interrupt outcome. interrupt-manager binds it as tool-approval only when the binding's inputSchemaHash, approvalSchemaHash and responseSchemaHash match the local tool (source). After approval, the server side also emits a client-tool-execution interrupt (buildActionableInterrupts in activities/chat).

withInterruptBinding, hashSchemaInput and normalizeApprovalSchema are exported from @tanstack/ai/client. So a connection adapter could, in theory, rebuild that whole interrupt and resume protocol by hand. But no docs cover it, and it would mean copying chat() internals into each adapter. So in practice, needsApproval is unusable for client-only apps.

Suggested direction

Either of these would cover it:

  1. When a success RUN_FINISHED hands over a pending call to a client tool with needsApproval: true, have ChatClient raise a local tool-approval interrupt for it, instead of dropping it. On approve, run the .client() function and continue as it does today. On deny, add a denied tool result.
  2. Or export a documented helper so a connection adapter can emit a valid approval interrupt for a client tool, and document the resume flow the adapter must handle.

Option 1 needs no changes to adapters, and it keeps the Approve/Deny UI the same as with a server.

Your Minimal, Reproducible Example

Code sketch above; no hosted sandbox.

Lenguaje dominante
TypeScript
Estrellas
3.3k
Forks
378
Merge medio
1 d 2 h
PR fusionados (30 d)
219

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de TanStack/ai

Todos los issues de TanStack/ai

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.