@mariozechner/pi-coding-agent 0.12.5 → 0.12.7
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/CHANGELOG.md +13 -1
- package/README.md +86 -10
- package/dist/compaction.d.ts +51 -0
- package/dist/compaction.d.ts.map +1 -0
- package/dist/compaction.js +218 -0
- package/dist/compaction.js.map +1 -0
- package/dist/export-html.d.ts +5 -3
- package/dist/export-html.d.ts.map +1 -1
- package/dist/export-html.js +480 -1314
- package/dist/export-html.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +80 -3
- package/dist/main.js.map +1 -1
- package/dist/session-manager.d.ts +66 -1
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +175 -59
- package/dist/session-manager.js.map +1 -1
- package/dist/settings-manager.d.ts +15 -0
- package/dist/settings-manager.d.ts.map +1 -1
- package/dist/settings-manager.js +23 -0
- package/dist/settings-manager.js.map +1 -1
- package/dist/tools-manager.d.ts.map +1 -1
- package/dist/tools-manager.js +2 -2
- package/dist/tools-manager.js.map +1 -1
- package/dist/tui/compaction.d.ts +15 -0
- package/dist/tui/compaction.d.ts.map +1 -0
- package/dist/tui/compaction.js +42 -0
- package/dist/tui/compaction.js.map +1 -0
- package/dist/tui/footer.d.ts +2 -0
- package/dist/tui/footer.d.ts.map +1 -1
- package/dist/tui/footer.js +8 -3
- package/dist/tui/footer.js.map +1 -1
- package/dist/tui/tui-renderer.d.ts +8 -1
- package/dist/tui/tui-renderer.d.ts.map +1 -1
- package/dist/tui/tui-renderer.js +286 -28
- package/dist/tui/tui-renderer.js.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [
|
|
3
|
+
## [0.12.7] - 2025-12-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Context Compaction**: Long sessions can now be compacted to reduce context usage while preserving recent conversation history. ([#92](https://github.com/badlogic/pi-mono/issues/92), [docs](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/README.md#context-compaction))
|
|
8
|
+
- `/compact [instructions]`: Manually compact context with optional custom instructions for the summary
|
|
9
|
+
- `/autocompact`: Toggle automatic compaction when context exceeds threshold
|
|
10
|
+
- Compaction summarizes older messages while keeping recent messages (default 20k tokens) verbatim
|
|
11
|
+
- Auto-compaction triggers when context reaches `contextWindow - reserveTokens` (default 16k reserve)
|
|
12
|
+
- Compacted sessions show a collapsible summary in the TUI (toggle with `o` key)
|
|
13
|
+
- HTML exports include compaction summaries as collapsible sections
|
|
14
|
+
- RPC mode supports `{"type":"compact"}` command and auto-compaction (emits compaction events)
|
|
15
|
+
- **Branch Source Tracking**: Branched sessions now store `branchedFrom` in the session header, containing the path to the original session file. Useful for tracing session lineage.
|
|
4
16
|
|
|
5
17
|
## [0.12.5] - 2025-12-03
|
|
6
18
|
|
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ Works on Linux, macOS, and Windows (barely tested, needs Git Bash running in the
|
|
|
17
17
|
- [Project Context Files](#project-context-files)
|
|
18
18
|
- [Image Support](#image-support)
|
|
19
19
|
- [Session Management](#session-management)
|
|
20
|
+
- [Context Compaction](#context-compaction)
|
|
20
21
|
- [CLI Options](#cli-options)
|
|
21
22
|
- [Tools](#tools)
|
|
22
23
|
- [Usage](#usage)
|
|
@@ -25,7 +26,6 @@ Works on Linux, macOS, and Windows (barely tested, needs Git Bash running in the
|
|
|
25
26
|
- [To-Dos](#to-dos)
|
|
26
27
|
- [Planning](#planning)
|
|
27
28
|
- [Background Bash](#background-bash)
|
|
28
|
-
- [Planned Features](#planned-features)
|
|
29
29
|
- [License](#license)
|
|
30
30
|
- [See Also](#see-also)
|
|
31
31
|
|
|
@@ -502,6 +502,27 @@ Clear the conversation context and start a fresh session:
|
|
|
502
502
|
|
|
503
503
|
Aborts any in-flight agent work, clears all messages, and creates a new session file.
|
|
504
504
|
|
|
505
|
+
### /compact
|
|
506
|
+
|
|
507
|
+
Manually compact the conversation context to reduce token usage:
|
|
508
|
+
|
|
509
|
+
```
|
|
510
|
+
/compact # Use default summary instructions
|
|
511
|
+
/compact Focus on the API changes # Custom instructions for summary
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Creates a summary of the conversation so far, replacing the message history with a condensed version. See [Context Compaction](#context-compaction) for details.
|
|
515
|
+
|
|
516
|
+
### /autocompact
|
|
517
|
+
|
|
518
|
+
Toggle automatic context compaction:
|
|
519
|
+
|
|
520
|
+
```
|
|
521
|
+
/autocompact
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
When enabled, the agent automatically compacts context when usage exceeds the configured threshold. The current state (enabled/disabled) is shown after toggling. See [Context Compaction](#context-compaction) for details.
|
|
525
|
+
|
|
505
526
|
### Custom Slash Commands
|
|
506
527
|
|
|
507
528
|
Define reusable prompt templates as Markdown files that appear in the `/` autocomplete.
|
|
@@ -758,6 +779,70 @@ To use a specific session file instead of auto-generating one:
|
|
|
758
779
|
pi --session /path/to/my-session.jsonl
|
|
759
780
|
```
|
|
760
781
|
|
|
782
|
+
## Context Compaction
|
|
783
|
+
|
|
784
|
+
> **Note:** Compaction is lossy and should generally be avoided. The agent loses access to the full conversation after compaction. Size your tasks to avoid hitting context limits. Alternatively, when context usage approaches 85-90%, ask the agent to write a summary to a markdown file, iterate until it captures everything important, then start a new session with that file.
|
|
785
|
+
>
|
|
786
|
+
> That said, compaction does not destroy history. The full session is preserved in the session file with compaction events as markers. You can branch (`/branch`) from any previous message, and branched sessions include the complete history. If compaction missed something, you can ask the agent to read the session file directly.
|
|
787
|
+
|
|
788
|
+
Long sessions can exhaust the model's context window. Context compaction summarizes older conversation history while preserving recent messages, allowing sessions to continue indefinitely.
|
|
789
|
+
|
|
790
|
+
### How It Works
|
|
791
|
+
|
|
792
|
+
When compaction runs (manually via `/compact` or automatically):
|
|
793
|
+
|
|
794
|
+
1. A **cut point** is calculated to keep approximately `keepRecentTokens` (default: 20k) worth of recent messages
|
|
795
|
+
2. Messages **before** the cut point are sent to the model for summarization
|
|
796
|
+
3. Messages **after** the cut point are kept verbatim
|
|
797
|
+
4. The summary replaces the older messages as a "context handoff" message
|
|
798
|
+
5. If there was a previous compaction, its summary is included as context for the new summary (chaining)
|
|
799
|
+
|
|
800
|
+
Cut points are always placed at user message boundaries to preserve turn integrity.
|
|
801
|
+
|
|
802
|
+
The summary is displayed in the TUI as a collapsible block (toggle with `o` key). HTML exports also show compaction summaries as collapsible sections.
|
|
803
|
+
|
|
804
|
+
### Manual Compaction
|
|
805
|
+
|
|
806
|
+
Use `/compact` to manually trigger compaction at any time:
|
|
807
|
+
|
|
808
|
+
```
|
|
809
|
+
/compact # Default summary
|
|
810
|
+
/compact Focus on the API changes # Custom instructions guide what to emphasize
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
Custom instructions are appended to the default summary prompt, letting you focus the summary on specific aspects of the conversation.
|
|
814
|
+
|
|
815
|
+
### Automatic Compaction
|
|
816
|
+
|
|
817
|
+
Enable auto-compaction with `/autocompact`. When enabled, compaction triggers automatically when context usage exceeds `contextWindow - reserveTokens`.
|
|
818
|
+
|
|
819
|
+
The context percentage is shown in the footer. When it approaches 100%, auto-compaction kicks in (if enabled) or you should manually compact.
|
|
820
|
+
|
|
821
|
+
### Configuration
|
|
822
|
+
|
|
823
|
+
Power users can tune compaction behavior in `~/.pi/agent/settings.json`:
|
|
824
|
+
|
|
825
|
+
```json
|
|
826
|
+
{
|
|
827
|
+
"compaction": {
|
|
828
|
+
"enabled": true,
|
|
829
|
+
"reserveTokens": 16384,
|
|
830
|
+
"keepRecentTokens": 20000
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
- **enabled**: Whether auto-compaction is active (toggle with `/autocompact`)
|
|
836
|
+
- **reserveTokens**: Token buffer to keep free (default: 16,384). Auto-compaction triggers when `contextTokens > contextWindow - reserveTokens`
|
|
837
|
+
- **keepRecentTokens**: How many tokens worth of recent messages to preserve verbatim (default: 20,000). Older messages are summarized.
|
|
838
|
+
|
|
839
|
+
### Supported Modes
|
|
840
|
+
|
|
841
|
+
Context compaction works in both interactive and RPC modes:
|
|
842
|
+
|
|
843
|
+
- **Interactive**: Use `/compact` and `/autocompact` commands
|
|
844
|
+
- **RPC**: Send `{"type":"compact"}` for manual compaction. Auto-compaction emits `{"type":"compaction","auto":true}` events. See [RPC documentation](docs/rpc.md) for details.
|
|
845
|
+
|
|
761
846
|
## CLI Options
|
|
762
847
|
|
|
763
848
|
```bash
|
|
@@ -1130,15 +1215,6 @@ The agent can read, update, and reference the plan as it works. Unlike ephemeral
|
|
|
1130
1215
|
|
|
1131
1216
|
**pi does not and will not implement background bash execution.** Instead, tell the agent to use `tmux` or something like [tterminal-cp](https://mariozechner.at/posts/2025-08-15-mcp-vs-cli/). Bonus points: you can watch the agent interact with a CLI like a debugger and even intervene if necessary.
|
|
1132
1217
|
|
|
1133
|
-
## Planned Features
|
|
1134
|
-
|
|
1135
|
-
Things that might happen eventually:
|
|
1136
|
-
|
|
1137
|
-
- **Auto-compaction**: Currently, watch the context percentage at the bottom. When it approaches 80%, either:
|
|
1138
|
-
- Ask the agent to write a summary .md file you can load in a new session
|
|
1139
|
-
- Switch to a model with bigger context (e.g., Gemini) using `/model` and either continue with that model, or let it summarize the session to a .md file to be loaded in a new session
|
|
1140
|
-
- **Better RPC mode docs**: It works, you'll figure it out (see `test/rpc-example.ts`)
|
|
1141
|
-
|
|
1142
1218
|
## Development
|
|
1143
1219
|
|
|
1144
1220
|
### Forking / Rebranding
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context compaction for long sessions.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for compaction logic. The session manager handles I/O,
|
|
5
|
+
* and after compaction the session is reloaded.
|
|
6
|
+
*/
|
|
7
|
+
import type { AppMessage } from "@mariozechner/pi-agent-core";
|
|
8
|
+
import type { Model, Usage } from "@mariozechner/pi-ai";
|
|
9
|
+
import type { CompactionEntry, SessionEntry } from "./session-manager.js";
|
|
10
|
+
export interface CompactionSettings {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
reserveTokens: number;
|
|
13
|
+
keepRecentTokens: number;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
16
|
+
/**
|
|
17
|
+
* Calculate total context tokens from usage.
|
|
18
|
+
*/
|
|
19
|
+
export declare function calculateContextTokens(usage: Usage): number;
|
|
20
|
+
/**
|
|
21
|
+
* Find the last non-aborted assistant message usage from session entries.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getLastAssistantUsage(entries: SessionEntry[]): Usage | null;
|
|
24
|
+
/**
|
|
25
|
+
* Check if compaction should trigger based on context usage.
|
|
26
|
+
*/
|
|
27
|
+
export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
|
30
|
+
* Returns the entry index of the first message to keep (a user message for turn integrity).
|
|
31
|
+
*
|
|
32
|
+
* Only considers entries between `startIndex` and `endIndex` (exclusive).
|
|
33
|
+
*/
|
|
34
|
+
export declare function findCutPoint(entries: SessionEntry[], startIndex: number, endIndex: number, keepRecentTokens: number): number;
|
|
35
|
+
/**
|
|
36
|
+
* Generate a summary of the conversation using the LLM.
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateSummary(currentMessages: AppMessage[], model: Model<any>, reserveTokens: number, apiKey: string, signal?: AbortSignal, customInstructions?: string): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Calculate compaction and generate summary.
|
|
41
|
+
* Returns the CompactionEntry to append to the session file.
|
|
42
|
+
*
|
|
43
|
+
* @param entries - All session entries
|
|
44
|
+
* @param model - Model to use for summarization
|
|
45
|
+
* @param settings - Compaction settings
|
|
46
|
+
* @param apiKey - API key for LLM
|
|
47
|
+
* @param signal - Optional abort signal
|
|
48
|
+
* @param customInstructions - Optional custom focus for the summary
|
|
49
|
+
*/
|
|
50
|
+
export declare function compact(entries: SessionEntry[], model: Model<any>, settings: CompactionSettings, apiKey: string, signal?: AbortSignal, customInstructions?: string): Promise<CompactionEntry>;
|
|
51
|
+
//# sourceMappingURL=compaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../src/compaction.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAC9D,OAAO,KAAK,EAAoB,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAE1E,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAM1E,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,MAAM,CAAC;CACzB;AAED,eAAO,MAAM,2BAA2B,EAAE,kBAIzC,CAAC;AAMF;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE3D;AAeD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,KAAK,GAAG,IAAI,CAS3E;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAGjH;AAoBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,YAAY,EAAE,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACtB,MAAM,CAgDR;AAiBD;;GAEG;AACH,wBAAsB,eAAe,CACpC,eAAe,EAAE,UAAU,EAAE,EAC7B,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,EACpB,kBAAkB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,MAAM,CAAC,CAwBjB;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAC5B,OAAO,EAAE,YAAY,EAAE,EACvB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,EACjB,QAAQ,EAAE,kBAAkB,EAC5B,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,EACpB,kBAAkB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,eAAe,CAAC,CA6D1B","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Model, Usage } from \"@mariozechner/pi-ai\";\nimport { complete } from \"@mariozechner/pi-ai\";\nimport type { CompactionEntry, SessionEntry } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n */\nfunction getAssistantUsage(msg: AppMessage): Usage | null {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\n/**\n * Find indices of message entries that are user messages (turn boundaries).\n */\nfunction findTurnBoundaries(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst boundaries: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\" && entry.message.role === \"user\") {\n\t\t\tboundaries.push(i);\n\t\t}\n\t}\n\treturn boundaries;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n * Returns the entry index of the first message to keep (a user message for turn integrity).\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): number {\n\tconst boundaries = findTurnBoundaries(entries, startIndex, endIndex);\n\n\tif (boundaries.length === 0) {\n\t\treturn startIndex; // No user messages, keep everything in range\n\t}\n\n\t// Collect assistant usages walking backwards from endIndex\n\tconst assistantUsages: Array<{ index: number; tokens: number }> = [];\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) {\n\t\t\t\tassistantUsages.push({\n\t\t\t\t\tindex: i,\n\t\t\t\t\ttokens: calculateContextTokens(usage),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tif (assistantUsages.length === 0) {\n\t\t// No usage info, keep last turn only\n\t\treturn boundaries[boundaries.length - 1];\n\t}\n\n\t// Walk through and find where cumulative token difference exceeds keepRecentTokens\n\tconst newestTokens = assistantUsages[0].tokens;\n\tlet cutIndex = startIndex; // Default: keep everything in range\n\n\tfor (let i = 1; i < assistantUsages.length; i++) {\n\t\tconst tokenDiff = newestTokens - assistantUsages[i].tokens;\n\t\tif (tokenDiff >= keepRecentTokens) {\n\t\t\t// Find the turn boundary at or before the assistant we want to keep\n\t\t\tconst lastKeptAssistantIndex = assistantUsages[i - 1].index;\n\n\t\t\tfor (let b = boundaries.length - 1; b >= 0; b--) {\n\t\t\t\tif (boundaries[b] <= lastKeptAssistantIndex) {\n\t\t\t\t\tcutIndex = boundaries[b];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn cutIndex;\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.\n\nInclude:\n- Current progress and key decisions made\n- Important context, constraints, or user preferences\n- Absolute file paths of any relevant files that were read or modified\n- What remains to be done (clear next steps)\n- Any critical data, examples, or references needed to continue\n\nBe concise, structured, and focused on helping the next LLM seamlessly continue the work.`;\n\n/**\n * Generate a summary of the conversation using the LLM.\n */\nexport async function generateSummary(\n\tcurrentMessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.8 * reserveTokens);\n\n\tconst prompt = customInstructions\n\t\t? `${SUMMARIZATION_PROMPT}\\n\\nAdditional focus: ${customInstructions}`\n\t\t: SUMMARIZATION_PROMPT;\n\n\tconst summarizationMessages = [\n\t\t...currentMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: prompt,\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\tconst textContent = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\treturn textContent;\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\n/**\n * Calculate compaction and generate summary.\n * Returns the CompactionEntry to append to the session file.\n *\n * @param entries - All session entries\n * @param model - Model to use for summarization\n * @param settings - Compaction settings\n * @param apiKey - API key for LLM\n * @param signal - Optional abort signal\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tentries: SessionEntry[],\n\tmodel: Model<any>,\n\tsettings: CompactionSettings,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<CompactionEntry> {\n\t// Don't compact if the last entry is already a compaction\n\tif (entries.length > 0 && entries[entries.length - 1].type === \"compaction\") {\n\t\tthrow new Error(\"Already compacted\");\n\t}\n\n\t// Find previous compaction boundary\n\tlet prevCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst boundaryStart = prevCompactionIndex + 1;\n\tconst boundaryEnd = entries.length;\n\n\t// Get token count before compaction\n\tconst lastUsage = getLastAssistantUsage(entries);\n\tconst tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;\n\n\t// Find cut point (entry index) within the valid range\n\tconst firstKeptEntryIndex = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Extract messages to summarize (before the cut point)\n\tconst messagesToSummarize: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < firstKeptEntryIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tmessagesToSummarize.push(entry.message);\n\t\t}\n\t}\n\n\t// Also include the previous summary if there was a compaction\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\t// Prepend the previous summary as context\n\t\tmessagesToSummarize.unshift({\n\t\t\trole: \"user\",\n\t\t\tcontent: `Previous session summary:\\n${prevCompaction.summary}`,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t// Generate summary from messages before the cut point\n\tconst summary = await generateSummary(\n\t\tmessagesToSummarize,\n\t\tmodel,\n\t\tsettings.reserveTokens,\n\t\tapiKey,\n\t\tsignal,\n\t\tcustomInstructions,\n\t);\n\n\treturn {\n\t\ttype: \"compaction\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tsummary,\n\t\tfirstKeptEntryIndex,\n\t\ttokensBefore,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context compaction for long sessions.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for compaction logic. The session manager handles I/O,
|
|
5
|
+
* and after compaction the session is reloaded.
|
|
6
|
+
*/
|
|
7
|
+
import { complete } from "@mariozechner/pi-ai";
|
|
8
|
+
export const DEFAULT_COMPACTION_SETTINGS = {
|
|
9
|
+
enabled: true,
|
|
10
|
+
reserveTokens: 16384,
|
|
11
|
+
keepRecentTokens: 20000,
|
|
12
|
+
};
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Token calculation
|
|
15
|
+
// ============================================================================
|
|
16
|
+
/**
|
|
17
|
+
* Calculate total context tokens from usage.
|
|
18
|
+
*/
|
|
19
|
+
export function calculateContextTokens(usage) {
|
|
20
|
+
return usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Get usage from an assistant message if available.
|
|
24
|
+
*/
|
|
25
|
+
function getAssistantUsage(msg) {
|
|
26
|
+
if (msg.role === "assistant" && "usage" in msg) {
|
|
27
|
+
const assistantMsg = msg;
|
|
28
|
+
if (assistantMsg.stopReason !== "aborted" && assistantMsg.usage) {
|
|
29
|
+
return assistantMsg.usage;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Find the last non-aborted assistant message usage from session entries.
|
|
36
|
+
*/
|
|
37
|
+
export function getLastAssistantUsage(entries) {
|
|
38
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
39
|
+
const entry = entries[i];
|
|
40
|
+
if (entry.type === "message") {
|
|
41
|
+
const usage = getAssistantUsage(entry.message);
|
|
42
|
+
if (usage)
|
|
43
|
+
return usage;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if compaction should trigger based on context usage.
|
|
50
|
+
*/
|
|
51
|
+
export function shouldCompact(contextTokens, contextWindow, settings) {
|
|
52
|
+
if (!settings.enabled)
|
|
53
|
+
return false;
|
|
54
|
+
return contextTokens > contextWindow - settings.reserveTokens;
|
|
55
|
+
}
|
|
56
|
+
// ============================================================================
|
|
57
|
+
// Cut point detection
|
|
58
|
+
// ============================================================================
|
|
59
|
+
/**
|
|
60
|
+
* Find indices of message entries that are user messages (turn boundaries).
|
|
61
|
+
*/
|
|
62
|
+
function findTurnBoundaries(entries, startIndex, endIndex) {
|
|
63
|
+
const boundaries = [];
|
|
64
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
65
|
+
const entry = entries[i];
|
|
66
|
+
if (entry.type === "message" && entry.message.role === "user") {
|
|
67
|
+
boundaries.push(i);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return boundaries;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
|
74
|
+
* Returns the entry index of the first message to keep (a user message for turn integrity).
|
|
75
|
+
*
|
|
76
|
+
* Only considers entries between `startIndex` and `endIndex` (exclusive).
|
|
77
|
+
*/
|
|
78
|
+
export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens) {
|
|
79
|
+
const boundaries = findTurnBoundaries(entries, startIndex, endIndex);
|
|
80
|
+
if (boundaries.length === 0) {
|
|
81
|
+
return startIndex; // No user messages, keep everything in range
|
|
82
|
+
}
|
|
83
|
+
// Collect assistant usages walking backwards from endIndex
|
|
84
|
+
const assistantUsages = [];
|
|
85
|
+
for (let i = endIndex - 1; i >= startIndex; i--) {
|
|
86
|
+
const entry = entries[i];
|
|
87
|
+
if (entry.type === "message") {
|
|
88
|
+
const usage = getAssistantUsage(entry.message);
|
|
89
|
+
if (usage) {
|
|
90
|
+
assistantUsages.push({
|
|
91
|
+
index: i,
|
|
92
|
+
tokens: calculateContextTokens(usage),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (assistantUsages.length === 0) {
|
|
98
|
+
// No usage info, keep last turn only
|
|
99
|
+
return boundaries[boundaries.length - 1];
|
|
100
|
+
}
|
|
101
|
+
// Walk through and find where cumulative token difference exceeds keepRecentTokens
|
|
102
|
+
const newestTokens = assistantUsages[0].tokens;
|
|
103
|
+
let cutIndex = startIndex; // Default: keep everything in range
|
|
104
|
+
for (let i = 1; i < assistantUsages.length; i++) {
|
|
105
|
+
const tokenDiff = newestTokens - assistantUsages[i].tokens;
|
|
106
|
+
if (tokenDiff >= keepRecentTokens) {
|
|
107
|
+
// Find the turn boundary at or before the assistant we want to keep
|
|
108
|
+
const lastKeptAssistantIndex = assistantUsages[i - 1].index;
|
|
109
|
+
for (let b = boundaries.length - 1; b >= 0; b--) {
|
|
110
|
+
if (boundaries[b] <= lastKeptAssistantIndex) {
|
|
111
|
+
cutIndex = boundaries[b];
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return cutIndex;
|
|
119
|
+
}
|
|
120
|
+
// ============================================================================
|
|
121
|
+
// Summarization
|
|
122
|
+
// ============================================================================
|
|
123
|
+
const SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
|
|
124
|
+
|
|
125
|
+
Include:
|
|
126
|
+
- Current progress and key decisions made
|
|
127
|
+
- Important context, constraints, or user preferences
|
|
128
|
+
- Absolute file paths of any relevant files that were read or modified
|
|
129
|
+
- What remains to be done (clear next steps)
|
|
130
|
+
- Any critical data, examples, or references needed to continue
|
|
131
|
+
|
|
132
|
+
Be concise, structured, and focused on helping the next LLM seamlessly continue the work.`;
|
|
133
|
+
/**
|
|
134
|
+
* Generate a summary of the conversation using the LLM.
|
|
135
|
+
*/
|
|
136
|
+
export async function generateSummary(currentMessages, model, reserveTokens, apiKey, signal, customInstructions) {
|
|
137
|
+
const maxTokens = Math.floor(0.8 * reserveTokens);
|
|
138
|
+
const prompt = customInstructions
|
|
139
|
+
? `${SUMMARIZATION_PROMPT}\n\nAdditional focus: ${customInstructions}`
|
|
140
|
+
: SUMMARIZATION_PROMPT;
|
|
141
|
+
const summarizationMessages = [
|
|
142
|
+
...currentMessages,
|
|
143
|
+
{
|
|
144
|
+
role: "user",
|
|
145
|
+
content: prompt,
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
const response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });
|
|
150
|
+
const textContent = response.content
|
|
151
|
+
.filter((c) => c.type === "text")
|
|
152
|
+
.map((c) => c.text)
|
|
153
|
+
.join("\n");
|
|
154
|
+
return textContent;
|
|
155
|
+
}
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Main compaction function
|
|
158
|
+
// ============================================================================
|
|
159
|
+
/**
|
|
160
|
+
* Calculate compaction and generate summary.
|
|
161
|
+
* Returns the CompactionEntry to append to the session file.
|
|
162
|
+
*
|
|
163
|
+
* @param entries - All session entries
|
|
164
|
+
* @param model - Model to use for summarization
|
|
165
|
+
* @param settings - Compaction settings
|
|
166
|
+
* @param apiKey - API key for LLM
|
|
167
|
+
* @param signal - Optional abort signal
|
|
168
|
+
* @param customInstructions - Optional custom focus for the summary
|
|
169
|
+
*/
|
|
170
|
+
export async function compact(entries, model, settings, apiKey, signal, customInstructions) {
|
|
171
|
+
// Don't compact if the last entry is already a compaction
|
|
172
|
+
if (entries.length > 0 && entries[entries.length - 1].type === "compaction") {
|
|
173
|
+
throw new Error("Already compacted");
|
|
174
|
+
}
|
|
175
|
+
// Find previous compaction boundary
|
|
176
|
+
let prevCompactionIndex = -1;
|
|
177
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
178
|
+
if (entries[i].type === "compaction") {
|
|
179
|
+
prevCompactionIndex = i;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const boundaryStart = prevCompactionIndex + 1;
|
|
184
|
+
const boundaryEnd = entries.length;
|
|
185
|
+
// Get token count before compaction
|
|
186
|
+
const lastUsage = getLastAssistantUsage(entries);
|
|
187
|
+
const tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;
|
|
188
|
+
// Find cut point (entry index) within the valid range
|
|
189
|
+
const firstKeptEntryIndex = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
|
|
190
|
+
// Extract messages to summarize (before the cut point)
|
|
191
|
+
const messagesToSummarize = [];
|
|
192
|
+
for (let i = boundaryStart; i < firstKeptEntryIndex; i++) {
|
|
193
|
+
const entry = entries[i];
|
|
194
|
+
if (entry.type === "message") {
|
|
195
|
+
messagesToSummarize.push(entry.message);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Also include the previous summary if there was a compaction
|
|
199
|
+
if (prevCompactionIndex >= 0) {
|
|
200
|
+
const prevCompaction = entries[prevCompactionIndex];
|
|
201
|
+
// Prepend the previous summary as context
|
|
202
|
+
messagesToSummarize.unshift({
|
|
203
|
+
role: "user",
|
|
204
|
+
content: `Previous session summary:\n${prevCompaction.summary}`,
|
|
205
|
+
timestamp: Date.now(),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// Generate summary from messages before the cut point
|
|
209
|
+
const summary = await generateSummary(messagesToSummarize, model, settings.reserveTokens, apiKey, signal, customInstructions);
|
|
210
|
+
return {
|
|
211
|
+
type: "compaction",
|
|
212
|
+
timestamp: new Date().toISOString(),
|
|
213
|
+
summary,
|
|
214
|
+
firstKeptEntryIndex,
|
|
215
|
+
tokensBefore,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
//# sourceMappingURL=compaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.js","sourceRoot":"","sources":["../src/compaction.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Context compaction for long sessions.\n *\n * Pure functions for compaction logic. The session manager handles I/O,\n * and after compaction the session is reloaded.\n */\n\nimport type { AppMessage } from \"@mariozechner/pi-agent-core\";\nimport type { AssistantMessage, Model, Usage } from \"@mariozechner/pi-ai\";\nimport { complete } from \"@mariozechner/pi-ai\";\nimport type { CompactionEntry, SessionEntry } from \"./session-manager.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\tkeepRecentTokens: number;\n}\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n};\n\n// ============================================================================\n// Token calculation\n// ============================================================================\n\n/**\n * Calculate total context tokens from usage.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n */\nfunction getAssistantUsage(msg: AppMessage): Usage | null {\n\tif (msg.role === \"assistant\" && \"usage\" in msg) {\n\t\tconst assistantMsg = msg as AssistantMessage;\n\t\tif (assistantMsg.stopReason !== \"aborted\" && assistantMsg.usage) {\n\t\t\treturn assistantMsg.usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Find the last non-aborted assistant message usage from session entries.\n */\nexport function getLastAssistantUsage(entries: SessionEntry[]): Usage | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) return usage;\n\t\t}\n\t}\n\treturn null;\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tif (!settings.enabled) return false;\n\treturn contextTokens > contextWindow - settings.reserveTokens;\n}\n\n// ============================================================================\n// Cut point detection\n// ============================================================================\n\n/**\n * Find indices of message entries that are user messages (turn boundaries).\n */\nfunction findTurnBoundaries(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst boundaries: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\" && entry.message.role === \"user\") {\n\t\t\tboundaries.push(i);\n\t\t}\n\t}\n\treturn boundaries;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n * Returns the entry index of the first message to keep (a user message for turn integrity).\n *\n * Only considers entries between `startIndex` and `endIndex` (exclusive).\n */\nexport function findCutPoint(\n\tentries: SessionEntry[],\n\tstartIndex: number,\n\tendIndex: number,\n\tkeepRecentTokens: number,\n): number {\n\tconst boundaries = findTurnBoundaries(entries, startIndex, endIndex);\n\n\tif (boundaries.length === 0) {\n\t\treturn startIndex; // No user messages, keep everything in range\n\t}\n\n\t// Collect assistant usages walking backwards from endIndex\n\tconst assistantUsages: Array<{ index: number; tokens: number }> = [];\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst usage = getAssistantUsage(entry.message);\n\t\t\tif (usage) {\n\t\t\t\tassistantUsages.push({\n\t\t\t\t\tindex: i,\n\t\t\t\t\ttokens: calculateContextTokens(usage),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\tif (assistantUsages.length === 0) {\n\t\t// No usage info, keep last turn only\n\t\treturn boundaries[boundaries.length - 1];\n\t}\n\n\t// Walk through and find where cumulative token difference exceeds keepRecentTokens\n\tconst newestTokens = assistantUsages[0].tokens;\n\tlet cutIndex = startIndex; // Default: keep everything in range\n\n\tfor (let i = 1; i < assistantUsages.length; i++) {\n\t\tconst tokenDiff = newestTokens - assistantUsages[i].tokens;\n\t\tif (tokenDiff >= keepRecentTokens) {\n\t\t\t// Find the turn boundary at or before the assistant we want to keep\n\t\t\tconst lastKeptAssistantIndex = assistantUsages[i - 1].index;\n\n\t\t\tfor (let b = boundaries.length - 1; b >= 0; b--) {\n\t\t\t\tif (boundaries[b] <= lastKeptAssistantIndex) {\n\t\t\t\t\tcutIndex = boundaries[b];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn cutIndex;\n}\n\n// ============================================================================\n// Summarization\n// ============================================================================\n\nconst SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.\n\nInclude:\n- Current progress and key decisions made\n- Important context, constraints, or user preferences\n- Absolute file paths of any relevant files that were read or modified\n- What remains to be done (clear next steps)\n- Any critical data, examples, or references needed to continue\n\nBe concise, structured, and focused on helping the next LLM seamlessly continue the work.`;\n\n/**\n * Generate a summary of the conversation using the LLM.\n */\nexport async function generateSummary(\n\tcurrentMessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.8 * reserveTokens);\n\n\tconst prompt = customInstructions\n\t\t? `${SUMMARIZATION_PROMPT}\\n\\nAdditional focus: ${customInstructions}`\n\t\t: SUMMARIZATION_PROMPT;\n\n\tconst summarizationMessages = [\n\t\t...currentMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: prompt,\n\t\t\ttimestamp: Date.now(),\n\t\t},\n\t];\n\n\tconst response = await complete(model, { messages: summarizationMessages }, { maxTokens, signal, apiKey });\n\n\tconst textContent = response.content\n\t\t.filter((c): c is { type: \"text\"; text: string } => c.type === \"text\")\n\t\t.map((c) => c.text)\n\t\t.join(\"\\n\");\n\n\treturn textContent;\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\n/**\n * Calculate compaction and generate summary.\n * Returns the CompactionEntry to append to the session file.\n *\n * @param entries - All session entries\n * @param model - Model to use for summarization\n * @param settings - Compaction settings\n * @param apiKey - API key for LLM\n * @param signal - Optional abort signal\n * @param customInstructions - Optional custom focus for the summary\n */\nexport async function compact(\n\tentries: SessionEntry[],\n\tmodel: Model<any>,\n\tsettings: CompactionSettings,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n\tcustomInstructions?: string,\n): Promise<CompactionEntry> {\n\t// Don't compact if the last entry is already a compaction\n\tif (entries.length > 0 && entries[entries.length - 1].type === \"compaction\") {\n\t\tthrow new Error(\"Already compacted\");\n\t}\n\n\t// Find previous compaction boundary\n\tlet prevCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tprevCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\tconst boundaryStart = prevCompactionIndex + 1;\n\tconst boundaryEnd = entries.length;\n\n\t// Get token count before compaction\n\tconst lastUsage = getLastAssistantUsage(entries);\n\tconst tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;\n\n\t// Find cut point (entry index) within the valid range\n\tconst firstKeptEntryIndex = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Extract messages to summarize (before the cut point)\n\tconst messagesToSummarize: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < firstKeptEntryIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tmessagesToSummarize.push(entry.message);\n\t\t}\n\t}\n\n\t// Also include the previous summary if there was a compaction\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\t// Prepend the previous summary as context\n\t\tmessagesToSummarize.unshift({\n\t\t\trole: \"user\",\n\t\t\tcontent: `Previous session summary:\\n${prevCompaction.summary}`,\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t}\n\n\t// Generate summary from messages before the cut point\n\tconst summary = await generateSummary(\n\t\tmessagesToSummarize,\n\t\tmodel,\n\t\tsettings.reserveTokens,\n\t\tapiKey,\n\t\tsignal,\n\t\tcustomInstructions,\n\t);\n\n\treturn {\n\t\ttype: \"compaction\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tsummary,\n\t\tfirstKeptEntryIndex,\n\t\ttokensBefore,\n\t};\n}\n"]}
|
package/dist/export-html.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { AgentState } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import type { SessionManager } from "./session-manager.js";
|
|
3
3
|
/**
|
|
4
|
-
* Export session to
|
|
4
|
+
* Export session to HTML using SessionManager and AgentState.
|
|
5
|
+
* Used by TUI's /export command.
|
|
5
6
|
*/
|
|
6
7
|
export declare function exportSessionToHtml(sessionManager: SessionManager, state: AgentState, outputPath?: string): string;
|
|
7
8
|
/**
|
|
8
|
-
* Export
|
|
9
|
-
* Auto-detects format: session manager format or streaming event format
|
|
9
|
+
* Export session file to HTML (standalone, without AgentState).
|
|
10
|
+
* Auto-detects format: session manager format or streaming event format.
|
|
11
|
+
* Used by CLI for exporting arbitrary session files.
|
|
10
12
|
*/
|
|
11
13
|
export declare function exportFromFile(inputPath: string, outputPath?: string): string;
|
|
12
14
|
//# sourceMappingURL=export-html.d.ts.map
|