본문으로 건너뛰기

@tanstack/ai-angular

헤드리스 클라이언트에 편리한 Angular 바인딩을 제공하는 TanStack AI용 Angular 시그널 기반 바인딩입니다.

주입 컨텍스트 요구 사항: 이 패키지의 모든 inject* 함수는 내부적으로 Angular의 inject()를 호출합니다. 이러한 함수는 반드시 Angular 주입 컨텍스트(컴포넌트 또는 디렉티브의 클래스 필드 초기화 구문, 생성자 또는 runInInjectionContext 내부)에서 호출해야 합니다. 주입 컨텍스트 외부에서 호출하면 런타임 오류가 발생합니다.

설치

npm install @tanstack/ai-angular

injectChat(options?)

완전한 타입 안전성을 갖추고 Angular에서 채팅 상태를 관리하는 기본 주입 가능 항목입니다.

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

@Component({
selector: "app-chat",
standalone: true,
template: `...`,
})
export class ChatComponent {
// injectChat is called in a field initializer — valid injection context.
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}

옵션

@tanstack/ai-clientChatClientOptions를 확장합니다(내부 상태 콜백 제외).

  • connection - 연결 어댑터(필수이며 fetcher를 사용할 수도 있음)
  • fetcher? - 단일 생성에 사용하는 직접 비동기 함수(connection의 대안)
  • tools? - 클라이언트 도구 구현 배열(.client() 메서드 포함)
  • initialMessages? - 초기 메시지 배열
  • threadId? - 이 채팅의 유일한 식별자입니다. 영속성이 켜져 있으면 필수입니다. 생략하면 마운트 후 생성됩니다.
  • forwardedProps? - AG-UI RunAgentInput.forwardedProps 필드로 서버에 전달하는 클라이언트 제어 JSON입니다. 반응형이며 일반 값, Angular Signal 또는 인자 없는 getter를 받습니다. 변경 사항은 effect를 통해 자동으로 동기화됩니다.
  • body? - 지원 중단 예정입니다. 대신 forwardedProps를 사용합니다. 이전 버전과의 호환성을 위해 계속 작동하며, 전송 시 값이 forwardedProps에 병합됩니다. 반응형입니다(forwardedProps와 동일한 형식).
  • byok? - defineByok의 선택적 BYOK 키링입니다. 전송할 때마다 클라이언트가 확인된 provider를 준비하고 x-byok-* 요청 헤더를 추가합니다. 키는 본문에 절대 들어가지 않습니다.
  • byokProvider? - 이 채팅의 provider slug를 반환하는 선택적 함수입니다. slug를 반환하면 해당 키만 준비해 전송합니다. 그렇지 않으면 forwardedProps, body, 호출별 sendMessage body의 병합된 provider를 사용합니다. 나중의 소스가 우선합니다. slug가 확인되지 않으면 저장된 모든 키를 첨부하는 대신 전송에서 오류가 발생합니다.
  • context? - 클라이언트 도구 구현에 전달하는 타입이 지정된 클라이언트 전용 런타임 컨텍스트입니다. 반응형입니다(동일한 형식). 이 값은 서버로 직렬화되지 않습니다.
  • live? - 라이브 구독 모드를 활성화합니다(자동 구독/구독 해제). 반응형입니다(동일한 형식).
  • outputSchema? - 표준 스키마 호환 스키마(Zod, Valibot, ArkType 또는 JSON Schema)입니다. 제공하면 반환값에 타입이 지정된 partialfinal 시그널을 추가합니다.
  • persistence? - 영속성 설정
  • devtools? - TanStack AI Devtools 표시 옵션
  • onResponse? - 응답을 수신할 때 호출하는 콜백
  • onChunk? - 스트림 청크를 수신할 때 호출하는 콜백
  • onFinish? - 응답이 완료될 때 호출하는 콜백
  • onError? - 오류가 발생할 때 호출하는 콜백
  • onInterruptStateChange? - 인터럽트 상태가 변경될 때 호출하는 콜백입니다. 컨텍스트 소스는 복원된 상태의 경우 hydrate, 스트리밍 또는 클라이언트가 시작한 업데이트의 경우 live입니다.
  • onCustomEvent? - 사용자 지정 스트림 이벤트용 콜백
  • streamProcessor? - 스트림 처리 설정

반응형 옵션(body, forwardedProps, context, live)은 다음 중 하나인 ReactiveOption<T>를 받습니다.

import type { Signal } from "@angular/core";

