# @bdky/chat-pilot-kit

[![npm version](https://img.shields.io/npm/v/@bdky/chat-pilot-kit.svg)](https://www.npmjs.com/package/@bdky/chat-pilot-kit)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)

**English** | [简体中文](./README.zh-CN.md)

> A headless, framework-agnostic AI chat SDK with streaming SSE support, extensible message nodes, and pluggable architecture.

`@bdky/chat-pilot-kit` is a lightweight, flexible SDK for building AI-powered chat applications. It provides a complete conversation management system with streaming support, multi-modal content handling, and a powerful extension system—all without imposing any UI framework constraints.

## ✨ Highlights

- 🎯 **Headless & framework-agnostic** — No UI dependencies, works with React/Vue/vanilla JavaScript
- 🌊 **Streaming-first** — Built-in SSE streaming with time-to-first-token (TTFT) tracking
- 🧩 **Extensible node system** — 10 built-in node types + custom extension API
- 🔌 **Plugin architecture** — MessageExtension system for custom message processing
- 🎨 **Multi-modal support** — Text, Markdown, Image, File, Audio, Video, Thinking Blocks, Tool Calls
- 📡 **Event-driven** — Emittery-based event system for real-time state updates
- 💾 **Conversation management** — Import/export, session management, history persistence
- 📘 **Full TypeScript support** — Complete type definitions out of the box

## 📦 Install

```bash
# npm
npm install @bdky/chat-pilot-kit reflect-metadata

# yarn
yarn add @bdky/chat-pilot-kit reflect-metadata

# pnpm
pnpm add @bdky/chat-pilot-kit reflect-metadata
```

**Peer Dependencies:**
- `reflect-metadata` (required for dependency injection)

**Formats:**
- ESM: `@bdky/chat-pilot-kit` (`.esm.js`)
- CJS: `@bdky/chat-pilot-kit` (`.cjs.js`)
- TypeScript declarations included

## 🚀 Quick Start

```typescript
import 'reflect-metadata';
import {createChatPilotKit, BaseAgentService} from '@bdky/chat-pilot-kit';
import type {AgentMessageData} from '@bdky/chat-pilot-kit';

// 1. Define your custom AgentService
class MyAgentService extends BaseAgentService {
    async query(text: string): Promise<void> {
        const sessionId = this.sessionId();
        const queryId = this.queryId();

        // Simulate SSE streaming
        const chunks: AgentMessageData[] = [
            {answer: 'Hello', nodeType: 'text', queryId, sessionId},
            {answer: ' world!', nodeType: 'text', queryId, sessionId}
        ];

        for (const chunk of chunks) {
            this.onData(chunk);
        }

        this.onCompleted();
    }

    dispose(): void {
        // Cleanup resources
    }
}

// 2. Create ChatPilotKit instance
const {controller, emitter} = createChatPilotKit({
    agentService: MyAgentService
});

// 3. Subscribe to events
emitter.on('conversation_change', payload => {
    console.log('Conversation updated:', payload);
});

// 4. Send a query
await controller.query('Hello');
```

## 🏗️ Core Concepts

```
User Input → Controller.query()
  → AgentService.query() → AI Backend (SSE)
    → AgentMessageData chunks
      → Extension.canProcess() → Extension.process()
        → ConversationNode created/updated
          → Events emitted → UI subscribes & renders
```

### Controller

The `ChatPilotKitController` is the main entry point for interacting with the SDK.

| Method | Parameters | Returns | Description |
|--------|-----------|---------|-------------|
| `query` | `text: string, options?: IQueryOptions` | `Promise<void>` | Send a text query to the AI agent |
| `queryWithAttachments` | `text: string, attachments: IAttachmentInput[], options?: IQueryOptions` | `Promise<void>` | Send a query with file attachments |
| `interrupt` | - | `void` | Interrupt the current query |
| `clear` | - | `void` | Clear all conversations |
| `dispose` | - | `void` | Cleanup and destroy the controller |
| `getOptions` | - | `IResolvedOptions` | Get current configuration |
| `exportConversations` | - | `IConversationBeanSnapshot[]` | Export all conversations as JSON |
| `importConversations` | `conversations: IConversationBeanInput[], options?: IImportOptions` | `void` | Import conversations from JSON |

### Agent Service

Extend `BaseAgentService` to integrate with your AI backend.

**Abstract Methods (must implement):**
- `query(text: string): Promise<void>` — Send query to AI backend
- `dispose(): void` — Cleanup resources

**Protected Methods (call from your implementation):**
- `onData(data: AgentMessageData): void` — Emit a data chunk
- `onCompleted(): void` — Signal query completion
- `onError(error: Error): void` — Report an error
- `onTtft(timestamp: number): void` — Report time-to-first-token

**Utility Methods:**
- `sessionId(): string` — Get current session ID
- `queryId(): string` — Get current query ID
- `setSessionId(id: string): void` — Set session ID
- `setQueryId(id: string): void` — Set query ID
- `abort(): void` — Abort current request

**AgentMessageData Interface:**

```typescript
interface AgentMessageData {
    answer: string;           // Message text content
    nodeType?: string;        // Node type identifier (matches Extension.name)
    nodeData?: Record<string, unknown>; // Node-specific data
    queryId: string;          // Current query ID
    sessionId: string;        // Current session ID
}
```

**Complete Example with SSE:**

```typescript
import {BaseAgentService} from '@bdky/chat-pilot-kit';
import {ky, sseHook} from '@bdky/chat-pilot-kit/http';
import type {AgentMessageData} from '@bdky/chat-pilot-kit';

class MySSEAgentService extends BaseAgentService {
    private abortController: AbortController | null = null;

    async query(text: string): Promise<void> {
        this.abortController = new AbortController();
        const sessionId = this.sessionId();
        const queryId = this.queryId();

        try {
            await ky.post('https://api.example.com/chat', {
                json: {message: text, sessionId, queryId},
                signal: this.abortController.signal,
                hooks: {
                    afterResponse: [
                        sseHook.afterResponse<AgentMessageData>({
                            onMessage: chunk => {
                                this.onData(chunk);
                            },
                            onComplete: () => {
                                this.onCompleted();
                            },
                            onError: error => {
                                this.onError(error);
                            }
                        })
                    ]
                }
            });
        }
        catch (error) {
            if (error.name !== 'AbortError') {
                this.onError(error as Error);
            }
        }
    }

    dispose(): void {
        this.abortController?.abort();
    }
}
```

### Conversations & Nodes

**ConversationBean Structure:**

```typescript
interface ConversationBean {
    id: string;                    // Unique conversation ID
    role: 'client' | 'aiWorker';   // Conversation role
    nodes: ConversationNode[];     // Array of message nodes
    completed: boolean;            // Whether conversation is complete
    createdAt: number;             // Creation timestamp
    updatedAt: number;             // Last update timestamp
}
```

**ConversationNode Base Class:**

All message nodes extend `ConversationNode<TContent>`:

```typescript
abstract class ConversationNode<TContent = unknown> {
    id: string;
    type: string;
    content: TContent;
    completed: boolean;
    createdAt: number;
    updatedAt: number;
    metadata?: Record<string, unknown>;

    abstract toJSON(): IConversationNodeSnapshot<TContent>;
    updateContent(content: TContent): void;
    updateMetadata(metadata: Record<string, unknown>): void;
}
```

**Built-in Node Types:**

| Node Class | Type String | Content Type | Streamable | Factory Methods |
|------------|-------------|--------------|------------|-----------------|
| `TextNode` | `'text'` | `string` | No | `TextNode.fromString(text)` |
| `MarkdownNode` | `'markdown'` | `string` | Yes | `MarkdownNode.fromString(text)` |
| `ImageNode` | `'image'` | `IImageContent` | No | `ImageNode.fromContent(content)` |
| `FileNode` | `'file'` | `IFileContent` | No | `FileNode.fromContent(content)` |
| `AudioNode` | `'audio'` | `IAudioContent` | No | `AudioNode.fromContent(content)` |
| `VideoNode` | `'video'` | `IVideoContent` | No | `VideoNode.fromContent(content)` |
| `ThinkingBlockNode` | `'thinking'` | `IThinkingContent` | Yes | `ThinkingBlockNode.fromContent(content)` |
| `ToolCallNode` | `'tool_call'` | `IToolCallContent` | No | `ToolCallNode.fromContent(content)` |
| `GenericNode` | custom | `unknown` | No | `new GenericNode(type, content)` |
| `StreamableGenericNode` | custom | `unknown` | Yes | `new StreamableGenericNode(type, content)` |

**Content Type Interfaces:**

```typescript
interface IImageContent {
    url: string;
    alt?: string;
    width?: number;
    height?: number;
}

interface IFileContent {
    url: string;
    fileName: string;
    fileSize: number;
    fileType: string;
}

interface IThinkingContent {
    text: string;
    collapsed?: boolean;
}

interface IAudioContent {
    url: string;
    duration?: number;
    mimeType?: string;
}

interface IVideoContent {
    url: string;
    duration?: number;
    poster?: string;
    mimeType?: string;
}

interface IToolCallContent {
    name: string;
    arguments: Record<string, unknown>;
    result?: unknown;
    status: 'pending' | 'running' | 'completed' | 'error';
    error?: string;
}
```

### Events

Subscribe to events using the `emitter` returned by `createChatPilotKit()`.

| Event | Payload | Description |
|-------|---------|-------------|
| `ready` | `never` | SDK initialized and ready |
| `conversation_add` | `{conversationId, role, timestamp}` | New conversation created |
| `conversation_change` | `IConversationChangePayload` | Conversation updated (nodes changed) |
| `node_add` | `{conversationId, node}` | New node added to conversation |
| `node_update` | `{conversationId, node}` | Existing node updated |
| `error` | `IChatPilotKitError` | Error occurred |
| `interrupt` | `{queryId, sessionId}` | Query interrupted |
| `clear` | `never` | All conversations cleared |
| `ttft` | `ITtftPayload` | Time-to-first-token measured |
| `history_import` | `{count, position}` | Conversations imported |

**Usage Example:**

```typescript
const {emitter} = createChatPilotKit({agentService: MyAgentService});

emitter.on('conversation_change', payload => {
    console.log('Conversation:', payload.conversationId);
    console.log('Nodes:', payload.nodes);
    console.log('Completed:', payload.completed);
});

emitter.on('error', error => {
    console.error('Error:', error.message);
    console.error('Category:', error.category);
    console.error('Severity:', error.severity);
});

emitter.on('ttft', payload => {
    console.log('Time to first token:', payload.totalLatency, 'ms');
});
```

## 🔌 Extensions

Extensions process incoming `AgentMessageData` chunks and create/update conversation nodes.

### Built-in Extensions

The SDK includes 8 built-in extensions (ordered by priority):

| Extension | Priority | Processes | Creates Node |
|-----------|----------|-----------|--------------|
| `ThinkingBlockExtension` | 50 | `nodeType: 'thinking'` | `ThinkingBlockNode` |
| `TextExtension` | 100 | `nodeType: 'text'` | `TextNode` |
| `ImageExtension` | 100 | `nodeType: 'image'` | `ImageNode` |
| `FileExtension` | 100 | `nodeType: 'file'` | `FileNode` |
| `AudioExtension` | 100 | `nodeType: 'audio'` | `AudioNode` |
| `VideoExtension` | 100 | `nodeType: 'video'` | `VideoNode` |
| `ToolCallExtension` | 100 | `nodeType: 'tool_call'` | `ToolCallNode` |
| `MarkdownExtension` | 200 | `nodeType: 'markdown'` | `MarkdownNode` |

**Get all built-in extensions:**

```typescript
import {getBuiltInExtensions} from '@bdky/chat-pilot-kit';

const extensions = getBuiltInExtensions();
```

### Creating Custom Extensions

Use `MessageExtension.create()` to define custom extensions:

```typescript
import {MessageExtension, GenericNode} from '@bdky/chat-pilot-kit';
import type {AgentMessageData} from '@bdky/chat-pilot-kit';

const ChartExtension = MessageExtension.create({
    name: 'chart',
    priority: 150,
    streamable: false,

    canProcess(data: AgentMessageData) {
        return data.nodeType === 'chart';
    },

    process(data: AgentMessageData) {
        // Create and return a new node from the data
        const content = {
            type: data.nodeData?.chartType as string,
            data: data.nodeData?.chartData
        };
        return new GenericNode('chart', content);
    },

    hydrate(snapshot) {
        // Restore node from snapshot when importing conversations
        return new GenericNode(snapshot.type, snapshot.content);
    },

    addNodeView() {
        return null; // Headless, no view
    }
});

// Use in createChatPilotKit
const {controller} = createChatPilotKit({
    agentService: MyAgentService,
    extensions: [ChartExtension]
});
```

**Streamable Extension Example:**

```typescript
import {MessageExtension, StreamableGenericNode} from '@bdky/chat-pilot-kit';

const StreamingChartExtension = MessageExtension.create({
    name: 'streaming_chart',
    priority: 150,
    streamable: true,

    canProcess(data) {
        return data.nodeType === 'streaming_chart';
    },

    process(data) {
        // Create initial node with empty content
        return new StreamableGenericNode('streaming_chart', '');
    },

    onStreamAppend(node, data) {
        // Append content chunks as they arrive
        node.appendContent(data.answer);
    },

    onStreamEnd(node) {
        // Mark as completed when stream ends
        node.markCompleted();
    },

    hydrate(snapshot) {
        const node = new StreamableGenericNode(snapshot.type, snapshot.content);
        node.completed = snapshot.completed;
        return node;
    },

    addNodeView() {
        return null;
    }
});
```

**Extension Lifecycle:**

```typescript
const MyExtension = MessageExtension.create({
    name: 'my_extension',
    priority: 100,

    onCreate() {
        console.log('Extension initialized');
        // Preload dependencies, setup resources
    },

    onDestroy() {
        console.log('Extension destroyed');
        // Cleanup resources
    },

    canProcess(data) {
        return data.nodeType === 'my_type';
    },

    process(data) {
        return new GenericNode('my_type', data.answer);
    },

    hydrate(snapshot) {
        return new GenericNode(snapshot.type, snapshot.content);
    },

    addNodeView() {
        return null;
    }
});
```

### Overriding Built-in Extensions

**Option 1: Replace with `overrideExtensions`**

```typescript
const CustomMarkdownExtension = MessageExtension.create({
    name: 'markdown', // Same name as built-in
    priority: 200,
    streamable: true,

    canProcess(data) {
        return data.nodeType === 'markdown';
    },

    process(data) {
        // Custom markdown processing
        return MarkdownNode.fromString(data.answer || '');
    },

    onStreamAppend(node, data) {
        // Custom streaming logic
        node.appendContent(data.answer || '');
    },

    onStreamEnd(node) {
        node.markCompleted();
    },

    hydrate(snapshot) {
        return MarkdownNode.fromJSON(snapshot);
    },

    addNodeView() {
        return null;
    }
});

createChatPilotKit({
    agentService: MyAgentService,
    overrideExtensions: [CustomMarkdownExtension] // Replaces built-in MarkdownExtension
});
```

**Option 2: Extend with `.extend()`**

```typescript
import {MarkdownExtension} from '@bdky/chat-pilot-kit';

const EnhancedMarkdownExtension = MarkdownExtension.extend({
    priority: 250, // Change priority

    onStreamAppend(node, data) {
        // Call original behavior (if needed, manually)
        node.appendContent(data.answer || '');

        // Add custom behavior
        console.log('Enhanced markdown streaming:', data.answer);
    }
});

createChatPilotKit({
    agentService: MyAgentService,
    overrideExtensions: [EnhancedMarkdownExtension]
});
```

## ⚠️ Error Handling

The SDK provides a comprehensive error management system.

**ErrorManager API:**

```typescript
const {controller, emitter} = createChatPilotKit({agentService: MyAgentService});

// Subscribe to errors
emitter.on('error', (error: IChatPilotKitError) => {
    console.error('Error:', error);
});
```

**Error Categories:**

```typescript
enum ErrorCategory {
    NETWORK = 'NETWORK',           // Network-related errors
    TIMEOUT = 'TIMEOUT',           // Request timeout
    VALIDATION = 'VALIDATION',     // Input validation errors
    SERVICE = 'SERVICE',           // Backend service errors
    CONFIGURATION = 'CONFIGURATION', // Configuration errors
    INTERNAL = 'INTERNAL'          // Internal SDK errors
}
```

**Error Severities:**

```typescript
enum ErrorSeverity {
    LOW = 'LOW',           // Minor issues, recoverable
    MEDIUM = 'MEDIUM',     // Moderate issues, may affect UX
    HIGH = 'HIGH',         // Serious issues, requires attention
    CRITICAL = 'CRITICAL'  // Critical failures, system unusable
}
```

**IChatPilotKitError Interface:**

```typescript
interface IChatPilotKitError {
    code: string;                    // Error code (e.g., 'NETWORK_ERROR')
    message: string;                 // Human-readable message
    category: ErrorCategory;         // Error category
    severity: ErrorSeverity;         // Severity level
    source: 'agent' | 'upload' | 'extension' | 'controller' | 'conversation';
    metadata?: Record<string, unknown>; // Additional context
    originalError?: Error;           // Original error object
}
```

**Usage Example:**

```typescript
emitter.on('error', error => {
    if (error.severity === ErrorSeverity.CRITICAL) {
        // Show critical error UI
        alert(`Critical error: ${error.message}`);
    }
    else if (error.category === ErrorCategory.NETWORK) {
        // Retry logic
        console.log('Network error, retrying...');
    }

    // Log to monitoring service
    logToMonitoring(error);
});
```

## 💾 Conversation Persistence

Export and import conversations for persistence or migration.

**Export Conversations:**

```typescript
const snapshots = controller.exportConversations();
// Save to localStorage, database, etc.
localStorage.setItem('conversations', JSON.stringify(snapshots));
```

**Import Conversations:**

```typescript
const snapshots = JSON.parse(localStorage.getItem('conversations') || '[]');

controller.importConversations(snapshots, {
    position: 'prepend' // or 'replace'
});
```

**IImportOptions:**

```typescript
interface IImportOptions {
    position?: 'prepend' | 'replace'; // Default: 'prepend'
}
```

- `'prepend'`: Insert imported conversations before existing ones
- `'replace'`: Replace all existing conversations

**Hydration Process:**

When importing conversations, the SDK:
1. Parses the JSON snapshots
2. Matches each node's `type` to registered extensions
3. Calls the extension's `process()` method to recreate nodes
4. Restores node state (content, metadata, completed status)
5. Emits `history_import` event

**Complete Round-trip Example:**

```typescript
// Export
const snapshots = controller.exportConversations();
const json = JSON.stringify(snapshots);

// Save to backend
await fetch('/api/save-history', {
    method: 'POST',
    body: json,
    headers: {'Content-Type': 'application/json'}
});

// Later: Load from backend
const response = await fetch('/api/load-history');
const loadedSnapshots = await response.json();

// Import
controller.importConversations(loadedSnapshots, {
    position: 'replace'
});
```

## 🌊 Streaming

Nodes that implement `IStreamableNode` support incremental content updates.

**IStreamableNode Interface:**

```typescript
interface IStreamableNode<TChunk = string> {
    appendContent(chunk: TChunk): void;
}
```

**Type Guard:**

```typescript
import {isStreamableNode} from '@bdky/chat-pilot-kit';

if (isStreamableNode(node)) {
    node.appendContent('new content');
}
```

**Built-in Streamable Nodes:**
- `MarkdownNode` — Appends markdown text
- `ThinkingBlockNode` — Appends thinking process text
- `StreamableGenericNode` — Generic streamable node

**How Streaming Works in Extensions:**

```typescript
const StreamingExtension = MessageExtension.create({
    name: 'streaming_text',
    priority: 100,
    streamable: true,

    canProcess(data) {
        return data.nodeType === 'streaming_text';
    },

    process(data) {
        // Create initial streamable node
        return new StreamableGenericNode('streaming_text', data.answer || '');
    },

    onStreamAppend(node, data) {
        // Append content chunks as they arrive
        node.appendContent(data.answer);
    },

    onStreamEnd(node) {
        // Mark as completed when stream ends
        node.markCompleted();
    },

    hydrate(snapshot) {
        const node = new StreamableGenericNode(snapshot.type, snapshot.content);
        node.completed = snapshot.completed;
        return node;
    },

    addNodeView() {
        return null;
    }
});
```

**MarkdownNode Streaming Example:**

```typescript
import {MarkdownNode} from '@bdky/chat-pilot-kit';

const node = MarkdownNode.fromString('');

// Simulate streaming
node.appendContent('# Hello\n');
node.appendContent('This is ');
node.appendContent('**streaming** ');
node.appendContent('markdown.');

console.log(node.content); // "# Hello\nThis is **streaming** markdown."
```

## 🎨 NodeView System (Framework Integration)

The NodeView system enables framework-specific rendering of conversation nodes.

**Core Interfaces:**

```typescript
interface NodeViewProps<TNode extends ConversationNode = ConversationNode> {
    node: TNode;
    updateContent: (content: TNode['content']) => void;
    updateMetadata: (metadata: Record<string, unknown>) => void;
    completed: boolean;
    role: 'client' | 'aiWorker';
    conversation: ConversationBean;
    destroy: () => void;
}

interface INodeView {
    mount(container: HTMLElement): void;
    update(props: NodeViewProps): void;
    destroy(): void;
}

type NodeViewFactory<TNode extends ConversationNode = ConversationNode> =
    (props: NodeViewProps<TNode>) => INodeView;
```

**How to Create Framework Adapters:**

1. Implement `NodeViewFactory` for each node type
2. Create a registry mapping node types to factories
3. Render nodes using framework-specific components
4. Subscribe to `conversation_change` events to trigger re-renders

**Reference Implementation:**

See `@bdky/chat-pilot-kit-react` for a complete React adapter implementation with:
- `ChatPilotKitProvider` context provider
- `useChatPilotKit()` hook
- Built-in NodeView components for all node types
- TypeScript support

## 🌐 HTTP Utilities

The SDK re-exports HTTP utilities from `ky` and `@bdky/ky-sse-hook` for convenience.

**Sub-path Export:**

```typescript
import {ky, sseHook, HTTPError, TimeoutError} from '@bdky/chat-pilot-kit/http';
import type {KyInstance, Options} from '@bdky/chat-pilot-kit/http';
```

**Re-exported from `ky`:**
- `ky` (default export) — HTTP client
- `HTTPError`, `TimeoutError` — Error classes
- All TypeScript types

**Re-exported from `@bdky/ky-sse-hook`:**
- `sseHook` — SSE streaming hook for ky

**Usage in AgentService:**

```typescript
import {BaseAgentService} from '@bdky/chat-pilot-kit';
import {ky, sseHook} from '@bdky/chat-pilot-kit/http';

class MyAgentService extends BaseAgentService {
    async query(text: string): Promise<void> {
        await ky.post('https://api.example.com/chat', {
            json: {message: text},
            hooks: {
                afterResponse: [
                    sseHook.afterResponse({
                        onMessage: chunk => this.onData(chunk),
                        onComplete: () => this.onCompleted(),
                        onError: error => this.onError(error)
                    })
                ]
            }
        });
    }

    dispose(): void {}
}
```

## ⚙️ Configuration Reference

**IOptions Interface:**

```typescript
interface IOptions<AS extends BaseAgentService = BaseAgentService> {
    agentService: Newable<AS>;           // Required: Your AgentService class
    enableDebugMode?: boolean;           // Default: false
    sessionTimeout?: number;             // Default: 300000 (5 minutes)
    extensions?: MessageExtensionInstance[]; // Custom extensions (appended)
    overrideExtensions?: MessageExtensionInstance[]; // Override built-in extensions
}
```

**IQueryOptions Interface:**

```typescript
interface IQueryOptions {
    sessionId?: string;                  // Custom session ID
    queryId?: string;                    // Custom query ID
    streaming?: boolean;                 // Enable streaming (default: true)
    metadata?: Record<string, unknown>;  // Custom metadata
}
```

**IAttachmentInput Interface:**

```typescript
interface IAttachmentInput {
    url: string;                         // File URL (uploaded)
    fileName: string;                    // File name
    fileSize: number;                    // File size in bytes
    fileType: string;                    // MIME type
    metadata?: Record<string, unknown>;  // Custom metadata
}
```

**Usage Example:**

```typescript
const {controller} = createChatPilotKit({
    agentService: MyAgentService,
    enableDebugMode: true,
    sessionTimeout: 600000, // 10 minutes
    extensions: [CustomExtension],
    overrideExtensions: [EnhancedMarkdownExtension]
});

await controller.query('Hello', {
    sessionId: 'custom-session-123',
    queryId: 'query-456',
    streaming: true,
    metadata: {source: 'web-app'}
});

await controller.queryWithAttachments(
    'Analyze this image',
    [
        {
            url: 'https://example.com/image.jpg',
            fileName: 'image.jpg',
            fileSize: 102400,
            fileType: 'image/jpeg'
        }
    ],
    {metadata: {feature: 'image-analysis'}}
);
```

## 📘 TypeScript

The SDK is written in TypeScript and provides complete type definitions.

**Type Imports:**

```typescript
import type {
    // Core
    IChatPilotKitController,
    IChatPilotKitEmitter,
    IOptions,
    IResolvedOptions,

    // Agent
    AgentMessageData,
    NBaseAgentService,

    // Nodes
    ConversationNode,
    IConversationNodeSnapshot,
    IConversationNodeInput,
    IStreamableNode,

    // Node Content
    IImageContent,
    IFileContent,
    IThinkingContent,
    IAudioContent,
    IVideoContent,
    IToolCallContent,
    ToolCallStatus,

    // Conversations
    ConversationRole,
    NConversationBean,
    IConversationBeanSnapshot,
    IConversationBeanInput,

    // Extensions
    MessageExtensionConfig,
    MessageExtensionInstance,

    // Events
    IConversationChangePayload,
    ITtftPayload,

    // Errors
    IChatPilotKitError,
    ICreateErrorParams,
    ErrorCategory,
    ErrorSeverity,

    // NodeView
    NodeViewProps,
    NodeViewFactory,
    INodeView,

    // Query
    IQueryOptions,
    IImportOptions,
    IAttachmentInput,

    // Node Data
    IConversationNodeData,
    ITextNodeData,
    IMarkdownNodeData,
    IImageNodeData,
    IFileNodeData,
    IThinkingNodeData,
    IAudioNodeData,
    IVideoNodeData,
    IToolCallNodeData,
    IBuiltInNodeData
} from '@bdky/chat-pilot-kit';
```

**Key Type Exports:**

- **Controller**: `IChatPilotKitController`
- **Emitter**: `IChatPilotKitEmitter`
- **Agent**: `AgentMessageData`, `NBaseAgentService`
- **Nodes**: `ConversationNode`, `IStreamableNode`, all content interfaces
- **Extensions**: `MessageExtensionConfig`, `MessageExtensionInstance`
- **Errors**: `IChatPilotKitError`, `ErrorCategory`, `ErrorSeverity`
- **Events**: `IConversationChangePayload`, `ITtftPayload`

## 🔗 Framework Adapters

Official framework adapters are available:

| Package | Framework | Description |
|---------|-----------|-------------|
| `@bdky/chat-pilot-kit-react` | React 19+ | React hooks, Provider, and NodeView components |
| `@bdky/chat-pilot-kit-vue3` | Vue 3 | Vue composables and components |

**React Example:**

```typescript
import {ChatPilotKitProvider, useChatPilotKit} from '@bdky/chat-pilot-kit-react';

function App() {
    return (
        <ChatPilotKitProvider agentService={MyAgentService}>
            <ChatUI />
        </ChatPilotKitProvider>
    );
}

function ChatUI() {
    const {controller, conversations} = useChatPilotKit();

    return (
        <div>
            {conversations.map(conv => (
                <div key={conv.id}>
                    {conv.nodes.map(node => (
                        <NodeView key={node.id} node={node} />
                    ))}
                </div>
            ))}
        </div>
    );
}
```

See adapter package READMEs for detailed documentation.

## 🌍 Browser Support

| Browser | Version |
|---------|---------|
| Chrome | ≥ 74 |
| Firefox | ≥ 90 |
| Safari | ≥ 14.1 |
| Edge | ≥ 79 |
| iOS Safari | ≥ 14.1 |
| Android Chrome | ≥ 74 |

## 📄 License

MIT

---

Made with ❤️ by 百度智能云客悦 Ky-FE Team
