@witqq/agent-sdk 0.6.0 → 0.7.0
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/README.md +433 -6
- package/dist/auth/index.cjs +188 -1
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +154 -138
- package/dist/auth/index.d.ts +154 -138
- package/dist/auth/index.js +188 -2
- package/dist/auth/index.js.map +1 -1
- package/dist/backends/claude.cjs +341 -22
- package/dist/backends/claude.cjs.map +1 -1
- package/dist/backends/claude.d.cts +2 -1
- package/dist/backends/claude.d.ts +2 -1
- package/dist/backends/claude.js +341 -22
- package/dist/backends/claude.js.map +1 -1
- package/dist/backends/copilot.cjs +133 -25
- package/dist/backends/copilot.cjs.map +1 -1
- package/dist/backends/copilot.d.cts +2 -1
- package/dist/backends/copilot.d.ts +2 -1
- package/dist/backends/copilot.js +133 -25
- package/dist/backends/copilot.js.map +1 -1
- package/dist/backends/vercel-ai.cjs +66 -19
- package/dist/backends/vercel-ai.cjs.map +1 -1
- package/dist/backends/vercel-ai.d.cts +1 -1
- package/dist/backends/vercel-ai.d.ts +1 -1
- package/dist/backends/vercel-ai.js +66 -19
- package/dist/backends/vercel-ai.js.map +1 -1
- package/dist/chat/accumulator.cjs +147 -0
- package/dist/chat/accumulator.cjs.map +1 -0
- package/dist/chat/accumulator.d.cts +61 -0
- package/dist/chat/accumulator.d.ts +61 -0
- package/dist/chat/accumulator.js +145 -0
- package/dist/chat/accumulator.js.map +1 -0
- package/dist/chat/backends.cjs +3534 -0
- package/dist/chat/backends.cjs.map +1 -0
- package/dist/chat/backends.d.cts +62 -0
- package/dist/chat/backends.d.ts +62 -0
- package/dist/chat/backends.js +3501 -0
- package/dist/chat/backends.js.map +1 -0
- package/dist/chat/context.cjs +230 -0
- package/dist/chat/context.cjs.map +1 -0
- package/dist/chat/context.d.cts +167 -0
- package/dist/chat/context.d.ts +167 -0
- package/dist/chat/context.js +227 -0
- package/dist/chat/context.js.map +1 -0
- package/dist/chat/core.cjs +282 -0
- package/dist/chat/core.cjs.map +1 -0
- package/dist/chat/core.d.cts +435 -0
- package/dist/chat/core.d.ts +435 -0
- package/dist/chat/core.js +261 -0
- package/dist/chat/core.js.map +1 -0
- package/dist/chat/errors.cjs +251 -0
- package/dist/chat/errors.cjs.map +1 -0
- package/dist/chat/errors.d.cts +122 -0
- package/dist/chat/errors.d.ts +122 -0
- package/dist/chat/errors.js +243 -0
- package/dist/chat/errors.js.map +1 -0
- package/dist/chat/events.cjs +203 -0
- package/dist/chat/events.cjs.map +1 -0
- package/dist/chat/events.d.cts +241 -0
- package/dist/chat/events.d.ts +241 -0
- package/dist/chat/events.js +196 -0
- package/dist/chat/events.js.map +1 -0
- package/dist/chat/index.cjs +5359 -0
- package/dist/chat/index.cjs.map +1 -0
- package/dist/chat/index.d.cts +52 -0
- package/dist/chat/index.d.ts +52 -0
- package/dist/chat/index.js +5296 -0
- package/dist/chat/index.js.map +1 -0
- package/dist/chat/react.cjs +2739 -0
- package/dist/chat/react.cjs.map +1 -0
- package/dist/chat/react.d.cts +619 -0
- package/dist/chat/react.d.ts +619 -0
- package/dist/chat/react.js +2714 -0
- package/dist/chat/react.js.map +1 -0
- package/dist/chat/runtime.cjs +1030 -0
- package/dist/chat/runtime.cjs.map +1 -0
- package/dist/chat/runtime.d.cts +118 -0
- package/dist/chat/runtime.d.ts +118 -0
- package/dist/chat/runtime.js +1028 -0
- package/dist/chat/runtime.js.map +1 -0
- package/dist/chat/server.cjs +643 -0
- package/dist/chat/server.cjs.map +1 -0
- package/dist/chat/server.d.cts +287 -0
- package/dist/chat/server.d.ts +287 -0
- package/dist/chat/server.js +617 -0
- package/dist/chat/server.js.map +1 -0
- package/dist/chat/sessions.cjs +398 -0
- package/dist/chat/sessions.cjs.map +1 -0
- package/dist/chat/sessions.d.cts +239 -0
- package/dist/chat/sessions.d.ts +239 -0
- package/dist/chat/sessions.js +394 -0
- package/dist/chat/sessions.js.map +1 -0
- package/dist/chat/state.cjs +177 -0
- package/dist/chat/state.cjs.map +1 -0
- package/dist/chat/state.d.cts +92 -0
- package/dist/chat/state.d.ts +92 -0
- package/dist/chat/state.js +167 -0
- package/dist/chat/state.js.map +1 -0
- package/dist/chat/storage.cjs +240 -0
- package/dist/chat/storage.cjs.map +1 -0
- package/dist/chat/storage.d.cts +191 -0
- package/dist/chat/storage.d.ts +191 -0
- package/dist/chat/storage.js +236 -0
- package/dist/chat/storage.js.map +1 -0
- package/dist/errors-BDLbNu9w.d.cts +13 -0
- package/dist/errors-BDLbNu9w.d.ts +13 -0
- package/dist/in-process-transport-C2oPTYs6.d.ts +223 -0
- package/dist/in-process-transport-DG-w5G6k.d.cts +223 -0
- package/dist/index.cjs +25 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -4
- package/dist/index.d.ts +32 -4
- package/dist/index.js +25 -13
- package/dist/index.js.map +1 -1
- package/dist/transport-D1OaUgRk.d.ts +67 -0
- package/dist/transport-DX1Nhm4N.d.cts +67 -0
- package/dist/types-Bh5AhqD-.d.ts +141 -0
- package/dist/types-CGF7AEX1.d.cts +141 -0
- package/dist/{types-BvwNzZCj.d.cts → types-CqvUAYxt.d.cts} +21 -3
- package/dist/{types-BvwNzZCj.d.ts → types-CqvUAYxt.d.ts} +21 -3
- package/dist/types-DLZzlJxt.d.ts +39 -0
- package/dist/types-tE0CXwBl.d.cts +39 -0
- package/package.json +149 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/chat/core.ts
|
|
4
|
+
function createChatId() {
|
|
5
|
+
return crypto.randomUUID();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/chat/accumulator.ts
|
|
9
|
+
var MessageAccumulator = class {
|
|
10
|
+
messageId;
|
|
11
|
+
parts = [];
|
|
12
|
+
status = "pending";
|
|
13
|
+
currentTextPart = null;
|
|
14
|
+
currentReasoningPart = null;
|
|
15
|
+
toolCallParts = /* @__PURE__ */ new Map();
|
|
16
|
+
_finalized = false;
|
|
17
|
+
constructor(messageId) {
|
|
18
|
+
this.messageId = messageId ?? createChatId();
|
|
19
|
+
}
|
|
20
|
+
/** Get current message ID */
|
|
21
|
+
get id() {
|
|
22
|
+
return this.messageId;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Apply an AgentEvent to accumulate into the message
|
|
26
|
+
* @param event - AgentEvent to process
|
|
27
|
+
* @throws Error if accumulator is already finalized
|
|
28
|
+
*/
|
|
29
|
+
apply(event) {
|
|
30
|
+
if (this._finalized) throw new Error("Cannot apply events to finalized accumulator");
|
|
31
|
+
if (this.status === "pending") {
|
|
32
|
+
this.status = "streaming";
|
|
33
|
+
}
|
|
34
|
+
switch (event.type) {
|
|
35
|
+
case "text_delta":
|
|
36
|
+
this.handleTextDelta(event.text);
|
|
37
|
+
break;
|
|
38
|
+
case "thinking_start":
|
|
39
|
+
this.finalizeCurrentText();
|
|
40
|
+
this.currentReasoningPart = { type: "reasoning", text: "", status: "streaming" };
|
|
41
|
+
this.parts.push(this.currentReasoningPart);
|
|
42
|
+
break;
|
|
43
|
+
case "thinking_delta":
|
|
44
|
+
if (this.currentReasoningPart) {
|
|
45
|
+
this.currentReasoningPart.text += event.text;
|
|
46
|
+
}
|
|
47
|
+
break;
|
|
48
|
+
case "thinking_end":
|
|
49
|
+
if (this.currentReasoningPart) {
|
|
50
|
+
this.currentReasoningPart.status = "complete";
|
|
51
|
+
this.currentReasoningPart = null;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case "tool_call_start": {
|
|
55
|
+
this.finalizeCurrentText();
|
|
56
|
+
const toolPart = {
|
|
57
|
+
type: "tool_call",
|
|
58
|
+
toolCallId: event.toolCallId,
|
|
59
|
+
name: event.toolName,
|
|
60
|
+
args: event.args,
|
|
61
|
+
status: "running"
|
|
62
|
+
};
|
|
63
|
+
this.toolCallParts.set(event.toolCallId, toolPart);
|
|
64
|
+
this.parts.push(toolPart);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "tool_call_end": {
|
|
68
|
+
const existing = this.toolCallParts.get(event.toolCallId);
|
|
69
|
+
if (existing) {
|
|
70
|
+
existing.result = event.result;
|
|
71
|
+
existing.status = "complete";
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "error":
|
|
76
|
+
this.status = "error";
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
handleTextDelta(text) {
|
|
81
|
+
if (!this.currentTextPart) {
|
|
82
|
+
this.currentTextPart = { type: "text", text: "", status: "streaming" };
|
|
83
|
+
this.parts.push(this.currentTextPart);
|
|
84
|
+
}
|
|
85
|
+
this.currentTextPart.text += text;
|
|
86
|
+
}
|
|
87
|
+
finalizeCurrentText() {
|
|
88
|
+
if (this.currentTextPart) {
|
|
89
|
+
this.currentTextPart.status = "complete";
|
|
90
|
+
this.currentTextPart = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get a snapshot of the current accumulated message (for streaming UI)
|
|
95
|
+
* @returns ChatMessage with current parts and "streaming" status
|
|
96
|
+
*/
|
|
97
|
+
snapshot() {
|
|
98
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
99
|
+
return {
|
|
100
|
+
id: this.messageId,
|
|
101
|
+
role: "assistant",
|
|
102
|
+
parts: this.parts.map((p) => ({ ...p })),
|
|
103
|
+
status: this.status === "pending" ? "pending" : "streaming",
|
|
104
|
+
createdAt: now,
|
|
105
|
+
updatedAt: now
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Finalize the accumulator and return the complete ChatMessage
|
|
110
|
+
* @returns Completed ChatMessage with all parts finalized
|
|
111
|
+
* @throws Error if accumulator is already finalized
|
|
112
|
+
*/
|
|
113
|
+
finalize() {
|
|
114
|
+
if (this._finalized) throw new Error("Accumulator already finalized");
|
|
115
|
+
this._finalized = true;
|
|
116
|
+
this.finalizeCurrentText();
|
|
117
|
+
if (this.currentReasoningPart) {
|
|
118
|
+
this.currentReasoningPart.status = "complete";
|
|
119
|
+
this.currentReasoningPart = null;
|
|
120
|
+
}
|
|
121
|
+
for (const [, toolPart] of this.toolCallParts) {
|
|
122
|
+
if (toolPart.status === "running" || toolPart.status === "pending") {
|
|
123
|
+
toolPart.status = "error";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (this.status !== "error" && this.status !== "cancelled") {
|
|
127
|
+
this.status = "complete";
|
|
128
|
+
}
|
|
129
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
130
|
+
return {
|
|
131
|
+
id: this.messageId,
|
|
132
|
+
role: "assistant",
|
|
133
|
+
parts: this.parts,
|
|
134
|
+
status: this.status,
|
|
135
|
+
createdAt: now,
|
|
136
|
+
updatedAt: now
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Check if the accumulator has been finalized */
|
|
140
|
+
get finalized() {
|
|
141
|
+
return this._finalized;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
exports.MessageAccumulator = MessageAccumulator;
|
|
146
|
+
//# sourceMappingURL=accumulator.cjs.map
|
|
147
|
+
//# sourceMappingURL=accumulator.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/chat/core.ts","../../src/chat/accumulator.ts"],"names":[],"mappings":"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","file":"accumulator.cjs","sourcesContent":["/**\n * @witqq/agent-sdk/chat/core\n *\n * Foundational chat types and utilities: ChatMessage, ChatSession, ChatEvent,\n * IChatProvider, type guards, and AgentEvent↔ChatEvent bridge functions.\n */\n\nimport type {\n AgentEvent,\n Message,\n ToolCall,\n ToolResult,\n ToolDefinition,\n UsageData,\n ModelInfo,\n JSONValue,\n} from \"../types.js\";\n\n// ─── Unique ID ─────────────────────────────────────────────────\n\n/** Branded type for unique identifiers */\nexport type ChatId = string & { readonly __brand: \"ChatId\" };\n\n/**\n * Generate a new unique ChatId (crypto.randomUUID-based)\n * @returns Branded ChatId string\n */\nexport function createChatId(): ChatId {\n return crypto.randomUUID() as ChatId;\n}\n\n/** UUID v4 pattern for ChatId validation */\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/**\n * Cast a string to ChatId with UUID format validation.\n * Use this instead of manual `as ChatId` type assertions.\n *\n * @param value - String to validate and cast\n * @returns Branded ChatId\n * @throws {TypeError} If value is not a valid UUID v4 format\n *\n * @example\n * ```ts\n * const id = toChatId(\"550e8400-e29b-41d4-a716-446655440000\");\n * ```\n */\nexport function toChatId(value: string): ChatId {\n if (!UUID_RE.test(value)) {\n throw new TypeError(`Invalid ChatId: \"${value}\" is not a valid UUID`);\n }\n return value as ChatId;\n}\n\n/**\n * Accepts either a plain string or branded ChatId for API convenience.\n * Use this in public API signatures so consumers don't need `as ChatId` casts.\n */\nexport type ChatIdLike = string | ChatId;\n\n// ─── Status Types ──────────────────────────────────────────────\n\n/** Lifecycle status of a message part (text, reasoning, etc.) */\nexport type PartStatus = \"pending\" | \"streaming\" | \"complete\" | \"error\";\n/** Lifecycle status of a tool call within a message */\nexport type ToolCallStatus = \"pending\" | \"running\" | \"requires_approval\" | \"complete\" | \"error\" | \"denied\";\n/** Lifecycle status of an entire message */\nexport type MessageStatus = \"pending\" | \"streaming\" | \"complete\" | \"error\" | \"cancelled\";\n/** Lifecycle status of a chat session */\nexport type SessionStatus = \"active\" | \"archived\";\n/** Lifecycle status of the chat runtime */\nexport type RuntimeStatus = \"idle\" | \"streaming\" | \"error\" | \"disposed\";\n\n// ─── Message Parts (union) ─────────────────────────────────────\n\n/** Plain text content part */\nexport interface TextPart { type: \"text\"; text: string; status: PartStatus; }\n/** Model reasoning/thinking content part */\nexport interface ReasoningPart { type: \"reasoning\"; text: string; status: PartStatus; }\n/** Tool invocation part with call ID, arguments, optional result */\nexport interface ToolCallPart { type: \"tool_call\"; toolCallId: string; name: string; args: unknown; result?: unknown; status: ToolCallStatus; error?: string; }\n/** Source reference part (URL citation) */\nexport interface SourcePart { type: \"source\"; url: string; title?: string; status: PartStatus; }\n/** File attachment part (base64-encoded data) */\nexport interface FilePart { type: \"file\"; name: string; mimeType: string; data: string; status: PartStatus; }\n/** Union of all message part types */\nexport type MessagePart = TextPart | ReasoningPart | ToolCallPart | SourcePart | FilePart;\n\n// ─── Chat Message ──────────────────────────────────────────────\n\n/** Role of message author */\nexport type ChatRole = \"user\" | \"assistant\" | \"system\";\n\n/** Metadata attached to messages — useful preset for the TMetadata generic */\nexport interface ChatMessageMetadata {\n model?: string;\n backend?: string;\n usage?: UsageData;\n isSummary?: boolean;\n isArchived?: boolean;\n estimatedTokens?: number;\n custom?: Record<string, unknown>;\n}\n\n/** Message status */\nexport type ChatMessageStatus = MessageStatus;\n\n/** A single chat message — the fundamental unit of conversation */\nexport interface ChatMessage<TMetadata = unknown> {\n id: ChatId;\n role: ChatRole;\n parts: MessagePart[];\n metadata?: TMetadata;\n createdAt: string;\n updatedAt?: string;\n status: MessageStatus;\n}\n\n// ─── Convenience Getters ───────────────────────────────────────\n\n/**\n * Join all TextPart texts in a message\n * @param message - The chat message to extract text from\n * @returns Concatenated text content\n */\nexport function getMessageText(message: ChatMessage): string {\n return message.parts\n .filter((p): p is TextPart => p.type === \"text\")\n .map((p) => p.text)\n .join(\"\");\n}\n\n/**\n * Filter all ToolCallParts from a message\n * @param message - The chat message to extract tool calls from\n * @returns Array of ToolCallPart\n */\nexport function getMessageToolCalls(message: ChatMessage): ToolCallPart[] {\n return message.parts.filter((p): p is ToolCallPart => p.type === \"tool_call\");\n}\n\n/**\n * Join all ReasoningPart texts in a message\n * @param message - The chat message to extract reasoning from\n * @returns Concatenated reasoning content\n */\nexport function getMessageReasoning(message: ChatMessage): string {\n return message.parts\n .filter((p): p is ReasoningPart => p.type === \"reasoning\")\n .map((p) => p.text)\n .join(\"\");\n}\n\n// ─── Supporting Types ──────────────────────────────────────────\n\n/** Options for sending a message */\nexport interface SendOpts { sessionId?: string; model?: string; signal?: AbortSignal; metadata?: Record<string, unknown>; }\n/** Options for creating a new session */\nexport interface CreateSessionOpts { id?: string; title?: string; model?: string; metadata?: Record<string, unknown>; }\n/** Options for listing sessions with pagination */\nexport interface ListOpts { limit?: number; offset?: number; status?: SessionStatus; }\n/** Options for backend execution (model, tokens, tools) */\nexport interface BackendOpts { model: string; signal?: AbortSignal; systemPrompt?: string; temperature?: number; maxTokens?: number; tools?: Record<string, unknown>; providerOptions?: Record<string, unknown>; }\n/** Context passed to tool execute functions */\nexport interface ToolContext { sessionId: string; userId?: string; signal: AbortSignal; }\n/** Configuration for creating a chat runtime */\nexport interface ChatRuntimeConfig { backend: string; model?: string; apiKey?: string; baseUrl?: string; context?: { maxTokens?: number; reserveTokens?: number; strategy?: \"sliding\" | \"summarize\" | \"truncate\"; }; retry?: { maxRetries?: number; initialDelay?: number; backoffFactor?: number; }; providerOptions?: Record<string, unknown>; }\n\n// ─── Chat Session ──────────────────────────────────────────────\n\n/** Session configuration snapshot */\nexport interface ChatSessionConfig {\n model: string;\n backend: string;\n systemPrompt?: string;\n temperature?: number;\n maxTokens?: number;\n}\n\n/** Session metadata */\nexport interface ChatSessionMetadata<TCustom extends Record<string, unknown> = Record<string, unknown>> {\n messageCount: number;\n totalTokens: number;\n tags?: string[];\n custom?: TCustom;\n}\n\n/** Chat session — a conversation with ordered messages */\nexport interface ChatSession<TCustom extends Record<string, unknown> = Record<string, unknown>> {\n id: ChatId;\n title?: string;\n messages: ChatMessage[];\n config: ChatSessionConfig;\n metadata: ChatSessionMetadata<TCustom>;\n status: SessionStatus;\n createdAt: string;\n updatedAt: string;\n backendSessionId?: string;\n /** Subscribe to session changes (for React useSyncExternalStore) */\n subscribe?(callback: () => void): () => void;\n /** Get immutable snapshot of session state (for React useSyncExternalStore) */\n getSnapshot?(): ChatSession<TCustom>;\n /** Last message in the session */\n readonly lastMessage?: ChatMessage;\n}\n\n/** Lightweight session info for listing (without full message array) */\nexport interface SessionInfo {\n id: ChatId;\n title?: string;\n status: SessionStatus;\n messageCount: number;\n lastMessage?: ChatMessage;\n createdAt: string;\n updatedAt: string;\n}\n\n// ─── Chat Events ───────────────────────────────────────────────\n\n/** Events emitted during chat operation */\nexport type ChatEvent =\n | { type: \"message:start\"; messageId: ChatId; role: ChatRole }\n | { type: \"message:delta\"; messageId: ChatId; text: string }\n | { type: \"message:complete\"; messageId: ChatId; message: ChatMessage }\n | {\n type: \"tool:start\";\n messageId: ChatId;\n toolCallId: string;\n toolName: string;\n args: Record<string, unknown>;\n }\n | {\n type: \"tool:complete\";\n messageId: ChatId;\n toolCallId: string;\n toolName: string;\n result: unknown;\n isError?: boolean;\n }\n | { type: \"thinking:start\"; messageId: ChatId }\n | { type: \"thinking:delta\"; messageId: ChatId; text: string }\n | { type: \"thinking:end\"; messageId: ChatId }\n | {\n type: \"permission:request\";\n messageId: ChatId;\n toolName: string;\n toolArgs: Record<string, unknown>;\n }\n | {\n type: \"permission:response\";\n messageId: ChatId;\n toolName: string;\n allowed: boolean;\n }\n | {\n type: \"usage\";\n promptTokens: number;\n completionTokens: number;\n model?: string;\n }\n | { type: \"session:created\"; sessionId: ChatId }\n | { type: \"session:updated\"; sessionId: ChatId }\n | {\n type: \"error\";\n error: string;\n recoverable: boolean;\n messageId?: ChatId;\n }\n | { type: \"typing:start\" }\n | { type: \"typing:end\" }\n | { type: \"heartbeat\" }\n | { type: \"done\"; finalOutput?: string };\n\n/** All possible ChatEvent type strings */\nexport type ChatEventType = ChatEvent[\"type\"];\n\n// ─── Chat Middleware ───────────────────────────────────────────\n\n/** Context passed to ChatMiddleware hooks */\nexport interface ChatMiddlewareContext {\n sessionId: ChatId;\n signal: AbortSignal;\n}\n\n/** Runtime-level middleware for the send/receive lifecycle.\n * Different from EventMiddleware which operates at the event bus level. */\nexport interface ChatMiddleware {\n /** Transform message before sending to backend */\n onBeforeSend?(message: ChatMessage, context: ChatMiddlewareContext): ChatMessage | Promise<ChatMessage>;\n /** Transform/intercept stream events */\n onEvent?(event: ChatEvent, context: ChatMiddlewareContext): ChatEvent | null | Promise<ChatEvent | null>;\n /** Transform completed message after receiving from backend */\n onAfterReceive?(message: ChatMessage, context: ChatMiddlewareContext): ChatMessage | Promise<ChatMessage>;\n /** Intercept errors — return null to suppress, return error to propagate */\n onError?(error: Error, context: ChatMiddlewareContext): Error | null | Promise<Error | null>;\n}\n\n// ─── Chat Provider Abstraction ─────────────────────────────────\n\n/** Options for sending a message to a provider */\nexport interface SendMessageOptions {\n signal?: AbortSignal;\n model?: string;\n context?: Record<string, unknown>;\n /** Additional tools to include in this request */\n tools?: ToolDefinition[];\n}\n\n/** Abstract chat provider — wraps an IAgentService for chat use */\nexport interface IChatProvider {\n readonly name: string;\n sendMessage(\n session: ChatSession,\n message: string,\n options?: SendMessageOptions,\n ): Promise<ChatMessage>;\n streamMessage(\n session: ChatSession,\n message: string,\n options?: SendMessageOptions,\n ): AsyncIterable<ChatEvent>;\n listModels(): Promise<ModelInfo[]>;\n validate(): Promise<{ valid: boolean; errors: string[] }>;\n dispose(): Promise<void>;\n}\n\n// ─── Type Guards ───────────────────────────────────────────────\n\n/**\n * Check if a value is a ChatMessage\n * @param value - Value to check\n * @returns True if value has ChatMessage shape\n */\nexport function isChatMessage(value: unknown): value is ChatMessage {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj.id === \"string\" &&\n typeof obj.role === \"string\" &&\n (obj.role === \"user\" ||\n obj.role === \"assistant\" ||\n obj.role === \"system\") &&\n Array.isArray(obj.parts) &&\n typeof obj.createdAt === \"string\" &&\n typeof obj.status === \"string\"\n );\n}\n\n/**\n * Check if a value is a ChatSession\n * @param value - Value to check\n * @returns True if value has ChatSession shape\n */\nexport function isChatSession(value: unknown): value is ChatSession {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj.id === \"string\" &&\n Array.isArray(obj.messages) &&\n typeof obj.config === \"object\" &&\n obj.config !== null &&\n typeof obj.createdAt === \"string\" &&\n typeof obj.updatedAt === \"string\" &&\n typeof obj.status === \"string\"\n );\n}\n\n/**\n * Check if a value is a MessagePart\n * @param value - Value to check\n * @returns True if value has MessagePart shape\n */\nexport function isMessagePart(value: unknown): value is MessagePart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj.type === \"string\" &&\n (obj.type === \"text\" ||\n obj.type === \"reasoning\" ||\n obj.type === \"tool_call\" ||\n obj.type === \"source\" ||\n obj.type === \"file\")\n );\n}\n\n/**\n * Check if a value is a TextPart\n * @param value - Value to check\n * @returns True if value is a TextPart\n */\nexport function isTextPart(value: unknown): value is TextPart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"text\" && typeof obj.text === \"string\";\n}\n\n/**\n * Check if a value is a ToolCallPart\n * @param value - Value to check\n * @returns True if value is a ToolCallPart\n */\nexport function isToolCallPart(value: unknown): value is ToolCallPart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"tool_call\" && typeof obj.toolCallId === \"string\" && typeof obj.name === \"string\";\n}\n\n/**\n * Check if a value is a ReasoningPart\n * @param value - Value to check\n * @returns True if value is a ReasoningPart\n */\nexport function isReasoningPart(value: unknown): value is ReasoningPart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"reasoning\" && typeof obj.text === \"string\";\n}\n\n/**\n * Check if a value is a SourcePart\n * @param value - Value to check\n * @returns True if value is a SourcePart\n */\nexport function isSourcePart(value: unknown): value is SourcePart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"source\" && typeof obj.url === \"string\";\n}\n\n/**\n * Check if a value is a FilePart\n * @param value - Value to check\n * @returns True if value is a FilePart\n */\nexport function isFilePart(value: unknown): value is FilePart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"file\" && typeof obj.name === \"string\" && typeof obj.mimeType === \"string\";\n}\n\n/**\n * Check if a value is a ChatEvent\n * @param value - Value to check\n * @returns True if value has a valid ChatEvent type\n */\nexport function isChatEvent(value: unknown): value is ChatEvent {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n const validTypes: ChatEventType[] = [\n \"message:start\",\n \"message:delta\",\n \"message:complete\",\n \"tool:start\",\n \"tool:complete\",\n \"thinking:start\",\n \"thinking:delta\",\n \"thinking:end\",\n \"permission:request\",\n \"permission:response\",\n \"usage\",\n \"session:created\",\n \"session:updated\",\n \"error\",\n \"typing:start\",\n \"typing:end\",\n \"heartbeat\",\n \"done\",\n ];\n return validTypes.includes(obj.type as ChatEventType);\n}\n\n// ─── Agent Event Adapter ───────────────────────────────────────\n\n/**\n * Map a single AgentEvent to a ChatEvent (or null if no mapping)\n * @param event - The AgentEvent to convert\n * @param messageId - ChatId to associate with the event\n * @returns Corresponding ChatEvent or null if unmappable\n */\nexport function agentEventToChatEvent(\n event: AgentEvent,\n messageId: ChatId,\n): ChatEvent | null {\n switch (event.type) {\n case \"text_delta\":\n return { type: \"message:delta\", messageId, text: event.text };\n case \"thinking_start\":\n return { type: \"thinking:start\", messageId };\n case \"thinking_delta\":\n return { type: \"thinking:delta\", messageId, text: event.text };\n case \"thinking_end\":\n return { type: \"thinking:end\", messageId };\n case \"tool_call_start\":\n return {\n type: \"tool:start\",\n messageId,\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n args: event.args as Record<string, unknown>,\n };\n case \"tool_call_end\":\n return {\n type: \"tool:complete\",\n messageId,\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n result: event.result,\n };\n case \"permission_request\":\n return {\n type: \"permission:request\",\n messageId,\n toolName: event.request.toolName,\n toolArgs: event.request.toolArgs,\n };\n case \"permission_response\":\n return {\n type: \"permission:response\",\n messageId,\n toolName: event.toolName,\n allowed: event.decision.allowed,\n };\n case \"usage_update\":\n return {\n type: \"usage\",\n promptTokens: event.promptTokens,\n completionTokens: event.completionTokens,\n model: event.model,\n };\n case \"error\":\n return {\n type: \"error\",\n error: event.error,\n recoverable: event.recoverable,\n messageId,\n };\n case \"heartbeat\":\n return { type: \"heartbeat\" };\n case \"ask_user\":\n case \"ask_user_response\":\n case \"session_info\":\n case \"done\":\n return null;\n default:\n return null;\n }\n}\n\n/**\n * Convert AgentEvent async iterable to ChatEvent async iterable\n * @param events - Source agent events\n * @param messageId - ChatId to associate with converted events\n * @returns Async iterable of ChatEvent (nulls filtered out)\n */\nexport async function* adaptAgentEvents(\n events: AsyncIterable<AgentEvent>,\n messageId: ChatId,\n): AsyncIterable<ChatEvent> {\n for await (const event of events) {\n const chatEvent = agentEventToChatEvent(event, messageId);\n if (chatEvent !== null) {\n yield chatEvent;\n }\n }\n}\n\n/**\n * Map a ChatEvent back to an AgentEvent for accumulator consumption.\n * Returns null for events that don't map to accumulator-relevant AgentEvents\n * (e.g. message:start, message:complete, usage, permission:*, heartbeat).\n *\n * @param event - The ChatEvent to convert\n * @returns Corresponding AgentEvent or null if not accumulator-relevant\n */\nexport function chatEventToAgentEvent(event: ChatEvent): AgentEvent | null {\n switch (event.type) {\n case \"message:delta\":\n return { type: \"text_delta\", text: event.text };\n case \"thinking:start\":\n return { type: \"thinking_start\" };\n case \"thinking:delta\":\n return { type: \"thinking_delta\", text: event.text };\n case \"thinking:end\":\n return { type: \"thinking_end\" };\n case \"tool:start\":\n return {\n type: \"tool_call_start\",\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n args: event.args as JSONValue,\n };\n case \"tool:complete\":\n return {\n type: \"tool_call_end\",\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n result: event.result as JSONValue,\n };\n case \"error\":\n return { type: \"error\", error: event.error, recoverable: event.recoverable };\n default:\n return null;\n }\n}\n\n// ─── Message Conversion ────────────────────────────────────────\n\n/**\n * Convert a ChatMessage to agent-sdk Message format\n * @param message - The ChatMessage to convert\n * @returns agent-sdk Message\n */\nexport function toAgentMessage(message: ChatMessage): Message {\n const textContent = getMessageText(message);\n const toolCallParts = getMessageToolCalls(message);\n\n switch (message.role) {\n case \"user\":\n return { role: \"user\", content: textContent };\n case \"assistant\": {\n const toolCalls: ToolCall[] | undefined = toolCallParts.length > 0\n ? toolCallParts.map((p) => ({ id: p.toolCallId, name: p.name, args: p.args as JSONValue }))\n : undefined;\n return {\n role: \"assistant\",\n content: textContent,\n toolCalls,\n };\n }\n case \"system\":\n return { role: \"system\", content: textContent };\n }\n}\n\n/**\n * Convert an agent-sdk Message to ChatMessage\n * @param message - The agent-sdk Message to convert\n * @param id - Optional ChatId (auto-generated if omitted)\n * @returns ChatMessage with status \"complete\"\n */\nexport function fromAgentMessage(message: Message, id?: ChatId): ChatMessage {\n const chatId = id ?? createChatId();\n const now = new Date().toISOString();\n\n const parts: MessagePart[] = [];\n\n // Build text content\n const textContent =\n typeof message.content === \"string\"\n ? message.content\n : Array.isArray(message.content)\n ? message.content\n .filter((part) => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\\n\")\n : (message.content ?? \"\");\n\n if (textContent) {\n parts.push({ type: \"text\", text: textContent, status: \"complete\" });\n }\n\n // Add tool calls from assistant messages\n if (message.role === \"assistant\" && message.toolCalls) {\n for (const tc of message.toolCalls) {\n parts.push({\n type: \"tool_call\",\n toolCallId: tc.id,\n name: tc.name,\n args: tc.args,\n status: \"complete\",\n });\n }\n }\n\n // Add tool results — map 'tool' role to 'assistant'\n if (message.role === \"tool\" && message.toolResults) {\n for (const tr of message.toolResults) {\n parts.push({\n type: \"tool_call\",\n toolCallId: tr.toolCallId,\n name: tr.name,\n args: {},\n result: tr.result,\n status: \"complete\",\n });\n }\n }\n\n // Ensure at least an empty text part for empty messages\n if (parts.length === 0) {\n parts.push({ type: \"text\", text: \"\", status: \"complete\" });\n }\n\n const role: ChatRole = message.role === \"tool\" ? \"assistant\" : message.role;\n\n return {\n id: chatId,\n role,\n parts,\n createdAt: now,\n status: \"complete\",\n };\n}\n\n/**\n * Extract ToolResults from ToolCallParts that have results\n * @param message - The ChatMessage to extract results from\n * @returns Array of ToolResult for completed tool calls\n */\nexport function extractToolResults(message: ChatMessage): ToolResult[] {\n return getMessageToolCalls(message)\n .filter((p) => p.result !== undefined)\n .map((p) => ({\n toolCallId: p.toolCallId,\n name: p.name,\n result: p.result as JSONValue,\n isError: p.status === \"error\" ? true : undefined,\n }));\n}\n","/**\n * @witqq/agent-sdk/chat/accumulator\n *\n * MessageAccumulator converts a stream of AgentEvent objects into a ChatMessage\n * with correct MessagePart array. Handles text, reasoning, and tool call\n * accumulation with proper status transitions.\n */\n\nimport type { AgentEvent } from \"../types.js\";\nimport type { ChatMessage, ChatId, MessagePart, TextPart, ReasoningPart, ToolCallPart } from \"./core.js\";\nimport { createChatId } from \"./core.js\";\n\n/**\n * Converts a stream of AgentEvent objects into a complete ChatMessage.\n * Tracks text, reasoning, and tool call parts with proper status transitions.\n *\n * @example\n * ```typescript\n * const acc = new MessageAccumulator();\n * for await (const event of agentEvents) {\n * acc.apply(event);\n * renderMessage(acc.snapshot()); // in-progress UI update\n * }\n * const message = acc.finalize();\n * ```\n */\nexport class MessageAccumulator {\n private readonly messageId: ChatId;\n private readonly parts: MessagePart[] = [];\n private status: \"pending\" | \"streaming\" | \"complete\" | \"error\" | \"cancelled\" = \"pending\";\n private currentTextPart: TextPart | null = null;\n private currentReasoningPart: ReasoningPart | null = null;\n private toolCallParts = new Map<string, ToolCallPart>();\n private _finalized = false;\n\n constructor(messageId?: ChatId) {\n this.messageId = messageId ?? createChatId();\n }\n\n /** Get current message ID */\n get id(): ChatId { return this.messageId; }\n\n /**\n * Apply an AgentEvent to accumulate into the message\n * @param event - AgentEvent to process\n * @throws Error if accumulator is already finalized\n */\n apply(event: AgentEvent): void {\n if (this._finalized) throw new Error(\"Cannot apply events to finalized accumulator\");\n\n if (this.status === \"pending\") {\n this.status = \"streaming\";\n }\n\n switch (event.type) {\n case \"text_delta\":\n this.handleTextDelta(event.text);\n break;\n case \"thinking_start\":\n this.finalizeCurrentText();\n this.currentReasoningPart = { type: \"reasoning\", text: \"\", status: \"streaming\" };\n this.parts.push(this.currentReasoningPart);\n break;\n case \"thinking_delta\":\n if (this.currentReasoningPart) {\n this.currentReasoningPart.text += event.text;\n }\n break;\n case \"thinking_end\":\n if (this.currentReasoningPart) {\n this.currentReasoningPart.status = \"complete\";\n this.currentReasoningPart = null;\n }\n break;\n case \"tool_call_start\": {\n this.finalizeCurrentText();\n const toolPart: ToolCallPart = {\n type: \"tool_call\",\n toolCallId: event.toolCallId,\n name: event.toolName,\n args: event.args,\n status: \"running\",\n };\n this.toolCallParts.set(event.toolCallId, toolPart);\n this.parts.push(toolPart);\n break;\n }\n case \"tool_call_end\": {\n const existing = this.toolCallParts.get(event.toolCallId);\n if (existing) {\n existing.result = event.result;\n existing.status = \"complete\";\n }\n break;\n }\n case \"error\":\n this.status = \"error\";\n break;\n case \"done\":\n break;\n // Other events (heartbeat, ask_user, etc.) — ignore\n }\n }\n\n private handleTextDelta(text: string): void {\n if (!this.currentTextPart) {\n this.currentTextPart = { type: \"text\", text: \"\", status: \"streaming\" };\n this.parts.push(this.currentTextPart);\n }\n this.currentTextPart.text += text;\n }\n\n private finalizeCurrentText(): void {\n if (this.currentTextPart) {\n this.currentTextPart.status = \"complete\";\n this.currentTextPart = null;\n }\n }\n\n /**\n * Get a snapshot of the current accumulated message (for streaming UI)\n * @returns ChatMessage with current parts and \"streaming\" status\n */\n snapshot(): ChatMessage {\n const now = new Date().toISOString();\n return {\n id: this.messageId,\n role: \"assistant\",\n parts: this.parts.map(p => ({ ...p })),\n status: this.status === \"pending\" ? \"pending\" : \"streaming\",\n createdAt: now,\n updatedAt: now,\n };\n }\n\n /**\n * Finalize the accumulator and return the complete ChatMessage\n * @returns Completed ChatMessage with all parts finalized\n * @throws Error if accumulator is already finalized\n */\n finalize(): ChatMessage {\n if (this._finalized) throw new Error(\"Accumulator already finalized\");\n this._finalized = true;\n\n // Finalize any open parts\n this.finalizeCurrentText();\n if (this.currentReasoningPart) {\n this.currentReasoningPart.status = \"complete\";\n this.currentReasoningPart = null;\n }\n\n // Mark incomplete tool calls as error\n for (const [, toolPart] of this.toolCallParts) {\n if (toolPart.status === \"running\" || toolPart.status === \"pending\") {\n toolPart.status = \"error\";\n }\n }\n\n // Set final message status\n if (this.status !== \"error\" && this.status !== \"cancelled\") {\n this.status = \"complete\";\n }\n\n const now = new Date().toISOString();\n return {\n id: this.messageId,\n role: \"assistant\",\n parts: this.parts,\n status: this.status,\n createdAt: now,\n updatedAt: now,\n };\n }\n\n /** Check if the accumulator has been finalized */\n get finalized(): boolean { return this._finalized; }\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { A as AgentEvent } from '../types-CqvUAYxt.cjs';
|
|
2
|
+
import { ChatId, ChatMessage } from './core.cjs';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @witqq/agent-sdk/chat/accumulator
|
|
7
|
+
*
|
|
8
|
+
* MessageAccumulator converts a stream of AgentEvent objects into a ChatMessage
|
|
9
|
+
* with correct MessagePart array. Handles text, reasoning, and tool call
|
|
10
|
+
* accumulation with proper status transitions.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts a stream of AgentEvent objects into a complete ChatMessage.
|
|
15
|
+
* Tracks text, reasoning, and tool call parts with proper status transitions.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const acc = new MessageAccumulator();
|
|
20
|
+
* for await (const event of agentEvents) {
|
|
21
|
+
* acc.apply(event);
|
|
22
|
+
* renderMessage(acc.snapshot()); // in-progress UI update
|
|
23
|
+
* }
|
|
24
|
+
* const message = acc.finalize();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare class MessageAccumulator {
|
|
28
|
+
private readonly messageId;
|
|
29
|
+
private readonly parts;
|
|
30
|
+
private status;
|
|
31
|
+
private currentTextPart;
|
|
32
|
+
private currentReasoningPart;
|
|
33
|
+
private toolCallParts;
|
|
34
|
+
private _finalized;
|
|
35
|
+
constructor(messageId?: ChatId);
|
|
36
|
+
/** Get current message ID */
|
|
37
|
+
get id(): ChatId;
|
|
38
|
+
/**
|
|
39
|
+
* Apply an AgentEvent to accumulate into the message
|
|
40
|
+
* @param event - AgentEvent to process
|
|
41
|
+
* @throws Error if accumulator is already finalized
|
|
42
|
+
*/
|
|
43
|
+
apply(event: AgentEvent): void;
|
|
44
|
+
private handleTextDelta;
|
|
45
|
+
private finalizeCurrentText;
|
|
46
|
+
/**
|
|
47
|
+
* Get a snapshot of the current accumulated message (for streaming UI)
|
|
48
|
+
* @returns ChatMessage with current parts and "streaming" status
|
|
49
|
+
*/
|
|
50
|
+
snapshot(): ChatMessage;
|
|
51
|
+
/**
|
|
52
|
+
* Finalize the accumulator and return the complete ChatMessage
|
|
53
|
+
* @returns Completed ChatMessage with all parts finalized
|
|
54
|
+
* @throws Error if accumulator is already finalized
|
|
55
|
+
*/
|
|
56
|
+
finalize(): ChatMessage;
|
|
57
|
+
/** Check if the accumulator has been finalized */
|
|
58
|
+
get finalized(): boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { MessageAccumulator };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { A as AgentEvent } from '../types-CqvUAYxt.js';
|
|
2
|
+
import { ChatId, ChatMessage } from './core.js';
|
|
3
|
+
import 'zod';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @witqq/agent-sdk/chat/accumulator
|
|
7
|
+
*
|
|
8
|
+
* MessageAccumulator converts a stream of AgentEvent objects into a ChatMessage
|
|
9
|
+
* with correct MessagePart array. Handles text, reasoning, and tool call
|
|
10
|
+
* accumulation with proper status transitions.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts a stream of AgentEvent objects into a complete ChatMessage.
|
|
15
|
+
* Tracks text, reasoning, and tool call parts with proper status transitions.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const acc = new MessageAccumulator();
|
|
20
|
+
* for await (const event of agentEvents) {
|
|
21
|
+
* acc.apply(event);
|
|
22
|
+
* renderMessage(acc.snapshot()); // in-progress UI update
|
|
23
|
+
* }
|
|
24
|
+
* const message = acc.finalize();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare class MessageAccumulator {
|
|
28
|
+
private readonly messageId;
|
|
29
|
+
private readonly parts;
|
|
30
|
+
private status;
|
|
31
|
+
private currentTextPart;
|
|
32
|
+
private currentReasoningPart;
|
|
33
|
+
private toolCallParts;
|
|
34
|
+
private _finalized;
|
|
35
|
+
constructor(messageId?: ChatId);
|
|
36
|
+
/** Get current message ID */
|
|
37
|
+
get id(): ChatId;
|
|
38
|
+
/**
|
|
39
|
+
* Apply an AgentEvent to accumulate into the message
|
|
40
|
+
* @param event - AgentEvent to process
|
|
41
|
+
* @throws Error if accumulator is already finalized
|
|
42
|
+
*/
|
|
43
|
+
apply(event: AgentEvent): void;
|
|
44
|
+
private handleTextDelta;
|
|
45
|
+
private finalizeCurrentText;
|
|
46
|
+
/**
|
|
47
|
+
* Get a snapshot of the current accumulated message (for streaming UI)
|
|
48
|
+
* @returns ChatMessage with current parts and "streaming" status
|
|
49
|
+
*/
|
|
50
|
+
snapshot(): ChatMessage;
|
|
51
|
+
/**
|
|
52
|
+
* Finalize the accumulator and return the complete ChatMessage
|
|
53
|
+
* @returns Completed ChatMessage with all parts finalized
|
|
54
|
+
* @throws Error if accumulator is already finalized
|
|
55
|
+
*/
|
|
56
|
+
finalize(): ChatMessage;
|
|
57
|
+
/** Check if the accumulator has been finalized */
|
|
58
|
+
get finalized(): boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { MessageAccumulator };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/chat/core.ts
|
|
2
|
+
function createChatId() {
|
|
3
|
+
return crypto.randomUUID();
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/chat/accumulator.ts
|
|
7
|
+
var MessageAccumulator = class {
|
|
8
|
+
messageId;
|
|
9
|
+
parts = [];
|
|
10
|
+
status = "pending";
|
|
11
|
+
currentTextPart = null;
|
|
12
|
+
currentReasoningPart = null;
|
|
13
|
+
toolCallParts = /* @__PURE__ */ new Map();
|
|
14
|
+
_finalized = false;
|
|
15
|
+
constructor(messageId) {
|
|
16
|
+
this.messageId = messageId ?? createChatId();
|
|
17
|
+
}
|
|
18
|
+
/** Get current message ID */
|
|
19
|
+
get id() {
|
|
20
|
+
return this.messageId;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Apply an AgentEvent to accumulate into the message
|
|
24
|
+
* @param event - AgentEvent to process
|
|
25
|
+
* @throws Error if accumulator is already finalized
|
|
26
|
+
*/
|
|
27
|
+
apply(event) {
|
|
28
|
+
if (this._finalized) throw new Error("Cannot apply events to finalized accumulator");
|
|
29
|
+
if (this.status === "pending") {
|
|
30
|
+
this.status = "streaming";
|
|
31
|
+
}
|
|
32
|
+
switch (event.type) {
|
|
33
|
+
case "text_delta":
|
|
34
|
+
this.handleTextDelta(event.text);
|
|
35
|
+
break;
|
|
36
|
+
case "thinking_start":
|
|
37
|
+
this.finalizeCurrentText();
|
|
38
|
+
this.currentReasoningPart = { type: "reasoning", text: "", status: "streaming" };
|
|
39
|
+
this.parts.push(this.currentReasoningPart);
|
|
40
|
+
break;
|
|
41
|
+
case "thinking_delta":
|
|
42
|
+
if (this.currentReasoningPart) {
|
|
43
|
+
this.currentReasoningPart.text += event.text;
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
case "thinking_end":
|
|
47
|
+
if (this.currentReasoningPart) {
|
|
48
|
+
this.currentReasoningPart.status = "complete";
|
|
49
|
+
this.currentReasoningPart = null;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case "tool_call_start": {
|
|
53
|
+
this.finalizeCurrentText();
|
|
54
|
+
const toolPart = {
|
|
55
|
+
type: "tool_call",
|
|
56
|
+
toolCallId: event.toolCallId,
|
|
57
|
+
name: event.toolName,
|
|
58
|
+
args: event.args,
|
|
59
|
+
status: "running"
|
|
60
|
+
};
|
|
61
|
+
this.toolCallParts.set(event.toolCallId, toolPart);
|
|
62
|
+
this.parts.push(toolPart);
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "tool_call_end": {
|
|
66
|
+
const existing = this.toolCallParts.get(event.toolCallId);
|
|
67
|
+
if (existing) {
|
|
68
|
+
existing.result = event.result;
|
|
69
|
+
existing.status = "complete";
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
case "error":
|
|
74
|
+
this.status = "error";
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
handleTextDelta(text) {
|
|
79
|
+
if (!this.currentTextPart) {
|
|
80
|
+
this.currentTextPart = { type: "text", text: "", status: "streaming" };
|
|
81
|
+
this.parts.push(this.currentTextPart);
|
|
82
|
+
}
|
|
83
|
+
this.currentTextPart.text += text;
|
|
84
|
+
}
|
|
85
|
+
finalizeCurrentText() {
|
|
86
|
+
if (this.currentTextPart) {
|
|
87
|
+
this.currentTextPart.status = "complete";
|
|
88
|
+
this.currentTextPart = null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Get a snapshot of the current accumulated message (for streaming UI)
|
|
93
|
+
* @returns ChatMessage with current parts and "streaming" status
|
|
94
|
+
*/
|
|
95
|
+
snapshot() {
|
|
96
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
97
|
+
return {
|
|
98
|
+
id: this.messageId,
|
|
99
|
+
role: "assistant",
|
|
100
|
+
parts: this.parts.map((p) => ({ ...p })),
|
|
101
|
+
status: this.status === "pending" ? "pending" : "streaming",
|
|
102
|
+
createdAt: now,
|
|
103
|
+
updatedAt: now
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Finalize the accumulator and return the complete ChatMessage
|
|
108
|
+
* @returns Completed ChatMessage with all parts finalized
|
|
109
|
+
* @throws Error if accumulator is already finalized
|
|
110
|
+
*/
|
|
111
|
+
finalize() {
|
|
112
|
+
if (this._finalized) throw new Error("Accumulator already finalized");
|
|
113
|
+
this._finalized = true;
|
|
114
|
+
this.finalizeCurrentText();
|
|
115
|
+
if (this.currentReasoningPart) {
|
|
116
|
+
this.currentReasoningPart.status = "complete";
|
|
117
|
+
this.currentReasoningPart = null;
|
|
118
|
+
}
|
|
119
|
+
for (const [, toolPart] of this.toolCallParts) {
|
|
120
|
+
if (toolPart.status === "running" || toolPart.status === "pending") {
|
|
121
|
+
toolPart.status = "error";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (this.status !== "error" && this.status !== "cancelled") {
|
|
125
|
+
this.status = "complete";
|
|
126
|
+
}
|
|
127
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
128
|
+
return {
|
|
129
|
+
id: this.messageId,
|
|
130
|
+
role: "assistant",
|
|
131
|
+
parts: this.parts,
|
|
132
|
+
status: this.status,
|
|
133
|
+
createdAt: now,
|
|
134
|
+
updatedAt: now
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Check if the accumulator has been finalized */
|
|
138
|
+
get finalized() {
|
|
139
|
+
return this._finalized;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export { MessageAccumulator };
|
|
144
|
+
//# sourceMappingURL=accumulator.js.map
|
|
145
|
+
//# sourceMappingURL=accumulator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/chat/core.ts","../../src/chat/accumulator.ts"],"names":[],"mappings":"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","file":"accumulator.js","sourcesContent":["/**\n * @witqq/agent-sdk/chat/core\n *\n * Foundational chat types and utilities: ChatMessage, ChatSession, ChatEvent,\n * IChatProvider, type guards, and AgentEvent↔ChatEvent bridge functions.\n */\n\nimport type {\n AgentEvent,\n Message,\n ToolCall,\n ToolResult,\n ToolDefinition,\n UsageData,\n ModelInfo,\n JSONValue,\n} from \"../types.js\";\n\n// ─── Unique ID ─────────────────────────────────────────────────\n\n/** Branded type for unique identifiers */\nexport type ChatId = string & { readonly __brand: \"ChatId\" };\n\n/**\n * Generate a new unique ChatId (crypto.randomUUID-based)\n * @returns Branded ChatId string\n */\nexport function createChatId(): ChatId {\n return crypto.randomUUID() as ChatId;\n}\n\n/** UUID v4 pattern for ChatId validation */\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\n\n/**\n * Cast a string to ChatId with UUID format validation.\n * Use this instead of manual `as ChatId` type assertions.\n *\n * @param value - String to validate and cast\n * @returns Branded ChatId\n * @throws {TypeError} If value is not a valid UUID v4 format\n *\n * @example\n * ```ts\n * const id = toChatId(\"550e8400-e29b-41d4-a716-446655440000\");\n * ```\n */\nexport function toChatId(value: string): ChatId {\n if (!UUID_RE.test(value)) {\n throw new TypeError(`Invalid ChatId: \"${value}\" is not a valid UUID`);\n }\n return value as ChatId;\n}\n\n/**\n * Accepts either a plain string or branded ChatId for API convenience.\n * Use this in public API signatures so consumers don't need `as ChatId` casts.\n */\nexport type ChatIdLike = string | ChatId;\n\n// ─── Status Types ──────────────────────────────────────────────\n\n/** Lifecycle status of a message part (text, reasoning, etc.) */\nexport type PartStatus = \"pending\" | \"streaming\" | \"complete\" | \"error\";\n/** Lifecycle status of a tool call within a message */\nexport type ToolCallStatus = \"pending\" | \"running\" | \"requires_approval\" | \"complete\" | \"error\" | \"denied\";\n/** Lifecycle status of an entire message */\nexport type MessageStatus = \"pending\" | \"streaming\" | \"complete\" | \"error\" | \"cancelled\";\n/** Lifecycle status of a chat session */\nexport type SessionStatus = \"active\" | \"archived\";\n/** Lifecycle status of the chat runtime */\nexport type RuntimeStatus = \"idle\" | \"streaming\" | \"error\" | \"disposed\";\n\n// ─── Message Parts (union) ─────────────────────────────────────\n\n/** Plain text content part */\nexport interface TextPart { type: \"text\"; text: string; status: PartStatus; }\n/** Model reasoning/thinking content part */\nexport interface ReasoningPart { type: \"reasoning\"; text: string; status: PartStatus; }\n/** Tool invocation part with call ID, arguments, optional result */\nexport interface ToolCallPart { type: \"tool_call\"; toolCallId: string; name: string; args: unknown; result?: unknown; status: ToolCallStatus; error?: string; }\n/** Source reference part (URL citation) */\nexport interface SourcePart { type: \"source\"; url: string; title?: string; status: PartStatus; }\n/** File attachment part (base64-encoded data) */\nexport interface FilePart { type: \"file\"; name: string; mimeType: string; data: string; status: PartStatus; }\n/** Union of all message part types */\nexport type MessagePart = TextPart | ReasoningPart | ToolCallPart | SourcePart | FilePart;\n\n// ─── Chat Message ──────────────────────────────────────────────\n\n/** Role of message author */\nexport type ChatRole = \"user\" | \"assistant\" | \"system\";\n\n/** Metadata attached to messages — useful preset for the TMetadata generic */\nexport interface ChatMessageMetadata {\n model?: string;\n backend?: string;\n usage?: UsageData;\n isSummary?: boolean;\n isArchived?: boolean;\n estimatedTokens?: number;\n custom?: Record<string, unknown>;\n}\n\n/** Message status */\nexport type ChatMessageStatus = MessageStatus;\n\n/** A single chat message — the fundamental unit of conversation */\nexport interface ChatMessage<TMetadata = unknown> {\n id: ChatId;\n role: ChatRole;\n parts: MessagePart[];\n metadata?: TMetadata;\n createdAt: string;\n updatedAt?: string;\n status: MessageStatus;\n}\n\n// ─── Convenience Getters ───────────────────────────────────────\n\n/**\n * Join all TextPart texts in a message\n * @param message - The chat message to extract text from\n * @returns Concatenated text content\n */\nexport function getMessageText(message: ChatMessage): string {\n return message.parts\n .filter((p): p is TextPart => p.type === \"text\")\n .map((p) => p.text)\n .join(\"\");\n}\n\n/**\n * Filter all ToolCallParts from a message\n * @param message - The chat message to extract tool calls from\n * @returns Array of ToolCallPart\n */\nexport function getMessageToolCalls(message: ChatMessage): ToolCallPart[] {\n return message.parts.filter((p): p is ToolCallPart => p.type === \"tool_call\");\n}\n\n/**\n * Join all ReasoningPart texts in a message\n * @param message - The chat message to extract reasoning from\n * @returns Concatenated reasoning content\n */\nexport function getMessageReasoning(message: ChatMessage): string {\n return message.parts\n .filter((p): p is ReasoningPart => p.type === \"reasoning\")\n .map((p) => p.text)\n .join(\"\");\n}\n\n// ─── Supporting Types ──────────────────────────────────────────\n\n/** Options for sending a message */\nexport interface SendOpts { sessionId?: string; model?: string; signal?: AbortSignal; metadata?: Record<string, unknown>; }\n/** Options for creating a new session */\nexport interface CreateSessionOpts { id?: string; title?: string; model?: string; metadata?: Record<string, unknown>; }\n/** Options for listing sessions with pagination */\nexport interface ListOpts { limit?: number; offset?: number; status?: SessionStatus; }\n/** Options for backend execution (model, tokens, tools) */\nexport interface BackendOpts { model: string; signal?: AbortSignal; systemPrompt?: string; temperature?: number; maxTokens?: number; tools?: Record<string, unknown>; providerOptions?: Record<string, unknown>; }\n/** Context passed to tool execute functions */\nexport interface ToolContext { sessionId: string; userId?: string; signal: AbortSignal; }\n/** Configuration for creating a chat runtime */\nexport interface ChatRuntimeConfig { backend: string; model?: string; apiKey?: string; baseUrl?: string; context?: { maxTokens?: number; reserveTokens?: number; strategy?: \"sliding\" | \"summarize\" | \"truncate\"; }; retry?: { maxRetries?: number; initialDelay?: number; backoffFactor?: number; }; providerOptions?: Record<string, unknown>; }\n\n// ─── Chat Session ──────────────────────────────────────────────\n\n/** Session configuration snapshot */\nexport interface ChatSessionConfig {\n model: string;\n backend: string;\n systemPrompt?: string;\n temperature?: number;\n maxTokens?: number;\n}\n\n/** Session metadata */\nexport interface ChatSessionMetadata<TCustom extends Record<string, unknown> = Record<string, unknown>> {\n messageCount: number;\n totalTokens: number;\n tags?: string[];\n custom?: TCustom;\n}\n\n/** Chat session — a conversation with ordered messages */\nexport interface ChatSession<TCustom extends Record<string, unknown> = Record<string, unknown>> {\n id: ChatId;\n title?: string;\n messages: ChatMessage[];\n config: ChatSessionConfig;\n metadata: ChatSessionMetadata<TCustom>;\n status: SessionStatus;\n createdAt: string;\n updatedAt: string;\n backendSessionId?: string;\n /** Subscribe to session changes (for React useSyncExternalStore) */\n subscribe?(callback: () => void): () => void;\n /** Get immutable snapshot of session state (for React useSyncExternalStore) */\n getSnapshot?(): ChatSession<TCustom>;\n /** Last message in the session */\n readonly lastMessage?: ChatMessage;\n}\n\n/** Lightweight session info for listing (without full message array) */\nexport interface SessionInfo {\n id: ChatId;\n title?: string;\n status: SessionStatus;\n messageCount: number;\n lastMessage?: ChatMessage;\n createdAt: string;\n updatedAt: string;\n}\n\n// ─── Chat Events ───────────────────────────────────────────────\n\n/** Events emitted during chat operation */\nexport type ChatEvent =\n | { type: \"message:start\"; messageId: ChatId; role: ChatRole }\n | { type: \"message:delta\"; messageId: ChatId; text: string }\n | { type: \"message:complete\"; messageId: ChatId; message: ChatMessage }\n | {\n type: \"tool:start\";\n messageId: ChatId;\n toolCallId: string;\n toolName: string;\n args: Record<string, unknown>;\n }\n | {\n type: \"tool:complete\";\n messageId: ChatId;\n toolCallId: string;\n toolName: string;\n result: unknown;\n isError?: boolean;\n }\n | { type: \"thinking:start\"; messageId: ChatId }\n | { type: \"thinking:delta\"; messageId: ChatId; text: string }\n | { type: \"thinking:end\"; messageId: ChatId }\n | {\n type: \"permission:request\";\n messageId: ChatId;\n toolName: string;\n toolArgs: Record<string, unknown>;\n }\n | {\n type: \"permission:response\";\n messageId: ChatId;\n toolName: string;\n allowed: boolean;\n }\n | {\n type: \"usage\";\n promptTokens: number;\n completionTokens: number;\n model?: string;\n }\n | { type: \"session:created\"; sessionId: ChatId }\n | { type: \"session:updated\"; sessionId: ChatId }\n | {\n type: \"error\";\n error: string;\n recoverable: boolean;\n messageId?: ChatId;\n }\n | { type: \"typing:start\" }\n | { type: \"typing:end\" }\n | { type: \"heartbeat\" }\n | { type: \"done\"; finalOutput?: string };\n\n/** All possible ChatEvent type strings */\nexport type ChatEventType = ChatEvent[\"type\"];\n\n// ─── Chat Middleware ───────────────────────────────────────────\n\n/** Context passed to ChatMiddleware hooks */\nexport interface ChatMiddlewareContext {\n sessionId: ChatId;\n signal: AbortSignal;\n}\n\n/** Runtime-level middleware for the send/receive lifecycle.\n * Different from EventMiddleware which operates at the event bus level. */\nexport interface ChatMiddleware {\n /** Transform message before sending to backend */\n onBeforeSend?(message: ChatMessage, context: ChatMiddlewareContext): ChatMessage | Promise<ChatMessage>;\n /** Transform/intercept stream events */\n onEvent?(event: ChatEvent, context: ChatMiddlewareContext): ChatEvent | null | Promise<ChatEvent | null>;\n /** Transform completed message after receiving from backend */\n onAfterReceive?(message: ChatMessage, context: ChatMiddlewareContext): ChatMessage | Promise<ChatMessage>;\n /** Intercept errors — return null to suppress, return error to propagate */\n onError?(error: Error, context: ChatMiddlewareContext): Error | null | Promise<Error | null>;\n}\n\n// ─── Chat Provider Abstraction ─────────────────────────────────\n\n/** Options for sending a message to a provider */\nexport interface SendMessageOptions {\n signal?: AbortSignal;\n model?: string;\n context?: Record<string, unknown>;\n /** Additional tools to include in this request */\n tools?: ToolDefinition[];\n}\n\n/** Abstract chat provider — wraps an IAgentService for chat use */\nexport interface IChatProvider {\n readonly name: string;\n sendMessage(\n session: ChatSession,\n message: string,\n options?: SendMessageOptions,\n ): Promise<ChatMessage>;\n streamMessage(\n session: ChatSession,\n message: string,\n options?: SendMessageOptions,\n ): AsyncIterable<ChatEvent>;\n listModels(): Promise<ModelInfo[]>;\n validate(): Promise<{ valid: boolean; errors: string[] }>;\n dispose(): Promise<void>;\n}\n\n// ─── Type Guards ───────────────────────────────────────────────\n\n/**\n * Check if a value is a ChatMessage\n * @param value - Value to check\n * @returns True if value has ChatMessage shape\n */\nexport function isChatMessage(value: unknown): value is ChatMessage {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj.id === \"string\" &&\n typeof obj.role === \"string\" &&\n (obj.role === \"user\" ||\n obj.role === \"assistant\" ||\n obj.role === \"system\") &&\n Array.isArray(obj.parts) &&\n typeof obj.createdAt === \"string\" &&\n typeof obj.status === \"string\"\n );\n}\n\n/**\n * Check if a value is a ChatSession\n * @param value - Value to check\n * @returns True if value has ChatSession shape\n */\nexport function isChatSession(value: unknown): value is ChatSession {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj.id === \"string\" &&\n Array.isArray(obj.messages) &&\n typeof obj.config === \"object\" &&\n obj.config !== null &&\n typeof obj.createdAt === \"string\" &&\n typeof obj.updatedAt === \"string\" &&\n typeof obj.status === \"string\"\n );\n}\n\n/**\n * Check if a value is a MessagePart\n * @param value - Value to check\n * @returns True if value has MessagePart shape\n */\nexport function isMessagePart(value: unknown): value is MessagePart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return (\n typeof obj.type === \"string\" &&\n (obj.type === \"text\" ||\n obj.type === \"reasoning\" ||\n obj.type === \"tool_call\" ||\n obj.type === \"source\" ||\n obj.type === \"file\")\n );\n}\n\n/**\n * Check if a value is a TextPart\n * @param value - Value to check\n * @returns True if value is a TextPart\n */\nexport function isTextPart(value: unknown): value is TextPart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"text\" && typeof obj.text === \"string\";\n}\n\n/**\n * Check if a value is a ToolCallPart\n * @param value - Value to check\n * @returns True if value is a ToolCallPart\n */\nexport function isToolCallPart(value: unknown): value is ToolCallPart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"tool_call\" && typeof obj.toolCallId === \"string\" && typeof obj.name === \"string\";\n}\n\n/**\n * Check if a value is a ReasoningPart\n * @param value - Value to check\n * @returns True if value is a ReasoningPart\n */\nexport function isReasoningPart(value: unknown): value is ReasoningPart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"reasoning\" && typeof obj.text === \"string\";\n}\n\n/**\n * Check if a value is a SourcePart\n * @param value - Value to check\n * @returns True if value is a SourcePart\n */\nexport function isSourcePart(value: unknown): value is SourcePart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"source\" && typeof obj.url === \"string\";\n}\n\n/**\n * Check if a value is a FilePart\n * @param value - Value to check\n * @returns True if value is a FilePart\n */\nexport function isFilePart(value: unknown): value is FilePart {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n return obj.type === \"file\" && typeof obj.name === \"string\" && typeof obj.mimeType === \"string\";\n}\n\n/**\n * Check if a value is a ChatEvent\n * @param value - Value to check\n * @returns True if value has a valid ChatEvent type\n */\nexport function isChatEvent(value: unknown): value is ChatEvent {\n if (typeof value !== \"object\" || value === null) return false;\n const obj = value as Record<string, unknown>;\n const validTypes: ChatEventType[] = [\n \"message:start\",\n \"message:delta\",\n \"message:complete\",\n \"tool:start\",\n \"tool:complete\",\n \"thinking:start\",\n \"thinking:delta\",\n \"thinking:end\",\n \"permission:request\",\n \"permission:response\",\n \"usage\",\n \"session:created\",\n \"session:updated\",\n \"error\",\n \"typing:start\",\n \"typing:end\",\n \"heartbeat\",\n \"done\",\n ];\n return validTypes.includes(obj.type as ChatEventType);\n}\n\n// ─── Agent Event Adapter ───────────────────────────────────────\n\n/**\n * Map a single AgentEvent to a ChatEvent (or null if no mapping)\n * @param event - The AgentEvent to convert\n * @param messageId - ChatId to associate with the event\n * @returns Corresponding ChatEvent or null if unmappable\n */\nexport function agentEventToChatEvent(\n event: AgentEvent,\n messageId: ChatId,\n): ChatEvent | null {\n switch (event.type) {\n case \"text_delta\":\n return { type: \"message:delta\", messageId, text: event.text };\n case \"thinking_start\":\n return { type: \"thinking:start\", messageId };\n case \"thinking_delta\":\n return { type: \"thinking:delta\", messageId, text: event.text };\n case \"thinking_end\":\n return { type: \"thinking:end\", messageId };\n case \"tool_call_start\":\n return {\n type: \"tool:start\",\n messageId,\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n args: event.args as Record<string, unknown>,\n };\n case \"tool_call_end\":\n return {\n type: \"tool:complete\",\n messageId,\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n result: event.result,\n };\n case \"permission_request\":\n return {\n type: \"permission:request\",\n messageId,\n toolName: event.request.toolName,\n toolArgs: event.request.toolArgs,\n };\n case \"permission_response\":\n return {\n type: \"permission:response\",\n messageId,\n toolName: event.toolName,\n allowed: event.decision.allowed,\n };\n case \"usage_update\":\n return {\n type: \"usage\",\n promptTokens: event.promptTokens,\n completionTokens: event.completionTokens,\n model: event.model,\n };\n case \"error\":\n return {\n type: \"error\",\n error: event.error,\n recoverable: event.recoverable,\n messageId,\n };\n case \"heartbeat\":\n return { type: \"heartbeat\" };\n case \"ask_user\":\n case \"ask_user_response\":\n case \"session_info\":\n case \"done\":\n return null;\n default:\n return null;\n }\n}\n\n/**\n * Convert AgentEvent async iterable to ChatEvent async iterable\n * @param events - Source agent events\n * @param messageId - ChatId to associate with converted events\n * @returns Async iterable of ChatEvent (nulls filtered out)\n */\nexport async function* adaptAgentEvents(\n events: AsyncIterable<AgentEvent>,\n messageId: ChatId,\n): AsyncIterable<ChatEvent> {\n for await (const event of events) {\n const chatEvent = agentEventToChatEvent(event, messageId);\n if (chatEvent !== null) {\n yield chatEvent;\n }\n }\n}\n\n/**\n * Map a ChatEvent back to an AgentEvent for accumulator consumption.\n * Returns null for events that don't map to accumulator-relevant AgentEvents\n * (e.g. message:start, message:complete, usage, permission:*, heartbeat).\n *\n * @param event - The ChatEvent to convert\n * @returns Corresponding AgentEvent or null if not accumulator-relevant\n */\nexport function chatEventToAgentEvent(event: ChatEvent): AgentEvent | null {\n switch (event.type) {\n case \"message:delta\":\n return { type: \"text_delta\", text: event.text };\n case \"thinking:start\":\n return { type: \"thinking_start\" };\n case \"thinking:delta\":\n return { type: \"thinking_delta\", text: event.text };\n case \"thinking:end\":\n return { type: \"thinking_end\" };\n case \"tool:start\":\n return {\n type: \"tool_call_start\",\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n args: event.args as JSONValue,\n };\n case \"tool:complete\":\n return {\n type: \"tool_call_end\",\n toolCallId: event.toolCallId,\n toolName: event.toolName,\n result: event.result as JSONValue,\n };\n case \"error\":\n return { type: \"error\", error: event.error, recoverable: event.recoverable };\n default:\n return null;\n }\n}\n\n// ─── Message Conversion ────────────────────────────────────────\n\n/**\n * Convert a ChatMessage to agent-sdk Message format\n * @param message - The ChatMessage to convert\n * @returns agent-sdk Message\n */\nexport function toAgentMessage(message: ChatMessage): Message {\n const textContent = getMessageText(message);\n const toolCallParts = getMessageToolCalls(message);\n\n switch (message.role) {\n case \"user\":\n return { role: \"user\", content: textContent };\n case \"assistant\": {\n const toolCalls: ToolCall[] | undefined = toolCallParts.length > 0\n ? toolCallParts.map((p) => ({ id: p.toolCallId, name: p.name, args: p.args as JSONValue }))\n : undefined;\n return {\n role: \"assistant\",\n content: textContent,\n toolCalls,\n };\n }\n case \"system\":\n return { role: \"system\", content: textContent };\n }\n}\n\n/**\n * Convert an agent-sdk Message to ChatMessage\n * @param message - The agent-sdk Message to convert\n * @param id - Optional ChatId (auto-generated if omitted)\n * @returns ChatMessage with status \"complete\"\n */\nexport function fromAgentMessage(message: Message, id?: ChatId): ChatMessage {\n const chatId = id ?? createChatId();\n const now = new Date().toISOString();\n\n const parts: MessagePart[] = [];\n\n // Build text content\n const textContent =\n typeof message.content === \"string\"\n ? message.content\n : Array.isArray(message.content)\n ? message.content\n .filter((part) => part.type === \"text\")\n .map((part) => part.text)\n .join(\"\\n\")\n : (message.content ?? \"\");\n\n if (textContent) {\n parts.push({ type: \"text\", text: textContent, status: \"complete\" });\n }\n\n // Add tool calls from assistant messages\n if (message.role === \"assistant\" && message.toolCalls) {\n for (const tc of message.toolCalls) {\n parts.push({\n type: \"tool_call\",\n toolCallId: tc.id,\n name: tc.name,\n args: tc.args,\n status: \"complete\",\n });\n }\n }\n\n // Add tool results — map 'tool' role to 'assistant'\n if (message.role === \"tool\" && message.toolResults) {\n for (const tr of message.toolResults) {\n parts.push({\n type: \"tool_call\",\n toolCallId: tr.toolCallId,\n name: tr.name,\n args: {},\n result: tr.result,\n status: \"complete\",\n });\n }\n }\n\n // Ensure at least an empty text part for empty messages\n if (parts.length === 0) {\n parts.push({ type: \"text\", text: \"\", status: \"complete\" });\n }\n\n const role: ChatRole = message.role === \"tool\" ? \"assistant\" : message.role;\n\n return {\n id: chatId,\n role,\n parts,\n createdAt: now,\n status: \"complete\",\n };\n}\n\n/**\n * Extract ToolResults from ToolCallParts that have results\n * @param message - The ChatMessage to extract results from\n * @returns Array of ToolResult for completed tool calls\n */\nexport function extractToolResults(message: ChatMessage): ToolResult[] {\n return getMessageToolCalls(message)\n .filter((p) => p.result !== undefined)\n .map((p) => ({\n toolCallId: p.toolCallId,\n name: p.name,\n result: p.result as JSONValue,\n isError: p.status === \"error\" ? true : undefined,\n }));\n}\n","/**\n * @witqq/agent-sdk/chat/accumulator\n *\n * MessageAccumulator converts a stream of AgentEvent objects into a ChatMessage\n * with correct MessagePart array. Handles text, reasoning, and tool call\n * accumulation with proper status transitions.\n */\n\nimport type { AgentEvent } from \"../types.js\";\nimport type { ChatMessage, ChatId, MessagePart, TextPart, ReasoningPart, ToolCallPart } from \"./core.js\";\nimport { createChatId } from \"./core.js\";\n\n/**\n * Converts a stream of AgentEvent objects into a complete ChatMessage.\n * Tracks text, reasoning, and tool call parts with proper status transitions.\n *\n * @example\n * ```typescript\n * const acc = new MessageAccumulator();\n * for await (const event of agentEvents) {\n * acc.apply(event);\n * renderMessage(acc.snapshot()); // in-progress UI update\n * }\n * const message = acc.finalize();\n * ```\n */\nexport class MessageAccumulator {\n private readonly messageId: ChatId;\n private readonly parts: MessagePart[] = [];\n private status: \"pending\" | \"streaming\" | \"complete\" | \"error\" | \"cancelled\" = \"pending\";\n private currentTextPart: TextPart | null = null;\n private currentReasoningPart: ReasoningPart | null = null;\n private toolCallParts = new Map<string, ToolCallPart>();\n private _finalized = false;\n\n constructor(messageId?: ChatId) {\n this.messageId = messageId ?? createChatId();\n }\n\n /** Get current message ID */\n get id(): ChatId { return this.messageId; }\n\n /**\n * Apply an AgentEvent to accumulate into the message\n * @param event - AgentEvent to process\n * @throws Error if accumulator is already finalized\n */\n apply(event: AgentEvent): void {\n if (this._finalized) throw new Error(\"Cannot apply events to finalized accumulator\");\n\n if (this.status === \"pending\") {\n this.status = \"streaming\";\n }\n\n switch (event.type) {\n case \"text_delta\":\n this.handleTextDelta(event.text);\n break;\n case \"thinking_start\":\n this.finalizeCurrentText();\n this.currentReasoningPart = { type: \"reasoning\", text: \"\", status: \"streaming\" };\n this.parts.push(this.currentReasoningPart);\n break;\n case \"thinking_delta\":\n if (this.currentReasoningPart) {\n this.currentReasoningPart.text += event.text;\n }\n break;\n case \"thinking_end\":\n if (this.currentReasoningPart) {\n this.currentReasoningPart.status = \"complete\";\n this.currentReasoningPart = null;\n }\n break;\n case \"tool_call_start\": {\n this.finalizeCurrentText();\n const toolPart: ToolCallPart = {\n type: \"tool_call\",\n toolCallId: event.toolCallId,\n name: event.toolName,\n args: event.args,\n status: \"running\",\n };\n this.toolCallParts.set(event.toolCallId, toolPart);\n this.parts.push(toolPart);\n break;\n }\n case \"tool_call_end\": {\n const existing = this.toolCallParts.get(event.toolCallId);\n if (existing) {\n existing.result = event.result;\n existing.status = \"complete\";\n }\n break;\n }\n case \"error\":\n this.status = \"error\";\n break;\n case \"done\":\n break;\n // Other events (heartbeat, ask_user, etc.) — ignore\n }\n }\n\n private handleTextDelta(text: string): void {\n if (!this.currentTextPart) {\n this.currentTextPart = { type: \"text\", text: \"\", status: \"streaming\" };\n this.parts.push(this.currentTextPart);\n }\n this.currentTextPart.text += text;\n }\n\n private finalizeCurrentText(): void {\n if (this.currentTextPart) {\n this.currentTextPart.status = \"complete\";\n this.currentTextPart = null;\n }\n }\n\n /**\n * Get a snapshot of the current accumulated message (for streaming UI)\n * @returns ChatMessage with current parts and \"streaming\" status\n */\n snapshot(): ChatMessage {\n const now = new Date().toISOString();\n return {\n id: this.messageId,\n role: \"assistant\",\n parts: this.parts.map(p => ({ ...p })),\n status: this.status === \"pending\" ? \"pending\" : \"streaming\",\n createdAt: now,\n updatedAt: now,\n };\n }\n\n /**\n * Finalize the accumulator and return the complete ChatMessage\n * @returns Completed ChatMessage with all parts finalized\n * @throws Error if accumulator is already finalized\n */\n finalize(): ChatMessage {\n if (this._finalized) throw new Error(\"Accumulator already finalized\");\n this._finalized = true;\n\n // Finalize any open parts\n this.finalizeCurrentText();\n if (this.currentReasoningPart) {\n this.currentReasoningPart.status = \"complete\";\n this.currentReasoningPart = null;\n }\n\n // Mark incomplete tool calls as error\n for (const [, toolPart] of this.toolCallParts) {\n if (toolPart.status === \"running\" || toolPart.status === \"pending\") {\n toolPart.status = \"error\";\n }\n }\n\n // Set final message status\n if (this.status !== \"error\" && this.status !== \"cancelled\") {\n this.status = \"complete\";\n }\n\n const now = new Date().toISOString();\n return {\n id: this.messageId,\n role: \"assistant\",\n parts: this.parts,\n status: this.status,\n createdAt: now,\n updatedAt: now,\n };\n }\n\n /** Check if the accumulator has been finalized */\n get finalized(): boolean { return this._finalized; }\n}\n"]}
|