@mariozechner/pi-coding-agent 0.30.2 → 0.31.0
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 +244 -1
- package/README.md +105 -84
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts +3 -3
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +7 -10
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +73 -34
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +464 -210
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +2 -2
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +2 -2
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts +2 -2
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +2 -2
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +84 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -0
- package/dist/core/compaction/branch-summarization.js +233 -0
- package/dist/core/compaction/branch-summarization.js.map +1 -0
- package/dist/core/{compaction.d.ts → compaction/compaction.d.ts} +38 -19
- package/dist/core/compaction/compaction.d.ts.map +1 -0
- package/dist/core/compaction/compaction.js +558 -0
- package/dist/core/compaction/compaction.js.map +1 -0
- package/dist/core/compaction/index.d.ts +7 -0
- package/dist/core/compaction/index.d.ts.map +1 -0
- package/dist/core/compaction/index.js +7 -0
- package/dist/core/compaction/index.js.map +1 -0
- package/dist/core/compaction/utils.d.ts +35 -0
- package/dist/core/compaction/utils.d.ts.map +1 -0
- package/dist/core/compaction/utils.js +138 -0
- package/dist/core/compaction/utils.js.map +1 -0
- package/dist/core/custom-tools/index.d.ts +2 -1
- package/dist/core/custom-tools/index.d.ts.map +1 -1
- package/dist/core/custom-tools/index.js +1 -0
- package/dist/core/custom-tools/index.js.map +1 -1
- package/dist/core/custom-tools/loader.d.ts.map +1 -1
- package/dist/core/custom-tools/loader.js +13 -80
- package/dist/core/custom-tools/loader.js.map +1 -1
- package/dist/core/custom-tools/types.d.ts +84 -59
- package/dist/core/custom-tools/types.d.ts.map +1 -1
- package/dist/core/custom-tools/types.js.map +1 -1
- package/dist/core/custom-tools/wrapper.d.ts +15 -0
- package/dist/core/custom-tools/wrapper.d.ts.map +1 -0
- package/dist/core/custom-tools/wrapper.js +23 -0
- package/dist/core/custom-tools/wrapper.js.map +1 -0
- package/dist/core/exec.d.ts +29 -0
- package/dist/core/exec.d.ts.map +1 -0
- package/dist/core/exec.js +71 -0
- package/dist/core/exec.js.map +1 -0
- package/dist/core/export-html/index.d.ts +17 -0
- package/dist/core/export-html/index.d.ts.map +1 -0
- package/dist/core/export-html/index.js +171 -0
- package/dist/core/export-html/index.js.map +1 -0
- package/dist/core/export-html/template.css +781 -0
- package/dist/core/export-html/template.html +54 -0
- package/dist/core/export-html/template.js +1185 -0
- package/dist/core/export-html/vendor/highlight.min.js +1213 -0
- package/dist/core/export-html/vendor/marked.min.js +6 -0
- package/dist/core/hooks/index.d.ts +4 -4
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +3 -3
- package/dist/core/hooks/index.js.map +1 -1
- package/dist/core/hooks/loader.d.ts +40 -5
- package/dist/core/hooks/loader.d.ts.map +1 -1
- package/dist/core/hooks/loader.js +43 -10
- package/dist/core/hooks/loader.js.map +1 -1
- package/dist/core/hooks/runner.d.ts +94 -18
- package/dist/core/hooks/runner.d.ts.map +1 -1
- package/dist/core/hooks/runner.js +199 -120
- package/dist/core/hooks/runner.js.map +1 -1
- package/dist/core/hooks/tool-wrapper.d.ts +1 -1
- package/dist/core/hooks/tool-wrapper.d.ts.map +1 -1
- package/dist/core/hooks/tool-wrapper.js +36 -19
- package/dist/core/hooks/tool-wrapper.js.map +1 -1
- package/dist/core/hooks/types.d.ts +407 -96
- package/dist/core/hooks/types.d.ts.map +1 -1
- package/dist/core/hooks/types.js.map +1 -1
- package/dist/core/index.d.ts +4 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/messages.d.ts +44 -12
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +82 -34
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +5 -5
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +7 -7
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +7 -7
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +45 -14
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts +7 -10
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +88 -32
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +202 -36
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +565 -133
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +9 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +13 -12
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +6 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/bash.d.ts +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +33 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -0
- package/dist/core/tools/edit-diff.js +171 -0
- package/dist/core/tools/edit-diff.js.map +1 -0
- package/dist/core/tools/edit.d.ts +7 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +20 -95
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/write.d.ts +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +22 -21
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +3 -4
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +6 -2
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts +12 -0
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -0
- package/dist/modes/interactive/components/bordered-loader.js +30 -0
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -0
- package/dist/modes/interactive/components/branch-summary-message.d.ts +14 -0
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/branch-summary-message.js +35 -0
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -0
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +14 -0
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/compaction-summary-message.js +36 -0
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -0
- package/dist/modes/interactive/components/dynamic-border.d.ts +5 -1
- package/dist/modes/interactive/components/dynamic-border.d.ts.map +1 -1
- package/dist/modes/interactive/components/dynamic-border.js +5 -1
- package/dist/modes/interactive/components/dynamic-border.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +12 -6
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +57 -25
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/hook-editor.d.ts +15 -0
- package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-editor.js +95 -0
- package/dist/modes/interactive/components/hook-editor.js.map +1 -0
- package/dist/modes/interactive/components/hook-message.d.ts +18 -0
- package/dist/modes/interactive/components/hook-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-message.js +80 -0
- package/dist/modes/interactive/components/hook-message.js.map +1 -0
- package/dist/modes/interactive/components/model-selector.d.ts +3 -3
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +1 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +15 -2
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +70 -21
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +52 -0
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/tree-selector.js +745 -0
- package/dist/modes/interactive/components/tree-selector.js.map +1 -0
- package/dist/modes/interactive/components/user-message-selector.d.ts +3 -3
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +1 -1
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +1 -1
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +2 -5
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +29 -12
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +589 -208
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +13 -1
- package/dist/modes/interactive/theme/light.json +13 -1
- package/dist/modes/interactive/theme/theme-schema.json +34 -0
- package/dist/modes/interactive/theme/theme.d.ts +20 -2
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +135 -2
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts +3 -3
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +26 -20
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +13 -10
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +11 -10
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +88 -35
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +30 -11
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/utils/shell.d.ts +4 -2
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +36 -7
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +2 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/compaction.md +388 -0
- package/docs/custom-tools.md +146 -43
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +562 -596
- package/docs/rpc.md +33 -19
- package/docs/sdk.md +93 -21
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +172 -21
- package/docs/skills.md +2 -0
- package/docs/theme.md +31 -2
- package/docs/tree.md +197 -0
- package/docs/tui.md +343 -0
- package/examples/README.md +1 -9
- package/examples/custom-tools/hello/index.ts +4 -3
- package/examples/custom-tools/question/index.ts +4 -4
- package/examples/custom-tools/subagent/index.ts +7 -6
- package/examples/custom-tools/todo/index.ts +11 -5
- package/examples/hooks/README.md +29 -71
- package/examples/hooks/auto-commit-on-exit.ts +8 -9
- package/examples/hooks/confirm-destructive.ts +29 -30
- package/examples/hooks/custom-compaction.ts +20 -21
- package/examples/hooks/dirty-repo-guard.ts +41 -40
- package/examples/hooks/file-trigger.ts +10 -5
- package/examples/hooks/git-checkpoint.ts +16 -12
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +1 -1
- package/examples/hooks/protected-paths.ts +1 -1
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +1 -1
- package/examples/sdk/02-custom-model.ts +1 -1
- package/examples/sdk/03-custom-prompt.ts +1 -1
- package/examples/sdk/04-skills.ts +1 -1
- package/examples/sdk/05-tools.ts +4 -4
- package/examples/sdk/06-hooks.ts +1 -1
- package/examples/sdk/07-context-files.ts +1 -1
- package/examples/sdk/08-slash-commands.ts +6 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +1 -1
- package/examples/sdk/10-settings.ts +1 -1
- package/examples/sdk/11-sessions.ts +1 -1
- package/examples/sdk/12-full-control.ts +4 -7
- package/package.json +6 -6
- package/dist/core/compaction.d.ts.map +0 -1
- package/dist/core/compaction.js +0 -412
- package/dist/core/compaction.js.map +0 -1
- package/dist/core/export-html.d.ts +0 -23
- package/dist/core/export-html.d.ts.map +0 -1
- package/dist/core/export-html.js +0 -1185
- package/dist/core/export-html.js.map +0 -1
- package/dist/modes/interactive/components/compaction.d.ts +0 -15
- package/dist/modes/interactive/components/compaction.d.ts.map +0 -1
- package/dist/modes/interactive/components/compaction.js +0 -41
- package/dist/modes/interactive/components/compaction.js.map +0 -1
- package/docs/hooks-v2.md +0 -385
- package/docs/session-tree.md +0 -452
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snake game hook - play snake with /snake command
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import { isArrowDown, isArrowLeft, isArrowRight, isArrowUp, isEscape, visibleWidth } from "@mariozechner/pi-tui";
|
|
7
|
+
|
|
8
|
+
const GAME_WIDTH = 40;
|
|
9
|
+
const GAME_HEIGHT = 15;
|
|
10
|
+
const TICK_MS = 100;
|
|
11
|
+
|
|
12
|
+
type Direction = "up" | "down" | "left" | "right";
|
|
13
|
+
type Point = { x: number; y: number };
|
|
14
|
+
|
|
15
|
+
interface GameState {
|
|
16
|
+
snake: Point[];
|
|
17
|
+
food: Point;
|
|
18
|
+
direction: Direction;
|
|
19
|
+
nextDirection: Direction;
|
|
20
|
+
score: number;
|
|
21
|
+
gameOver: boolean;
|
|
22
|
+
highScore: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createInitialState(): GameState {
|
|
26
|
+
const startX = Math.floor(GAME_WIDTH / 2);
|
|
27
|
+
const startY = Math.floor(GAME_HEIGHT / 2);
|
|
28
|
+
return {
|
|
29
|
+
snake: [
|
|
30
|
+
{ x: startX, y: startY },
|
|
31
|
+
{ x: startX - 1, y: startY },
|
|
32
|
+
{ x: startX - 2, y: startY },
|
|
33
|
+
],
|
|
34
|
+
food: spawnFood([{ x: startX, y: startY }]),
|
|
35
|
+
direction: "right",
|
|
36
|
+
nextDirection: "right",
|
|
37
|
+
score: 0,
|
|
38
|
+
gameOver: false,
|
|
39
|
+
highScore: 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function spawnFood(snake: Point[]): Point {
|
|
44
|
+
let food: Point;
|
|
45
|
+
do {
|
|
46
|
+
food = {
|
|
47
|
+
x: Math.floor(Math.random() * GAME_WIDTH),
|
|
48
|
+
y: Math.floor(Math.random() * GAME_HEIGHT),
|
|
49
|
+
};
|
|
50
|
+
} while (snake.some((s) => s.x === food.x && s.y === food.y));
|
|
51
|
+
return food;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class SnakeComponent {
|
|
55
|
+
private state: GameState;
|
|
56
|
+
private interval: ReturnType<typeof setInterval> | null = null;
|
|
57
|
+
private onClose: () => void;
|
|
58
|
+
private onSave: (state: GameState | null) => void;
|
|
59
|
+
private tui: { requestRender: () => void };
|
|
60
|
+
private cachedLines: string[] = [];
|
|
61
|
+
private cachedWidth = 0;
|
|
62
|
+
private version = 0;
|
|
63
|
+
private cachedVersion = -1;
|
|
64
|
+
private paused: boolean;
|
|
65
|
+
|
|
66
|
+
constructor(
|
|
67
|
+
tui: { requestRender: () => void },
|
|
68
|
+
onClose: () => void,
|
|
69
|
+
onSave: (state: GameState | null) => void,
|
|
70
|
+
savedState?: GameState,
|
|
71
|
+
) {
|
|
72
|
+
this.tui = tui;
|
|
73
|
+
if (savedState && !savedState.gameOver) {
|
|
74
|
+
// Resume from saved state, start paused
|
|
75
|
+
this.state = savedState;
|
|
76
|
+
this.paused = true;
|
|
77
|
+
} else {
|
|
78
|
+
// New game or saved game was over
|
|
79
|
+
this.state = createInitialState();
|
|
80
|
+
if (savedState) {
|
|
81
|
+
this.state.highScore = savedState.highScore;
|
|
82
|
+
}
|
|
83
|
+
this.paused = false;
|
|
84
|
+
this.startGame();
|
|
85
|
+
}
|
|
86
|
+
this.onClose = onClose;
|
|
87
|
+
this.onSave = onSave;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private startGame(): void {
|
|
91
|
+
this.interval = setInterval(() => {
|
|
92
|
+
if (!this.state.gameOver) {
|
|
93
|
+
this.tick();
|
|
94
|
+
this.version++;
|
|
95
|
+
this.tui.requestRender();
|
|
96
|
+
}
|
|
97
|
+
}, TICK_MS);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private tick(): void {
|
|
101
|
+
// Apply queued direction change
|
|
102
|
+
this.state.direction = this.state.nextDirection;
|
|
103
|
+
|
|
104
|
+
// Calculate new head position
|
|
105
|
+
const head = this.state.snake[0];
|
|
106
|
+
let newHead: Point;
|
|
107
|
+
|
|
108
|
+
switch (this.state.direction) {
|
|
109
|
+
case "up":
|
|
110
|
+
newHead = { x: head.x, y: head.y - 1 };
|
|
111
|
+
break;
|
|
112
|
+
case "down":
|
|
113
|
+
newHead = { x: head.x, y: head.y + 1 };
|
|
114
|
+
break;
|
|
115
|
+
case "left":
|
|
116
|
+
newHead = { x: head.x - 1, y: head.y };
|
|
117
|
+
break;
|
|
118
|
+
case "right":
|
|
119
|
+
newHead = { x: head.x + 1, y: head.y };
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check wall collision
|
|
124
|
+
if (newHead.x < 0 || newHead.x >= GAME_WIDTH || newHead.y < 0 || newHead.y >= GAME_HEIGHT) {
|
|
125
|
+
this.state.gameOver = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check self collision
|
|
130
|
+
if (this.state.snake.some((s) => s.x === newHead.x && s.y === newHead.y)) {
|
|
131
|
+
this.state.gameOver = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Move snake
|
|
136
|
+
this.state.snake.unshift(newHead);
|
|
137
|
+
|
|
138
|
+
// Check food collision
|
|
139
|
+
if (newHead.x === this.state.food.x && newHead.y === this.state.food.y) {
|
|
140
|
+
this.state.score += 10;
|
|
141
|
+
if (this.state.score > this.state.highScore) {
|
|
142
|
+
this.state.highScore = this.state.score;
|
|
143
|
+
}
|
|
144
|
+
this.state.food = spawnFood(this.state.snake);
|
|
145
|
+
} else {
|
|
146
|
+
this.state.snake.pop();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
handleInput(data: string): void {
|
|
151
|
+
// If paused (resuming), wait for any key
|
|
152
|
+
if (this.paused) {
|
|
153
|
+
if (isEscape(data) || data === "q" || data === "Q") {
|
|
154
|
+
// Quit without clearing save
|
|
155
|
+
this.dispose();
|
|
156
|
+
this.onClose();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Any other key resumes
|
|
160
|
+
this.paused = false;
|
|
161
|
+
this.startGame();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ESC to pause and save
|
|
166
|
+
if (isEscape(data)) {
|
|
167
|
+
this.dispose();
|
|
168
|
+
this.onSave(this.state);
|
|
169
|
+
this.onClose();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Q to quit without saving (clears saved state)
|
|
174
|
+
if (data === "q" || data === "Q") {
|
|
175
|
+
this.dispose();
|
|
176
|
+
this.onSave(null); // Clear saved state
|
|
177
|
+
this.onClose();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Arrow keys or WASD
|
|
182
|
+
if (isArrowUp(data) || data === "w" || data === "W") {
|
|
183
|
+
if (this.state.direction !== "down") this.state.nextDirection = "up";
|
|
184
|
+
} else if (isArrowDown(data) || data === "s" || data === "S") {
|
|
185
|
+
if (this.state.direction !== "up") this.state.nextDirection = "down";
|
|
186
|
+
} else if (isArrowRight(data) || data === "d" || data === "D") {
|
|
187
|
+
if (this.state.direction !== "left") this.state.nextDirection = "right";
|
|
188
|
+
} else if (isArrowLeft(data) || data === "a" || data === "A") {
|
|
189
|
+
if (this.state.direction !== "right") this.state.nextDirection = "left";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Restart on game over
|
|
193
|
+
if (this.state.gameOver && (data === "r" || data === "R" || data === " ")) {
|
|
194
|
+
const highScore = this.state.highScore;
|
|
195
|
+
this.state = createInitialState();
|
|
196
|
+
this.state.highScore = highScore;
|
|
197
|
+
this.onSave(null); // Clear saved state on restart
|
|
198
|
+
this.version++;
|
|
199
|
+
this.tui.requestRender();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
invalidate(): void {
|
|
204
|
+
this.cachedWidth = 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
render(width: number): string[] {
|
|
208
|
+
if (width === this.cachedWidth && this.cachedVersion === this.version) {
|
|
209
|
+
return this.cachedLines;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const lines: string[] = [];
|
|
213
|
+
|
|
214
|
+
// Each game cell is 2 chars wide to appear square (terminal cells are ~2:1 aspect)
|
|
215
|
+
const cellWidth = 2;
|
|
216
|
+
const effectiveWidth = Math.min(GAME_WIDTH, Math.floor((width - 4) / cellWidth));
|
|
217
|
+
const effectiveHeight = GAME_HEIGHT;
|
|
218
|
+
|
|
219
|
+
// Colors
|
|
220
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
|
|
221
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
222
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
223
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
224
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
|
|
225
|
+
|
|
226
|
+
const boxWidth = effectiveWidth * cellWidth;
|
|
227
|
+
|
|
228
|
+
// Helper to pad content inside box
|
|
229
|
+
const boxLine = (content: string) => {
|
|
230
|
+
const contentLen = visibleWidth(content);
|
|
231
|
+
const padding = Math.max(0, boxWidth - contentLen);
|
|
232
|
+
return dim(" │") + content + " ".repeat(padding) + dim("│");
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// Top border
|
|
236
|
+
lines.push(this.padLine(dim(` ╭${"─".repeat(boxWidth)}╮`), width));
|
|
237
|
+
|
|
238
|
+
// Header with score
|
|
239
|
+
const scoreText = `Score: ${bold(yellow(String(this.state.score)))}`;
|
|
240
|
+
const highText = `High: ${bold(yellow(String(this.state.highScore)))}`;
|
|
241
|
+
const title = `${bold(green("SNAKE"))} │ ${scoreText} │ ${highText}`;
|
|
242
|
+
lines.push(this.padLine(boxLine(title), width));
|
|
243
|
+
|
|
244
|
+
// Separator
|
|
245
|
+
lines.push(this.padLine(dim(` ├${"─".repeat(boxWidth)}┤`), width));
|
|
246
|
+
|
|
247
|
+
// Game grid
|
|
248
|
+
for (let y = 0; y < effectiveHeight; y++) {
|
|
249
|
+
let row = "";
|
|
250
|
+
for (let x = 0; x < effectiveWidth; x++) {
|
|
251
|
+
const isHead = this.state.snake[0].x === x && this.state.snake[0].y === y;
|
|
252
|
+
const isBody = this.state.snake.slice(1).some((s) => s.x === x && s.y === y);
|
|
253
|
+
const isFood = this.state.food.x === x && this.state.food.y === y;
|
|
254
|
+
|
|
255
|
+
if (isHead) {
|
|
256
|
+
row += green("██"); // Snake head (2 chars)
|
|
257
|
+
} else if (isBody) {
|
|
258
|
+
row += green("▓▓"); // Snake body (2 chars)
|
|
259
|
+
} else if (isFood) {
|
|
260
|
+
row += red("◆ "); // Food (2 chars)
|
|
261
|
+
} else {
|
|
262
|
+
row += " "; // Empty cell (2 spaces)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
lines.push(this.padLine(dim(" │") + row + dim("│"), width));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Separator
|
|
269
|
+
lines.push(this.padLine(dim(` ├${"─".repeat(boxWidth)}┤`), width));
|
|
270
|
+
|
|
271
|
+
// Footer
|
|
272
|
+
let footer: string;
|
|
273
|
+
if (this.paused) {
|
|
274
|
+
footer = `${yellow(bold("PAUSED"))} Press any key to continue, ${bold("Q")} to quit`;
|
|
275
|
+
} else if (this.state.gameOver) {
|
|
276
|
+
footer = `${red(bold("GAME OVER!"))} Press ${bold("R")} to restart, ${bold("Q")} to quit`;
|
|
277
|
+
} else {
|
|
278
|
+
footer = `↑↓←→ or WASD to move, ${bold("ESC")} pause, ${bold("Q")} quit`;
|
|
279
|
+
}
|
|
280
|
+
lines.push(this.padLine(boxLine(footer), width));
|
|
281
|
+
|
|
282
|
+
// Bottom border
|
|
283
|
+
lines.push(this.padLine(dim(` ╰${"─".repeat(boxWidth)}╯`), width));
|
|
284
|
+
|
|
285
|
+
this.cachedLines = lines;
|
|
286
|
+
this.cachedWidth = width;
|
|
287
|
+
this.cachedVersion = this.version;
|
|
288
|
+
|
|
289
|
+
return lines;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private padLine(line: string, width: number): string {
|
|
293
|
+
// Calculate visible length (strip ANSI codes)
|
|
294
|
+
const visibleLen = line.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
295
|
+
const padding = Math.max(0, width - visibleLen);
|
|
296
|
+
return line + " ".repeat(padding);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
dispose(): void {
|
|
300
|
+
if (this.interval) {
|
|
301
|
+
clearInterval(this.interval);
|
|
302
|
+
this.interval = null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const SNAKE_SAVE_TYPE = "snake-save";
|
|
308
|
+
|
|
309
|
+
export default function (pi: HookAPI) {
|
|
310
|
+
pi.registerCommand("snake", {
|
|
311
|
+
description: "Play Snake!",
|
|
312
|
+
|
|
313
|
+
handler: async (_args, ctx) => {
|
|
314
|
+
if (!ctx.hasUI) {
|
|
315
|
+
ctx.ui.notify("Snake requires interactive mode", "error");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Load saved state from session
|
|
320
|
+
const entries = ctx.sessionManager.getEntries();
|
|
321
|
+
let savedState: GameState | undefined;
|
|
322
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
323
|
+
const entry = entries[i];
|
|
324
|
+
if (entry.type === "custom" && entry.customType === SNAKE_SAVE_TYPE) {
|
|
325
|
+
savedState = entry.data as GameState;
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
await ctx.ui.custom((tui, _theme, done) => {
|
|
331
|
+
return new SnakeComponent(
|
|
332
|
+
tui,
|
|
333
|
+
() => done(undefined),
|
|
334
|
+
(state) => {
|
|
335
|
+
// Save or clear state
|
|
336
|
+
pi.appendEntry(SNAKE_SAVE_TYPE, state);
|
|
337
|
+
},
|
|
338
|
+
savedState,
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status Line Hook
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates ctx.ui.setStatus() for displaying persistent status text in the footer.
|
|
5
|
+
* Shows turn progress with themed colors.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
export default function (pi: HookAPI) {
|
|
11
|
+
let turnCount = 0;
|
|
12
|
+
|
|
13
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
14
|
+
const theme = ctx.ui.theme;
|
|
15
|
+
ctx.ui.setStatus("status-demo", theme.fg("dim", "Ready"));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
pi.on("turn_start", async (_event, ctx) => {
|
|
19
|
+
turnCount++;
|
|
20
|
+
const theme = ctx.ui.theme;
|
|
21
|
+
const spinner = theme.fg("accent", "●");
|
|
22
|
+
const text = theme.fg("dim", ` Turn ${turnCount}...`);
|
|
23
|
+
ctx.ui.setStatus("status-demo", spinner + text);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
27
|
+
const theme = ctx.ui.theme;
|
|
28
|
+
const check = theme.fg("success", "✓");
|
|
29
|
+
const text = theme.fg("dim", ` Turn ${turnCount} complete`);
|
|
30
|
+
ctx.ui.setStatus("status-demo", check + text);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
pi.on("session_switch", async (event, ctx) => {
|
|
34
|
+
if (event.reason === "new") {
|
|
35
|
+
turnCount = 0;
|
|
36
|
+
const theme = ctx.ui.theme;
|
|
37
|
+
ctx.ui.setStatus("status-demo", theme.fg("dim", "Ready"));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* from cwd and ~/.pi/agent. Model chosen from settings or first available.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { createAgentSession } from "
|
|
8
|
+
import { createAgentSession } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
|
|
10
10
|
const { session } = await createAgentSession();
|
|
11
11
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getModel } from "@mariozechner/pi-ai";
|
|
8
|
-
import { createAgentSession, discoverAuthStorage, discoverModels } from "
|
|
8
|
+
import { createAgentSession, discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
|
|
10
10
|
// Set up auth storage and model registry
|
|
11
11
|
const authStorage = discoverAuthStorage();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Shows how to replace or modify the default system prompt.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createAgentSession, SessionManager } from "
|
|
7
|
+
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
// Option 1: Replace prompt entirely
|
|
10
10
|
const { session: session1 } = await createAgentSession({
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Discover, filter, merge, or replace them.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { createAgentSession, discoverSkills, SessionManager, type Skill } from "
|
|
8
|
+
import { createAgentSession, discoverSkills, SessionManager, type Skill } from "@mariozechner/pi-coding-agent";
|
|
9
9
|
|
|
10
10
|
// Discover all skills from cwd/.pi/skills, ~/.pi/agent/skills, etc.
|
|
11
11
|
const allSkills = discoverSkills();
|
package/examples/sdk/05-tools.ts
CHANGED
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* tools resolve paths relative to your cwd, not process.cwd().
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { Type } from "@sinclair/typebox";
|
|
12
11
|
import {
|
|
13
12
|
bashTool, // read, bash, edit, write - uses process.cwd()
|
|
14
|
-
type
|
|
13
|
+
type CustomTool,
|
|
15
14
|
createAgentSession,
|
|
16
15
|
createBashTool,
|
|
17
16
|
createCodingTools, // Factory: creates tools for specific cwd
|
|
@@ -21,7 +20,8 @@ import {
|
|
|
21
20
|
readOnlyTools, // read, grep, find, ls - uses process.cwd()
|
|
22
21
|
readTool,
|
|
23
22
|
SessionManager,
|
|
24
|
-
} from "
|
|
23
|
+
} from "@mariozechner/pi-coding-agent";
|
|
24
|
+
import { Type } from "@sinclair/typebox";
|
|
25
25
|
|
|
26
26
|
// Read-only mode (no edit/write) - uses process.cwd()
|
|
27
27
|
await createAgentSession({
|
|
@@ -55,7 +55,7 @@ await createAgentSession({
|
|
|
55
55
|
console.log("Specific tools with custom cwd session created");
|
|
56
56
|
|
|
57
57
|
// Inline custom tool (needs TypeBox schema)
|
|
58
|
-
const weatherTool:
|
|
58
|
+
const weatherTool: CustomTool = {
|
|
59
59
|
name: "get_weather",
|
|
60
60
|
label: "Get Weather",
|
|
61
61
|
description: "Get current weather for a city",
|
package/examples/sdk/06-hooks.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Hooks intercept agent events for logging, blocking, or modification.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createAgentSession, type HookFactory, SessionManager } from "
|
|
7
|
+
import { createAgentSession, type HookFactory, SessionManager } from "@mariozechner/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
// Logging hook
|
|
10
10
|
const loggingHook: HookFactory = (api) => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Context files provide project-specific instructions loaded into the system prompt.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createAgentSession, discoverContextFiles, SessionManager } from "
|
|
7
|
+
import { createAgentSession, discoverContextFiles, SessionManager } from "@mariozechner/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
// Discover AGENTS.md files walking up from cwd
|
|
10
10
|
const discovered = discoverContextFiles();
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
* File-based commands that inject content when invoked with /commandname.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
createAgentSession,
|
|
9
|
+
discoverSlashCommands,
|
|
10
|
+
type FileSlashCommand,
|
|
11
|
+
SessionManager,
|
|
12
|
+
} from "@mariozechner/pi-coding-agent";
|
|
8
13
|
|
|
9
14
|
// Discover commands from cwd/.pi/commands/ and ~/.pi/agent/commands/
|
|
10
15
|
const discovered = discoverSlashCommands();
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
discoverModels,
|
|
12
12
|
ModelRegistry,
|
|
13
13
|
SessionManager,
|
|
14
|
-
} from "
|
|
14
|
+
} from "@mariozechner/pi-coding-agent";
|
|
15
15
|
|
|
16
16
|
// Default: discoverAuthStorage() uses ~/.pi/agent/auth.json
|
|
17
17
|
// discoverModels() loads built-in + custom models from ~/.pi/agent/models.json
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Override settings using SettingsManager.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createAgentSession, loadSettings, SessionManager, SettingsManager } from "
|
|
7
|
+
import { createAgentSession, loadSettings, SessionManager, SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
// Load current settings (merged global + project)
|
|
10
10
|
const settings = loadSettings();
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Control session persistence: in-memory, new file, continue, or open specific.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createAgentSession, SessionManager } from "
|
|
7
|
+
import { createAgentSession, SessionManager } from "@mariozechner/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
// In-memory (no persistence)
|
|
10
10
|
const { session: inMemory } = await createAgentSession({
|
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { getModel } from "@mariozechner/pi-ai";
|
|
12
|
-
import { Type } from "@sinclair/typebox";
|
|
13
12
|
import {
|
|
14
13
|
AuthStorage,
|
|
15
|
-
type
|
|
14
|
+
type CustomTool,
|
|
16
15
|
createAgentSession,
|
|
17
16
|
createBashTool,
|
|
18
17
|
createReadTool,
|
|
@@ -20,7 +19,8 @@ import {
|
|
|
20
19
|
ModelRegistry,
|
|
21
20
|
SessionManager,
|
|
22
21
|
SettingsManager,
|
|
23
|
-
} from "
|
|
22
|
+
} from "@mariozechner/pi-coding-agent";
|
|
23
|
+
import { Type } from "@sinclair/typebox";
|
|
24
24
|
|
|
25
25
|
// Custom auth storage location
|
|
26
26
|
const authStorage = new AuthStorage("/tmp/my-agent/auth.json");
|
|
@@ -42,7 +42,7 @@ const auditHook: HookFactory = (api) => {
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
// Inline custom tool
|
|
45
|
-
const statusTool:
|
|
45
|
+
const statusTool: CustomTool = {
|
|
46
46
|
name: "status",
|
|
47
47
|
label: "Status",
|
|
48
48
|
description: "Get system status",
|
|
@@ -68,15 +68,12 @@ const cwd = process.cwd();
|
|
|
68
68
|
const { session } = await createAgentSession({
|
|
69
69
|
cwd,
|
|
70
70
|
agentDir: "/tmp/my-agent",
|
|
71
|
-
|
|
72
71
|
model,
|
|
73
72
|
thinkingLevel: "off",
|
|
74
73
|
authStorage,
|
|
75
74
|
modelRegistry,
|
|
76
|
-
|
|
77
75
|
systemPrompt: `You are a minimal assistant.
|
|
78
76
|
Available: read, bash, status. Be concise.`,
|
|
79
|
-
|
|
80
77
|
// Use factory functions with the same cwd to ensure path resolution works correctly
|
|
81
78
|
tools: [createReadTool(cwd), createBashTool(cwd)],
|
|
82
79
|
customTools: [{ tool: statusTool }],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mariozechner/pi-coding-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -32,15 +32,15 @@
|
|
|
32
32
|
"clean": "rm -rf dist",
|
|
33
33
|
"build": "tsgo -p tsconfig.build.json && chmod +x dist/cli.js && npm run copy-assets",
|
|
34
34
|
"build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
|
|
35
|
-
"copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/",
|
|
36
|
-
"copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && cp -r docs dist/ && cp -r examples dist/",
|
|
35
|
+
"copy-assets": "mkdir -p dist/modes/interactive/theme && cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && mkdir -p dist/core/export-html/vendor && cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",
|
|
36
|
+
"copy-binary-assets": "cp package.json dist/ && cp README.md dist/ && cp CHANGELOG.md dist/ && mkdir -p dist/theme && cp src/modes/interactive/theme/*.json dist/theme/ && mkdir -p dist/export-html/vendor && cp src/core/export-html/template.html dist/export-html/ && cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && cp -r docs dist/ && cp -r examples dist/",
|
|
37
37
|
"test": "vitest --run",
|
|
38
38
|
"prepublishOnly": "npm run clean && npm run build"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@mariozechner/pi-agent-core": "^0.
|
|
42
|
-
"@mariozechner/pi-ai": "^0.
|
|
43
|
-
"@mariozechner/pi-tui": "^0.
|
|
41
|
+
"@mariozechner/pi-agent-core": "^0.31.0",
|
|
42
|
+
"@mariozechner/pi-ai": "^0.31.0",
|
|
43
|
+
"@mariozechner/pi-tui": "^0.31.0",
|
|
44
44
|
"chalk": "^5.5.0",
|
|
45
45
|
"cli-highlight": "^2.1.11",
|
|
46
46
|
"diff": "^8.0.2",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/core/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;AAG1E,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;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAE3D;AAgBD;;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;AAMD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAoD1D;AAyBD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAW1G;AAED,MAAM,WAAW,cAAc;IAC9B,mCAAmC;IACnC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,qFAAqF;IACrF,cAAc,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,WAAW,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,YAAY,EAAE,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,gBAAgB,EAAE,MAAM,GACtB,cAAc,CAyDhB;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,CA2BjB;AAMD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,cAAc,CAAC;IACzB,qDAAqD;IACrD,mBAAmB,EAAE,UAAU,EAAE,CAAC;IAClC,kEAAkE;IAClE,cAAc,EAAE,UAAU,EAAE,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,QAAQ,EAAE,kBAAkB,GAAG,qBAAqB,GAAG,IAAI,CAyCrH;AAgBD;;;;;;;;;;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,CAuF1B","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 { messageTransformer } from \"./messages.js\";\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 * Uses the native totalTokens field when available, falls back to computing from components.\n */\nexport function calculateContextTokens(usage: Usage): number {\n\treturn usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;\n}\n\n/**\n * Get usage from an assistant message if available.\n * Skips aborted and error messages as they don't have valid usage data.\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.stopReason !== \"error\" && 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 * Estimate token count for a message using chars/4 heuristic.\n * This is conservative (overestimates tokens).\n */\nexport function estimateTokens(message: AppMessage): number {\n\tlet chars = 0;\n\n\t// Handle bashExecution messages\n\tif (message.role === \"bashExecution\") {\n\t\tconst bash = message as unknown as { command: string; output: string };\n\t\tchars = bash.command.length + bash.output.length;\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle user messages\n\tif (message.role === \"user\") {\n\t\tconst content = (message as { content: string | Array<{ type: string; text?: string }> }).content;\n\t\tif (typeof content === \"string\") {\n\t\t\tchars = content.length;\n\t\t} else if (Array.isArray(content)) {\n\t\t\tfor (const block of content) {\n\t\t\t\tif (block.type === \"text\" && block.text) {\n\t\t\t\t\tchars += block.text.length;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle assistant messages\n\tif (message.role === \"assistant\") {\n\t\tconst assistant = message as AssistantMessage;\n\t\tfor (const block of assistant.content) {\n\t\t\tif (block.type === \"text\") {\n\t\t\t\tchars += block.text.length;\n\t\t\t} else if (block.type === \"thinking\") {\n\t\t\t\tchars += block.thinking.length;\n\t\t\t} else if (block.type === \"toolCall\") {\n\t\t\t\tchars += block.name.length + JSON.stringify(block.arguments).length;\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\t// Handle tool results\n\tif (message.role === \"toolResult\") {\n\t\tconst toolResult = message as { content: Array<{ type: string; text?: string }> };\n\t\tfor (const block of toolResult.content) {\n\t\t\tif (block.type === \"text\" && block.text) {\n\t\t\t\tchars += block.text.length;\n\t\t\t}\n\t\t}\n\t\treturn Math.ceil(chars / 4);\n\t}\n\n\treturn 0;\n}\n\n/**\n * Find valid cut points: indices of user, assistant, or bashExecution messages.\n * Never cut at tool results (they must follow their tool call).\n * When we cut at an assistant message with tool calls, its tool results follow it\n * and will be kept.\n * BashExecutionMessage is treated like a user message (user-initiated context).\n */\nfunction findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {\n\tconst cutPoints: number[] = [];\n\tfor (let i = startIndex; i < endIndex; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\t// user, assistant, and bashExecution are valid cut points\n\t\t\t// toolResult must stay with its preceding tool call\n\t\t\tif (role === \"user\" || role === \"assistant\" || role === \"bashExecution\") {\n\t\t\t\tcutPoints.push(i);\n\t\t\t}\n\t\t}\n\t}\n\treturn cutPoints;\n}\n\n/**\n * Find the user message (or bashExecution) that starts the turn containing the given entry index.\n * Returns -1 if no turn start found before the index.\n * BashExecutionMessage is treated like a user message for turn boundaries.\n */\nexport function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {\n\tfor (let i = entryIndex; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tconst role = entry.message.role;\n\t\t\tif (role === \"user\" || role === \"bashExecution\") {\n\t\t\t\treturn i;\n\t\t\t}\n\t\t}\n\t}\n\treturn -1;\n}\n\nexport interface CutPointResult {\n\t/** Index of first entry to keep */\n\tfirstKeptEntryIndex: number;\n\t/** Index of user message that starts the turn being split, or -1 if not splitting */\n\tturnStartIndex: number;\n\t/** Whether this cut splits a turn (cut point is not a user message) */\n\tisSplitTurn: boolean;\n}\n\n/**\n * Find the cut point in session entries that keeps approximately `keepRecentTokens`.\n *\n * Algorithm: Walk backwards from newest, accumulating estimated message sizes.\n * Stop when we've accumulated >= keepRecentTokens. Cut at that point.\n *\n * Can cut at user OR assistant messages (never tool results). When cutting at an\n * assistant message with tool calls, its tool results come after and will be kept.\n *\n * Returns CutPointResult with:\n * - firstKeptEntryIndex: the entry index to start keeping from\n * - turnStartIndex: if cutting mid-turn, the user message that started that turn\n * - isSplitTurn: whether we're cutting in the middle of a turn\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): CutPointResult {\n\tconst cutPoints = findValidCutPoints(entries, startIndex, endIndex);\n\n\tif (cutPoints.length === 0) {\n\t\treturn { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };\n\t}\n\n\t// Walk backwards from newest, accumulating estimated message sizes\n\tlet accumulatedTokens = 0;\n\tlet cutIndex = startIndex; // Default: keep everything in range\n\n\tfor (let i = endIndex - 1; i >= startIndex; i--) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type !== \"message\") continue;\n\n\t\t// Estimate this message's size\n\t\tconst messageTokens = estimateTokens(entry.message);\n\t\taccumulatedTokens += messageTokens;\n\n\t\t// Check if we've exceeded the budget\n\t\tif (accumulatedTokens >= keepRecentTokens) {\n\t\t\t// Find the closest valid cut point at or after this entry\n\t\t\tfor (let c = 0; c < cutPoints.length; c++) {\n\t\t\t\tif (cutPoints[c] >= i) {\n\t\t\t\t\tcutIndex = cutPoints[c];\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\t// Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.)\n\twhile (cutIndex > startIndex) {\n\t\tconst prevEntry = entries[cutIndex - 1];\n\t\t// Stop at compaction boundaries\n\t\tif (prevEntry.type === \"compaction\") {\n\t\t\tbreak;\n\t\t}\n\t\tif (prevEntry.type === \"message\") {\n\t\t\t// Stop if we hit any message\n\t\t\tbreak;\n\t\t}\n\t\t// Include this non-message entry (bash, settings change, etc.)\n\t\tcutIndex--;\n\t}\n\n\t// Determine if this is a split turn\n\tconst cutEntry = entries[cutIndex];\n\tconst isUserMessage = cutEntry.type === \"message\" && cutEntry.message.role === \"user\";\n\tconst turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);\n\n\treturn {\n\t\tfirstKeptEntryIndex: cutIndex,\n\t\tturnStartIndex,\n\t\tisSplitTurn: !isUserMessage && turnStartIndex !== -1,\n\t};\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\t// Transform custom messages (like bashExecution) to LLM-compatible messages\n\tconst transformedMessages = messageTransformer(currentMessages);\n\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: 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// Compaction Preparation (for hooks)\n// ============================================================================\n\nexport interface CompactionPreparation {\n\tcutPoint: CutPointResult;\n\t/** Messages that will be summarized and discarded */\n\tmessagesToSummarize: AppMessage[];\n\t/** Messages that will be kept after the summary (recent turns) */\n\tmessagesToKeep: AppMessage[];\n\ttokensBefore: number;\n\tboundaryStart: number;\n}\n\nexport function prepareCompaction(entries: SessionEntry[], settings: CompactionSettings): CompactionPreparation | null {\n\tif (entries.length > 0 && entries[entries.length - 1].type === \"compaction\") {\n\t\treturn null;\n\t}\n\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\tconst lastUsage = getLastAssistantUsage(entries);\n\tconst tokensBefore = lastUsage ? calculateContextTokens(lastUsage) : 0;\n\n\tconst cutPoint = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\tconst historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;\n\n\t// Messages to summarize (will be discarded after summary)\n\tconst messagesToSummarize: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; 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// Messages to keep (recent turns, kept after summary)\n\tconst messagesToKeep: AppMessage[] = [];\n\tfor (let i = cutPoint.firstKeptEntryIndex; i < boundaryEnd; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tmessagesToKeep.push(entry.message);\n\t\t}\n\t}\n\n\treturn { cutPoint, messagesToSummarize, messagesToKeep, tokensBefore, boundaryStart };\n}\n\n// ============================================================================\n// Main compaction function\n// ============================================================================\n\nconst TURN_PREFIX_SUMMARIZATION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION for a split turn. \nThis is the PREFIX of a turn that was too large to keep in full. The SUFFIX (recent work) is being kept.\n\nCreate a handoff summary that captures:\n- What the user originally asked for in this turn\n- Key decisions and progress made early in this turn\n- Important context needed to understand the kept suffix\n\nBe concise. Focus on information needed to understand the retained recent work.`;\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 cutResult = findCutPoint(entries, boundaryStart, boundaryEnd, settings.keepRecentTokens);\n\n\t// Extract messages for history summary (before the turn that contains the cut point)\n\tconst historyEnd = cutResult.isSplitTurn ? cutResult.turnStartIndex : cutResult.firstKeptEntryIndex;\n\tconst historyMessages: AppMessage[] = [];\n\tfor (let i = boundaryStart; i < historyEnd; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\thistoryMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Include previous summary if there was a compaction\n\tif (prevCompactionIndex >= 0) {\n\t\tconst prevCompaction = entries[prevCompactionIndex] as CompactionEntry;\n\t\thistoryMessages.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// Extract messages for turn prefix summary (if splitting a turn)\n\tconst turnPrefixMessages: AppMessage[] = [];\n\tif (cutResult.isSplitTurn) {\n\t\tfor (let i = cutResult.turnStartIndex; i < cutResult.firstKeptEntryIndex; i++) {\n\t\t\tconst entry = entries[i];\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tturnPrefixMessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Generate summaries (can be parallel if both needed) and merge into one\n\tlet summary: string;\n\n\tif (cutResult.isSplitTurn && turnPrefixMessages.length > 0) {\n\t\t// Generate both summaries in parallel\n\t\tconst [historyResult, turnPrefixResult] = await Promise.all([\n\t\t\thistoryMessages.length > 0\n\t\t\t\t? generateSummary(historyMessages, model, settings.reserveTokens, apiKey, signal, customInstructions)\n\t\t\t\t: Promise.resolve(\"No prior history.\"),\n\t\t\tgenerateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal),\n\t\t]);\n\t\t// Merge into single summary\n\t\tsummary = `${historyResult}\\n\\n---\\n\\n**Turn Context (split turn):**\\n\\n${turnPrefixResult}`;\n\t} else {\n\t\t// Just generate history summary\n\t\tsummary = await generateSummary(\n\t\t\thistoryMessages,\n\t\t\tmodel,\n\t\t\tsettings.reserveTokens,\n\t\t\tapiKey,\n\t\t\tsignal,\n\t\t\tcustomInstructions,\n\t\t);\n\t}\n\n\treturn {\n\t\ttype: \"compaction\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tsummary,\n\t\tfirstKeptEntryIndex: cutResult.firstKeptEntryIndex,\n\t\ttokensBefore,\n\t};\n}\n\n/**\n * Generate a summary for a turn prefix (when splitting a turn).\n */\nasync function generateTurnPrefixSummary(\n\tmessages: AppMessage[],\n\tmodel: Model<any>,\n\treserveTokens: number,\n\tapiKey: string,\n\tsignal?: AbortSignal,\n): Promise<string> {\n\tconst maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix\n\n\tconst transformedMessages = messageTransformer(messages);\n\tconst summarizationMessages = [\n\t\t...transformedMessages,\n\t\t{\n\t\t\trole: \"user\" as const,\n\t\t\tcontent: [{ type: \"text\" as const, text: TURN_PREFIX_SUMMARIZATION_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\treturn 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"]}
|