Skip to content

Message Types and convertToLlm

源码版本v0.73.1

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

  1. Type extension: uses declare module to add four roles to the CustomAgentMessages interface. See packages/coding-agent/src/core/messages.ts:69-77.
  2. Bash execution messages: results of ! commands, with command, output, exitCode, cancelled, truncated, fullOutputPath, and optional excludeFromContext (for the !! prefix). See packages/coding-agent/src/core/messages.ts:29-40.
  3. Custom messages: messages injected by extensions via sendMessage. customType distinguishes the kind, display controls whether to render in the UI, and details is optional structured data. See packages/coding-agent/src/core/messages.ts:46-53.
  4. 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. See packages/coding-agent/src/core/messages.ts:55-67.
  5. Factory functions: createBranchSummaryMessage / createCompactionSummaryMessage / createCustomMessage convert string timestamps to numeric milliseconds. See packages/coding-agent/src/core/messages.ts:100-138.
  6. convertToLlm: converts custom messages to Message[]. bash is converted to a user message via bashExecutionToText; branch/compaction summaries are wrapped in <summary> tags; bash with excludeFromContext returns undefined and is filtered out. See packages/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

convertToLlm uses a switch + never exhaustiveness check, so forgetting to handle a new type is a compile error:

typescript
// 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:

typescript
// 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 in convertToLlm; the model never sees them, but the UI still shows them. See packages/coding-agent/src/core/messages.ts:152-156.
  • custom content dual form: CustomMessage.content can be a string or an Array<TextContent | ImageContent>; a string is converted to a single-element TextContent array. See packages/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. See packages/coding-agent/src/core/messages.ts:105-106.
  • blockImages wrapper: createAgentSession wraps convertToLlm in an outer convertToLlmWithBlockImages layer that reads settings dynamically and replaces images with placeholder text. See the edges section of createAgentSession Wiring.
  • compaction uses the same function: both Agent's transformToLlm and compaction's generateSummary use convertToLlm, 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.