Skip to content

Latest commit

 

History

History
431 lines (323 loc) · 10.9 KB

File metadata and controls

431 lines (323 loc) · 10.9 KB
title @tanstack/ai-client
slug /api/ai-client
order 2
description API reference for @tanstack/ai-client — the framework-agnostic headless client for managing chat state and streaming transports.
keywords
tanstack ai
@tanstack/ai-client
headless client
ChatClient
chat state
connection adapters
api reference

Framework-agnostic headless client for managing chat state and streaming.

Installation

npm install @tanstack/ai-client

ChatClient

The main client class for managing chat state.

import { ChatClient, fetchServerSentEvents } from "@tanstack/ai-client";

const client = new ChatClient({
  connection: fetchServerSentEvents("/api/chat"),
  initialMessages: [],
  onMessagesChange: (messages) => {
    console.log("Messages updated:", messages);
  },
  onToolCall: async ({ toolName, input }) => {
    // Handle client tool execution
    return { result: "..." };
  },
});

Constructor Options

  • connection - Connection adapter for streaming
  • initialMessages? - Initial messages array
  • id? - Unique identifier for this chat instance
  • threadId? - Thread ID for AG-UI run correlation. Persists across sends; auto-generated if omitted
  • forwardedProps? - Arbitrary client-controlled JSON forwarded to the server in the AG-UI RunAgentInput.forwardedProps field
  • body? - Deprecated. Use forwardedProps instead. Still works — values are merged into forwardedProps on the wire and mirrored under the legacy data field for backward compatibility
  • onResponse? - Callback when response is received
  • onChunk? - Callback when stream chunk is received
  • onFinish? - Callback when response finishes
  • onError? - Callback when error occurs
  • onMessagesChange? - Callback when messages change
  • onLoadingChange? - Callback when loading state changes
  • onErrorChange? - Callback when error state changes
  • onToolCall? - Callback for client-side tool execution
  • streamProcessor? - Stream processing configuration

Methods

sendMessage(content: string)

Sends a user message and gets a response.

await client.sendMessage("Hello!");

append(message: ModelMessage | UIMessage)

Appends a message to the conversation.

await client.append({
  role: "user",
  content: "Additional context",
});

reload()

Reloads the last assistant message.

await client.reload();

stop()

Stops the current response generation.

client.stop();

clear()

Clears all messages.

client.clear();

setMessagesManually(messages: UIMessage[])

Manually sets the messages array.

client.setMessagesManually([...newMessages]);

addToolResult(result)

Adds the result of a client-side tool execution.

await client.addToolResult({
  toolCallId: "call_123",
  tool: "toolName",
  output: { result: "..." },
  state: "output-available",
});

addToolApprovalResponse(response)

Responds to a tool approval request.

await client.addToolApprovalResponse({
  id: "approval_123",
  approved: true,
});

Properties

  • messages: UIMessage[] - Current messages
  • isLoading: boolean - Whether a response is being generated
  • error: Error | undefined - Current error, if any

Connection Adapters

For a complete transport walkthrough, see Connection Adapters. For React Native and Expo, see Quick Start: React Native.

fetchServerSentEvents(url, options?)

Creates an SSE connection adapter.

import { fetchServerSentEvents } from "@tanstack/ai-client";

const adapter = fetchServerSentEvents("/api/chat", {
  headers: {
    Authorization: "Bearer token",
  },
});

fetchHttpStream(url, options?)

Creates a newline-delimited JSON HTTP stream connection adapter. Pair it with toHttpResponse() on the server.

import { fetchHttpStream } from "@tanstack/ai-client";

const adapter = fetchHttpStream("/api/chat");

fetchHttpStream() requires a runtime with streaming fetch, Response.body.getReader(), and TextDecoder. If the runtime cannot expose an incremental response body, it throws UnsupportedResponseStreamError; use the XHR adapters in React Native or Expo.

xhrHttpStream(url, options?)

Creates an XMLHttpRequest-backed newline-delimited JSON stream adapter. This is the recommended default for React Native and Expo chat screens. Pair it with toHttpResponse() on the server.

import { xhrHttpStream } from "@tanstack/ai-client";

const adapter = xhrHttpStream("http://192.168.1.10:8787/chat/http", {
  headers: { Authorization: "Bearer token" },
  withCredentials: true,
});

xhrServerSentEvents(url, options?)

Creates an XMLHttpRequest-backed SSE adapter for runtimes where XHR progress events are more reliable than streaming fetch. Pair it with toServerSentEventsResponse() on the server.

import { xhrServerSentEvents } from "@tanstack/ai-client";

const adapter = xhrServerSentEvents("http://192.168.1.10:8787/chat/sse");

Adapter options

Fetch adapters accept:

  • headers?: Record<string, string> | Headers
  • credentials?: RequestCredentials
  • signal?: AbortSignal
  • body?: Record<string, any>
  • fetchClient?: typeof globalThis.fetch

