wave-agent-sdk 1.0.5 → 1.0.6

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.
Files changed (193) hide show
  1. package/dist/agent.d.ts +23 -1
  2. package/dist/agent.js +34 -4
  3. package/dist/managers/aiManager.d.ts +36 -0
  4. package/dist/managers/aiManager.js +240 -8
  5. package/dist/managers/messageManager.d.ts +1 -3
  6. package/dist/managers/messageManager.js +13 -6
  7. package/dist/managers/slashCommandManager.js +28 -0
  8. package/dist/managers/subagentManager.d.ts +0 -2
  9. package/dist/telemetry/instrumentation.d.ts +1 -1
  10. package/dist/telemetry/instrumentation.js +57 -28
  11. package/dist/tools/bashTool.js +16 -6
  12. package/dist/tools/grepTool.js +11 -3
  13. package/dist/types/index.d.ts +3 -0
  14. package/dist/types/messaging.d.ts +2 -0
  15. package/dist/utils/containerSetup.js +0 -3
  16. package/dist/utils/messageOperations.d.ts +2 -1
  17. package/dist/utils/messageOperations.js +2 -1
  18. package/dist/utils/notificationXml.js +5 -0
  19. package/dist/utils/ripgrep.d.ts +4 -4
  20. package/dist/utils/ripgrep.js +4 -27
  21. package/package.json +20 -8
  22. package/bin/rg +0 -79
  23. package/dist/managers/forkedAgentManager.d.ts +0 -50
  24. package/dist/managers/forkedAgentManager.js +0 -130
  25. package/scripts/install_ripgrep.js +0 -111
  26. package/scripts/postinstall.js +0 -38
  27. package/src/agent.ts +0 -1271
  28. package/src/constants/subagents.ts +0 -4
  29. package/src/constants/toolLimits.ts +0 -15
  30. package/src/constants/tools.ts +0 -24
  31. package/src/core/plugin.ts +0 -237
  32. package/src/core/session.ts +0 -9
  33. package/src/index.ts +0 -38
  34. package/src/managers/MemoryRuleManager.ts +0 -198
  35. package/src/managers/aiManager.ts +0 -2429
  36. package/src/managers/backgroundTaskManager.ts +0 -480
  37. package/src/managers/bangManager.ts +0 -119
  38. package/src/managers/cronManager.ts +0 -401
  39. package/src/managers/foregroundTaskManager.ts +0 -30
  40. package/src/managers/forkedAgentManager.ts +0 -216
  41. package/src/managers/hookManager.ts +0 -1089
  42. package/src/managers/liveConfigManager.ts +0 -445
  43. package/src/managers/lspManager.ts +0 -470
  44. package/src/managers/mcpManager.ts +0 -1010
  45. package/src/managers/messageManager.ts +0 -1184
  46. package/src/managers/messageQueue.ts +0 -157
  47. package/src/managers/permissionManager.ts +0 -1140
  48. package/src/managers/planManager.ts +0 -112
  49. package/src/managers/pluginManager.ts +0 -330
  50. package/src/managers/pluginScopeManager.ts +0 -124
  51. package/src/managers/reversionManager.ts +0 -178
  52. package/src/managers/skillManager.ts +0 -578
  53. package/src/managers/slashCommandManager.ts +0 -564
  54. package/src/managers/subagentManager.ts +0 -915
  55. package/src/managers/toolManager.ts +0 -489
  56. package/src/managers/workflowManager.ts +0 -493
  57. package/src/prompts/autoMemory.ts +0 -33
  58. package/src/prompts/autoMemoryExtraction.ts +0 -146
  59. package/src/prompts/index.ts +0 -506
  60. package/src/prompts/planModeReminders.ts +0 -126
  61. package/src/services/GitService.ts +0 -131
  62. package/src/services/MarketplaceService.ts +0 -1052
  63. package/src/services/MemoryRuleService.ts +0 -71
  64. package/src/services/aiService.ts +0 -919
  65. package/src/services/authService.ts +0 -565
  66. package/src/services/autoMemoryService.ts +0 -280
  67. package/src/services/configurationService.ts +0 -1520
  68. package/src/services/fileWatcher.ts +0 -319
  69. package/src/services/hook.ts +0 -348
  70. package/src/services/initializationService.ts +0 -357
  71. package/src/services/interactionService.ts +0 -229
  72. package/src/services/jsonlHandler.ts +0 -295
  73. package/src/services/memory.ts +0 -234
  74. package/src/services/pluginLoader.ts +0 -252
  75. package/src/services/remoteSettingsService.ts +0 -366
  76. package/src/services/reversionService.ts +0 -122
  77. package/src/services/session.ts +0 -917
  78. package/src/services/taskManager.ts +0 -321
  79. package/src/telemetry/events.ts +0 -65
  80. package/src/telemetry/instrumentation.ts +0 -499
  81. package/src/telemetry/sessionTracing.ts +0 -348
  82. package/src/tools/agentTool.ts +0 -322
  83. package/src/tools/askUserQuestion.ts +0 -160
  84. package/src/tools/bashTool.ts +0 -621
  85. package/src/tools/buildTool.ts +0 -61
  86. package/src/tools/cronCreateTool.ts +0 -161
  87. package/src/tools/cronDeleteTool.ts +0 -51
  88. package/src/tools/cronListTool.ts +0 -47
  89. package/src/tools/editTool.ts +0 -313
  90. package/src/tools/enterPlanMode.ts +0 -124
  91. package/src/tools/enterWorktreeTool.ts +0 -190
  92. package/src/tools/exitPlanMode.ts +0 -139
  93. package/src/tools/exitWorktreeTool.ts +0 -236
  94. package/src/tools/globTool.ts +0 -167
  95. package/src/tools/grepTool.ts +0 -399
  96. package/src/tools/lspTool.ts +0 -883
  97. package/src/tools/readTool.ts +0 -426
  98. package/src/tools/skillTool.ts +0 -248
  99. package/src/tools/taskManagementTools.ts +0 -574
  100. package/src/tools/taskStopTool.ts +0 -78
  101. package/src/tools/types.ts +0 -137
  102. package/src/tools/webFetchTool.ts +0 -373
  103. package/src/tools/workflowTool.ts +0 -205
  104. package/src/tools/writeTool.ts +0 -267
  105. package/src/types/agent.ts +0 -128
  106. package/src/types/auth.ts +0 -19
  107. package/src/types/commands.ts +0 -30
  108. package/src/types/config.ts +0 -43
  109. package/src/types/configuration.ts +0 -168
  110. package/src/types/core.ts +0 -105
  111. package/src/types/cron.ts +0 -13
  112. package/src/types/environment.ts +0 -106
  113. package/src/types/fileSearch.ts +0 -4
  114. package/src/types/history.ts +0 -7
  115. package/src/types/hooks.ts +0 -256
  116. package/src/types/index.ts +0 -41
  117. package/src/types/lsp.ts +0 -98
  118. package/src/types/marketplace.ts +0 -71
  119. package/src/types/mcp.ts +0 -43
  120. package/src/types/memoryRule.ts +0 -31
  121. package/src/types/messaging.ts +0 -114
  122. package/src/types/permissions.ts +0 -80
  123. package/src/types/plugins.ts +0 -39
  124. package/src/types/processes.ts +0 -99
  125. package/src/types/reversion.ts +0 -29
  126. package/src/types/session.ts +0 -12
  127. package/src/types/skills.ts +0 -108
  128. package/src/types/tasks.ts +0 -13
  129. package/src/types/telemetry.ts +0 -98
  130. package/src/types/tools.ts +0 -44
  131. package/src/types/workflow.ts +0 -6
  132. package/src/utils/abortUtils.ts +0 -118
  133. package/src/utils/atomicWrite.ts +0 -61
  134. package/src/utils/bashParser.ts +0 -899
  135. package/src/utils/cacheControlUtils.ts +0 -426
  136. package/src/utils/commandArgumentParser.ts +0 -161
  137. package/src/utils/commandPathResolver.ts +0 -88
  138. package/src/utils/configPaths.ts +0 -220
  139. package/src/utils/configValidator.ts +0 -100
  140. package/src/utils/constants.ts +0 -39
  141. package/src/utils/container.ts +0 -92
  142. package/src/utils/containerSetup.ts +0 -371
  143. package/src/utils/convertMessagesForAPI.ts +0 -326
  144. package/src/utils/cronTasks.ts +0 -128
  145. package/src/utils/cronTasksLock.ts +0 -182
  146. package/src/utils/cronToHuman.ts +0 -99
  147. package/src/utils/customCommands.ts +0 -95
  148. package/src/utils/editUtils.ts +0 -20
  149. package/src/utils/fileFormat.ts +0 -40
  150. package/src/utils/fileSearch.ts +0 -151
  151. package/src/utils/fileUtils.ts +0 -223
  152. package/src/utils/gitUtils.ts +0 -315
  153. package/src/utils/globalLogger.ts +0 -128
  154. package/src/utils/groupMessagesByApiRound.ts +0 -121
  155. package/src/utils/hookMatcher.ts +0 -176
  156. package/src/utils/markdownParser.ts +0 -317
  157. package/src/utils/mcpUtils.ts +0 -138
  158. package/src/utils/messageOperations.ts +0 -659
  159. package/src/utils/modelCapabilities.ts +0 -30
  160. package/src/utils/nameGenerator.ts +0 -99
  161. package/src/utils/notificationXml.ts +0 -57
  162. package/src/utils/openaiClient.ts +0 -251
  163. package/src/utils/parseCronExpression.ts +0 -78
  164. package/src/utils/path.ts +0 -72
  165. package/src/utils/pathEncoder.ts +0 -400
  166. package/src/utils/pathSafety.ts +0 -40
  167. package/src/utils/promptHistory.ts +0 -168
  168. package/src/utils/ripgrep.ts +0 -35
  169. package/src/utils/shellResolver.ts +0 -190
  170. package/src/utils/skillParser.ts +0 -246
  171. package/src/utils/stringUtils.ts +0 -156
  172. package/src/utils/subagentParser.ts +0 -279
  173. package/src/utils/taskReminder.ts +0 -96
  174. package/src/utils/tokenCalculation.ts +0 -43
  175. package/src/utils/tokenEstimate.ts +0 -34
  176. package/src/utils/toolResultStorage.ts +0 -117
  177. package/src/utils/worktreeSession.ts +0 -26
  178. package/src/utils/worktreeUtils.ts +0 -786
  179. package/src/workflow/budgetTracker.ts +0 -34
  180. package/src/workflow/concurrencyLimiter.ts +0 -47
  181. package/src/workflow/journal.ts +0 -95
  182. package/src/workflow/progressReporter.ts +0 -141
  183. package/src/workflow/runState.ts +0 -65
  184. package/src/workflow/scriptRuntime.ts +0 -274
  185. package/src/workflow/structuredOutput.ts +0 -123
  186. package/src/workflow/types.ts +0 -101
  187. package/src/workflow/workflowApis.ts +0 -410
  188. package/vendor/ripgrep/linux-aarch64/rg +0 -0
  189. package/vendor/ripgrep/linux-x86_64/rg +0 -0
  190. package/vendor/ripgrep/macos-aarch64/rg +0 -0
  191. package/vendor/ripgrep/macos-x86_64/rg +0 -0
  192. package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
  193. package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
