Skip to content
API

@tanstack/ai-angular

Angular injectables for TanStack AI, built on signals. To build your first chat, start with the Quick Start. For a typed chat layout, see Angular Chat UI.

Installation

shell
npm i @tanstack/ai-angular

Warning

Call every inject* function in an Angular injection context: a class field initializer, the constructor, or runInInjectionContext. Outside one, the call throws.

ts
import { Injector, inject, runInInjectionContext } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";

export class MyComponent {
  // Field initializer
  chat = injectChat({ connection: fetchServerSentEvents("/api/chat"), threadId: "support-chat" });

  private injector = inject(Injector);

  // Later, outside the constructor
  openSecondChat() {
    return runInInjectionContext(this.injector, () =>
      injectChat({ connection: fetchServerSentEvents("/api/chat"), threadId: "support-chat" }),
    );
  }
}

injectChat(options)

Holds the messages and the run state of one chat, and talks to your server route.

ts
import { Component } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";

@Component({
  selector: "app-chat",
  standalone: true,
  template: `
    @for (message of chat.messages(); track message.id) {
      <p>
        <strong>{{ message.role }}:</strong>
        @for (part of message.parts; track $index) {
          @if (part.type === 'text') {
            <span>{{ part.content }}</span>
          }
        }
      </p>
    }
    <input #input />
    <button
      (click)="chat.sendMessage(input.value); input.value = ''"
      [disabled]="chat.isLoading()"
    >
      Send
    </button>
  `,
})
export class ChatComponent {
  chat = injectChat({
    connection: fetchServerSentEvents("/api/chat"),
    threadId: "support-chat",
  });
}

Options

You must pass threadId. You must also pass connection or fetcher, not both.

OptionWhat it does
connectionConnection adapter, for example fetchServerSentEvents("/api/chat"). See Transports.
fetcherA function that returns a Response or a stream. Use it with server functions.
toolsClient tools from .client(). They run when the model calls them. Reactive. See Client Tools.
threadIdRequired. The id of this chat. Use a stable id for each conversation, for example the id from your route.
initialMessagesThe messages to show first.
forwardedPropsJSON sent to the server in the AG-UI forwardedProps field, for example { model: "gpt-6.1-sol" }. Reactive.
persistencetrue to load the thread from the server, or a storage adapter. See Chat Persistence.
outputSchemaA Standard Schema. Adds typed partial and final signals to the return value. See Streaming structured output.
contextClient-only runtime context for client tools. It is not sent to the server. Reactive.
liveSubscribe while live is true. Reactive.
queueWhat sendMessage does while a run is busy. Default: queue the message. See Queueing.
interruptsDefinitions from defineInterrupt(). See Interrupts.
byok, byokProviderA keyring from defineByok(). See Bring Your Own Key.
devtoolsDisplay options for TanStack AI Devtools.

The injectable also takes the ChatClient callbacks (onResponse, onChunk, onFinish, onError, onInterruptStateChange, onCustomEvent) and the other client options. See ChatClient options.

A reactive option is a ReactiveOption<T>: a plain value, a Signal<T>, or a getter () => T. A getter runs in computed, so the signals it reads are tracked. A change updates the current chat.

ts
import { Component, signal } from "@angular/core";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";

@Component({ selector: "app-model-chat", standalone: true, template: "" })
export class ModelChatComponent {
  model = signal("gpt-6.1-sol");

  chat = injectChat({
    connection: fetchServerSentEvents("/api/chat"),
    threadId: "support-chat",
    forwardedProps: () => ({ model: this.model() }),
  });
}

Returns

State values are read-only Angular Signals. Call them to read the value, for example chat.messages(). Cleanup runs in DestroyRef.onDestroy.

