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.
- package/dist/agent.d.ts +23 -1
- package/dist/agent.js +34 -4
- package/dist/managers/aiManager.d.ts +36 -0
- package/dist/managers/aiManager.js +240 -8
- package/dist/managers/messageManager.d.ts +1 -3
- package/dist/managers/messageManager.js +19 -9
- package/dist/managers/slashCommandManager.js +28 -0
- package/dist/managers/subagentManager.d.ts +0 -2
- package/dist/services/session.js +2 -9
- package/dist/telemetry/instrumentation.d.ts +1 -1
- package/dist/telemetry/instrumentation.js +57 -28
- package/dist/tools/bashTool.js +16 -6
- package/dist/tools/grepTool.js +11 -3
- package/dist/types/index.d.ts +3 -0
- package/dist/types/messaging.d.ts +2 -0
- package/dist/utils/containerSetup.js +0 -3
- package/dist/utils/messageOperations.d.ts +10 -1
- package/dist/utils/messageOperations.js +19 -1
- package/dist/utils/notificationXml.js +5 -0
- package/dist/utils/ripgrep.d.ts +4 -4
- package/dist/utils/ripgrep.js +4 -27
- package/package.json +20 -8
- package/bin/rg +0 -79
- package/dist/managers/forkedAgentManager.d.ts +0 -50
- package/dist/managers/forkedAgentManager.js +0 -130
- package/scripts/install_ripgrep.js +0 -111
- package/scripts/postinstall.js +0 -38
- package/src/agent.ts +0 -1271
- package/src/constants/subagents.ts +0 -4
- package/src/constants/toolLimits.ts +0 -15
- package/src/constants/tools.ts +0 -24
- package/src/core/plugin.ts +0 -237
- package/src/core/session.ts +0 -9
- package/src/index.ts +0 -38
- package/src/managers/MemoryRuleManager.ts +0 -198
- package/src/managers/aiManager.ts +0 -2429
- package/src/managers/backgroundTaskManager.ts +0 -480
- package/src/managers/bangManager.ts +0 -119
- package/src/managers/cronManager.ts +0 -401
- package/src/managers/foregroundTaskManager.ts +0 -30
- package/src/managers/forkedAgentManager.ts +0 -216
- package/src/managers/hookManager.ts +0 -1089
- package/src/managers/liveConfigManager.ts +0 -445
- package/src/managers/lspManager.ts +0 -470
- package/src/managers/mcpManager.ts +0 -1010
- package/src/managers/messageManager.ts +0 -1180
- package/src/managers/messageQueue.ts +0 -157
- package/src/managers/permissionManager.ts +0 -1140
- package/src/managers/planManager.ts +0 -112
- package/src/managers/pluginManager.ts +0 -330
- package/src/managers/pluginScopeManager.ts +0 -124
- package/src/managers/reversionManager.ts +0 -178
- package/src/managers/skillManager.ts +0 -578
- package/src/managers/slashCommandManager.ts +0 -564
- package/src/managers/subagentManager.ts +0 -915
- package/src/managers/toolManager.ts +0 -489
- package/src/managers/workflowManager.ts +0 -493
- package/src/prompts/autoMemory.ts +0 -33
- package/src/prompts/autoMemoryExtraction.ts +0 -146
- package/src/prompts/index.ts +0 -506
- package/src/prompts/planModeReminders.ts +0 -126
- package/src/services/GitService.ts +0 -131
- package/src/services/MarketplaceService.ts +0 -1052
- package/src/services/MemoryRuleService.ts +0 -71
- package/src/services/aiService.ts +0 -919
- package/src/services/authService.ts +0 -565
- package/src/services/autoMemoryService.ts +0 -280
- package/src/services/configurationService.ts +0 -1520
- package/src/services/fileWatcher.ts +0 -319
- package/src/services/hook.ts +0 -348
- package/src/services/initializationService.ts +0 -357
- package/src/services/interactionService.ts +0 -229
- package/src/services/jsonlHandler.ts +0 -295
- package/src/services/memory.ts +0 -234
- package/src/services/pluginLoader.ts +0 -252
- package/src/services/remoteSettingsService.ts +0 -366
- package/src/services/reversionService.ts +0 -122
- package/src/services/session.ts +0 -922
- package/src/services/taskManager.ts +0 -321
- package/src/telemetry/events.ts +0 -65
- package/src/telemetry/instrumentation.ts +0 -499
- package/src/telemetry/sessionTracing.ts +0 -348
- package/src/tools/agentTool.ts +0 -322
- package/src/tools/askUserQuestion.ts +0 -160
- package/src/tools/bashTool.ts +0 -621
- package/src/tools/buildTool.ts +0 -61
- package/src/tools/cronCreateTool.ts +0 -161
- package/src/tools/cronDeleteTool.ts +0 -51
- package/src/tools/cronListTool.ts +0 -47
- package/src/tools/editTool.ts +0 -313
- package/src/tools/enterPlanMode.ts +0 -124
- package/src/tools/enterWorktreeTool.ts +0 -190
- package/src/tools/exitPlanMode.ts +0 -139
- package/src/tools/exitWorktreeTool.ts +0 -236
- package/src/tools/globTool.ts +0 -167
- package/src/tools/grepTool.ts +0 -399
- package/src/tools/lspTool.ts +0 -883
- package/src/tools/readTool.ts +0 -426
- package/src/tools/skillTool.ts +0 -248
- package/src/tools/taskManagementTools.ts +0 -574
- package/src/tools/taskStopTool.ts +0 -78
- package/src/tools/types.ts +0 -137
- package/src/tools/webFetchTool.ts +0 -373
- package/src/tools/workflowTool.ts +0 -205
- package/src/tools/writeTool.ts +0 -267
- package/src/types/agent.ts +0 -128
- package/src/types/auth.ts +0 -19
- package/src/types/commands.ts +0 -30
- package/src/types/config.ts +0 -43
- package/src/types/configuration.ts +0 -168
- package/src/types/core.ts +0 -105
- package/src/types/cron.ts +0 -13
- package/src/types/environment.ts +0 -106
- package/src/types/fileSearch.ts +0 -4
- package/src/types/history.ts +0 -7
- package/src/types/hooks.ts +0 -256
- package/src/types/index.ts +0 -41
- package/src/types/lsp.ts +0 -98
- package/src/types/marketplace.ts +0 -71
- package/src/types/mcp.ts +0 -43
- package/src/types/memoryRule.ts +0 -31
- package/src/types/messaging.ts +0 -114
- package/src/types/permissions.ts +0 -80
- package/src/types/plugins.ts +0 -39
- package/src/types/processes.ts +0 -99
- package/src/types/reversion.ts +0 -29
- package/src/types/session.ts +0 -12
- package/src/types/skills.ts +0 -108
- package/src/types/tasks.ts +0 -13
- package/src/types/telemetry.ts +0 -98
- package/src/types/tools.ts +0 -44
- package/src/types/workflow.ts +0 -6
- package/src/utils/abortUtils.ts +0 -118
- package/src/utils/atomicWrite.ts +0 -61
- package/src/utils/bashParser.ts +0 -899
- package/src/utils/cacheControlUtils.ts +0 -426
- package/src/utils/commandArgumentParser.ts +0 -161
- package/src/utils/commandPathResolver.ts +0 -88
- package/src/utils/configPaths.ts +0 -220
- package/src/utils/configValidator.ts +0 -100
- package/src/utils/constants.ts +0 -39
- package/src/utils/container.ts +0 -92
- package/src/utils/containerSetup.ts +0 -371
- package/src/utils/convertMessagesForAPI.ts +0 -326
- package/src/utils/cronTasks.ts +0 -128
- package/src/utils/cronTasksLock.ts +0 -182
- package/src/utils/cronToHuman.ts +0 -99
- package/src/utils/customCommands.ts +0 -95
- package/src/utils/editUtils.ts +0 -20
- package/src/utils/fileFormat.ts +0 -40
- package/src/utils/fileSearch.ts +0 -151
- package/src/utils/fileUtils.ts +0 -223
- package/src/utils/gitUtils.ts +0 -315
- package/src/utils/globalLogger.ts +0 -128
- package/src/utils/groupMessagesByApiRound.ts +0 -121
- package/src/utils/hookMatcher.ts +0 -176
- package/src/utils/markdownParser.ts +0 -317
- package/src/utils/mcpUtils.ts +0 -138
- package/src/utils/messageOperations.ts +0 -641
- package/src/utils/modelCapabilities.ts +0 -30
- package/src/utils/nameGenerator.ts +0 -99
- package/src/utils/notificationXml.ts +0 -57
- package/src/utils/openaiClient.ts +0 -251
- package/src/utils/parseCronExpression.ts +0 -78
- package/src/utils/path.ts +0 -72
- package/src/utils/pathEncoder.ts +0 -400
- package/src/utils/pathSafety.ts +0 -40
- package/src/utils/promptHistory.ts +0 -168
- package/src/utils/ripgrep.ts +0 -35
- package/src/utils/shellResolver.ts +0 -190
- package/src/utils/skillParser.ts +0 -246
- package/src/utils/stringUtils.ts +0 -156
- package/src/utils/subagentParser.ts +0 -279
- package/src/utils/taskReminder.ts +0 -96
- package/src/utils/tokenCalculation.ts +0 -43
- package/src/utils/tokenEstimate.ts +0 -34
- package/src/utils/toolResultStorage.ts +0 -117
- package/src/utils/worktreeSession.ts +0 -26
- package/src/utils/worktreeUtils.ts +0 -786
- package/src/workflow/budgetTracker.ts +0 -34
- package/src/workflow/concurrencyLimiter.ts +0 -47
- package/src/workflow/journal.ts +0 -95
- package/src/workflow/progressReporter.ts +0 -141
- package/src/workflow/runState.ts +0 -65
- package/src/workflow/scriptRuntime.ts +0 -274
- package/src/workflow/structuredOutput.ts +0 -123
- package/src/workflow/types.ts +0 -101
- package/src/workflow/workflowApis.ts +0 -410
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/linux-x86_64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- 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
|
-
}
|