Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .changeset/ai-octane-port.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
'@tanstack/ai-octane': minor
---

Add `@tanstack/ai-octane` — [Octane](https://github.com/octanejs/octane) bindings for TanStack AI.

This is a port of `@octanejs/tanstack-ai@0.0.11`, which lived in the
octanejs/octane repo as a temporary stopgap. The code moves here essentially
unchanged apart from the rename; the runtime surface is the same.

The package covers the `@tanstack/ai-react` hook surface — `useChat`,
`useRealtimeChat`, `useMcpAppBridge`, `useGeneration`, `useGenerateImage` /
`Audio` / `Speech` / `Video`, `useTranscription`, `useSummarize`,
`useAudioRecorder` — plus the 30 `@tanstack/ai-client` convenience re-exports,
reusing `@tanstack/ai` and `@tanstack/ai-client` unchanged. SSR through
`octane/server` is supported and tested.

Three defects found while reviewing the port were fixed rather than mirrored, and
are covered by tests (each verified to fail if the fix is reverted). Issues are
filed upstream so the React adapter can catch up:

- `useAudioRecorder`'s transforming overload now requires `onComplete`.
Previously, passing any unrelated option (`useAudioRecorder({ onError })`)
matched it, inferred `TOnComplete` as `unknown`, and silently collapsed
`recording`/`stop()` to `unknown`.
- `useGeneration` spreads caller `devtools` metadata before the hardcoded
`framework`/`hookName`, so a caller can no longer misattribute the binding in
the devtools. The sibling hooks already ordered it this way.
- `UseGenerationReturn` is now `<TInput, TOutput>` and types `generate` as
`(input: TInput)` instead of widening to `(input: Record<string, any>)`, so
required and narrow input fields are checked at the call site. This is the one
place the public _type_ surface differs in shape from `@tanstack/ai-react`;
the runtime surface is unchanged.

Two other things to know:

- Like Svelte packages shipping `.svelte`, this one publishes **uncompiled
source**. The hook modules are `.tsrx` and are compiled by the consumer's
Octane plugin, so there is no `dist` and `octane` is a required peer. The
`.tsrx.d.ts` companions are checked declaration emits, so the full generic
surface is preserved for TypeScript consumers.
- `useChat` matches the current ChatClient shape: `threadId` identity, queue,
`runId`, interrupts, `attach`/`detach`, and `SendMessageOptions`. The
`./mcp-apps` subpath is not ported (it renders a React-only component). See
`packages/ai-octane/status.json` for the full scope and divergence list.
294 changes: 294 additions & 0 deletions docs/api/ai-octane.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
---
title: "@tanstack/ai-octane"
id: ai-octane
order: 8
description: "API reference for @tanstack/ai-octane. Octane hooks including useChat for streaming chat with full type safety."
keywords:
- tanstack ai
- "@tanstack/ai-octane"
- octane
- useChat
- octane hooks
- api reference
---

Install `@tanstack/ai-octane`, then call `useChat` the same way you would in React. The hook modules are `.tsrx` and compile in your Octane plugin.

## Installation

```bash
npm install @tanstack/ai-octane octane
```

`octane` is a required peer. This package publishes uncompiled source, like Svelte packages that ship `.svelte`.

## `useChat(options)`

Manages chat state in an Octane component.

```tsx
import { useState } from 'octane'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-octane'
import {
createChatClientOptions,
type InferChatMessages,
} from '@tanstack/ai-client'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'

const updateUIDef = toolDefinition({
name: 'updateUI',
description: 'Show a notification in the UI',
inputSchema: z.object({ message: z.string() }),
})

export function ChatComponent() {
const [notification, setNotification] = useState<string | null>(null)
const updateUI = updateUIDef.client((input) => {
setNotification(input.message)
return { success: true }
})
const tools = [updateUI]

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

type ChatMessages = InferChatMessages<typeof chatOptions>

const { messages, sendMessage, isLoading, error, addToolApprovalResponse } =
useChat(chatOptions)

return (
<div>
{notification}
{isLoading ? 'Loading' : null}
{error ? error.message : null}
<button onClick={() => void sendMessage('hi')} type="button">
Send
</button>
<button
onClick={() =>
void addToolApprovalResponse({ id: 'approval-1', approved: true })
}
type="button"
>
Approve
</button>
{messages.length}
</div>
)
}
```

The matching server route still runs `chat({ adapter, messages })` and returns SSE. See [Quick Start: Octane](../getting-started/quick-start-octane).

### Options you pass first

Extends `ChatClientOptions` from `@tanstack/ai-client`. Pass `connection` or `fetcher`, not both.

- `connection` or `fetcher` - how the hook talks to your server
- `tools?` - client tool implementations from `.client()`
- `threadId?` - the only identity for this chat. Required when persistence is on
- `initialMessages?` - starting transcript
- `forwardedProps?` - JSON sent to the server on the AG-UI `forwardedProps` field

### Options you add later

- `live?` - subscribe on mount, unsubscribe on unmount
- `queue?` - what to do when `sendMessage` runs while a turn is in flight. Default queues
- `interrupts?` - typed interrupt definitions
- `context?` - client-only runtime context for client tools. Not sent to the server
- `onResponse?` / `onChunk?` / `onFinish?` / `onError?` / `onInterruptStateChange?`
- `devtools?` - display options. The hook always tags `framework: 'octane'`
- `body?` - deprecated. Use `forwardedProps`

Client tools run automatically. There is no `onToolCall` callback.

Changing `connection` or `fetcher` updates the live `ChatClient`. Changing `threadId` creates a new client.

### Returns

```typescript
import type { UIMessage } from '@tanstack/ai-octane'
import type { ModelMessage } from '@tanstack/ai/client'
import type {
MultimodalContent,
ChatClientState,
ConnectionStatus,
QueuedMessage,
SendMessageOptions,
} from '@tanstack/ai-client'

interface UseChatReturn {
messages: Array<UIMessage>
sendMessage: (
content: string | MultimodalContent,
options?: SendMessageOptions,
) => Promise<void>
append: (message: ModelMessage | UIMessage) => Promise<void>
addToolResult: (result: {
toolCallId: string
tool: string
output: unknown
state?: 'output-available' | 'output-error'
errorText?: string
}) => Promise<void>
addToolApprovalResponse: (response: {
id: string
approved: boolean
}) => Promise<void>
reload: () => Promise<void>
stop: () => void
isLoading: boolean
error: Error | undefined
status: ChatClientState
isSubscribed: boolean
connectionStatus: ConnectionStatus
sessionGenerating: boolean
setMessages: (messages: Array<UIMessage>) => void
clear: () => void
queue: Array<QueuedMessage>
cancelQueued: (id: string) => void
runId: string | null
}
```

`queue` holds sends that wait while a run is busy. `runId` is the in-flight turn, or `null`. Interrupt helpers (`interrupts`, `resolveInterrupts`, `cancelInterrupts`, `retryInterrupts`) are on the same object when you pass `interrupts`.

## Connection adapters

Re-exported from `@tanstack/ai-client`:

```typescript
import {
fetchServerSentEvents,
fetchHttpStream,
stream,
type ConnectionAdapter,
} from '@tanstack/ai-octane'
```

## Example: basic chat

```tsx
import { useState } from 'octane'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-octane'

export function Chat() {
const [input, setInput] = useState('')
const { messages, sendMessage, isLoading } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})

return (
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role}:</strong>
{message.parts
.filter((part) => part.type === 'text')
.map((part) => part.content)
.join('')}
</div>
))}
<input
value={input}
disabled={isLoading}
onInput={(event) => setInput(event.currentTarget.value)}
/>
<button
disabled={isLoading}
onClick={() => {
void sendMessage(input)
setInput('')
}}
type="button"
>
Send
</button>
</div>
)
}
```

## Example: tool approval

```tsx
import { useChat, fetchServerSentEvents } from '@tanstack/ai-octane'

export function ChatWithApproval() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
connection: fetchServerSentEvents('/api/chat'),
})

return (
<div>
<button onClick={() => void sendMessage('run the tool')} type="button">
Send
</button>
{messages.map((message) =>
message.parts.map((part) => {
if (
part.type !== 'tool-call' ||
part.state !== 'approval-requested' ||
!part.approval
) {
return null
}
const approvalId = part.approval.id
return (
<div key={part.id}>
<p>Approve: {part.name}</p>
<button
onClick={() =>
void addToolApprovalResponse({
id: approvalId,
approved: true,
})
}
type="button"
>
Approve
</button>
<button
onClick={() =>
void addToolApprovalResponse({
id: approvalId,
approved: false,
})
}
type="button"
>
Deny
</button>
</div>
)
}),
)}
</div>
)
}
```

## Other hooks

The package also exports `useRealtimeChat`, `useMcpAppBridge`, `useGeneration`, `useGenerateImage`, `useGenerateAudio`, `useGenerateSpeech`, `useGenerateVideo`, `useTranscription`, `useSummarize`, and `useAudioRecorder`.

The `./mcp-apps` React `AppRenderer` subpath is not in this package. `useMcpAppBridge` is.

## Types

Re-exported from `@tanstack/ai-client`:

- `UIMessage<TTools>`
- `ChatClientOptions<TTools, TContext>`
- `InferChatMessages<T>`
- `QueuedMessage`, `SendMessageOptions`, `WhenBusy`

## Next

- [Quick Start: Octane](../getting-started/quick-start-octane)
- [Tools](../tools/tools)
- [Client tools](../tools/client-tools)
15 changes: 13 additions & 2 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
"label": "Overview",
"to": "getting-started/overview",
"addedAt": "2026-04-15",
"updatedAt": "2026-08-19"
"updatedAt": "2026-08-21"
},
{
"label": "Quick Start: React",
"to": "getting-started/quick-start",
"addedAt": "2026-04-15"
"addedAt": "2026-04-15",
"updatedAt": "2026-08-21"
},
{
"label": "Quick Start: React Native",
Expand Down Expand Up @@ -46,6 +47,11 @@
"to": "getting-started/quick-start-angular",
"addedAt": "2026-06-15"
},
{
"label": "Quick Start: Octane",
"to": "getting-started/quick-start-octane",
"addedAt": "2026-08-21"
},
{
"label": "Quick Start: Server Only",
"to": "getting-started/quick-start-server",
Expand Down Expand Up @@ -872,6 +878,11 @@
"to": "api/ai-angular",
"addedAt": "2026-06-15",
"updatedAt": "2026-08-20"
},
{
"label": "@tanstack/ai-octane",
"to": "api/ai-octane",
"addedAt": "2026-08-21"
}
]
},
Expand Down
6 changes: 6 additions & 0 deletions docs/getting-started/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ Solid hooks for TanStack AI:
- Tool approval flow support
- Type-safe message handling with `InferChatMessages`

### `@tanstack/ai-octane`
Octane hooks for TanStack AI:
- `useChat` hook for chat interfaces
- Same hook names as `@tanstack/ai-react`
- Compiled by the Octane plugin from `.tsrx` source

## Adapters

With the help of adapters, TanStack AI can connect to various LLM providers. Available adapters include:
Expand Down
Loading
Loading