@@ -1,1184 +0,0 @@
1
- import {
2
- addAssistantMessageToMessages,
3
- updateToolBlockInMessage,
4
- addErrorBlockToMessage,
5
- addUserMessageToMessages,
6
- updateUserMessageInMessages,
7
- addBangMessage,
8
- updateBangInMessage,
9
- completeBangInMessage,
10
- removeLastUserMessage,
11
- addToolBlockToMessageInMessages,
12
- addNotificationMessageToMessages,
13
- UserMessageParams,
14
- type AgentToolBlockUpdateParams,
15
- type ToolBlockUpdateCallbackParams,
16
- type AddNotificationMessageParams,
17
- generateMessageId,
18
- sliceFromLastCompact,
19
- } from "../utils/messageOperations.js";
20
- import type { Message, Usage, ToolBlock } from "../types/index.js";
21
- import { getLastApiRounds } from "../utils/groupMessagesByApiRound.js";
22
- import { join, isAbsolute, relative } from "path";
23
- import {
24
- appendMessages,
25
- createSession,
26
- generateSessionId,
27
- SessionData,
28
- SESSION_DIR,
29
- } from "../services/session.js";
30
- import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
31
- import type { MemoryRuleManager } from "./MemoryRuleManager.js";
32
- import type { MemoryRule } from "../types/memoryRule.js";
33
- import type { MemoryService } from "../services/memory.js";
34
- import { pathEncoder } from "../utils/pathEncoder.js";
35
- import { estimateTokens } from "../utils/tokenEstimate.js";
36
- import { READ_TOOL_NAME } from "../constants/tools.js";
37
-
38
- import { Container } from "../utils/container.js";
39
-
40
- export interface MessageManagerCallbacks {
41
- onSessionIdChange?: (sessionId: string) => void;
42
- onLatestTotalTokensChange?: (latestTotalTokens: number) => void;
43
- onUsagesChange?: (usages: Usage[]) => void;
44
- // Incremental callback
45
- onUserMessageAdded?: (params: UserMessageParams) => void;
46
- // MODIFIED: Remove arguments for separation of concerns
47
- onAssistantMessageAdded?: (messageId: string) => void;
48
- // NEW: Streaming content callback - FR-001: receives chunk and accumulated content
49
- onAssistantContentUpdated?: (params: {
50
- messageId: string;
51
- chunk: string;
52
- accumulated: string;
53
- stage: "streaming" | "end";
54
- }) => void;
55
- // NEW: Streaming reasoning callback
56
- onAssistantReasoningUpdated?: (params: {
57
- messageId: string;
58
- chunk: string;
59
- accumulated: string;
60
- stage: "streaming" | "end";
61
- }) => void;
62
- onToolBlockUpdated?: (params: ToolBlockUpdateCallbackParams) => void;
63
- onErrorBlockAdded?: (error: string) => void;
64
- onCompactBlockAdded?: (content: string) => void;
65
- onCompactionStateChange?: (isCompacting: boolean) => void;
66
- // Bang callback
67
- onAddBangMessage?: (command: string, messageId: string) => void;
68
- onUpdateBangMessage?: (
69
- command: string,
70
- output: string,
71
- messageId: string,
72
- ) => void;
73
- onCompleteBangMessage?: (
74
- command: string,
75
- exitCode: number,
76
- messageId: string,
77
- ) => void;
78
- onInfoBlockAdded?: (content: string) => void;
79
- // Rewind callbacks
80
- onShowRewind?: () => void;
81
- onFileHistoryBlockAdded?: (
82
- snapshots: import("../types/reversion.js").FileSnapshot[],
83
- ) => void;
84
- // Notification callback
85
- onNotificationMessageAdded?: (params: {
86
- taskId: string;
87
- taskType: "shell" | "agent" | "workflow";
88
- status: "completed" | "failed" | "killed" | "aborted";
89
- summary: string;
90
- }) => void;
91
- }
92
-
93
- import { logger } from "../utils/globalLogger.js";
94
-
95
- export interface MessageManagerOptions {
96
- callbacks: MessageManagerCallbacks;
97
- workdir: string;
98
- sessionType?: "main" | "subagent";
99
- subagentType?: string;
100
- }
101
-
102
- export class MessageManager {
103
- // Private state properties
104
- private sessionId: string;
105
- private messages: Message[];
106
- private latestTotalTokens: number;
107
- private workdir: string;
108
- private encodedWorkdir: string; // Cached encoded workdir
109
- private callbacks: MessageManagerCallbacks;
110
- private transcriptPath: string; // Cached transcript path
111
- private savedMessageCount: number; // Track how many messages have been saved to prevent duplication
112
- private pendingFileReadTriggers: Set<string> = new Set(); // File paths read via Read tool, awaiting rule matching
113
- private loadedRuleIds: Set<string> = new Set(); // IDs of conditional rules already injected as meta messages
114
- private recentFileReads: Map<string, { content: string; timestamp: number }> =
115
- new Map(); // Track file read contents
116
- private invokedSkills: Map<string, { skillName: string; timestamp: number }> =
117
- new Map(); // Track invoked skill names
118
- private sessionType: "main" | "subagent";
119
- private subagentType?: string;
120
- private _usages: Usage[] = [];
121
-
122
- constructor(
123
- private container: Container,
124
- options: MessageManagerOptions,
125
- ) {
126
- this.sessionId = generateSessionId();
127
- this.messages = [];
128
- this.latestTotalTokens = 0;
129
- this.workdir = options.workdir;
130
- this.encodedWorkdir = pathEncoder.encodeSync(this.workdir); // Cache encoded workdir
131
- this.callbacks = options.callbacks;
132
- this.savedMessageCount = 0; // Initialize saved message count tracker
133
- this.sessionType = options.sessionType || "main";
134
- this.subagentType = options.subagentType;
135
-
136
- // Compute and cache the transcript path
137
- this.transcriptPath = this.computeTranscriptPath();
138
- }
139
-
140
- private get memoryRuleManager(): MemoryRuleManager | undefined {
141
- return this.container.has("MemoryRuleManager")
142
- ? this.container.get<MemoryRuleManager>("MemoryRuleManager")
143
- : undefined;
144
- }
145
-
146
- private get memoryService(): MemoryService {
147
- const service = this.container.get<MemoryService>("MemoryService");
148
- if (!service) {
149
- throw new Error("MemoryService not found in container");
150
- }
151
- return service;
152
- }
153
-
154
- // Getter methods
155
- public getSessionId(): string {
156
- return this.sessionId;
157
- }
158
-
159
- public getRootSessionId(): string {
160
- return this.sessionId;
161
- }
162
-
163
- public getMessages(): Message[] {
164
- return [...this.messages];
165
- }
166
-
167
- public getUsages(): Usage[] {
168
- return [...this._usages];
169
- }
170
-
171
- public getLatestTotalTokens(): number {
172
- return this.latestTotalTokens;
173
- }
174
-
175
- public getWorkdir(): string {
176
- return this.workdir;
177
- }
178
-
179
- /**
180
- * Process pending file read triggers: match against conditional rules,
181
- * return rules not yet loaded (dedup via loadedRuleIds), mark them as loaded.
182
- * Clears triggers after processing.
183
- */
184
- public processTriggeredRules(): MemoryRule[] {
185
- if (!this.memoryRuleManager || this.pendingFileReadTriggers.size === 0) {
186
- this.pendingFileReadTriggers.clear();
187
- return [];
188
- }
189
- const filePaths = Array.from(this.pendingFileReadTriggers);
190
- this.pendingFileReadTriggers.clear();
191
-
192
- const { conditional } =
193
- this.memoryRuleManager.getActiveRulesSplit(filePaths);
194
- const newRules = conditional.filter(
195
- (rule) => !this.loadedRuleIds.has(rule.id),
196
- );
197
- for (const rule of newRules) {
198
- this.loadedRuleIds.add(rule.id);
199
- }
200
- return newRules;
201
- }
202
-
203
- public getSessionDir(): string {
204
- return SESSION_DIR;
205
- }
206
-
207
- public getTranscriptPath(): string {
208
- return this.transcriptPath;
209
- }
210
-
211
- /**
212
- * Get combined memory content (project memory + user memory + unconditional rules)
213
- * Note: conditional rules are handled separately via processTriggeredRules().
214
- */
215
- public async getCombinedMemory(): Promise<string> {
216
- let combined = await this.memoryService.getCombinedMemoryContent(
217
- this.workdir,
218
- );
219
-
220
- if (this.memoryRuleManager) {
221
- const { unconditional } = this.memoryRuleManager.getActiveRulesSplit([]);
222
- if (unconditional.length > 0) {
223
- combined += "\n\n";
224
- combined += unconditional.map((r) => r.content).join("\n\n");
225
- }
226
- }
227
-
228
- return combined;
229
- }
230
-
231
- /**
232
- * Get memory content for message-array injection:
233
- * - prependContent: AGENTS.md + user memory + unconditional rules (for head injection)
234
- * Conditional rules are handled separately via processTriggeredRules().
235
- */
236
- public async getMemoryForInjection(): Promise<{
237
- prependContent: string;
238
- }> {
239
- const baseMemory = await this.memoryService.getCombinedMemoryContent(
240
- this.workdir,
241
- );
242
-
243
- let prependContent = baseMemory;
244
-
245
- if (this.memoryRuleManager) {
246
- const { unconditional } = this.memoryRuleManager.getActiveRulesSplit([]);
247
- if (unconditional.length > 0) {
248
- prependContent += "\n\n";
249
- prependContent += unconditional.map((r) => r.content).join("\n\n");
250
- }
251
- }
252
-
253
- return { prependContent };
254
- }
255
-
256
- /**
257
- * Compute the transcript path using cached encoded workdir
258
- * Called during construction and when sessionId changes
259
- */
260
- private computeTranscriptPath(): string {
261
- const baseDir = join(SESSION_DIR, this.encodedWorkdir);
262
-
263
- // All sessions now go in the same directory
264
- // Session type is determined by metadata, not file path
265
- return join(baseDir, `${this.sessionId}.jsonl`);
266
- }
267
-
268
- // Setter methods, will trigger callbacks
269
- public setSessionId(sessionId: string): void {
270
- if (this.sessionId !== sessionId) {
271
- this.sessionId = sessionId;
272
- // Reset saved message count for new session
273
- this.savedMessageCount = 0;
274
- // Recompute transcript path since session ID changed
275
- this.transcriptPath = this.computeTranscriptPath();
276
-
277
- this.callbacks.onSessionIdChange?.(sessionId);
278
- }
279
- }
280
-
281
- /**
282
- * Create session if needed (async helper)
283
- */
284
- private async createSessionIfNeeded(): Promise<void> {
285
- try {
286
- await createSession(this.sessionId, this.workdir, this.sessionType);
287
- } catch (error) {
288
- logger?.error("Failed to create session:", error);
289
- }
290
- }
291
-
292
- /**
293
- * Record a file path read via the Read tool, to be matched against
294
- * conditional memory rules before the next AI call.
295
- */
296
- public triggerFileRead(filePath: string): void {
297
- const normalizedPath = isAbsolute(filePath)
298
- ? relative(this.workdir, filePath)
299
- : filePath;
300
- this.pendingFileReadTriggers.add(normalizedPath);
301
- }
302
-
303
- /**
304
- * Check if a file has been read (for read-before-edit enforcement).
305
- * Uses recentFileReads Map populated from Read tool execution.
306
- */
307
- public hasFileBeenRead(filePath: string): boolean {
308
- const normalizedPath = isAbsolute(filePath)
309
- ? relative(this.workdir, filePath)
310
- : filePath;
311
- return (
312
- this.recentFileReads.has(normalizedPath) ||
313
- this.recentFileReads.has(filePath)
314
- );
315
- }
316
-
317
- public setMessages(messages: Message[]): void {
318
- const oldLength = this.messages.length;
319
- this.messages = [...messages];
320
-
321
- // Incrementally extract metadata from new messages
322
- const newMessages = messages.slice(oldLength);
323
- for (const message of newMessages) {
324
- this.extractFileReadsFromMessage(message);
325
- this.extractSkillInvocationsFromMessage(message);
326
- }
327
-
328
- // Also check if the last message was updated (common for tool blocks)
329
- if (messages.length > 0 && messages.length === oldLength) {
330
- this.extractFileReadsFromMessage(messages[messages.length - 1]);
331
- this.extractSkillInvocationsFromMessage(messages[messages.length - 1]);
332
- }
333
- }
334
-
335
- /**
336
- * Save current session
337
- */
338
- public async saveSession(): Promise<void> {
339
- try {
340
- // Only save messages that haven't been saved yet
341
- const unsavedMessages = this.messages.slice(this.savedMessageCount);
342
-
343
- if (unsavedMessages.length === 0) {
344
- // No new messages to save
345
- return;
346
- }
347
-
348
- // CC-aligned lazy materialization: when the session file has not been
349
- // materialized yet (no saved messages) and every unsaved message is a
350
- // meta message (isMeta: true, e.g. SessionStart hook context), skip
351
- // persistence. Persisting meta-only sessions would create "0 tokens /
352
- // No content" ghost entries in the resume list. The meta messages stay
353
- // in memory and are flushed together with the first real user/assistant
354
- // message (matches CC's pendingEntries buffering).
355
- if (
356
- this.savedMessageCount === 0 &&
357
- unsavedMessages.every((m) => m.isMeta)
358
- ) {
359
- return;
360
- }
361
-
362
- // Create session if needed (only when we have messages to save)
363
- if (this.savedMessageCount === 0) {
364
- // This is the first time saving messages, so create the session
365
- await this.createSessionIfNeeded();
366
- }
367
-
368
- // Use JSONL format for new sessions
369
- await appendMessages(
370
- this.sessionId,
371
- unsavedMessages,
372
- this.workdir,
373
- this.sessionType,
374
- );
375
-
376
- // Update the saved message count
377
- this.savedMessageCount = this.messages.length;
378
- } catch (error) {
379
- logger?.error("Failed to save session:", error);
380
- }
381
- }
382
-
383
- public setlatestTotalTokens(latestTotalTokens: number): void {
384
- if (this.latestTotalTokens !== latestTotalTokens) {
385
- this.latestTotalTokens = latestTotalTokens;
386
-
387
- this.callbacks.onLatestTotalTokensChange?.(latestTotalTokens);
388
- }
389
- }
390
-
391
- /**
392
- * Clear messages
393
- */
394
- public clearMessages(): void {
395
- this.setMessages([]);
396
- const newSessionId = generateSessionId();
397
- this.setSessionId(newSessionId);
398
- this.setlatestTotalTokens(0);
399
- this.savedMessageCount = 0; // Reset saved message count
400
- }
401
-
402
- /**
403
- * Trigger the rewind UI callback
404
- */
405
- public triggerShowRewind(): void {
406
- this.callbacks.onShowRewind?.();
407
- }
408
-
409
- // Initialize state from session data
410
- public initializeFromSession(sessionData: SessionData): void {
411
- this.setSessionId(sessionData.id);
412
- this.setMessages([...sessionData.messages]);
413
- this.setlatestTotalTokens(sessionData.metadata.latestTotalTokens);
414
-
415
- // Rebuild loadedRuleIds from persisted meta messages (session restore)
416
- this.rebuildLoadedRuleIds();
417
-
418
- // Set saved message count to the number of loaded messages since they're already saved
419
- // This must be done after setSessionId which resets it to 0
420
- this.savedMessageCount = sessionData.messages.length;
421
- }
422
-
423
- // Encapsulated message operation functions
424
- public addUserMessage(params: UserMessageParams): string {
425
- const id = generateMessageId();
426
- const newMessages = addUserMessageToMessages({
427
- messages: this.messages,
428
- ...params,
429
- id,
430
- });
431
- this.setMessages(newMessages);
432
- this.callbacks.onUserMessageAdded?.(params);
433
-
434
- // Note: Subagent-specific callbacks are now handled by SubagentManager
435
- return id;
436
- }
437
-
438
- /**
439
- * Update an existing user message by its ID.
440
- */
441
- public updateUserMessage(
442
- id: string,
443
- params: Partial<UserMessageParams>,
444
- ): void {
445
- const newMessages = updateUserMessageInMessages(this.messages, id, params);
446
- this.setMessages(newMessages);
447
- }
448
-
449
- public addAssistantMessage(
450
- content?: string,
451
- toolCalls?: ChatCompletionMessageFunctionToolCall[],
452
- usage?: Usage,
453
- additionalFields?: Record<string, unknown>,
454
- ): void {
455
- const additionalFieldsRecord = additionalFields
456
- ? Object.fromEntries(
457
- Object.entries(additionalFields).filter(
458
- ([, value]) => value !== undefined,
459
- ),
460
- )
461
- : undefined;
462
-
463
- const newMessages = addAssistantMessageToMessages(
464
- this.messages,
465
- content,
466
- toolCalls,
467
- usage,
468
- additionalFieldsRecord,
469
- );
470
- this.setMessages(newMessages);
471
- const messageId = this.messages[this.messages.length - 1]?.id;
472
- this.callbacks.onAssistantMessageAdded?.(messageId);
473
-
474
- // Note: Subagent-specific callbacks are now handled by SubagentManager
475
- }
476
-
477
- public mergeAssistantAdditionalFields(
478
- additionalFields: Record<string, unknown>,
479
- ): void {
480
- if (!additionalFields || Object.keys(additionalFields).length === 0) {
481
- return;
482
- }
483
-
484
- const newMessages = [...this.messages];
485
- for (let i = newMessages.length - 1; i >= 0; i--) {
486
- const message = newMessages[i];
487
- if (message.role === "assistant") {
488
- const mergedAdditionalFields = {
489
- ...(message.additionalFields || {}),
490
- } as Record<string, unknown>;
491
-
492
- for (const [key, value] of Object.entries(additionalFields)) {
493
- if (value === undefined) {
494
- continue;
495
- }
496
- mergedAdditionalFields[key] = value;
497
- }
498
-
499
- if (Object.keys(mergedAdditionalFields).length === 0) {
500
- return;
501
- }
502
-
503
- message.additionalFields = mergedAdditionalFields;
504
- this.setMessages(newMessages);
505
- return;
506
- }
507
- }
508
- }
509
-
510
- public updateToolBlock(params: AgentToolBlockUpdateParams): void {
511
- // Finalize any streaming text/reasoning blocks before adding/updating a tool block
512
- this.finalizeStreamingBlocks();
513
- const { messages: newMessages, messageId: resolvedMessageId } =
514
- updateToolBlockInMessage({
515
- messages: this.messages,
516
- ...params,
517
- });
518
- this.setMessages(newMessages);
519
- const callbackParams: ToolBlockUpdateCallbackParams = {
520
- ...params,
521
- messageId:
522
- params.messageId ||
523
- resolvedMessageId ||
524
- this.messages[this.messages.length - 1]?.id ||
525
- "",
526
- };
527
- this.callbacks.onToolBlockUpdated?.(callbackParams);
528
-
529
- // Note: Subagent-specific callbacks are now handled by SubagentManager
530
- }
531
-
532
- /**
533
- * Add a tool block to a specific message by ID.
534
- */
535
- public addToolBlockToMessage(
536
- messageId: string,
537
- params: Omit<AgentToolBlockUpdateParams, "id">,
538
- ): string {
539
- // Finalize any streaming text/reasoning blocks before adding a tool block
540
- this.finalizeStreamingBlocks();
541
- const { messages: newMessages, toolBlockId } =
542
- addToolBlockToMessageInMessages(this.messages, messageId, params);
543
- this.setMessages(newMessages);
544
- this.callbacks.onToolBlockUpdated?.({
545
- ...params,
546
- id: toolBlockId,
547
- messageId,
548
- });
549
- return toolBlockId;
550
- }
551
-
552
- public addErrorBlock(error: string): void {
553
- const newMessages = addErrorBlockToMessage({
554
- messages: this.messages,
555
- error,
556
- });
557
- this.setMessages(newMessages);
558
- this.callbacks.onErrorBlockAdded?.(error);
559
- }
560
-
561
- public addInfoBlock(content: string): void {
562
- const lastMessage = this.messages[this.messages.length - 1];
563
- if (lastMessage && lastMessage.role === "assistant") {
564
- lastMessage.blocks.push({
565
- type: "info",
566
- content,
567
- } as unknown as import("../types/index.js").MessageBlock);
568
- this.setMessages([...this.messages]);
569
- this.callbacks.onInfoBlockAdded?.(content);
570
- }
571
- }
572
-
573
- /**
574
- * Compact messages and update session — append compact message to same file (append-only).
575
- * After compaction, in-memory messages are [compactMessage, ...lastThreeMessages].
576
- */
577
- public async compactMessagesAndUpdateSession(
578
- compactedContent: string,
579
- usage?: Usage,
580
- ): Promise<void> {
581
- // Get last 2 API rounds to preserve (structurally safe boundary)
582
- const lastThreeMessages = getLastApiRounds(this.messages, 2);
583
-
584
- // Create compacted message
585
- const compactMessage: Message = {
586
- id: generateMessageId(),
587
- role: "assistant",
588
- blocks: [
589
- {
590
- type: "compact",
591
- content: compactedContent,
592
- },
593
- ],
594
- timestamp: new Date().toISOString(),
595
- ...(usage && { usage }),
596
- };
597
-
598
- // Build new message array: keep the compacted message and last messages
599
- const newMessages: Message[] = [compactMessage, ...lastThreeMessages];
600
-
601
- // APPEND to the same session file (append-only, like Claude Code)
602
- const { appendMessages } = await import("../services/session.js");
603
- await appendMessages(
604
- this.sessionId,
605
- newMessages,
606
- this.workdir,
607
- this.sessionType,
608
- );
609
-
610
- // Update in-memory state
611
- this.setMessages(newMessages);
612
- this.savedMessageCount = newMessages.length;
613
-
614
- // Clear and rebuild loaded rule IDs from remaining meta messages
615
- this.clearLoadedRuleIds();
616
- this.rebuildLoadedRuleIds();
617
-
618
- // Trigger compaction callback
619
- this.callbacks.onCompactBlockAdded?.(compactedContent);
620
- }
621
-
622
- public addFileHistoryBlock(
623
- snapshots: import("../types/reversion.js").FileSnapshot[],
624
- ): void {
625
- if (snapshots.length === 0) return;
626
-
627
- const lastMessage = this.messages[this.messages.length - 1];
628
- if (lastMessage && lastMessage.role === "assistant") {
629
- lastMessage.blocks.push({
630
- type: "file_history",
631
- snapshots,
632
- } as unknown as import("../types/index.js").MessageBlock);
633
- this.setMessages([...this.messages]);
634
- this.callbacks.onFileHistoryBlockAdded?.(snapshots);
635
- }
636
- }
637
-
638
- // Bang related message operations
639
- public addBangMessage(command: string): void {
640
- const updatedMessages = addBangMessage({
641
- messages: this.messages,
642
- command,
643
- });
644
- this.setMessages(updatedMessages);
645
- // The bang message is appended as the last message
646
- const messageId = this.messages[this.messages.length - 1]?.id ?? "";
647
- this.callbacks.onAddBangMessage?.(command, messageId);
648
- }
649
-
650
- public updateBangMessage(command: string, output: string): void {
651
- const updatedMessages = updateBangInMessage({
652
- messages: this.messages,
653
- command,
654
- output,
655
- });
656
- this.setMessages(updatedMessages);
657
- const messageId = this.findBangMessageId(command) ?? "";
658
- this.callbacks.onUpdateBangMessage?.(command, output, messageId);
659
- }
660
-
661
- public completeBangMessage(
662
- command: string,
663
- exitCode: number,
664
- output?: string,
665
- ): void {
666
- const updatedMessages = completeBangInMessage({
667
- messages: this.messages,
668
- command,
669
- exitCode,
670
- output,
671
- });
672
- this.setMessages(updatedMessages);
673
- const messageId = this.findBangMessageId(command) ?? "";
674
- this.callbacks.onCompleteBangMessage?.(command, exitCode, messageId);
675
- }
676
-
677
- /**
678
- * Find the message ID of the most recent message containing a bang block
679
- * for the given command. Bang callbacks do not carry a block ID, so the
680
- * message is located by matching the command against bang blocks.
681
- */
682
- private findBangMessageId(command: string): string | undefined {
683
- for (let i = this.messages.length - 1; i >= 0; i--) {
684
- const message = this.messages[i];
685
- if (message.role !== "user") continue;
686
- const hasBangBlock = message.blocks?.some(
687
- (block) => block.type === "bang" && block.command === command,
688
- );
689
- if (hasBangBlock) return message.id;
690
- }
691
- return undefined;
692
- }
693
-
694
- public addNotificationMessage(
695
- params: Omit<AddNotificationMessageParams, "messages">,
696
- ): void {
697
- const newMessages = addNotificationMessageToMessages({
698
- messages: this.messages,
699
- ...params,
700
- });
701
- this.setMessages(newMessages);
702
- this.callbacks.onNotificationMessageAdded?.({
703
- taskId: params.taskId,
704
- taskType: params.taskType,
705
- status: params.status,
706
- summary: params.summary,
707
- });
708
- }
709
-
710
- /**
711
- * Rebuild usage array from messages containing usage metadata
712
- * Called during session restoration to reconstruct usage tracking
713
- */
714
- public rebuildUsageFromMessages(messages: Message[]): void {
715
- this._usages = [];
716
- messages.forEach((message) => {
717
- if (message.role === "assistant" && message.usage) {
718
- this._usages.push(message.usage);
719
- }
720
- });
721
- // Trigger callback after rebuilding usage array
722
- this.triggerUsageChange();
723
- }
724
-
725
- /**
726
- * Add usage data to the tracking array and trigger callbacks
727
- * @param usage Usage data from AI operations
728
- */
729
- public addUsage(usage: Usage): void {
730
- this._usages.push(usage);
731
- this.triggerUsageChange();
732
- }
733
-
734
- /**
735
- * Trigger usage change callback with all usage data from assistant messages
736
- */
737
- public triggerUsageChange(): void {
738
- this.callbacks.onUsagesChange?.([...this._usages]);
739
- }
740
-
741
- /**
742
- * Finalize a streaming block of the given type by setting its stage to "end".
743
- * Fires the corresponding incremental callback with chunk="" to signal finalization.
744
- * Returns true if a block was finalized.
745
- */
746
- private finalizeStreamingBlock(
747
- lastMessage: Message,
748
- type: "text" | "reasoning",
749
- ): boolean {
750
- const index = lastMessage.blocks.findIndex(
751
- (block) =>
752
- block.type === type &&
753
- (block as { stage?: string }).stage === "streaming",
754
- );
755
- if (index < 0) return false;
756
-
757
- const block = lastMessage.blocks[index] as {
758
- type: "text" | "reasoning";
759
- content: string;
760
- stage?: string;
761
- startTime?: number;
762
- };
763
- if (block.type === "reasoning") {
764
- lastMessage.blocks[index] = {
765
- type: "reasoning",
766
- content: block.content,
767
- stage: "end" as const,
768
- startTime: block.startTime,
769
- endTime: Date.now(),
770
- };
771
- } else {
772
- lastMessage.blocks[index] = {
773
- type: block.type,
774
- content: block.content,
775
- stage: "end" as const,
776
- };
777
- }
778
-
779
- // Fire incremental callback to signal finalization
780
- const callbackParams = {
781
- messageId: lastMessage.id,
782
- chunk: "",
783
- accumulated: block.content,
784
- stage: "end" as const,
785
- };
786
- if (type === "text") {
787
- this.callbacks.onAssistantContentUpdated?.(callbackParams);
788
- } else {
789
- this.callbacks.onAssistantReasoningUpdated?.(callbackParams);
790
- }
791
- return true;
792
- }
793
-
794
- /**
795
- * Update the current assistant message content during streaming
796
- * This method updates the last assistant message's content without creating a new message
797
- * FR-001: Tracks and provides both chunk (new content) and accumulated (total content)
798
- */
799
- public updateCurrentMessageContent(newAccumulatedContent: string): void {
800
- if (this.messages.length === 0) return;
801
-
802
- const lastMessage = this.messages[this.messages.length - 1];
803
- if (lastMessage.role !== "assistant") return;
804
-
805
- // Finalize any streaming reasoning blocks before text content arrives
806
- this.finalizeStreamingBlock(lastMessage, "reasoning");
807
-
808
- // Get the current content to calculate the chunk
809
- const textBlockIndex = lastMessage.blocks.findIndex(
810
- (block) => block.type === "text",
811
- );
812
- const currentContent =
813
- textBlockIndex >= 0
814
- ? (
815
- lastMessage.blocks[textBlockIndex] as {
816
- type: "text";
817
- content: string;
818
- }
819
- ).content || ""
820
- : "";
821
-
822
- // Calculate the chunk (new content since last update)
823
- const chunk = newAccumulatedContent.slice(currentContent.length);
824
-
825
- if (textBlockIndex >= 0) {
826
- // Update existing text block
827
- lastMessage.blocks[textBlockIndex] = {
828
- type: "text",
829
- content: newAccumulatedContent,
830
- stage: "streaming",
831
- };
832
- } else {
833
- // Add new text block if none exists
834
- lastMessage.blocks.push({
835
- type: "text",
836
- content: newAccumulatedContent,
837
- stage: "streaming",
838
- });
839
- }
840
-
841
- // FR-001: Trigger callbacks with chunk and accumulated content
842
- this.callbacks.onAssistantContentUpdated?.({
843
- messageId: lastMessage.id,
844
- chunk,
845
- accumulated: newAccumulatedContent,
846
- stage: "streaming",
847
- });
848
-
849
- // Note: Subagent-specific callbacks are now handled by SubagentManager
850
- }
851
-
852
- /**
853
- * Update the current assistant message reasoning during streaming
854
- * This method updates the last assistant message's reasoning content without creating a new message
855
- */
856
- public updateCurrentMessageReasoning(newAccumulatedReasoning: string): void {
857
- if (this.messages.length === 0) return;
858
-
859
- const lastMessage = this.messages[this.messages.length - 1];
860
- if (lastMessage.role !== "assistant") return;
861
-
862
- // Finalize any streaming text blocks before reasoning content arrives
863
- this.finalizeStreamingBlock(lastMessage, "text");
864
-
865
- // Get the current reasoning content to calculate the chunk
866
- const reasoningBlockIndex = lastMessage.blocks.findIndex(
867
- (block) => block.type === "reasoning",
868
- );
869
- const currentReasoning =
870
- reasoningBlockIndex >= 0
871
- ? (
872
- lastMessage.blocks[reasoningBlockIndex] as {
873
- type: "reasoning";
874
- content: string;
875
- }
876
- ).content || ""
877
- : "";
878
-
879
- // Calculate the chunk (new content since last update)
880
- const chunk = newAccumulatedReasoning.slice(currentReasoning.length);
881
-
882
- if (reasoningBlockIndex >= 0) {
883
- // Update existing reasoning block
884
- const existingStartTime = (
885
- lastMessage.blocks[reasoningBlockIndex] as {
886
- startTime?: number;
887
- }
888
- ).startTime;
889
- lastMessage.blocks[reasoningBlockIndex] = {
890
- type: "reasoning",
891
- content: newAccumulatedReasoning,
892
- stage: "streaming",
893
- startTime: existingStartTime,
894
- };
895
- } else {
896
- // Add new reasoning block if none exists
897
- lastMessage.blocks.push({
898
- type: "reasoning",
899
- content: newAccumulatedReasoning,
900
- stage: "streaming",
901
- startTime: Date.now(),
902
- });
903
- }
904
-
905
- // Trigger callbacks with chunk and accumulated reasoning content
906
- this.callbacks.onAssistantReasoningUpdated?.({
907
- messageId: lastMessage.id,
908
- chunk,
909
- accumulated: newAccumulatedReasoning,
910
- stage: "streaming",
911
- });
912
- }
913
-
914
- /**
915
- * Finalize streaming text/reasoning blocks by setting their stage to "end".
916
- * Called when a new block (e.g. tool) is appended during streaming, or when streaming completes.
917
- * Fires incremental callbacks for each finalized block.
918
- */
919
- public finalizeStreamingBlocks(): void {
920
- if (this.messages.length === 0) return;
921
- const lastMessage = this.messages[this.messages.length - 1];
922
- if (lastMessage.role !== "assistant") return;
923
-
924
- this.finalizeStreamingBlock(lastMessage, "text");
925
- this.finalizeStreamingBlock(lastMessage, "reasoning");
926
- }
927
-
928
- /**
929
- * Finalize any tool blocks still in a non-terminal stage (start/streaming/running)
930
- * by marking them as ended with an error. Called when the AI call is aborted or
931
- * fails mid-tool-stream so the UI stops showing the "running" spinner.
932
- * Fires onToolBlockUpdated for each finalized tool block.
933
- */
934
- public finalizeAbortedToolBlocks(error?: string): void {
935
- if (this.messages.length === 0) return;
936
- const lastMessage = this.messages[this.messages.length - 1];
937
- if (lastMessage.role !== "assistant") return;
938
-
939
- const errorMessage = error ?? "Tool execution was aborted";
940
- let finalized = false;
941
-
942
- for (let i = 0; i < lastMessage.blocks.length; i++) {
943
- const block = lastMessage.blocks[i] as ToolBlock;
944
- if (block.type !== "tool" || block.stage === "end") continue;
945
-
946
- const timestamp = Date.now();
947
- lastMessage.blocks[i] = {
948
- ...block,
949
- stage: "end",
950
- success: false,
951
- error: errorMessage,
952
- timestamp,
953
- };
954
- finalized = true;
955
-
956
- this.callbacks.onToolBlockUpdated?.({
957
- id: block.id ?? "",
958
- messageId: lastMessage.id ?? "",
959
- name: block.name,
960
- parameters: block.parameters,
961
- parametersChunk: block.parametersChunk,
962
- compactParams: block.compactParams,
963
- stage: "end",
964
- success: false,
965
- error: errorMessage,
966
- isManuallyBackgrounded: block.isManuallyBackgrounded,
967
- timestamp,
968
- });
969
- }
970
-
971
- if (finalized) {
972
- this.setMessages([...this.messages]);
973
- }
974
- }
975
-
976
- /**
977
- * Remove the last user message from the conversation
978
- * Used for hook error handling when the user prompt needs to be erased
979
- */
980
- public removeLastUserMessage(): void {
981
- const newMessages = removeLastUserMessage(this.messages);
982
- this.setMessages(newMessages);
983
- }
984
-
985
- public async getFullMessageThread(): Promise<{
986
- messages: Message[];
987
- sessionIds: string[];
988
- }> {
989
- const { loadFullMessageThread } = await import("../services/session.js");
990
- return loadFullMessageThread(this.sessionId, this.workdir);
991
- }
992
-
993
- /**
994
- * Truncate history to a specific index and revert file changes.
995
- * @param index - The index of the user message to truncate to.
996
- * @param reversionManager - Optional ReversionManager to handle file rollbacks.
997
- */
998
- public async truncateHistory(
999
- index: number,
1000
- reversionManager?: import("./reversionManager.js").ReversionManager,
1001
- ): Promise<void> {
1002
- const { messages } = await this.getFullMessageThread();
1003
-
1004
- if (index < 0 || index >= messages.length) {
1005
- throw new Error(`Invalid message index: ${index}`);
1006
- }
1007
-
1008
- const newMessages = messages.slice(0, index);
1009
- const messagesToRemove = messages.slice(index);
1010
- const messageIdsToRemove = messagesToRemove
1011
- .map((m) => m.id as string)
1012
- .filter((id) => !!id);
1013
-
1014
- if (reversionManager && messageIdsToRemove.length > 0) {
1015
- await reversionManager.revertTo(messageIdsToRemove, messages);
1016
- }
1017
-
1018
- // Rewrite file with truncated messages (full history kept on disk)
1019
- await this.rewriteSessionFile(newMessages);
1020
- // Fold the in-memory messages at the last compact boundary so the agent
1021
- // only sees messages from the latest compact summary forward — matching
1022
- // the compact and resume behaviors (which also fold memory).
1023
- this.setMessages(sliceFromLastCompact(newMessages));
1024
- this.savedMessageCount = newMessages.length;
1025
- }
1026
-
1027
- /**
1028
- * Rewrite the session file with the current messages.
1029
- */
1030
- private async rewriteSessionFile(messages: Message[]): Promise<void> {
1031
- try {
1032
- const { writeFile } = await import("fs/promises");
1033
-
1034
- const content =
1035
- messages
1036
- .map((m) => {
1037
- const { timestamp, ...rest } = m;
1038
- return JSON.stringify({ timestamp, ...rest });
1039
- })
1040
- .join("\n") + (messages.length > 0 ? "\n" : "");
1041
-
1042
- await writeFile(this.transcriptPath, content, "utf8");
1043
- } catch (error) {
1044
- logger?.error("Failed to rewrite session file:", error);
1045
- }
1046
- }
1047
-
1048
- /**
1049
- * Extract file read contents from tool result blocks in a message.
1050
- */
1051
- private extractFileReadsFromMessage(message: Message): void {
1052
- for (const block of message.blocks) {
1053
- if (
1054
- block.type === "tool" &&
1055
- block.name === READ_TOOL_NAME &&
1056
- block.stage === "end" &&
1057
- block.result &&
1058
- block.parameters
1059
- ) {
1060
- let filePath: string | undefined;
1061
- try {
1062
- const params = JSON.parse(block.parameters) as Record<
1063
- string,
1064
- unknown
1065
- >;
1066
- filePath = params.file_path as string | undefined;
1067
- } catch {
1068
- // Ignore parse errors
1069
- }
1070
- if (filePath) {
1071
- this.recentFileReads.set(filePath, {
1072
- content: block.result,
1073
- timestamp: Date.now(),
1074
- });
1075
- }
1076
- }
1077
- }
1078
- }
1079
-
1080
- /**
1081
- * Get recent file read contents, sorted by timestamp (newest first).
1082
- * @param maxFiles - Maximum number of files to return
1083
- * @param maxTokensPerFile - Maximum tokens per file (CJK-aware estimation)
1084
- * @returns Array of { path, content } sorted by recency
1085
- */
1086
- public getRecentFileReads(
1087
- maxFiles = 5,
1088
- maxTokensPerFile = 5000,
1089
- ): Array<{ path: string; content: string }> {
1090
- const sorted = Array.from(this.recentFileReads.entries())
1091
- .sort(([, a], [, b]) => b.timestamp - a.timestamp)
1092
- .slice(0, maxFiles);
1093
-
1094
- const result: Array<{ path: string; content: string }> = [];
1095
- for (const [path, { content }] of sorted) {
1096
- const tokenCount = estimateTokens(content);
1097
- let truncated = content;
1098
- if (tokenCount > maxTokensPerFile) {
1099
- // Truncate proportionally to fit within the token budget
1100
- const ratio = maxTokensPerFile / tokenCount;
1101
- const cutIndex = Math.floor(content.length * ratio);
1102
- truncated = content.slice(0, cutIndex);
1103
- }
1104
- result.push({ path, content: truncated });
1105
- }
1106
- return result;
1107
- }
1108
-
1109
- /**
1110
- * Extract skill invocations from tool blocks in a message.
1111
- */
1112
- private extractSkillInvocationsFromMessage(message: Message): void {
1113
- for (const block of message.blocks) {
1114
- if (
1115
- block.type === "tool" &&
1116
- block.name === "Skill" &&
1117
- block.stage === "end" &&
1118
- block.parameters
1119
- ) {
1120
- try {
1121
- const params = JSON.parse(block.parameters) as Record<
1122
- string,
1123
- unknown
1124
- >;
1125
- const skillName = params.skill_name as string | undefined;
1126
- if (skillName) {
1127
- this.invokedSkills.set(skillName, {
1128
- skillName,
1129
- timestamp: Date.now(),
1130
- });
1131
- }
1132
- } catch {
1133
- // Ignore parse errors
1134
- }
1135
- }
1136
- }
1137
- }
1138
-
1139
- /**
1140
- * Get recently invoked skill names, sorted by timestamp (newest first).
1141
- * @param maxSkills - Maximum number of skills to return
1142
- * @returns Array of skill names sorted by recency
1143
- */
1144
- public getInvokedSkillNames(maxSkills = 10): string[] {
1145
- const sorted = Array.from(this.invokedSkills.entries())
1146
- .sort(([, a], [, b]) => b.timestamp - a.timestamp)
1147
- .slice(0, maxSkills);
1148
-
1149
- return sorted.map(([, { skillName }]) => skillName);
1150
- }
1151
-
1152
- /**
1153
- * Clear all invoked skills (e.g., after compaction).
1154
- */
1155
- public clearInvokedSkills(): void {
1156
- this.invokedSkills.clear();
1157
- }
1158
-
1159
- /**
1160
- * Clear the loaded rule IDs set (e.g., after compaction resets context).
1161
- */
1162
- public clearLoadedRuleIds(): void {
1163
- this.loadedRuleIds.clear();
1164
- }
1165
-
1166
- /**
1167
- * Rebuild loadedRuleIds from persisted messages (session restore / post-compaction).
1168
- * Scans for <!-- rule: {ruleId} --> markers in user meta message content.
1169
- */
1170
- public rebuildLoadedRuleIds(): void {
1171
- for (const message of this.messages) {
1172
- if (message.role !== "user" || !message.isMeta) continue;
1173
- for (const block of message.blocks) {
1174
- if (block.type === "text") {
1175
- const content = (block as { content: string }).content;
1176
- const match = content.match(/<!-- rule: (.+?) -->/);
1177
- if (match) {
1178
- this.loadedRuleIds.add(match[1]);
1179
- }
1180
- }
1181
- }
1182
- }
1183
- }
1184
- }