@incremark/chat-core 0.4.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/dist/chunk-FOTOSDBV.js +89 -0
- package/dist/chunk-FOTOSDBV.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +198 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol/index.d.ts +287 -0
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 wangyishuai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
5
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
6
|
+
var TOOL_CALL_STATES = {
|
|
7
|
+
INPUT_STREAMING: "input-streaming",
|
|
8
|
+
INPUT_AVAILABLE: "input-available",
|
|
9
|
+
APPROVAL_REQUESTED: "approval-requested",
|
|
10
|
+
APPROVAL_RESPONDED: "approval-responded",
|
|
11
|
+
EXECUTING: "executing",
|
|
12
|
+
OUTPUT_AVAILABLE: "output-available",
|
|
13
|
+
OUTPUT_ERROR: "output-error",
|
|
14
|
+
OUTPUT_DENIED: "output-denied"
|
|
15
|
+
};
|
|
16
|
+
function isPartType(part, type) {
|
|
17
|
+
return part.type === type;
|
|
18
|
+
}
|
|
19
|
+
function isTextPart(part) {
|
|
20
|
+
return part.type === "text";
|
|
21
|
+
}
|
|
22
|
+
function isToolCallPart(part) {
|
|
23
|
+
return part.type === "tool-call";
|
|
24
|
+
}
|
|
25
|
+
function isSourcePart(part) {
|
|
26
|
+
return part.type === "source";
|
|
27
|
+
}
|
|
28
|
+
function isFilePart(part) {
|
|
29
|
+
return part.type === "file";
|
|
30
|
+
}
|
|
31
|
+
function isUIPart(part) {
|
|
32
|
+
return part.type === "ui";
|
|
33
|
+
}
|
|
34
|
+
function isReasoningPart(part) {
|
|
35
|
+
return part.type === "reasoning";
|
|
36
|
+
}
|
|
37
|
+
function createTextMessage(role, content, format = "markdown") {
|
|
38
|
+
return {
|
|
39
|
+
id: generateId(),
|
|
40
|
+
role,
|
|
41
|
+
parts: [{ type: "text", content, format }],
|
|
42
|
+
status: "success",
|
|
43
|
+
createdAt: Date.now()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createStreamingMessage(role) {
|
|
47
|
+
return {
|
|
48
|
+
id: generateId(),
|
|
49
|
+
role,
|
|
50
|
+
parts: [],
|
|
51
|
+
status: "streaming",
|
|
52
|
+
createdAt: Date.now()
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function generateId() {
|
|
56
|
+
return `msg-${Date.now()}-${nanoid()}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/protocol/transport.ts
|
|
60
|
+
var MockTransport = class {
|
|
61
|
+
constructor() {
|
|
62
|
+
__publicField(this, "aborted", false);
|
|
63
|
+
}
|
|
64
|
+
async *send(messages) {
|
|
65
|
+
messages[messages.length - 1];
|
|
66
|
+
const response = "This is a mock response from the AI assistant.";
|
|
67
|
+
const words = response.split(" ");
|
|
68
|
+
for (const word of words) {
|
|
69
|
+
if (this.aborted) {
|
|
70
|
+
yield { type: "error", error: "Aborted by user" };
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
yield {
|
|
74
|
+
type: "text",
|
|
75
|
+
content: word + " ",
|
|
76
|
+
format: "markdown"
|
|
77
|
+
};
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
79
|
+
}
|
|
80
|
+
yield { type: "done" };
|
|
81
|
+
}
|
|
82
|
+
abort() {
|
|
83
|
+
this.aborted = true;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export { MockTransport, TOOL_CALL_STATES, __publicField, createStreamingMessage, createTextMessage, isFilePart, isPartType, isReasoningPart, isSourcePart, isTextPart, isToolCallPart, isUIPart };
|
|
88
|
+
//# sourceMappingURL=chunk-FOTOSDBV.js.map
|
|
89
|
+
//# sourceMappingURL=chunk-FOTOSDBV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol/message.ts","../src/protocol/transport.ts"],"names":[],"mappings":";;;;;AA4DO,IAAM,gBAAA,GAAmB;AAAA,EAC9B,eAAA,EAAiB,iBAAA;AAAA,EACjB,eAAA,EAAiB,iBAAA;AAAA,EACjB,kBAAA,EAAoB,oBAAA;AAAA,EACpB,kBAAA,EAAoB,oBAAA;AAAA,EACpB,SAAA,EAAW,WAAA;AAAA,EACX,gBAAA,EAAkB,kBAAA;AAAA,EAClB,YAAA,EAAc,cAAA;AAAA,EACd,aAAA,EAAe;AACjB;AA4HO,SAAS,UAAA,CACd,MACA,IAAA,EAC0B;AAC1B,EAAA,OAAO,KAAK,IAAA,KAAS,IAAA;AACvB;AAKO,SAAS,WAAW,IAAA,EAAqC;AAC9D,EAAA,OAAO,KAAK,IAAA,KAAS,MAAA;AACvB;AAKO,SAAS,eAAe,IAAA,EAAyC;AACtE,EAAA,OAAO,KAAK,IAAA,KAAS,WAAA;AACvB;AAKO,SAAS,aAAa,IAAA,EAAuC;AAClE,EAAA,OAAO,KAAK,IAAA,KAAS,QAAA;AACvB;AAKO,SAAS,WAAW,IAAA,EAAqC;AAC9D,EAAA,OAAO,KAAK,IAAA,KAAS,MAAA;AACvB;AAKO,SAAS,SAAS,IAAA,EAAmC;AAC1D,EAAA,OAAO,KAAK,IAAA,KAAS,IAAA;AACvB;AAKO,SAAS,gBAAgB,IAAA,EAA0C;AACxE,EAAA,OAAO,KAAK,IAAA,KAAS,WAAA;AACvB;AAyBO,SAAS,iBAAA,CACd,IAAA,EACA,OAAA,EACA,MAAA,GAA+B,UAAA,EAClB;AACb,EAAA,OAAO;AAAA,IACL,IAAI,UAAA,EAAW;AAAA,IACf,IAAA;AAAA,IACA,OAAO,CAAC,EAAE,MAAM,MAAA,EAAQ,OAAA,EAAS,QAAQ,CAAA;AAAA,IACzC,MAAA,EAAQ,SAAA;AAAA,IACR,SAAA,EAAW,KAAK,GAAA;AAAI,GACtB;AACF;AAKO,SAAS,uBAAuB,IAAA,EAA0C;AAC/E,EAAA,OAAO;AAAA,IACL,IAAI,UAAA,EAAW;AAAA,IACf,IAAA;AAAA,IACA,OAAO,EAAC;AAAA,IACR,MAAA,EAAQ,WAAA;AAAA,IACR,SAAA,EAAW,KAAK,GAAA;AAAI,GACtB;AACF;AAKA,SAAS,UAAA,GAAqB;AAC5B,EAAA,OAAO,OAAO,IAAA,CAAK,GAAA,EAAK,CAAA,CAAA,EAAI,QAAQ,CAAA,CAAA;AACtC;;;AC1MO,IAAM,gBAAN,MAA6C;AAAA,EAA7C,WAAA,GAAA;AACL,IAAA,aAAA,CAAA,IAAA,EAAQ,SAAA,EAAU,KAAA,CAAA;AAAA,EAAA;AAAA,EAElB,OAAO,KAAK,QAAA,EAAqD;AAC/D,IAAoB,QAAA,CAAS,QAAA,CAAS,MAAA,GAAS,CAAC;AAGhD,IAAA,MAAM,QAAA,GAAW,gDAAA;AACjB,IAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA;AAEhC,IAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,MAAA,IAAI,KAAK,OAAA,EAAS;AAChB,QAAA,MAAM,EAAE,IAAA,EAAM,OAAA,EAAS,KAAA,EAAO,iBAAA,EAAkB;AAChD,QAAA;AAAA,MACF;AAEA,MAAA,MAAM;AAAA,QACJ,IAAA,EAAM,MAAA;AAAA,QACN,SAAS,IAAA,GAAO,GAAA;AAAA,QAChB,MAAA,EAAQ;AAAA,OACV;AAGA,MAAA,MAAM,IAAI,OAAA,CAAQ,CAAA,OAAA,KAAW,UAAA,CAAW,OAAA,EAAS,EAAE,CAAC,CAAA;AAAA,IACtD;AAEA,IAAA,MAAM,EAAE,MAAM,MAAA,EAAO;AAAA,EACvB;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AACF","file":"chunk-FOTOSDBV.js","sourcesContent":["import { nanoid } from 'nanoid'\n\n/**\n * Message protocol types for Incremark Chat-UI\n * Based on Vercel AI SDK & TanStack AI's parts-based message structure\n *\n * Extensibility: Users can extend MessagePart via module augmentation:\n * ```ts\n * declare module '@incremark/chat-core' {\n * interface CustomParts {\n * 'my-custom': { type: 'my-custom'; data: string };\n * }\n * }\n * ```\n */\n\n// ============================================================================\n// Base Types\n// ============================================================================\n\n/**\n * Supported message roles\n */\nexport type MessageRole = 'user' | 'assistant' | 'system' | 'agent';\n\n/**\n * Message status during lifecycle\n */\nexport type MessageStatus = 'pending' | 'streaming' | 'success' | 'error';\n\n// ============================================================================\n// Part Types\n// ============================================================================\n\n/**\n * Base part interface - all parts must have a type\n */\nexport interface BasePart {\n type: string;\n}\n\n/**\n * Text content part\n */\nexport interface TextPart extends BasePart {\n type: 'text';\n content: string;\n format?: 'markdown' | 'plain';\n}\n\n/**\n * Tool call state - 用户可自定义任意状态\n * 提供预定义常量供参考,但不限制用户使用其他值\n */\nexport type ToolCallState = string;\n\n/**\n * 预定义的工具调用状态常量(参考 Vercel AI SDK)\n * 用户可以使用这些常量,也可以使用自定义状态\n */\nexport const TOOL_CALL_STATES = {\n INPUT_STREAMING: 'input-streaming',\n INPUT_AVAILABLE: 'input-available',\n APPROVAL_REQUESTED: 'approval-requested',\n APPROVAL_RESPONDED: 'approval-responded',\n EXECUTING: 'executing',\n OUTPUT_AVAILABLE: 'output-available',\n OUTPUT_ERROR: 'output-error',\n OUTPUT_DENIED: 'output-denied'\n} as const;\n\n/**\n * Tool call part (when AI wants to use a tool)\n */\nexport interface ToolCallPart extends BasePart {\n type: 'tool-call';\n toolCallId: string;\n toolName: string;\n args: Record<string, unknown>;\n state: ToolCallState;\n /** 工具执行结果 */\n output?: unknown;\n /** 错误信息(当 state 为 output-error 时) */\n error?: string;\n}\n\n/**\n * Source reference type\n */\nexport type SourceType = 'url' | 'document';\n\n/**\n * Source part (reference to external content)\n */\nexport interface SourcePart extends BasePart {\n type: 'source';\n sourceId: string;\n sourceType: SourceType;\n url?: string;\n title?: string;\n /** 文档类型(当 sourceType 为 document 时) */\n mediaType?: string;\n}\n\n/**\n * File part (generated or uploaded file)\n */\nexport interface FilePart extends BasePart {\n type: 'file';\n fileId: string;\n /** base64 编码的文件内容或 URL */\n data: string;\n mediaType: string;\n filename?: string;\n}\n\n/**\n * UI component part (for A2UI - Agent to UI)\n */\nexport interface UIPart extends BasePart {\n type: 'ui';\n component: string;\n props: Record<string, unknown>;\n}\n\n/**\n * Reasoning/thought part (for chain-of-thought display)\n */\nexport interface ReasoningPart extends BasePart {\n type: 'reasoning';\n content: string;\n /** 推理状态 */\n status?: 'thinking' | 'completed' | 'error';\n /** 思考开始时间(毫秒时间戳) */\n startTime?: number;\n /** 思考结束时间(毫秒时间戳) */\n endTime?: number;\n /** 思考标题 */\n title?: string;\n}\n\n// ============================================================================\n// Extensibility Support\n// ============================================================================\n\n/**\n * Built-in parts registry\n */\nexport interface BuiltinParts {\n text: TextPart;\n 'tool-call': ToolCallPart;\n source: SourcePart;\n file: FilePart;\n ui: UIPart;\n reasoning: ReasoningPart;\n}\n\n/**\n * Custom parts registry - extend via module augmentation\n * @example\n * ```ts\n * declare module '@incremark/chat-core' {\n * interface CustomParts {\n * 'weather-card': { type: 'weather-card'; city: string; temp: number };\n * }\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-object-type\nexport interface CustomParts {}\n\n/**\n * All registered parts\n */\nexport type PartsRegistry = BuiltinParts & CustomParts;\n\n/**\n * Union type for all message parts (built-in + custom)\n */\nexport type MessagePart = PartsRegistry[keyof PartsRegistry];\n\n/**\n * Get part type by name\n */\nexport type PartByType<T extends keyof PartsRegistry> = PartsRegistry[T];\n\n// ============================================================================\n// Type Guards\n// ============================================================================\n\n/**\n * Generic type guard for any part type\n */\nexport function isPartType<T extends keyof PartsRegistry>(\n part: MessagePart,\n type: T\n): part is PartsRegistry[T] {\n return part.type === type;\n}\n\n/**\n * Type guard for TextPart\n */\nexport function isTextPart(part: MessagePart): part is TextPart {\n return part.type === 'text';\n}\n\n/**\n * Type guard for ToolCallPart\n */\nexport function isToolCallPart(part: MessagePart): part is ToolCallPart {\n return part.type === 'tool-call';\n}\n\n/**\n * Type guard for SourcePart\n */\nexport function isSourcePart(part: MessagePart): part is SourcePart {\n return part.type === 'source';\n}\n\n/**\n * Type guard for FilePart\n */\nexport function isFilePart(part: MessagePart): part is FilePart {\n return part.type === 'file';\n}\n\n/**\n * Type guard for UIPart\n */\nexport function isUIPart(part: MessagePart): part is UIPart {\n return part.type === 'ui';\n}\n\n/**\n * Type guard for ReasoningPart\n */\nexport function isReasoningPart(part: MessagePart): part is ReasoningPart {\n return part.type === 'reasoning';\n}\n\n// ============================================================================\n// Message Types\n// ============================================================================\n\n/**\n * Complete message structure\n */\nexport interface ChatMessage {\n id: string;\n role: MessageRole;\n parts: MessagePart[];\n status: MessageStatus;\n createdAt: number;\n metadata?: Record<string, unknown>;\n}\n\n// ============================================================================\n// Factory Functions\n// ============================================================================\n\n/**\n * Create a new text message\n */\nexport function createTextMessage(\n role: MessageRole,\n content: string,\n format: 'markdown' | 'plain' = 'markdown'\n): ChatMessage {\n return {\n id: generateId(),\n role,\n parts: [{ type: 'text', content, format }],\n status: 'success',\n createdAt: Date.now()\n };\n}\n\n/**\n * Create a new empty streaming message\n */\nexport function createStreamingMessage(role: 'assistant' | 'agent'): ChatMessage {\n return {\n id: generateId(),\n role,\n parts: [],\n status: 'streaming',\n createdAt: Date.now()\n };\n}\n\n/**\n * Simple ID generator (can be replaced with more sophisticated one)\n */\nfunction generateId(): string {\n return `msg-${Date.now()}-${nanoid()}`;\n}\n","/**\n * Transport layer protocol for chat communication\n * Inspired by Vercel AI SDK 5.0+ transport architecture\n */\n\nimport type { ChatMessage } from './message.js';\n\n/**\n * Stream part - chunks of data received during streaming\n */\nexport type StreamPart =\n | TextStreamPart\n | ToolCallStreamPart\n | UIStreamPart\n | ReasoningStreamPart\n | DoneStreamPart\n | ErrorStreamPart;\n\n/**\n * Text content stream part\n */\nexport interface TextStreamPart {\n type: 'text';\n content: string;\n format?: 'markdown' | 'plain';\n}\n\n/**\n * Tool call stream part\n */\nexport interface ToolCallStreamPart {\n type: 'tool-call';\n toolCallId?: string;\n toolName: string;\n args: Record<string, any>;\n}\n\n/**\n * UI component stream part\n */\nexport interface UIStreamPart {\n type: 'ui';\n component: string;\n props: Record<string, any>;\n}\n\n/**\n * Reasoning stream part\n */\nexport interface ReasoningStreamPart {\n type: 'reasoning';\n content: string;\n}\n\n/**\n * Stream completion marker\n */\nexport interface DoneStreamPart {\n type: 'done';\n}\n\n/**\n * Error stream part\n */\nexport interface ErrorStreamPart {\n type: 'error';\n error: string;\n}\n\n/**\n * Transport interface for sending messages and receiving stream\n * This is the key abstraction that allows different implementations:\n * - HTTP streaming\n * - WebSocket\n * - Server-Sent Events (SSE)\n * - Mock for testing\n */\nexport interface ChatTransport {\n /**\n * Send messages and return async generator of stream parts\n *\n * @param messages - Full conversation history (or context window)\n * @returns Async generator yielding stream parts\n */\n send(messages: ChatMessage[]): AsyncGenerator<StreamPart>;\n\n /**\n * Abort the current stream (optional)\n */\n abort?(): void;\n}\n\n/**\n * Simple mock transport for testing\n */\nexport class MockTransport implements ChatTransport {\n private aborted = false;\n\n async *send(messages: ChatMessage[]): AsyncGenerator<StreamPart> {\n const lastMessage = messages[messages.length - 1];\n\n // Simulate streaming response\n const response = \"This is a mock response from the AI assistant.\";\n const words = response.split(' ');\n\n for (const word of words) {\n if (this.aborted) {\n yield { type: 'error', error: 'Aborted by user' };\n return;\n }\n\n yield {\n type: 'text',\n content: word + ' ',\n format: 'markdown'\n };\n\n // Simulate network delay\n await new Promise(resolve => setTimeout(resolve, 50));\n }\n\n yield { type: 'done' };\n }\n\n abort(): void {\n this.aborted = true;\n }\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { ChatMessage, MessageStatus, ChatTransport } from './protocol/index.js';
|
|
2
|
+
export { BasePart, BuiltinParts, CustomParts, DoneStreamPart, ErrorStreamPart, FilePart, MessagePart, MessageRole, MockTransport, PartByType, PartsRegistry, ReasoningPart, ReasoningStreamPart, SourcePart, SourceType, StreamPart, TOOL_CALL_STATES, TextPart, TextStreamPart, ToolCallPart, ToolCallState, ToolCallStreamPart, UIPart, UIStreamPart, createStreamingMessage, createTextMessage, isFilePart, isPartType, isReasoningPart, isSourcePart, isTextPart, isToolCallPart, isUIPart } from './protocol/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* ChatEngine - Core engine for managing chat state and streaming
|
|
6
|
+
* Framework-agnostic implementation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* State update listener type
|
|
11
|
+
*/
|
|
12
|
+
type StateListener = (state: ChatEngineState) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Engine state snapshot
|
|
15
|
+
*/
|
|
16
|
+
interface ChatEngineState {
|
|
17
|
+
messages: ChatMessage[];
|
|
18
|
+
status: MessageStatus;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* ID generator function type
|
|
23
|
+
*/
|
|
24
|
+
type IDGenerator = () => string;
|
|
25
|
+
/**
|
|
26
|
+
* Default ID generator using nanoid
|
|
27
|
+
*/
|
|
28
|
+
declare const defaultIDGenerator: IDGenerator;
|
|
29
|
+
/**
|
|
30
|
+
* Configuration for ChatEngine
|
|
31
|
+
*/
|
|
32
|
+
interface ChatEngineConfig {
|
|
33
|
+
transport: ChatTransport;
|
|
34
|
+
initialMessages?: ChatMessage[];
|
|
35
|
+
idGenerator?: IDGenerator;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Core engine for chat functionality
|
|
39
|
+
* - Manages message history
|
|
40
|
+
* - Handles streaming responses
|
|
41
|
+
* - Emits state updates
|
|
42
|
+
*/
|
|
43
|
+
declare class ChatEngine {
|
|
44
|
+
private messages;
|
|
45
|
+
private currentMessage;
|
|
46
|
+
private status;
|
|
47
|
+
private error;
|
|
48
|
+
private listeners;
|
|
49
|
+
private transport;
|
|
50
|
+
private abortController;
|
|
51
|
+
private idGenerator;
|
|
52
|
+
constructor(config: ChatEngineConfig);
|
|
53
|
+
/**
|
|
54
|
+
* Get current state (immutable snapshot)
|
|
55
|
+
*/
|
|
56
|
+
getState(): ChatEngineState;
|
|
57
|
+
/**
|
|
58
|
+
* Subscribe to state updates
|
|
59
|
+
* @returns Unsubscribe function
|
|
60
|
+
*/
|
|
61
|
+
subscribe(listener: StateListener): () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Send a text message and process the streaming response
|
|
64
|
+
*/
|
|
65
|
+
sendMessage(text: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Abort current streaming
|
|
68
|
+
*/
|
|
69
|
+
abort(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Process streaming response from transport
|
|
72
|
+
*/
|
|
73
|
+
private processStream;
|
|
74
|
+
/**
|
|
75
|
+
* Handle individual stream part
|
|
76
|
+
*/
|
|
77
|
+
private handleStreamPart;
|
|
78
|
+
/**
|
|
79
|
+
* Handle text stream part - incrementally update last text part or create new one
|
|
80
|
+
*/
|
|
81
|
+
private handleTextPart;
|
|
82
|
+
/**
|
|
83
|
+
* Handle tool call part
|
|
84
|
+
*/
|
|
85
|
+
private handleToolCallPart;
|
|
86
|
+
/**
|
|
87
|
+
* Handle UI part
|
|
88
|
+
*/
|
|
89
|
+
private handleUIPart;
|
|
90
|
+
/**
|
|
91
|
+
* Handle reasoning part
|
|
92
|
+
*/
|
|
93
|
+
private handleReasoningPart;
|
|
94
|
+
/**
|
|
95
|
+
* Emit state update to all listeners
|
|
96
|
+
*/
|
|
97
|
+
private emitUpdate;
|
|
98
|
+
/**
|
|
99
|
+
* Generate unique ID using configured generator
|
|
100
|
+
*/
|
|
101
|
+
private generateId;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { ChatEngine, type ChatEngineConfig, type ChatEngineState, ChatMessage, ChatTransport, type IDGenerator, MessageStatus, type StateListener, defaultIDGenerator };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { __publicField } from './chunk-FOTOSDBV.js';
|
|
2
|
+
export { MockTransport, TOOL_CALL_STATES, createStreamingMessage, createTextMessage, isFilePart, isPartType, isReasoningPart, isSourcePart, isTextPart, isToolCallPart, isUIPart } from './chunk-FOTOSDBV.js';
|
|
3
|
+
import { nanoid } from 'nanoid';
|
|
4
|
+
|
|
5
|
+
var defaultIDGenerator = () => `msg_${nanoid()}`;
|
|
6
|
+
var ChatEngine = class {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
__publicField(this, "messages", []);
|
|
9
|
+
__publicField(this, "currentMessage", null);
|
|
10
|
+
__publicField(this, "status", "success");
|
|
11
|
+
__publicField(this, "error");
|
|
12
|
+
__publicField(this, "listeners", /* @__PURE__ */ new Set());
|
|
13
|
+
__publicField(this, "transport");
|
|
14
|
+
__publicField(this, "abortController", null);
|
|
15
|
+
__publicField(this, "idGenerator");
|
|
16
|
+
this.transport = config.transport;
|
|
17
|
+
this.idGenerator = config.idGenerator || defaultIDGenerator;
|
|
18
|
+
if (config.initialMessages) {
|
|
19
|
+
this.messages = [...config.initialMessages];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get current state (immutable snapshot)
|
|
24
|
+
*/
|
|
25
|
+
getState() {
|
|
26
|
+
return {
|
|
27
|
+
messages: [...this.messages],
|
|
28
|
+
status: this.status,
|
|
29
|
+
error: this.error
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Subscribe to state updates
|
|
34
|
+
* @returns Unsubscribe function
|
|
35
|
+
*/
|
|
36
|
+
subscribe(listener) {
|
|
37
|
+
this.listeners.add(listener);
|
|
38
|
+
return () => this.listeners.delete(listener);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Send a text message and process the streaming response
|
|
42
|
+
*/
|
|
43
|
+
async sendMessage(text) {
|
|
44
|
+
const userMessage = {
|
|
45
|
+
id: this.generateId(),
|
|
46
|
+
role: "user",
|
|
47
|
+
parts: [{ type: "text", content: text, format: "plain" }],
|
|
48
|
+
status: "success",
|
|
49
|
+
createdAt: Date.now()
|
|
50
|
+
};
|
|
51
|
+
this.messages.push(userMessage);
|
|
52
|
+
this.emitUpdate();
|
|
53
|
+
const assistantMessage = {
|
|
54
|
+
id: this.generateId(),
|
|
55
|
+
role: "assistant",
|
|
56
|
+
parts: [],
|
|
57
|
+
status: "streaming",
|
|
58
|
+
createdAt: Date.now()
|
|
59
|
+
};
|
|
60
|
+
this.messages.push(assistantMessage);
|
|
61
|
+
this.currentMessage = assistantMessage;
|
|
62
|
+
this.status = "streaming";
|
|
63
|
+
this.error = void 0;
|
|
64
|
+
this.emitUpdate();
|
|
65
|
+
try {
|
|
66
|
+
await this.processStream();
|
|
67
|
+
assistantMessage.status = "success";
|
|
68
|
+
this.status = "success";
|
|
69
|
+
} catch (err) {
|
|
70
|
+
assistantMessage.status = "error";
|
|
71
|
+
this.status = "error";
|
|
72
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
73
|
+
} finally {
|
|
74
|
+
this.currentMessage = null;
|
|
75
|
+
this.emitUpdate();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Abort current streaming
|
|
80
|
+
*/
|
|
81
|
+
abort() {
|
|
82
|
+
if (this.abortController) {
|
|
83
|
+
this.abortController.abort();
|
|
84
|
+
this.abortController = null;
|
|
85
|
+
}
|
|
86
|
+
this.transport.abort?.();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Process streaming response from transport
|
|
90
|
+
*/
|
|
91
|
+
async processStream() {
|
|
92
|
+
this.abortController = new AbortController();
|
|
93
|
+
for await (const part of this.transport.send(this.messages)) {
|
|
94
|
+
if (this.abortController.signal.aborted) {
|
|
95
|
+
throw new Error("Aborted by user");
|
|
96
|
+
}
|
|
97
|
+
this.handleStreamPart(part);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Handle individual stream part
|
|
102
|
+
*/
|
|
103
|
+
handleStreamPart(part) {
|
|
104
|
+
if (!this.currentMessage) return;
|
|
105
|
+
switch (part.type) {
|
|
106
|
+
case "text":
|
|
107
|
+
this.handleTextPart(part);
|
|
108
|
+
break;
|
|
109
|
+
case "tool-call":
|
|
110
|
+
this.handleToolCallPart(part);
|
|
111
|
+
break;
|
|
112
|
+
case "ui":
|
|
113
|
+
this.handleUIPart(part);
|
|
114
|
+
break;
|
|
115
|
+
case "reasoning":
|
|
116
|
+
this.handleReasoningPart(part);
|
|
117
|
+
break;
|
|
118
|
+
case "done":
|
|
119
|
+
break;
|
|
120
|
+
case "error":
|
|
121
|
+
throw new Error(part.error);
|
|
122
|
+
}
|
|
123
|
+
this.emitUpdate();
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Handle text stream part - incrementally update last text part or create new one
|
|
127
|
+
*/
|
|
128
|
+
handleTextPart(part) {
|
|
129
|
+
if (!this.currentMessage) return;
|
|
130
|
+
const lastPart = this.currentMessage.parts[this.currentMessage.parts.length - 1];
|
|
131
|
+
if (lastPart && lastPart.type === "text") {
|
|
132
|
+
lastPart.content += part.content;
|
|
133
|
+
} else {
|
|
134
|
+
this.currentMessage.parts.push({
|
|
135
|
+
type: "text",
|
|
136
|
+
content: part.content,
|
|
137
|
+
format: part.format || "markdown"
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Handle tool call part
|
|
143
|
+
*/
|
|
144
|
+
handleToolCallPart(part) {
|
|
145
|
+
if (!this.currentMessage) return;
|
|
146
|
+
this.currentMessage.parts.push({
|
|
147
|
+
type: "tool-call",
|
|
148
|
+
toolCallId: part.toolCallId || this.generateId(),
|
|
149
|
+
toolName: part.toolName,
|
|
150
|
+
args: part.args,
|
|
151
|
+
state: "input-available"
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Handle UI part
|
|
156
|
+
*/
|
|
157
|
+
handleUIPart(part) {
|
|
158
|
+
if (!this.currentMessage) return;
|
|
159
|
+
this.currentMessage.parts.push({
|
|
160
|
+
type: "ui",
|
|
161
|
+
component: part.component,
|
|
162
|
+
props: part.props
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Handle reasoning part
|
|
167
|
+
*/
|
|
168
|
+
handleReasoningPart(part) {
|
|
169
|
+
if (!this.currentMessage) return;
|
|
170
|
+
this.currentMessage.parts.push({
|
|
171
|
+
type: "reasoning",
|
|
172
|
+
content: part.content
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Emit state update to all listeners
|
|
177
|
+
*/
|
|
178
|
+
emitUpdate() {
|
|
179
|
+
const state = this.getState();
|
|
180
|
+
this.listeners.forEach((listener) => {
|
|
181
|
+
try {
|
|
182
|
+
listener(state);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error("Error in state listener:", err);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Generate unique ID using configured generator
|
|
190
|
+
*/
|
|
191
|
+
generateId() {
|
|
192
|
+
return this.idGenerator();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export { ChatEngine, defaultIDGenerator };
|
|
197
|
+
//# sourceMappingURL=index.js.map
|
|
198
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engine/chat-engine.ts"],"names":[],"mappings":";;;;AAwCO,IAAM,kBAAA,GAAkC,MAAM,CAAA,IAAA,EAAO,MAAA,EAAQ,CAAA;AAiB7D,IAAM,aAAN,MAAiB;AAAA,EAUtB,YAAY,MAAA,EAA0B;AATtC,IAAA,aAAA,CAAA,IAAA,EAAQ,YAA0B,EAAC,CAAA;AACnC,IAAA,aAAA,CAAA,IAAA,EAAQ,gBAAA,EAAqC,IAAA,CAAA;AAC7C,IAAA,aAAA,CAAA,IAAA,EAAQ,QAAA,EAAwB,SAAA,CAAA;AAChC,IAAA,aAAA,CAAA,IAAA,EAAQ,OAAA,CAAA;AACR,IAAA,aAAA,CAAA,IAAA,EAAQ,WAAA,sBAAoC,GAAA,EAAI,CAAA;AAChD,IAAA,aAAA,CAAA,IAAA,EAAQ,WAAA,CAAA;AACR,IAAA,aAAA,CAAA,IAAA,EAAQ,iBAAA,EAA0C,IAAA,CAAA;AAClD,IAAA,aAAA,CAAA,IAAA,EAAQ,aAAA,CAAA;AAGN,IAAA,IAAA,CAAK,YAAY,MAAA,CAAO,SAAA;AACxB,IAAA,IAAA,CAAK,WAAA,GAAc,OAAO,WAAA,IAAe,kBAAA;AACzC,IAAA,IAAI,OAAO,eAAA,EAAiB;AAC1B,MAAA,IAAA,CAAK,QAAA,GAAW,CAAC,GAAG,MAAA,CAAO,eAAe,CAAA;AAAA,IAC5C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAA,GAA4B;AAC1B,IAAA,OAAO;AAAA,MACL,QAAA,EAAU,CAAC,GAAG,IAAA,CAAK,QAAQ,CAAA;AAAA,MAC3B,QAAQ,IAAA,CAAK,MAAA;AAAA,MACb,OAAO,IAAA,CAAK;AAAA,KACd;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAU,QAAA,EAAqC;AAC7C,IAAA,IAAA,CAAK,SAAA,CAAU,IAAI,QAAQ,CAAA;AAC3B,IAAA,OAAO,MAAM,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,QAAQ,CAAA;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,IAAA,EAA6B;AAE7C,IAAA,MAAM,WAAA,GAA2B;AAAA,MAC/B,EAAA,EAAI,KAAK,UAAA,EAAW;AAAA,MACpB,IAAA,EAAM,MAAA;AAAA,MACN,KAAA,EAAO,CAAC,EAAE,IAAA,EAAM,QAAQ,OAAA,EAAS,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,CAAA;AAAA,MACxD,MAAA,EAAQ,SAAA;AAAA,MACR,SAAA,EAAW,KAAK,GAAA;AAAI,KACtB;AAEA,IAAA,IAAA,CAAK,QAAA,CAAS,KAAK,WAAW,CAAA;AAC9B,IAAA,IAAA,CAAK,UAAA,EAAW;AAGhB,IAAA,MAAM,gBAAA,GAAgC;AAAA,MACpC,EAAA,EAAI,KAAK,UAAA,EAAW;AAAA,MACpB,IAAA,EAAM,WAAA;AAAA,MACN,OAAO,EAAC;AAAA,MACR,MAAA,EAAQ,WAAA;AAAA,MACR,SAAA,EAAW,KAAK,GAAA;AAAI,KACtB;AAEA,IAAA,IAAA,CAAK,QAAA,CAAS,KAAK,gBAAgB,CAAA;AACnC,IAAA,IAAA,CAAK,cAAA,GAAiB,gBAAA;AACtB,IAAA,IAAA,CAAK,MAAA,GAAS,WAAA;AACd,IAAA,IAAA,CAAK,KAAA,GAAQ,MAAA;AACb,IAAA,IAAA,CAAK,UAAA,EAAW;AAGhB,IAAA,IAAI;AACF,MAAA,MAAM,KAAK,aAAA,EAAc;AACzB,MAAA,gBAAA,CAAiB,MAAA,GAAS,SAAA;AAC1B,MAAA,IAAA,CAAK,MAAA,GAAS,SAAA;AAAA,IAChB,SAAS,GAAA,EAAK;AACZ,MAAA,gBAAA,CAAiB,MAAA,GAAS,OAAA;AAC1B,MAAA,IAAA,CAAK,MAAA,GAAS,OAAA;AACd,MAAA,IAAA,CAAK,QAAQ,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAAA,IAC9D,CAAA,SAAE;AACA,MAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AACtB,MAAA,IAAA,CAAK,UAAA,EAAW;AAAA,IAClB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAA,GAAc;AACZ,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA,IAAA,CAAK,gBAAgB,KAAA,EAAM;AAC3B,MAAA,IAAA,CAAK,eAAA,GAAkB,IAAA;AAAA,IACzB;AACA,IAAA,IAAA,CAAK,UAAU,KAAA,IAAQ;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,aAAA,GAA+B;AAC3C,IAAA,IAAA,CAAK,eAAA,GAAkB,IAAI,eAAA,EAAgB;AAE3C,IAAA,WAAA,MAAiB,QAAQ,IAAA,CAAK,SAAA,CAAU,IAAA,CAAK,IAAA,CAAK,QAAQ,CAAA,EAAG;AAC3D,MAAA,IAAI,IAAA,CAAK,eAAA,CAAgB,MAAA,CAAO,OAAA,EAAS;AACvC,QAAA,MAAM,IAAI,MAAM,iBAAiB,CAAA;AAAA,MACnC;AAEA,MAAA,IAAA,CAAK,iBAAiB,IAAI,CAAA;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAiB,IAAA,EAAwB;AAC/C,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAE1B,IAAA,QAAQ,KAAK,IAAA;AAAM,MACjB,KAAK,MAAA;AACH,QAAA,IAAA,CAAK,eAAe,IAAI,CAAA;AACxB,QAAA;AAAA,MAEF,KAAK,WAAA;AACH,QAAA,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAC5B,QAAA;AAAA,MAEF,KAAK,IAAA;AACH,QAAA,IAAA,CAAK,aAAa,IAAI,CAAA;AACtB,QAAA;AAAA,MAEF,KAAK,WAAA;AACH,QAAA,IAAA,CAAK,oBAAoB,IAAI,CAAA;AAC7B,QAAA;AAAA,MAEF,KAAK,MAAA;AAEH,QAAA;AAAA,MAEF,KAAK,OAAA;AACH,QAAA,MAAM,IAAI,KAAA,CAAM,IAAA,CAAK,KAAK,CAAA;AAAA;AAG9B,IAAA,IAAA,CAAK,UAAA,EAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAe,IAAA,EAA2C;AAChE,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAE1B,IAAA,MAAM,QAAA,GAAW,KAAK,cAAA,CAAe,KAAA,CAAM,KAAK,cAAA,CAAe,KAAA,CAAM,SAAS,CAAC,CAAA;AAG/E,IAAA,IAAI,QAAA,IAAY,QAAA,CAAS,IAAA,KAAS,MAAA,EAAQ;AACxC,MAAA,QAAA,CAAS,WAAW,IAAA,CAAK,OAAA;AAAA,IAC3B,CAAA,MAAO;AAEL,MAAA,IAAA,CAAK,cAAA,CAAe,MAAM,IAAA,CAAK;AAAA,QAC7B,IAAA,EAAM,MAAA;AAAA,QACN,SAAS,IAAA,CAAK,OAAA;AAAA,QACd,MAAA,EAAQ,KAAK,MAAA,IAAU;AAAA,OACxB,CAAA;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,mBAAmB,IAAA,EAAgD;AACzE,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAE1B,IAAA,IAAA,CAAK,cAAA,CAAe,MAAM,IAAA,CAAK;AAAA,MAC7B,IAAA,EAAM,WAAA;AAAA,MACN,UAAA,EAAY,IAAA,CAAK,UAAA,IAAc,IAAA,CAAK,UAAA,EAAW;AAAA,MAC/C,UAAU,IAAA,CAAK,QAAA;AAAA,MACf,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,KAAA,EAAO;AAAA,KACR,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,aAAa,IAAA,EAAyC;AAC5D,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAE1B,IAAA,IAAA,CAAK,cAAA,CAAe,MAAM,IAAA,CAAK;AAAA,MAC7B,IAAA,EAAM,IAAA;AAAA,MACN,WAAW,IAAA,CAAK,SAAA;AAAA,MAChB,OAAO,IAAA,CAAK;AAAA,KACb,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,oBAAoB,IAAA,EAAgD;AAC1E,IAAA,IAAI,CAAC,KAAK,cAAA,EAAgB;AAE1B,IAAA,IAAA,CAAK,cAAA,CAAe,MAAM,IAAA,CAAK;AAAA,MAC7B,IAAA,EAAM,WAAA;AAAA,MACN,SAAS,IAAA,CAAK;AAAA,KACf,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,UAAA,GAAmB;AACzB,IAAA,MAAM,KAAA,GAAQ,KAAK,QAAA,EAAS;AAC5B,IAAA,IAAA,CAAK,SAAA,CAAU,QAAQ,CAAA,QAAA,KAAY;AACjC,MAAA,IAAI;AACF,QAAA,QAAA,CAAS,KAAK,CAAA;AAAA,MAChB,SAAS,GAAA,EAAK;AACZ,QAAA,OAAA,CAAQ,KAAA,CAAM,4BAA4B,GAAG,CAAA;AAAA,MAC/C;AAAA,IACF,CAAC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKQ,UAAA,GAAqB;AAC3B,IAAA,OAAO,KAAK,WAAA,EAAY;AAAA,EAC1B;AACF","file":"index.js","sourcesContent":["/**\n * ChatEngine - Core engine for managing chat state and streaming\n * Framework-agnostic implementation\n */\n\nimport { nanoid } from 'nanoid';\nimport type {\n ChatMessage,\n MessagePart,\n MessageStatus,\n StreamPart,\n ChatTransport,\n TextPart,\n ToolCallPart,\n UIPart,\n ReasoningPart\n} from '../protocol/index.js';\n\n/**\n * State update listener type\n */\nexport type StateListener = (state: ChatEngineState) => void;\n\n/**\n * Engine state snapshot\n */\nexport interface ChatEngineState {\n messages: ChatMessage[];\n status: MessageStatus;\n error?: string;\n}\n\n/**\n * ID generator function type\n */\nexport type IDGenerator = () => string;\n\n/**\n * Default ID generator using nanoid\n */\nexport const defaultIDGenerator: IDGenerator = () => `msg_${nanoid()}`;\n\n/**\n * Configuration for ChatEngine\n */\nexport interface ChatEngineConfig {\n transport: ChatTransport;\n initialMessages?: ChatMessage[];\n idGenerator?: IDGenerator;\n}\n\n/**\n * Core engine for chat functionality\n * - Manages message history\n * - Handles streaming responses\n * - Emits state updates\n */\nexport class ChatEngine {\n private messages: ChatMessage[] = [];\n private currentMessage: ChatMessage | null = null;\n private status: MessageStatus = 'success';\n private error: string | undefined;\n private listeners: Set<StateListener> = new Set();\n private transport: ChatTransport;\n private abortController: AbortController | null = null;\n private idGenerator: IDGenerator;\n\n constructor(config: ChatEngineConfig) {\n this.transport = config.transport;\n this.idGenerator = config.idGenerator || defaultIDGenerator;\n if (config.initialMessages) {\n this.messages = [...config.initialMessages];\n }\n }\n\n /**\n * Get current state (immutable snapshot)\n */\n getState(): ChatEngineState {\n return {\n messages: [...this.messages],\n status: this.status,\n error: this.error\n };\n }\n\n /**\n * Subscribe to state updates\n * @returns Unsubscribe function\n */\n subscribe(listener: StateListener): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n /**\n * Send a text message and process the streaming response\n */\n async sendMessage(text: string): Promise<void> {\n // Create user message\n const userMessage: ChatMessage = {\n id: this.generateId(),\n role: 'user',\n parts: [{ type: 'text', content: text, format: 'plain' }],\n status: 'success',\n createdAt: Date.now()\n };\n\n this.messages.push(userMessage);\n this.emitUpdate();\n\n // Create assistant message placeholder\n const assistantMessage: ChatMessage = {\n id: this.generateId(),\n role: 'assistant',\n parts: [],\n status: 'streaming',\n createdAt: Date.now()\n };\n\n this.messages.push(assistantMessage);\n this.currentMessage = assistantMessage;\n this.status = 'streaming';\n this.error = undefined;\n this.emitUpdate();\n\n // Process stream\n try {\n await this.processStream();\n assistantMessage.status = 'success';\n this.status = 'success';\n } catch (err) {\n assistantMessage.status = 'error';\n this.status = 'error';\n this.error = err instanceof Error ? err.message : String(err);\n } finally {\n this.currentMessage = null;\n this.emitUpdate();\n }\n }\n\n /**\n * Abort current streaming\n */\n abort(): void {\n if (this.abortController) {\n this.abortController.abort();\n this.abortController = null;\n }\n this.transport.abort?.();\n }\n\n /**\n * Process streaming response from transport\n */\n private async processStream(): Promise<void> {\n this.abortController = new AbortController();\n\n for await (const part of this.transport.send(this.messages)) {\n if (this.abortController.signal.aborted) {\n throw new Error('Aborted by user');\n }\n\n this.handleStreamPart(part);\n }\n }\n\n /**\n * Handle individual stream part\n */\n private handleStreamPart(part: StreamPart): void {\n if (!this.currentMessage) return;\n\n switch (part.type) {\n case 'text':\n this.handleTextPart(part);\n break;\n\n case 'tool-call':\n this.handleToolCallPart(part);\n break;\n\n case 'ui':\n this.handleUIPart(part);\n break;\n\n case 'reasoning':\n this.handleReasoningPart(part);\n break;\n\n case 'done':\n // Stream completed\n break;\n\n case 'error':\n throw new Error(part.error);\n }\n\n this.emitUpdate();\n }\n\n /**\n * Handle text stream part - incrementally update last text part or create new one\n */\n private handleTextPart(part: StreamPart & { type: 'text' }): void {\n if (!this.currentMessage) return;\n\n const lastPart = this.currentMessage.parts[this.currentMessage.parts.length - 1];\n\n // If last part is text, append to it (incremental update)\n if (lastPart && lastPart.type === 'text') {\n lastPart.content += part.content;\n } else {\n // Otherwise create new text part\n this.currentMessage.parts.push({\n type: 'text',\n content: part.content,\n format: part.format || 'markdown'\n });\n }\n }\n\n /**\n * Handle tool call part\n */\n private handleToolCallPart(part: StreamPart & { type: 'tool-call' }): void {\n if (!this.currentMessage) return;\n\n this.currentMessage.parts.push({\n type: 'tool-call',\n toolCallId: part.toolCallId || this.generateId(),\n toolName: part.toolName,\n args: part.args,\n state: 'input-available'\n });\n }\n\n /**\n * Handle UI part\n */\n private handleUIPart(part: StreamPart & { type: 'ui' }): void {\n if (!this.currentMessage) return;\n\n this.currentMessage.parts.push({\n type: 'ui',\n component: part.component,\n props: part.props\n });\n }\n\n /**\n * Handle reasoning part\n */\n private handleReasoningPart(part: StreamPart & { type: 'reasoning' }): void {\n if (!this.currentMessage) return;\n\n this.currentMessage.parts.push({\n type: 'reasoning',\n content: part.content\n });\n }\n\n /**\n * Emit state update to all listeners\n */\n private emitUpdate(): void {\n const state = this.getState();\n this.listeners.forEach(listener => {\n try {\n listener(state);\n } catch (err) {\n console.error('Error in state listener:', err);\n }\n });\n }\n\n /**\n * Generate unique ID using configured generator\n */\n private generateId(): string {\n return this.idGenerator();\n }\n}\n"]}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message protocol types for Incremark Chat-UI
|
|
3
|
+
* Based on Vercel AI SDK & TanStack AI's parts-based message structure
|
|
4
|
+
*
|
|
5
|
+
* Extensibility: Users can extend MessagePart via module augmentation:
|
|
6
|
+
* ```ts
|
|
7
|
+
* declare module '@incremark/chat-core' {
|
|
8
|
+
* interface CustomParts {
|
|
9
|
+
* 'my-custom': { type: 'my-custom'; data: string };
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Supported message roles
|
|
16
|
+
*/
|
|
17
|
+
type MessageRole = 'user' | 'assistant' | 'system' | 'agent';
|
|
18
|
+
/**
|
|
19
|
+
* Message status during lifecycle
|
|
20
|
+
*/
|
|
21
|
+
type MessageStatus = 'pending' | 'streaming' | 'success' | 'error';
|
|
22
|
+
/**
|
|
23
|
+
* Base part interface - all parts must have a type
|
|
24
|
+
*/
|
|
25
|
+
interface BasePart {
|
|
26
|
+
type: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Text content part
|
|
30
|
+
*/
|
|
31
|
+
interface TextPart extends BasePart {
|
|
32
|
+
type: 'text';
|
|
33
|
+
content: string;
|
|
34
|
+
format?: 'markdown' | 'plain';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Tool call state - 用户可自定义任意状态
|
|
38
|
+
* 提供预定义常量供参考,但不限制用户使用其他值
|
|
39
|
+
*/
|
|
40
|
+
type ToolCallState = string;
|
|
41
|
+
/**
|
|
42
|
+
* 预定义的工具调用状态常量(参考 Vercel AI SDK)
|
|
43
|
+
* 用户可以使用这些常量,也可以使用自定义状态
|
|
44
|
+
*/
|
|
45
|
+
declare const TOOL_CALL_STATES: {
|
|
46
|
+
readonly INPUT_STREAMING: "input-streaming";
|
|
47
|
+
readonly INPUT_AVAILABLE: "input-available";
|
|
48
|
+
readonly APPROVAL_REQUESTED: "approval-requested";
|
|
49
|
+
readonly APPROVAL_RESPONDED: "approval-responded";
|
|
50
|
+
readonly EXECUTING: "executing";
|
|
51
|
+
readonly OUTPUT_AVAILABLE: "output-available";
|
|
52
|
+
readonly OUTPUT_ERROR: "output-error";
|
|
53
|
+
readonly OUTPUT_DENIED: "output-denied";
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Tool call part (when AI wants to use a tool)
|
|
57
|
+
*/
|
|
58
|
+
interface ToolCallPart extends BasePart {
|
|
59
|
+
type: 'tool-call';
|
|
60
|
+
toolCallId: string;
|
|
61
|
+
toolName: string;
|
|
62
|
+
args: Record<string, unknown>;
|
|
63
|
+
state: ToolCallState;
|
|
64
|
+
/** 工具执行结果 */
|
|
65
|
+
output?: unknown;
|
|
66
|
+
/** 错误信息(当 state 为 output-error 时) */
|
|
67
|
+
error?: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Source reference type
|
|
71
|
+
*/
|
|
72
|
+
type SourceType = 'url' | 'document';
|
|
73
|
+
/**
|
|
74
|
+
* Source part (reference to external content)
|
|
75
|
+
*/
|
|
76
|
+
interface SourcePart extends BasePart {
|
|
77
|
+
type: 'source';
|
|
78
|
+
sourceId: string;
|
|
79
|
+
sourceType: SourceType;
|
|
80
|
+
url?: string;
|
|
81
|
+
title?: string;
|
|
82
|
+
/** 文档类型(当 sourceType 为 document 时) */
|
|
83
|
+
mediaType?: string;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* File part (generated or uploaded file)
|
|
87
|
+
*/
|
|
88
|
+
interface FilePart extends BasePart {
|
|
89
|
+
type: 'file';
|
|
90
|
+
fileId: string;
|
|
91
|
+
/** base64 编码的文件内容或 URL */
|
|
92
|
+
data: string;
|
|
93
|
+
mediaType: string;
|
|
94
|
+
filename?: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* UI component part (for A2UI - Agent to UI)
|
|
98
|
+
*/
|
|
99
|
+
interface UIPart extends BasePart {
|
|
100
|
+
type: 'ui';
|
|
101
|
+
component: string;
|
|
102
|
+
props: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Reasoning/thought part (for chain-of-thought display)
|
|
106
|
+
*/
|
|
107
|
+
interface ReasoningPart extends BasePart {
|
|
108
|
+
type: 'reasoning';
|
|
109
|
+
content: string;
|
|
110
|
+
/** 推理状态 */
|
|
111
|
+
status?: 'thinking' | 'completed' | 'error';
|
|
112
|
+
/** 思考开始时间(毫秒时间戳) */
|
|
113
|
+
startTime?: number;
|
|
114
|
+
/** 思考结束时间(毫秒时间戳) */
|
|
115
|
+
endTime?: number;
|
|
116
|
+
/** 思考标题 */
|
|
117
|
+
title?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Built-in parts registry
|
|
121
|
+
*/
|
|
122
|
+
interface BuiltinParts {
|
|
123
|
+
text: TextPart;
|
|
124
|
+
'tool-call': ToolCallPart;
|
|
125
|
+
source: SourcePart;
|
|
126
|
+
file: FilePart;
|
|
127
|
+
ui: UIPart;
|
|
128
|
+
reasoning: ReasoningPart;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Custom parts registry - extend via module augmentation
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* declare module '@incremark/chat-core' {
|
|
135
|
+
* interface CustomParts {
|
|
136
|
+
* 'weather-card': { type: 'weather-card'; city: string; temp: number };
|
|
137
|
+
* }
|
|
138
|
+
* }
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
interface CustomParts {
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* All registered parts
|
|
145
|
+
*/
|
|
146
|
+
type PartsRegistry = BuiltinParts & CustomParts;
|
|
147
|
+
/**
|
|
148
|
+
* Union type for all message parts (built-in + custom)
|
|
149
|
+
*/
|
|
150
|
+
type MessagePart = PartsRegistry[keyof PartsRegistry];
|
|
151
|
+
/**
|
|
152
|
+
* Get part type by name
|
|
153
|
+
*/
|
|
154
|
+
type PartByType<T extends keyof PartsRegistry> = PartsRegistry[T];
|
|
155
|
+
/**
|
|
156
|
+
* Generic type guard for any part type
|
|
157
|
+
*/
|
|
158
|
+
declare function isPartType<T extends keyof PartsRegistry>(part: MessagePart, type: T): part is PartsRegistry[T];
|
|
159
|
+
/**
|
|
160
|
+
* Type guard for TextPart
|
|
161
|
+
*/
|
|
162
|
+
declare function isTextPart(part: MessagePart): part is TextPart;
|
|
163
|
+
/**
|
|
164
|
+
* Type guard for ToolCallPart
|
|
165
|
+
*/
|
|
166
|
+
declare function isToolCallPart(part: MessagePart): part is ToolCallPart;
|
|
167
|
+
/**
|
|
168
|
+
* Type guard for SourcePart
|
|
169
|
+
*/
|
|
170
|
+
declare function isSourcePart(part: MessagePart): part is SourcePart;
|
|
171
|
+
/**
|
|
172
|
+
* Type guard for FilePart
|
|
173
|
+
*/
|
|
174
|
+
declare function isFilePart(part: MessagePart): part is FilePart;
|
|
175
|
+
/**
|
|
176
|
+
* Type guard for UIPart
|
|
177
|
+
*/
|
|
178
|
+
declare function isUIPart(part: MessagePart): part is UIPart;
|
|
179
|
+
/**
|
|
180
|
+
* Type guard for ReasoningPart
|
|
181
|
+
*/
|
|
182
|
+
declare function isReasoningPart(part: MessagePart): part is ReasoningPart;
|
|
183
|
+
/**
|
|
184
|
+
* Complete message structure
|
|
185
|
+
*/
|
|
186
|
+
interface ChatMessage {
|
|
187
|
+
id: string;
|
|
188
|
+
role: MessageRole;
|
|
189
|
+
parts: MessagePart[];
|
|
190
|
+
status: MessageStatus;
|
|
191
|
+
createdAt: number;
|
|
192
|
+
metadata?: Record<string, unknown>;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Create a new text message
|
|
196
|
+
*/
|
|
197
|
+
declare function createTextMessage(role: MessageRole, content: string, format?: 'markdown' | 'plain'): ChatMessage;
|
|
198
|
+
/**
|
|
199
|
+
* Create a new empty streaming message
|
|
200
|
+
*/
|
|
201
|
+
declare function createStreamingMessage(role: 'assistant' | 'agent'): ChatMessage;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Transport layer protocol for chat communication
|
|
205
|
+
* Inspired by Vercel AI SDK 5.0+ transport architecture
|
|
206
|
+
*/
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Stream part - chunks of data received during streaming
|
|
210
|
+
*/
|
|
211
|
+
type StreamPart = TextStreamPart | ToolCallStreamPart | UIStreamPart | ReasoningStreamPart | DoneStreamPart | ErrorStreamPart;
|
|
212
|
+
/**
|
|
213
|
+
* Text content stream part
|
|
214
|
+
*/
|
|
215
|
+
interface TextStreamPart {
|
|
216
|
+
type: 'text';
|
|
217
|
+
content: string;
|
|
218
|
+
format?: 'markdown' | 'plain';
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Tool call stream part
|
|
222
|
+
*/
|
|
223
|
+
interface ToolCallStreamPart {
|
|
224
|
+
type: 'tool-call';
|
|
225
|
+
toolCallId?: string;
|
|
226
|
+
toolName: string;
|
|
227
|
+
args: Record<string, any>;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* UI component stream part
|
|
231
|
+
*/
|
|
232
|
+
interface UIStreamPart {
|
|
233
|
+
type: 'ui';
|
|
234
|
+
component: string;
|
|
235
|
+
props: Record<string, any>;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Reasoning stream part
|
|
239
|
+
*/
|
|
240
|
+
interface ReasoningStreamPart {
|
|
241
|
+
type: 'reasoning';
|
|
242
|
+
content: string;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Stream completion marker
|
|
246
|
+
*/
|
|
247
|
+
interface DoneStreamPart {
|
|
248
|
+
type: 'done';
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Error stream part
|
|
252
|
+
*/
|
|
253
|
+
interface ErrorStreamPart {
|
|
254
|
+
type: 'error';
|
|
255
|
+
error: string;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Transport interface for sending messages and receiving stream
|
|
259
|
+
* This is the key abstraction that allows different implementations:
|
|
260
|
+
* - HTTP streaming
|
|
261
|
+
* - WebSocket
|
|
262
|
+
* - Server-Sent Events (SSE)
|
|
263
|
+
* - Mock for testing
|
|
264
|
+
*/
|
|
265
|
+
interface ChatTransport {
|
|
266
|
+
/**
|
|
267
|
+
* Send messages and return async generator of stream parts
|
|
268
|
+
*
|
|
269
|
+
* @param messages - Full conversation history (or context window)
|
|
270
|
+
* @returns Async generator yielding stream parts
|
|
271
|
+
*/
|
|
272
|
+
send(messages: ChatMessage[]): AsyncGenerator<StreamPart>;
|
|
273
|
+
/**
|
|
274
|
+
* Abort the current stream (optional)
|
|
275
|
+
*/
|
|
276
|
+
abort?(): void;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Simple mock transport for testing
|
|
280
|
+
*/
|
|
281
|
+
declare class MockTransport implements ChatTransport {
|
|
282
|
+
private aborted;
|
|
283
|
+
send(messages: ChatMessage[]): AsyncGenerator<StreamPart>;
|
|
284
|
+
abort(): void;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export { type BasePart, type BuiltinParts, type ChatMessage, type ChatTransport, type CustomParts, type DoneStreamPart, type ErrorStreamPart, type FilePart, type MessagePart, type MessageRole, type MessageStatus, MockTransport, type PartByType, type PartsRegistry, type ReasoningPart, type ReasoningStreamPart, type SourcePart, type SourceType, type StreamPart, TOOL_CALL_STATES, type TextPart, type TextStreamPart, type ToolCallPart, type ToolCallState, type ToolCallStreamPart, type UIPart, type UIStreamPart, createStreamingMessage, createTextMessage, isFilePart, isPartType, isReasoningPart, isSourcePart, isTextPart, isToolCallPart, isUIPart };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { MockTransport, TOOL_CALL_STATES, createStreamingMessage, createTextMessage, isFilePart, isPartType, isReasoningPart, isSourcePart, isTextPart, isToolCallPart, isUIPart } from '../chunk-FOTOSDBV.js';
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
3
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@incremark/chat-core",
|
|
3
|
+
"version": "0.4.0-alpha.1",
|
|
4
|
+
"description": "Framework-agnostic core library for building AI chat interfaces with incremental rendering support.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./protocol": {
|
|
15
|
+
"types": "./dist/protocol/index.d.ts",
|
|
16
|
+
"import": "./dist/protocol/index.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"tsup": "^8.5.1",
|
|
24
|
+
"typescript": "^5.3.0",
|
|
25
|
+
"vitest": "^4.0.16"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"ai",
|
|
29
|
+
"chat",
|
|
30
|
+
"llm",
|
|
31
|
+
"streaming",
|
|
32
|
+
"incremental",
|
|
33
|
+
"framework-agnostic",
|
|
34
|
+
"typescript"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/kingshuaishuai/incremark.git",
|
|
40
|
+
"directory": "packages/chat-core"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"nanoid": "^5.1.6"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup",
|
|
47
|
+
"dev": "tsup --watch",
|
|
48
|
+
"test": "vitest",
|
|
49
|
+
"test:run": "vitest run",
|
|
50
|
+
"test:coverage": "vitest run -- --coverage"
|
|
51
|
+
}
|
|
52
|
+
}
|