type ReactiveOption<T> = T | Signal<T> | (() => T);

일반 값은 상수가 되고, Signal은 직접 읽습니다. 인자 없는 getter는 computed로 감싸므로 내부에서 읽는 모든 시그널이 추적됩니다.

참고: 클라이언트 도구는 자동으로 실행되므로 onToolCall 콜백이 필요하지 않습니다.

반환값

import type { Signal } from "@angular/core";
import type {
UIMessage,
MultimodalContent,
DeepPartial,
} from "@tanstack/ai-angular";
import type { ModelMessage, InferSchemaType } from "@tanstack/ai/client";
import type {
ChatClientState,
ConnectionStatus,
SendMessageOptions,
} from "@tanstack/ai-client";
type TSchema = any;

interface InjectChatResult {
messages: Signal<UIMessage[]>;
sendMessage: (
content: string | MultimodalContent,
options?: SendMessageOptions,
) => Promise<void>;
append: (message: ModelMessage | UIMessage) => Promise<void>;
addToolResult: (result: {
toolCallId: string;
tool: string;
output: any;
state?: "output-available" | "output-error";
errorText?: string;
}) => Promise<void>;
addToolApprovalResponse: (response: {
id: string;
approved: boolean;
}) => Promise<void>;
reload: () => Promise<void>;
stop: () => void;
clear: () => void;
setMessages: (messages: UIMessage[]) => void;
isLoading: Signal<boolean>;
error: Signal<Error | undefined>;
status: Signal<ChatClientState>;
isSubscribed: Signal<boolean>;
connectionStatus: Signal<ConnectionStatus>;
sessionGenerating: Signal<boolean>;
// Only present when outputSchema is supplied:
partial: Signal<DeepPartial<InferSchemaType<TSchema>>>;
final: Signal<InferSchemaType<TSchema> | null>;
}

참고: 모든 반응형 상태(messages, isLoading, error, status, isSubscribed, connectionStatus, sessionGenerating)는 읽기 전용 Angular Signal로 노출됩니다. 함수로 호출해 읽습니다(예: chat.messages(), chat.isLoading()). 정리는 DestroyRef.onDestroy를 통해 자동으로 수행됩니다.

injectByok(client)

Angular에서 ByokClient 스냅샷을 구독합니다. 주입 컨텍스트에서 호출해야 합니다. 반환값은 읽기 전용 Signal입니다.

import { Component } 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() {
const openai = this.snapshot().status.openai;
return openai && "masked" in openai ? openai.masked : "No key";
}
}

snapshot()에는 status, locked, prompt가 있습니다. 자체 UI에서 byok.update(provider, value)를 호출해 키를 저장합니다. Bring Your Own Key를 참고하세요.

연결 어댑터

편의를 위해 @tanstack/ai-client에서 다시 내보냅니다.

import {
fetchServerSentEvents,
fetchHttpStream,
xhrServerSentEvents,
xhrHttpStream,
stream,
rpcStream,
type ConnectionAdapter,
} from "@tanstack/ai-angular";

예시: 기본 채팅

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

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

예시: 도구 승인

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

