@mariozechner/pi-coding-agent 0.16.0 → 0.18.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 +38 -0
- package/README.md +58 -1
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +30 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +181 -21
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction.d.ts +30 -5
- package/dist/core/compaction.d.ts.map +1 -1
- package/dist/core/compaction.js +194 -61
- package/dist/core/compaction.js.map +1 -1
- package/dist/core/hooks/index.d.ts +5 -0
- package/dist/core/hooks/index.d.ts.map +1 -0
- package/dist/core/hooks/index.js +4 -0
- package/dist/core/hooks/index.js.map +1 -0
- package/dist/core/hooks/loader.d.ts +56 -0
- package/dist/core/hooks/loader.d.ts.map +1 -0
- package/dist/core/hooks/loader.js +158 -0
- package/dist/core/hooks/loader.js.map +1 -0
- package/dist/core/hooks/runner.d.ts +69 -0
- package/dist/core/hooks/runner.d.ts.map +1 -0
- package/dist/core/hooks/runner.js +203 -0
- package/dist/core/hooks/runner.js.map +1 -0
- package/dist/core/hooks/tool-wrapper.d.ts +16 -0
- package/dist/core/hooks/tool-wrapper.d.ts.map +1 -0
- package/dist/core/hooks/tool-wrapper.js +71 -0
- package/dist/core/hooks/tool-wrapper.js.map +1 -0
- package/dist/core/hooks/types.d.ts +220 -0
- package/dist/core/hooks/types.d.ts.map +1 -0
- package/dist/core/hooks/types.js +8 -0
- package/dist/core/hooks/types.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/session-manager.d.ts +10 -3
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +78 -28
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +6 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +14 -0
- 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 +5 -3
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/truncate.d.ts +6 -2
- package/dist/core/tools/truncate.d.ts.map +1 -1
- package/dist/core/tools/truncate.js +11 -1
- package/dist/core/tools/truncate.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +23 -12
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +1 -0
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +17 -6
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/hook-input.d.ts +12 -0
- package/dist/modes/interactive/components/hook-input.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-input.js +46 -0
- package/dist/modes/interactive/components/hook-input.js.map +1 -0
- package/dist/modes/interactive/components/hook-selector.d.ts +16 -0
- package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -0
- package/dist/modes/interactive/components/hook-selector.js +76 -0
- package/dist/modes/interactive/components/hook-selector.js.map +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +12 -7
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +37 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +190 -7
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +15 -0
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +118 -3
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +41 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/compaction.md +519 -0
- package/docs/hooks.md +609 -0
- package/docs/rpc.md +870 -0
- package/docs/session.md +89 -0
- package/docs/theme.md +586 -0
- package/docs/truncation.md +235 -0
- package/docs/undercompaction.md +313 -0
- package/package.json +18 -6
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook system types.
|
|
3
|
+
*
|
|
4
|
+
* Hooks are TypeScript modules that can subscribe to agent lifecycle events
|
|
5
|
+
* and interact with the user via UI primitives.
|
|
6
|
+
*/
|
|
7
|
+
import type { AppMessage, Attachment } from "@mariozechner/pi-agent-core";
|
|
8
|
+
import type { SessionEntry } from "../session-manager.js";
|
|
9
|
+
/**
|
|
10
|
+
* Result of executing a command via ctx.exec()
|
|
11
|
+
*/
|
|
12
|
+
export interface ExecResult {
|
|
13
|
+
stdout: string;
|
|
14
|
+
stderr: string;
|
|
15
|
+
code: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* UI context for hooks to request interactive UI from the harness.
|
|
19
|
+
* Each mode (interactive, RPC, print) provides its own implementation.
|
|
20
|
+
*/
|
|
21
|
+
export interface HookUIContext {
|
|
22
|
+
/**
|
|
23
|
+
* Show a selector and return the user's choice.
|
|
24
|
+
* @param title - Title to display
|
|
25
|
+
* @param options - Array of string options
|
|
26
|
+
* @returns Selected option string, or null if cancelled
|
|
27
|
+
*/
|
|
28
|
+
select(title: string, options: string[]): Promise<string | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Show a confirmation dialog.
|
|
31
|
+
* @returns true if confirmed, false if cancelled
|
|
32
|
+
*/
|
|
33
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Show a text input dialog.
|
|
36
|
+
* @returns User input, or null if cancelled
|
|
37
|
+
*/
|
|
38
|
+
input(title: string, placeholder?: string): Promise<string | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Show a notification to the user.
|
|
41
|
+
*/
|
|
42
|
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Context passed to hook event handlers.
|
|
46
|
+
*/
|
|
47
|
+
export interface HookEventContext {
|
|
48
|
+
/** Execute a command and return stdout/stderr/code */
|
|
49
|
+
exec(command: string, args: string[]): Promise<ExecResult>;
|
|
50
|
+
/** UI methods for user interaction */
|
|
51
|
+
ui: HookUIContext;
|
|
52
|
+
/** Whether UI is available (false in print mode) */
|
|
53
|
+
hasUI: boolean;
|
|
54
|
+
/** Current working directory */
|
|
55
|
+
cwd: string;
|
|
56
|
+
/** Path to session file, or null if --no-session */
|
|
57
|
+
sessionFile: string | null;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Event data for session_start event.
|
|
61
|
+
* Fired once when the coding agent starts up.
|
|
62
|
+
*/
|
|
63
|
+
export interface SessionStartEvent {
|
|
64
|
+
type: "session_start";
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Event data for session_switch event.
|
|
68
|
+
* Fired when the session changes (branch or session switch).
|
|
69
|
+
*/
|
|
70
|
+
export interface SessionSwitchEvent {
|
|
71
|
+
type: "session_switch";
|
|
72
|
+
/** New session file path */
|
|
73
|
+
newSessionFile: string;
|
|
74
|
+
/** Previous session file path */
|
|
75
|
+
previousSessionFile: string;
|
|
76
|
+
/** Reason for the switch */
|
|
77
|
+
reason: "branch" | "switch";
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Event data for agent_start event.
|
|
81
|
+
* Fired when an agent loop starts (once per user prompt).
|
|
82
|
+
*/
|
|
83
|
+
export interface AgentStartEvent {
|
|
84
|
+
type: "agent_start";
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Event data for agent_end event.
|
|
88
|
+
*/
|
|
89
|
+
export interface AgentEndEvent {
|
|
90
|
+
type: "agent_end";
|
|
91
|
+
messages: AppMessage[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Event data for turn_start event.
|
|
95
|
+
*/
|
|
96
|
+
export interface TurnStartEvent {
|
|
97
|
+
type: "turn_start";
|
|
98
|
+
turnIndex: number;
|
|
99
|
+
timestamp: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Event data for turn_end event.
|
|
103
|
+
*/
|
|
104
|
+
export interface TurnEndEvent {
|
|
105
|
+
type: "turn_end";
|
|
106
|
+
turnIndex: number;
|
|
107
|
+
message: AppMessage;
|
|
108
|
+
toolResults: AppMessage[];
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Event data for tool_call event.
|
|
112
|
+
* Fired before a tool is executed. Hooks can block execution.
|
|
113
|
+
*/
|
|
114
|
+
export interface ToolCallEvent {
|
|
115
|
+
type: "tool_call";
|
|
116
|
+
/** Tool name (e.g., "bash", "edit", "write") */
|
|
117
|
+
toolName: string;
|
|
118
|
+
/** Tool call ID */
|
|
119
|
+
toolCallId: string;
|
|
120
|
+
/** Tool input parameters */
|
|
121
|
+
input: Record<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Event data for tool_result event.
|
|
125
|
+
* Fired after a tool is executed. Hooks can modify the result.
|
|
126
|
+
*/
|
|
127
|
+
export interface ToolResultEvent {
|
|
128
|
+
type: "tool_result";
|
|
129
|
+
/** Tool name (e.g., "bash", "edit", "write") */
|
|
130
|
+
toolName: string;
|
|
131
|
+
/** Tool call ID */
|
|
132
|
+
toolCallId: string;
|
|
133
|
+
/** Tool input parameters */
|
|
134
|
+
input: Record<string, unknown>;
|
|
135
|
+
/** Tool result content (text) */
|
|
136
|
+
result: string;
|
|
137
|
+
/** Whether the tool execution was an error */
|
|
138
|
+
isError: boolean;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Event data for branch event.
|
|
142
|
+
*/
|
|
143
|
+
export interface BranchEvent {
|
|
144
|
+
type: "branch";
|
|
145
|
+
/** Index of the turn to branch from */
|
|
146
|
+
targetTurnIndex: number;
|
|
147
|
+
/** Full session history */
|
|
148
|
+
entries: SessionEntry[];
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Union of all hook event types.
|
|
152
|
+
*/
|
|
153
|
+
export type HookEvent = SessionStartEvent | SessionSwitchEvent | AgentStartEvent | AgentEndEvent | TurnStartEvent | TurnEndEvent | ToolCallEvent | ToolResultEvent | BranchEvent;
|
|
154
|
+
/**
|
|
155
|
+
* Return type for tool_call event handlers.
|
|
156
|
+
* Allows hooks to block tool execution.
|
|
157
|
+
*/
|
|
158
|
+
export interface ToolCallEventResult {
|
|
159
|
+
/** If true, block the tool from executing */
|
|
160
|
+
block?: boolean;
|
|
161
|
+
/** Reason for blocking (returned to LLM as error) */
|
|
162
|
+
reason?: string;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Return type for tool_result event handlers.
|
|
166
|
+
* Allows hooks to modify tool results.
|
|
167
|
+
*/
|
|
168
|
+
export interface ToolResultEventResult {
|
|
169
|
+
/** Modified result text (if not set, original result is used) */
|
|
170
|
+
result?: string;
|
|
171
|
+
/** Override isError flag */
|
|
172
|
+
isError?: boolean;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Return type for branch event handlers.
|
|
176
|
+
* Allows hooks to control branch behavior.
|
|
177
|
+
*/
|
|
178
|
+
export interface BranchEventResult {
|
|
179
|
+
/** If true, skip restoring the conversation (only restore code) */
|
|
180
|
+
skipConversationRestore?: boolean;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Handler function type for each event.
|
|
184
|
+
*/
|
|
185
|
+
export type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;
|
|
186
|
+
/**
|
|
187
|
+
* HookAPI passed to hook factory functions.
|
|
188
|
+
* Hooks use pi.on() to subscribe to events and pi.send() to inject messages.
|
|
189
|
+
*/
|
|
190
|
+
export interface HookAPI {
|
|
191
|
+
on(event: "session_start", handler: HookHandler<SessionStartEvent>): void;
|
|
192
|
+
on(event: "session_switch", handler: HookHandler<SessionSwitchEvent>): void;
|
|
193
|
+
on(event: "agent_start", handler: HookHandler<AgentStartEvent>): void;
|
|
194
|
+
on(event: "agent_end", handler: HookHandler<AgentEndEvent>): void;
|
|
195
|
+
on(event: "turn_start", handler: HookHandler<TurnStartEvent>): void;
|
|
196
|
+
on(event: "turn_end", handler: HookHandler<TurnEndEvent>): void;
|
|
197
|
+
on(event: "tool_call", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;
|
|
198
|
+
on(event: "tool_result", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;
|
|
199
|
+
on(event: "branch", handler: HookHandler<BranchEvent, BranchEventResult | undefined>): void;
|
|
200
|
+
/**
|
|
201
|
+
* Send a message to the agent.
|
|
202
|
+
* If the agent is streaming, the message is queued.
|
|
203
|
+
* If the agent is idle, a new agent loop is started.
|
|
204
|
+
*/
|
|
205
|
+
send(text: string, attachments?: Attachment[]): void;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Hook factory function type.
|
|
209
|
+
* Hooks export a default function that receives the HookAPI.
|
|
210
|
+
*/
|
|
211
|
+
export type HookFactory = (pi: HookAPI) => void;
|
|
212
|
+
/**
|
|
213
|
+
* Error emitted when a hook fails.
|
|
214
|
+
*/
|
|
215
|
+
export interface HookError {
|
|
216
|
+
hookPath: string;
|
|
217
|
+
event: string;
|
|
218
|
+
error: string;
|
|
219
|
+
}
|
|
220
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Hook system types.\n *\n * Hooks are TypeScript modules that can subscribe to agent lifecycle events\n * and interact with the user via UI primitives.\n */\n\nimport type { AppMessage, Attachment } from \"@mariozechner/pi-agent-core\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n// ============================================================================\n// Execution Context\n// ============================================================================\n\n/**\n * Result of executing a command via ctx.exec()\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\n/**\n * UI context for hooks to request interactive UI from the harness.\n * Each mode (interactive, RPC, print) provides its own implementation.\n */\nexport interface HookUIContext {\n\t/**\n\t * Show a selector and return the user's choice.\n\t * @param title - Title to display\n\t * @param options - Array of string options\n\t * @returns Selected option string, or null if cancelled\n\t */\n\tselect(title: string, options: string[]): Promise<string | null>;\n\n\t/**\n\t * Show a confirmation dialog.\n\t * @returns true if confirmed, false if cancelled\n\t */\n\tconfirm(title: string, message: string): Promise<boolean>;\n\n\t/**\n\t * Show a text input dialog.\n\t * @returns User input, or null if cancelled\n\t */\n\tinput(title: string, placeholder?: string): Promise<string | null>;\n\n\t/**\n\t * Show a notification to the user.\n\t */\n\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void;\n}\n\n/**\n * Context passed to hook event handlers.\n */\nexport interface HookEventContext {\n\t/** Execute a command and return stdout/stderr/code */\n\texec(command: string, args: string[]): Promise<ExecResult>;\n\t/** UI methods for user interaction */\n\tui: HookUIContext;\n\t/** Whether UI is available (false in print mode) */\n\thasUI: boolean;\n\t/** Current working directory */\n\tcwd: string;\n\t/** Path to session file, or null if --no-session */\n\tsessionFile: string | null;\n}\n\n// ============================================================================\n// Events\n// ============================================================================\n\n/**\n * Event data for session_start event.\n * Fired once when the coding agent starts up.\n */\nexport interface SessionStartEvent {\n\ttype: \"session_start\";\n}\n\n/**\n * Event data for session_switch event.\n * Fired when the session changes (branch or session switch).\n */\nexport interface SessionSwitchEvent {\n\ttype: \"session_switch\";\n\t/** New session file path */\n\tnewSessionFile: string;\n\t/** Previous session file path */\n\tpreviousSessionFile: string;\n\t/** Reason for the switch */\n\treason: \"branch\" | \"switch\";\n}\n\n/**\n * Event data for agent_start event.\n * Fired when an agent loop starts (once per user prompt).\n */\nexport interface AgentStartEvent {\n\ttype: \"agent_start\";\n}\n\n/**\n * Event data for agent_end event.\n */\nexport interface AgentEndEvent {\n\ttype: \"agent_end\";\n\tmessages: AppMessage[];\n}\n\n/**\n * Event data for turn_start event.\n */\nexport interface TurnStartEvent {\n\ttype: \"turn_start\";\n\tturnIndex: number;\n\ttimestamp: number;\n}\n\n/**\n * Event data for turn_end event.\n */\nexport interface TurnEndEvent {\n\ttype: \"turn_end\";\n\tturnIndex: number;\n\tmessage: AppMessage;\n\ttoolResults: AppMessage[];\n}\n\n/**\n * Event data for tool_call event.\n * Fired before a tool is executed. Hooks can block execution.\n */\nexport interface ToolCallEvent {\n\ttype: \"tool_call\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n}\n\n/**\n * Event data for tool_result event.\n * Fired after a tool is executed. Hooks can modify the result.\n */\nexport interface ToolResultEvent {\n\ttype: \"tool_result\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n\t/** Tool result content (text) */\n\tresult: string;\n\t/** Whether the tool execution was an error */\n\tisError: boolean;\n}\n\n/**\n * Event data for branch event.\n */\nexport interface BranchEvent {\n\ttype: \"branch\";\n\t/** Index of the turn to branch from */\n\ttargetTurnIndex: number;\n\t/** Full session history */\n\tentries: SessionEntry[];\n}\n\n/**\n * Union of all hook event types.\n */\nexport type HookEvent =\n\t| SessionStartEvent\n\t| SessionSwitchEvent\n\t| AgentStartEvent\n\t| AgentEndEvent\n\t| TurnStartEvent\n\t| TurnEndEvent\n\t| ToolCallEvent\n\t| ToolResultEvent\n\t| BranchEvent;\n\n// ============================================================================\n// Event Results\n// ============================================================================\n\n/**\n * Return type for tool_call event handlers.\n * Allows hooks to block tool execution.\n */\nexport interface ToolCallEventResult {\n\t/** If true, block the tool from executing */\n\tblock?: boolean;\n\t/** Reason for blocking (returned to LLM as error) */\n\treason?: string;\n}\n\n/**\n * Return type for tool_result event handlers.\n * Allows hooks to modify tool results.\n */\nexport interface ToolResultEventResult {\n\t/** Modified result text (if not set, original result is used) */\n\tresult?: string;\n\t/** Override isError flag */\n\tisError?: boolean;\n}\n\n/**\n * Return type for branch event handlers.\n * Allows hooks to control branch behavior.\n */\nexport interface BranchEventResult {\n\t/** If true, skip restoring the conversation (only restore code) */\n\tskipConversationRestore?: boolean;\n}\n\n// ============================================================================\n// Hook API\n// ============================================================================\n\n/**\n * Handler function type for each event.\n */\nexport type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;\n\n/**\n * HookAPI passed to hook factory functions.\n * Hooks use pi.on() to subscribe to events and pi.send() to inject messages.\n */\nexport interface HookAPI {\n\ton(event: \"session_start\", handler: HookHandler<SessionStartEvent>): void;\n\ton(event: \"session_switch\", handler: HookHandler<SessionSwitchEvent>): void;\n\ton(event: \"agent_start\", handler: HookHandler<AgentStartEvent>): void;\n\ton(event: \"agent_end\", handler: HookHandler<AgentEndEvent>): void;\n\ton(event: \"turn_start\", handler: HookHandler<TurnStartEvent>): void;\n\ton(event: \"turn_end\", handler: HookHandler<TurnEndEvent>): void;\n\ton(event: \"tool_call\", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;\n\ton(event: \"tool_result\", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;\n\ton(event: \"branch\", handler: HookHandler<BranchEvent, BranchEventResult | undefined>): void;\n\n\t/**\n\t * Send a message to the agent.\n\t * If the agent is streaming, the message is queued.\n\t * If the agent is idle, a new agent loop is started.\n\t */\n\tsend(text: string, attachments?: Attachment[]): void;\n}\n\n/**\n * Hook factory function type.\n * Hooks export a default function that receives the HookAPI.\n */\nexport type HookFactory = (pi: HookAPI) => void;\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Error emitted when a hook fails.\n */\nexport interface HookError {\n\thookPath: string;\n\tevent: string;\n\terror: string;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/core/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * Hook system types.\n *\n * Hooks are TypeScript modules that can subscribe to agent lifecycle events\n * and interact with the user via UI primitives.\n */\n\nimport type { AppMessage, Attachment } from \"@mariozechner/pi-agent-core\";\nimport type { SessionEntry } from \"../session-manager.js\";\n\n// ============================================================================\n// Execution Context\n// ============================================================================\n\n/**\n * Result of executing a command via ctx.exec()\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n}\n\n/**\n * UI context for hooks to request interactive UI from the harness.\n * Each mode (interactive, RPC, print) provides its own implementation.\n */\nexport interface HookUIContext {\n\t/**\n\t * Show a selector and return the user's choice.\n\t * @param title - Title to display\n\t * @param options - Array of string options\n\t * @returns Selected option string, or null if cancelled\n\t */\n\tselect(title: string, options: string[]): Promise<string | null>;\n\n\t/**\n\t * Show a confirmation dialog.\n\t * @returns true if confirmed, false if cancelled\n\t */\n\tconfirm(title: string, message: string): Promise<boolean>;\n\n\t/**\n\t * Show a text input dialog.\n\t * @returns User input, or null if cancelled\n\t */\n\tinput(title: string, placeholder?: string): Promise<string | null>;\n\n\t/**\n\t * Show a notification to the user.\n\t */\n\tnotify(message: string, type?: \"info\" | \"warning\" | \"error\"): void;\n}\n\n/**\n * Context passed to hook event handlers.\n */\nexport interface HookEventContext {\n\t/** Execute a command and return stdout/stderr/code */\n\texec(command: string, args: string[]): Promise<ExecResult>;\n\t/** UI methods for user interaction */\n\tui: HookUIContext;\n\t/** Whether UI is available (false in print mode) */\n\thasUI: boolean;\n\t/** Current working directory */\n\tcwd: string;\n\t/** Path to session file, or null if --no-session */\n\tsessionFile: string | null;\n}\n\n// ============================================================================\n// Events\n// ============================================================================\n\n/**\n * Event data for session_start event.\n * Fired once when the coding agent starts up.\n */\nexport interface SessionStartEvent {\n\ttype: \"session_start\";\n}\n\n/**\n * Event data for session_switch event.\n * Fired when the session changes (branch or session switch).\n */\nexport interface SessionSwitchEvent {\n\ttype: \"session_switch\";\n\t/** New session file path */\n\tnewSessionFile: string;\n\t/** Previous session file path */\n\tpreviousSessionFile: string;\n\t/** Reason for the switch */\n\treason: \"branch\" | \"switch\";\n}\n\n/**\n * Event data for agent_start event.\n * Fired when an agent loop starts (once per user prompt).\n */\nexport interface AgentStartEvent {\n\ttype: \"agent_start\";\n}\n\n/**\n * Event data for agent_end event.\n */\nexport interface AgentEndEvent {\n\ttype: \"agent_end\";\n\tmessages: AppMessage[];\n}\n\n/**\n * Event data for turn_start event.\n */\nexport interface TurnStartEvent {\n\ttype: \"turn_start\";\n\tturnIndex: number;\n\ttimestamp: number;\n}\n\n/**\n * Event data for turn_end event.\n */\nexport interface TurnEndEvent {\n\ttype: \"turn_end\";\n\tturnIndex: number;\n\tmessage: AppMessage;\n\ttoolResults: AppMessage[];\n}\n\n/**\n * Event data for tool_call event.\n * Fired before a tool is executed. Hooks can block execution.\n */\nexport interface ToolCallEvent {\n\ttype: \"tool_call\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n}\n\n/**\n * Event data for tool_result event.\n * Fired after a tool is executed. Hooks can modify the result.\n */\nexport interface ToolResultEvent {\n\ttype: \"tool_result\";\n\t/** Tool name (e.g., \"bash\", \"edit\", \"write\") */\n\ttoolName: string;\n\t/** Tool call ID */\n\ttoolCallId: string;\n\t/** Tool input parameters */\n\tinput: Record<string, unknown>;\n\t/** Tool result content (text) */\n\tresult: string;\n\t/** Whether the tool execution was an error */\n\tisError: boolean;\n}\n\n/**\n * Event data for branch event.\n */\nexport interface BranchEvent {\n\ttype: \"branch\";\n\t/** Index of the turn to branch from */\n\ttargetTurnIndex: number;\n\t/** Full session history */\n\tentries: SessionEntry[];\n}\n\n/**\n * Union of all hook event types.\n */\nexport type HookEvent =\n\t| SessionStartEvent\n\t| SessionSwitchEvent\n\t| AgentStartEvent\n\t| AgentEndEvent\n\t| TurnStartEvent\n\t| TurnEndEvent\n\t| ToolCallEvent\n\t| ToolResultEvent\n\t| BranchEvent;\n\n// ============================================================================\n// Event Results\n// ============================================================================\n\n/**\n * Return type for tool_call event handlers.\n * Allows hooks to block tool execution.\n */\nexport interface ToolCallEventResult {\n\t/** If true, block the tool from executing */\n\tblock?: boolean;\n\t/** Reason for blocking (returned to LLM as error) */\n\treason?: string;\n}\n\n/**\n * Return type for tool_result event handlers.\n * Allows hooks to modify tool results.\n */\nexport interface ToolResultEventResult {\n\t/** Modified result text (if not set, original result is used) */\n\tresult?: string;\n\t/** Override isError flag */\n\tisError?: boolean;\n}\n\n/**\n * Return type for branch event handlers.\n * Allows hooks to control branch behavior.\n */\nexport interface BranchEventResult {\n\t/** If true, skip restoring the conversation (only restore code) */\n\tskipConversationRestore?: boolean;\n}\n\n// ============================================================================\n// Hook API\n// ============================================================================\n\n/**\n * Handler function type for each event.\n */\nexport type HookHandler<E, R = void> = (event: E, ctx: HookEventContext) => Promise<R>;\n\n/**\n * HookAPI passed to hook factory functions.\n * Hooks use pi.on() to subscribe to events and pi.send() to inject messages.\n */\nexport interface HookAPI {\n\ton(event: \"session_start\", handler: HookHandler<SessionStartEvent>): void;\n\ton(event: \"session_switch\", handler: HookHandler<SessionSwitchEvent>): void;\n\ton(event: \"agent_start\", handler: HookHandler<AgentStartEvent>): void;\n\ton(event: \"agent_end\", handler: HookHandler<AgentEndEvent>): void;\n\ton(event: \"turn_start\", handler: HookHandler<TurnStartEvent>): void;\n\ton(event: \"turn_end\", handler: HookHandler<TurnEndEvent>): void;\n\ton(event: \"tool_call\", handler: HookHandler<ToolCallEvent, ToolCallEventResult | undefined>): void;\n\ton(event: \"tool_result\", handler: HookHandler<ToolResultEvent, ToolResultEventResult | undefined>): void;\n\ton(event: \"branch\", handler: HookHandler<BranchEvent, BranchEventResult | undefined>): void;\n\n\t/**\n\t * Send a message to the agent.\n\t * If the agent is streaming, the message is queued.\n\t * If the agent is idle, a new agent loop is started.\n\t */\n\tsend(text: string, attachments?: Attachment[]): void;\n}\n\n/**\n * Hook factory function type.\n * Hooks export a default function that receives the HookAPI.\n */\nexport type HookFactory = (pi: HookAPI) => void;\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Error emitted when a hook fails.\n */\nexport interface HookError {\n\thookPath: string;\n\tevent: string;\n\terror: string;\n}\n"]}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AgentSession, type AgentSessionConfig, type AgentSessionEvent, type AgentSessionEventListener, type CompactionResult, type ModelCycleResult, type PromptOptions, type SessionStats, } from "./agent-session.js";
|
|
5
5
|
export { type BashExecutorOptions, type BashResult, executeBash } from "./bash-executor.js";
|
|
6
|
+
export { type HookAPI, type HookError, type HookEvent, type HookEventContext, type HookFactory, HookRunner, type HookUIContext, loadHooks, } from "./hooks/index.js";
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC","sourcesContent":["/**\n * Core modules shared between all run modes.\n */\n\nexport {\n\tAgentSession,\n\ttype AgentSessionConfig,\n\ttype AgentSessionEvent,\n\ttype AgentSessionEventListener,\n\ttype CompactionResult,\n\ttype ModelCycleResult,\n\ttype PromptOptions,\n\ttype SessionStats,\n} from \"./agent-session.js\";\nexport { type BashExecutorOptions, type BashResult, executeBash } from \"./bash-executor.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,YAAY,EACZ,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,GACjB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,KAAK,mBAAmB,EAAE,KAAK,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC5F,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,UAAU,EACV,KAAK,aAAa,EAClB,SAAS,GACT,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * Core modules shared between all run modes.\n */\n\nexport {\n\tAgentSession,\n\ttype AgentSessionConfig,\n\ttype AgentSessionEvent,\n\ttype AgentSessionEventListener,\n\ttype CompactionResult,\n\ttype ModelCycleResult,\n\ttype PromptOptions,\n\ttype SessionStats,\n} from \"./agent-session.js\";\nexport { type BashExecutorOptions, type BashResult, executeBash } from \"./bash-executor.js\";\nexport {\n\ttype HookAPI,\n\ttype HookError,\n\ttype HookEvent,\n\ttype HookEventContext,\n\ttype HookFactory,\n\tHookRunner,\n\ttype HookUIContext,\n\tloadHooks,\n} from \"./hooks/index.js\";\n"]}
|
package/dist/core/index.js
CHANGED
package/dist/core/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,YAAY,GAQZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAA6C,WAAW,EAAE,MAAM,oBAAoB,CAAC","sourcesContent":["/**\n * Core modules shared between all run modes.\n */\n\nexport {\n\tAgentSession,\n\ttype AgentSessionConfig,\n\ttype AgentSessionEvent,\n\ttype AgentSessionEventListener,\n\ttype CompactionResult,\n\ttype ModelCycleResult,\n\ttype PromptOptions,\n\ttype SessionStats,\n} from \"./agent-session.js\";\nexport { type BashExecutorOptions, type BashResult, executeBash } from \"./bash-executor.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACN,YAAY,GAQZ,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAA6C,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAC5F,OAAO,EAMN,UAAU,EAEV,SAAS,GACT,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * Core modules shared between all run modes.\n */\n\nexport {\n\tAgentSession,\n\ttype AgentSessionConfig,\n\ttype AgentSessionEvent,\n\ttype AgentSessionEventListener,\n\ttype CompactionResult,\n\ttype ModelCycleResult,\n\ttype PromptOptions,\n\ttype SessionStats,\n} from \"./agent-session.js\";\nexport { type BashExecutorOptions, type BashResult, executeBash } from \"./bash-executor.js\";\nexport {\n\ttype HookAPI,\n\ttype HookError,\n\ttype HookEvent,\n\ttype HookEventContext,\n\ttype HookFactory,\n\tHookRunner,\n\ttype HookUIContext,\n\tloadHooks,\n} from \"./hooks/index.js\";\n"]}
|
|
@@ -71,13 +71,14 @@ export declare class SessionManager {
|
|
|
71
71
|
private sessionDir;
|
|
72
72
|
private enabled;
|
|
73
73
|
private sessionInitialized;
|
|
74
|
-
private
|
|
74
|
+
private pendingEntries;
|
|
75
|
+
private inMemoryEntries;
|
|
75
76
|
constructor(continueSession?: boolean, customSessionPath?: string);
|
|
76
77
|
/** Disable session saving (for --no-session mode) */
|
|
77
78
|
disable(): void;
|
|
78
79
|
private getSessionDirectory;
|
|
79
80
|
private initNewSession;
|
|
80
|
-
/** Reset to a fresh session. Clears pending
|
|
81
|
+
/** Reset to a fresh session. Clears pending entries and starts a new session file. */
|
|
81
82
|
reset(): void;
|
|
82
83
|
private findMostRecentlyModifiedSession;
|
|
83
84
|
private loadSessionId;
|
|
@@ -108,7 +109,13 @@ export declare class SessionManager {
|
|
|
108
109
|
getSessionId(): string;
|
|
109
110
|
getSessionFile(): string;
|
|
110
111
|
/**
|
|
111
|
-
* Load
|
|
112
|
+
* Load entries directly from the session file (internal helper).
|
|
113
|
+
*/
|
|
114
|
+
private loadEntriesFromFile;
|
|
115
|
+
/**
|
|
116
|
+
* Load all entries from the session file or in-memory store.
|
|
117
|
+
* When file persistence is enabled, reads from file (source of truth for resumed sessions).
|
|
118
|
+
* When disabled (--no-session), returns in-memory entries.
|
|
112
119
|
*/
|
|
113
120
|
loadEntries(): SessionEntry[];
|
|
114
121
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState, AppMessage } from \"@mariozechner/pi-agent-core\";\nimport { randomBytes } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nfunction uuidv4(): string {\n\tconst bytes = randomBytes(16);\n\tbytes[6] = (bytes[6] & 0x0f) | 0x40;\n\tbytes[8] = (bytes[8] & 0x3f) | 0x80;\n\tconst hex = bytes.toString(\"hex\");\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n// ============================================================================\n// Session entry types\n// ============================================================================\n\nexport interface SessionHeader {\n\ttype: \"session\";\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tprovider: string;\n\tmodelId: string;\n\tthinkingLevel: string;\n\tbranchedFrom?: string;\n}\n\nexport interface SessionMessageEntry {\n\ttype: \"message\";\n\ttimestamp: string;\n\tmessage: AppMessage;\n}\n\nexport interface ThinkingLevelChangeEntry {\n\ttype: \"thinking_level_change\";\n\ttimestamp: string;\n\tthinkingLevel: string;\n}\n\nexport interface ModelChangeEntry {\n\ttype: \"model_change\";\n\ttimestamp: string;\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport interface CompactionEntry {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\tfirstKeptEntryIndex: number; // Index into session entries where we start keeping\n\ttokensBefore: number;\n}\n\n/** Union of all session entry types */\nexport type SessionEntry =\n\t| SessionHeader\n\t| SessionMessageEntry\n\t| ThinkingLevelChangeEntry\n\t| ModelChangeEntry\n\t| CompactionEntry;\n\n// ============================================================================\n// Session loading with compaction support\n// ============================================================================\n\nexport interface LoadedSession {\n\tmessages: AppMessage[];\n\tthinkingLevel: string;\n\tmodel: { provider: string; modelId: string } | null;\n}\n\nexport const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n`;\n\nexport const SUMMARY_SUFFIX = `\n</summary>`;\n\n/**\n * Create a user message containing the summary with the standard prefix.\n */\nexport function createSummaryMessage(summary: string): AppMessage {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/**\n * Parse session file content into entries.\n */\nexport function parseSessionEntries(content: string): SessionEntry[] {\n\tconst entries: SessionEntry[] = [];\n\tconst lines = content.trim().split(\"\\n\");\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\tentries.push(entry);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Load session from entries, handling compaction events.\n *\n * Algorithm:\n * 1. Find latest compaction event (if any)\n * 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)\n * 3. Prepend summary as user message\n */\n/**\n * Get the latest compaction entry from session entries, if any.\n */\nexport function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\treturn entries[i] as CompactionEntry;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {\n\t// Find model and thinking level (always scan all entries)\n\tlet thinkingLevel = \"off\";\n\tlet model: { provider: string; modelId: string } | null = null;\n\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"session\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t} else if (entry.type === \"thinking_level_change\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t}\n\t}\n\n\t// Find latest compaction event\n\tlet latestCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tlatestCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// No compaction: return all messages\n\tif (latestCompactionIndex === -1) {\n\t\tconst messages: AppMessage[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t\treturn { messages, thinkingLevel, model };\n\t}\n\n\tconst compactionEvent = entries[latestCompactionIndex] as CompactionEntry;\n\n\t// Extract messages from firstKeptEntryIndex to end (skipping compaction entries)\n\tconst keptMessages: AppMessage[] = [];\n\tfor (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tkeptMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Build final messages: summary + kept messages\n\tconst summaryMessage = createSummaryMessage(compactionEvent.summary);\n\tconst messages = [summaryMessage, ...keptMessages];\n\n\treturn { messages, thinkingLevel, model };\n}\n\nexport class SessionManager {\n\tprivate sessionId!: string;\n\tprivate sessionFile!: string;\n\tprivate sessionDir: string;\n\tprivate enabled: boolean = true;\n\tprivate sessionInitialized: boolean = false;\n\tprivate pendingMessages: any[] = [];\n\n\tconstructor(continueSession: boolean = false, customSessionPath?: string) {\n\t\tthis.sessionDir = this.getSessionDirectory();\n\n\t\tif (customSessionPath) {\n\t\t\t// Use custom session file path\n\t\t\tthis.sessionFile = resolve(customSessionPath);\n\t\t\tthis.loadSessionId();\n\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\tthis.sessionInitialized = existsSync(this.sessionFile);\n\t\t} else if (continueSession) {\n\t\t\tconst mostRecent = this.findMostRecentlyModifiedSession();\n\t\t\tif (mostRecent) {\n\t\t\t\tthis.sessionFile = mostRecent;\n\t\t\t\tthis.loadSessionId();\n\t\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\t\tthis.sessionInitialized = true;\n\t\t\t} else {\n\t\t\t\tthis.initNewSession();\n\t\t\t}\n\t\t} else {\n\t\t\tthis.initNewSession();\n\t\t}\n\t}\n\n\t/** Disable session saving (for --no-session mode) */\n\tdisable() {\n\t\tthis.enabled = false;\n\t}\n\n\tprivate getSessionDirectory(): string {\n\t\tconst cwd = process.cwd();\n\t\t// Replace all path separators and colons (for Windows drive letters) with dashes\n\t\tconst safePath = \"--\" + cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\") + \"--\";\n\n\t\tconst configDir = getAgentDir();\n\t\tconst sessionDir = join(configDir, \"sessions\", safePath);\n\t\tif (!existsSync(sessionDir)) {\n\t\t\tmkdirSync(sessionDir, { recursive: true });\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tprivate initNewSession(): void {\n\t\tthis.sessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tthis.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);\n\t}\n\n\t/** Reset to a fresh session. Clears pending messages and starts a new session file. */\n\treset(): void {\n\t\tthis.pendingMessages = [];\n\t\tthis.sessionInitialized = false;\n\t\tthis.initNewSession();\n\t}\n\n\tprivate findMostRecentlyModifiedSession(): string | null {\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => ({\n\t\t\t\t\tname: f,\n\t\t\t\t\tpath: join(this.sessionDir, f),\n\t\t\t\t\tmtime: statSync(join(this.sessionDir, f)).mtime,\n\t\t\t\t}))\n\t\t\t\t.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\n\t\t\treturn files[0]?.path || null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate loadSessionId(): void {\n\t\tif (!existsSync(this.sessionFile)) return;\n\n\t\tconst lines = readFileSync(this.sessionFile, \"utf8\").trim().split(\"\\n\");\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"session\") {\n\t\t\t\t\tthis.sessionId = entry.id;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\t\tthis.sessionId = uuidv4();\n\t}\n\n\tstartSession(state: AgentState): void {\n\t\tif (!this.enabled || this.sessionInitialized) return;\n\t\tthis.sessionInitialized = true;\n\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: this.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t};\n\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\n\t\t// Write any queued messages\n\t\tfor (const msg of this.pendingMessages) {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(msg) + \"\\n\");\n\t\t}\n\t\tthis.pendingMessages = [];\n\t}\n\n\tsaveMessage(message: any): void {\n\t\tif (!this.enabled) return;\n\t\tconst entry: SessionMessageEntry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmessage,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingMessages.push(entry);\n\t\t} else {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tif (!this.enabled) return;\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tthinkingLevel,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingMessages.push(entry);\n\t\t} else {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tif (!this.enabled) return;\n\t\tconst entry: ModelChangeEntry = {\n\t\t\ttype: \"model_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tprovider,\n\t\t\tmodelId,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingMessages.push(entry);\n\t\t} else {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\tif (!this.enabled) return;\n\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t}\n\n\t/**\n\t * Load session data (messages, model, thinking level) with compaction support.\n\t */\n\tloadSession(): LoadedSession {\n\t\tconst entries = this.loadEntries();\n\t\treturn loadSessionFromEntries(entries);\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().messages instead\n\t */\n\tloadMessages(): AppMessage[] {\n\t\treturn this.loadSession().messages;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().thinkingLevel instead\n\t */\n\tloadThinkingLevel(): string {\n\t\treturn this.loadSession().thinkingLevel;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().model instead\n\t */\n\tloadModel(): { provider: string; modelId: string } | null {\n\t\treturn this.loadSession().model;\n\t}\n\n\tgetSessionId(): string {\n\t\treturn this.sessionId;\n\t}\n\n\tgetSessionFile(): string {\n\t\treturn this.sessionFile;\n\t}\n\n\t/**\n\t * Load all entries from the session file.\n\t */\n\tloadEntries(): SessionEntry[] {\n\t\tif (!existsSync(this.sessionFile)) return [];\n\n\t\tconst content = readFileSync(this.sessionFile, \"utf8\");\n\t\tconst entries: SessionEntry[] = [];\n\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\tfor (const line of lines) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\t\tentries.push(entry);\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/**\n\t * Load all sessions for the current directory with metadata\n\t */\n\tloadAllSessions(): Array<{\n\t\tpath: string;\n\t\tid: string;\n\t\tcreated: Date;\n\t\tmodified: Date;\n\t\tmessageCount: number;\n\t\tfirstMessage: string;\n\t\tallMessagesText: string;\n\t}> {\n\t\tconst sessions: Array<{\n\t\t\tpath: string;\n\t\t\tid: string;\n\t\t\tcreated: Date;\n\t\t\tmodified: Date;\n\t\t\tmessageCount: number;\n\t\t\tfirstMessage: string;\n\t\t\tallMessagesText: string;\n\t\t}> = [];\n\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => join(this.sessionDir, f));\n\n\t\t\tfor (const file of files) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(file);\n\t\t\t\t\tconst content = readFileSync(file, \"utf8\");\n\t\t\t\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\t\t\t\tlet sessionId = \"\";\n\t\t\t\t\tlet created = stats.birthtime;\n\t\t\t\t\tlet messageCount = 0;\n\t\t\t\t\tlet firstMessage = \"\";\n\t\t\t\t\tconst allMessages: string[] = [];\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entry = JSON.parse(line);\n\n\t\t\t\t\t\t\t// Extract session ID from first session entry\n\t\t\t\t\t\t\tif (entry.type === \"session\" && !sessionId) {\n\t\t\t\t\t\t\t\tsessionId = entry.id;\n\t\t\t\t\t\t\t\tcreated = new Date(entry.timestamp);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Count messages and collect all text\n\t\t\t\t\t\t\tif (entry.type === \"message\") {\n\t\t\t\t\t\t\t\tmessageCount++;\n\n\t\t\t\t\t\t\t\t// Extract text from user and assistant messages\n\t\t\t\t\t\t\t\tif (entry.message.role === \"user\" || entry.message.role === \"assistant\") {\n\t\t\t\t\t\t\t\t\tconst textContent = entry.message.content\n\t\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t\t.join(\" \");\n\n\t\t\t\t\t\t\t\t\tif (textContent) {\n\t\t\t\t\t\t\t\t\t\tallMessages.push(textContent);\n\n\t\t\t\t\t\t\t\t\t\t// Get first user message for display\n\t\t\t\t\t\t\t\t\t\tif (!firstMessage && entry.message.role === \"user\") {\n\t\t\t\t\t\t\t\t\t\t\tfirstMessage = textContent;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip malformed lines\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tsessions.push({\n\t\t\t\t\t\tpath: file,\n\t\t\t\t\t\tid: sessionId || \"unknown\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodified: stats.mtime,\n\t\t\t\t\t\tmessageCount,\n\t\t\t\t\t\tfirstMessage: firstMessage || \"(no messages)\",\n\t\t\t\t\t\tallMessagesText: allMessages.join(\" \"),\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Skip files that can't be read\n\t\t\t\t\tconsole.error(`Failed to read session file ${file}:`, error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sort by modified date (most recent first)\n\t\t\tsessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to load sessions:\", error);\n\t\t}\n\n\t\treturn sessions;\n\t}\n\n\t/**\n\t * Set the session file to an existing session\n\t */\n\tsetSessionFile(path: string): void {\n\t\tthis.sessionFile = path;\n\t\tthis.loadSessionId();\n\t\t// Mark as initialized since we're loading an existing session\n\t\tthis.sessionInitialized = existsSync(path);\n\t}\n\n\t/**\n\t * Check if we should initialize the session based on message history.\n\t * Session is initialized when we have at least 1 user message and 1 assistant message.\n\t */\n\tshouldInitializeSession(messages: any[]): boolean {\n\t\tif (this.sessionInitialized) return false;\n\n\t\tconst userMessages = messages.filter((m) => m.role === \"user\");\n\t\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\");\n\n\t\treturn userMessages.length >= 1 && assistantMessages.length >= 1;\n\t}\n\n\t/**\n\t * Create a branched session from a specific message index.\n\t * If branchFromIndex is -1, creates an empty session.\n\t * Returns the new session file path.\n\t */\n\tcreateBranchedSession(state: any, branchFromIndex: number): string {\n\t\t// Create a new session ID for the branch\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Write session header\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: newSessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tbranchedFrom: this.sessionFile,\n\t\t};\n\t\tappendFileSync(newSessionFile, JSON.stringify(entry) + \"\\n\");\n\n\t\t// Write messages up to and including the branch point (if >= 0)\n\t\tif (branchFromIndex >= 0) {\n\t\t\tconst messagesToWrite = state.messages.slice(0, branchFromIndex + 1);\n\t\t\tfor (const message of messagesToWrite) {\n\t\t\t\tconst messageEntry: SessionMessageEntry = {\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tmessage,\n\t\t\t\t};\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(messageEntry) + \"\\n\");\n\t\t\t}\n\t\t}\n\n\t\treturn newSessionFile;\n\t}\n\n\t/**\n\t * Create a branched session from session entries up to (but not including) a specific entry index.\n\t * This preserves compaction events and all entry types.\n\t * Returns the new session file path.\n\t */\n\tcreateBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string {\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Copy all entries up to (but not including) the branch point\n\t\tfor (let i = 0; i < branchBeforeIndex; i++) {\n\t\t\tconst entry = entries[i];\n\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\t// Rewrite session header with new ID and branchedFrom\n\t\t\t\tconst newHeader: SessionHeader = {\n\t\t\t\t\t...entry,\n\t\t\t\t\tid: newSessionId,\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tbranchedFrom: this.sessionFile,\n\t\t\t\t};\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(newHeader) + \"\\n\");\n\t\t\t} else {\n\t\t\t\t// Copy other entries as-is\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\n\t\treturn newSessionFile;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"session-manager.d.ts","sourceRoot":"","sources":["../../src/core/session-manager.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentState, AppMessage } from \"@mariozechner/pi-agent-core\";\nimport { randomBytes } from \"crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from \"fs\";\nimport { join, resolve } from \"path\";\nimport { getAgentDir } from \"../config.js\";\n\nfunction uuidv4(): string {\n\tconst bytes = randomBytes(16);\n\tbytes[6] = (bytes[6] & 0x0f) | 0x40;\n\tbytes[8] = (bytes[8] & 0x3f) | 0x80;\n\tconst hex = bytes.toString(\"hex\");\n\treturn `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;\n}\n\n// ============================================================================\n// Session entry types\n// ============================================================================\n\nexport interface SessionHeader {\n\ttype: \"session\";\n\tid: string;\n\ttimestamp: string;\n\tcwd: string;\n\tprovider: string;\n\tmodelId: string;\n\tthinkingLevel: string;\n\tbranchedFrom?: string;\n}\n\nexport interface SessionMessageEntry {\n\ttype: \"message\";\n\ttimestamp: string;\n\tmessage: AppMessage;\n}\n\nexport interface ThinkingLevelChangeEntry {\n\ttype: \"thinking_level_change\";\n\ttimestamp: string;\n\tthinkingLevel: string;\n}\n\nexport interface ModelChangeEntry {\n\ttype: \"model_change\";\n\ttimestamp: string;\n\tprovider: string;\n\tmodelId: string;\n}\n\nexport interface CompactionEntry {\n\ttype: \"compaction\";\n\ttimestamp: string;\n\tsummary: string;\n\tfirstKeptEntryIndex: number; // Index into session entries where we start keeping\n\ttokensBefore: number;\n}\n\n/** Union of all session entry types */\nexport type SessionEntry =\n\t| SessionHeader\n\t| SessionMessageEntry\n\t| ThinkingLevelChangeEntry\n\t| ModelChangeEntry\n\t| CompactionEntry;\n\n// ============================================================================\n// Session loading with compaction support\n// ============================================================================\n\nexport interface LoadedSession {\n\tmessages: AppMessage[];\n\tthinkingLevel: string;\n\tmodel: { provider: string; modelId: string } | null;\n}\n\nexport const SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary:\n\n<summary>\n`;\n\nexport const SUMMARY_SUFFIX = `\n</summary>`;\n\n/**\n * Create a user message containing the summary with the standard prefix.\n */\nexport function createSummaryMessage(summary: string): AppMessage {\n\treturn {\n\t\trole: \"user\",\n\t\tcontent: SUMMARY_PREFIX + summary + SUMMARY_SUFFIX,\n\t\ttimestamp: Date.now(),\n\t};\n}\n\n/**\n * Parse session file content into entries.\n */\nexport function parseSessionEntries(content: string): SessionEntry[] {\n\tconst entries: SessionEntry[] = [];\n\tconst lines = content.trim().split(\"\\n\");\n\n\tfor (const line of lines) {\n\t\tif (!line.trim()) continue;\n\t\ttry {\n\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\tentries.push(entry);\n\t\t} catch {\n\t\t\t// Skip malformed lines\n\t\t}\n\t}\n\n\treturn entries;\n}\n\n/**\n * Load session from entries, handling compaction events.\n *\n * Algorithm:\n * 1. Find latest compaction event (if any)\n * 2. Keep all entries from firstKeptEntryIndex onwards (extracting messages)\n * 3. Prepend summary as user message\n */\n/**\n * Get the latest compaction entry from session entries, if any.\n */\nexport function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null {\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\treturn entries[i] as CompactionEntry;\n\t\t}\n\t}\n\treturn null;\n}\n\nexport function loadSessionFromEntries(entries: SessionEntry[]): LoadedSession {\n\t// Find model and thinking level (always scan all entries)\n\tlet thinkingLevel = \"off\";\n\tlet model: { provider: string; modelId: string } | null = null;\n\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"session\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t} else if (entry.type === \"thinking_level_change\") {\n\t\t\tthinkingLevel = entry.thinkingLevel;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = { provider: entry.provider, modelId: entry.modelId };\n\t\t}\n\t}\n\n\t// Find latest compaction event\n\tlet latestCompactionIndex = -1;\n\tfor (let i = entries.length - 1; i >= 0; i--) {\n\t\tif (entries[i].type === \"compaction\") {\n\t\t\tlatestCompactionIndex = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// No compaction: return all messages\n\tif (latestCompactionIndex === -1) {\n\t\tconst messages: AppMessage[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.type === \"message\") {\n\t\t\t\tmessages.push(entry.message);\n\t\t\t}\n\t\t}\n\t\treturn { messages, thinkingLevel, model };\n\t}\n\n\tconst compactionEvent = entries[latestCompactionIndex] as CompactionEntry;\n\n\t// Extract messages from firstKeptEntryIndex to end (skipping compaction entries)\n\tconst keptMessages: AppMessage[] = [];\n\tfor (let i = compactionEvent.firstKeptEntryIndex; i < entries.length; i++) {\n\t\tconst entry = entries[i];\n\t\tif (entry.type === \"message\") {\n\t\t\tkeptMessages.push(entry.message);\n\t\t}\n\t}\n\n\t// Build final messages: summary + kept messages\n\tconst messages: AppMessage[] = [];\n\tmessages.push(createSummaryMessage(compactionEvent.summary));\n\tmessages.push(...keptMessages);\n\n\treturn { messages, thinkingLevel, model };\n}\n\nexport class SessionManager {\n\tprivate sessionId!: string;\n\tprivate sessionFile!: string;\n\tprivate sessionDir: string;\n\tprivate enabled: boolean = true;\n\tprivate sessionInitialized: boolean = false;\n\tprivate pendingEntries: SessionEntry[] = [];\n\t// In-memory entries for --no-session mode (when enabled=false)\n\tprivate inMemoryEntries: SessionEntry[] = [];\n\n\tconstructor(continueSession: boolean = false, customSessionPath?: string) {\n\t\tthis.sessionDir = this.getSessionDirectory();\n\n\t\tif (customSessionPath) {\n\t\t\t// Use custom session file path\n\t\t\tthis.sessionFile = resolve(customSessionPath);\n\t\t\tthis.loadSessionId();\n\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\tthis.sessionInitialized = existsSync(this.sessionFile);\n\t\t\t// Load entries into memory\n\t\t\tif (this.sessionInitialized) {\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t}\n\t\t} else if (continueSession) {\n\t\t\tconst mostRecent = this.findMostRecentlyModifiedSession();\n\t\t\tif (mostRecent) {\n\t\t\t\tthis.sessionFile = mostRecent;\n\t\t\t\tthis.loadSessionId();\n\t\t\t\t// Mark as initialized since we're loading an existing session\n\t\t\t\tthis.sessionInitialized = true;\n\t\t\t\t// Load entries into memory\n\t\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t\t} else {\n\t\t\t\tthis.initNewSession();\n\t\t\t}\n\t\t} else {\n\t\t\tthis.initNewSession();\n\t\t}\n\t}\n\n\t/** Disable session saving (for --no-session mode) */\n\tdisable() {\n\t\tthis.enabled = false;\n\t}\n\n\tprivate getSessionDirectory(): string {\n\t\tconst cwd = process.cwd();\n\t\t// Replace all path separators and colons (for Windows drive letters) with dashes\n\t\tconst safePath = \"--\" + cwd.replace(/^[/\\\\]/, \"\").replace(/[/\\\\:]/g, \"-\") + \"--\";\n\n\t\tconst configDir = getAgentDir();\n\t\tconst sessionDir = join(configDir, \"sessions\", safePath);\n\t\tif (!existsSync(sessionDir)) {\n\t\t\tmkdirSync(sessionDir, { recursive: true });\n\t\t}\n\t\treturn sessionDir;\n\t}\n\n\tprivate initNewSession(): void {\n\t\tthis.sessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tthis.sessionFile = join(this.sessionDir, `${timestamp}_${this.sessionId}.jsonl`);\n\t}\n\n\t/** Reset to a fresh session. Clears pending entries and starts a new session file. */\n\treset(): void {\n\t\tthis.pendingEntries = [];\n\t\tthis.inMemoryEntries = [];\n\t\tthis.sessionInitialized = false;\n\t\tthis.initNewSession();\n\t}\n\n\tprivate findMostRecentlyModifiedSession(): string | null {\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => ({\n\t\t\t\t\tname: f,\n\t\t\t\t\tpath: join(this.sessionDir, f),\n\t\t\t\t\tmtime: statSync(join(this.sessionDir, f)).mtime,\n\t\t\t\t}))\n\t\t\t\t.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());\n\n\t\t\treturn files[0]?.path || null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate loadSessionId(): void {\n\t\tif (!existsSync(this.sessionFile)) return;\n\n\t\tconst lines = readFileSync(this.sessionFile, \"utf8\").trim().split(\"\\n\");\n\t\tfor (const line of lines) {\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line);\n\t\t\t\tif (entry.type === \"session\") {\n\t\t\t\t\tthis.sessionId = entry.id;\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\t\tthis.sessionId = uuidv4();\n\t}\n\n\tstartSession(state: AgentState): void {\n\t\tif (this.sessionInitialized) return;\n\t\tthis.sessionInitialized = true;\n\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: this.sessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t};\n\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\tfor (const pending of this.pendingEntries) {\n\t\t\tthis.inMemoryEntries.push(pending);\n\t\t}\n\t\tthis.pendingEntries = [];\n\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\tfor (const memEntry of this.inMemoryEntries.slice(1)) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(memEntry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveMessage(message: any): void {\n\t\tconst entry: SessionMessageEntry = {\n\t\t\ttype: \"message\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmessage,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveThinkingLevelChange(thinkingLevel: string): void {\n\t\tconst entry: ThinkingLevelChangeEntry = {\n\t\t\ttype: \"thinking_level_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tthinkingLevel,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveModelChange(provider: string, modelId: string): void {\n\t\tconst entry: ModelChangeEntry = {\n\t\t\ttype: \"model_change\",\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tprovider,\n\t\t\tmodelId,\n\t\t};\n\n\t\tif (!this.sessionInitialized) {\n\t\t\tthis.pendingEntries.push(entry);\n\t\t} else {\n\t\t\t// Always track in memory\n\t\t\tthis.inMemoryEntries.push(entry);\n\t\t\t// Write to file only if enabled\n\t\t\tif (this.enabled) {\n\t\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\t}\n\n\tsaveCompaction(entry: CompactionEntry): void {\n\t\t// Always track in memory\n\t\tthis.inMemoryEntries.push(entry);\n\t\t// Write to file only if enabled\n\t\tif (this.enabled) {\n\t\t\tappendFileSync(this.sessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t}\n\t}\n\n\t/**\n\t * Load session data (messages, model, thinking level) with compaction support.\n\t */\n\tloadSession(): LoadedSession {\n\t\tconst entries = this.loadEntries();\n\t\treturn loadSessionFromEntries(entries);\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().messages instead\n\t */\n\tloadMessages(): AppMessage[] {\n\t\treturn this.loadSession().messages;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().thinkingLevel instead\n\t */\n\tloadThinkingLevel(): string {\n\t\treturn this.loadSession().thinkingLevel;\n\t}\n\n\t/**\n\t * @deprecated Use loadSession().model instead\n\t */\n\tloadModel(): { provider: string; modelId: string } | null {\n\t\treturn this.loadSession().model;\n\t}\n\n\tgetSessionId(): string {\n\t\treturn this.sessionId;\n\t}\n\n\tgetSessionFile(): string {\n\t\treturn this.sessionFile;\n\t}\n\n\t/**\n\t * Load entries directly from the session file (internal helper).\n\t */\n\tprivate loadEntriesFromFile(): SessionEntry[] {\n\t\tif (!existsSync(this.sessionFile)) return [];\n\n\t\tconst content = readFileSync(this.sessionFile, \"utf8\");\n\t\tconst entries: SessionEntry[] = [];\n\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\tfor (const line of lines) {\n\t\t\tif (!line.trim()) continue;\n\t\t\ttry {\n\t\t\t\tconst entry = JSON.parse(line) as SessionEntry;\n\t\t\t\tentries.push(entry);\n\t\t\t} catch {\n\t\t\t\t// Skip malformed lines\n\t\t\t}\n\t\t}\n\n\t\treturn entries;\n\t}\n\n\t/**\n\t * Load all entries from the session file or in-memory store.\n\t * When file persistence is enabled, reads from file (source of truth for resumed sessions).\n\t * When disabled (--no-session), returns in-memory entries.\n\t */\n\tloadEntries(): SessionEntry[] {\n\t\t// If file persistence is enabled and file exists, read from file\n\t\tif (this.enabled && existsSync(this.sessionFile)) {\n\t\t\treturn this.loadEntriesFromFile();\n\t\t}\n\n\t\t// Otherwise return in-memory entries (for --no-session mode)\n\t\treturn [...this.inMemoryEntries];\n\t}\n\n\t/**\n\t * Load all sessions for the current directory with metadata\n\t */\n\tloadAllSessions(): Array<{\n\t\tpath: string;\n\t\tid: string;\n\t\tcreated: Date;\n\t\tmodified: Date;\n\t\tmessageCount: number;\n\t\tfirstMessage: string;\n\t\tallMessagesText: string;\n\t}> {\n\t\tconst sessions: Array<{\n\t\t\tpath: string;\n\t\t\tid: string;\n\t\t\tcreated: Date;\n\t\t\tmodified: Date;\n\t\t\tmessageCount: number;\n\t\t\tfirstMessage: string;\n\t\t\tallMessagesText: string;\n\t\t}> = [];\n\n\t\ttry {\n\t\t\tconst files = readdirSync(this.sessionDir)\n\t\t\t\t.filter((f) => f.endsWith(\".jsonl\"))\n\t\t\t\t.map((f) => join(this.sessionDir, f));\n\n\t\t\tfor (const file of files) {\n\t\t\t\ttry {\n\t\t\t\t\tconst stats = statSync(file);\n\t\t\t\t\tconst content = readFileSync(file, \"utf8\");\n\t\t\t\t\tconst lines = content.trim().split(\"\\n\");\n\n\t\t\t\t\tlet sessionId = \"\";\n\t\t\t\t\tlet created = stats.birthtime;\n\t\t\t\t\tlet messageCount = 0;\n\t\t\t\t\tlet firstMessage = \"\";\n\t\t\t\t\tconst allMessages: string[] = [];\n\n\t\t\t\t\tfor (const line of lines) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst entry = JSON.parse(line);\n\n\t\t\t\t\t\t\t// Extract session ID from first session entry\n\t\t\t\t\t\t\tif (entry.type === \"session\" && !sessionId) {\n\t\t\t\t\t\t\t\tsessionId = entry.id;\n\t\t\t\t\t\t\t\tcreated = new Date(entry.timestamp);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Count messages and collect all text\n\t\t\t\t\t\t\tif (entry.type === \"message\") {\n\t\t\t\t\t\t\t\tmessageCount++;\n\n\t\t\t\t\t\t\t\t// Extract text from user and assistant messages\n\t\t\t\t\t\t\t\tif (entry.message.role === \"user\" || entry.message.role === \"assistant\") {\n\t\t\t\t\t\t\t\t\tconst textContent = entry.message.content\n\t\t\t\t\t\t\t\t\t\t.filter((c: any) => c.type === \"text\")\n\t\t\t\t\t\t\t\t\t\t.map((c: any) => c.text)\n\t\t\t\t\t\t\t\t\t\t.join(\" \");\n\n\t\t\t\t\t\t\t\t\tif (textContent) {\n\t\t\t\t\t\t\t\t\t\tallMessages.push(textContent);\n\n\t\t\t\t\t\t\t\t\t\t// Get first user message for display\n\t\t\t\t\t\t\t\t\t\tif (!firstMessage && entry.message.role === \"user\") {\n\t\t\t\t\t\t\t\t\t\t\tfirstMessage = textContent;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Skip malformed lines\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tsessions.push({\n\t\t\t\t\t\tpath: file,\n\t\t\t\t\t\tid: sessionId || \"unknown\",\n\t\t\t\t\t\tcreated,\n\t\t\t\t\t\tmodified: stats.mtime,\n\t\t\t\t\t\tmessageCount,\n\t\t\t\t\t\tfirstMessage: firstMessage || \"(no messages)\",\n\t\t\t\t\t\tallMessagesText: allMessages.join(\" \"),\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\t// Skip files that can't be read\n\t\t\t\t\tconsole.error(`Failed to read session file ${file}:`, error);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Sort by modified date (most recent first)\n\t\t\tsessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to load sessions:\", error);\n\t\t}\n\n\t\treturn sessions;\n\t}\n\n\t/**\n\t * Set the session file to an existing session\n\t */\n\tsetSessionFile(path: string): void {\n\t\tthis.sessionFile = path;\n\t\tthis.loadSessionId();\n\t\t// Mark as initialized since we're loading an existing session\n\t\tthis.sessionInitialized = existsSync(path);\n\t\t// Load entries into memory for consistency\n\t\tif (this.sessionInitialized) {\n\t\t\tthis.inMemoryEntries = this.loadEntriesFromFile();\n\t\t} else {\n\t\t\tthis.inMemoryEntries = [];\n\t\t}\n\t\tthis.pendingEntries = [];\n\t}\n\n\t/**\n\t * Check if we should initialize the session based on message history.\n\t * Session is initialized when we have at least 1 user message and 1 assistant message.\n\t */\n\tshouldInitializeSession(messages: any[]): boolean {\n\t\tif (this.sessionInitialized) return false;\n\n\t\tconst userMessages = messages.filter((m) => m.role === \"user\");\n\t\tconst assistantMessages = messages.filter((m) => m.role === \"assistant\");\n\n\t\treturn userMessages.length >= 1 && assistantMessages.length >= 1;\n\t}\n\n\t/**\n\t * Create a branched session from a specific message index.\n\t * If branchFromIndex is -1, creates an empty session.\n\t * Returns the new session file path.\n\t */\n\tcreateBranchedSession(state: any, branchFromIndex: number): string {\n\t\t// Create a new session ID for the branch\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Write session header\n\t\tconst entry: SessionHeader = {\n\t\t\ttype: \"session\",\n\t\t\tid: newSessionId,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tcwd: process.cwd(),\n\t\t\tprovider: state.model.provider,\n\t\t\tmodelId: state.model.id,\n\t\t\tthinkingLevel: state.thinkingLevel,\n\t\t\tbranchedFrom: this.sessionFile,\n\t\t};\n\t\tappendFileSync(newSessionFile, JSON.stringify(entry) + \"\\n\");\n\n\t\t// Write messages up to and including the branch point (if >= 0)\n\t\tif (branchFromIndex >= 0) {\n\t\t\tconst messagesToWrite = state.messages.slice(0, branchFromIndex + 1);\n\t\t\tfor (const message of messagesToWrite) {\n\t\t\t\tconst messageEntry: SessionMessageEntry = {\n\t\t\t\t\ttype: \"message\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tmessage,\n\t\t\t\t};\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(messageEntry) + \"\\n\");\n\t\t\t}\n\t\t}\n\n\t\treturn newSessionFile;\n\t}\n\n\t/**\n\t * Create a branched session from session entries up to (but not including) a specific entry index.\n\t * This preserves compaction events and all entry types.\n\t * Returns the new session file path.\n\t */\n\tcreateBranchedSessionFromEntries(entries: SessionEntry[], branchBeforeIndex: number): string {\n\t\tconst newSessionId = uuidv4();\n\t\tconst timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n\t\tconst newSessionFile = join(this.sessionDir, `${timestamp}_${newSessionId}.jsonl`);\n\n\t\t// Copy all entries up to (but not including) the branch point\n\t\tfor (let i = 0; i < branchBeforeIndex; i++) {\n\t\t\tconst entry = entries[i];\n\n\t\t\tif (entry.type === \"session\") {\n\t\t\t\t// Rewrite session header with new ID and branchedFrom\n\t\t\t\tconst newHeader: SessionHeader = {\n\t\t\t\t\t...entry,\n\t\t\t\t\tid: newSessionId,\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tbranchedFrom: this.sessionFile,\n\t\t\t\t};\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(newHeader) + \"\\n\");\n\t\t\t} else {\n\t\t\t\t// Copy other entries as-is\n\t\t\t\tappendFileSync(newSessionFile, JSON.stringify(entry) + \"\\n\");\n\t\t\t}\n\t\t}\n\n\t\treturn newSessionFile;\n\t}\n}\n"]}
|