import { v4 as uuidv4 } from "uuid"; import { APIMessage, CompletionRequest, CompletionResult, CompletionUsage, MessageContent, MessageError, MessageRole, MessageType, SerializedMessage, ToolCall, ToolResult, } from "./types"; export class Message { readonly id: string; readonly index: number; private readonly originalType: MessageType; private currentType: MessageType; private _payload?: CompletionRequest | CompletionResult | ToolResult; readonly error?: MessageError; constructor( id: string, index: number, type: MessageType, payload?: CompletionRequest | CompletionResult | ToolResult, error?: MessageError, ) { this.id = id; this.index = index; this.originalType = type; this.currentType = type; this._payload = payload; this.error = error; } // Convenience factory methods static system(content: string, index = 0): Message { return new Message(uuidv4(), index, MessageType.CompletionRequest, { role: MessageRole.SYSTEM, content, } as CompletionRequest); } static request(content: string, index = 0, attachments?: MessageContent[]): Message { if (attachments && attachments.length > 0) { const parts: MessageContent[] = [{ type: "text", text: content }, ...attachments]; return new Message(uuidv4(), index, MessageType.CompletionRequest, { role: MessageRole.USER, content: parts, } as CompletionRequest); } return new Message(uuidv4(), index, MessageType.CompletionRequest, { role: MessageRole.USER, content, } as CompletionRequest); } static response(content: string, index = 0, usage?: CompletionUsage): Message { return new Message(uuidv4(), index, MessageType.CompletionResult, { id: uuidv4(), choices: [{ index: 0, message: { role: MessageRole.ASSISTANT, content } }], usage, } as CompletionResult); } static toolCallResponse(content: string, toolCalls: ToolCall[], index = 0, usage?: CompletionUsage): Message { return new Message(uuidv4(), index, MessageType.CompletionResult, { id: uuidv4(), choices: [{ index: 0, message: { role: MessageRole.ASSISTANT, content, tool_calls: toolCalls }, finish_reason: "tool_calls" }], usage, } as CompletionResult); } static error(content: string, index = 0): Message { return new Message(uuidv4(), index, MessageType.CompletionError, undefined, { code: "error", message: content, }); } public get payload() { return this._payload; } public get type(): MessageType { return this.currentType; } // Serialization public get serialized(): SerializedMessage { const s: SerializedMessage = { id: this.id, index: this.index, originalType: this.originalType, currentType: this.currentType, payload: this._payload ? JSON.parse(JSON.stringify(this._payload)) : undefined, }; if (this.error) { s.error = this.error; } return s; } public static deserialize(serialized: SerializedMessage): Message { const message = new Message( serialized.id, serialized.index, serialized.originalType, serialized.payload ? JSON.parse(JSON.stringify(serialized.payload)) : undefined, serialized.error, ); message.currentType = serialized.currentType; return message; } public withIndex(index: number): Message { if (this.index === index) return this; const m = new Message(this.id, index, this.originalType, this._payload, this.error); m.currentType = this.currentType; return m; } public clone(): Message { return Message.deserialize(this.serialized); } // Role public get role(): MessageRole | undefined { switch (this.originalType) { case MessageType.CompletionRequest: return (this._payload as CompletionRequest)?.role; case MessageType.CompletionResult: return (this._payload as CompletionResult)?.choices?.[0]?.message?.role; case MessageType.CompletionError: return MessageRole.ASSISTANT; case MessageType.ToolResult: return (this._payload as ToolResult)?.role ?? MessageRole.TOOL; default: return undefined; } } public set role(role: MessageRole) { switch (this.originalType) { case MessageType.CompletionRequest: (this._payload as CompletionRequest).role = role; break; case MessageType.CompletionResult: (this._payload as CompletionResult).choices?.forEach((choice) => { choice.message.role = role; }); break; case MessageType.ToolResult: (this._payload as any).role = role; break; } switch (role) { case MessageRole.ASSISTANT: this.currentType = MessageType.CompletionResult; break; case MessageRole.USER: case MessageRole.SYSTEM: case MessageRole.DEVELOPER: this.currentType = MessageType.CompletionRequest; break; case MessageRole.TOOL: this.currentType = MessageType.ToolResult; break; default: this.currentType = MessageType.CompletionResult; break; } } // Content public get content(): string { switch (this.originalType) { case MessageType.CompletionRequest: { const payload = this._payload as CompletionRequest; if (!payload?.content) return ""; if (typeof payload.content === "string") return payload.content; const textPart = payload.content.find((c) => c.type === "text"); return textPart?.text || ""; } case MessageType.CompletionResult: return (this._payload as CompletionResult)?.choices?.[0]?.message?.content ?? ""; case MessageType.ToolResult: return (this._payload as ToolResult)?.content ?? ""; default: return this.error?.message || ""; } } public set content(content: string) { switch (this.originalType) { case MessageType.CompletionRequest: { const payload = this._payload as CompletionRequest; if (typeof payload.content === "string" || payload.content === null) { this._payload = { ...payload, content } as CompletionRequest; } else if (Array.isArray(payload.content)) { const updated = JSON.parse(JSON.stringify(payload.content)); if (updated.length > 0 && updated[0].type === "text") { updated[0].text = content; } else { updated.unshift({ type: "text", text: content }); } this._payload = { ...payload, content: updated } as CompletionRequest; } break; } case MessageType.ToolResult: this._payload = { ...this._payload, content } as ToolResult; break; case MessageType.CompletionResult: { const result = this._payload as CompletionResult; const choices = result?.choices?.map((c) => ({ ...c })) ?? []; if (choices[0]) { choices[0].message = { ...choices[0].message, content }; } this._payload = { ...result, choices } as CompletionResult; break; } } } // Attachments (non-text content parts) public get attachments(): MessageContent[] { if (this.originalType !== MessageType.CompletionRequest) return []; const payload = this._payload as CompletionRequest; if (!Array.isArray(payload?.content)) return []; return payload.content.filter((c) => c.type !== "text"); } public set attachments(parts: MessageContent[]) { if (this.originalType !== MessageType.CompletionRequest) return; const payload = this._payload as CompletionRequest; const text = this.content; if (parts.length === 0) { this._payload = { ...payload, content: text } as CompletionRequest; } else { this._payload = { ...payload, content: [{ type: "text", text } as MessageContent, ...parts], } as CompletionRequest; } } // Tool calls public get toolCalls(): ToolCall[] | undefined { if (this.originalType === MessageType.CompletionResult) { const calls = (this._payload as CompletionResult)?.choices?.map((c) => c.message.tool_calls ?? []).flat(); return calls && calls.length > 0 ? calls : undefined; } if (this.originalType === MessageType.CompletionRequest) { const calls = (this._payload as CompletionRequest)?.tool_calls; return calls && calls.length > 0 ? calls : undefined; } return undefined; } public get toolCallId(): string | undefined { if (this.originalType === MessageType.ToolResult || this.currentType === MessageType.ToolResult) { return (this._payload as ToolResult)?.tool_call_id; } if (this.originalType === MessageType.CompletionRequest) { return (this._payload as CompletionRequest)?.tool_call_id; } return undefined; } public set toolCallId(id: string) { if (this.originalType === MessageType.ToolResult) { (this._payload as ToolResult).tool_call_id = id; } else if (this.originalType === MessageType.CompletionRequest) { (this._payload as CompletionRequest).tool_call_id = id; } } public get isToolCallError(): boolean | undefined { if (this.originalType === MessageType.ToolResult || this.currentType === MessageType.ToolResult) { return !!(this._payload as ToolResult)?.isError; } return undefined; } public setToolCallError(isError: boolean | undefined): void { if (this.originalType === MessageType.ToolResult || this.currentType === MessageType.ToolResult) { this._payload = { ...this._payload, isError } as ToolResult; } } public get finishReasons(): string | undefined { if (this.originalType === MessageType.CompletionResult) { return (this._payload as CompletionResult)?.choices?.find((c) => c.finish_reason)?.finish_reason; } return undefined; } // Usage public get usage(): CompletionUsage | undefined { if (this.originalType === MessageType.CompletionResult) { return (this._payload as CompletionResult)?.usage; } return undefined; } public set usage(usage: CompletionUsage | undefined) { if (this.originalType === MessageType.CompletionResult) { (this._payload as CompletionResult).usage = usage; } } // Batch helpers static serializeAll(messages: Message[]): SerializedMessage[] { return messages.map((m) => m.serialized); } static deserializeAll(data: SerializedMessage[]): Message[] { return data.map((d) => Message.deserialize(d)); } /** * Convert to OpenAI-compatible API format for chat completions. * Excludes error messages. */ static toAPIMessages(messages: Message[]): APIMessage[] { return messages .filter((m) => m.type !== MessageType.CompletionError) .map((m): APIMessage => { // When role has been changed, currentType differs from originalType — // fall back to a generic conversion using the public getters. if (m.currentType !== m.originalType) { const msg: APIMessage = { role: m.role ?? MessageRole.ASSISTANT, content: m.content }; if (m.toolCalls && m.toolCalls.length > 0) msg.tool_calls = m.toolCalls; if (m.toolCallId) msg.tool_call_id = m.toolCallId; return msg; } switch (m.originalType) { case MessageType.CompletionRequest: { const p = m._payload as CompletionRequest; const msg: APIMessage = { role: p.role, content: p.content }; if (p.tool_calls && p.tool_calls.length > 0) msg.tool_calls = p.tool_calls; if (p.tool_call_id) msg.tool_call_id = p.tool_call_id; return msg; } case MessageType.CompletionResult: { const choice = (m._payload as CompletionResult)?.choices?.[0]?.message; const msg: APIMessage = { role: choice?.role ?? MessageRole.ASSISTANT, content: choice?.content ?? "", }; if (choice?.tool_calls && choice.tool_calls.length > 0) { msg.tool_calls = choice.tool_calls; } return msg; } case MessageType.ToolResult: { const p = m._payload as ToolResult; return { role: MessageRole.TOOL, content: p.content, tool_call_id: p.tool_call_id, }; } default: return { role: MessageRole.ASSISTANT, content: m.content }; } }); } /** * Backward-compatible deserialization for old { role, content } format. * Detects whether data is the new serialized format or old flat format. */ static fromLegacy( data: SerializedMessage | { role: string; content: string | null; tool_calls?: ToolCall[]; tool_call_id?: string }, index: number, ): Message { // New format has originalType if ("originalType" in data && data.originalType) { return Message.deserialize(data as SerializedMessage); } // Legacy { role, content } format const legacy = data as { role: string; content: string | null; tool_calls?: ToolCall[]; tool_call_id?: string }; if (legacy.tool_calls && legacy.tool_calls.length > 0) { return new Message(uuidv4(), index, MessageType.CompletionResult, { id: uuidv4(), choices: [ { index: 0, message: { role: MessageRole.ASSISTANT, content: (legacy.content as string) ?? "", tool_calls: legacy.tool_calls, }, }, ], }); } if (legacy.role === "tool" && legacy.tool_call_id) { return new Message(uuidv4(), index, MessageType.ToolResult, { role: MessageRole.TOOL, content: (legacy.content as string) ?? "", tool_call_id: legacy.tool_call_id, }); } const role = (legacy.role as MessageRole) ?? MessageRole.USER; if (role === MessageRole.ASSISTANT) { return new Message(uuidv4(), index, MessageType.CompletionResult, { id: uuidv4(), choices: [{ index: 0, message: { role: MessageRole.ASSISTANT, content: (legacy.content as string) ?? "" } }], }); } return new Message(uuidv4(), index, MessageType.CompletionRequest, { role, content: legacy.content, } as CompletionRequest); } static fromLegacyAll(data: any[]): Message[] { return data.map((d, i) => Message.fromLegacy(d, i)); } }