Example

Name the eight message roles

Read the eight AgentMessage roles and the SystemMessage declaration behind them, including the replace field the 1.0.4 type does not have.

Pi uses the AgentMessage union in SDK state, lifecycle events, RPC responses, and persisted session entries — one type across four surfaces. On 1.0.4 the coding agent extends it with four roles, and applications can add still more by declaration merging. The declaration below reproduces the exported types from ch26-message-types/declared.ts; assert-equal.ts checks those reproductions two ways against the real exports.

The eight roles

Role What it is for
system the prompt and tool state; later system messages patch it
user a prompt: a string or an array of text/image blocks
assistant content blocks, usage, stopReason, the three-way model identity
toolResult a tool’s result, joined to its call by toolCallId
bashExecution a direct shell command — not an LLM tool result
custom an extension context message, converted to a user message
branchSummary the summary a branch_summary entry becomes in context
compactionSummary the summary a compaction entry becomes in context

The declaration that carries the caveat

SystemMessage is the role with the book’s most famous mismatch, so it is the one worth showing. An excerpt of the declared reproduction — a declaration, not executable code:

	export interface SystemMessage {
	  role: "system";
	  content: string | TextContent[];
	  sections?: Record<string, string | null>;
	  toolsAdded?: Tool[];
	  toolsRemoved?: ToolReference[];
	  timestamp: number;
	}

declared.ts really does write these lines inside a namespace, and assert-equal.ts proves each one is mutually assignable with, and has the same key set as, the exported type. sections is a named map where a null value removes a section, and toolsAdded / toolsRemoved patch the tool set without redeclaring it.

The discrepancy the chapter records: the prose in message-types.md lists replace?: boolean on SystemMessage and describes it as discarding earlier state, but the exported 1.0.4 declaration has no replace field, and no shipped JavaScript reads one. The book’s rule is that the declaration wins, so the interface above omits it — reported upstream as earendil-works/pi#10462, framed as a question rather than a bug.

Mechanism and limitations

Documented: the union, the role fields, and the four-surfaces statement are the exported 1.0.4 types, verified at compile time by assert-equal.ts (declaration-assignability, declarations scope in the evidence ledger). Two limits carry over even from the type alone: stopReason: "pending" is a stream-only state that Pi does not persist to session JSONL, so one type really does carry different values on different surfaces; and a timestamp on the entry is ISO 8601 while the nested message timestamp is Unix milliseconds — same field name, two formats. Observed: nothing here runs a model — the declaration check is the only evidence row this chapter has, and the SystemMessage in the type says nothing about how a real model produces these roles.

Full source and test .

Understand this example

Copy this prompt into your AI tool. No code runs here.

Apply this example

Copy this prompt into your AI tool. No code runs here.