FieldWhat it is
messagesThe UIMessage array of the chat.
sendMessage(content, options?)Sends a user message. content is a string or { content, id?, metadata? }. options takes whenBusy and body.
isLoadingtrue while a response streams.
errorThe last error, or undefined.
status"ready", "submitted", "streaming", or "error".
stop()Stops the current response.
reload()Runs the last user message again.
append(message)Adds a UIMessage or ModelMessage and starts a run.
addToolResult(result)Gives the result of a client tool by hand: { toolCallId, tool, output, state?, errorText? }.
setMessages(messages), clear()Replace or clear the messages.
queue, cancelQueued(id)Messages that wait while a run is busy. See Queueing.
runIdThe id of the run in flight, or null.
interruptsPending interrupts. Each item has resolveInterrupt(...). See Interrupts.
resolveInterrupts, cancelInterrupts, retryInterrupts, resumeInterruptsUnsafeAct on all pending interrupts at once.
interruptErrors, resumingErrors from the last resume, and true while a resume runs.
isHydratingtrue while the chat loads from persistence.
hasOlderMessages, loadOlderMessages()Page in older messages. See Chat Persistence.
isSubscribed, connectionStatus, sessionGeneratingState of the live subscription.
partial, finalStructured output. Only when you pass outputSchema.

Example: tool approval

A tool with needsApproval: true pauses the run. The pending call shows up in interrupts.

ts
import { Component } from "@angular/core";
import { toolDefinition } from "@tanstack/ai";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
import { z } from "zod";

const deleteFileTool = toolDefinition({
  name: "delete_file",
  description: "Delete a file",
  needsApproval: true,
  inputSchema: z.object({ path: z.string() }),
});

@Component({
  selector: "app-approval-chat",
  standalone: true,
  template: `
    @for (interrupt of chat.interrupts(); track interrupt.id) {
      @if (interrupt.kind === 'tool-approval') {
        <div>
          <p>Approve: {{ interrupt.toolName }}</p>
          <button (click)="interrupt.resolveInterrupt(true)">Approve</button>
          <button (click)="interrupt.resolveInterrupt(false)">Deny</button>
        </div>
      }
    }
  `,
})
export class ApprovalChatComponent {
  chat = injectChat({
    connection: fetchServerSentEvents("/api/chat"),
    threadId: "support-chat",
    tools: [deleteFileTool],
  });
}

See Tool Approval for the server side.

createChatHook(options)

This package does not export createChatHook from its main entry. @tanstack/ai-angular/ui has a createChatHook that binds chat options and typed UI components. See Angular Chat UI.

Generation hooks

Each injectable runs one generation request and keeps its result. See Generation Hooks.

Injectablegenerate() inputresult
injectGenerateImageImageGenerateInputImageGenerationResult
injectGenerateAudioAudioGenerateInputAudioGenerationResult
injectGenerateSpeechSpeechGenerateInputTTSResult
injectTranscriptionTranscriptionGenerateInputTranscriptionResult
injectSummarizeSummarizeGenerateInputSummarizationResult
injectGenerateVideoVideoGenerateInputVideoGenerateResult
injectGenerationYour own input typeYour own result type
ts
import { Component } from "@angular/core";
import { injectGenerateImage, fetchServerSentEvents } from "@tanstack/ai-angular";

@Component({
  selector: "app-image",
  standalone: true,
  template: `
    <button
      (click)="image.generate({ prompt: 'A mountain at sunset' })"
      [disabled]="image.isLoading()"
    >
      Generate
    </button>
  `,
})
export class ImageComponent {
  image = injectGenerateImage({
    connection: fetchServerSentEvents("/api/generate/image"),
    threadId: "image-studio",
  });
}

Options: threadId (required), connection or fetcher, body (reactive), persistence, byok, byokProvider, devtools, onResult, onError, onProgress, onChunk.

Returns: generate(input), result, isLoading, error, status, stop(), reset(), runId. The state fields are read-only signals. Cleanup runs in DestroyRef.onDestroy.

injectGenerateVideo also returns jobId and videoStatus signals, and takes onJobCreated and onStatusUpdate.

Realtime chat

This package does not export a realtime injectable. Use RealtimeClient, which this package re-exports. See Realtime Chat.

injectAudioRecorder(options?)

Records audio from the microphone. See Audio Recording.

