import type {
AssistantMessage,
ImageContent,
Message,
MessageAttribution,
ProviderPayload,
TextContent,
ToolResultMessage,
} from "@oh-my-pi/pi-ai";
import { prompt } from "@oh-my-pi/pi-utils";
import type { AgentMessage } from "../types";
import branchSummaryContextPrompt from "./prompts/branch-summary-context.md" with { type: "text" };
import compactionSummaryContextPrompt from "./prompts/compaction-summary-context.md" with { type: "text" };
const COMPACTION_SUMMARY_TEMPLATE = compactionSummaryContextPrompt;
const BRANCH_SUMMARY_TEMPLATE = branchSummaryContextPrompt;
export interface CustomMessage<T = unknown> {
role: "custom";
customType: string;
content: string | (TextContent | ImageContent)[];
display: boolean;
details?: T;
attribution?: MessageAttribution;
timestamp: number;
}
export interface HookMessage<T = unknown> {
role: "hookMessage";
customType: string;
content: string | (TextContent | ImageContent)[];
display: boolean;
details?: T;
attribution?: MessageAttribution;
timestamp: number;
}
export interface BranchSummaryMessage {
role: "branchSummary";
summary: string;
fromId: string;
timestamp: number;
}
export interface CompactionSummaryMessage {
role: "compactionSummary";
summary: string;
shortSummary?: string;
tokensBefore: number;
providerPayload?: ProviderPayload;
timestamp: number;
}
export type CoreCompactionMessage = CustomMessage | HookMessage | BranchSummaryMessage | CompactionSummaryMessage;
declare module "../types" {
interface CustomAgentMessages {
custom: CustomMessage;
hookMessage: HookMessage;
branchSummary: BranchSummaryMessage;
compactionSummary: CompactionSummaryMessage;
}
}
export type ConvertToLlm = (messages: AgentMessage[]) => Message[];
function getPrunedToolResultContent(message: ToolResultMessage): (TextContent | ImageContent)[] {
if (message.prunedAt === undefined) {
return message.content;
}
const textBlocks = message.content.filter((content): content is TextContent => content.type === "text");
const text = textBlocks.map(block => block.text).join("") || "[Output truncated]";
return [{ type: "text", text }];
}
export function renderBranchSummaryContext(summary: string): string {
return prompt.render(BRANCH_SUMMARY_TEMPLATE, { summary });
}
export function renderCompactionSummaryContext(summary: string): string {
return prompt.render(COMPACTION_SUMMARY_TEMPLATE, { summary });
}
export function createBranchSummaryMessage(summary: string, fromId: string, timestamp: string): BranchSummaryMessage {
return {
role: "branchSummary",
summary,
fromId,
timestamp: new Date(timestamp).getTime(),
};
}
export function createCompactionSummaryMessage(
summary: string,
tokensBefore: number,
timestamp: string,
shortSummary?: string,
providerPayload?: ProviderPayload,
): CompactionSummaryMessage {
return {
role: "compactionSummary",
summary,
shortSummary,
tokensBefore,
providerPayload,
timestamp: new Date(timestamp).getTime(),
};
}
export function createCustomMessage(
customType: string,
content: string | (TextContent | ImageContent)[],
display: boolean,
details: unknown | undefined,
timestamp: string,
attribution?: MessageAttribution,
): CustomMessage {
return {
role: "custom",
customType,
content,
display,
details,
attribution,
timestamp: new Date(timestamp).getTime(),
};
}
function isCoreCompactionMessage(message: AgentMessage): message is AgentMessage & CoreCompactionMessage {
return (
message.role === "custom" ||
message.role === "hookMessage" ||
message.role === "branchSummary" ||
message.role === "compactionSummary"
);
}
* Default compaction-domain transformer.
*
* Embedders with their own app messages should pass a richer transformer through
* `SummaryOptions.convertToLlm`; this default intentionally preserves only the
* core LLM roles and the compaction messages owned by this package.
*/
export function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
return messages
.map((message): Message | undefined => {
if (isCoreCompactionMessage(message)) {
switch (message.role) {
case "custom":
case "hookMessage": {
const content =
typeof message.content === "string"
? [{ type: "text" as const, text: message.content }]
: message.content;
return {
role: "user",
content,
attribution: message.attribution,
timestamp: message.timestamp,
};
}
case "branchSummary":
return {
role: "user",
content: [
{
type: "text" as const,
text: renderBranchSummaryContext(message.summary),
},
],
attribution: "agent",
timestamp: message.timestamp,
};
case "compactionSummary":
return {
role: "user",
content: [
{
type: "text" as const,
text: renderCompactionSummaryContext(message.summary),
},
],
attribution: "agent",
providerPayload: message.providerPayload,
timestamp: message.timestamp,
};
}
}
switch (message.role) {
case "user":
return { ...message, attribution: message.attribution ?? "user" };
case "developer":
return { ...message, attribution: message.attribution ?? "agent" };
case "assistant":
return message as AssistantMessage;
case "toolResult":
return {
...message,
content: getPrunedToolResultContent(message as ToolResultMessage),
attribution: message.attribution ?? "agent",
};
default:
return undefined;
}
})
.filter(message => message !== undefined);
}
export const convertToLlm = defaultConvertToLlm;