125k

TanStack AI

TanStack AI メッセージを作成し、モデル、API ルート、ネットワーク リクエスト、または API キーを使用せずに useChat を通じて事前定義された会話をストリーミングします。

@shadcn/helpers/tanstack-ai を使うと、モデル、API ルート、ネットワーク リクエスト、API キーを 使わずに AI 会話をコードで定義できます。

ネイティブな TanStack UIMessage[] 値を作成し、ローカル接続アダプターを通して各アシスタント 応答を実際の AG-UI イベントとして再生します。そのため、コンポーネントは本番環境と同じように 動作します。テキストと推論は単語ごとにストリーミングされ、ツール呼び出しは入力から結果へ進みます。

import { createChat } from "@shadcn/helpers/tanstack-ai"
 
const chat = createChat()
  .user("What changed in this release?")
  .assistant("The release adds keyboard shortcuts and faster search.")
  .user("Can you show me the shortcuts?")
  .assistant("Press ⌘K to search and ⌘Enter to submit.")

初期メッセージとローカル接続を指定して、チャットを useChat に渡します。TanStack AI は、 サーバーから受信するものと同じ AG-UI イベントを受け取ります。

import { useChat } from "@tanstack/ai-react"
 
function Chat() {
  const { messages, append } = useChat({
    initialMessages: chat.get(0), // starts with no messages.
    connection: chat.transport(),
  })
 
  const nextMessage = chat.next(messages)
 
  return (
    <button
      disabled={!nextMessage}
      onClick={() => {
        if (nextMessage) {
          void append(nextMessage)
        }
      }}
    >
      Send next message
    </button>
  )
}

get(0) はメッセージなしで開始します。append(nextMessage) は次の定義済みユーザー メッセージを 追加し、接続がそのアシスタント応答を AG-UI イベントとしてストリーミングします。


用途

このヘルパーはチャット UI をモデルやバックエンドから切り離すため、フロントエンドだけで 開発できます。オフラインで即座に実行でき、毎回同じように動作します。

  • コンポーネントを構築します。 メッセージ バブル、ツール カード、推論パネルを、最初に モデルを接続することなく、現実的なストリーミング出力で作成できます。
  • プレビューとデモ。 稼働中のモデルに依存しない、再現可能なプレビュー、スクリーンショット、 ビデオを提供できます。
  • ドキュメントを作成します。 読み込むたびに同じように描画される会話で、ドキュメントの例を動かせます。
  • テスト ネットワーク呼び出しを行わずに、CI の決定論的ストリームに対してアサートします。 トークンの支出、または不安定なモデル出力を拒否します。

インストール

pnpm add @shadcn/helpers

使い方

会話を作成し、そのメッセージと接続を useChat に渡してから、next() で定義済みのユーザー メッセージを順番に送信します。

"use client"
 
import { createChat } from "@shadcn/helpers/tanstack-ai"
import { useChat } from "@tanstack/ai-react"
 
const chat = createChat()
  .user("What changed in this release?")
  .assistant("The release adds keyboard shortcuts and faster search.")
  .user("Can you show me the shortcuts?")
  .assistant("Press ⌘K to search and ⌘Enter to submit.")
 
const initialMessages = chat.get(0)
const connection = chat.transport()
 
export function Chat() {
  const { messages, append, status } = useChat({
    initialMessages,
    connection,
  })
  const nextMessage = chat.next(messages)
  const isBusy = status === "submitted" || status === "streaming"
 
  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>{/* Render the message */}</div>
      ))}
      <button
        disabled={!nextMessage || isBusy}
        onClick={() => {
          if (nextMessage && !isBusy) {
            void append(nextMessage)
          }
        }}
      >
        Send
      </button>
    </div>
  )
}
New Chat
How can I help you today?
Morning, shadcn!
What are we working on today? Press send to start a new conversation
Demo is read only. Press send to send messages.
"use client"

import { createChat } from "@shadcn/helpers/tanstack-ai"

ユーザーメッセージ

このヘルパーで追加するメッセージには user と assistant のロールが設定されます。user() でユーザー メッセージを追加できます。createChat({ messages }) で開始した場合、既存の TanStack メッセージは保持されます。

const chat = createChat().user("What changed in this release?")
 
const [message] = chat.get()
 
message.role // "user"
message.parts // [{ type: "text", content: "What changed in this release?" }]

第 2 引数では id、または metadata 経由の createdAt タイムスタンプも指定できます。

