Message Types and convertToLlm
messages.ts adds four custom message types to pi-agent-core's AgentMessage: bashExecution, custom, branchSummary, and compactionSummary. The underlying Agent only understands user / assistant / toolResult. convertToLlm translates these four custom types into Message[] and filters out bash execution results marked excludeFromContext. Declaration merging via the CustomAgentMessages interface extension keeps the type layer and runtime layer aligned.
Responsibilities
- Type extension: uses
declare moduleto add four roles to theCustomAgentMessagesinterface. Seepackages/coding-agent/src/core/messages.ts:69-77. - Bash execution messages: results of
!commands, withcommand,output,exitCode,cancelled,truncated,fullOutputPath, and optionalexcludeFromContext(for the!!prefix). Seepackages/coding-agent/src/core/messages.ts:29-40. - Custom messages: messages injected by extensions via
sendMessage.customTypedistinguishes the kind,displaycontrols whether to render in the UI, anddetailsis optional structured data. Seepackages/coding-agent/src/core/messages.ts:46-53. - Branch / compaction summary: a branch summary is inserted when a fork returns to the mainline; a compaction summary is inserted after compaction. Both are wrapped in
<summary>tags. Seepackages/coding-agent/src/core/messages.ts:55-67. - Factory functions:
createBranchSummaryMessage/createCompactionSummaryMessage/createCustomMessageconvert string timestamps to numeric milliseconds. Seepackages/coding-agent/src/core/messages.ts:100-138. - convertToLlm: converts custom messages to
Message[]. bash is converted to a user message viabashExecutionToText; branch/compaction summaries are wrapped in<summary>tags; bash withexcludeFromContextreturns undefined and is filtered out. Seepackages/coding-agent/src/core/messages.ts:148-195.
Design Motivation
Why not just reuse the user message? Because the UI layer needs to distinguish "this is bash execution output" from "this is user input" — rendering style, collapsibility, and re-send behavior all differ. But the LLM only needs to see text — convertToLlm flattens all custom types into user messages before they hit the provider, so the model has no idea the custom types exist. This preserves UI semantic richness without polluting the model's context.
bashExecutionToText formats bash output as a Markdown code block, with a Ran \command`prefix and an exit-code suffix, so the LLM sees roughly the same format the user sees in the UI. Bash with the!!prefix is removed from LLM context entirely viaexcludeFromContext: true` — this is the user opting to hide a step from the model, common for sensitive ops or noisy output.
branchSummary and compactionSummary are wrapped in <summary> XML tags — a format Anthropic recommends, since the model tends to treat tagged content as a coherent unit rather than scattered paragraphs.
Key Files
packages/coding-agent/src/core/messages.ts:11-24—COMPACTION_SUMMARY_PREFIX/SUFFIXandBRANCH_SUMMARY_PREFIX/SUFFIXconstants.packages/coding-agent/src/core/messages.ts:29-40—BashExecutionMessageinterface.packages/coding-agent/src/core/messages.ts:46-53—CustomMessage<T>interface.packages/coding-agent/src/core/messages.ts:55-67—BranchSummaryMessageandCompactionSummaryMessage; the latter also storestokensBeforefor diagnostics.packages/coding-agent/src/core/messages.ts:69-77—declare moduledeclaration merging, extendingCustomAgentMessages.packages/coding-agent/src/core/messages.ts:82-98—bashExecutionToText, formats bash output as LLM-friendly text.packages/coding-agent/src/core/messages.ts:100-138— the three create factory functions.packages/coding-agent/src/core/messages.ts:148-195—convertToLlm, the switch + filter pattern.
convertToLlm uses a switch + never exhaustiveness check, so forgetting to handle a new type is a compile error:
// packages/coding-agent/src/core/messages.ts:148-195
export function convertToLlm(messages: AgentMessage[]): Message[] {
return messages
.map((m): Message | undefined => {
switch (m.role) {
case "bashExecution":
if (m.excludeFromContext) {
return undefined;
}
return {
role: "user",
content: [{ type: "text", text: bashExecutionToText(m) }],
timestamp: m.timestamp,
};
case "custom": {
const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
return { role: "user", content, timestamp: m.timestamp };
}
// ... branchSummary / compactionSummary ...
case "user":
case "assistant":
case "toolResult":
return m;
default:
const _exhaustiveCheck: never = m;
return undefined;
}
})
.filter((m) => m !== undefined);
}bashExecutionToText wraps the execution result in a Markdown code block, with exit code and truncation hints:
// packages/coding-agent/src/core/messages.ts:82-98
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;
}Data Flow
The path from message creation to the LLM:
Edges and Failures
- excludeFromContext: bash execution results with the
!!prefix are filtered out entirely inconvertToLlm; the model never sees them, but the UI still shows them. Seepackages/coding-agent/src/core/messages.ts:152-156. - custom content dual form:
CustomMessage.contentcan be a string or anArray<TextContent | ImageContent>; a string is converted to a single-element TextContent array. Seepackages/coding-agent/src/core/messages.ts:162-168. - timestamp string to number: the three create functions all use
new Date(timestamp).getTime(); passing an already-numeric timestamp yields NaN, so callers must pass ISO strings. Seepackages/coding-agent/src/core/messages.ts:105-106. - blockImages wrapper:
createAgentSessionwrapsconvertToLlmin an outerconvertToLlmWithBlockImageslayer that reads settings dynamically and replaces images with placeholder text. See the edges section of createAgentSession Wiring. - compaction uses the same function: both
Agent'stransformToLlmand compaction'sgenerateSummaryuseconvertToLlm, so the context seen during compaction matches what's sent to the provider.
Summary
messages.ts uses TypeScript declaration merging to add four custom types to AgentMessage. convertToLlm flattens them into user messages before they hit the provider, while the UI layer keeps the original types for differentiated rendering. At assembly time this function is wrapped by a blockImages layer in createAgentSession — see createAgentSession Wiring. The compaction flow that produces CompactionSummaryMessage is covered in the compaction directory.