Files
makelore/src/lib/opencode-message.ts
inman 80e8386fa6
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled
feat: update Makelore modules and conversations
2026-07-31 10:08:41 +08:00

923 lines
30 KiB
TypeScript

import type {
AttachedFileMeta,
ContentBlock,
RawMessage,
RuntimeToolEvidence,
ToolStatus,
} from '@/types/chat';
const EMPTY_ASSISTANT_RESPONSE_MESSAGE = '模型没有返回任何内容。请重试,或切换到支持图片输入的模型。';
function toMs(value: unknown): number | undefined {
if (typeof value === 'number' && Number.isFinite(value)) {
return value < 1e12 ? value * 1000 : value;
}
if (typeof value === 'string' && value.trim()) {
const numeric = Number(value);
if (Number.isFinite(numeric)) {
return numeric < 1e12 ? numeric * 1000 : numeric;
}
const date = new Date(value);
if (!Number.isNaN(date.getTime())) {
return date.getTime();
}
}
return undefined;
}
function getRecord(input: unknown): Record<string, unknown> | null {
return input && typeof input === 'object' && !Array.isArray(input)
? input as Record<string, unknown>
: null;
}
function getErrorMessage(input: unknown): string | undefined {
if (typeof input === 'string') {
const trimmed = input.trim();
return trimmed || undefined;
}
const record = getRecord(input);
if (!record) return undefined;
const data = getRecord(record.data);
const nestedError = getRecord(record.error);
const metadata = getRecord(record.metadata);
const candidates = [
data?.message,
data?.error,
nestedError?.message,
record.message,
record.errorMessage,
record.error_message,
record.detail,
metadata?.message,
];
for (const candidate of candidates) {
const message = getErrorMessage(candidate);
if (message) return message;
}
if (typeof record.data === 'string') {
const rawData = record.data.trim();
if (!rawData) return undefined;
try {
const parsed = JSON.parse(rawData) as unknown;
return getErrorMessage(parsed) ?? rawData;
} catch {
return rawData;
}
}
return undefined;
}
function normalizeRole(input: unknown): RawMessage['role'] {
switch (input) {
case 'user':
case 'assistant':
case 'system':
case 'toolresult':
return input;
default:
return 'assistant';
}
}
function basenameFromPath(path: string): string {
const parts = path.split(/[\\/]/).filter(Boolean);
return parts.at(-1) ?? path;
}
function normalizeTimestamp(record: Record<string, unknown>): number | undefined {
const time = getRecord(record.time);
return toMs(time?.created)
?? toMs(record.createdAt)
?? toMs(record.updatedAt)
?? toMs(record.timestamp);
}
function hasValue(value: unknown): boolean {
if (value === true) return true;
if (typeof value === 'number' && Number.isFinite(value)) return true;
if (typeof value === 'string' && value.trim()) return true;
return false;
}
function hasCompletedAssistantSignal(
info: Record<string, unknown>,
input: Record<string, unknown>,
parts: unknown[],
): boolean {
const time = getRecord(info.time);
if (
hasValue(time?.completed)
|| hasValue(info.completed)
|| hasValue(info.completedAt)
|| hasValue(input.completed)
|| hasValue(input.completedAt)
) {
return true;
}
return parts.some((part) => getRecord(part)?.type === 'step-finish');
}
function normalizeToolStatus(input: unknown): ToolStatus['status'] {
switch (input) {
case 'completed':
return 'completed';
case 'error':
case 'errored':
return 'error';
default:
return 'running';
}
}
function sanitizeToolEvidenceText(value: unknown, limit = 600): string | undefined {
const redact = (text: string) => text
.replace(/(authorization|bearer|token|secret|password|api[_-]?key)(\s*[:=]\s*|\s+)[^\s,;]+/gi, '$1=[REDACTED]')
.replace(/\s+/g, ' ')
.trim();
const visit = (candidate: unknown, depth = 0): string => {
if (depth > 3 || candidate === null || candidate === undefined) return '';
if (typeof candidate === 'string') return candidate;
if (typeof candidate === 'number' || typeof candidate === 'boolean') return String(candidate);
if (Array.isArray(candidate)) {
return candidate.map((item) => visit(item, depth + 1)).filter(Boolean).join(' ');
}
const record = getRecord(candidate);
if (!record) return '';
return Object.entries(record)
.filter(([key]) => !/(authorization|bearer|token|secret|password|api[_-]?key|cookie)/i.test(key))
.map(([key, item]) => `${key}: ${visit(item, depth + 1)}`)
.filter(Boolean)
.join('; ');
};
const text = redact(visit(value));
return text ? text.slice(0, limit) : undefined;
}
function getToolExitCode(state: Record<string, unknown> | null): number | undefined {
const metadata = getRecord(state?.metadata);
const candidates = [
state?.exitCode,
state?.exit_code,
state?.exit,
metadata?.exitCode,
metadata?.exit_code,
metadata?.exit,
];
for (const candidate of candidates) {
const numeric = typeof candidate === 'number'
? candidate
: typeof candidate === 'string' && candidate.trim()
? Number(candidate)
: Number.NaN;
if (Number.isInteger(numeric)) return numeric;
}
return undefined;
}
function normalizeToolEvidence(part: Record<string, unknown>): RuntimeToolEvidence | null {
if (part.type !== 'tool' || typeof part.tool !== 'string' || !part.tool.trim()) return null;
const state = getRecord(part.state);
const time = getRecord(state?.time);
const startedAt = toMs(time?.start);
const endedAt = toMs(time?.end);
return {
id: typeof part.id === 'string' ? part.id : undefined,
toolCallId: typeof part.callID === 'string' ? part.callID : undefined,
name: part.tool,
status: normalizeToolStatus(state?.status),
inputSummary: sanitizeToolEvidenceText(state?.input),
outputExcerpt: sanitizeToolEvidenceText(state?.output ?? state?.error),
exitCode: getToolExitCode(state),
startedAt,
endedAt,
};
}
function mergeToolEvidence(
current: RuntimeToolEvidence[] | undefined,
next: RuntimeToolEvidence | null,
): RuntimeToolEvidence[] | undefined {
if (!next) return current;
const combined = [...(current ?? [])];
const key = next.toolCallId ?? next.id ?? `${next.name}:${next.startedAt ?? ''}`;
const index = combined.findIndex((item) => (
(item.toolCallId ?? item.id ?? `${item.name}:${item.startedAt ?? ''}`) === key
));
if (index >= 0) {
combined[index] = { ...combined[index], ...next };
} else {
combined.push(next);
}
return combined;
}
function normalizeLegacyBlocks(content: unknown): ContentBlock[] {
if (typeof content === 'string') {
return [{ type: 'text', text: content }];
}
if (!Array.isArray(content)) return [];
const blocks: ContentBlock[] = [];
for (const block of content) {
const record = getRecord(block);
if (!record) continue;
const type = record.type;
if (type === 'text' && typeof record.text === 'string') {
blocks.push({ type: 'text', text: record.text, id: typeof record.id === 'string' ? record.id : undefined });
continue;
}
if (type === 'thinking' && typeof record.thinking === 'string') {
blocks.push({ type: 'thinking', thinking: record.thinking, id: typeof record.id === 'string' ? record.id : undefined });
continue;
}
if (type === 'reasoning') {
const reasoning = typeof record.text === 'string'
? record.text
: typeof record.reasoning === 'string'
? record.reasoning
: typeof record.reasoning_content === 'string'
? record.reasoning_content
: typeof record.reasoning_text === 'string'
? record.reasoning_text
: undefined;
if (reasoning) {
blocks.push({ type: 'thinking', thinking: reasoning, id: typeof record.id === 'string' ? record.id : undefined });
}
continue;
}
if ((type === 'tool_use' || type === 'toolCall') && typeof record.name === 'string') {
blocks.push({
type: 'tool_use',
id: typeof record.id === 'string' ? record.id : undefined,
name: record.name,
input: record.input ?? record.arguments ?? {},
});
continue;
}
if ((type === 'image' || type === 'file') && typeof record.url === 'string' && typeof record.mimeType === 'string') {
blocks.push({
type: 'image',
url: record.url,
mimeType: record.mimeType,
alt: typeof record.alt === 'string' ? record.alt : undefined,
id: typeof record.id === 'string' ? record.id : undefined,
});
}
}
return blocks;
}
function normalizeLegacyMessage(input: Record<string, unknown>): RawMessage {
const contentBlocks = normalizeLegacyBlocks(input.content);
const topLevelReasoning = typeof input.reasoning === 'string'
? input.reasoning
: typeof input.reasoning_content === 'string'
? input.reasoning_content
: typeof input.reasoning_text === 'string'
? input.reasoning_text
: undefined;
if (topLevelReasoning?.trim()) {
contentBlocks.unshift({ type: 'thinking', thinking: topLevelReasoning });
}
if (contentBlocks.length > 0) {
return {
id: typeof input.id === 'string' ? input.id : undefined,
role: normalizeRole(input.role),
timestamp: normalizeTimestamp(input),
content: contentBlocks,
};
}
if (typeof input.content === 'string') {
return {
id: typeof input.id === 'string' ? input.id : undefined,
role: normalizeRole(input.role),
timestamp: normalizeTimestamp(input),
content: [{ type: 'text', text: input.content }],
};
}
const fallbackText = typeof input.text === 'string'
? input.text
: typeof input.message === 'string'
? input.message
: JSON.stringify(input);
return {
id: typeof input.id === 'string' ? input.id : undefined,
role: normalizeRole(input.role),
timestamp: normalizeTimestamp(input),
content: [{ type: 'text', text: fallbackText }],
};
}
function normalizeFilePart(part: Record<string, unknown>): { blocks: ContentBlock[]; attachments: AttachedFileMeta[] } {
const url = typeof part.url === 'string' ? part.url : undefined;
const mimeType = typeof part.mime === 'string' && part.mime.trim() ? part.mime : 'application/octet-stream';
const source = getRecord(part.source);
const filePath = source?.type === 'file' && typeof source.path === 'string' ? source.path : undefined;
const fileName = typeof part.filename === 'string' && part.filename.trim()
? part.filename
: filePath
? basenameFromPath(filePath)
: url
? basenameFromPath(url)
: 'file';
const attachments: AttachedFileMeta[] = [{
fileName,
mimeType,
fileSize: 0,
preview: null,
filePath,
source: 'message-ref',
}];
if (mimeType.trim().toLowerCase().startsWith('image/') && url) {
return {
blocks: [{
type: 'image',
id: typeof part.id === 'string' ? part.id : undefined,
url,
mimeType,
alt: fileName,
}],
attachments,
};
}
return { blocks: [], attachments };
}
function normalizePart(part: unknown): { blocks: ContentBlock[]; attachments: AttachedFileMeta[] } {
const record = getRecord(part);
if (!record || typeof record.type !== 'string') {
return { blocks: [], attachments: [] };
}
if (record.type === 'text' && typeof record.text === 'string') {
return {
blocks: [{
type: 'text',
id: typeof record.id === 'string' ? record.id : undefined,
text: record.text,
...(record.synthetic === true ? { synthetic: true as const } : {}),
}],
attachments: [],
};
}
if (record.type === 'reasoning' && typeof record.text === 'string') {
return {
blocks: [{
type: 'thinking',
id: typeof record.id === 'string' ? record.id : undefined,
thinking: record.text,
}],
attachments: [],
};
}
if (record.type === 'tool' && typeof record.tool === 'string') {
const state = getRecord(record.state);
return {
blocks: [{
type: 'tool_use',
id: typeof record.callID === 'string' ? record.callID : typeof record.id === 'string' ? record.id : undefined,
name: record.tool,
input: state?.input ?? {},
}],
attachments: [],
};
}
if (record.type === 'file') {
return normalizeFilePart(record);
}
return { blocks: [], attachments: [] };
}
const COMMAND_HISTORY_PART_ID = /^prt[A-Za-z0-9_-]+$/u;
const COMMAND_HISTORY_NAME = /^[^\s/][^\s]*$/u;
const COMMAND_HISTORY_CONTROL = /\p{Cc}/u;
interface CommandHistorySelection {
invocationBlock?: ContentBlock;
parts: unknown[];
}
function isSafeCommandHistoryContextPart(input: unknown): boolean {
const record = getRecord(input);
return record?.type === 'text' || record?.type === 'file';
}
function selectCommandHistoryParts(parts: unknown[]): CommandHistorySelection {
const markers = parts
.map(getRecord)
.filter((part) => part?.type === 'command-invocation');
if (markers.length === 0) return { parts };
const marker = markers.length === 1 ? markers[0] : null;
const command = typeof marker?.command === 'string' ? marker.command : '';
const args = typeof marker?.arguments === 'string' ? marker.arguments : '';
const ids = Array.isArray(marker?.contextPartIDs) ? marker.contextPartIDs : null;
const valid = Boolean(
marker
&& typeof marker.id === 'string'
&& COMMAND_HISTORY_PART_ID.test(marker.id)
&& command.length > 0
&& command.length <= 256
&& COMMAND_HISTORY_NAME.test(command)
&& !COMMAND_HISTORY_CONTROL.test(command)
&& typeof marker.arguments === 'string'
&& args.length <= 8_192
&& ids
&& ids.length <= 64
&& ids.every((id) => typeof id === 'string' && COMMAND_HISTORY_PART_ID.test(id)),
);
if (!valid) {
return {
invocationBlock: { type: 'text', text: '[Project command metadata unavailable]' },
parts: [],
};
}
const allowed = new Set(ids as string[]);
return {
invocationBlock: {
id: marker!.id as string,
type: 'text',
text: `/${command}${args ? ` ${args}` : ''}`,
},
parts: parts.filter((part) => {
const record = getRecord(part);
return typeof record?.id === 'string'
&& allowed.has(record.id)
&& isSafeCommandHistoryContextPart(record);
}),
};
}
const RUNTIME_IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
function boundedRuntimeIdentifier(value: unknown, maxLength: number): string | undefined {
if (typeof value !== 'string') return undefined;
const candidate = value.trim();
return candidate.length > 0
&& candidate.length <= maxLength
&& RUNTIME_IDENTIFIER_PATTERN.test(candidate)
? candidate
: undefined;
}
function getRuntimeMessageContext(
role: RawMessage['role'],
info: Record<string, unknown>,
): RawMessage['_runtimeContext'] {
if (role !== 'user') return undefined;
const model = getRecord(info.model);
const agent = boundedRuntimeIdentifier(info.agent, 64);
const providerID = boundedRuntimeIdentifier(model?.providerID, 128);
const modelID = boundedRuntimeIdentifier(model?.modelID, 128);
const variant = boundedRuntimeIdentifier(model?.variant, 64);
const context = {
...(agent ? { agent } : {}),
...(providerID && modelID ? { model: `${providerID}/${modelID}` } : {}),
...(variant ? { variant } : {}),
};
return Object.keys(context).length > 0 ? context : undefined;
}
function mergeAttachments(
current: AttachedFileMeta[] | undefined,
next: AttachedFileMeta[],
): AttachedFileMeta[] | undefined {
if (next.length === 0) return current;
const combined = [...(current ?? [])];
const seen = new Set(combined.map((file) => `${file.filePath ?? file.fileName}:${file.mimeType}`));
for (const file of next) {
const key = `${file.filePath ?? file.fileName}:${file.mimeType}`;
if (seen.has(key)) continue;
seen.add(key);
combined.push(file);
}
return combined;
}
function appendAttachments(
current: AttachedFileMeta[] | undefined,
next: AttachedFileMeta[],
): AttachedFileMeta[] | undefined {
if (next.length === 0) return current;
return [...(current ?? []), ...next];
}
function isCommandInvocationMarker(input: unknown): boolean {
return getRecord(input)?.type === 'command-invocation';
}
function hasDirectCommandInvocationMarker(input: unknown): boolean {
if (isCommandInvocationMarker(input)) return true;
if (Array.isArray(input)) return input.some(isCommandInvocationMarker);
const record = getRecord(input);
return record
? Object.values(record).some(isCommandInvocationMarker)
: false;
}
function hasLegacyCommandInvocationMarker(input: Record<string, unknown>): boolean {
if (isCommandInvocationMarker(input)) return true;
if (Array.isArray(input.content)) {
return input.content.some(isCommandInvocationMarker);
}
return isCommandInvocationMarker(input.content);
}
function hasOwn(record: Record<string, unknown>, key: string): boolean {
return Object.prototype.hasOwnProperty.call(record, key);
}
function commandHistoryUnavailableMessage(input: Record<string, unknown>): RawMessage {
const info = getRecord(input.info);
return {
id: typeof info?.id === 'string'
? info.id
: typeof input.id === 'string'
? input.id
: undefined,
role: normalizeRole(info?.role ?? input.role),
timestamp: normalizeTimestamp(info ?? input),
content: [{
type: 'text',
text: '[Project command metadata unavailable]',
}],
};
}
function getStreamingDelta(record: Record<string, unknown> | null, part: Record<string, unknown>): string {
const rawDelta = record?.delta ?? part.delta;
if (typeof rawDelta === 'string') return rawDelta;
const delta = getRecord(rawDelta);
if (typeof delta?.text === 'string') return delta.text;
if (typeof delta?.thinking === 'string') return delta.thinking;
if (typeof delta?.reasoning === 'string') return delta.reasoning;
if (typeof delta?.reasoning_content === 'string') return delta.reasoning_content;
if (typeof delta?.reasoning_text === 'string') return delta.reasoning_text;
if (typeof part.reasoning === 'string') return part.reasoning;
if (typeof part.reasoning_content === 'string') return part.reasoning_content;
if (typeof part.reasoning_text === 'string') return part.reasoning_text;
return '';
}
function mergeStreamingText(existingText: string, incomingText: string, delta: string): string {
if (!delta) return incomingText;
if (incomingText && incomingText.startsWith(existingText) && incomingText.length > existingText.length) return incomingText;
return `${existingText}${delta}`;
}
function applyStreamingDeltaToBlock(
candidate: ContentBlock,
existingBlock: ContentBlock | undefined,
delta: string,
): ContentBlock {
if (!delta) return candidate;
if (candidate.type === 'text') {
const existingText = existingBlock?.type === 'text' && typeof existingBlock.text === 'string'
? existingBlock.text
: '';
return {
...candidate,
text: mergeStreamingText(existingText, candidate.text ?? '', delta),
};
}
if (candidate.type === 'thinking') {
const existingThinking = existingBlock?.type === 'thinking' && typeof existingBlock.thinking === 'string'
? existingBlock.thinking
: '';
return {
...candidate,
thinking: mergeStreamingText(existingThinking, candidate.thinking ?? '', delta),
};
}
return candidate;
}
function normalizeStructuredMessage(input: Record<string, unknown>): RawMessage | null {
const info = getRecord(input.info);
const parts = Array.isArray(input.parts) ? input.parts : null;
if (!info || !parts) return null;
const errorMessage = getErrorMessage(info.error ?? input.error ?? input.errorMessage ?? input.error_message);
const role = normalizeRole(info.role);
const commandHistory: CommandHistorySelection = role === 'user'
? selectCommandHistoryParts(parts)
: { parts };
const blocks: ContentBlock[] = [];
let attachedFiles: AttachedFileMeta[] | undefined;
let toolEvidence: RuntimeToolEvidence[] | undefined;
if (commandHistory.invocationBlock) blocks.push(commandHistory.invocationBlock);
for (const part of commandHistory.parts) {
const record = getRecord(part);
const normalized = normalizePart(part);
blocks.push(...normalized.blocks);
attachedFiles = appendAttachments(attachedFiles, normalized.attachments);
toolEvidence = mergeToolEvidence(toolEvidence, record ? normalizeToolEvidence(record) : null);
}
const finalErrorMessage = errorMessage
?? (role === 'assistant' && blocks.length === 0 && hasCompletedAssistantSignal(info, input, parts)
? EMPTY_ASSISTANT_RESPONSE_MESSAGE
: undefined);
const runtimeContext = getRuntimeMessageContext(role, info);
return {
id: typeof info.id === 'string' ? info.id : undefined,
role,
timestamp: normalizeTimestamp(info),
content: blocks.length > 0 || !finalErrorMessage
? blocks
: [{ type: 'text', text: finalErrorMessage }],
...(attachedFiles?.length ? { _attachedFiles: attachedFiles } : {}),
...(toolEvidence?.length ? { _toolEvidence: toolEvidence } : {}),
...(runtimeContext ? { _runtimeContext: runtimeContext } : {}),
...(finalErrorMessage ? { isError: true, errorMessage: finalErrorMessage } : {}),
};
}
export function normalizeOpencodeSessionMessages(input: unknown): RawMessage[] {
if (!Array.isArray(input)) return [];
return input.flatMap((entry) => {
const record = getRecord(entry);
if (!record) return [];
const structuredInfo = getRecord(record.info);
const structuredParts = Array.isArray(record.parts) ? record.parts : null;
const structuredLike = hasOwn(record, 'info') || hasOwn(record, 'parts');
if (structuredLike && (!structuredInfo || !structuredParts)) {
return hasDirectCommandInvocationMarker(record.parts)
|| hasLegacyCommandInvocationMarker(record)
? [commandHistoryUnavailableMessage(record)]
: [normalizeLegacyMessage(record)];
}
if (structuredInfo && structuredParts) {
const containsCommandMarker = structuredParts.some(
isCommandInvocationMarker,
);
if (
containsCommandMarker
&& normalizeRole(structuredInfo.role) !== 'user'
) {
return [commandHistoryUnavailableMessage(record)];
}
const structured = normalizeStructuredMessage(record);
return structured
? [structured]
: [commandHistoryUnavailableMessage(record)];
}
if (hasLegacyCommandInvocationMarker(record)) {
return [commandHistoryUnavailableMessage(record)];
}
return [normalizeLegacyMessage(record)];
});
}
export function createOptimisticUserMessage(
text: string,
timestamp = Date.now(),
deliveryStatus?: RawMessage['_deliveryStatus'],
attachedFiles?: AttachedFileMeta[],
): RawMessage {
return {
id: `local-user-${timestamp}`,
role: 'user',
timestamp,
content: [{ type: 'text', text }],
...(attachedFiles?.length ? { _attachedFiles: attachedFiles } : {}),
...(deliveryStatus ? { _deliveryStatus: deliveryStatus } : {}),
};
}
export function createStreamingMessageFromEvent(input: unknown): RawMessage | null {
const record = getRecord(input);
const info = getRecord(record?.info ?? input);
if (!info) return null;
return {
id: typeof info.id === 'string' ? info.id : undefined,
role: normalizeRole(info.role),
timestamp: normalizeTimestamp(info),
content: [],
};
}
export function getOpencodeEventSessionId(input: unknown): string | null {
const record = getRecord(input);
if (!record) return null;
if (typeof record.sessionID === 'string' && record.sessionID.trim()) return record.sessionID;
if (typeof record.sessionId === 'string' && record.sessionId.trim()) return record.sessionId;
return getOpencodeEventSessionId(record.part)
?? getOpencodeEventSessionId(record.info)
?? getOpencodeEventSessionId(record.message)
?? null;
}
export function applyStreamingPartToMessage(message: RawMessage | null, input: unknown): RawMessage | null {
const record = getRecord(input);
const part = getRecord(record?.part ?? input);
if (!part) return message;
const current = message ?? {
role: 'assistant',
content: [],
};
const nextContent = Array.isArray(current.content) ? [...current.content] : [];
const normalized = normalizePart(part);
const toolEvidence = normalizeToolEvidence(part);
const rawCandidate = normalized.blocks.at(0);
const delta = getStreamingDelta(record, part);
const messageId = typeof record?.messageID === 'string'
? record.messageID
: typeof record?.messageId === 'string'
? record.messageId
: typeof part.messageID === 'string'
? part.messageID
: undefined;
const blockId = rawCandidate?.id ?? (typeof part.id === 'string' ? part.id : undefined);
const index = blockId
? nextContent.findIndex((block) => ('id' in block ? block.id : undefined) === blockId)
: -1;
const existingBlock = index >= 0 ? nextContent[index] : undefined;
const candidate = rawCandidate
? applyStreamingDeltaToBlock(rawCandidate, existingBlock, delta)
: delta
? { type: 'text', id: blockId, text: delta } satisfies ContentBlock
: undefined;
if (candidate) {
const nextBlock = blockId ? { ...candidate, id: blockId } : candidate;
if (index >= 0) {
nextContent[index] = nextBlock;
} else {
nextContent.push(nextBlock);
}
}
return {
...current,
...(current.id || !messageId ? {} : { id: messageId }),
content: nextContent,
_attachedFiles: mergeAttachments(current._attachedFiles, normalized.attachments),
_toolEvidence: mergeToolEvidence(current._toolEvidence, toolEvidence),
};
}
export function applyStreamingPartDeltaToMessage(message: RawMessage | null, input: unknown): RawMessage | null {
const record = getRecord(input);
if (!record) return message;
const part = getRecord(record.part) ?? record;
const delta = getStreamingDelta(record, part);
if (!delta) return message;
const messageID = typeof record.messageID === 'string'
? record.messageID
: typeof record.messageId === 'string'
? record.messageId
: undefined;
const partID = typeof record.partID === 'string'
? record.partID
: typeof record.partId === 'string'
? record.partId
: typeof part?.id === 'string'
? part.id
: undefined;
const field = typeof record.field === 'string'
? record.field
: typeof part?.field === 'string'
? part.field
: undefined;
const isThinkingDelta = part?.type === 'reasoning'
|| part?.type === 'thinking'
|| field === 'reasoning'
|| field === 'thinking'
|| field === 'reasoning_content'
|| field === 'reasoning_text';
if (message?.id && messageID && message.id !== messageID) return message;
const current = message ?? {
id: messageID,
role: 'assistant',
content: [],
} satisfies RawMessage;
const nextContent = Array.isArray(current.content) ? [...current.content] : [];
const index = partID
? nextContent.findIndex((block) => ('id' in block ? block.id : undefined) === partID)
: -1;
const existingBlock = index >= 0 ? nextContent[index] : undefined;
if (existingBlock?.type === 'thinking' || isThinkingDelta) {
const existingThinking = existingBlock?.type === 'thinking'
? existingBlock.thinking ?? ''
: existingBlock?.type === 'text'
? existingBlock.text ?? ''
: '';
const nextBlock = {
type: 'thinking',
id: partID,
thinking: `${existingThinking}${delta}`,
} satisfies ContentBlock;
if (index >= 0) {
nextContent[index] = nextBlock;
} else {
nextContent.push(nextBlock);
}
} else if (existingBlock?.type === 'text') {
nextContent[index] = {
...existingBlock,
text: `${existingBlock.text ?? ''}${delta}`,
};
} else {
nextContent.push({
type: 'text',
id: partID,
text: delta,
});
}
return {
...current,
content: nextContent,
};
}
export function removeStreamingPartFromMessage(
message: RawMessage | null,
input: unknown,
): RawMessage | null {
if (!message || !input || typeof input !== 'object') return message;
const record = input as Record<string, unknown>;
const messageID = typeof record.messageID === 'string'
? record.messageID
: typeof record.messageId === 'string'
? record.messageId
: undefined;
if (message.id && messageID && message.id !== messageID) return message;
const part = record.part && typeof record.part === 'object' && !Array.isArray(record.part)
? record.part as Record<string, unknown>
: null;
const partID = typeof record.partID === 'string'
? record.partID
: typeof record.partId === 'string'
? record.partId
: typeof part?.id === 'string'
? part.id
: undefined;
if (!partID || !Array.isArray(message.content)) return message;
const content = message.content.filter((block) => (
!('id' in block) || block.id !== partID
));
const toolEvidence = message._toolEvidence?.filter((evidence) => (
evidence.id !== partID && evidence.toolCallId !== partID
));
return {
...message,
content,
...(toolEvidence?.length ? { _toolEvidence: toolEvidence } : { _toolEvidence: undefined }),
};
}
export function getStreamingToolStatus(input: unknown): ToolStatus | null {
const record = getRecord(input);
const part = getRecord(record?.part ?? input);
if (!part || part.type !== 'tool' || typeof part.tool !== 'string') return null;
const state = getRecord(part.state);
const time = getRecord(state?.time);
const startedAt = toMs(time?.start);
const endedAt = toMs(time?.end);
return {
id: typeof part.id === 'string' ? part.id : undefined,
toolCallId: typeof part.callID === 'string' ? part.callID : undefined,
name: part.tool,
status: normalizeToolStatus(state?.status),
durationMs: startedAt !== undefined && endedAt !== undefined ? Math.max(0, endedAt - startedAt) : undefined,
summary: typeof state?.title === 'string' && state.title.trim()
? state.title
: typeof state?.error === 'string' && state.error.trim()
? state.error
: undefined,
updatedAt: endedAt ?? startedAt ?? Date.now(),
};
}