@convex-dev/agent
Version:
A agent component for Convex.
268 lines • 9.71 kB
JavaScript
import { toUIMessages } from "./toUIMessages.js";
export { toUIMessages };
export function mergeDeltas(threadId, streamMessages, existingStreams, allDeltas) {
const newStreams = [];
// Seed the existing chunks
let changed = false;
for (const streamMessage of streamMessages) {
const deltas = allDeltas.filter((d) => d.streamId === streamMessage.streamId);
const existing = existingStreams.find((s) => s.streamId === streamMessage.streamId);
const [newStream, messageChanged] = applyDeltasToStreamMessage(threadId, streamMessage, existing, deltas);
newStreams.push(newStream);
if (messageChanged)
changed = true;
}
for (const { streamId } of existingStreams) {
if (!newStreams.find((s) => s.streamId === streamId)) {
// There's a stream that's no longer active.
changed = true;
}
}
const messages = newStreams
.map((s) => s.messages)
.flat()
.sort((a, b) => a.order - b.order || a.stepOrder - b.stepOrder);
return [messages, newStreams, changed];
}
// exported for testing
export function applyDeltasToStreamMessage(threadId, streamMessage, existing, deltas) {
let changed = false;
let cursor = existing?.cursor ?? 0;
let parts = [];
for (const delta of deltas.sort((a, b) => a.start - b.start)) {
if (delta.parts.length === 0) {
console.warn(`Got delta with no parts: ${JSON.stringify(delta)}`);
continue;
}
if (cursor !== delta.start) {
if (cursor >= delta.end) {
console.debug(`Got duplicate delta for stream ${delta.streamId} at ${delta.start}`);
continue;
}
else if (cursor < delta.start) {
console.warn(`Got delta for stream ${delta.streamId} that has a gap ${cursor} -> ${delta.start}`);
continue;
}
else {
throw new Error(`Got unexpected delta for stream ${delta.streamId}: delta: ${delta.start} -> ${delta.end} existing cursor: ${cursor}`);
}
}
changed = true;
cursor = delta.end;
parts.push(...delta.parts);
}
if (!changed) {
return [
existing ?? { streamId: streamMessage.streamId, cursor, messages: [] },
false,
];
}
const existingMessages = existing?.messages ?? [];
let currentMessage;
if (existingMessages.length > 0) {
// replace the last message with a new one
const lastMessage = existingMessages.at(-1);
currentMessage = {
...lastMessage,
message: cloneMessageAndContent(lastMessage.message),
};
}
else {
const newMessage = createStreamingMessage(threadId, streamMessage, parts[0], existingMessages.length);
parts = parts.slice(1);
currentMessage = newMessage;
}
const newStream = {
streamId: streamMessage.streamId,
cursor,
messages: [...existingMessages.slice(0, -1), currentMessage],
};
let lastContent = getLastContent(currentMessage);
for (const part of parts) {
let contentToAdd;
const isToolRole = part.type === "source" || part.type === "tool-result";
if (isToolRole !== (currentMessage.message.role === "tool")) {
currentMessage = createStreamingMessage(threadId, streamMessage, part, newStream.messages.length);
lastContent = getLastContent(currentMessage);
newStream.messages.push(currentMessage);
continue;
}
switch (part.type) {
case "text-delta":
currentMessage.text += part.textDelta;
if (lastContent?.type === "text") {
lastContent.text += part.textDelta;
}
else {
contentToAdd = {
type: "text",
text: part.textDelta,
};
}
break;
case "tool-call-streaming-start":
currentMessage.tool = true;
contentToAdd = {
type: "tool-call",
toolCallId: part.toolCallId,
toolName: part.toolName,
args: "",
};
break;
case "tool-call-delta":
{
currentMessage.tool = true;
if (lastContent?.type !== "tool-call") {
throw new Error("Expected last content to be a tool call");
}
if (typeof lastContent.args !== "string") {
throw new Error("Expected args to be a string");
}
lastContent.args += part.argsTextDelta;
}
break;
case "tool-call":
currentMessage.tool = true;
contentToAdd = part;
break;
case "reasoning":
currentMessage.reasoning += part.textDelta;
if (lastContent?.type === "reasoning") {
lastContent.text += part.textDelta;
}
else {
contentToAdd = {
type: "reasoning",
text: part.textDelta,
};
}
break;
case "source":
if (!currentMessage.sources) {
currentMessage.sources = [];
}
currentMessage.sources.push(part.source);
break;
case "tool-result":
contentToAdd = part;
break;
default:
console.warn(`Received unexpected part: ${JSON.stringify(part)}`);
break;
}
if (contentToAdd) {
if (!currentMessage.message.content) {
currentMessage.message.content = [];
}
if (!Array.isArray(currentMessage.message?.content)) {
throw new Error("Expected message content to be an array");
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
currentMessage.message.content.push(contentToAdd);
lastContent = contentToAdd;
}
}
return [newStream, true];
}
function cloneMessageAndContent(message) {
return (message &&
{
...message,
content: Array.isArray(message.content)
? [...message.content]
: message.content,
});
}
function getLastContent(message) {
if (Array.isArray(message.message?.content)) {
return message.message.content.at(-1);
}
return undefined;
}
export function createStreamingMessage(threadId, message, part, index) {
const { streamId, ...rest } = message;
const metadata = {
_id: `${streamId}-${index}`,
_creationTime: Date.now(),
status: "pending",
threadId,
tool: false,
...rest,
};
switch (part.type) {
case "text-delta":
return {
...metadata,
message: {
role: "assistant",
content: [{ type: "text", text: part.textDelta }],
},
text: part.textDelta,
};
case "tool-call-streaming-start":
return {
...metadata,
tool: true,
message: {
role: "assistant",
content: [
{
type: "tool-call",
toolName: part.toolName,
toolCallId: part.toolCallId,
args: "", // when it's a string, it's a partial call
},
],
},
};
case "reasoning":
return {
...metadata,
message: {
role: "assistant",
content: [{ type: "reasoning", text: part.textDelta }],
},
reasoning: part.textDelta,
};
case "source":
console.warn("Received source part first??");
return {
...metadata,
tool: true,
message: { role: "tool", content: [] },
sources: [part.source],
};
case "tool-call":
return {
...metadata,
tool: true,
message: { role: "assistant", content: [part] },
};
case "tool-call-delta":
console.warn("Received tool call delta part first??");
return {
...metadata,
tool: true,
message: {
role: "assistant",
content: [
{
type: "tool-call",
toolCallId: part.toolCallId,
toolName: part.toolName,
args: part.argsTextDelta,
},
],
},
};
case "tool-result":
return {
...metadata,
tool: true,
message: { role: "tool", content: [part] },
};
default:
throw new Error(`Unexpected part type: ${JSON.stringify(part)}`);
}
}
//# sourceMappingURL=deltas.js.map