wave-agent-sdk 0.19.4 → 0.19.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 +21 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +21 -0
- package/dist/managers/aiManager.d.ts +15 -1
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +96 -2
- package/dist/managers/backgroundTaskManager.d.ts +1 -1
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
- package/dist/managers/backgroundTaskManager.js +4 -3
- package/dist/managers/bangManager.d.ts.map +1 -1
- package/dist/managers/bangManager.js +2 -1
- package/dist/managers/messageManager.d.ts +7 -0
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +64 -5
- package/dist/managers/messageQueue.d.ts +8 -0
- package/dist/managers/messageQueue.d.ts.map +1 -1
- package/dist/managers/messageQueue.js +13 -0
- package/dist/managers/permissionManager.d.ts.map +1 -1
- package/dist/managers/permissionManager.js +3 -3
- package/dist/managers/subagentManager.d.ts.map +1 -1
- package/dist/managers/subagentManager.js +7 -1
- package/dist/prompts/index.d.ts +3 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +2 -4
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +12 -0
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +7 -5
- package/dist/tools/enterWorktreeTool.d.ts.map +1 -1
- package/dist/tools/enterWorktreeTool.js +3 -8
- package/dist/tools/exitWorktreeTool.d.ts.map +1 -1
- package/dist/tools/exitWorktreeTool.js +5 -8
- package/dist/tools/grepTool.d.ts.map +1 -1
- package/dist/tools/grepTool.js +23 -1
- package/dist/types/hooks.d.ts +21 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +2 -0
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +3 -0
- package/dist/utils/shellResolver.d.ts.map +1 -1
- package/dist/utils/shellResolver.js +156 -7
- package/dist/utils/worktreeSession.d.ts +6 -6
- package/dist/utils/worktreeSession.d.ts.map +1 -1
- package/dist/utils/worktreeSession.js +7 -11
- package/package.json +1 -1
- package/src/agent.ts +31 -0
- package/src/managers/aiManager.ts +118 -2
- package/src/managers/backgroundTaskManager.ts +4 -2
- package/src/managers/bangManager.ts +2 -1
- package/src/managers/messageManager.ts +72 -6
- package/src/managers/messageQueue.ts +17 -0
- package/src/managers/permissionManager.ts +6 -3
- package/src/managers/subagentManager.ts +13 -1
- package/src/prompts/index.ts +4 -4
- package/src/services/hook.ts +16 -0
- package/src/tools/bashTool.ts +11 -5
- package/src/tools/enterWorktreeTool.ts +4 -14
- package/src/tools/exitWorktreeTool.ts +5 -13
- package/src/tools/grepTool.ts +25 -1
- package/src/types/hooks.ts +31 -0
- package/src/types/messaging.ts +2 -0
- package/src/utils/containerSetup.ts +4 -0
- package/src/utils/shellResolver.ts +167 -8
- package/src/utils/worktreeSession.ts +6 -16
package/package.json
CHANGED
package/src/agent.ts
CHANGED
|
@@ -42,6 +42,7 @@ import { TaskManager } from "./services/taskManager.js";
|
|
|
42
42
|
import { btw } from "./services/aiService.js";
|
|
43
43
|
import { convertMessagesForAPI } from "./utils/convertMessagesForAPI.js";
|
|
44
44
|
import { supportsVision } from "./utils/modelCapabilities.js";
|
|
45
|
+
import type { WorktreeSession } from "./utils/worktreeSession.js";
|
|
45
46
|
import { parseTaskNotificationXml } from "./utils/notificationXml.js";
|
|
46
47
|
import { InitializationService } from "./services/initializationService.js";
|
|
47
48
|
import { InteractionService } from "./services/interactionService.js";
|
|
@@ -298,6 +299,15 @@ export class Agent {
|
|
|
298
299
|
this.options.callbacks?.onWorkdirChange?.(newCwd);
|
|
299
300
|
}
|
|
300
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Set this session's worktree session state (used by CLI -w startup to inject the
|
|
304
|
+
* session created before the agent existed). Delegates to AIManager so the state is
|
|
305
|
+
* stored in this agent's own DI container.
|
|
306
|
+
*/
|
|
307
|
+
public setWorktreeSession(session: WorktreeSession | null): void {
|
|
308
|
+
this.aiManager.setWorktreeSession(session);
|
|
309
|
+
}
|
|
310
|
+
|
|
301
311
|
/** Get project memory content */
|
|
302
312
|
public get projectMemory(): string {
|
|
303
313
|
const memoryService =
|
|
@@ -389,6 +399,27 @@ export class Agent {
|
|
|
389
399
|
return removed;
|
|
390
400
|
}
|
|
391
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Update a queued message's content by its ID (does not change queue order)
|
|
404
|
+
* @param id - The ID of the message to update
|
|
405
|
+
* @param patch - The fields to update
|
|
406
|
+
* @returns true if the message was updated, false if not found
|
|
407
|
+
*/
|
|
408
|
+
public updateQueuedMessageById(
|
|
409
|
+
id: string,
|
|
410
|
+
patch: {
|
|
411
|
+
content?: string;
|
|
412
|
+
images?: Array<{ path: string; mimeType: string }>;
|
|
413
|
+
type?: "message" | "bang";
|
|
414
|
+
},
|
|
415
|
+
): boolean {
|
|
416
|
+
const updated = this.messageQueue.updateById(id, patch);
|
|
417
|
+
if (updated) {
|
|
418
|
+
this.options.callbacks?.onQueuedMessagesChange?.(this.queuedMessages);
|
|
419
|
+
}
|
|
420
|
+
return updated;
|
|
421
|
+
}
|
|
422
|
+
|
|
392
423
|
/**
|
|
393
424
|
* Unified dequeue trigger — checks state machine before processing.
|
|
394
425
|
* Called from onMessageEnqueued, onLoadingChange(false), and
|
|
@@ -26,8 +26,10 @@ import { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
|
|
|
26
26
|
|
|
27
27
|
import type { HookManager } from "./hookManager.js";
|
|
28
28
|
import type { ExtendedHookExecutionContext } from "../types/hooks.js";
|
|
29
|
+
import type { BackgroundTaskInfo, SessionCronInfo } from "../types/hooks.js";
|
|
29
30
|
import type { PermissionManager } from "./permissionManager.js";
|
|
30
31
|
import type { SubagentManager } from "./subagentManager.js";
|
|
32
|
+
import type { CronManager } from "./cronManager.js";
|
|
31
33
|
import type { SkillManager } from "./skillManager.js";
|
|
32
34
|
import { buildSystemPrompt } from "../prompts/index.js";
|
|
33
35
|
import {
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
buildExitedPlanModeReminder,
|
|
37
39
|
} from "../prompts/planModeReminders.js";
|
|
38
40
|
import { Container } from "../utils/container.js";
|
|
41
|
+
import type { WorktreeSession } from "../utils/worktreeSession.js";
|
|
39
42
|
import { recoverTruncatedJson } from "../utils/stringUtils.js";
|
|
40
43
|
import { ConfigurationService } from "../services/configurationService.js";
|
|
41
44
|
import type { NotificationQueue } from "./notificationQueue.js";
|
|
@@ -49,6 +52,47 @@ import {
|
|
|
49
52
|
resetTracingState,
|
|
50
53
|
} from "../telemetry/sessionTracing.js";
|
|
51
54
|
import { logOTelEvent } from "../telemetry/events.js";
|
|
55
|
+
import type { BackgroundTask } from "../types/processes.js";
|
|
56
|
+
|
|
57
|
+
// Truncate text to `max` chars and append a "… [+N chars]" marker when exceeded.
|
|
58
|
+
// Used for background_tasks description/command fields (≤1000 chars per spec FR-063).
|
|
59
|
+
function truncateWithMarker(text: string, max: number): string {
|
|
60
|
+
if (text.length <= max) return text;
|
|
61
|
+
return text.slice(0, max) + `… [+${text.length - max} chars]`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Map a BackgroundTask (from BackgroundTaskManager) to the Stop hook's
|
|
65
|
+
// background_tasks array element shape (aligned with Claude Code v2.1.145+).
|
|
66
|
+
// Conditional fields are populated based on task type:
|
|
67
|
+
// - shell: `command`
|
|
68
|
+
// - subagent: `agent_type` (looked up via SubagentManager by subagentId)
|
|
69
|
+
// - workflow: `name` (extracted from "Workflow: <name>" description)
|
|
70
|
+
function mapBackgroundTask(
|
|
71
|
+
task: BackgroundTask,
|
|
72
|
+
subagentManager: SubagentManager | undefined,
|
|
73
|
+
): BackgroundTaskInfo {
|
|
74
|
+
const base: BackgroundTaskInfo = {
|
|
75
|
+
id: task.id,
|
|
76
|
+
type: task.type,
|
|
77
|
+
status: task.status,
|
|
78
|
+
description: truncateWithMarker(task.description ?? "", 1000),
|
|
79
|
+
};
|
|
80
|
+
if (task.type === "shell") {
|
|
81
|
+
return { ...base, command: truncateWithMarker(task.command ?? "", 1000) };
|
|
82
|
+
}
|
|
83
|
+
if (task.type === "subagent") {
|
|
84
|
+
const instance = subagentManager
|
|
85
|
+
?.getActiveInstances()
|
|
86
|
+
.find((i) => i.subagentId === task.subagentId);
|
|
87
|
+
return { ...base, agent_type: instance?.subagentType ?? "" };
|
|
88
|
+
}
|
|
89
|
+
// workflow
|
|
90
|
+
const prefix = "Workflow: ";
|
|
91
|
+
const name = task.description?.startsWith(prefix)
|
|
92
|
+
? task.description.slice(prefix.length)
|
|
93
|
+
: (task.description ?? "");
|
|
94
|
+
return { ...base, name };
|
|
95
|
+
}
|
|
52
96
|
|
|
53
97
|
export interface AIManagerCallbacks {
|
|
54
98
|
onCompactionStateChange?: (isCompacting: boolean) => void;
|
|
@@ -217,11 +261,31 @@ export class AIManager {
|
|
|
217
261
|
|
|
218
262
|
/**
|
|
219
263
|
* Update the working directory mid-session (e.g., when entering/exiting a worktree).
|
|
220
|
-
*
|
|
264
|
+
* Only updates this session's DI container; it does NOT change the process-level
|
|
265
|
+
* process.cwd(), so concurrent sessions in the same stdio process are unaffected.
|
|
266
|
+
* Triggers `_onCwdChange` (wired by Agent to `onWorkdirChange`) so the host is
|
|
267
|
+
* notified of worktree switches. Unlike bash `cd`, this does NOT run CwdChanged
|
|
268
|
+
* hooks (worktree has its own WorktreeCreate/WorktreeRemove hooks).
|
|
221
269
|
*/
|
|
222
270
|
public setWorkdir(newWorkdir: string): void {
|
|
223
271
|
this.container.register("Workdir", newWorkdir);
|
|
224
|
-
|
|
272
|
+
this._onCwdChange?.(newWorkdir);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get this session's worktree session state (null if not in a worktree).
|
|
277
|
+
*/
|
|
278
|
+
public getWorktreeSession(): WorktreeSession | null {
|
|
279
|
+
return (
|
|
280
|
+
this.container.get<WorktreeSession | null>("WorktreeSession") ?? null
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Set this session's worktree session state.
|
|
286
|
+
*/
|
|
287
|
+
public setWorktreeSession(session: WorktreeSession | null): void {
|
|
288
|
+
this.container.register("WorktreeSession", session);
|
|
225
289
|
}
|
|
226
290
|
|
|
227
291
|
public setOnCwdChange(callback: (newCwd: string) => void): void {
|
|
@@ -734,6 +798,10 @@ export class AIManager {
|
|
|
734
798
|
return this.container.get<SubagentManager>("SubagentManager");
|
|
735
799
|
}
|
|
736
800
|
|
|
801
|
+
private get cronManager(): CronManager | undefined {
|
|
802
|
+
return this.container.get<CronManager>("CronManager");
|
|
803
|
+
}
|
|
804
|
+
|
|
737
805
|
private get skillManager(): SkillManager | undefined {
|
|
738
806
|
return this.container.get<SkillManager>("SkillManager");
|
|
739
807
|
}
|
|
@@ -895,6 +963,7 @@ export class AIManager {
|
|
|
895
963
|
originalWorkdir: this.getOriginalWorkdir(),
|
|
896
964
|
language: this.getLanguage(),
|
|
897
965
|
isSubagent: !!this.subagentType,
|
|
966
|
+
worktreeSession: this.getWorktreeSession(),
|
|
898
967
|
autoMemory: autoMemoryOptions,
|
|
899
968
|
},
|
|
900
969
|
), // Pass custom system prompt
|
|
@@ -1279,6 +1348,16 @@ export class AIManager {
|
|
|
1279
1348
|
message: error instanceof Error ? error.message : String(error),
|
|
1280
1349
|
}).catch(() => {}); // Non-blocking
|
|
1281
1350
|
|
|
1351
|
+
// Finalize any streaming text/reasoning blocks so the UI stops ticking
|
|
1352
|
+
// its in-progress timer (e.g. when the request is aborted mid-thought).
|
|
1353
|
+
this.messageManager.finalizeStreamingBlocks();
|
|
1354
|
+
|
|
1355
|
+
// Finalize any tool blocks stuck in start/streaming/running so the UI
|
|
1356
|
+
// stops showing the yellow "running" spinner (e.g. abort mid-tool-stream).
|
|
1357
|
+
this.messageManager.finalizeAbortedToolBlocks(
|
|
1358
|
+
error instanceof Error ? error.message : undefined,
|
|
1359
|
+
);
|
|
1360
|
+
|
|
1282
1361
|
this.messageManager.addErrorBlock(
|
|
1283
1362
|
error instanceof Error ? error.message : "Unknown error occurred",
|
|
1284
1363
|
);
|
|
@@ -1435,6 +1514,40 @@ export class AIManager {
|
|
|
1435
1514
|
// Use "SubagentStop" hook name when triggered by a subagent, otherwise use "Stop"
|
|
1436
1515
|
const hookName = this.subagentType ? "SubagentStop" : "Stop";
|
|
1437
1516
|
|
|
1517
|
+
// For main-agent Stop events, snapshot running background tasks and
|
|
1518
|
+
// session cron jobs so the hook can decide whether to block stopping.
|
|
1519
|
+
// SubagentStop does not include these fields (per spec FR-065).
|
|
1520
|
+
const backgroundTasks: BackgroundTaskInfo[] | undefined =
|
|
1521
|
+
hookName === "Stop"
|
|
1522
|
+
? (this.backgroundTaskManager?.getAllTasks() ?? [])
|
|
1523
|
+
.filter((t) => t.status === "running")
|
|
1524
|
+
.map((t) => mapBackgroundTask(t, this.subagentManager))
|
|
1525
|
+
: undefined;
|
|
1526
|
+
|
|
1527
|
+
const sessionCrons: SessionCronInfo[] | undefined =
|
|
1528
|
+
hookName === "Stop"
|
|
1529
|
+
? (this.cronManager?.listJobs() ?? []).map((j) => ({
|
|
1530
|
+
id: j.id,
|
|
1531
|
+
schedule: j.cron,
|
|
1532
|
+
recurring: j.recurring,
|
|
1533
|
+
prompt: j.prompt,
|
|
1534
|
+
}))
|
|
1535
|
+
: undefined;
|
|
1536
|
+
|
|
1537
|
+
// Extract text content from the last assistant message so hooks can
|
|
1538
|
+
// inspect the final response without reading the transcript file.
|
|
1539
|
+
const allMessages = this.messageManager.getMessages();
|
|
1540
|
+
const lastAssistant = [...allMessages]
|
|
1541
|
+
.reverse()
|
|
1542
|
+
.find((m) => m.role === "assistant");
|
|
1543
|
+
const lastAssistantText = lastAssistant
|
|
1544
|
+
? lastAssistant.blocks
|
|
1545
|
+
.filter((b) => b.type === "text")
|
|
1546
|
+
.map((b) => b.content)
|
|
1547
|
+
.join("\n")
|
|
1548
|
+
.trim() || undefined
|
|
1549
|
+
: undefined;
|
|
1550
|
+
|
|
1438
1551
|
const context: ExtendedHookExecutionContext = {
|
|
1439
1552
|
event: hookName,
|
|
1440
1553
|
projectDir: this.getWorkdir(),
|
|
@@ -1443,6 +1556,9 @@ export class AIManager {
|
|
|
1443
1556
|
transcriptPath: this.messageManager.getTranscriptPath(),
|
|
1444
1557
|
cwd: this.getWorkdir(),
|
|
1445
1558
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
1559
|
+
backgroundTasks, // Stop-only: running background tasks snapshot
|
|
1560
|
+
sessionCrons, // Stop-only: session cron jobs snapshot
|
|
1561
|
+
lastAssistantMessage: lastAssistantText, // Stop/SubagentStop: last assistant message text
|
|
1446
1562
|
// Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
|
|
1447
1563
|
env: Object.fromEntries(
|
|
1448
1564
|
Object.entries(process.env).filter((e) => e[1] !== undefined),
|
|
@@ -7,6 +7,7 @@ import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
|
7
7
|
import { logger } from "../utils/globalLogger.js";
|
|
8
8
|
import { Container } from "../utils/container.js";
|
|
9
9
|
import { NotificationQueue } from "./notificationQueue.js";
|
|
10
|
+
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
10
11
|
|
|
11
12
|
export interface BackgroundTaskManagerCallbacks {
|
|
12
13
|
onBackgroundTasksChange?: (tasks: BackgroundTask[]) => void;
|
|
@@ -64,15 +65,16 @@ export class BackgroundTaskManager {
|
|
|
64
65
|
public startShell(
|
|
65
66
|
command: string,
|
|
66
67
|
timeout?: number,
|
|
68
|
+
cwd?: string,
|
|
67
69
|
): { id: string; child: ChildProcess; detach: () => void } {
|
|
68
70
|
const id = this.generateId();
|
|
69
71
|
const startTime = Date.now();
|
|
70
72
|
|
|
71
73
|
const child = spawn(command, {
|
|
72
|
-
shell: true,
|
|
74
|
+
shell: resolveShellPath() ?? true,
|
|
73
75
|
stdio: "pipe",
|
|
74
76
|
detached: true,
|
|
75
|
-
cwd: this.workdir,
|
|
77
|
+
cwd: cwd ?? this.workdir,
|
|
76
78
|
env: {
|
|
77
79
|
...process.env,
|
|
78
80
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawn, type ChildProcess } from "child_process";
|
|
2
2
|
import type { MessageManager } from "./messageManager.js";
|
|
3
3
|
import { Container } from "../utils/container.js";
|
|
4
|
+
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
4
5
|
|
|
5
6
|
export interface BangManagerOptions {
|
|
6
7
|
workdir: string;
|
|
@@ -45,7 +46,7 @@ export class BangManager {
|
|
|
45
46
|
|
|
46
47
|
return new Promise<number>((resolve) => {
|
|
47
48
|
const child = spawn(command, {
|
|
48
|
-
shell: true,
|
|
49
|
+
shell: resolveShellPath() ?? true,
|
|
49
50
|
stdio: "pipe",
|
|
50
51
|
cwd: this.workdir,
|
|
51
52
|
env: {
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
type AddNotificationMessageParams,
|
|
17
17
|
generateMessageId,
|
|
18
18
|
} from "../utils/messageOperations.js";
|
|
19
|
-
import type { Message, Usage } from "../types/index.js";
|
|
19
|
+
import type { Message, Usage, ToolBlock } from "../types/index.js";
|
|
20
20
|
import { getLastApiRounds } from "../utils/groupMessagesByApiRound.js";
|
|
21
21
|
import { join, isAbsolute, relative } from "path";
|
|
22
22
|
import {
|
|
@@ -718,12 +718,23 @@ export class MessageManager {
|
|
|
718
718
|
type: "text" | "reasoning";
|
|
719
719
|
content: string;
|
|
720
720
|
stage?: string;
|
|
721
|
+
startTime?: number;
|
|
721
722
|
};
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
723
|
+
if (block.type === "reasoning") {
|
|
724
|
+
lastMessage.blocks[index] = {
|
|
725
|
+
type: "reasoning",
|
|
726
|
+
content: block.content,
|
|
727
|
+
stage: "end" as const,
|
|
728
|
+
startTime: block.startTime,
|
|
729
|
+
endTime: Date.now(),
|
|
730
|
+
};
|
|
731
|
+
} else {
|
|
732
|
+
lastMessage.blocks[index] = {
|
|
733
|
+
type: block.type,
|
|
734
|
+
content: block.content,
|
|
735
|
+
stage: "end" as const,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
727
738
|
|
|
728
739
|
// Fire incremental callback to signal finalization
|
|
729
740
|
const callbackParams = {
|
|
@@ -832,10 +843,16 @@ export class MessageManager {
|
|
|
832
843
|
|
|
833
844
|
if (reasoningBlockIndex >= 0) {
|
|
834
845
|
// Update existing reasoning block
|
|
846
|
+
const existingStartTime = (
|
|
847
|
+
lastMessage.blocks[reasoningBlockIndex] as {
|
|
848
|
+
startTime?: number;
|
|
849
|
+
}
|
|
850
|
+
).startTime;
|
|
835
851
|
lastMessage.blocks[reasoningBlockIndex] = {
|
|
836
852
|
type: "reasoning",
|
|
837
853
|
content: newAccumulatedReasoning,
|
|
838
854
|
stage: "streaming",
|
|
855
|
+
startTime: existingStartTime,
|
|
839
856
|
};
|
|
840
857
|
} else {
|
|
841
858
|
// Add new reasoning block if none exists
|
|
@@ -843,6 +860,7 @@ export class MessageManager {
|
|
|
843
860
|
type: "reasoning",
|
|
844
861
|
content: newAccumulatedReasoning,
|
|
845
862
|
stage: "streaming",
|
|
863
|
+
startTime: Date.now(),
|
|
846
864
|
});
|
|
847
865
|
}
|
|
848
866
|
|
|
@@ -878,6 +896,54 @@ export class MessageManager {
|
|
|
878
896
|
}
|
|
879
897
|
}
|
|
880
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Finalize any tool blocks still in a non-terminal stage (start/streaming/running)
|
|
901
|
+
* by marking them as ended with an error. Called when the AI call is aborted or
|
|
902
|
+
* fails mid-tool-stream so the UI stops showing the "running" spinner.
|
|
903
|
+
* Fires onToolBlockUpdated for each finalized tool block.
|
|
904
|
+
*/
|
|
905
|
+
public finalizeAbortedToolBlocks(error?: string): void {
|
|
906
|
+
if (this.messages.length === 0) return;
|
|
907
|
+
const lastMessage = this.messages[this.messages.length - 1];
|
|
908
|
+
if (lastMessage.role !== "assistant") return;
|
|
909
|
+
|
|
910
|
+
const errorMessage = error ?? "Tool execution was aborted";
|
|
911
|
+
let finalized = false;
|
|
912
|
+
|
|
913
|
+
for (let i = 0; i < lastMessage.blocks.length; i++) {
|
|
914
|
+
const block = lastMessage.blocks[i] as ToolBlock;
|
|
915
|
+
if (block.type !== "tool" || block.stage === "end") continue;
|
|
916
|
+
|
|
917
|
+
const timestamp = Date.now();
|
|
918
|
+
lastMessage.blocks[i] = {
|
|
919
|
+
...block,
|
|
920
|
+
stage: "end",
|
|
921
|
+
success: false,
|
|
922
|
+
error: errorMessage,
|
|
923
|
+
timestamp,
|
|
924
|
+
};
|
|
925
|
+
finalized = true;
|
|
926
|
+
|
|
927
|
+
this.callbacks.onToolBlockUpdated?.({
|
|
928
|
+
id: block.id ?? "",
|
|
929
|
+
messageId: lastMessage.id ?? "",
|
|
930
|
+
name: block.name,
|
|
931
|
+
parameters: block.parameters,
|
|
932
|
+
parametersChunk: block.parametersChunk,
|
|
933
|
+
compactParams: block.compactParams,
|
|
934
|
+
stage: "end",
|
|
935
|
+
success: false,
|
|
936
|
+
error: errorMessage,
|
|
937
|
+
isManuallyBackgrounded: block.isManuallyBackgrounded,
|
|
938
|
+
timestamp,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (finalized) {
|
|
943
|
+
this.setMessages([...this.messages]);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
881
947
|
/**
|
|
882
948
|
* Remove the last user message from the conversation
|
|
883
949
|
* Used for hook error handling when the user prompt needs to be erased
|
|
@@ -74,6 +74,23 @@ export class MessageQueue {
|
|
|
74
74
|
return true;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
updateById(
|
|
78
|
+
id: string,
|
|
79
|
+
patch: {
|
|
80
|
+
content?: string;
|
|
81
|
+
images?: Array<{ path: string; mimeType: string }>;
|
|
82
|
+
type?: "message" | "bang";
|
|
83
|
+
},
|
|
84
|
+
): boolean {
|
|
85
|
+
const m = this.queue.find((x) => x.id === id);
|
|
86
|
+
if (!m) return false;
|
|
87
|
+
if (patch.content !== undefined) m.content = patch.content;
|
|
88
|
+
if (patch.images !== undefined) m.images = patch.images;
|
|
89
|
+
if (patch.type !== undefined) m.type = patch.type;
|
|
90
|
+
this.onMessageEnqueued?.();
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
popLastEditable(): QueuedMessage | null {
|
|
78
95
|
for (let i = this.queue.length - 1; i >= 0; i--) {
|
|
79
96
|
if (this.queue[i].editable !== false) {
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
} from "../constants/tools.js";
|
|
36
36
|
import { Container } from "../utils/container.js";
|
|
37
37
|
import { ConfigurationService } from "../services/configurationService.js";
|
|
38
|
-
import {
|
|
38
|
+
import type { WorktreeSession } from "../utils/worktreeSession.js";
|
|
39
39
|
|
|
40
40
|
const SAFE_COMMANDS = [
|
|
41
41
|
"cd",
|
|
@@ -452,8 +452,11 @@ export class PermissionManager {
|
|
|
452
452
|
}
|
|
453
453
|
|
|
454
454
|
// 1.0 Check worktree safety for Write and Edit tools
|
|
455
|
-
// Support both CLI -w sessions (container-registered) and EnterWorktree mid-session
|
|
456
|
-
|
|
455
|
+
// Support both CLI -w sessions (container-registered) and EnterWorktree mid-session
|
|
456
|
+
// (per-agent WorktreeSession stored in this session's container)
|
|
457
|
+
const worktreeSession = this.container.get<WorktreeSession | null>(
|
|
458
|
+
"WorktreeSession",
|
|
459
|
+
);
|
|
457
460
|
const effectiveWorktreeName =
|
|
458
461
|
this.worktreeName || worktreeSession?.worktreeName;
|
|
459
462
|
const effectiveWorkdir = this.workdir || worktreeSession?.worktreePath;
|
|
@@ -7,7 +7,13 @@ import type { Message, Usage } from "../types/index.js";
|
|
|
7
7
|
import { AIManager } from "./aiManager.js";
|
|
8
8
|
import { MessageManager } from "./messageManager.js";
|
|
9
9
|
import { ToolManager } from "./toolManager.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
AGENT_TOOL_NAME,
|
|
12
|
+
TASK_CREATE_TOOL_NAME,
|
|
13
|
+
TASK_GET_TOOL_NAME,
|
|
14
|
+
TASK_LIST_TOOL_NAME,
|
|
15
|
+
TASK_UPDATE_TOOL_NAME,
|
|
16
|
+
} from "../constants/tools.js";
|
|
11
17
|
import {
|
|
12
18
|
addConsolidatedAbortListener,
|
|
13
19
|
createAbortPromise,
|
|
@@ -321,6 +327,12 @@ export class SubagentManager {
|
|
|
321
327
|
instanceDeniedRules: [
|
|
322
328
|
...(parentPermissionManager?.getInstanceDeniedRules?.() || []),
|
|
323
329
|
AGENT_TOOL_NAME, // Always deny Agent tool in subagents to prevent recursion
|
|
330
|
+
// Deny Task tools in subagents — they share the parent's TaskManager
|
|
331
|
+
// and could mutate the main agent's task list
|
|
332
|
+
TASK_CREATE_TOOL_NAME,
|
|
333
|
+
TASK_GET_TOOL_NAME,
|
|
334
|
+
TASK_UPDATE_TOOL_NAME,
|
|
335
|
+
TASK_LIST_TOOL_NAME,
|
|
324
336
|
],
|
|
325
337
|
additionalDirectories:
|
|
326
338
|
parentPermissionManager?.getAdditionalDirectories(),
|
package/src/prompts/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import { ToolPlugin } from "../tools/types.js";
|
|
3
3
|
import { isGitRepository } from "../utils/gitUtils.js";
|
|
4
|
-
import {
|
|
4
|
+
import type { WorktreeSession } from "../utils/worktreeSession.js";
|
|
5
5
|
import { buildAutoMemoryPrompt } from "./autoMemory.js";
|
|
6
6
|
import {
|
|
7
7
|
EXPLORE_SUBAGENT_TYPE,
|
|
@@ -252,6 +252,7 @@ export function buildSystemPrompt(
|
|
|
252
252
|
originalWorkdir?: string;
|
|
253
253
|
language?: string;
|
|
254
254
|
isSubagent?: boolean;
|
|
255
|
+
worktreeSession?: WorktreeSession | null;
|
|
255
256
|
autoMemory?: {
|
|
256
257
|
directory: string;
|
|
257
258
|
content: string;
|
|
@@ -292,7 +293,7 @@ export function buildSystemPrompt(
|
|
|
292
293
|
? "bash"
|
|
293
294
|
: shell;
|
|
294
295
|
|
|
295
|
-
const worktreeSession =
|
|
296
|
+
const worktreeSession = options.worktreeSession;
|
|
296
297
|
|
|
297
298
|
dynamicText += `
|
|
298
299
|
|
|
@@ -326,6 +327,7 @@ export function enhanceSystemPromptWithEnvDetails(
|
|
|
326
327
|
existingSystemPrompt: string,
|
|
327
328
|
workdir: string,
|
|
328
329
|
originalWorkdir?: string,
|
|
330
|
+
worktreeSession?: WorktreeSession | null,
|
|
329
331
|
): string {
|
|
330
332
|
const isGitRepo = isGitRepository(workdir);
|
|
331
333
|
const platform = os.platform();
|
|
@@ -338,8 +340,6 @@ export function enhanceSystemPromptWithEnvDetails(
|
|
|
338
340
|
? "bash"
|
|
339
341
|
: shell;
|
|
340
342
|
|
|
341
|
-
const worktreeSession = getCurrentWorktreeSession();
|
|
342
|
-
|
|
343
343
|
const notes = `Notes:
|
|
344
344
|
- Agent threads always have their cwd reset between bash calls, as a result please only use absolute file paths.${worktreeSession ? `\n- You are in a git worktree at ${worktreeSession.worktreePath} (branch: ${worktreeSession.worktreeBranch}). Absolute paths from prior context may refer to the original repo at ${worktreeSession.originalCwd}; translate them to your worktree. Do NOT edit files outside this worktree.` : ""}
|
|
345
345
|
- In your final response, share file paths (always absolute, never relative) that are relevant to the task. Include code snippets only when the exact text is load-bearing (e.g., a bug you found, a function signature the caller asked for) — do not recap code you merely read.
|
package/src/services/hook.ts
CHANGED
|
@@ -113,6 +113,22 @@ async function buildHookJsonInput(
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// Add background_tasks and session_crons for Stop events only (not SubagentStop).
|
|
117
|
+
// Fields are always present for Stop (empty arrays when nothing in flight).
|
|
118
|
+
if (context.event === "Stop") {
|
|
119
|
+
jsonInput.background_tasks = context.backgroundTasks ?? [];
|
|
120
|
+
jsonInput.session_crons = context.sessionCrons ?? [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Add last_assistant_message for Stop and SubagentStop events.
|
|
124
|
+
// Omitted when undefined (no text content in last assistant message).
|
|
125
|
+
if (
|
|
126
|
+
(context.event === "Stop" || context.event === "SubagentStop") &&
|
|
127
|
+
context.lastAssistantMessage !== undefined
|
|
128
|
+
) {
|
|
129
|
+
jsonInput.last_assistant_message = context.lastAssistantMessage;
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
return jsonInput;
|
|
117
133
|
}
|
|
118
134
|
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -173,14 +173,16 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
// Resolve shell path: on Windows, use Git Bash; on
|
|
176
|
+
// Resolve shell path: on Windows, use Git Bash; on macOS/Linux, use bash or zsh
|
|
177
177
|
const shellPath = resolveShellPath();
|
|
178
|
-
if (
|
|
178
|
+
if (!shellPath) {
|
|
179
179
|
return {
|
|
180
180
|
success: false,
|
|
181
181
|
content: "",
|
|
182
182
|
error:
|
|
183
|
-
|
|
183
|
+
process.platform === "win32"
|
|
184
|
+
? "Git Bash not found. Please install Git for Windows or set WAVE_GIT_BASH_PATH environment variable."
|
|
185
|
+
: "No suitable shell found. Please ensure bash or zsh is installed, or set WAVE_SHELL environment variable.",
|
|
184
186
|
};
|
|
185
187
|
}
|
|
186
188
|
|
|
@@ -242,7 +244,11 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
242
244
|
};
|
|
243
245
|
}
|
|
244
246
|
|
|
245
|
-
const { id: taskId } = backgroundTaskManager.startShell(
|
|
247
|
+
const { id: taskId } = backgroundTaskManager.startShell(
|
|
248
|
+
command,
|
|
249
|
+
undefined,
|
|
250
|
+
context.workdir,
|
|
251
|
+
);
|
|
246
252
|
const task = backgroundTaskManager.getTask(taskId);
|
|
247
253
|
const outputPath = task?.outputPath;
|
|
248
254
|
const backgroundMsg = [
|
|
@@ -273,7 +279,7 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
273
279
|
);
|
|
274
280
|
|
|
275
281
|
const child: ChildProcess = spawn(wrappedCommand, {
|
|
276
|
-
shell: shellPath
|
|
282
|
+
shell: shellPath,
|
|
277
283
|
stdio: "pipe",
|
|
278
284
|
detached: true,
|
|
279
285
|
cwd: context.workdir,
|
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
7
|
-
import {
|
|
8
|
-
getCurrentWorktreeSession,
|
|
9
|
-
setCurrentWorktreeSession,
|
|
10
|
-
type WorktreeSession,
|
|
11
|
-
} from "../utils/worktreeSession.js";
|
|
7
|
+
import { type WorktreeSession } from "../utils/worktreeSession.js";
|
|
12
8
|
import {
|
|
13
9
|
createWorktree,
|
|
14
10
|
validateWorktreeName,
|
|
@@ -72,7 +68,7 @@ export const enterWorktreeTool: ToolPlugin = {
|
|
|
72
68
|
context: ToolContext,
|
|
73
69
|
): Promise<ToolResult> {
|
|
74
70
|
// Validate not already in a worktree created by this session
|
|
75
|
-
if (
|
|
71
|
+
if (context.aiManager?.getWorktreeSession()) {
|
|
76
72
|
return {
|
|
77
73
|
success: false,
|
|
78
74
|
content:
|
|
@@ -119,19 +115,13 @@ export const enterWorktreeTool: ToolPlugin = {
|
|
|
119
115
|
originalHeadCommit: worktreeInfo.originalHeadCommit,
|
|
120
116
|
};
|
|
121
117
|
|
|
122
|
-
// Set
|
|
123
|
-
setCurrentWorktreeSession(session);
|
|
124
|
-
|
|
125
|
-
// Update CWD via AIManager
|
|
118
|
+
// Set per-session worktree state and update CWD via AIManager
|
|
126
119
|
const aiManager = context.aiManager;
|
|
127
120
|
if (aiManager) {
|
|
121
|
+
aiManager.setWorktreeSession(session);
|
|
128
122
|
aiManager.setWorkdir(worktreeInfo.path);
|
|
129
123
|
}
|
|
130
124
|
|
|
131
|
-
// Also update the container's Workdir entry
|
|
132
|
-
// (Container is not directly accessible from ToolContext, but AIManager.setWorkdir
|
|
133
|
-
// handles both its internal field and process.chdir)
|
|
134
|
-
|
|
135
125
|
// Trigger WorktreeCreate hook if worktree is new
|
|
136
126
|
let hookTriggered = false;
|
|
137
127
|
if (session.isNew && context.hookManager) {
|