wave-agent-sdk 1.1.5 → 1.3.0
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 +128 -28
- package/dist/agent.js +201 -49
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/plugins.js +11 -20
- package/dist/builtin/skills/settings.js +7 -20
- package/dist/builtin/skills/wave-daemon.d.ts +1 -0
- package/dist/builtin/skills/wave-daemon.js +194 -0
- package/dist/constants/images.d.ts +26 -0
- package/dist/constants/images.js +26 -0
- package/dist/constants/index.d.ts +16 -0
- package/dist/constants/index.js +16 -0
- package/dist/constants/memory.d.ts +26 -0
- package/dist/constants/memory.js +34 -0
- package/dist/constants/messages.d.ts +11 -0
- package/dist/constants/messages.js +11 -0
- package/dist/constants/plugins.d.ts +8 -0
- package/dist/constants/plugins.js +8 -0
- package/dist/constants/tools.d.ts +1 -0
- package/dist/constants/tools.js +1 -0
- package/dist/core/plugin.d.ts +54 -10
- package/dist/core/plugin.js +137 -23
- package/dist/core/session.d.ts +1 -1
- package/dist/core/session.js +1 -1
- package/dist/exec/catalog.d.ts +140 -0
- package/dist/exec/catalog.js +470 -0
- package/dist/exec/catalogAnnouncement.d.ts +89 -0
- package/dist/exec/catalogAnnouncement.js +293 -0
- package/dist/exec/constants.d.ts +51 -0
- package/dist/exec/constants.js +51 -0
- package/dist/exec/execRuntime.d.ts +55 -0
- package/dist/exec/execRuntime.js +217 -0
- package/dist/exec/workerSource.d.ts +28 -0
- package/dist/exec/workerSource.js +299 -0
- package/dist/host/index.d.ts +23 -0
- package/dist/host/index.js +23 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +8 -1
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -25
- package/dist/managers/aiManager.js +204 -202
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +18 -0
- package/dist/managers/hookManager.js +37 -3
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +106 -11
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +68 -10
- package/dist/managers/mcpManager.js +265 -15
- package/dist/managers/messageManager.d.ts +60 -18
- package/dist/managers/messageManager.js +170 -81
- package/dist/managers/permissionManager.d.ts +69 -0
- package/dist/managers/permissionManager.js +221 -78
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- package/dist/managers/pluginManager.d.ts +46 -2
- package/dist/managers/pluginManager.js +117 -11
- package/dist/managers/pluginScopeManager.d.ts +15 -2
- package/dist/managers/pluginScopeManager.js +20 -1
- package/dist/managers/skillManager.d.ts +50 -0
- package/dist/managers/skillManager.js +166 -12
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +44 -31
- package/dist/managers/subagentManager.d.ts +15 -0
- package/dist/managers/subagentManager.js +81 -9
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- package/dist/managers/workflowManager.js +6 -0
- package/dist/prompts/autoMemory.d.ts +9 -0
- package/dist/prompts/autoMemory.js +30 -31
- package/dist/prompts/autoMemoryExtraction.d.ts +4 -0
- package/dist/prompts/autoMemoryExtraction.js +8 -111
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/prompts/memoryTypes.d.ts +63 -0
- package/dist/prompts/memoryTypes.js +191 -0
- package/dist/services/GitService.d.ts +7 -0
- package/dist/services/GitService.js +23 -0
- package/dist/services/MarketplaceService.d.ts +101 -17
- package/dist/services/MarketplaceService.js +323 -102
- package/dist/services/artifactContent.d.ts +84 -0
- package/dist/services/artifactContent.js +204 -0
- package/dist/services/artifactSession.d.ts +6 -0
- package/dist/services/artifactSession.js +17 -0
- package/dist/services/autoMemoryService.js +5 -13
- package/dist/services/configurationService.d.ts +92 -9
- package/dist/services/configurationService.js +246 -64
- package/dist/services/contentSummarizer.d.ts +15 -0
- package/dist/services/contentSummarizer.js +45 -0
- package/dist/services/execAvailability.d.ts +9 -0
- package/dist/services/execAvailability.js +32 -0
- package/dist/services/fileWatcher.js +61 -6
- package/dist/services/initializationService.js +21 -17
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +98 -0
- package/dist/services/jsonlHandler.js +250 -12
- package/dist/services/memory.d.ts +17 -1
- package/dist/services/memory.js +44 -7
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +290 -0
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +20 -6
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +174 -16
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +12 -0
- package/dist/stdio/index.js +12 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +97 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +229 -0
- package/dist/stdio/stdioAgent.js +360 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +10 -6
- package/dist/tools/editTool.js +6 -3
- package/dist/tools/execTool.d.ts +2 -0
- package/dist/tools/execTool.js +165 -0
- package/dist/tools/exitPlanMode.js +10 -2
- package/dist/tools/grepTool.js +7 -1
- package/dist/tools/readTool.js +30 -2
- package/dist/tools/types.d.ts +34 -8
- package/dist/tools/webFetchTool.js +15 -166
- package/dist/tools/workflowTool.js +40 -8
- package/dist/tools/writeTool.js +6 -3
- package/dist/types/agent.d.ts +24 -1
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +45 -2
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +42 -0
- package/dist/types/messaging.d.ts +1 -8
- package/dist/types/permissions.d.ts +22 -0
- package/dist/types/permissions.js +17 -0
- package/dist/types/plugins.d.ts +26 -2
- package/dist/types/skills.d.ts +26 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +48 -6
- package/dist/utils/convertMessagesForAPI.d.ts +7 -1
- package/dist/utils/convertMessagesForAPI.js +64 -14
- package/dist/utils/fileChangeReminder.d.ts +20 -0
- package/dist/utils/fileChangeReminder.js +153 -0
- package/dist/utils/fileSearch.js +4 -3
- package/dist/utils/fileUtils.d.ts +44 -0
- package/dist/utils/fileUtils.js +118 -0
- package/dist/utils/frontmatterYaml.d.ts +33 -0
- package/dist/utils/frontmatterYaml.js +192 -0
- package/dist/utils/imageBudget.d.ts +85 -0
- package/dist/utils/imageBudget.js +109 -0
- package/dist/utils/imageDimensions.d.ts +83 -0
- package/dist/utils/imageDimensions.js +232 -0
- package/dist/utils/imageProcessor.d.ts +66 -0
- package/dist/utils/imageProcessor.js +84 -0
- package/dist/utils/imageRewrite.d.ts +29 -0
- package/dist/utils/imageRewrite.js +251 -0
- package/dist/utils/markdownParser.d.ts +5 -1
- package/dist/utils/markdownParser.js +9 -51
- package/dist/utils/mcpInstructions.d.ts +61 -0
- package/dist/utils/mcpInstructions.js +126 -0
- package/dist/utils/mcpUtils.d.ts +7 -0
- package/dist/utils/mcpUtils.js +11 -2
- package/dist/utils/memoryAge.d.ts +32 -0
- package/dist/utils/memoryAge.js +47 -0
- package/dist/utils/memoryEntrypoint.d.ts +20 -0
- package/dist/utils/memoryEntrypoint.js +49 -0
- package/dist/utils/memoryIndex.d.ts +30 -0
- package/dist/utils/memoryIndex.js +76 -0
- package/dist/utils/messageOperations.d.ts +6 -20
- package/dist/utils/messageOperations.js +40 -91
- package/dist/utils/nestedMemory.d.ts +22 -0
- package/dist/utils/nestedMemory.js +61 -0
- package/dist/utils/npmTarball.d.ts +19 -0
- package/dist/utils/npmTarball.js +92 -0
- package/dist/utils/pluginSource.d.ts +37 -0
- package/dist/utils/pluginSource.js +73 -0
- package/dist/utils/ripgrep.d.ts +18 -4
- package/dist/utils/ripgrep.js +56 -4
- package/dist/utils/runtimeDeps.d.ts +35 -0
- package/dist/utils/runtimeDeps.js +426 -0
- package/dist/utils/skillParser.js +22 -52
- package/dist/utils/subagentParser.js +48 -45
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +12 -4
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
|
@@ -2,18 +2,20 @@ import * as aiService from "../services/aiService.js";
|
|
|
2
2
|
import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
3
3
|
import { supportsVision } from "../utils/modelCapabilities.js";
|
|
4
4
|
import { persistToolImages } from "../utils/toolImagePersistence.js";
|
|
5
|
-
import { parseTaskNotificationXml
|
|
5
|
+
import { parseTaskNotificationXml } from "../utils/notificationXml.js";
|
|
6
6
|
import { estimateContextTokens } from "../utils/tokenCalculation.js";
|
|
7
7
|
import { estimateTokens } from "../utils/tokenEstimate.js";
|
|
8
8
|
import { getTaskReminderTurnCounts, maybeInjectTaskReminder, TASK_REMINDER_CONFIG, } from "../utils/taskReminder.js";
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
9
|
+
import { getChangedFilesReminder } from "../utils/fileChangeReminder.js";
|
|
10
|
+
import { buildMcpInstructionsAnnouncement, collectAnnouncedServers, } from "../utils/mcpInstructions.js";
|
|
11
|
+
import { buildExecCatalogAnnouncement, collectExecCatalogState, } from "../exec/catalogAnnouncement.js";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
11
14
|
import * as path from "node:path";
|
|
15
|
+
import { EDIT_TOOL_NAME, WRITE_TOOL_NAME } from "../constants/tools.js";
|
|
12
16
|
import { buildSystemPrompt, formatCompactSummary, getCompactPrompt, } from "../prompts/index.js";
|
|
13
17
|
import { buildPlanModeReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, wrapInSystemReminder, } from "../prompts/planModeReminders.js";
|
|
14
18
|
import { recoverTruncatedJson } from "../utils/stringUtils.js";
|
|
15
|
-
import { AGENT_TOOL_NAME, TASK_CREATE_TOOL_NAME, TASK_GET_TOOL_NAME, TASK_LIST_TOOL_NAME, TASK_UPDATE_TOOL_NAME, } from "../constants/tools.js";
|
|
16
|
-
import { addConsolidatedAbortListener } from "../utils/abortUtils.js";
|
|
17
19
|
import { logger } from "../utils/globalLogger.js";
|
|
18
20
|
import { startInteractionSpan, endInteractionSpan, startLLMRequestSpan, endLLMRequestSpan, resetTracingState, } from "../telemetry/sessionTracing.js";
|
|
19
21
|
import { logOTelEvent } from "../telemetry/events.js";
|
|
@@ -115,6 +117,9 @@ export class AIManager {
|
|
|
115
117
|
get permissionManager() {
|
|
116
118
|
return this.container.get("PermissionManager");
|
|
117
119
|
}
|
|
120
|
+
get mcpManager() {
|
|
121
|
+
return this.container.get("McpManager");
|
|
122
|
+
}
|
|
118
123
|
get planManager() {
|
|
119
124
|
return this.container.get("PlanManager");
|
|
120
125
|
}
|
|
@@ -423,6 +428,12 @@ export class AIManager {
|
|
|
423
428
|
logger?.warn("Compaction already in progress");
|
|
424
429
|
return;
|
|
425
430
|
}
|
|
431
|
+
// 压缩绑定发起时的会话:LLM fork 期间用户可能已把 agent 原地切到另一会话
|
|
432
|
+
// (IDE「历史对话」restoreSession / CLI 与插件 `/clear` 都会换 sessionId,
|
|
433
|
+
// 且均不被 isCompacting 阻拦)。写入阶段前必须复核,否则摘要块会追加进
|
|
434
|
+
// 切换后会话的内存列表并持久化进其转录文件(spec message-compact「压缩
|
|
435
|
+
// 期间的会话保护」;PM 缺陷 3478352534577408)。
|
|
436
|
+
const sessionIdAtStart = this.messageManager.getSessionId();
|
|
426
437
|
// 1. Run PreCompact hooks
|
|
427
438
|
let hookInstructions;
|
|
428
439
|
if (this.hookManager) {
|
|
@@ -443,7 +454,7 @@ export class AIManager {
|
|
|
443
454
|
this.setIsCompacting(true);
|
|
444
455
|
try {
|
|
445
456
|
const modelConfig = this.getModelConfig();
|
|
446
|
-
const recentChatMessages = convertMessagesForAPI(messagesToCompact, {
|
|
457
|
+
const recentChatMessages = await convertMessagesForAPI(messagesToCompact, {
|
|
447
458
|
supportsVision: supportsVision(modelConfig.capabilities),
|
|
448
459
|
});
|
|
449
460
|
const compactPrompt = getCompactPrompt(mergedInstructions);
|
|
@@ -470,7 +481,15 @@ export class AIManager {
|
|
|
470
481
|
const formattedSummary = formatCompactSummary(summaryContent);
|
|
471
482
|
// 7. Build post-compact context restoration
|
|
472
483
|
const enhancedSummary = await this.buildPostCompactContext(formattedSummary);
|
|
473
|
-
// 8. Execute message reconstruction
|
|
484
|
+
// 8. Execute message reconstruction — only if the session is still the
|
|
485
|
+
// one this compaction started for. Abandoning here skips the entire
|
|
486
|
+
// write phase (summary append, post-compact context restore, plan-mode
|
|
487
|
+
// reminder, SessionStart/PostCompact hooks); the spent LLM call is not
|
|
488
|
+
// recovered and the original session simply stays uncompacted.
|
|
489
|
+
if (this.messageManager.getSessionId() !== sessionIdAtStart) {
|
|
490
|
+
logger?.warn(`Compaction abandoned: session switched from ${sessionIdAtStart} to ${this.messageManager.getSessionId()} while the summary was being generated`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
474
493
|
await this.messageManager.compactMessagesAndUpdateSession(enhancedSummary, compactUsage);
|
|
475
494
|
// Clear readFileState after compaction (aligned with Claude Code's
|
|
476
495
|
// compact flow): a file_unchanged stub would otherwise reference Read
|
|
@@ -537,7 +556,10 @@ export class AIManager {
|
|
|
537
556
|
catch (compactError) {
|
|
538
557
|
this.consecutiveCompactionFailures++;
|
|
539
558
|
logger?.error(`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive): ${compactError instanceof Error ? compactError.message : String(compactError)}`);
|
|
540
|
-
|
|
559
|
+
// 会话已被切走时错误块同样不得写进切换后的会话(与上面的写入放弃同口径)
|
|
560
|
+
if (this.messageManager.getSessionId() === sessionIdAtStart) {
|
|
561
|
+
this.messageManager.addErrorBlock(`Failed to compact conversation history: ${compactError instanceof Error ? compactError.message : String(compactError)}. You may encounter context limit issues.`);
|
|
562
|
+
}
|
|
541
563
|
}
|
|
542
564
|
finally {
|
|
543
565
|
this.setIsCompacting(false);
|
|
@@ -565,6 +587,58 @@ export class AIManager {
|
|
|
565
587
|
autoMemory: autoMemoryOptions,
|
|
566
588
|
});
|
|
567
589
|
}
|
|
590
|
+
/**
|
|
591
|
+
* Announce the connected MCP servers' own usage notes in the conversation,
|
|
592
|
+
* once per server. Not part of the system prompt: this content appears when a
|
|
593
|
+
* connection happens, and rewriting the system prompt mid-session would drop
|
|
594
|
+
* the whole cached prefix (see `utils/mcpInstructions.ts`).
|
|
595
|
+
*/
|
|
596
|
+
maybeAnnounceMcpInstructions() {
|
|
597
|
+
const mcpManager = this.mcpManager;
|
|
598
|
+
// Optional for the same reason as `getEffectiveAdditionalDirectories` above:
|
|
599
|
+
// hosts and tests register partial McpManager doubles, and a missing
|
|
600
|
+
// usage-notes channel is not a reason to fail the turn. Bailing out matters
|
|
601
|
+
// more here than elsewhere — reading an absent manager as "no servers left"
|
|
602
|
+
// would announce every previously announced server as disconnected.
|
|
603
|
+
if (!mcpManager || typeof mcpManager.getServerInstructions !== "function") {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const available = mcpManager.getServerInstructions((toolName) => this.permissionManager?.isToolDenied?.(toolName) === true);
|
|
607
|
+
const announced = collectAnnouncedServers(this.messageManager.getMessages());
|
|
608
|
+
const availableNames = new Set(available.map((server) => server.name));
|
|
609
|
+
const text = buildMcpInstructionsAnnouncement({
|
|
610
|
+
added: available.filter((server) => !announced.has(server.name)),
|
|
611
|
+
removed: Array.from(announced).filter((name) => !availableNames.has(name)),
|
|
612
|
+
});
|
|
613
|
+
if (!text)
|
|
614
|
+
return;
|
|
615
|
+
this.messageManager.addUserMessage({
|
|
616
|
+
content: wrapInSystemReminder(text),
|
|
617
|
+
isMeta: true,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Announce the MCP catalog in the conversation, whenever the pool renders
|
|
622
|
+
* differently from the last announcement. Keeping it out of `Exec`'s description
|
|
623
|
+
* is what makes that declaration a constant: a server connecting would otherwise
|
|
624
|
+
* rewrite `tools[]`, which sits in the cached prefix (see
|
|
625
|
+
* `exec/catalogAnnouncement.ts`).
|
|
626
|
+
*/
|
|
627
|
+
maybeAnnounceExecCatalog() {
|
|
628
|
+
const toolManager = this.toolManager;
|
|
629
|
+
// Optional for the same reason as `maybeAnnounceMcpInstructions` above, and it
|
|
630
|
+
// matters just as much here: reading an absent channel as "the catalog is gone"
|
|
631
|
+
// would tell the model to stop using every tool an earlier catalog listed.
|
|
632
|
+
if (!toolManager || typeof toolManager.getExecCatalog !== "function")
|
|
633
|
+
return;
|
|
634
|
+
const text = buildExecCatalogAnnouncement(collectExecCatalogState(this.messageManager.getMessages()), toolManager.getExecCatalog());
|
|
635
|
+
if (!text)
|
|
636
|
+
return;
|
|
637
|
+
this.messageManager.addUserMessage({
|
|
638
|
+
content: wrapInSystemReminder(text),
|
|
639
|
+
isMeta: true,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
568
642
|
resolveFilteredTools() {
|
|
569
643
|
const toolsConfig = this.getFilteredToolsConfig();
|
|
570
644
|
const toolNames = new Set(toolsConfig.map((t) => t.function.name));
|
|
@@ -599,9 +673,13 @@ export class AIManager {
|
|
|
599
673
|
forkMessages.push({ role: "user", content: prompt });
|
|
600
674
|
const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
|
|
601
675
|
const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
|
|
676
|
+
// Clone the parent session's read state so the fork inherits the read/dedup
|
|
677
|
+
// baseline (aligned with Claude Code's cloneFileStateCache): the fork can
|
|
678
|
+
// Edit a file the parent already read without re-reading it first. The
|
|
679
|
+
// clone is an independent Map with copied entries, so reads/writes inside
|
|
680
|
+
// the fork never leak back into the main session's dedup and staleness
|
|
681
|
+
// tracking.
|
|
682
|
+
const forkReadFileState = new Map(Array.from(this.readFileState, ([path, entry]) => [path, { ...entry }]));
|
|
605
683
|
let totalUsage;
|
|
606
684
|
let content;
|
|
607
685
|
for (let turn = 0; turn < options.maxTurns; turn++) {
|
|
@@ -709,7 +787,7 @@ export class AIManager {
|
|
|
709
787
|
*/
|
|
710
788
|
async runAutoMemoryFork(messages, prompt, options, abortSignal) {
|
|
711
789
|
const modelConfig = this.getModelConfig();
|
|
712
|
-
const historyMessages = convertMessagesForAPI(messages, {
|
|
790
|
+
const historyMessages = await convertMessagesForAPI(messages, {
|
|
713
791
|
supportsVision: supportsVision(modelConfig.capabilities),
|
|
714
792
|
});
|
|
715
793
|
// Give the fork a real signal even when the caller has none, so tools
|
|
@@ -729,182 +807,6 @@ export class AIManager {
|
|
|
729
807
|
}
|
|
730
808
|
return result;
|
|
731
809
|
}
|
|
732
|
-
/**
|
|
733
|
-
* Fork subagent ("/subtask"): spawn a background agent that inherits the
|
|
734
|
-
* parent's full conversation context — the same system prompt, tools, model,
|
|
735
|
-
* and message prefix as the main loop — so the forked request prefix matches
|
|
736
|
-
* exactly and the prompt cache is reused. The fork runs to completion in the
|
|
737
|
-
* background via the shared BackgroundTaskManager (visible in /tasks, log at
|
|
738
|
-
* `os.tmpdir()/wave-subagent-<taskId>.log`); its final response is enqueued
|
|
739
|
-
* as a task notification carrying the `<result>` tag, so the main agent
|
|
740
|
-
* picks it up on the next turn and can act on it.
|
|
741
|
-
*
|
|
742
|
-
* Recursion is prevented by denying the Agent tool and the Task management
|
|
743
|
-
* tools (the fork cannot spawn further subagents or mutate the shared task
|
|
744
|
-
* list), and by honoring the parent's denied rules for everything else.
|
|
745
|
-
* Gate-approved tools execute locally in the stripped fork context (no
|
|
746
|
-
* permission prompts), mirroring the other fork paths.
|
|
747
|
-
*
|
|
748
|
-
* Returns the background task ID (empty string when no BackgroundTaskManager
|
|
749
|
-
* is registered, in which case the fork still runs but without task
|
|
750
|
-
* bookkeeping or notification).
|
|
751
|
-
*/
|
|
752
|
-
async runForkSubagent(prompt, options, abortSignal) {
|
|
753
|
-
const modelConfig = this.getModelConfig();
|
|
754
|
-
// Snapshot the conversation synchronously: the /subtask handler records
|
|
755
|
-
// the command echo in the transcript after this call, so the fork never
|
|
756
|
-
// sees its own trigger message duplicated.
|
|
757
|
-
const historyMessages = convertMessagesForAPI(this.messageManager.getMessages(), { supportsVision: supportsVision(modelConfig.capabilities) });
|
|
758
|
-
const backgroundTaskManager = this.backgroundTaskManager;
|
|
759
|
-
const permissionManager = this.permissionManager;
|
|
760
|
-
const messageQueue = this.container.has("MessageQueue")
|
|
761
|
-
? this.container.get("MessageQueue")
|
|
762
|
-
: undefined;
|
|
763
|
-
// Combine the caller's signal with an internal one so a task kill
|
|
764
|
-
// (BackgroundTaskManager.stopTask / cleanup) aborts the in-flight fork.
|
|
765
|
-
const abortController = new AbortController();
|
|
766
|
-
let abortCleanup;
|
|
767
|
-
if (abortSignal) {
|
|
768
|
-
abortCleanup = addConsolidatedAbortListener(abortSignal, [
|
|
769
|
-
() => abortController.abort(),
|
|
770
|
-
]);
|
|
771
|
-
}
|
|
772
|
-
// Recursion prevention: deny the Agent tool and Task tools, then honor the
|
|
773
|
-
// parent's denied rules for everything else.
|
|
774
|
-
const deniedTools = new Set([
|
|
775
|
-
AGENT_TOOL_NAME,
|
|
776
|
-
TASK_CREATE_TOOL_NAME,
|
|
777
|
-
TASK_GET_TOOL_NAME,
|
|
778
|
-
TASK_UPDATE_TOOL_NAME,
|
|
779
|
-
TASK_LIST_TOOL_NAME,
|
|
780
|
-
]);
|
|
781
|
-
const canUseTool = (name) => {
|
|
782
|
-
if (deniedTools.has(name))
|
|
783
|
-
return false;
|
|
784
|
-
if (permissionManager?.isToolDenied(name))
|
|
785
|
-
return false;
|
|
786
|
-
return true;
|
|
787
|
-
};
|
|
788
|
-
const deniedToolMessage = "The Agent tool and Task tools are not available in a fork subagent.";
|
|
789
|
-
// Background task bookkeeping mirrors subagentManager.executeAgent.
|
|
790
|
-
let taskId = "";
|
|
791
|
-
let logStream;
|
|
792
|
-
if (backgroundTaskManager) {
|
|
793
|
-
taskId = backgroundTaskManager.generateId();
|
|
794
|
-
const startTime = Date.now();
|
|
795
|
-
const logPath = path.join(os.tmpdir(), `wave-subagent-${taskId}.log`);
|
|
796
|
-
logStream = createWriteStream(logPath, { flags: "a" });
|
|
797
|
-
backgroundTaskManager.addTask({
|
|
798
|
-
id: taskId,
|
|
799
|
-
type: "subagent",
|
|
800
|
-
status: "running",
|
|
801
|
-
startTime,
|
|
802
|
-
description: truncateWithMarker(options.description, 1000),
|
|
803
|
-
stdout: "",
|
|
804
|
-
stderr: "",
|
|
805
|
-
outputPath: logPath,
|
|
806
|
-
onStop: () => {
|
|
807
|
-
logStream?.destroy();
|
|
808
|
-
logStream = undefined;
|
|
809
|
-
abortController.abort();
|
|
810
|
-
},
|
|
811
|
-
});
|
|
812
|
-
logStream.write(`[${new Date().toISOString()}] Fork subagent started: ${options.description}\n`);
|
|
813
|
-
}
|
|
814
|
-
const finish = async (status, content) => {
|
|
815
|
-
if (!backgroundTaskManager || !taskId)
|
|
816
|
-
return;
|
|
817
|
-
const task = backgroundTaskManager.getTask(taskId);
|
|
818
|
-
if (!task)
|
|
819
|
-
return;
|
|
820
|
-
const wasAlreadyKilled = task.status === "killed";
|
|
821
|
-
const endTime = Date.now();
|
|
822
|
-
if (status === "completed") {
|
|
823
|
-
task.status = "completed";
|
|
824
|
-
task.stdout = content;
|
|
825
|
-
logStream?.write(`[${new Date().toISOString()}] Final response:\n${content}\n`);
|
|
826
|
-
}
|
|
827
|
-
else {
|
|
828
|
-
task.status = "failed";
|
|
829
|
-
task.stderr = content;
|
|
830
|
-
logStream?.write(`[${new Date().toISOString()}] Fork subagent failed: ${content}\n`);
|
|
831
|
-
}
|
|
832
|
-
task.endTime = endTime;
|
|
833
|
-
if (task.startTime)
|
|
834
|
-
task.runtime = endTime - task.startTime;
|
|
835
|
-
logStream?.end();
|
|
836
|
-
logStream = undefined;
|
|
837
|
-
// Skip the notification when the task was already stopped (e.g. by main
|
|
838
|
-
// agent shutdown) — mirrors subagentManager's wasAlreadyKilled guard.
|
|
839
|
-
if (!wasAlreadyKilled && messageQueue) {
|
|
840
|
-
const description = truncateWithMarker(options.description, 1000);
|
|
841
|
-
const summary = status === "completed"
|
|
842
|
-
? `Subtask "${description}" completed`
|
|
843
|
-
: `Subtask "${description}" failed: ${content}`;
|
|
844
|
-
messageQueue.enqueueNotification(taskNotificationToXml({
|
|
845
|
-
type: "task_notification",
|
|
846
|
-
taskId,
|
|
847
|
-
taskType: "agent",
|
|
848
|
-
status,
|
|
849
|
-
summary,
|
|
850
|
-
...(status === "completed" && { result: content }),
|
|
851
|
-
}));
|
|
852
|
-
}
|
|
853
|
-
};
|
|
854
|
-
// Fire-and-forget: the fork runs to completion in the background; the
|
|
855
|
-
// caller gets the task ID immediately. Tracked in the async work registry
|
|
856
|
-
// so destroy() drains it before returning (abort via the task's onStop
|
|
857
|
-
// makes the loop settle).
|
|
858
|
-
const forkPromise = (async () => {
|
|
859
|
-
try {
|
|
860
|
-
const result = await this.runForkLoop(historyMessages, prompt, {
|
|
861
|
-
// /subtask aligns with Claude Code's fork subagent
|
|
862
|
-
// (forkSubagent.ts:65): a loose upper bound that practically
|
|
863
|
-
// never binds.
|
|
864
|
-
maxTurns: options.maxTurns ?? 200,
|
|
865
|
-
canUseTool,
|
|
866
|
-
deniedToolMessage,
|
|
867
|
-
}, abortController.signal);
|
|
868
|
-
if (result.usage && this.callbacks?.onUsageAdded) {
|
|
869
|
-
this.callbacks.onUsageAdded({
|
|
870
|
-
...result.usage,
|
|
871
|
-
model: modelConfig.model,
|
|
872
|
-
operation_type: "agent",
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
if (result.content?.trim()) {
|
|
876
|
-
await finish("completed", result.content);
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
await finish("failed", "Fork subagent produced no text response");
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
catch (error) {
|
|
883
|
-
// Abort is the expected exit for a killed task; stopTask has already
|
|
884
|
-
// marked it "killed", so no notification is sent.
|
|
885
|
-
if (abortController.signal.aborted) {
|
|
886
|
-
const task = backgroundTaskManager?.getTask(taskId);
|
|
887
|
-
if (task && task.status === "running") {
|
|
888
|
-
task.status = "killed";
|
|
889
|
-
task.endTime = Date.now();
|
|
890
|
-
if (task.startTime)
|
|
891
|
-
task.runtime = task.endTime - task.startTime;
|
|
892
|
-
}
|
|
893
|
-
logStream?.end();
|
|
894
|
-
logStream = undefined;
|
|
895
|
-
}
|
|
896
|
-
else {
|
|
897
|
-
logger?.error("Fork subagent execution failed:", error);
|
|
898
|
-
await finish("failed", error instanceof Error ? error.message : String(error));
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
finally {
|
|
902
|
-
abortCleanup?.();
|
|
903
|
-
}
|
|
904
|
-
})();
|
|
905
|
-
this.asyncWorkRegistry?.track(forkPromise);
|
|
906
|
-
return taskId;
|
|
907
|
-
}
|
|
908
810
|
/**
|
|
909
811
|
* Parse a fork tool call's JSON arguments, recovering truncated JSON the
|
|
910
812
|
* same way the main loop does. Unparseable arguments fall back to `{}` and
|
|
@@ -983,7 +885,7 @@ export class AIManager {
|
|
|
983
885
|
const lastMessage = rawMessages[rawMessages.length - 1];
|
|
984
886
|
const hasInProgressMessage = lastMessage?.role === "assistant" &&
|
|
985
887
|
lastMessage.blocks.some((b) => "stage" in b && b.stage === "streaming");
|
|
986
|
-
const forkMessages = convertMessagesForAPI(hasInProgressMessage ? rawMessages.slice(0, -1) : rawMessages, { supportsVision: supportsVision(modelConfig.capabilities) });
|
|
888
|
+
const forkMessages = await convertMessagesForAPI(hasInProgressMessage ? rawMessages.slice(0, -1) : rawMessages, { supportsVision: supportsVision(modelConfig.capabilities) });
|
|
987
889
|
// Mirror the main loop's memory injection so the request prefix matches.
|
|
988
890
|
const { prependContent } = await this.messageManager.getMemoryForInjection();
|
|
989
891
|
if (prependContent.trim()) {
|
|
@@ -1112,29 +1014,32 @@ ${question}`;
|
|
|
1112
1014
|
// 4. Invoked skills context (with token budget, matching Claude Code)
|
|
1113
1015
|
const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25000;
|
|
1114
1016
|
const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5000;
|
|
1115
|
-
const
|
|
1116
|
-
if (
|
|
1017
|
+
const invokedSkills = this.messageManager.getInvokedSkills?.(10) ?? [];
|
|
1018
|
+
if (invokedSkills.length > 0 && this.skillManager) {
|
|
1117
1019
|
const invokedSkillParts = [];
|
|
1118
1020
|
let skillsUsedTokens = 0;
|
|
1119
|
-
for (const
|
|
1021
|
+
for (const invoked of invokedSkills) {
|
|
1120
1022
|
try {
|
|
1121
|
-
const skill = await this.skillManager.loadSkill(skillName);
|
|
1122
|
-
|
|
1023
|
+
const skill = await this.skillManager.loadSkill(invoked.skillName);
|
|
1024
|
+
// Re-inject what the model actually saw (already substituted, with the
|
|
1025
|
+
// base-directory header). Re-render from the file only when that
|
|
1026
|
+
// record is missing — never hand the model a raw SKILL.md, whose
|
|
1027
|
+
// `${WAVE_SKILL_DIR}` would be a literal placeholder
|
|
1028
|
+
// (spec core/message-compact 场景 4).
|
|
1029
|
+
let skillContent = invoked.content?.trim() ??
|
|
1030
|
+
(skill ? this.skillManager.renderSkillContent(skill) : undefined);
|
|
1031
|
+
if (!skillContent)
|
|
1123
1032
|
continue;
|
|
1124
|
-
const contentMatch = skill.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
1125
|
-
let skillContent = contentMatch
|
|
1126
|
-
? contentMatch[1].trim()
|
|
1127
|
-
: skill.content;
|
|
1128
1033
|
const maxSkillChars = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4;
|
|
1129
1034
|
if (skillContent.length > maxSkillChars) {
|
|
1130
|
-
skillContent =
|
|
1131
|
-
skillContent.slice(0, maxSkillChars) + "\n\n...[truncated]...";
|
|
1035
|
+
skillContent = `${skillContent.slice(0, maxSkillChars)}\n\n...[truncated]...`;
|
|
1132
1036
|
}
|
|
1133
1037
|
const skillTokens = estimateTokens(skillContent);
|
|
1134
1038
|
if (skillsUsedTokens + skillTokens > POST_COMPACT_SKILLS_TOKEN_BUDGET)
|
|
1135
1039
|
break;
|
|
1136
1040
|
skillsUsedTokens += skillTokens;
|
|
1137
|
-
|
|
1041
|
+
const description = skill?.description;
|
|
1042
|
+
invokedSkillParts.push(`\n\n## ${skill?.name ?? invoked.skillName}\n${description ? `*${description}*\n\n` : ""}\`\`\`\n${skillContent}\n\`\`\``);
|
|
1138
1043
|
}
|
|
1139
1044
|
catch {
|
|
1140
1045
|
// Skip skills that can't be loaded
|
|
@@ -1209,9 +1114,22 @@ ${question}`;
|
|
|
1209
1114
|
get skillManager() {
|
|
1210
1115
|
return this.container.get("SkillManager");
|
|
1211
1116
|
}
|
|
1117
|
+
get liveConfigManager() {
|
|
1118
|
+
return this.container.get("LiveConfigManager");
|
|
1119
|
+
}
|
|
1212
1120
|
async sendAIMessage(options = {}) {
|
|
1213
1121
|
const { recursionDepth = 0, model, allowedRules, maxTokens } = options;
|
|
1214
1122
|
let turnOffset = recursionDepth;
|
|
1123
|
+
// Pin the settings.json-derived configuration for the whole turn: live
|
|
1124
|
+
// reload can land mid-turn and the turn must not see values shift under it
|
|
1125
|
+
// (core/agent-config.md scenario 5). Nested turns (subagent turns inside
|
|
1126
|
+
// their parent's) share the outer snapshot instead of re-capturing it; the
|
|
1127
|
+
// release at the end of the turn mirrors this capture and lives on the
|
|
1128
|
+
// single exit path of this function.
|
|
1129
|
+
const ownsTurnSnapshot = recursionDepth === 0;
|
|
1130
|
+
if (ownsTurnSnapshot) {
|
|
1131
|
+
this.liveConfigManager?.onTurnStart();
|
|
1132
|
+
}
|
|
1215
1133
|
// Reserve this turn synchronously before any async work. Bumping the
|
|
1216
1134
|
// generation invalidates any in-flight turn's end-of-turn cleanup, and
|
|
1217
1135
|
// recording myGeneration lets us skip cleanup if a newer turn (or abort)
|
|
@@ -1271,6 +1189,18 @@ ${question}`;
|
|
|
1271
1189
|
this.abortController = abortController;
|
|
1272
1190
|
toolAbortController = new AbortController();
|
|
1273
1191
|
this.toolAbortController = toolAbortController;
|
|
1192
|
+
// Every tool call in a turn wires its own abort listener onto the
|
|
1193
|
+
// shared tool signal (see toolAbortController.signal in executeTool),
|
|
1194
|
+
// so a fan-out of >=11 parallel calls trips Node's default 10-listener
|
|
1195
|
+
// cap and spams MaxListenersExceededWarning. The signal dies with the
|
|
1196
|
+
// turn (toolAbortController is cleared in the tool loop), so there is
|
|
1197
|
+
// no real leak — raise the cap to silence the noise. Optional chaining
|
|
1198
|
+
// keeps this a no-op on Node >=26, which removed the EventTarget
|
|
1199
|
+
// warning mechanism (and the setMaxListeners method) entirely.
|
|
1200
|
+
const toolSignal = toolAbortController.signal;
|
|
1201
|
+
const turnSignal = abortController.signal;
|
|
1202
|
+
toolSignal.setMaxListeners?.(0);
|
|
1203
|
+
turnSignal.setMaxListeners?.(0);
|
|
1274
1204
|
}
|
|
1275
1205
|
else {
|
|
1276
1206
|
// Reuse existing controllers
|
|
@@ -1299,14 +1229,33 @@ ${question}`;
|
|
|
1299
1229
|
isMeta: true,
|
|
1300
1230
|
});
|
|
1301
1231
|
}
|
|
1232
|
+
// Nested memory: a read inside a subtree pulls in that subtree's
|
|
1233
|
+
// AGENTS.md (persisted as a meta message, aligned with Claude Code's
|
|
1234
|
+
// nested_memory attachment). Runs with the rules above — before the
|
|
1235
|
+
// message snapshot below, so the file lands in this request rather
|
|
1236
|
+
// than the next one. Optional call: hosts and tests register partial
|
|
1237
|
+
// MessageManager doubles, and absence reads as "nothing triggered".
|
|
1238
|
+
const nestedMemories = (await this.messageManager.collectNestedMemoryFiles?.()) ?? [];
|
|
1239
|
+
for (const memory of nestedMemories) {
|
|
1240
|
+
this.messageManager.addUserMessage({
|
|
1241
|
+
content: wrapInSystemReminder(`Contents of ${memory.path}:\n\n${memory.content}`),
|
|
1242
|
+
isMeta: true,
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1302
1245
|
// Pre-request auto-compaction: estimate the context about to be sent
|
|
1303
1246
|
// and compact BEFORE issuing the request, so an over-limit request
|
|
1304
1247
|
// never goes out. Skipped on fork paths (they use runForkLoop).
|
|
1305
1248
|
await this.maybeAutoCompactBeforeRequest(abortController);
|
|
1249
|
+
// Announce MCP servers' own usage notes (persisted meta message) before
|
|
1250
|
+
// the snapshot below, so a server that connected mid-session explains
|
|
1251
|
+
// itself in the request that can already call it. After compaction, so
|
|
1252
|
+
// the announcement is not the content that just got summarized away.
|
|
1253
|
+
this.maybeAnnounceMcpInstructions();
|
|
1254
|
+
this.maybeAnnounceExecCatalog();
|
|
1306
1255
|
// Get recent message history
|
|
1307
1256
|
const rawMessages = this.messageManager.getMessages();
|
|
1308
1257
|
const currentModelConfig = this.getModelConfig();
|
|
1309
|
-
const recentMessages = convertMessagesForAPI(rawMessages, {
|
|
1258
|
+
const recentMessages = await convertMessagesForAPI(rawMessages, {
|
|
1310
1259
|
supportsVision: supportsVision(currentModelConfig.capabilities),
|
|
1311
1260
|
});
|
|
1312
1261
|
// Track if assistant message has been created
|
|
@@ -1350,6 +1299,19 @@ ${question}`;
|
|
|
1350
1299
|
isMeta: true,
|
|
1351
1300
|
});
|
|
1352
1301
|
}
|
|
1302
|
+
// External file-change reminder (aligned with Claude Code's
|
|
1303
|
+
// changed_files attachment): notify the agent about files it read
|
|
1304
|
+
// that were since changed on disk by another process — the
|
|
1305
|
+
// auto-memory extraction fork, an external editor, another session.
|
|
1306
|
+
// Persisted as a meta message like the task reminder, so it stays in
|
|
1307
|
+
// the message prefix the auto-memory fork mirrors for cache reuse.
|
|
1308
|
+
const changedFilesReminder = await getChangedFilesReminder(this.readFileState);
|
|
1309
|
+
if (changedFilesReminder) {
|
|
1310
|
+
this.messageManager.addUserMessage({
|
|
1311
|
+
content: wrapInSystemReminder(changedFilesReminder),
|
|
1312
|
+
isMeta: true,
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1353
1315
|
// Add streaming callbacks only if streaming is enabled
|
|
1354
1316
|
if (this.stream) {
|
|
1355
1317
|
callAgentOptions.onContentUpdate = (content) => {
|
|
@@ -1746,6 +1708,9 @@ ${question}`;
|
|
|
1746
1708
|
if (myGeneration === this.turnGeneration) {
|
|
1747
1709
|
this.setIsLoading(false);
|
|
1748
1710
|
}
|
|
1711
|
+
if (ownsTurnSnapshot) {
|
|
1712
|
+
this.liveConfigManager?.onTurnEnd();
|
|
1713
|
+
}
|
|
1749
1714
|
break outer;
|
|
1750
1715
|
}
|
|
1751
1716
|
}
|
|
@@ -2005,6 +1970,10 @@ ${question}`;
|
|
|
2005
1970
|
images: toolImages,
|
|
2006
1971
|
timestamp: Date.now(),
|
|
2007
1972
|
});
|
|
1973
|
+
// Plan panel refresh (spec: 计划文件更新后刷新计划面板): a successful
|
|
1974
|
+
// Write/Edit of the plan file broadcasts the new content so hosts can
|
|
1975
|
+
// update an already-open plan panel without waiting for ExitPlanMode.
|
|
1976
|
+
await this.notifyPlanFileUpdate(toolName, toolArgs, toolResult.success);
|
|
2008
1977
|
// Execute PostToolUse hooks after successful tool completion
|
|
2009
1978
|
await this.executePostToolUseHooks(toolId, toolName, toolArgs, toolResult);
|
|
2010
1979
|
}
|
|
@@ -2023,6 +1992,39 @@ ${question}`;
|
|
|
2023
1992
|
});
|
|
2024
1993
|
}
|
|
2025
1994
|
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Plan panel refresh (spec: 计划文件更新后刷新计划面板).
|
|
1997
|
+
*
|
|
1998
|
+
* When a successful `Write`/`Edit` targets the current plan file, read it and
|
|
1999
|
+
* emit the full content so hosts can update an already-open plan panel. The
|
|
2000
|
+
* path comparison mirrors the plan-mode allow branch in PermissionManager so
|
|
2001
|
+
* "the write that was allowed" and "the write that refreshes the panel" stay
|
|
2002
|
+
* the same set. The path is only set while plan mode is active, so post-exit
|
|
2003
|
+
* writes can't reach the panels.
|
|
2004
|
+
*/
|
|
2005
|
+
async notifyPlanFileUpdate(toolName, toolArgs, success) {
|
|
2006
|
+
if (!success)
|
|
2007
|
+
return;
|
|
2008
|
+
if (toolName !== WRITE_TOOL_NAME && toolName !== EDIT_TOOL_NAME)
|
|
2009
|
+
return;
|
|
2010
|
+
const targetPath = toolArgs.file_path;
|
|
2011
|
+
if (typeof targetPath !== "string" || targetPath.trim() === "")
|
|
2012
|
+
return;
|
|
2013
|
+
const planFilePath = this.container
|
|
2014
|
+
.get("PermissionManager")
|
|
2015
|
+
?.getPlanFilePath();
|
|
2016
|
+
if (!planFilePath)
|
|
2017
|
+
return;
|
|
2018
|
+
if (path.resolve(targetPath) !== path.resolve(planFilePath))
|
|
2019
|
+
return;
|
|
2020
|
+
try {
|
|
2021
|
+
const content = await readFile(planFilePath, "utf8");
|
|
2022
|
+
this.callbacks?.onPlanFileUpdated?.(content);
|
|
2023
|
+
}
|
|
2024
|
+
catch (error) {
|
|
2025
|
+
logger?.warn(`Failed to read plan file for panel refresh: ${planFilePath}`, error);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2026
2028
|
/**
|
|
2027
2029
|
* Execute PreToolUse hooks before tool execution
|
|
2028
2030
|
* Returns true if hooks allow tool execution, false if blocked
|
|
@@ -3,6 +3,7 @@ import * as os from "os";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { stripAnsiColors } from "../utils/stringUtils.js";
|
|
6
|
+
import { readTailTextSync } from "../utils/fileUtils.js";
|
|
6
7
|
import { WindowsStreamDecoder } from "../utils/encoding.js";
|
|
7
8
|
import { logger } from "../utils/globalLogger.js";
|
|
8
9
|
import { resolveShellPath } from "../utils/shellResolver.js";
|
|
@@ -386,6 +387,19 @@ export class BackgroundTaskManager {
|
|
|
386
387
|
}
|
|
387
388
|
let stdout = task.stdout;
|
|
388
389
|
let stderr = task.stderr;
|
|
390
|
+
// A running agent task keeps its tool-call log in `outputPath` while
|
|
391
|
+
// `stdout` stays empty until the terminal snapshot fills it (see
|
|
392
|
+
// SubagentManager) — so /tasks shows an empty output block for the whole
|
|
393
|
+
// run. Fall back to the log file tail to surface live progress.
|
|
394
|
+
//
|
|
395
|
+
// Only when the in-memory buffer is empty, and only while running: shell
|
|
396
|
+
// tasks accumulate stdout chunk by chunk (that buffer is authoritative and
|
|
397
|
+
// identical to the file), so reading the file on top of it would duplicate
|
|
398
|
+
// the text; once a task reaches a terminal state the snapshot already
|
|
399
|
+
// carries the final output.
|
|
400
|
+
if (!stdout && task.status === "running" && task.outputPath) {
|
|
401
|
+
stdout = stripAnsiColors(readTailTextSync(task.outputPath));
|
|
402
|
+
}
|
|
389
403
|
// Apply regex filter if provided
|
|
390
404
|
if (filter) {
|
|
391
405
|
try {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Container } from "../utils/container.js";
|
|
2
|
+
export interface BashModeManagerOptions {
|
|
3
|
+
workdir: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CommandExecutionResult {
|
|
6
|
+
exitCode: number;
|
|
7
|
+
output: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Bash mode (`!command`) execution. The command is surfaced as a user-message
|
|
11
|
+
* tool block (name: BASH_TOOL_NAME) — the same rendering as fork-skill / `/plan`
|
|
12
|
+
* outputs (⎿ prefix + markdown, result enters the model context via the
|
|
13
|
+
* generic `<local-command-stdout>` rule) — and executed through the shared
|
|
14
|
+
* bash tool engine (shell resolution, CWD tracking, timeout, output handling).
|
|
15
|
+
*/
|
|
16
|
+
export declare class BashModeManager {
|
|
17
|
+
private container;
|
|
18
|
+
private workdir;
|
|
19
|
+
isCommandRunning: boolean;
|
|
20
|
+
private abortController;
|
|
21
|
+
onCommandRunningChange?: (running: boolean) => void;
|
|
22
|
+
constructor(container: Container, options: BashModeManagerOptions);
|
|
23
|
+
private get messageManager();
|
|
24
|
+
/**
|
|
25
|
+
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
26
|
+
* bash-mode subprocesses, so settings `env` vars reach them without
|
|
27
|
+
* polluting other sessions in one `wave --stdio` process.
|
|
28
|
+
*/
|
|
29
|
+
private get sessionEnv();
|
|
30
|
+
private setCommandRunning;
|
|
31
|
+
executeCommand(command: string): Promise<number>;
|
|
32
|
+
abortCommand(): void;
|
|
33
|
+
}
|