
Research
Shai-Hulud Descends to Hades: Miasma Worm Campaign Spreads with New PyPI Wave
Socket found 37 malicious PyPI wheels that abuse Python startup hooks to launch a Bun-powered credential stealer tied to Mini Shai-Hulud/Miasma.
@bdky/chat-pilot-kit
Advanced tools
企业级 AI Agent 对话 SDK,框架无关,支持 SSE 流式对话与 WebSocket,内置 Agent 管理、消息处理管道与 NodeView 扩展机制
English | 简体中文
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.
# 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:
@bdky/chat-pilot-kit (.esm.js)@bdky/chat-pilot-kit (.cjs.js)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');
User Input → Controller.query()
→ AgentService.query() → AI Backend (SSE)
→ AgentMessageData chunks
→ Extension.canProcess() → Extension.process()
→ ConversationNode created/updated
→ Events emitted → UI subscribes & renders
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 |
Extend BaseAgentService to integrate with your AI backend.
Abstract Methods (must implement):
query(text: string): Promise<void> — Send query to AI backenddispose(): void — Cleanup resourcesProtected Methods (call from your implementation):
onData(data: AgentMessageData): void — Emit a data chunkonCompleted(): void — Signal query completiononError(error: Error): void — Report an erroronTtft(timestamp: number): void — Report time-to-first-tokenUtility Methods:
sessionId(): string — Get current session IDqueryId(): string — Get current query IDsetSessionId(id: string): void — Set session IDsetQueryId(id: string): void — Set query IDabort(): void — Abort current requestAgentMessageData Interface:
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:
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();
}
}
ConversationBean Structure:
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>:
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:
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;
}
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:
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 process incoming AgentMessageData chunks and create/update conversation nodes.
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:
import {getBuiltInExtensions} from '@bdky/chat-pilot-kit';
const extensions = getBuiltInExtensions();
Use MessageExtension.create() to define custom extensions:
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:
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:
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;
}
});
Option 1: Replace with overrideExtensions
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()
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]
});
The SDK provides a comprehensive error management system.
ErrorManager API:
const {controller, emitter} = createChatPilotKit({agentService: MyAgentService});
// Subscribe to errors
emitter.on('error', (error: IChatPilotKitError) => {
console.error('Error:', error);
});
Error Categories:
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:
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:
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:
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);
});
Export and import conversations for persistence or migration.
Export Conversations:
const snapshots = controller.exportConversations();
// Save to localStorage, database, etc.
localStorage.setItem('conversations', JSON.stringify(snapshots));
Import Conversations:
const snapshots = JSON.parse(localStorage.getItem('conversations') || '[]');
controller.importConversations(snapshots, {
position: 'prepend' // or 'replace'
});
IImportOptions:
interface IImportOptions {
position?: 'prepend' | 'replace'; // Default: 'prepend'
}
'prepend': Insert imported conversations before existing ones'replace': Replace all existing conversationsHydration Process:
When importing conversations, the SDK:
type to registered extensionsprocess() method to recreate nodeshistory_import eventComplete Round-trip Example:
// 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'
});
Nodes that implement IStreamableNode support incremental content updates.
IStreamableNode Interface:
interface IStreamableNode<TChunk = string> {
appendContent(chunk: TChunk): void;
}
Type Guard:
import {isStreamableNode} from '@bdky/chat-pilot-kit';
if (isStreamableNode(node)) {
node.appendContent('new content');
}
Built-in Streamable Nodes:
MarkdownNode — Appends markdown textThinkingBlockNode — Appends thinking process textStreamableGenericNode — Generic streamable nodeHow Streaming Works in Extensions:
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:
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."
The NodeView system enables framework-specific rendering of conversation nodes.
Core Interfaces:
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:
NodeViewFactory for each node typeconversation_change events to trigger re-rendersReference Implementation:
See @bdky/chat-pilot-kit-react for a complete React adapter implementation with:
ChatPilotKitProvider context provideruseChatPilotKit() hookThe SDK re-exports HTTP utilities from ky and @bdky/ky-sse-hook for convenience.
Sub-path Export:
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 clientHTTPError, TimeoutError — Error classesRe-exported from @bdky/ky-sse-hook:
sseHook — SSE streaming hook for kyUsage in AgentService:
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 {}
}
IOptions Interface:
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:
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:
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:
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'}}
);
The SDK is written in TypeScript and provides complete type definitions.
Type Imports:
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:
IChatPilotKitControllerIChatPilotKitEmitterAgentMessageData, NBaseAgentServiceConversationNode, IStreamableNode, all content interfacesMessageExtensionConfig, MessageExtensionInstanceIChatPilotKitError, ErrorCategory, ErrorSeverityIConversationChangePayload, ITtftPayloadOfficial 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:
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 | Version |
|---|---|
| Chrome | ≥ 74 |
| Firefox | ≥ 90 |
| Safari | ≥ 14.1 |
| Edge | ≥ 79 |
| iOS Safari | ≥ 14.1 |
| Android Chrome | ≥ 74 |
MIT
Made with ❤️ by 百度智能云客悦 Ky-FE Team
FAQs
企业级 AI Agent 对话 SDK,框架无关,支持 SSE 流式对话与 WebSocket,内置 Agent 管理、消息处理管道与 NodeView 扩展机制
The npm package @bdky/chat-pilot-kit receives a total of 9 weekly downloads. As such, @bdky/chat-pilot-kit popularity was classified as not popular.
We found that @bdky/chat-pilot-kit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
Socket found 37 malicious PyPI wheels that abuse Python startup hooks to launch a Bun-powered credential stealer tied to Mini Shai-Hulud/Miasma.

Security News
RubyGems and Bundler 4.0.13 introduced an opt-in cooldown feature that delays newly published gems during dependency resolution.

Security News
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.