XHR adapters accept:

  • headers?: Record<string, string> | Headers
  • withCredentials?: boolean
  • signal?: AbortSignal
  • body?: Record<string, any>
  • xhrFactory?: () => XMLHttpRequest

body is merged into the AG-UI forwardedProps payload. Values from forwardedProps on the client and per-message sendMessage(..., data) calls override static adapter body values.

Stream errors

  • UnsupportedResponseStreamError - thrown by fetch-based adapters when Response.body, Response.body.getReader(), or TextDecoder is missing.
  • StreamTruncatedError - thrown when an SSE or NDJSON stream ends with unterminated trailing data, usually because the server, proxy, or network cut the connection mid-line.

stream(connectFn)

Creates a custom connection adapter.

import { stream } from "@tanstack/ai-client";

const adapter = stream(async (messages, data, signal) => {
  // `data` here carries the merged forwardedProps. The fetch-based
  // adapters serialize it as the AG-UI `RunAgentInput.forwardedProps`
  // field on the wire (with a backward-compat `data` mirror).
  const response = await fetch("/api/chat", {
    method: "POST",
    body: JSON.stringify({ messages, forwardedProps: data }),
    signal,
  });
  return processStream(response);
});

Helper Functions

clientTools(...tools)

Creates a typed array of client tools with proper type inference. This eliminates the need for as const when defining tool arrays and enables proper discriminated union type narrowing.

import { clientTools } from "@tanstack/ai-client";
import { myTool1, myTool2 } from "./tools";

// Create client implementations
const tool1Client = myTool1.client((input) => {
  // Implementation
  return { result: "..." };
});

const tool2Client = myTool2.client((input) => {
  // Implementation
  return { result: "..." };
});

// Create typed tools array (no 'as const' needed!)
const tools = clientTools(tool1Client, tool2Client);

// Now when you use these tools in chat options:
const chatOptions = createChatClientOptions({
  connection: fetchServerSentEvents("/api/chat"),
  tools, // Fully typed with literal tool names
});

// In your component:
messages.forEach((message) => {
  message.parts.forEach((part) => {
    if (part.type === "tool-call" && part.name === "myTool1") {
      // ✅ TypeScript knows part.name is literally "myTool1"
      // ✅ part.input is typed from myTool1's input schema
      // ✅ part.output is typed from myTool1's output schema
    }
  });
});

createChatClientOptions(options)

Helper function to create typed chat client options with proper type inference.

import { createChatClientOptions, clientTools } from "@tanstack/ai-client";

const tools = clientTools(tool1, tool2);

const chatOptions = createChatClientOptions({
  connection: fetchServerSentEvents("/api/chat"),
  tools,
});

// Use InferChatMessages to extract message types
type ChatMessages = InferChatMessages<typeof chatOptions>;

Types

UIMessage

interface UIMessage {
  id: string;
  role: "user" | "assistant";
  parts: MessagePart[];
  createdAt?: Date;
}

MessagePart

type MessagePart = TextPart | ThinkingPart | ToolCallPart | ToolResultPart;

TextPart

interface TextPart {
  type: "text";
  content: string;
}

ThinkingPart

interface ThinkingPart {
  type: "thinking";
  content: string;
}

Thinking parts represent the model's internal reasoning process. They are typically displayed in a collapsible format and automatically collapse when the response text appears. Thinking parts are UI-only and are not sent back to the model in subsequent requests.

Note: Thinking parts are only available when using models that support reasoning/thinking (e.g., Anthropic Claude with thinking enabled, OpenAI GPT-5 with reasoning enabled).

ToolCallPart

interface ToolCallPart {
  type: "tool-call";
  id: string;
  name: string;
  arguments: string; // JSON string (may be incomplete during streaming)
  input?: any; // Parsed tool input (typed from tool's inputSchema)
  state: ToolCallState;
  approval?: ApprovalRequest;
  output?: any; // Tool execution output (typed from tool's outputSchema)
}

When using typed tools with clientTools() and createChatClientOptions(), the input and output fields are automatically typed based on your tool's Zod schemas, and name becomes a discriminated union enabling type narrowing.

ToolResultPart

interface ToolResultPart {
  type: "tool-result";
  id: string;
  toolCallId: string;
  tool: string;
  output: any;
  state: ToolResultState;
  errorText?: string;
}

ToolCallState

type ToolCallState =
  | "pending"
  | "approval-requested"
  | "executing"
  | "output-available"
  | "output-error"
  | "cancelled";

ToolResultState

type ToolResultState =
  | "pending"
  | "executing"
  | "output-available"
  | "output-error";

Stream Processing

Configure stream processing with chunk strategies:

import { ImmediateStrategy, fetchServerSentEvents } from "@tanstack/ai-client";

const client = new ChatClient({
  connection: fetchServerSentEvents("/api/chat"),
  streamProcessor: {
    chunkStrategy: new ImmediateStrategy(), // Emit every chunk
  },
});

Next Steps