# Example: Pi Compactor

**Example plugin** · `compaction.summarize` · uses `host.session.llm` · no permissions

When a long mission's conversation is compacted, the default summary is prose. Pi Compactor (after the Pi
coding-agent's compaction pattern) writes a **structured** summary instead — Goal, Constraints, Progress, Key
Decisions, Next Steps, Critical Context, and cumulative lists of files read and modified — and **refines** its own
previous summary each time instead of starting over. Long missions keep their file lists and decisions intact
across many compactions.

## The template

```ts
// infrastructure/plugins/pi-compactor/index.ts
const MAX_MESSAGE_CHARS = 2000; // Pi truncates bulky tool results before serializing.
const SUMMARY_MARKER = "# Conversation summary"; // first line of our output — lets the next compaction detect it.

const TEMPLATE =
  "## Goal\n" +
  "[What the user is trying to accomplish]\n\n" +
  "## Constraints & Preferences\n" +
  "- [Requirements/preferences the user stated]\n\n" +
  "## Progress\n" +
  "### Done\n" +
  "- [x] [Completed work]\n" +
  "### In Progress\n" +
  "- [ ] [Current work]\n" +
  "### Blocked\n" +
  "- [Issues, if any]\n\n" +
  "## Key Decisions\n" +
  "- **[Decision]**: [Rationale]\n\n" +
  "## Next Steps\n" +
  "1. [What should happen next]\n\n" +
  "## Critical Context\n" +
  "- [Facts/identifiers/data needed to continue]\n\n" +
  "<read-files>\n[one file path per line, cumulative across the whole session]\n</read-files>\n\n" +
  "<modified-files>\n[one file path per line, cumulative across the whole session]\n</modified-files>";
```

## The handler

```ts
// infrastructure/plugins/pi-compactor/index.ts
function serialize(messages: CompactionMessage[]): string {
  return messages
    .map((message) => {
      let content = message.content || "";
      if (content.length > MAX_MESSAGE_CHARS) {
        content = content.slice(0, MAX_MESSAGE_CHARS) + "\n…[truncated " + (content.length - MAX_MESSAGE_CHARS) + " chars]";
      }
      return "[" + message.role + "]\n" + content;
    })
    .join("\n\n");
}

globalThis.compaction.summarize = async (context) => {
  const messages = context.messages || [];
  if (messages.length === 0) {
    return "";
  }

  const first = messages[0];
  const hasPriorSummary = first.content.indexOf(SUMMARY_MARKER) === 0;
  const priorSummary = hasPriorSummary ? first.content : null;
  const transcript = serialize(hasPriorSummary ? messages.slice(1) : messages);

  const system =
    "You compact a long agent session into a single structured summary that preserves everything needed to " +
    "continue with NO loss of intent. Output EXACTLY the template below, filling every section from the " +
    "conversation (drop a placeholder bullet only when nothing applies). Keep file paths, identifiers, commands, " +
    "and decisions byte-exact. Accumulate <read-files>/<modified-files> across the entire history. Your FIRST " +
    'line MUST be "' + SUMMARY_MARKER + '". Output only the summary — no preamble.\n\n' +
    "TEMPLATE:\n" + SUMMARY_MARKER + "\n\n" + TEMPLATE +
    (priorSummary
      ? "\n\nPREVIOUS SUMMARY — refine and extend this; carry its file lists forward:\n" + priorSummary
      : "");

  const result = await host.session!.llm!.complete({
    messages: [
      { role: "system", content: system },
      { role: "user", content: "Conversation to summarize (oldest first):\n\n" + transcript },
    ],
    params: { temperature: 0.2 },
  });

  return result.assistant.text || "";
};
```

## Walkthrough

1. **What it's given.** `context.messages` is the history since (and including) the last checkpoint, oldest first.
   The current request isn't there — the platform keeps it verbatim after the summary.
2. **Recognizing its own previous summary.** Every summary starts with `# Conversation summary`. On the next
   compaction the platform hands that summary back as `messages[0]`; the plugin spots the marker and feeds it to the
   model as *"refine and extend this"*. That's what keeps the file lists cumulative.
3. **Keeping the prompt bounded.** Each message is cut to 2,000 characters — tool results are the usual bulk.
4. **Calling a model without a key.** `host.session.llm.complete` runs the **chat's own agent model**. No setting,
   no egress, no permission — it's granted by context. The `!`s are safe because a compaction always runs inside a
   chat. Temperature 0.2 keeps it faithful.
5. **Returning.** One string. An empty return (the `messages.length === 0` case) or a summary more than half the
   compaction threshold makes the platform use its built-in summarizer and post a notice — the agent is never left
   uncompacted.

## Using it

Install, then **Settings → Engines → Long conversations → Pi Compactor**. It applies to automatic compaction and
to `/compact`; `/compact keep the deploy plan` arrives as `context.focus` (this plugin doesn't use it yet — adding
`context.focus` to the system prompt is a one-line improvement).