chat.user("What changed in this release?", {
  id: "user-release-question",
  metadata: {
    createdAt: "2026-01-01T10:00:00.000Z",
  },
})

ユーザー メッセージにはファイルも含められます。ファイルを参照してください。


アシスタントメッセージ

assistant() を使ってアシスタント メッセージを追加します。

const chat = createChat().assistant(
  "The release adds keyboard shortcuts and faster search."
)
 
const [message] = chat.get()
 
message.role // "assistant"
message.parts // [{ type: "text", content: "The release adds..." }]

文字列を渡すと 1 つのテキスト パーツが作成されます。TanStack メッセージ パーツの配列も渡せます。

chat.assistant([
  { type: "thinking", content: "I should summarize the release." },
  { type: "text", content: "The release adds keyboard shortcuts." },
])

メッセージをより小さなステップでストリーミングする必要がある場合は、ライター コールバックを使用します。

chat.assistant(({ writer }) => {
  writer.reasoning("I should summarize the release.")
  writer.text("The release adds keyboard shortcuts and faster search.")
})

ライターは、呼び出した順序でパーツを追加します。


メッセージパーツ

TanStack ライターは、AG-UI 接続を通して往復できるパーツ(テキスト、推論、ツール呼び出し)を サポートします。また、タイミングとエラーも扱えます。

chat.assistant(({ writer }) => {
  writer.reasoning("I should answer directly.")
  writer.text("Hello.")
})

この接続に相当する機能がないパーツについては、アダプターの違いを参照してください。


テキスト

text() を使ってテキストを追加します。

chat.assistant(({ writer }) => {
  writer.text("The release adds keyboard shortcuts.")
  writer.text(" Search is faster too.")
})

chat.transport() と一緒に使うと、テキストは単語ごとにストリーミングされます。連続する テキスト呼び出しは get() では別々のパーツになりますが、再生中は TanStack のストリーム プロセッサによって 1 つのテキスト パーツに結合されます。

値全体を一度に送信するには mode: "instant" を使い、テキスト差分の間隔を変更するには delayMs を使います。

writer.text("Done.", { mode: "instant" })
writer.text("This part streams more slowly.", { delayMs: 100 })

推理

reasoning() を使って推論を追加します。TanStack の thinking パーツになります。

chat.assistant(({ writer }) => {
  writer.reasoning("I should check the latest conditions first.")
  writer.text("Let me check the weather.")
})

推論では、テキストと同じ delayMs および mode オプションを使います。

writer.reasoning("Checking the forecast.", { mode: "instant" })

ツール呼び出し

tool() を使ってツール呼び出しを追加します。入力から出力までのライフサイクルを追跡する ハンドルが返されます。

chat.assistant(({ writer }) => {
  writer
    .tool("getWeather", {
      input: { city: "San Francisco" },
    })
    .sleep(900)
    .output({
      city: "San Francisco",
      temperature: 18,
      condition: "Breezy",
    })
 
  writer.text("It is 18°C and breezy in San Francisco.")
})

ツールは output() または error() で完了できます。

writer.tool("getWeather", { input: { city: "San Francisco" } }).error()

完了した TanStack メッセージには、tool-call パーツと、それに対応する tool-result パーツが 含まれます。

アプリで使っているクライアント ツールのタプルを渡すと、ツール名、入力、出力に型を付けられます。

import { clientTools } from "@tanstack/ai-client"
 
import { getWeatherTool } from "@/lib/tools"
 
const tools = clientTools(getWeatherTool.client())
const chat = createChat<typeof tools>()
 
chat.assistant(({ writer }) => {
  writer
    .tool("getWeather", {
      input: { city: "San Francisco" },
    })
    .output({
      city: "San Francisco",
      temperature: 18,
      condition: "Breezy",
    })
})

ファイル

files オプションを使ってユーザー メッセージにファイルを追加します。

chat.user("Describe this image.", {
  files: [
    {
      mediaType: "image/png",
      url: "https://example.com/screenshot.png",
    },
  ],
})

このヘルパーは各ファイルをメディア タイプに応じた TanStack メディア パーツに変換します。