@Component({
selector: "app-approval-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (
part.type === 'tool-call' &&
part.state === 'approval-requested' &&
part.approval
) {
<div>
<p>Approve: {{ part.name }}</p>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: true })">
Approve
</button>
<button (click)="chat.addToolApprovalResponse({ id: part.approval!.id, approved: false })">
Deny
</button>
</div>
}
}
}
`,
})
export class ApprovalChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
});
}

예시: 타입 안전성을 갖춘 클라이언트 도구

import { Component } from "@angular/core";
import { CommonModule } from "@angular/common";
import { injectChat, fetchServerSentEvents } from "@tanstack/ai-angular";
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(), type: z.string() }),
});

const saveToStorageDef = toolDefinition({
name: "saveToStorage",
description: "Save a value to localStorage",
inputSchema: z.object({ key: z.string(), value: z.string() }),
});

@Component({
selector: "app-typed-chat",
standalone: true,
imports: [CommonModule],
template: `
@for (message of chat.messages(); track message.id) {
@for (part of message.parts; track $index) {
@if (part.type === 'tool-call' && part.name === 'updateUI') {
<div>Tool executed: {{ part.name }}</div>
}
}
}
`,
})
export class TypedChatComponent {
// Create client implementations
private updateUI = updateUIDef.client((input) => {
// input is fully typed!
return { success: true };
});

private saveToStorage = saveToStorageDef.client((input) => {
localStorage.setItem(input.key, input.value);
return { saved: true };
});

// Create typed tools array (no 'as const' needed!)
private tools = [this.updateUI, this.saveToStorage];

chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
tools: this.tools, // Automatic execution, full type safety
});
}

예시: 시그널을 사용하는 반응형 옵션

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

@Component({
selector: "app-reactive-chat",
standalone: true,
template: `
<button (click)="toggleLanguage()">Toggle Language</button>
@for (message of chat.messages(); track message.id) {
<p>{{ message.role }}: {{ message.parts[0]?.content }}</p>
}
`,
})
export class ReactiveChatComponent {
language = signal("en");

// forwardedProps is reactive — the signal is read on every request
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
forwardedProps: () => ({ language: this.language() }),
});

toggleLanguage() {
this.language.set(this.language() === "en" ? "fr" : "en");
}
}

예시: 구조화된 출력

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

const recipeSchema = z.object({
title: z.string(),
ingredients: z.array(z.string()),
steps: z.array(z.string()),
});

@Component({
selector: "app-recipe-chat",
standalone: true,
template: `
<button (click)="chat.sendMessage('Give me a pasta recipe')">Ask</button>
@if (chat.partial().title) {
<h2>{{ chat.partial().title }}</h2>
}
@if (chat.final()) {
<ul>
@for (step of chat.final()!.steps; track $index) {
<li>{{ step }}</li>
}
</ul>
}
`,
})
export class RecipeChatComponent {
chat = injectChat({
connection: fetchServerSentEvents("/api/chat"),
outputSchema: recipeSchema,
});
}

생성 주입 가능 항목

단일 생성 작업(이미지, 오디오, 음성, 전사, 요약, 동영상)을 위한 Angular 주입 가능 항목입니다. 모두 동일한 패턴을 따릅니다. connection 또는 fetcher를 제공하고 generate()를 호출한 뒤 반응형 시그널을 읽습니다.

injectGeneration(options)

사용자 지정 생성 유형을 위한 기본 주입 가능 항목입니다. 아래의 모든 특화 주입 가능 항목이 이를 기반으로 합니다.

import { Component } from "@angular/core";
import { injectGeneration } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";

@Component({ selector: "app-custom", standalone: true, template: `...` })
export class CustomGenerationComponent {
gen = injectGeneration({
connection: fetchServerSentEvents("/api/generate/custom"),
});

// Call gen.generate(input), read gen.result(), gen.isLoading(), etc.
}

옵션: connection?, fetcher?, threadId?, body? (reactive), devtools?, onResult?, onError?, onProgress?, onChunk?

반환값: generate, result, isLoading, error, status, stop, reset, runId. 모든 반응형 상태는 읽기 전용 Signal<T>입니다.

injectGenerateImage(options)

이미지 생성 주입 가능 항목입니다. generate()ImageGenerateInput을 받고, 결과는 ImageGenerationResult입니다.

import { Component } from "@angular/core";
import { injectGenerateImage } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";

@Component({
selector: "app-image",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A mountain at sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<img [src]="gen.result()!.images[0]!.url" alt="Generated image" />
}
`,
})
export class ImageComponent {
gen = injectGenerateImage({
connection: fetchServerSentEvents("/api/generate/image"),
});
}

injectGenerateAudio(options)

오디오 생성 주입 가능 항목(음악, 음향 효과)입니다. generate()AudioGenerateInput을 받고, 결과는 AudioGenerationResult입니다.

import { Component } from "@angular/core";
import { injectGenerateAudio } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";

