9f9d5b1e76
Files referenced with @path/to/file.ext syntax are automatically read and injected as synthetic Read tool calls before the agent responds, eliminating the need for manual file reads.
212 lines
5.4 KiB
TypeScript
212 lines
5.4 KiB
TypeScript
/**
|
|
* Custom message types and transformers for the coding agent.
|
|
*
|
|
* Extends the base AgentMessage type with coding-agent specific message types,
|
|
* and provides a transformer to convert them to LLM-compatible messages.
|
|
*/
|
|
|
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
|
|
|
|
export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:
|
|
|
|
<summary>
|
|
`;
|
|
|
|
export const COMPACTION_SUMMARY_SUFFIX = `
|
|
</summary>`;
|
|
|
|
export const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from:
|
|
|
|
<summary>
|
|
`;
|
|
|
|
export const BRANCH_SUMMARY_SUFFIX = `</summary>`;
|
|
|
|
/**
|
|
* Message type for bash executions via the ! command.
|
|
*/
|
|
export interface BashExecutionMessage {
|
|
role: "bashExecution";
|
|
command: string;
|
|
output: string;
|
|
exitCode: number | undefined;
|
|
cancelled: boolean;
|
|
truncated: boolean;
|
|
fullOutputPath?: string;
|
|
timestamp: number;
|
|
}
|
|
|
|
/**
|
|
* Message type for hook-injected messages via sendMessage().
|
|
* These are custom messages that hooks can inject into the conversation.
|
|
*/
|
|
export interface HookMessage<T = unknown> {
|
|
role: "hookMessage";
|
|
customType: string;
|
|
content: string | (TextContent | ImageContent)[];
|
|
display: boolean;
|
|
details?: T;
|
|
timestamp: number;
|
|
}
|
|
|
|
export interface BranchSummaryMessage {
|
|
role: "branchSummary";
|
|
summary: string;
|
|
fromId: string;
|
|
timestamp: number;
|
|
}
|
|
|
|
export interface CompactionSummaryMessage {
|
|
role: "compactionSummary";
|
|
summary: string;
|
|
tokensBefore: number;
|
|
timestamp: number;
|
|
}
|
|
|
|
/**
|
|
* Message type for auto-read file mentions via @filepath syntax.
|
|
*/
|
|
export interface FileMentionMessage {
|
|
role: "fileMention";
|
|
files: Array<{
|
|
path: string;
|
|
content: string;
|
|
lineCount: number;
|
|
}>;
|
|
timestamp: number;
|
|
}
|
|
|
|
// Extend CustomAgentMessages via declaration merging
|
|
declare module "@oh-my-pi/pi-agent-core" {
|
|
interface CustomAgentMessages {
|
|
bashExecution: BashExecutionMessage;
|
|
hookMessage: HookMessage;
|
|
branchSummary: BranchSummaryMessage;
|
|
compactionSummary: CompactionSummaryMessage;
|
|
fileMention: FileMentionMessage;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a BashExecutionMessage to user message text for LLM context.
|
|
*/
|
|
export function bashExecutionToText(msg: BashExecutionMessage): string {
|
|
let text = `Ran \`${msg.command}\`\n`;
|
|
if (msg.output) {
|
|
text += `\`\`\`\n${msg.output}\n\`\`\``;
|
|
} else {
|
|
text += "(no output)";
|
|
}
|
|
if (msg.cancelled) {
|
|
text += "\n\n(command cancelled)";
|
|
} else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) {
|
|
text += `\n\nCommand exited with code ${msg.exitCode}`;
|
|
}
|
|
if (msg.truncated && msg.fullOutputPath) {
|
|
text += `\n\n[Output truncated. Full output: ${msg.fullOutputPath}]`;
|
|
}
|
|
return text;
|
|
}
|
|
|
|
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,
|
|
): CompactionSummaryMessage {
|
|
return {
|
|
role: "compactionSummary",
|
|
summary: summary,
|
|
tokensBefore,
|
|
timestamp: new Date(timestamp).getTime(),
|
|
};
|
|
}
|
|
|
|
/** Convert CustomMessageEntry to AgentMessage format */
|
|
export function createHookMessage(
|
|
customType: string,
|
|
content: string | (TextContent | ImageContent)[],
|
|
display: boolean,
|
|
details: unknown | undefined,
|
|
timestamp: string,
|
|
): HookMessage {
|
|
return {
|
|
role: "hookMessage",
|
|
customType,
|
|
content,
|
|
display,
|
|
details,
|
|
timestamp: new Date(timestamp).getTime(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Transform AgentMessages (including custom types) to LLM-compatible Messages.
|
|
*
|
|
* This is used by:
|
|
* - Agent's transormToLlm option (for prompt calls and queued messages)
|
|
* - Compaction's generateSummary (for summarization)
|
|
* - Custom hooks and tools
|
|
*/
|
|
export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
return messages
|
|
.map((m): Message | undefined => {
|
|
switch (m.role) {
|
|
case "bashExecution":
|
|
return {
|
|
role: "user",
|
|
content: [{ type: "text", text: bashExecutionToText(m) }],
|
|
timestamp: m.timestamp,
|
|
};
|
|
case "hookMessage": {
|
|
const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
|
|
return {
|
|
role: "user",
|
|
content,
|
|
timestamp: m.timestamp,
|
|
};
|
|
}
|
|
case "branchSummary":
|
|
return {
|
|
role: "user",
|
|
content: [{ type: "text" as const, text: BRANCH_SUMMARY_PREFIX + m.summary + BRANCH_SUMMARY_SUFFIX }],
|
|
timestamp: m.timestamp,
|
|
};
|
|
case "compactionSummary":
|
|
return {
|
|
role: "user",
|
|
content: [
|
|
{ type: "text" as const, text: COMPACTION_SUMMARY_PREFIX + m.summary + COMPACTION_SUMMARY_SUFFIX },
|
|
],
|
|
timestamp: m.timestamp,
|
|
};
|
|
case "fileMention": {
|
|
const fileContents = m.files.map((f) => `<file path="${f.path}">\n${f.content}\n</file>`).join("\n\n");
|
|
return {
|
|
role: "user",
|
|
content: [{ type: "text" as const, text: `<system-reminder>\n${fileContents}\n</system-reminder>` }],
|
|
timestamp: m.timestamp,
|
|
};
|
|
}
|
|
case "user":
|
|
case "assistant":
|
|
case "toolResult":
|
|
return m;
|
|
default:
|
|
// biome-ignore lint/correctness/noSwitchDeclarations: fine
|
|
const _exhaustiveCheck: never = m;
|
|
return undefined;
|
|
}
|
|
})
|
|
.filter((m) => m !== undefined);
|
|
}
|