wave-agent-sdk 1.2.0 → 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 +58 -4
- package/dist/agent.js +91 -19
- package/dist/builtin/index.js +2 -0
- package/dist/builtin/skills/settings.js +1 -12
- 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 +53 -13
- package/dist/core/plugin.js +134 -26
- 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 +5 -0
- package/dist/index.js +6 -0
- package/dist/managers/MemoryRuleManager.d.ts +6 -0
- package/dist/managers/MemoryRuleManager.js +12 -0
- package/dist/managers/aiManager.d.ts +35 -1
- package/dist/managers/aiManager.js +190 -21
- package/dist/managers/backgroundTaskManager.js +14 -0
- package/dist/managers/hookManager.d.ts +13 -0
- package/dist/managers/hookManager.js +31 -4
- package/dist/managers/liveConfigManager.d.ts +33 -0
- package/dist/managers/liveConfigManager.js +103 -8
- package/dist/managers/lspManager.d.ts +9 -0
- package/dist/managers/lspManager.js +47 -18
- package/dist/managers/mcpManager.d.ts +45 -10
- package/dist/managers/mcpManager.js +103 -1
- package/dist/managers/messageManager.d.ts +48 -5
- package/dist/managers/messageManager.js +107 -21
- package/dist/managers/permissionManager.d.ts +40 -0
- package/dist/managers/permissionManager.js +63 -8
- 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 +19 -0
- package/dist/managers/skillManager.js +44 -0
- package/dist/managers/slashCommandManager.d.ts +10 -0
- package/dist/managers/slashCommandManager.js +35 -3
- package/dist/managers/subagentManager.d.ts +8 -0
- package/dist/managers/subagentManager.js +20 -0
- package/dist/managers/toolManager.d.ts +29 -3
- package/dist/managers/toolManager.js +87 -13
- 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/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 +318 -119
- 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 +60 -9
- package/dist/services/configurationService.js +129 -54
- 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 +19 -15
- package/dist/services/interactionService.d.ts +9 -1
- package/dist/services/interactionService.js +28 -8
- package/dist/services/jsonlHandler.d.ts +84 -0
- package/dist/services/jsonlHandler.js +209 -14
- package/dist/services/memory.d.ts +3 -1
- package/dist/services/memory.js +13 -9
- package/dist/services/officialMarketplaceMirror.js +3 -2
- package/dist/services/pluginLoader.d.ts +12 -4
- package/dist/services/pluginLoader.js +38 -7
- package/dist/services/remoteSettingsService.js +16 -2
- package/dist/services/session.d.ts +74 -0
- package/dist/services/session.js +144 -3
- package/dist/services/sessionEntries.d.ts +2 -0
- package/dist/services/sessionEntries.js +20 -0
- package/dist/stdio/index.d.ts +3 -1
- package/dist/stdio/index.js +3 -1
- package/dist/stdio/notificationRouter.js +1 -0
- package/dist/stdio/stdioAgent.d.ts +14 -7
- package/dist/stdio/stdioAgent.js +19 -0
- package/dist/tools/artifactTool.js +406 -273
- package/dist/tools/bashTool.js +8 -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/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 +20 -5
- package/dist/types/configuration.d.ts +39 -1
- package/dist/types/marketplace.d.ts +40 -2
- package/dist/types/mcp.d.ts +39 -0
- 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 +15 -0
- package/dist/utils/constants.d.ts +10 -0
- package/dist/utils/constants.js +10 -0
- package/dist/utils/containerSetup.js +43 -0
- 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 +33 -0
- package/dist/utils/fileUtils.js +81 -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 -2
- package/dist/utils/messageOperations.js +40 -29
- 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 +39 -43
- package/dist/utils/userSettings.d.ts +90 -0
- package/dist/utils/userSettings.js +291 -0
- package/package.json +10 -7
|
@@ -6,8 +6,13 @@ 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 { getChangedFilesReminder } from "../utils/fileChangeReminder.js";
|
|
10
|
+
import { buildMcpInstructionsAnnouncement, collectAnnouncedServers, } from "../utils/mcpInstructions.js";
|
|
11
|
+
import { buildExecCatalogAnnouncement, collectExecCatalogState, } from "../exec/catalogAnnouncement.js";
|
|
9
12
|
import { existsSync } from "node:fs";
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
10
14
|
import * as path from "node:path";
|
|
15
|
+
import { EDIT_TOOL_NAME, WRITE_TOOL_NAME } from "../constants/tools.js";
|
|
11
16
|
import { buildSystemPrompt, formatCompactSummary, getCompactPrompt, } from "../prompts/index.js";
|
|
12
17
|
import { buildPlanModeReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, wrapInSystemReminder, } from "../prompts/planModeReminders.js";
|
|
13
18
|
import { recoverTruncatedJson } from "../utils/stringUtils.js";
|
|
@@ -112,6 +117,9 @@ export class AIManager {
|
|
|
112
117
|
get permissionManager() {
|
|
113
118
|
return this.container.get("PermissionManager");
|
|
114
119
|
}
|
|
120
|
+
get mcpManager() {
|
|
121
|
+
return this.container.get("McpManager");
|
|
122
|
+
}
|
|
115
123
|
get planManager() {
|
|
116
124
|
return this.container.get("PlanManager");
|
|
117
125
|
}
|
|
@@ -420,6 +428,12 @@ export class AIManager {
|
|
|
420
428
|
logger?.warn("Compaction already in progress");
|
|
421
429
|
return;
|
|
422
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();
|
|
423
437
|
// 1. Run PreCompact hooks
|
|
424
438
|
let hookInstructions;
|
|
425
439
|
if (this.hookManager) {
|
|
@@ -440,7 +454,7 @@ export class AIManager {
|
|
|
440
454
|
this.setIsCompacting(true);
|
|
441
455
|
try {
|
|
442
456
|
const modelConfig = this.getModelConfig();
|
|
443
|
-
const recentChatMessages = convertMessagesForAPI(messagesToCompact, {
|
|
457
|
+
const recentChatMessages = await convertMessagesForAPI(messagesToCompact, {
|
|
444
458
|
supportsVision: supportsVision(modelConfig.capabilities),
|
|
445
459
|
});
|
|
446
460
|
const compactPrompt = getCompactPrompt(mergedInstructions);
|
|
@@ -467,7 +481,15 @@ export class AIManager {
|
|
|
467
481
|
const formattedSummary = formatCompactSummary(summaryContent);
|
|
468
482
|
// 7. Build post-compact context restoration
|
|
469
483
|
const enhancedSummary = await this.buildPostCompactContext(formattedSummary);
|
|
470
|
-
// 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
|
+
}
|
|
471
493
|
await this.messageManager.compactMessagesAndUpdateSession(enhancedSummary, compactUsage);
|
|
472
494
|
// Clear readFileState after compaction (aligned with Claude Code's
|
|
473
495
|
// compact flow): a file_unchanged stub would otherwise reference Read
|
|
@@ -534,7 +556,10 @@ export class AIManager {
|
|
|
534
556
|
catch (compactError) {
|
|
535
557
|
this.consecutiveCompactionFailures++;
|
|
536
558
|
logger?.error(`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive): ${compactError instanceof Error ? compactError.message : String(compactError)}`);
|
|
537
|
-
|
|
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
|
+
}
|
|
538
563
|
}
|
|
539
564
|
finally {
|
|
540
565
|
this.setIsCompacting(false);
|
|
@@ -562,6 +587,58 @@ export class AIManager {
|
|
|
562
587
|
autoMemory: autoMemoryOptions,
|
|
563
588
|
});
|
|
564
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
|
+
}
|
|
565
642
|
resolveFilteredTools() {
|
|
566
643
|
const toolsConfig = this.getFilteredToolsConfig();
|
|
567
644
|
const toolNames = new Set(toolsConfig.map((t) => t.function.name));
|
|
@@ -596,9 +673,13 @@ export class AIManager {
|
|
|
596
673
|
forkMessages.push({ role: "user", content: prompt });
|
|
597
674
|
const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
|
|
598
675
|
const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
|
|
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 }]));
|
|
602
683
|
let totalUsage;
|
|
603
684
|
let content;
|
|
604
685
|
for (let turn = 0; turn < options.maxTurns; turn++) {
|
|
@@ -706,7 +787,7 @@ export class AIManager {
|
|
|
706
787
|
*/
|
|
707
788
|
async runAutoMemoryFork(messages, prompt, options, abortSignal) {
|
|
708
789
|
const modelConfig = this.getModelConfig();
|
|
709
|
-
const historyMessages = convertMessagesForAPI(messages, {
|
|
790
|
+
const historyMessages = await convertMessagesForAPI(messages, {
|
|
710
791
|
supportsVision: supportsVision(modelConfig.capabilities),
|
|
711
792
|
});
|
|
712
793
|
// Give the fork a real signal even when the caller has none, so tools
|
|
@@ -804,7 +885,7 @@ export class AIManager {
|
|
|
804
885
|
const lastMessage = rawMessages[rawMessages.length - 1];
|
|
805
886
|
const hasInProgressMessage = lastMessage?.role === "assistant" &&
|
|
806
887
|
lastMessage.blocks.some((b) => "stage" in b && b.stage === "streaming");
|
|
807
|
-
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) });
|
|
808
889
|
// Mirror the main loop's memory injection so the request prefix matches.
|
|
809
890
|
const { prependContent } = await this.messageManager.getMemoryForInjection();
|
|
810
891
|
if (prependContent.trim()) {
|
|
@@ -933,29 +1014,32 @@ ${question}`;
|
|
|
933
1014
|
// 4. Invoked skills context (with token budget, matching Claude Code)
|
|
934
1015
|
const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25000;
|
|
935
1016
|
const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5000;
|
|
936
|
-
const
|
|
937
|
-
if (
|
|
1017
|
+
const invokedSkills = this.messageManager.getInvokedSkills?.(10) ?? [];
|
|
1018
|
+
if (invokedSkills.length > 0 && this.skillManager) {
|
|
938
1019
|
const invokedSkillParts = [];
|
|
939
1020
|
let skillsUsedTokens = 0;
|
|
940
|
-
for (const
|
|
1021
|
+
for (const invoked of invokedSkills) {
|
|
941
1022
|
try {
|
|
942
|
-
const skill = await this.skillManager.loadSkill(skillName);
|
|
943
|
-
|
|
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)
|
|
944
1032
|
continue;
|
|
945
|
-
const contentMatch = skill.content.match(/^---\n[\s\S]*?\n---\n([\s\S]*)$/);
|
|
946
|
-
let skillContent = contentMatch
|
|
947
|
-
? contentMatch[1].trim()
|
|
948
|
-
: skill.content;
|
|
949
1033
|
const maxSkillChars = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4;
|
|
950
1034
|
if (skillContent.length > maxSkillChars) {
|
|
951
|
-
skillContent =
|
|
952
|
-
skillContent.slice(0, maxSkillChars) + "\n\n...[truncated]...";
|
|
1035
|
+
skillContent = `${skillContent.slice(0, maxSkillChars)}\n\n...[truncated]...`;
|
|
953
1036
|
}
|
|
954
1037
|
const skillTokens = estimateTokens(skillContent);
|
|
955
1038
|
if (skillsUsedTokens + skillTokens > POST_COMPACT_SKILLS_TOKEN_BUDGET)
|
|
956
1039
|
break;
|
|
957
1040
|
skillsUsedTokens += skillTokens;
|
|
958
|
-
|
|
1041
|
+
const description = skill?.description;
|
|
1042
|
+
invokedSkillParts.push(`\n\n## ${skill?.name ?? invoked.skillName}\n${description ? `*${description}*\n\n` : ""}\`\`\`\n${skillContent}\n\`\`\``);
|
|
959
1043
|
}
|
|
960
1044
|
catch {
|
|
961
1045
|
// Skip skills that can't be loaded
|
|
@@ -1030,9 +1114,22 @@ ${question}`;
|
|
|
1030
1114
|
get skillManager() {
|
|
1031
1115
|
return this.container.get("SkillManager");
|
|
1032
1116
|
}
|
|
1117
|
+
get liveConfigManager() {
|
|
1118
|
+
return this.container.get("LiveConfigManager");
|
|
1119
|
+
}
|
|
1033
1120
|
async sendAIMessage(options = {}) {
|
|
1034
1121
|
const { recursionDepth = 0, model, allowedRules, maxTokens } = options;
|
|
1035
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
|
+
}
|
|
1036
1133
|
// Reserve this turn synchronously before any async work. Bumping the
|
|
1037
1134
|
// generation invalidates any in-flight turn's end-of-turn cleanup, and
|
|
1038
1135
|
// recording myGeneration lets us skip cleanup if a newer turn (or abort)
|
|
@@ -1132,14 +1229,33 @@ ${question}`;
|
|
|
1132
1229
|
isMeta: true,
|
|
1133
1230
|
});
|
|
1134
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
|
+
}
|
|
1135
1245
|
// Pre-request auto-compaction: estimate the context about to be sent
|
|
1136
1246
|
// and compact BEFORE issuing the request, so an over-limit request
|
|
1137
1247
|
// never goes out. Skipped on fork paths (they use runForkLoop).
|
|
1138
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();
|
|
1139
1255
|
// Get recent message history
|
|
1140
1256
|
const rawMessages = this.messageManager.getMessages();
|
|
1141
1257
|
const currentModelConfig = this.getModelConfig();
|
|
1142
|
-
const recentMessages = convertMessagesForAPI(rawMessages, {
|
|
1258
|
+
const recentMessages = await convertMessagesForAPI(rawMessages, {
|
|
1143
1259
|
supportsVision: supportsVision(currentModelConfig.capabilities),
|
|
1144
1260
|
});
|
|
1145
1261
|
// Track if assistant message has been created
|
|
@@ -1183,6 +1299,19 @@ ${question}`;
|
|
|
1183
1299
|
isMeta: true,
|
|
1184
1300
|
});
|
|
1185
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
|
+
}
|
|
1186
1315
|
// Add streaming callbacks only if streaming is enabled
|
|
1187
1316
|
if (this.stream) {
|
|
1188
1317
|
callAgentOptions.onContentUpdate = (content) => {
|
|
@@ -1579,6 +1708,9 @@ ${question}`;
|
|
|
1579
1708
|
if (myGeneration === this.turnGeneration) {
|
|
1580
1709
|
this.setIsLoading(false);
|
|
1581
1710
|
}
|
|
1711
|
+
if (ownsTurnSnapshot) {
|
|
1712
|
+
this.liveConfigManager?.onTurnEnd();
|
|
1713
|
+
}
|
|
1582
1714
|
break outer;
|
|
1583
1715
|
}
|
|
1584
1716
|
}
|
|
@@ -1838,6 +1970,10 @@ ${question}`;
|
|
|
1838
1970
|
images: toolImages,
|
|
1839
1971
|
timestamp: Date.now(),
|
|
1840
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);
|
|
1841
1977
|
// Execute PostToolUse hooks after successful tool completion
|
|
1842
1978
|
await this.executePostToolUseHooks(toolId, toolName, toolArgs, toolResult);
|
|
1843
1979
|
}
|
|
@@ -1856,6 +1992,39 @@ ${question}`;
|
|
|
1856
1992
|
});
|
|
1857
1993
|
}
|
|
1858
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
|
+
}
|
|
1859
2028
|
/**
|
|
1860
2029
|
* Execute PreToolUse hooks before tool execution
|
|
1861
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 {
|
|
@@ -13,6 +13,12 @@ export declare class HookManager {
|
|
|
13
13
|
private container;
|
|
14
14
|
private configuration;
|
|
15
15
|
private programmaticHooks;
|
|
16
|
+
/**
|
|
17
|
+
* Plugin hooks grouped by plugin root path. A map rather than one merged
|
|
18
|
+
* object because mergeHooksConfiguration only appends — a flat object can
|
|
19
|
+
* never be reversed, so the in-place plugin reload path could not drop
|
|
20
|
+
* exactly one plugin's hooks (and re-registering would double them).
|
|
21
|
+
*/
|
|
16
22
|
private pluginHooks;
|
|
17
23
|
private waveConfigHooks;
|
|
18
24
|
private readonly matcher;
|
|
@@ -120,6 +126,13 @@ export declare class HookManager {
|
|
|
120
126
|
* Register hooks provided by a plugin
|
|
121
127
|
*/
|
|
122
128
|
registerPluginHooks(pluginRoot: string, hooks: PartialHookConfiguration): void;
|
|
129
|
+
/**
|
|
130
|
+
* Drop every hook contributed by a plugin, addressed by its root path.
|
|
131
|
+
* Used by the in-place plugin reload path (before re-registering) and when a
|
|
132
|
+
* plugin is disabled or uninstalled.
|
|
133
|
+
* @returns true if this root had hooks registered
|
|
134
|
+
*/
|
|
135
|
+
unregisterPluginHooks(pluginRoot: string): boolean;
|
|
123
136
|
/**
|
|
124
137
|
* Get hooks registered by plugins (read-only view for the settings UI).
|
|
125
138
|
* Plugin hooks carry pluginRoot on each command and are read-only in the UI.
|
|
@@ -13,7 +13,13 @@ export class HookManager {
|
|
|
13
13
|
constructor(container, workdir, matcher = new HookMatcher()) {
|
|
14
14
|
this.container = container;
|
|
15
15
|
this.programmaticHooks = {};
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Plugin hooks grouped by plugin root path. A map rather than one merged
|
|
18
|
+
* object because mergeHooksConfiguration only appends — a flat object can
|
|
19
|
+
* never be reversed, so the in-place plugin reload path could not drop
|
|
20
|
+
* exactly one plugin's hooks (and re-registering would double them).
|
|
21
|
+
*/
|
|
22
|
+
this.pluginHooks = new Map();
|
|
17
23
|
this.waveConfigHooks = {};
|
|
18
24
|
this.workdir = workdir;
|
|
19
25
|
this.matcher = matcher;
|
|
@@ -78,7 +84,9 @@ export class HookManager {
|
|
|
78
84
|
rebuildConfiguration() {
|
|
79
85
|
const rebuilt = {};
|
|
80
86
|
this.mergeHooksConfiguration(rebuilt, this.programmaticHooks);
|
|
81
|
-
|
|
87
|
+
for (const hooks of this.pluginHooks.values()) {
|
|
88
|
+
this.mergeHooksConfiguration(rebuilt, hooks);
|
|
89
|
+
}
|
|
82
90
|
this.mergeHooksConfiguration(rebuilt, this.waveConfigHooks);
|
|
83
91
|
this.configuration = Object.keys(rebuilt).length > 0 ? rebuilt : undefined;
|
|
84
92
|
}
|
|
@@ -667,15 +675,34 @@ export class HookManager {
|
|
|
667
675
|
hooks: config.hooks.map((cmd) => ({ ...cmd, pluginRoot })),
|
|
668
676
|
}));
|
|
669
677
|
}
|
|
670
|
-
|
|
678
|
+
// Replace (not append) the previous registration for this root, so
|
|
679
|
+
// re-registering the same plugin never doubles its hooks.
|
|
680
|
+
this.pluginHooks.set(pluginRoot, stampedHooks);
|
|
671
681
|
this.rebuildConfiguration();
|
|
672
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* Drop every hook contributed by a plugin, addressed by its root path.
|
|
685
|
+
* Used by the in-place plugin reload path (before re-registering) and when a
|
|
686
|
+
* plugin is disabled or uninstalled.
|
|
687
|
+
* @returns true if this root had hooks registered
|
|
688
|
+
*/
|
|
689
|
+
unregisterPluginHooks(pluginRoot) {
|
|
690
|
+
const removed = this.pluginHooks.delete(pluginRoot);
|
|
691
|
+
if (removed) {
|
|
692
|
+
this.rebuildConfiguration();
|
|
693
|
+
}
|
|
694
|
+
return removed;
|
|
695
|
+
}
|
|
673
696
|
/**
|
|
674
697
|
* Get hooks registered by plugins (read-only view for the settings UI).
|
|
675
698
|
* Plugin hooks carry pluginRoot on each command and are read-only in the UI.
|
|
676
699
|
*/
|
|
677
700
|
getPluginHooks() {
|
|
678
|
-
|
|
701
|
+
const merged = {};
|
|
702
|
+
for (const hooks of this.pluginHooks.values()) {
|
|
703
|
+
this.mergeHooksConfiguration(merged, hooks);
|
|
704
|
+
}
|
|
705
|
+
return merged;
|
|
679
706
|
}
|
|
680
707
|
/**
|
|
681
708
|
* Execute SessionStart hooks during initialization.
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Configuration file watching for settings.json files
|
|
7
7
|
* - Coordination between file watchers and configuration updates
|
|
8
8
|
*/
|
|
9
|
+
import type { TurnConfigurationSnapshot } from "../services/configurationService.js";
|
|
9
10
|
import { Container } from "../utils/container.js";
|
|
10
11
|
import type { WaveConfiguration } from "../types/configuration.js";
|
|
11
12
|
export interface LiveConfigManagerOptions {
|
|
@@ -19,6 +20,8 @@ export declare class LiveConfigManager {
|
|
|
19
20
|
private isInitialized;
|
|
20
21
|
private currentConfiguration;
|
|
21
22
|
private lastValidConfiguration;
|
|
23
|
+
private turnSnapshot;
|
|
24
|
+
private turnDepth;
|
|
22
25
|
private fileWatcher;
|
|
23
26
|
private userConfigPaths?;
|
|
24
27
|
private projectConfigPaths?;
|
|
@@ -28,6 +31,15 @@ export declare class LiveConfigManager {
|
|
|
28
31
|
private get hookManager();
|
|
29
32
|
private get permissionManager();
|
|
30
33
|
private get configurationService();
|
|
34
|
+
private get memoryService();
|
|
35
|
+
/**
|
|
36
|
+
* Keep the auto-memory system safe zone in sync with the live auto-memory
|
|
37
|
+
* toggle (core/agent-config.md scenario 6). Turning auto-memory off must
|
|
38
|
+
* revoke the privilege its directory got at construction time, and turning it
|
|
39
|
+
* back on must restore it — both without rebuilding the session. Idempotent:
|
|
40
|
+
* the underlying add/remove are dedup'ed by path.
|
|
41
|
+
*/
|
|
42
|
+
private syncAutoMemorySafeZone;
|
|
31
43
|
/**
|
|
32
44
|
* Initialize configuration watching
|
|
33
45
|
* Maps to FR-004: System MUST watch settings.json files
|
|
@@ -46,6 +58,19 @@ export declare class LiveConfigManager {
|
|
|
46
58
|
* Shutdown configuration management and cleanup resources
|
|
47
59
|
*/
|
|
48
60
|
shutdown(): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Turn boundary: pin the settings.json-derived configuration for the duration
|
|
63
|
+
* of a turn (called by AIManager). A live reload may land mid-turn and the
|
|
64
|
+
* turn must not see it (core/agent-config.md scenario 5) — the change is
|
|
65
|
+
* picked up by the snapshot taken when the next turn starts. Permission rules,
|
|
66
|
+
* hooks and env stay live: those are enforcement state and follow the file
|
|
67
|
+
* immediately.
|
|
68
|
+
*/
|
|
69
|
+
onTurnStart(): void;
|
|
70
|
+
/** Turn boundary: drop the snapshot so the next turn reads live values. */
|
|
71
|
+
onTurnEnd(): void;
|
|
72
|
+
/** Configuration snapshot of the running turn, or null outside a turn. */
|
|
73
|
+
getTurnSnapshot(): TurnConfigurationSnapshot | null;
|
|
49
74
|
/**
|
|
50
75
|
* Reload configuration from files
|
|
51
76
|
* Maps to FR-008: Continue with previous valid configuration on errors
|
|
@@ -77,6 +102,14 @@ export declare class LiveConfigManager {
|
|
|
77
102
|
private setupFileWatcherEvents;
|
|
78
103
|
private handleFileChange;
|
|
79
104
|
private detectChanges;
|
|
105
|
+
/**
|
|
106
|
+
* Chokidar only starts watching a non-existent path when its parent directory
|
|
107
|
+
* exists at watch time (a path whose parents are all missing is never
|
|
108
|
+
* picked up, not even after they appear). Walk up to the deepest node whose
|
|
109
|
+
* parent exists and watch that instead — events for its descendants still
|
|
110
|
+
* reach this path's callback (FileWatcherService matches by path prefix).
|
|
111
|
+
*/
|
|
112
|
+
private resolveWatchTarget;
|
|
80
113
|
/**
|
|
81
114
|
* Get configuration file paths for user and project settings
|
|
82
115
|
* Returns paths in priority order (local.json first, then .json)
|