Returns recording and isRecording (signals), isSupported, start(), stop(), and cancel(). Pass onComplete to transform the recording. Then recording and stop() use the transformed type.

injectByok(client)

Subscribes to a BYOK keyring and returns a read-only Signal of its snapshot. See Bring Your Own Key.

ts
import { Component, computed } from "@angular/core";
import { injectByok } from "@tanstack/ai-angular";
import { byok } from "./byok";

@Component({
  selector: "app-key-status",
  standalone: true,
  template: `<p>{{ last4() }}</p>`,
})
export class KeyStatusComponent {
  snapshot = injectByok(byok);

  last4 = computed(() => {
    const openai = this.snapshot().status.openai;
    return openai && "masked" in openai ? openai.masked : "No key";
  });
}

The snapshot has status, locked, prompt, and storageError. Save a key from your own UI with byok.update(provider, value).

MCP App bridge

This package does not export a bridge injectable. Use createMcpAppBridge, which this package re-exports. See MCP Apps.

injectRegisterWebMCPTools(tools, options?)

Exposes client tools to the browser WebMCP API for the current injection owner. Angular removes them when it destroys that owner. See WebMCP Tools.

ts
import { Component } from "@angular/core";
import { injectRegisterWebMCPTools } from "@tanstack/ai-angular";
import { searchProducts } from "./tools";

@Component({ selector: "app-products", standalone: true, template: "" })
export class ProductsComponent {
  registration = injectRegisterWebMCPTools([searchProducts], {
    onError: (error) => console.error(error),
  });
}

Options: toolOptions, context, and onError. context is required when a tool declares a required runtime context.

injectPageWebMCPTools(options?)

Reads the WebMCP tools that the page registered, as client tools. The signal starts empty and updates when the page adds or removes a tool. Pass the signal to injectChat as tools. See Page WebMCP Tools in Chat.

ts
import { Component } from "@angular/core";
import { injectPageWebMCPTools } from "@tanstack/ai-angular";

@Component({ selector: "app-page-tools", standalone: true, template: "" })
export class PageToolsComponent {
  pageTools = injectPageWebMCPTools({
    filter: (tool) => tool.origin === location.origin,
  });
}

Options: filter (return false to skip a tool) and onError.

Re-exports from @tanstack/ai-client

You can import these from @tanstack/ai-angular directly. See @tanstack/ai-client for details.

GroupExports
Connection adaptersfetchServerSentEvents, fetchHttpStream, fetchJson, xhrServerSentEvents, xhrHttpStream, webSocket, stream, rpcStream
Typed optionscreateChatClientOptions, clientTools
PersistencelocalStoragePersistence, sessionStoragePersistence, indexedDBPersistence
ClientsChatClient, GenerationClient, VideoGenerationClient, RealtimeClient, AudioRecorder, InterruptManager
WebMCP and MCP AppsregisterWebMCPTools, getWebMCPTools, subscribeWebMCPTools, createMcpAppBridge
ErrorsUnsupportedResponseStreamError, StreamTruncatedError, DurableStreamIncompleteError, StreamReconnectLimitError, StorageUnavailableError
Stream helpersStreamProcessor, chunk strategies, parsePartialJSON, message converters

BYOK helpers (defineByok, defaultByokStorage, and more) are on the @tanstack/ai-angular/byok subpath.

Types

TypeWhat it is
InjectChatOptions, InjectChatResultThe options and return value of injectChat.
ReactiveOption<T>T, Signal<T>, or () => T.
UIMessage<TTools>A chat message with typed parts.
InferChatMessages<T>The message type for a createChatClientOptions result.
MultimodalContent, SendMessageOptions, WhenBusy, QueueOption, QueuedMessageSend and queue types.
DeepPartial<T>The type of partial while structured output streams.
Inject<Name>Options, Inject<Name>ResultOptions and return types for the generation and recorder injectables. The WebMCP injectables export only an options type.

Pass a plain tools array to injectChat or createChatClientOptions. Tool names, inputs, and outputs stay typed without as const. Import toolDefinition from @tanstack/ai. This package does not re-export it.