Examples

Example: Pi Compactor

A structured, iterative context summary written with the agent's own model — no key, no permissions.

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

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

// 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 → Context compaction → 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).