メディア タイプTanStack パーツ
image/*image
audio/*audio
video/*video
その他document

これらのパーツは get() から取得できます。next() は完全なユーザー UIMessage を返すため、 append() はテキストとメディア パーツを保持できます。

const nextMessage = chat.next(messages)
 
if (nextMessage) {
  void append(nextMessage)
}

アシスタントのメディア パーツは静的なパーツ配列に含めて get() で読み取れますが、AG-UI 接続は アシスタントのファイルをストリーミングしません。


アダプターの違い

TanStack AI と AI SDK は異なるメッセージおよびストリーミング モデルを使用します。このアダプターは、最初から最後まで表現できる操作のみを公開します。

機能動作
テキストAG-UI テキストとしてストリーミングされ、text パーツになります。
推論AG-UI 推論としてストリーミングされ、thinking パーツになります。
ツール呼び出し入力、出力、エラーを tool-call と tool-result にストリーミングします。
データと構造化出力ライター メソッドはありません。既存のメッセージ パーツは get() から引き続き利用できます。
カスタムイベント公開アダプターにライター メソッドはありません。
ソース、推論ファイル、ステップ開始このアダプターには TanStack メッセージまたは同等の AG-UI はありません。
アシスタント ファイルget() では利用できますが、接続ストリームからは省略されます。
停止ストリームを閉じます。AG-UI には個別の中止イベントはありません。

プレビューで AI SDK の完全なメッセージ パーツ モデルが必要な場合は、AI SDK ヘルパー を使います。


メッセージを読む

get() を使うと、会話の先頭からメッセージを取得できます。

chat.get() // Every message.
chat.get(2) // The first two messages.
chat.get(0) // An empty initial conversation.

get() は複製された TanStack UIMessage[] 値を返し、チャットを変更しません。

next() を使うと、すでに表示したメッセージの次にある定義済みユーザー メッセージを取得できます。

const initialMessages = chat.get(2)
const nextMessage = chat.next(initialMessages)

next() は、インデックスではなく、常にメッセージ トランスクリプトを受け入れます。次の ユーザー メッセージが使用されるか、何も残っていない場合は null が使用されます。


既存のメッセージから開始する

保存した会話やフィクスチャから続行するには messages を渡します。

import { createChat } from "@shadcn/helpers/tanstack-ai"
import type { UIMessage } from "@tanstack/ai-client"
 
declare const savedMessages: UIMessage[]
 
const chat = createChat({ messages: savedMessages })
  .user("What should we do next?")
  .assistant("Turn the open questions into a checklist.")

既存の ID、タイムスタンプ、パーツは保持されます。


トランスポート

transport() は TanStack の ConnectConnectionAdapter を作成し、useChat の connection として 渡せるようにします。

const connection = chat.transport()
 
const { messages, append, sendMessage } = useChat({
  initialMessages: chat.get(0),
  connection,
})
 
const nextMessage = chat.next(messages)
 
// Replay the next predefined user message.
if (nextMessage) {
  void append(nextMessage)
}
 
// Or send ordinary user input as text or multimodal content.
void sendMessage("Tell me more.")

append() または sendMessage() で実行を開始すると、接続は現在の会話履歴に続くアシスタント メッセージを見つけ、AG-UI イベントを出力します。まずメッセージ ID で照合し、見つからない場合は 最新メッセージのロールとテキストで照合します。

append(nextMessage) を使うと、定義済みユーザー メッセージを再生できます。メッセージの ID と メディア パーツは保持されます。通常のユーザー入力には sendMessage() を使います。これは完全な UIMessage ではなく、文字列またはマルチモーダル コンテンツを受け取ります。

イベント

応答は、入力から結果まで次の順序でイベントを出力します。

RUN_STARTED
REASONING_MESSAGE_START → REASONING_MESSAGE_CONTENT → REASONING_MESSAGE_END
TOOL_CALL_START → TOOL_CALL_ARGS → TOOL_CALL_END → TOOL_CALL_RESULT
TEXT_MESSAGE_START → TEXT_MESSAGE_CONTENT → TEXT_MESSAGE_END
RUN_FINISHED

事前定義されたエラーは RUN_ERROR を出力します。 TanStack は、threadId および runId は各実行で使用されます。

オプション

オプション型デフォルト説明
delayMsnumber50テキストと推論の差分の間隔。0 または undefined で無効化します。
fallbackstring、MessagePart[]、またはコールバックなし事前定義されたアシスタント応答が残っていない場合のストリームへの応答。

接続レベルの delayMs は、すべてのストリーミングされるテキストおよび推論パーツに適用されます。 1 つのパーツだけ変更するには、writer.text(..., { delayMs }) または writer.reasoning(..., { delayMs }) を使います。

const connection = chat.transport({
  delayMs: 25,
})

フォールバック

定義済みのアシスタント応答が残っていない場合は fallback を使います。定義済みの応答を使い 切った後もデモを利用できるようになります。

const connection = chat.transport({
  fallback: "This demo has no more predefined replies.",
})

フォールバックには、TanStack メッセージ パーツの配列やライター コールバックも指定できます。 コールバックには受信した会話履歴が渡されるため、現在の状態から応答を作成できます。

const connection = chat.transport({
  fallback: ({ writer, messages }) => {
    writer.text(`This example already has ${messages.length} messages.`, {
      mode: "instant",
    })
  },
})

フォールバック応答はアシスタント応答と同様にストリーミングされますが、定義済みの会話には追加 されません。フォールバックがない状態で会話が終了すると、接続は "No assistant response found for this transcript." をスローします。

useChat から stop() を呼び出すと、アクティブな接続ストリームが閉じられます。


タイミング

遅延を使うと、実際の応答のテンポを再現できます。

const chat = createChat()
  .user("Give me a project update.")
  .sleep(800)
  .assistant(({ writer }) => {
    writer.reasoning("I should lead with the completed milestone.")
    writer.sleep(500)
    writer.text("The first milestone is complete.", { mode: "instant" })
  })
 
const connection = chat.transport({ delayMs: 50 })
  • chat.sleep(ms) は、次のアシスタント応答が始まる前に待機します。
  • writer.sleep(ms) は、アシスタント応答のパーツ間で待機します。
  • tool.sleep(ms) は、ツールの入力とその結果の間で待機します。
  • transport({ delayMs }) は、テキストと推論の差分間隔のデフォルト値を設定します。
  • writer.text(text, { delayMs }) と writer.reasoning(text, { delayMs }) は、1 つの パーツだけその間隔を上書きします。
  • mode: "instant" は、テキストまたは推論の値全体を 1 回の差分で送信します。

高速なテストでは、chat.transport({ delayMs: 0 }) と即時テキストを使用します。


エラー

アシスタント応答全体を失敗させるには、チャットで error() を使います。

const chat = createChat()
  .user("Load the report.")
  .error("The report could not be loaded.")

TanStack の RUN_ERROR イベントが出力されます。

他のコンテンツをストリーミングした後に失敗させるには、writer.error() を使います。

chat.assistant(({ writer }) => {
  writer.text("I found the report.")
  writer.error("The connection closed before it could be read.")
})

メタデータと ID

TanStack メッセージは createdAt タイムスタンプをサポートします。メッセージのメタデータとして 渡すと、get() と next() を通して保持されます。AG-UI 接続はメッセージのメタデータを ストリーミングしません。

const chat = createChat()
  .user("Hello", {
    metadata: { createdAt: "2026-01-01T10:00:00.000Z" },
  })
  .assistant("Hi.", {
    id: "assistant-welcome",
    metadata: { createdAt: new Date("2026-01-01T10:00:01.000Z") },
  })

フィクスチャにカスタム プレフィックスまたは固定クロックが必要な場合は、チャット オプションを使用します。

const chat = createChat({
  messageIdPrefix: "demo-message",
  toolCallIdPrefix: "demo-tool",
  now: "2026-01-01T00:00:00.000Z",
})

API リファレンス

上記のセクションでは一般的なフローを説明しました。このリファレンスには、すべての公開されている エクスポートとオプションを一覧にしています。

createChat()

型付きの会話を作成し、メソッドをチェーンできる TanStack チャット インターフェースを返します。

function createChat<
  TOOLS extends ReadonlyArray<AnyClientTool> = AnyClientTool[],
  DATA = unknown,
>(options?: CreateChatOptions<TOOLS, DATA>): TanStackChat<TOOLS, DATA>

型パラメーター

パラメーター説明
TOOLSツール名、入力、出力の型付けに使うクライアント ツールのタプル。
DATA既存の TanStack メッセージにある structured-output パーツのペイロード型。

ヘルパーのライターは structured-output パーツを作成しません。既存のメッセージからチャットを 開始する場合、DATA によってその型が保持されます。

オプション

オプション型デフォルト説明
messagesUIMessage<TOOLS, DATA>[][]既存の会話履歴から開始します。メッセージは複製されます。
messageIdPrefixstring"msg"生成されたメッセージ ID のプレフィックス。
toolCallIdPrefixstring"call"生成するツール呼び出し ID のプレフィックス。
nowDate | string"2026-01-01T00:00:00.000Z"生成する createdAt 値に使う固定時刻。

messages 内にある ID は予約されるため、新しく生成する ID は既存の会話履歴の続きから始まります。

TanStackChat

コンテンツを追加するすべてのメソッドは同じチャットを返すため、呼び出しをチェーンできます。

メソッド戻り値説明
user(text?, options?)TanStackChatテキストと任意のファイルを含むユーザー メッセージを追加します。
assistant(input?, options?)TanStackChatテキスト、パーツ、またはライター コールバックからアシスタント メッセージを追加します。
sleep(delayMs)TanStackChat次のアシスタント応答が始まる前に待機します。
error(errorText?)TanStackChatRUN_ERROR を発行するアシスタント応答を追加します。
get(count?)UIMessage[]会話の開始時から複製されたメッセージを返します。
next(messages)UIMessage | null会話履歴の次にある定義済みユーザー メッセージを返します。
transport(options?)ConnectConnectionAdapteruseChat が使う接続を作成します。

user() または assistant() を内容なしで呼ぶと "Summarize the uploaded receipt." が使われます。 メッセージなしで error() を呼ぶと "An error occurred." が使われます。count が負数または 整数でない場合、get(count) は例外を発生させます。

user() オプション

オプション型説明
idstring特定のメッセージ ID を使用します。
metadataTanStackMessageMetadataメッセージの createdAt タイムスタンプを設定します。
filesFilePayload[]ユーザーのテキスト パーツの後にメディア パーツを追加します。

ユーザー ファイルの形式は次のとおりです。

type FilePayload = {
  type?: "file"
  mediaType: string
  url: string
  filename?: string
  providerMetadata?: Record<string, unknown>
}

TanStack のメディア パーツでは URL とメディア タイプが保持されます。メッセージ形式では filename と providerMetadata は保持されません。

assistant() の入力

入力結果
string単語ごとにストリーミングされる 1 つのテキスト パーツ。
MessagePart[]既存の順序を保った静的な TanStack パーツ。
({ writer }) => voidパーツ、ツール、エラー、タイミングを定義する同期コールバック。

assistant() オプション

オプションタイプ説明
idstring特定のメッセージ ID を使用します。
metadataTanStackMessageMetadataメッセージの createdAt タイムスタンプを設定します。

transport() オプション

オプション型デフォルト説明
delayMsnumber50テキストと推論の差分の間隔。0 または undefined で無効化します。
fallbackstring | MessagePart[] | ({ writer, messages }) => voidなし定義済みのアシスタント応答が残っていない場合に使う応答。

照合、フォールバック、エラー、AG-UI の動作についてはトランスポートを参照してください。

ライター

ライターは assistant(({ writer }) => {}) とフォールバック コールバック内で使用できます。

メソッド説明
text(text?, options?)テキストをストリーミングします。
reasoning(text?, options?)thinking パーツになる推論をストリーミングします。
tool(name, options?)型付きツール呼び出しを追加し、そのライフサイクル ハンドルを返します。
sleep(delayMs)次のライター イベントまで待機します。
error(errorText?)RUN_ERROR を発行し、RUN_FINISHED イベントなしで終了します。

内容なしで text() を呼ぶと "Summarize the uploaded receipt." が使われます。内容なしで reasoning() を呼ぶと "I need to inspect the available context before answering." が使われます。 error() は "An error occurred." を使用します。

テキストと推論のオプション

オプションタイプデフォルト説明
delayMsnumber接続この値の接続遅延を上書きします。
mode"stream" | "instant""stream"単語単位で差分をストリーミングするか、値全体を 1 回の差分で送信します。

ツール オプション

オプション型説明
toolCallIdstring特定のツール呼び出し ID を使用します。
inputTOOLS[NAME]["input"]型付きツール入力を設定します。
outputTOOLS[NAME]["output"]型付きの出力で直ちに完了します。
errorTextstringエラーですぐに終了します。

tool() は sleep(delayMs)、output(value)、error(errorText?) を持つハンドルを返します。 これらを使ってツールのライフサイクル イベントを出力できます。メッセージなしで tool.error() を呼ぶと "Tool call failed." が使われます。

型

エクスポート説明
TanStackChatcreateChat() が返す、メソッドをチェーンできる型付きチャット。
CreateChatOptionscreateChat() が受け取るオプション。
TanStackMessageMetadataサポートされる { createdAt? } メッセージ メタデータ。
TanStackToolHandlewriter.tool() が返すライフサイクル ハンドル。
TanStackWriterアシスタントおよびフォールバック コールバック内で使えるライター。