@8monkey/pi-context-history 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,19 +1,10 @@
1
1
  # pi-context-history
2
2
 
3
- A [Pi](https://github.com/earendil-works/pi-coding-agent) extension that manages conversation context end-to-end. It trims old history, strips stale tool chatter from earlier turns, keeps a rolling session summary, and folds that summary back into the system prompt — so long-running conversations stay lean, focused, and continuous.
3
+ A [Pi](https://github.com/earendil-works/pi-coding-agent) extension that keeps long-running conversations lean and focused.
4
4
 
5
- Each feature is independent and can be turned off individually, so you can run the whole pipeline or just the parts you want.
5
+ In long sessions, the context window fills with old messages and tool output the model no longer needs. This extension removes what's stale and keeps a rolling summary so earlier context isn't lost.
6
6
 
7
- ## Features
8
-
9
- | Feature | Hook | What it does |
10
- |---|---|---|
11
- | **Trim history** | `context` | Drops messages older than a configurable age from the context. |
12
- | **Strip tool history** | `context` | Removes tool calls and results from prior turns, keeping the current turn's tool interactions intact. |
13
- | **Generate summary** | `session_start` | Regenerates a stale rolling summary in the background when a session resumes; also available on demand via `/summarize-session`. |
14
- | **Inject summary** | `before_agent_start` | Folds the rolling summary into the system prompt so the model continues with prior context. |
15
-
16
- The summary features are a producer/consumer pair: **Generate summary** writes `~/.pi/agent/summary.md`, and **Inject summary** reads it.
7
+ Everything is on by default and works once installed. Each feature can be disabled independently.
17
8
 
18
9
  ## Install
19
10
 
@@ -21,51 +12,55 @@ The summary features are a producer/consumer pair: **Generate summary** writes `
21
12
  pi install npm:@8monkey/pi-context-history
22
13
  ```
23
14
 
24
- ## How it works
25
-
26
- - **Trim history.** On each `context` event the message list is filtered to those whose `timestamp` is within `PI_HISTORY_DAYS`. Messages exactly at the cutoff are kept.
27
- - **Strip tool history.** Tool calls and results from earlier turns are removed; the current turn is left untouched. At the start of each agent loop the extension marks where the current turn begins (scanning back past the latest tool exchange) and holds that mark steady across the loop, so a running turn is never stripped mid-flight. Before the mark, tool results are dropped and assistant messages lose their tool-call blocks (if nothing else remains, the message goes too).
28
- - **Generate summary.** When you resume a session whose summary has gone stale — older than `PI_SUMMARY_STALENESS_DAYS`, with a first message older than the window too — the extension rebuilds `~/.pi/agent/summary.md` by shelling out to `pi -p` (extensions, context files, and skills disabled). New and empty sessions are skipped. To override the built-in prompt, drop your own at `prompts/session-summary.md` (project `.pi/` wins over `~/.pi/`); it must contain the `{conversation_history}` placeholder.
29
- - **Inject summary.** Before each agent run, when `~/.pi/agent/summary.md` exists, its contents are wrapped in a `<summary date="…">` block (the date is the file's modified time) followed by an `<additional_context>` note telling the model to maintain continuity and match the existing language and tone.
15
+ ## Features
30
16
 
31
- Zero runtime dependencies. Pi loads the TypeScript directly, so there's no build step. Runs under Node or Bun.
17
+ - **Trim history.** Drops messages older than `PI_HISTORY_DAYS` (60 by default) on each context rebuild.
18
+ - **Strip tool history.** Removes tool calls and results from turns before the current one, leaving the in-progress turn untouched.
19
+ - **Compact.** Keeps a rolling summary of your sessions in `~/.pi/agent/compact.md` and folds it into the system prompt, so the model continues with prior context. The summary regenerates during session start when you resume a session after it has gone stale. Run `/compact-session` to regenerate on demand.
20
+ - **Append message.** Adds a user or assistant message to the end of the history with `/add-user-message` and `/add-assistant-message`.
32
21
 
33
- ## Command
22
+ ## Commands
34
23
 
35
- | Command | Description |
24
+ | Command | What it does |
36
25
  |---|---|
37
- | `/summarize-session` | Regenerate the current session's summary on demand, ignoring the staleness window. Writes `~/.pi/agent/summary.md` and reports success, failure, or an empty session. Available only when the **Generate summary** feature is enabled. |
26
+ | `/compact-session` | Write a fresh summary of the current session now, ignoring the staleness window. |
27
+ | `/add-user-message <text>` | Add a message to the conversation as if you had typed it. |
28
+ | `/add-assistant-message <text>` | Add a message as if the assistant had said it. Requires a selected model. |
29
+
30
+ Appended messages are persisted immediately but only enter the live context on the next rebuild (resume or branch).
38
31
 
39
32
  ## Configuration
40
33
 
41
- All configuration is via environment variables.
34
+ All configuration is via environment variables, and all of it is optional.
42
35
 
43
36
  ### Feature toggles
44
37
 
45
- Every feature is **on by default** and switched off by setting its flag to `false` or `0`:
38
+ Each feature is on by default. Set its variable to `false` to disable it.
46
39
 
47
- | Variable | Default | Feature |
48
- |---|---|---|
49
- | `PI_TRIM_HISTORY` | on | Trim history |
50
- | `PI_STRIP_TOOL_HISTORY` | on | Strip tool history |
51
- | `PI_GENERATE_SUMMARY` | on | Generate summary |
52
- | `PI_INJECT_SUMMARY` | on | Inject summary |
40
+ | Variable | Feature |
41
+ |---|---|
42
+ | `PI_TRIM_HISTORY` | Trim history |
43
+ | `PI_STRIP_TOOL_HISTORY` | Strip tool history |
44
+ | `PI_COMPACT` | Compact (generation and injection) |
45
+ | `PI_APPEND_MESSAGE` | The append-message commands |
53
46
 
54
47
  ### Settings
55
48
 
56
49
  | Variable | Default | Description |
57
50
  |---|---|---|
58
- | `PI_HISTORY_DAYS` | `60` | Maximum age, in days, of messages kept in the context. |
59
- | `PI_SUMMARY_STALENESS_DAYS` | `3` | How many days old a summary (and the session's first message) must be before automatic regeneration kicks in on session start. |
51
+ | `PI_HISTORY_DAYS` | `60` | Maximum age, in days, of messages kept in context. |
52
+ | `PI_COMPACT_STALENESS_DAYS` | `3` | How old the summary may get before it's regenerated on resume. |
60
53
 
61
- | File | Default | Description |
62
- |---|---|---|
63
- | `prompts/session-summary.md` | built-in default | Optional override for the summary prompt. Read from project `.pi/` first, then `~/.pi/`. Must contain the `{conversation_history}` placeholder. |
54
+ ### Compact prompt
55
+
56
+ Override the built-in prompt with a file at `prompts/compact.md`, read from the project `.pi/` first, then `~/.pi/`. It must contain the `{conversation_history}` placeholder.
64
57
 
65
58
  ## Development
66
59
 
60
+ No runtime dependencies. Pi loads the TypeScript directly, so there's no build step, and it runs under Node or Bun.
61
+
67
62
  ```bash
68
- node --test # run tests
63
+ node --test # run tests
69
64
  npm run typecheck
70
65
  ```
71
66
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@8monkey/pi-context-history",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Pi extension that manages conversation context: trims old history, strips stale tool calls, and keeps a rolling summary fed back into the system prompt. Each feature toggles independently.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { DEFAULT_SUMMARY_PROMPT } from "./default-prompt.ts";
2
+ import { DEFAULT_COMPACT_PROMPT } from "./default-prompt.ts";
3
3
 
4
4
  const DAY_MS = 86_400_000;
5
5
 
@@ -43,10 +43,10 @@ export function isStale(summaryMtime: Date | null, firstUserDate: Date | null, n
43
43
  }
44
44
 
45
45
  export function resolvePromptTemplate(fileContents: string | null) {
46
- return fileContents ?? DEFAULT_SUMMARY_PROMPT;
46
+ return fileContents ?? DEFAULT_COMPACT_PROMPT;
47
47
  }
48
48
 
49
- export function runPiSummary(template: string, history: string) {
49
+ export function runPiCompact(template: string, history: string) {
50
50
  return execFileSync(
51
51
  "pi",
52
52
  [
package/src/config.ts CHANGED
@@ -5,4 +5,4 @@ export function featureEnabled(flag: string) {
5
5
  }
6
6
 
7
7
  export const HISTORY_DAYS = Number(process.env["PI_HISTORY_DAYS"]) || 60;
8
- export const SUMMARY_STALENESS_DAYS = Number(process.env["PI_SUMMARY_STALENESS_DAYS"]) || 3;
8
+ export const COMPACT_STALENESS_DAYS = Number(process.env["PI_COMPACT_STALENESS_DAYS"]) || 3;
@@ -1,4 +1,4 @@
1
- export const DEFAULT_SUMMARY_PROMPT = `You are summarizing a conversation so it can be continued later. Produce a concise, structured summary capturing the main topics, decisions, open questions, and any important context. Be factual and neutral.
1
+ export const DEFAULT_COMPACT_PROMPT = `You are summarizing a conversation so it can be continued later. Produce a concise, structured summary capturing the main topics, decisions, open questions, and any important context. Be factual and neutral.
2
2
 
3
3
  Conversation:
4
4
  {conversation_history}`;
package/src/index.ts CHANGED
@@ -1,22 +1,31 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
4
+ import type { ExtensionAPI, SessionManager } from "@earendil-works/pi-coding-agent";
5
5
  import { buildContextPrompt } from "./build-prompt.ts";
6
- import { featureEnabled, HISTORY_DAYS, SUMMARY_STALENESS_DAYS } from "./config.ts";
7
- import { filterByAge } from "./filter.ts";
8
- import { piFileMtime, readPiFile } from "./pi-file.ts";
9
- import { findBoundary, stripBeforeBoundary, type Message } from "./strip.ts";
10
6
  import {
11
7
  buildTranscript,
12
8
  isStale,
13
9
  resolvePromptTemplate,
14
- runPiSummary,
10
+ runPiCompact,
15
11
  type TranscriptEntry,
16
- } from "./summary.ts";
12
+ } from "./compact.ts";
13
+ import { COMPACT_STALENESS_DAYS, featureEnabled, HISTORY_DAYS } from "./config.ts";
14
+ import { filterByAge } from "./filter.ts";
15
+ import { piFileMtime, readPiFile } from "./pi-file.ts";
16
+ import { findBoundary, stripBeforeBoundary, type Message } from "./strip.ts";
17
17
 
18
18
  const HISTORY_MS = HISTORY_DAYS * 86_400_000;
19
- const SUMMARY_PATH = join(homedir(), ".pi", "agent", "summary.md");
19
+ const COMPACT_RELATIVE_PATH = "agent/compact.md";
20
+ const COMPACT_PATH = join(homedir(), ".pi", COMPACT_RELATIVE_PATH);
21
+ const ZERO_USAGE = {
22
+ input: 0,
23
+ output: 0,
24
+ cacheRead: 0,
25
+ cacheWrite: 0,
26
+ totalTokens: 0,
27
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
28
+ };
20
29
 
21
30
  function registerTrimHistory(pi: ExtensionAPI) {
22
31
  pi.on("context", (event) => {
@@ -55,18 +64,18 @@ function firstUserDate(entries: TranscriptEntry[]) {
55
64
  function generate(entries: TranscriptEntry[]): string | "empty" {
56
65
  const history = buildTranscript(entries);
57
66
  if (!history) return "empty";
58
- const template = resolvePromptTemplate(readPiFile("prompts/session-summary.md"));
59
- const summary = runPiSummary(template, history);
67
+ const template = resolvePromptTemplate(readPiFile("prompts/compact.md"));
68
+ const summary = runPiCompact(template, history);
60
69
  if (!summary) return "empty";
61
- writeFileSync(SUMMARY_PATH, summary);
70
+ writeFileSync(COMPACT_PATH, summary);
62
71
  return summary;
63
72
  }
64
73
 
65
- function registerGenerateSummary(pi: ExtensionAPI) {
74
+ function registerCompact(pi: ExtensionAPI) {
66
75
  pi.on("session_start", (event, ctx) => {
67
76
  if (event.reason === "new") return;
68
77
  const entries = ctx.sessionManager.getEntries() as TranscriptEntry[];
69
- if (!isStale(piFileMtime("agent/summary.md"), firstUserDate(entries), Date.now(), SUMMARY_STALENESS_DAYS)) {
78
+ if (!isStale(piFileMtime(COMPACT_RELATIVE_PATH), firstUserDate(entries), Date.now(), COMPACT_STALENESS_DAYS)) {
70
79
  return;
71
80
  }
72
81
  try {
@@ -74,9 +83,9 @@ function registerGenerateSummary(pi: ExtensionAPI) {
74
83
  } catch {}
75
84
  });
76
85
 
77
- pi.registerCommand("summarize-session", {
78
- description: "Regenerate the current session's summary now (ignores staleness)",
79
- handler: async (_args, ctx: ExtensionCommandContext) => {
86
+ pi.registerCommand("compact-session", {
87
+ description: "Regenerate the session summary in ~/.pi/agent/compact.md now (ignores staleness)",
88
+ handler: async (_args, ctx) => {
80
89
  const entries = ctx.sessionManager.getEntries() as TranscriptEntry[];
81
90
  let result: string | "empty";
82
91
  try {
@@ -89,16 +98,14 @@ function registerGenerateSummary(pi: ExtensionAPI) {
89
98
  if (result === "empty") {
90
99
  ctx.ui.notify("Nothing to summarize in this session.", "info");
91
100
  } else {
92
- ctx.ui.notify("Session summary written to ~/.pi/agent/summary.md.", "info");
101
+ ctx.ui.notify("Session summary written to ~/.pi/agent/compact.md.", "info");
93
102
  }
94
103
  },
95
104
  });
96
- }
97
105
 
98
- function registerInjectSummary(pi: ExtensionAPI) {
99
106
  pi.on("before_agent_start", (event) => {
100
- const summary = readPiFile("agent/summary.md");
101
- const mtime = summary ? piFileMtime("agent/summary.md") : null;
107
+ const summary = readPiFile(COMPACT_RELATIVE_PATH);
108
+ const mtime = summary ? piFileMtime(COMPACT_RELATIVE_PATH) : null;
102
109
  const summaryDate = mtime ? (mtime.toISOString().split("T")[0] ?? "unknown") : "unknown";
103
110
 
104
111
  const systemPrompt = buildContextPrompt(event.systemPrompt, summary, summaryDate);
@@ -106,9 +113,55 @@ function registerInjectSummary(pi: ExtensionAPI) {
106
113
  });
107
114
  }
108
115
 
116
+ function registerAppendMessage(pi: ExtensionAPI) {
117
+ pi.registerCommand("add-user-message", {
118
+ description: "Append a user message to the end of the conversation history",
119
+ handler: async (args, ctx) => {
120
+ const text = args.trim();
121
+ if (!text) {
122
+ if (ctx.hasUI) ctx.ui.notify("Usage: /add-user-message <text>", "warning");
123
+ return;
124
+ }
125
+ const session = ctx.sessionManager as SessionManager;
126
+ session.appendMessage({ role: "user", content: [{ type: "text", text }], timestamp: Date.now() });
127
+ if (ctx.hasUI) ctx.ui.notify("Appended a user message; it applies on the next session rebuild.", "info");
128
+ },
129
+ });
130
+
131
+ pi.registerCommand("add-assistant-message", {
132
+ description: "Append an assistant message to the end of the conversation history",
133
+ handler: async (args, ctx) => {
134
+ const text = args.trim();
135
+ if (!text) {
136
+ if (ctx.hasUI) ctx.ui.notify("Usage: /add-assistant-message <text>", "warning");
137
+ return;
138
+ }
139
+ if (!ctx.model) {
140
+ if (ctx.hasUI) ctx.ui.notify("Cannot append an assistant message: no model selected.", "error");
141
+ return;
142
+ }
143
+ const { api, provider, id } = ctx.model;
144
+ const session = ctx.sessionManager as SessionManager;
145
+ session.appendMessage({
146
+ role: "assistant",
147
+ content: [{ type: "text", text }],
148
+ api,
149
+ provider,
150
+ model: id,
151
+ usage: ZERO_USAGE,
152
+ stopReason: "stop",
153
+ timestamp: Date.now(),
154
+ });
155
+ if (ctx.hasUI) {
156
+ ctx.ui.notify("Appended an assistant message; it applies on the next session rebuild.", "info");
157
+ }
158
+ },
159
+ });
160
+ }
161
+
109
162
  export default function (pi: ExtensionAPI) {
110
163
  if (featureEnabled("PI_TRIM_HISTORY")) registerTrimHistory(pi);
111
164
  if (featureEnabled("PI_STRIP_TOOL_HISTORY")) registerStripToolHistory(pi);
112
- if (featureEnabled("PI_GENERATE_SUMMARY")) registerGenerateSummary(pi);
113
- if (featureEnabled("PI_INJECT_SUMMARY")) registerInjectSummary(pi);
165
+ if (featureEnabled("PI_COMPACT")) registerCompact(pi);
166
+ if (featureEnabled("PI_APPEND_MESSAGE")) registerAppendMessage(pi);
114
167
  }