Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@office-agents/bridge": "workspace:*",
"@types/node": "^25.1.0",
"happy-dom": "^20.8.4",
"office-addin-dev-certs": "^2.0.6",
"svelte-check": "^4.3.3",
"typescript": "^5.4.2",
"vitest": "^4.0.18",
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ export {
type ChatMessage,
deriveStats,
extractPartsFromAssistantMessage,
filterMessagesForLLM,
generateId,
isContextOverflow,
type MessagePart,
type SessionStats,
stripEnrichment,
Expand Down
131 changes: 111 additions & 20 deletions packages/sdk/src/message-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,87 @@ import type {

export type ToolCallStatus = "pending" | "running" | "complete" | "error";

/**
* Filters agent messages for LLM context.
* Finds the last compactionSummary message and returns it + everything after it.
* The compactionSummary is converted to a UserMessage with the summary wrapped in tags.
* This is used to prevent context overflow when messages become too long.
*/
export function filterMessagesForLLM(
messages: AgentMessage[],
): AgentMessage[] {
// Find the last compactionSummary message
let lastCompactionIdx = -1;
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i] as unknown as { role: string; summary?: string; timestamp?: number };
if (msg.role === "compactionSummary") {
lastCompactionIdx = i;
break;
}
}

// If no compactionSummary, return all
if (lastCompactionIdx === -1) {
return messages;
}

// Get the compactionSummary and extract summary text
const compactionMsg = messages[lastCompactionIdx] as unknown as { summary?: string; timestamp?: number };
const summaryText = compactionMsg.summary || "Previous conversation summary";

// Create a user message with the summary wrapped in tags
const summaryUserMsg = {
role: "user",
content: [
{
type: "text",
text: `<compaction_summary>\n${summaryText}\n</compaction_summary>`,
},
],
timestamp: (compactionMsg.timestamp as number) || Date.now(),
} as unknown as AgentMessage;

// Return: summary as user message + everything after it
return [summaryUserMsg, ...messages.slice(lastCompactionIdx + 1)] as AgentMessage[];
}

/**
* Detects if an assistant message indicates a context overflow error.
*/
export function isContextOverflow(
message: AssistantMessage,
contextWindow: number,
reserveTokens: number = 4000,
): boolean {
// Check if it's an error with overflow indicators
if (message.stopReason === "error" && message.errorMessage) {
const errorLower = message.errorMessage.toLowerCase();
if (
errorLower.includes("context") ||
errorLower.includes("limit") ||
errorLower.includes("max") ||
errorLower.includes("token") ||
errorLower.includes("overflow") ||
errorLower.includes("context_length")
) {
return true;
}
}

// Check if usage exceeds context window (with reserve)
if (message.usage && contextWindow > 0) {
const totalTokens =
message.usage.input +
message.usage.cacheRead +
message.usage.cacheWrite;
if (totalTokens > contextWindow - reserveTokens) {
return true;
}
}

return false;
}

export type MessagePart =
| { type: "text"; text: string }
| { type: "thinking"; thinking: string }
Expand Down Expand Up @@ -56,11 +137,11 @@ export function stripEnrichment(
if (metadataTag) {
const escaped = metadataTag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
text = text.replace(
new RegExp(`^<${escaped}>\\n[\\s\\S]*?\\n</${escaped}>\\n\\n`),
new RegExp(`^<${escaped}>\n[\\s\\S]*?</${escaped}>\n\n`),
"",
);
} else {
text = text.replace(/^<\w+_context>\n[\s\S]*?\n<\/\w+_context>\n\n/, "");
text = text.replace(/^<\w+_context>\n[\\s\\S]*?<\/\w+_context>\n\n/, "");
}
return text;
}
Expand All @@ -73,9 +154,11 @@ export function extractPartsFromAssistantMessage(
message: AgentMessage,
existingParts: MessagePart[] = [],
): MessagePart[] {
if (message.role !== "assistant") return existingParts;
// Skip non-assistant messages
const role = message.role;
if (role !== "assistant") return existingParts;

const assistantMsg = message as AssistantMessage;
const assistantMsg = message as unknown as AssistantMessage;
const existingToolCalls = new Map<string, MessagePart>();
for (const part of existingParts) {
if (part.type === "toolCall") {
Expand Down Expand Up @@ -108,24 +191,31 @@ export function agentMessagesToChatMessages(
): ChatMessage[] {
const result: ChatMessage[] = [];
for (const msg of agentMessages) {
if (msg.role === "user") {
const text = stripEnrichment((msg as UserMessage).content, metadataTag);
// Skip compactionSummary messages - they're for LLM context only
const rawMsg = msg as unknown as { role: string };
if (rawMsg.role === "compactionSummary") {
continue;
}

if (rawMsg.role === "user") {
const userMsg = msg as unknown as UserMessage;
const text = stripEnrichment(userMsg.content, metadataTag);
result.push({
id: generateId(),
role: "user",
parts: [{ type: "text", text }],
timestamp: msg.timestamp,
timestamp: userMsg.timestamp,
});
} else if (msg.role === "assistant") {
const parts = extractPartsFromAssistantMessage(msg);
result.push({
id: generateId(),
role: "assistant",
parts,
timestamp: msg.timestamp,
timestamp: (msg as unknown as AssistantMessage).timestamp,
});
} else if (msg.role === "toolResult") {
const toolResult = msg as ToolResultMessage;
const toolResult = msg as unknown as ToolResultMessage;
for (let i = result.length - 1; i >= 0; i--) {
const chatMsg = result[i];
if (chatMsg.role !== "assistant") continue;
Expand Down Expand Up @@ -167,16 +257,17 @@ export function deriveStats(
let totalCost = 0;
let lastInputTokens = 0;
for (const msg of agentMessages) {
if (msg.role === "assistant") {
const u = (msg as AssistantMessage).usage;
if (u) {
inputTokens += u.input;
outputTokens += u.output;
cacheRead += u.cacheRead;
cacheWrite += u.cacheWrite;
totalCost += u.cost.total;
lastInputTokens = u.input + u.cacheRead + u.cacheWrite;
}
// Skip non-assistant messages
if (msg.role !== "assistant") continue;

const u = (msg as unknown as AssistantMessage).usage;
if (u) {
inputTokens += u.input;
outputTokens += u.output;
cacheRead += u.cacheRead;
cacheWrite += u.cacheWrite;
totalCost += u.cost.total;
lastInputTokens = u.input + u.cacheRead + u.cacheWrite;
}
}
return {
Expand All @@ -187,4 +278,4 @@ export function deriveStats(
totalCost,
lastInputTokens,
};
}
}
Loading