wave-agent-sdk 0.19.7 → 0.19.9
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/builtin/plugins/sdd/.wave-plugin/plugin.json +8 -0
- package/builtin/plugins/sdd/hooks/hooks.json +14 -0
- package/builtin/plugins/sdd/scripts/session-start.js +24 -0
- package/builtin/plugins/sdd/scripts/spec-count.js +77 -0
- package/builtin/plugins/sdd/skills/specify/SKILL.md +48 -0
- package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +47 -0
- package/dist/agent.d.ts +8 -0
- package/dist/agent.js +30 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +18 -0
- package/dist/managers/aiManager.js +155 -46
- package/dist/managers/permissionManager.d.ts +7 -0
- package/dist/managers/permissionManager.js +102 -142
- package/dist/managers/pluginManager.d.ts +7 -0
- package/dist/managers/pluginManager.js +31 -0
- package/dist/managers/subagentManager.js +6 -0
- package/dist/prompts/index.d.ts +12 -1
- package/dist/prompts/index.js +133 -45
- package/dist/services/aiService.d.ts +1 -17
- package/dist/services/aiService.js +3 -85
- package/dist/services/configurationService.d.ts +6 -0
- package/dist/services/configurationService.js +31 -0
- package/dist/services/remoteSettingsService.js +2 -0
- package/dist/services/session.d.ts +3 -1
- package/dist/services/session.js +12 -4
- package/dist/services/taskManager.d.ts +1 -0
- package/dist/services/taskManager.js +34 -5
- package/dist/tools/editTool.js +24 -10
- package/dist/tools/enterWorktreeTool.js +2 -1
- package/dist/tools/grepTool.js +8 -2
- package/dist/tools/writeTool.js +36 -0
- package/dist/types/configuration.d.ts +5 -0
- package/dist/types/permissions.d.ts +0 -2
- package/dist/types/processes.d.ts +27 -0
- package/dist/types/workflow.d.ts +1 -1
- package/dist/utils/bashParser.d.ts +25 -0
- package/dist/utils/bashParser.js +103 -0
- package/dist/utils/configPaths.d.ts +4 -0
- package/dist/utils/configPaths.js +6 -0
- package/dist/utils/containerSetup.js +0 -9
- package/dist/utils/fileSearch.js +4 -2
- package/dist/utils/worktreeSession.d.ts +1 -1
- package/dist/utils/worktreeSession.js +1 -1
- package/dist/utils/worktreeUtils.d.ts +7 -1
- package/dist/utils/worktreeUtils.js +10 -4
- package/dist/workflow/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/agent.ts +29 -10
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +219 -61
- package/src/managers/permissionManager.ts +116 -168
- package/src/managers/pluginManager.ts +29 -0
- package/src/managers/subagentManager.ts +6 -0
- package/src/prompts/index.ts +144 -37
- package/src/services/aiService.ts +9 -128
- package/src/services/configurationService.ts +37 -0
- package/src/services/remoteSettingsService.ts +1 -0
- package/src/services/session.ts +18 -4
- package/src/services/taskManager.ts +46 -7
- package/src/tools/editTool.ts +29 -11
- package/src/tools/enterWorktreeTool.ts +2 -1
- package/src/tools/grepTool.ts +11 -2
- package/src/tools/writeTool.ts +43 -0
- package/src/types/configuration.ts +5 -0
- package/src/types/permissions.ts +0 -2
- package/src/types/processes.ts +29 -0
- package/src/types/workflow.ts +1 -0
- package/src/utils/bashParser.ts +106 -0
- package/src/utils/configPaths.ts +7 -0
- package/src/utils/containerSetup.ts +0 -11
- package/src/utils/fileSearch.ts +6 -2
- package/src/utils/worktreeSession.ts +1 -1
- package/src/utils/worktreeUtils.ts +14 -4
- package/src/workflow/types.ts +6 -0
|
@@ -18,6 +18,10 @@ export declare function getBuiltinSkillsDir(): string;
|
|
|
18
18
|
* Get the builtin subagents directory path
|
|
19
19
|
*/
|
|
20
20
|
export declare function getBuiltinSubagentsDir(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Get the builtin plugins directory path
|
|
23
|
+
*/
|
|
24
|
+
export declare function getBuiltinPluginsDir(): string;
|
|
21
25
|
/**
|
|
22
26
|
* Get the user-specific configuration file path (legacy function)
|
|
23
27
|
* @deprecated Use getUserConfigPaths() for better priority support
|
|
@@ -46,6 +46,12 @@ export function getBuiltinSkillsDir() {
|
|
|
46
46
|
export function getBuiltinSubagentsDir() {
|
|
47
47
|
return join(getPackageRoot(), "builtin", "subagents");
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Get the builtin plugins directory path
|
|
51
|
+
*/
|
|
52
|
+
export function getBuiltinPluginsDir() {
|
|
53
|
+
return join(getPackageRoot(), "builtin", "plugins");
|
|
54
|
+
}
|
|
49
55
|
/**
|
|
50
56
|
* Get the user-specific configuration file path (legacy function)
|
|
51
57
|
* @deprecated Use getUserConfigPaths() for better priority support
|
|
@@ -179,21 +179,12 @@ export function setupAgentContainer(setupOptions) {
|
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
const decision = await options.canUseTool(context);
|
|
182
|
-
const planFilePath = permissionManager.getPlanFilePath();
|
|
183
182
|
if (decision.newPermissionMode) {
|
|
184
183
|
setPermissionMode(decision.newPermissionMode);
|
|
185
184
|
}
|
|
186
185
|
if (decision.newPermissionRule) {
|
|
187
186
|
await addPermissionRule(decision.newPermissionRule);
|
|
188
187
|
}
|
|
189
|
-
if (decision.clearContext) {
|
|
190
|
-
messageManager.clearMessages();
|
|
191
|
-
if (planFilePath) {
|
|
192
|
-
messageManager.addUserMessage({
|
|
193
|
-
content: `Implement the plan at ${planFilePath}`,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
188
|
return decision;
|
|
198
189
|
}
|
|
199
190
|
: undefined;
|
package/dist/utils/fileSearch.js
CHANGED
|
@@ -25,9 +25,11 @@ async function getAllFiles(workingDirectory) {
|
|
|
25
25
|
stderr += data.toString();
|
|
26
26
|
});
|
|
27
27
|
child.on("close", (code) => {
|
|
28
|
+
// Exit 2 = some files were unreadable (e.g. device-name files like
|
|
29
|
+
// "nul" on Windows); stdout still holds usable partial results.
|
|
30
|
+
// Spawn failures surface via the 'error' listener instead.
|
|
28
31
|
if (code !== 0 && code !== 1) {
|
|
29
|
-
|
|
30
|
-
return;
|
|
32
|
+
logger.warn(`ripgrep exited with code ${code}, keeping partial results: ${stderr.trim()}`);
|
|
31
33
|
}
|
|
32
34
|
const files = stdout
|
|
33
35
|
.trim()
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* "WorktreeSession" container slot registered in containerSetup.ts and accessed via
|
|
6
6
|
* AIManager.getWorktreeSession()/setWorktreeSession()). This keeps worktree state
|
|
7
7
|
* isolated per session in stdio multi-agent mode — a process-level singleton would
|
|
8
|
-
* leak state across concurrent sessions (see specs/
|
|
8
|
+
* leak state across concurrent sessions (see docs/specs/multi-agent/worktree.md FR-042).
|
|
9
9
|
*/
|
|
10
10
|
export interface WorktreeSession {
|
|
11
11
|
/** The working directory the session was in before EnterWorktree */
|
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
* "WorktreeSession" container slot registered in containerSetup.ts and accessed via
|
|
6
6
|
* AIManager.getWorktreeSession()/setWorktreeSession()). This keeps worktree state
|
|
7
7
|
* isolated per session in stdio multi-agent mode — a process-level singleton would
|
|
8
|
-
* leak state across concurrent sessions (see specs/
|
|
8
|
+
* leak state across concurrent sessions (see docs/specs/multi-agent/worktree.md FR-042).
|
|
9
9
|
*/
|
|
10
10
|
export {};
|
|
@@ -25,8 +25,14 @@ export declare function generateWorktreeName(): string;
|
|
|
25
25
|
export declare function getHeadCommit(cwd: string): string;
|
|
26
26
|
/**
|
|
27
27
|
* Create a git worktree for use during a session.
|
|
28
|
+
* @param name Worktree name
|
|
29
|
+
* @param cwd Current working directory (will be resolved to main repo root)
|
|
30
|
+
* @param options Optional creation options
|
|
31
|
+
* @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
|
|
28
32
|
*/
|
|
29
|
-
export declare function createWorktree(name: string, cwd: string
|
|
33
|
+
export declare function createWorktree(name: string, cwd: string, options?: {
|
|
34
|
+
baseRef?: "fresh" | "head";
|
|
35
|
+
}): WorktreeInfo;
|
|
30
36
|
/**
|
|
31
37
|
* Remove a git worktree and its branch.
|
|
32
38
|
*/
|
|
@@ -68,8 +68,12 @@ export function getHeadCommit(cwd) {
|
|
|
68
68
|
}
|
|
69
69
|
/**
|
|
70
70
|
* Create a git worktree for use during a session.
|
|
71
|
+
* @param name Worktree name
|
|
72
|
+
* @param cwd Current working directory (will be resolved to main repo root)
|
|
73
|
+
* @param options Optional creation options
|
|
74
|
+
* @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
|
|
71
75
|
*/
|
|
72
|
-
export function createWorktree(name, cwd) {
|
|
76
|
+
export function createWorktree(name, cwd, options) {
|
|
73
77
|
const repoRoot = getGitMainRepoRoot(cwd);
|
|
74
78
|
if (!repoRoot) {
|
|
75
79
|
throw new Error("Cannot create a worktree: not in a git repository. Configure WorktreeCreate and WorktreeRemove hooks in settings.json to use worktree isolation with other VCS systems.");
|
|
@@ -78,7 +82,8 @@ export function createWorktree(name, cwd) {
|
|
|
78
82
|
const originalHeadCommit = getHeadCommit(cwd);
|
|
79
83
|
const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
|
|
80
84
|
const branchName = `worktree-${name}`;
|
|
81
|
-
const
|
|
85
|
+
const useHead = options?.baseRef === "head";
|
|
86
|
+
const baseBranch = useHead ? "HEAD" : getDefaultRemoteBranch(cwd);
|
|
82
87
|
// Ensure Wave runtime files are git-excluded in this repo
|
|
83
88
|
ensureWaveRuntimeFilesExcluded(cwd);
|
|
84
89
|
// Ensure parent directory exists
|
|
@@ -144,8 +149,9 @@ export function createWorktree(name, cwd) {
|
|
|
144
149
|
throw new Error(`Failed to add worktree: ${innerError.message}`);
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
|
-
if (
|
|
148
|
-
stderr.includes("
|
|
152
|
+
if (!useHead &&
|
|
153
|
+
(stderr.includes("not a valid object name") ||
|
|
154
|
+
stderr.includes("unknown revision"))) {
|
|
149
155
|
// Base branch not fetched yet — try fetching then retrying
|
|
150
156
|
const branchNameOnly = baseBranch.split("/").pop();
|
|
151
157
|
try {
|
package/dist/workflow/types.d.ts
CHANGED
|
@@ -38,6 +38,11 @@ export interface WorkflowRun {
|
|
|
38
38
|
/** Error message from the failed agent */
|
|
39
39
|
failedAgentError?: string;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Serializable workflow run for stdio transport, with the non-serializable
|
|
43
|
+
* `completionPromise` stripped. Returned by the `getWorkflowRuns` RPC.
|
|
44
|
+
*/
|
|
45
|
+
export type SerializableWorkflowRun = Omit<WorkflowRun, "completionPromise">;
|
|
41
46
|
export interface JournalEntry {
|
|
42
47
|
agentIndex: number;
|
|
43
48
|
prompt: string;
|
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -78,6 +78,7 @@ export class Agent {
|
|
|
78
78
|
private reversionManager: ReversionManager;
|
|
79
79
|
private messageQueue: MessageQueue; // Unified queue for messages, bang commands, and notifications
|
|
80
80
|
private dispatchPromise: Promise<void> | null = null; // Track current dispatch for teardown
|
|
81
|
+
private isAborting = false; // Guard: prevents tryDispatch from firing during abortMessage
|
|
81
82
|
private memoryRuleManager: MemoryRuleManager; // Add memory rule manager instance
|
|
82
83
|
private liveConfigManager: LiveConfigManager; // Add live configuration manager
|
|
83
84
|
private taskManager: TaskManager;
|
|
@@ -412,6 +413,7 @@ export class Agent {
|
|
|
412
413
|
* onLoadingChange(false), and onCommandRunningChange(false).
|
|
413
414
|
*/
|
|
414
415
|
private tryDispatch(): void {
|
|
416
|
+
if (this.isAborting) return; // Suppress dispatch during abort to prevent queued notifications from being dispatched as a side-effect
|
|
415
417
|
if (this.messageQueue.state !== "idle") return;
|
|
416
418
|
if (!this.messageQueue.hasPending()) return;
|
|
417
419
|
if (this.aiManager.isLoading || this.isCommandRunning) return;
|
|
@@ -847,17 +849,24 @@ export class Agent {
|
|
|
847
849
|
|
|
848
850
|
/** Unified interrupt method, interrupts both AI messages and command execution */
|
|
849
851
|
public abortMessage(): void {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
this.
|
|
855
|
-
|
|
852
|
+
// Guard: prevent tryDispatch (triggered by abortAIMessage → setIsLoading(false))
|
|
853
|
+
// from dispatching preserved notifications as a new AI turn during the abort.
|
|
854
|
+
this.isAborting = true;
|
|
855
|
+
try {
|
|
856
|
+
if (this.aiManager.isLoading || this.isCommandRunning) {
|
|
857
|
+
// Clear user-facing queue items first to prevent processQueuedMessage
|
|
858
|
+
// from dequeuing when abortAIMessage triggers onLoadingChange(false).
|
|
859
|
+
// Notifications are preserved so background task results aren't lost.
|
|
860
|
+
this.messageQueue.clear();
|
|
861
|
+
this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
|
|
862
|
+
}
|
|
863
|
+
this.messageQueue.transitionTo("idle"); // Reset state on abort
|
|
864
|
+
this.abortAIMessage(); // This will abort tools including Agent tool (subagents)
|
|
865
|
+
this.abortBashCommand();
|
|
866
|
+
this.abortSlashCommand();
|
|
867
|
+
} finally {
|
|
868
|
+
this.isAborting = false;
|
|
856
869
|
}
|
|
857
|
-
this.messageQueue.transitionTo("idle"); // Reset state on abort
|
|
858
|
-
this.abortAIMessage(); // This will abort tools including Agent tool (subagents)
|
|
859
|
-
this.abortBashCommand();
|
|
860
|
-
this.abortSlashCommand();
|
|
861
870
|
}
|
|
862
871
|
|
|
863
872
|
/** Interrupt bash command execution */
|
|
@@ -1266,4 +1275,14 @@ export class Agent {
|
|
|
1266
1275
|
this.subagentManager.getActiveInstances().length > 0;
|
|
1267
1276
|
return runningTasks || activeSubagents;
|
|
1268
1277
|
}
|
|
1278
|
+
|
|
1279
|
+
/**
|
|
1280
|
+
* Check if there are pending items (messages, bang commands, or background
|
|
1281
|
+
* task notifications) in the message queue. Background task completion
|
|
1282
|
+
* notifications are enqueued before the main agent's dispatch consumes them,
|
|
1283
|
+
* so callers waiting for the agent to fully settle must also wait on this.
|
|
1284
|
+
*/
|
|
1285
|
+
public get hasPendingMessages(): boolean {
|
|
1286
|
+
return this.messageQueue.hasPending();
|
|
1287
|
+
}
|
|
1269
1288
|
}
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ export * from "./utils/tokenCalculation.js";
|
|
|
29
29
|
export * from "./utils/gitUtils.js";
|
|
30
30
|
export * from "./utils/nameGenerator.js";
|
|
31
31
|
export * from "./utils/worktreeSession.js";
|
|
32
|
+
export { loadMergedWaveConfig } from "./services/configurationService.js";
|
|
32
33
|
export * from "./types/index.js";
|
|
33
34
|
|
|
34
35
|
// Export tool building utilities
|
|
@@ -22,7 +22,10 @@ import type { ToolManager } from "./toolManager.js";
|
|
|
22
22
|
import type { ToolContext, ToolResult } from "../tools/types.js";
|
|
23
23
|
import type { MessageManager } from "./messageManager.js";
|
|
24
24
|
import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
ChatCompletionMessageFunctionToolCall,
|
|
27
|
+
type ChatCompletionMessageParam,
|
|
28
|
+
} from "openai/resources.js";
|
|
26
29
|
|
|
27
30
|
import type { HookManager } from "./hookManager.js";
|
|
28
31
|
import type { ExtendedHookExecutionContext } from "../types/hooks.js";
|
|
@@ -31,11 +34,16 @@ import type { PermissionManager } from "./permissionManager.js";
|
|
|
31
34
|
import type { SubagentManager } from "./subagentManager.js";
|
|
32
35
|
import type { CronManager } from "./cronManager.js";
|
|
33
36
|
import type { SkillManager } from "./skillManager.js";
|
|
34
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
buildSystemPrompt,
|
|
39
|
+
formatCompactSummary,
|
|
40
|
+
getCompactPrompt,
|
|
41
|
+
} from "../prompts/index.js";
|
|
35
42
|
import {
|
|
36
43
|
buildPlanModeReminder,
|
|
37
44
|
buildPlanModeReEntryReminder,
|
|
38
45
|
buildExitedPlanModeReminder,
|
|
46
|
+
wrapInSystemReminder,
|
|
39
47
|
} from "../prompts/planModeReminders.js";
|
|
40
48
|
import { Container } from "../utils/container.js";
|
|
41
49
|
import type { WorktreeSession } from "../utils/worktreeSession.js";
|
|
@@ -252,6 +260,10 @@ export class AIManager {
|
|
|
252
260
|
return this.configurationService.resolveAutoMemoryEnabled();
|
|
253
261
|
}
|
|
254
262
|
|
|
263
|
+
public getWorktreeBaseRef(): "fresh" | "head" {
|
|
264
|
+
return this.configurationService.resolveWorktreeBaseRef();
|
|
265
|
+
}
|
|
266
|
+
|
|
255
267
|
public getWorkdir(): string {
|
|
256
268
|
return this.container.get<string>("Workdir") ?? process.cwd();
|
|
257
269
|
}
|
|
@@ -533,36 +545,47 @@ export class AIManager {
|
|
|
533
545
|
|
|
534
546
|
this.setIsCompacting(true);
|
|
535
547
|
try {
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const compactResult = await aiService.compactMessages({
|
|
540
|
-
gatewayConfig: this.getGatewayConfig(),
|
|
541
|
-
modelConfig: this.getModelConfig(),
|
|
542
|
-
messages: recentChatMessages,
|
|
543
|
-
abortSignal: options.abortSignal,
|
|
544
|
-
model: this.getModelConfig().fastModel,
|
|
545
|
-
customInstructions: mergedInstructions,
|
|
548
|
+
const modelConfig = this.getModelConfig();
|
|
549
|
+
const recentChatMessages = convertMessagesForAPI(messagesToCompact, {
|
|
550
|
+
supportsVision: supportsVision(modelConfig.capabilities),
|
|
546
551
|
});
|
|
552
|
+
const compactPrompt = getCompactPrompt(mergedInstructions);
|
|
553
|
+
|
|
554
|
+
// 4. Fork path: fork the conversation with the same system prompt,
|
|
555
|
+
// tools, model, and generation params as the main loop so the forked
|
|
556
|
+
// request prefix matches exactly and the prompt cache is reused.
|
|
557
|
+
const forkResult = await this.runCompactFork(
|
|
558
|
+
recentChatMessages,
|
|
559
|
+
compactPrompt,
|
|
560
|
+
options.abortSignal,
|
|
561
|
+
);
|
|
562
|
+
const summaryContent = forkResult.content;
|
|
563
|
+
const compactTokens = forkResult.usage;
|
|
564
|
+
if (!summaryContent) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
"Compaction failed: the model produced no summary output",
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const compactModel = modelConfig.model;
|
|
547
570
|
|
|
548
571
|
// 5. Handle usage tracking
|
|
549
572
|
let compactUsage: Usage | undefined;
|
|
550
|
-
if (
|
|
573
|
+
if (compactTokens) {
|
|
551
574
|
compactUsage = {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
total_tokens: compactResult.usage.total_tokens,
|
|
555
|
-
model: this.getModelConfig().fastModel,
|
|
575
|
+
...compactTokens,
|
|
576
|
+
model: compactModel,
|
|
556
577
|
operation_type: "compact",
|
|
557
578
|
};
|
|
558
579
|
}
|
|
559
580
|
|
|
560
|
-
// 6.
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
581
|
+
// 6. Strip the <analysis> scratchpad and extract the <summary> body
|
|
582
|
+
const formattedSummary = formatCompactSummary(summaryContent);
|
|
583
|
+
|
|
584
|
+
// 7. Build post-compact context restoration
|
|
585
|
+
const enhancedSummary =
|
|
586
|
+
await this.buildPostCompactContext(formattedSummary);
|
|
564
587
|
|
|
565
|
-
//
|
|
588
|
+
// 8. Execute message reconstruction
|
|
566
589
|
await this.messageManager.compactMessagesAndUpdateSession(
|
|
567
590
|
enhancedSummary,
|
|
568
591
|
compactUsage,
|
|
@@ -587,7 +610,7 @@ export class AIManager {
|
|
|
587
610
|
}
|
|
588
611
|
}
|
|
589
612
|
|
|
590
|
-
//
|
|
613
|
+
// 9. Track usage
|
|
591
614
|
if (compactUsage && this.callbacks?.onUsageAdded) {
|
|
592
615
|
this.callbacks.onUsageAdded(compactUsage);
|
|
593
616
|
}
|
|
@@ -597,14 +620,14 @@ export class AIManager {
|
|
|
597
620
|
// Reset incremental tracing state after compaction
|
|
598
621
|
resetTracingState();
|
|
599
622
|
|
|
600
|
-
//
|
|
623
|
+
// 10. Log OTEL event
|
|
601
624
|
logOTelEvent("compaction", {
|
|
602
625
|
beforeTokens: String(messagesToCompact.length),
|
|
603
626
|
afterTokens: "1",
|
|
604
|
-
model:
|
|
627
|
+
model: compactModel,
|
|
605
628
|
}).catch(() => {});
|
|
606
629
|
|
|
607
|
-
//
|
|
630
|
+
// 11. Run SessionStart hooks (existing behavior)
|
|
608
631
|
if (this.hookManager) {
|
|
609
632
|
try {
|
|
610
633
|
const newSessionId = this.messageManager.getSessionId();
|
|
@@ -634,13 +657,13 @@ export class AIManager {
|
|
|
634
657
|
}
|
|
635
658
|
}
|
|
636
659
|
|
|
637
|
-
//
|
|
660
|
+
// 12. Run PostCompact hooks
|
|
638
661
|
if (this.hookManager) {
|
|
639
662
|
try {
|
|
640
663
|
await this.hookManager.executePostCompactHooks(
|
|
641
664
|
this.messageManager.getSessionId(),
|
|
642
665
|
this.messageManager.getTranscriptPath(),
|
|
643
|
-
|
|
666
|
+
formattedSummary,
|
|
644
667
|
);
|
|
645
668
|
} catch (error) {
|
|
646
669
|
logger?.warn(`PostCompact hooks failed: ${(error as Error).message}`);
|
|
@@ -664,6 +687,158 @@ export class AIManager {
|
|
|
664
687
|
}
|
|
665
688
|
}
|
|
666
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Build the system prompt used by the main agent loop. Extracted so the
|
|
692
|
+
* compaction fork can mirror it exactly — the forked request prefix must
|
|
693
|
+
* match the main conversation's for the prompt cache to be reused.
|
|
694
|
+
*/
|
|
695
|
+
private async buildMainSystemPrompt(
|
|
696
|
+
filteredToolPlugins: ReturnType<ToolManager["getTools"]>,
|
|
697
|
+
) {
|
|
698
|
+
let autoMemoryOptions: { directory: string; content: string } | undefined;
|
|
699
|
+
|
|
700
|
+
if (this.getAutoMemoryEnabled()) {
|
|
701
|
+
const directory = this.memoryService.getAutoMemoryDirectory(
|
|
702
|
+
this.getWorkdir(),
|
|
703
|
+
);
|
|
704
|
+
const content = await this.memoryService.getAutoMemoryContent(
|
|
705
|
+
this.getWorkdir(),
|
|
706
|
+
);
|
|
707
|
+
autoMemoryOptions = { directory, content };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return buildSystemPrompt(this.systemPrompt, filteredToolPlugins, {
|
|
711
|
+
workdir: this.getWorkdir(),
|
|
712
|
+
originalWorkdir: this.getOriginalWorkdir(),
|
|
713
|
+
language: this.getLanguage(),
|
|
714
|
+
isSubagent: !!this.subagentType,
|
|
715
|
+
worktreeSession: this.getWorktreeSession(),
|
|
716
|
+
autoMemory: autoMemoryOptions,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private resolveFilteredTools() {
|
|
721
|
+
const toolsConfig = this.getFilteredToolsConfig();
|
|
722
|
+
const toolNames = new Set(toolsConfig.map((t) => t.function.name));
|
|
723
|
+
const filteredToolPlugins = this.toolManager
|
|
724
|
+
.getTools()
|
|
725
|
+
.filter((t) => toolNames.has(t.name));
|
|
726
|
+
return { toolsConfig, toolNames, filteredToolPlugins };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Fork-path compaction: run a bounded agent loop over a copy of the
|
|
731
|
+
* conversation using the same system prompt, tools, model, and generation
|
|
732
|
+
* params as the main loop, so the forked request prefix matches exactly
|
|
733
|
+
* and the prompt cache is reused. Tool calls are denied locally (the model
|
|
734
|
+
* is told to summarize, not act) and their rejections are fed back for
|
|
735
|
+
* another turn. Returns undefined content when the model never produces
|
|
736
|
+
* text; the caller treats that as a compaction failure.
|
|
737
|
+
*/
|
|
738
|
+
private async runCompactFork(
|
|
739
|
+
historyMessages: ChatCompletionMessageParam[],
|
|
740
|
+
compactPrompt: string,
|
|
741
|
+
abortSignal?: AbortSignal,
|
|
742
|
+
): Promise<{
|
|
743
|
+
content?: string;
|
|
744
|
+
usage?: {
|
|
745
|
+
prompt_tokens: number;
|
|
746
|
+
completion_tokens: number;
|
|
747
|
+
total_tokens: number;
|
|
748
|
+
};
|
|
749
|
+
}> {
|
|
750
|
+
const MAX_FORK_TURNS = 3;
|
|
751
|
+
const modelConfig = this.getModelConfig();
|
|
752
|
+
const gatewayConfig = this.getGatewayConfig();
|
|
753
|
+
const sessionId = this.messageManager.getSessionId();
|
|
754
|
+
const workdir = this.getWorkdir();
|
|
755
|
+
|
|
756
|
+
const forkMessages: ChatCompletionMessageParam[] = [...historyMessages];
|
|
757
|
+
|
|
758
|
+
// Mirror the main loop's memory injection so the request prefix matches.
|
|
759
|
+
const { prependContent } =
|
|
760
|
+
await this.messageManager.getMemoryForInjection();
|
|
761
|
+
if (prependContent.trim()) {
|
|
762
|
+
forkMessages.unshift({
|
|
763
|
+
role: "user",
|
|
764
|
+
content: wrapInSystemReminder(prependContent),
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
forkMessages.push({ role: "user", content: compactPrompt });
|
|
769
|
+
|
|
770
|
+
const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
|
|
771
|
+
const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
772
|
+
|
|
773
|
+
let totalUsage:
|
|
774
|
+
| {
|
|
775
|
+
prompt_tokens: number;
|
|
776
|
+
completion_tokens: number;
|
|
777
|
+
total_tokens: number;
|
|
778
|
+
}
|
|
779
|
+
| undefined;
|
|
780
|
+
let content: string | undefined;
|
|
781
|
+
|
|
782
|
+
for (let turn = 0; turn < MAX_FORK_TURNS; turn++) {
|
|
783
|
+
const result = await aiService.callAgent({
|
|
784
|
+
gatewayConfig,
|
|
785
|
+
modelConfig,
|
|
786
|
+
messages: forkMessages,
|
|
787
|
+
sessionId,
|
|
788
|
+
abortSignal,
|
|
789
|
+
workdir,
|
|
790
|
+
tools: toolsConfig,
|
|
791
|
+
systemPrompt,
|
|
792
|
+
toolChoice: this.toolChoiceOverride,
|
|
793
|
+
// Stream so a slow reasoning model emits first bytes before the
|
|
794
|
+
// gateway's idle timeout fires (non-streaming waits for the full
|
|
795
|
+
// summary, which exceeds the timeout on large contexts).
|
|
796
|
+
stream: true,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
if (result.usage) {
|
|
800
|
+
totalUsage = {
|
|
801
|
+
prompt_tokens:
|
|
802
|
+
(totalUsage?.prompt_tokens ?? 0) + result.usage.prompt_tokens,
|
|
803
|
+
completion_tokens:
|
|
804
|
+
(totalUsage?.completion_tokens ?? 0) +
|
|
805
|
+
result.usage.completion_tokens,
|
|
806
|
+
total_tokens:
|
|
807
|
+
(totalUsage?.total_tokens ?? 0) + result.usage.total_tokens,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (result.content?.trim()) {
|
|
812
|
+
content = result.content;
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (result.tool_calls && result.tool_calls.length > 0) {
|
|
817
|
+
// Deny all tool calls locally and feed the rejections back so the
|
|
818
|
+
// model gets another turn to produce the summary text.
|
|
819
|
+
forkMessages.push({
|
|
820
|
+
role: "assistant",
|
|
821
|
+
content: result.content ?? null,
|
|
822
|
+
tool_calls: result.tool_calls,
|
|
823
|
+
});
|
|
824
|
+
for (const toolCall of result.tool_calls) {
|
|
825
|
+
forkMessages.push({
|
|
826
|
+
role: "tool",
|
|
827
|
+
tool_call_id: toolCall.id,
|
|
828
|
+
content: "Tool use is not allowed during compaction",
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Neither text nor tool calls: retrying the identical request is
|
|
835
|
+
// pointless, bail out and let the caller fail the compaction.
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return { content, usage: totalUsage };
|
|
840
|
+
}
|
|
841
|
+
|
|
667
842
|
/**
|
|
668
843
|
* Build post-compact context restoration content.
|
|
669
844
|
* Restores file reads, working directory, plan mode, skills, and background tasks.
|
|
@@ -829,7 +1004,7 @@ export class AIManager {
|
|
|
829
1004
|
// has superseded us. setIsLoading(true) here also closes the "idle but
|
|
830
1005
|
// previous turn not finished" race window by marking us busy immediately.
|
|
831
1006
|
this.turnGeneration++;
|
|
832
|
-
|
|
1007
|
+
let myGeneration = this.turnGeneration;
|
|
833
1008
|
this.setIsLoading(true);
|
|
834
1009
|
|
|
835
1010
|
outer: while (true) {
|
|
@@ -934,30 +1109,16 @@ export class AIManager {
|
|
|
934
1109
|
|
|
935
1110
|
logger?.debug("modelConfig in sendAIMessage", this.getModelConfig());
|
|
936
1111
|
|
|
937
|
-
const toolsConfig =
|
|
938
|
-
|
|
939
|
-
const filteredToolPlugins = this.toolManager
|
|
940
|
-
.getTools()
|
|
941
|
-
.filter((t) => toolNames.has(t.name));
|
|
942
|
-
|
|
943
|
-
let autoMemoryOptions:
|
|
944
|
-
| { directory: string; content: string }
|
|
945
|
-
| undefined;
|
|
946
|
-
|
|
947
|
-
if (this.getAutoMemoryEnabled()) {
|
|
948
|
-
const directory = this.memoryService.getAutoMemoryDirectory(
|
|
949
|
-
this.getWorkdir(),
|
|
950
|
-
);
|
|
951
|
-
const content = await this.memoryService.getAutoMemoryContent(
|
|
952
|
-
this.getWorkdir(),
|
|
953
|
-
);
|
|
954
|
-
autoMemoryOptions = { directory, content };
|
|
955
|
-
}
|
|
1112
|
+
const { toolsConfig, toolNames, filteredToolPlugins } =
|
|
1113
|
+
this.resolveFilteredTools();
|
|
956
1114
|
|
|
957
1115
|
// Get memory for message-array injection (not system prompt)
|
|
958
1116
|
const { prependContent } =
|
|
959
1117
|
await this.messageManager.getMemoryForInjection();
|
|
960
1118
|
|
|
1119
|
+
const mainSystemPrompt =
|
|
1120
|
+
await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
1121
|
+
|
|
961
1122
|
// Call AI service with streaming callbacks if enabled
|
|
962
1123
|
const callAgentOptions: CallAgentOptions = {
|
|
963
1124
|
gatewayConfig: this.getGatewayConfig(),
|
|
@@ -968,18 +1129,7 @@ export class AIManager {
|
|
|
968
1129
|
workdir: this.getWorkdir(), // Pass working directory
|
|
969
1130
|
tools: toolsConfig, // Pass filtered tool configuration
|
|
970
1131
|
model: model, // Use passed model
|
|
971
|
-
systemPrompt:
|
|
972
|
-
this.systemPrompt,
|
|
973
|
-
filteredToolPlugins,
|
|
974
|
-
{
|
|
975
|
-
workdir: this.getWorkdir(),
|
|
976
|
-
originalWorkdir: this.getOriginalWorkdir(),
|
|
977
|
-
language: this.getLanguage(),
|
|
978
|
-
isSubagent: !!this.subagentType,
|
|
979
|
-
worktreeSession: this.getWorktreeSession(),
|
|
980
|
-
autoMemory: autoMemoryOptions,
|
|
981
|
-
},
|
|
982
|
-
), // Pass custom system prompt
|
|
1132
|
+
systemPrompt: mainSystemPrompt, // Pass custom system prompt
|
|
983
1133
|
maxTokens: maxTokens, // Pass max tokens override
|
|
984
1134
|
toolChoice: this.toolChoiceOverride, // Pass tool_choice override
|
|
985
1135
|
};
|
|
@@ -988,7 +1138,7 @@ export class AIManager {
|
|
|
988
1138
|
if (prependContent.trim()) {
|
|
989
1139
|
callAgentOptions.messages.unshift({
|
|
990
1140
|
role: "user",
|
|
991
|
-
content:
|
|
1141
|
+
content: wrapInSystemReminder(prependContent),
|
|
992
1142
|
});
|
|
993
1143
|
}
|
|
994
1144
|
|
|
@@ -1503,6 +1653,14 @@ export class AIManager {
|
|
|
1503
1653
|
});
|
|
1504
1654
|
}
|
|
1505
1655
|
}
|
|
1656
|
+
// Re-assert loading state before restarting: if this turn was
|
|
1657
|
+
// aborted, abortAIMessage already reset loading to false, and the
|
|
1658
|
+
// restart below continues the conversation — the UI must show
|
|
1659
|
+
// streaming again (cursor, stop button, ESC handling).
|
|
1660
|
+
this.setIsLoading(true);
|
|
1661
|
+
// Adopt the current (possibly abort-bumped) generation so this
|
|
1662
|
+
// continued turn's end-of-turn cleanup is not skipped as superseded.
|
|
1663
|
+
myGeneration = this.turnGeneration;
|
|
1506
1664
|
// Restart outer loop to process the notifications
|
|
1507
1665
|
shouldRestart = true;
|
|
1508
1666
|
turnOffset = 0;
|