@Component({
selector: "app-audio",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'An upbeat electronic track', duration: 10 })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.result()) {
<audio [src]="gen.result()!.audio.url" controls></audio>
}
`,
})
export class AudioComponent {
gen = injectGenerateAudio({
connection: fetchServerSentEvents("/api/generate/audio"),
});
}

injectGenerateSpeech(options)

텍스트 음성 변환 주입 가능 항목입니다. generate()SpeechGenerateInput을 받고, 결과는 TTSResult입니다.

injectTranscription(options)

오디오 전사 주입 가능 항목입니다. generate()TranscriptionGenerateInput을 받고, 결과는 TranscriptionResult입니다.

injectSummarize(options)

텍스트 요약 주입 가능 항목입니다. generate()SummarizeGenerateInput을 받고, 결과는 SummarizationResult입니다.

injectGenerateVideo(options)

작업 폴링을 지원하는 동영상 생성 주입 가능 항목입니다. 추가로 jobIdvideoStatus 시그널을 반환하며, onJobCreated?onStatusUpdate? 콜백을 받습니다.

import { Component } from "@angular/core";
import { injectGenerateVideo } from "@tanstack/ai-angular";
import { fetchServerSentEvents } from "@tanstack/ai-client";

@Component({
selector: "app-video",
standalone: true,
template: `
<button (click)="gen.generate({ prompt: 'A time-lapse of a sunset' })" [disabled]="gen.isLoading()">
Generate
</button>
@if (gen.videoStatus()) {
<p>Status: {{ gen.videoStatus()!.status }}</p>
}
@if (gen.result()) {
<video [src]="gen.result()!.url" controls></video>
}
`,
})
export class VideoComponent {
gen = injectGenerateVideo({
connection: fetchServerSentEvents("/api/generate/video"),
onJobCreated: (jobId) => console.log("Job created:", jobId),
});
}

추가 반환값(동영상만 해당):

  • jobId: Signal<string | null> — 서버가 생성한 폴링 작업 ID
  • videoStatus: Signal<VideoStatusInfo | null> — 폴링 루프의 실시간 상태 업데이트

모든 생성 주입 가능 항목은 DestroyRef.onDestroy를 통해 자동으로 정리됩니다.

주입 컨텍스트

Angular의 DI 시스템에서는 컴포넌트 생성 중에 inject()를 호출해야 합니다. 이 패키지의 모든 inject* 함수는 내부적으로 inject()를 호출합니다. 유효한 호출 위치는 다음과 같습니다.

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

const injector = inject(Injector);

// Field initializer (recommended)
export class MyComponent {
chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}

// Constructor
export class MyComponentAlt {
chat: ReturnType<typeof injectChat>;
constructor() {
this.chat = injectChat({ connection: fetchServerSentEvents("/api/chat") });
}
}

// Inside runInInjectionContext
const chat = runInInjectionContext(injector, () =>
injectChat({ connection: fetchServerSentEvents("/api/chat") }),
);

createChatClientOptions(options)

타입이 지정된 채팅 옵션을 생성하는 헬퍼입니다(@tanstack/ai-client에서 다시 내보냄).

import {
createChatClientOptions,
type InferChatMessages,
} from "@tanstack/ai-client";
import { fetchServerSentEvents } from "@tanstack/ai-angular";
import { tool1, tool2 } from "./tools";

// Create typed tools array (no 'as const' needed!)
const tools = [tool1, tool2];

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

type Messages = InferChatMessages<typeof chatOptions>;

타입

@tanstack/ai-angular에서 다시 내보냅니다(@tanstack/ai-client에서 제공됨).

  • UIMessage<TTools> - 도구 타입 매개변수를 포함한 메시지 타입
  • InjectChatOptions<TTools, TSchema, TContext> - 채팅 주입 가능 항목 옵션
  • InjectChatResult<TTools, TSchema> - 채팅 주입 가능 항목 반환 타입
  • ReactiveOption<T> - 반응형 옵션 필드를 위한 T | Signal<T> | (() => T)의 유니언
  • DeepPartial<T> - 재귀적 partial 타입으로, 진행 중인 partial 값의 타입 지정에 사용됩니다.
  • ChatRequestBody - 요청 본문 타입
  • MultimodalContent - sendMessage용 멀티모달 콘텐츠 타입
  • ConnectionAdapter - 연결 어댑터 인터페이스
  • InferChatMessages<T> - 옵션에서 메시지 타입 추출
  • GenerationClientState - 생성 수명 주기 상태
  • ImageGenerateInput - 이미지 생성 입력 타입
  • AudioGenerateInput - 오디오 생성 입력 타입
  • SpeechGenerateInput - 음성 생성 입력 타입
  • TranscriptionGenerateInput - 전사 입력 타입
  • SummarizeGenerateInput - 요약 입력 타입
  • VideoGenerateInput - 동영상 생성 입력 타입
  • VideoGenerateResult - 동영상 생성 결과 타입
  • VideoStatusInfo - 동영상 작업 상태 정보

도구 작성 타입 — @tanstack/ai에서 직접 가져옵니다(@tanstack/ai-angular에서는 다시 내보내지 않음).

  • toolDefinition() - 동형 도구 정의 생성
  • ToolDefinitionInstance - 도구 정의 타입
  • ClientTool - 클라이언트 도구 타입
  • ServerTool - 서버 도구 타입

다음 단계