56 lines
3.0 KiB
TypeScript
56 lines
3.0 KiB
TypeScript
import type { Message } from "./api";
|
|
|
|
// The runtime also uses the Unix epoch for historical entries without a time.
|
|
// The transcript renders these as "Earlier", without inventing a send time.
|
|
export const UNKNOWN_AGENT_TIME = "1970-01-01T00:00:00.000Z";
|
|
const object = (value: unknown): Record<string, unknown> | undefined => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
? value as Record<string, unknown> : undefined;
|
|
const timestamp = (value: unknown): string | undefined => {
|
|
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
const time = Date.parse(value);
|
|
return Number.isFinite(time) && time !== 0 ? new Date(time).toISOString() : undefined;
|
|
};
|
|
|
|
export function agentMessageTime(message: Pick<Message, "createdAt" | "metadata">): string | undefined {
|
|
const info = object(message.metadata);
|
|
return timestamp(message.createdAt) ?? timestamp(info?.startedAt) ?? timestamp(info?.completedAt);
|
|
}
|
|
|
|
/** Normalize historical/partial rows at both transports, before state or cursors change. */
|
|
export function normalizeAgentMessage(value: unknown): Message {
|
|
const message = object(value);
|
|
if (!message || typeof message.id !== "string" || !message.id
|
|
|| typeof message.role !== "string" || !["system", "user", "assistant", "tool"].includes(message.role)) throw new Error("Invalid agent message.");
|
|
const info = object(message.metadata);
|
|
// Older runtime REST redaction emitted protected tool rows with only id/role/name.
|
|
// Keep their identity for a later complete upsert; never discard a whole page.
|
|
const content = typeof message.content === "string" ? message.content
|
|
: message.role === "tool" && message.content == null ? (typeof message.name === "string" ? message.name : "Tool") : undefined;
|
|
if (content === undefined) throw new Error("Invalid agent message.");
|
|
const createdAt = agentMessageTime(message as Message) ?? UNKNOWN_AGENT_TIME;
|
|
const needsToolMetadata = message.role === "tool" && info?.kind !== "tool_call";
|
|
if (message.createdAt === createdAt && message.content === content && !needsToolMetadata) return message as Message;
|
|
return { ...message, content, createdAt,
|
|
...(needsToolMetadata ? { metadata: { ...info, kind: "tool_call" } } : {}),
|
|
} as Message;
|
|
}
|
|
|
|
function cursor(message: Message): bigint | undefined {
|
|
const entryId = object(message.metadata)?.entryId;
|
|
if ((typeof entryId === "number" && Number.isSafeInteger(entryId) && entryId >= 0)
|
|
|| (typeof entryId === "string" && /^\d+$/.test(entryId))) return BigInt(entryId);
|
|
}
|
|
|
|
/** One total order for history, queue snapshots and optimistic rows, including ties. */
|
|
export function compareAgentMessages(left: Message, right: Message): number {
|
|
const byTime = Date.parse(agentMessageTime(left) ?? UNKNOWN_AGENT_TIME) - Date.parse(agentMessageTime(right) ?? UNKNOWN_AGENT_TIME);
|
|
if (byTime) return byTime;
|
|
const a = cursor(left), b = cursor(right);
|
|
if (a !== b) {
|
|
if (a === undefined) return 1;
|
|
if (b === undefined) return -1;
|
|
return a < b ? -1 : 1;
|
|
}
|
|
return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
|
|
}
|