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