wave-agent-sdk 0.19.8 → 1.0.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/builtin/plugins/sdd/.wave-plugin/plugin.json +8 -0
- package/builtin/plugins/sdd/hooks/hooks.json +14 -0
- package/builtin/plugins/sdd/scripts/session-start.js +24 -0
- package/builtin/plugins/sdd/scripts/spec-count.js +77 -0
- package/builtin/plugins/sdd/skills/specify/SKILL.md +47 -0
- package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +47 -0
- package/builtin/skills/settings/ENV.md +15 -9
- package/builtin/skills/settings/HOOKS.md +27 -2
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +26 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts +25 -0
- package/dist/managers/aiManager.js +172 -51
- package/dist/managers/backgroundTaskManager.d.ts +6 -0
- package/dist/managers/backgroundTaskManager.js +11 -0
- package/dist/managers/bangManager.d.ts +6 -0
- package/dist/managers/bangManager.js +11 -0
- package/dist/managers/hookManager.d.ts +8 -2
- package/dist/managers/hookManager.js +14 -4
- package/dist/managers/mcpManager.d.ts +18 -4
- package/dist/managers/mcpManager.js +40 -18
- package/dist/managers/permissionManager.d.ts +7 -0
- package/dist/managers/permissionManager.js +102 -142
- package/dist/managers/pluginManager.d.ts +7 -0
- package/dist/managers/pluginManager.js +31 -0
- package/dist/managers/toolManager.js +5 -0
- package/dist/prompts/index.d.ts +12 -1
- package/dist/prompts/index.js +133 -45
- package/dist/services/aiService.d.ts +1 -17
- package/dist/services/aiService.js +3 -85
- package/dist/services/configurationService.d.ts +21 -2
- package/dist/services/configurationService.js +72 -23
- package/dist/services/initializationService.js +14 -4
- package/dist/services/interactionService.js +35 -7
- package/dist/services/remoteSettingsService.d.ts +12 -0
- package/dist/services/remoteSettingsService.js +15 -1
- package/dist/services/session.d.ts +3 -1
- package/dist/services/session.js +12 -4
- package/dist/services/taskManager.d.ts +1 -0
- package/dist/services/taskManager.js +41 -6
- package/dist/tools/bashTool.js +1 -0
- package/dist/tools/editTool.js +24 -10
- package/dist/tools/enterWorktreeTool.js +14 -3
- package/dist/tools/exitWorktreeTool.js +11 -10
- package/dist/tools/grepTool.js +8 -2
- package/dist/tools/types.d.ts +7 -0
- package/dist/tools/writeTool.js +36 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/hooks.d.ts +2 -2
- package/dist/utils/bashParser.d.ts +25 -0
- package/dist/utils/bashParser.js +103 -0
- package/dist/utils/configPaths.d.ts +4 -0
- package/dist/utils/configPaths.js +6 -0
- package/dist/utils/containerSetup.js +1 -1
- package/dist/utils/fileSearch.js +4 -2
- package/dist/utils/openaiClient.js +2 -1
- package/dist/utils/pathEncoder.js +7 -2
- package/dist/utils/worktreeUtils.d.ts +17 -0
- package/dist/utils/worktreeUtils.js +339 -1
- package/package.json +1 -1
- package/src/agent.ts +26 -12
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +238 -66
- package/src/managers/backgroundTaskManager.ts +15 -0
- package/src/managers/bangManager.ts +15 -0
- package/src/managers/hookManager.ts +20 -5
- package/src/managers/mcpManager.ts +60 -18
- package/src/managers/permissionManager.ts +116 -168
- package/src/managers/pluginManager.ts +29 -0
- package/src/managers/toolManager.ts +7 -0
- package/src/prompts/index.ts +144 -37
- package/src/services/aiService.ts +9 -128
- package/src/services/configurationService.ts +84 -23
- package/src/services/initializationService.ts +17 -4
- package/src/services/interactionService.ts +49 -6
- package/src/services/remoteSettingsService.ts +16 -1
- package/src/services/session.ts +18 -4
- package/src/services/taskManager.ts +56 -8
- package/src/tools/bashTool.ts +1 -0
- package/src/tools/editTool.ts +29 -11
- package/src/tools/enterWorktreeTool.ts +19 -2
- package/src/tools/exitWorktreeTool.ts +15 -12
- package/src/tools/grepTool.ts +11 -2
- package/src/tools/types.ts +7 -0
- package/src/tools/writeTool.ts +43 -0
- package/src/types/config.ts +2 -0
- package/src/types/hooks.ts +2 -2
- package/src/utils/bashParser.ts +106 -0
- package/src/utils/configPaths.ts +7 -0
- package/src/utils/containerSetup.ts +3 -1
- package/src/utils/fileSearch.ts +6 -2
- package/src/utils/openaiClient.ts +2 -0
- package/src/utils/pathEncoder.ts +7 -2
- package/src/utils/worktreeUtils.ts +401 -1
|
@@ -6,8 +6,8 @@ import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js"
|
|
|
6
6
|
import { estimateTokens } from "../utils/tokenEstimate.js";
|
|
7
7
|
import { getTaskReminderTurnCounts, maybeInjectTaskReminder, TASK_REMINDER_CONFIG, } from "../utils/taskReminder.js";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { buildSystemPrompt } from "../prompts/index.js";
|
|
10
|
-
import { buildPlanModeReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, } from "../prompts/planModeReminders.js";
|
|
9
|
+
import { buildSystemPrompt, formatCompactSummary, getCompactPrompt, } from "../prompts/index.js";
|
|
10
|
+
import { buildPlanModeReminder, buildPlanModeReEntryReminder, buildExitedPlanModeReminder, wrapInSystemReminder, } from "../prompts/planModeReminders.js";
|
|
11
11
|
import { recoverTruncatedJson } from "../utils/stringUtils.js";
|
|
12
12
|
import { logger } from "../utils/globalLogger.js";
|
|
13
13
|
import { startInteractionSpan, endInteractionSpan, startLLMRequestSpan, endLLMRequestSpan, resetTracingState, } from "../telemetry/sessionTracing.js";
|
|
@@ -99,9 +99,24 @@ export class AIManager {
|
|
|
99
99
|
get configurationService() {
|
|
100
100
|
return this.container.get("ConfigurationService");
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* OS env merged with the per-session env snapshot. Falls back to process.env
|
|
104
|
+
* when ConfigurationService is absent or its getMergedEnv is missing (e.g. in
|
|
105
|
+
* unit tests with partial mocks), so hook-context env construction never
|
|
106
|
+
* throws. Use this (not the non-null `configurationService` getter) when
|
|
107
|
+
* building hook context env.
|
|
108
|
+
*/
|
|
109
|
+
get mergedEnv() {
|
|
110
|
+
return (this.container
|
|
111
|
+
.get("ConfigurationService")
|
|
112
|
+
?.getMergedEnv?.() ?? process.env);
|
|
113
|
+
}
|
|
102
114
|
// Getter methods for accessing dynamic configuration
|
|
103
115
|
getGatewayConfig() {
|
|
104
|
-
return
|
|
116
|
+
return {
|
|
117
|
+
...this.configurationService.resolveGatewayConfig(),
|
|
118
|
+
sessionId: this.messageManager.getSessionId(),
|
|
119
|
+
};
|
|
105
120
|
}
|
|
106
121
|
getModelConfig() {
|
|
107
122
|
const permissionMode = this.container.has("PermissionMode")
|
|
@@ -348,30 +363,35 @@ export class AIManager {
|
|
|
348
363
|
await this.messageManager.saveSession();
|
|
349
364
|
this.setIsCompacting(true);
|
|
350
365
|
try {
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
gatewayConfig: this.getGatewayConfig(),
|
|
355
|
-
modelConfig: this.getModelConfig(),
|
|
356
|
-
messages: recentChatMessages,
|
|
357
|
-
abortSignal: options.abortSignal,
|
|
358
|
-
model: this.getModelConfig().fastModel,
|
|
359
|
-
customInstructions: mergedInstructions,
|
|
366
|
+
const modelConfig = this.getModelConfig();
|
|
367
|
+
const recentChatMessages = convertMessagesForAPI(messagesToCompact, {
|
|
368
|
+
supportsVision: supportsVision(modelConfig.capabilities),
|
|
360
369
|
});
|
|
370
|
+
const compactPrompt = getCompactPrompt(mergedInstructions);
|
|
371
|
+
// 4. Fork path: fork the conversation with the same system prompt,
|
|
372
|
+
// tools, model, and generation params as the main loop so the forked
|
|
373
|
+
// request prefix matches exactly and the prompt cache is reused.
|
|
374
|
+
const forkResult = await this.runCompactFork(recentChatMessages, compactPrompt, options.abortSignal);
|
|
375
|
+
const summaryContent = forkResult.content;
|
|
376
|
+
const compactTokens = forkResult.usage;
|
|
377
|
+
if (!summaryContent) {
|
|
378
|
+
throw new Error("Compaction failed: the model produced no summary output");
|
|
379
|
+
}
|
|
380
|
+
const compactModel = modelConfig.model;
|
|
361
381
|
// 5. Handle usage tracking
|
|
362
382
|
let compactUsage;
|
|
363
|
-
if (
|
|
383
|
+
if (compactTokens) {
|
|
364
384
|
compactUsage = {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
total_tokens: compactResult.usage.total_tokens,
|
|
368
|
-
model: this.getModelConfig().fastModel,
|
|
385
|
+
...compactTokens,
|
|
386
|
+
model: compactModel,
|
|
369
387
|
operation_type: "compact",
|
|
370
388
|
};
|
|
371
389
|
}
|
|
372
|
-
// 6.
|
|
373
|
-
const
|
|
374
|
-
// 7.
|
|
390
|
+
// 6. Strip the <analysis> scratchpad and extract the <summary> body
|
|
391
|
+
const formattedSummary = formatCompactSummary(summaryContent);
|
|
392
|
+
// 7. Build post-compact context restoration
|
|
393
|
+
const enhancedSummary = await this.buildPostCompactContext(formattedSummary);
|
|
394
|
+
// 8. Execute message reconstruction
|
|
375
395
|
await this.messageManager.compactMessagesAndUpdateSession(enhancedSummary, compactUsage);
|
|
376
396
|
// Re-add plan mode reminder as persistent meta message after compaction
|
|
377
397
|
const postCompactMode = this.permissionManager?.getCurrentEffectiveMode(this.getModelConfig().permissionMode);
|
|
@@ -385,20 +405,20 @@ export class AIManager {
|
|
|
385
405
|
});
|
|
386
406
|
}
|
|
387
407
|
}
|
|
388
|
-
//
|
|
408
|
+
// 9. Track usage
|
|
389
409
|
if (compactUsage && this.callbacks?.onUsageAdded) {
|
|
390
410
|
this.callbacks.onUsageAdded(compactUsage);
|
|
391
411
|
}
|
|
392
412
|
this.consecutiveCompactionFailures = 0;
|
|
393
413
|
// Reset incremental tracing state after compaction
|
|
394
414
|
resetTracingState();
|
|
395
|
-
//
|
|
415
|
+
// 10. Log OTEL event
|
|
396
416
|
logOTelEvent("compaction", {
|
|
397
417
|
beforeTokens: String(messagesToCompact.length),
|
|
398
418
|
afterTokens: "1",
|
|
399
|
-
model:
|
|
419
|
+
model: compactModel,
|
|
400
420
|
}).catch(() => { });
|
|
401
|
-
//
|
|
421
|
+
// 11. Run SessionStart hooks (existing behavior)
|
|
402
422
|
if (this.hookManager) {
|
|
403
423
|
try {
|
|
404
424
|
const newSessionId = this.messageManager.getSessionId();
|
|
@@ -420,10 +440,10 @@ export class AIManager {
|
|
|
420
440
|
logger?.warn(`SessionStart hooks on compact failed: ${error.message}`);
|
|
421
441
|
}
|
|
422
442
|
}
|
|
423
|
-
//
|
|
443
|
+
// 12. Run PostCompact hooks
|
|
424
444
|
if (this.hookManager) {
|
|
425
445
|
try {
|
|
426
|
-
await this.hookManager.executePostCompactHooks(this.messageManager.getSessionId(), this.messageManager.getTranscriptPath(),
|
|
446
|
+
await this.hookManager.executePostCompactHooks(this.messageManager.getSessionId(), this.messageManager.getTranscriptPath(), formattedSummary);
|
|
427
447
|
}
|
|
428
448
|
catch (error) {
|
|
429
449
|
logger?.warn(`PostCompact hooks failed: ${error.message}`);
|
|
@@ -440,6 +460,115 @@ export class AIManager {
|
|
|
440
460
|
this.setIsCompacting(false);
|
|
441
461
|
}
|
|
442
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Build the system prompt used by the main agent loop. Extracted so the
|
|
465
|
+
* compaction fork can mirror it exactly — the forked request prefix must
|
|
466
|
+
* match the main conversation's for the prompt cache to be reused.
|
|
467
|
+
*/
|
|
468
|
+
async buildMainSystemPrompt(filteredToolPlugins) {
|
|
469
|
+
let autoMemoryOptions;
|
|
470
|
+
if (this.getAutoMemoryEnabled()) {
|
|
471
|
+
const directory = this.memoryService.getAutoMemoryDirectory(this.getWorkdir());
|
|
472
|
+
const content = await this.memoryService.getAutoMemoryContent(this.getWorkdir());
|
|
473
|
+
autoMemoryOptions = { directory, content };
|
|
474
|
+
}
|
|
475
|
+
return buildSystemPrompt(this.systemPrompt, filteredToolPlugins, {
|
|
476
|
+
workdir: this.getWorkdir(),
|
|
477
|
+
originalWorkdir: this.getOriginalWorkdir(),
|
|
478
|
+
language: this.getLanguage(),
|
|
479
|
+
isSubagent: !!this.subagentType,
|
|
480
|
+
worktreeSession: this.getWorktreeSession(),
|
|
481
|
+
autoMemory: autoMemoryOptions,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
resolveFilteredTools() {
|
|
485
|
+
const toolsConfig = this.getFilteredToolsConfig();
|
|
486
|
+
const toolNames = new Set(toolsConfig.map((t) => t.function.name));
|
|
487
|
+
const filteredToolPlugins = this.toolManager
|
|
488
|
+
.getTools()
|
|
489
|
+
.filter((t) => toolNames.has(t.name));
|
|
490
|
+
return { toolsConfig, toolNames, filteredToolPlugins };
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Fork-path compaction: run a bounded agent loop over a copy of the
|
|
494
|
+
* conversation using the same system prompt, tools, model, and generation
|
|
495
|
+
* params as the main loop, so the forked request prefix matches exactly
|
|
496
|
+
* and the prompt cache is reused. Tool calls are denied locally (the model
|
|
497
|
+
* is told to summarize, not act) and their rejections are fed back for
|
|
498
|
+
* another turn. Returns undefined content when the model never produces
|
|
499
|
+
* text; the caller treats that as a compaction failure.
|
|
500
|
+
*/
|
|
501
|
+
async runCompactFork(historyMessages, compactPrompt, abortSignal) {
|
|
502
|
+
const MAX_FORK_TURNS = 3;
|
|
503
|
+
const modelConfig = this.getModelConfig();
|
|
504
|
+
const gatewayConfig = this.getGatewayConfig();
|
|
505
|
+
const sessionId = this.messageManager.getSessionId();
|
|
506
|
+
const workdir = this.getWorkdir();
|
|
507
|
+
const forkMessages = [...historyMessages];
|
|
508
|
+
// Mirror the main loop's memory injection so the request prefix matches.
|
|
509
|
+
const { prependContent } = await this.messageManager.getMemoryForInjection();
|
|
510
|
+
if (prependContent.trim()) {
|
|
511
|
+
forkMessages.unshift({
|
|
512
|
+
role: "user",
|
|
513
|
+
content: wrapInSystemReminder(prependContent),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
forkMessages.push({ role: "user", content: compactPrompt });
|
|
517
|
+
const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
|
|
518
|
+
const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
519
|
+
let totalUsage;
|
|
520
|
+
let content;
|
|
521
|
+
for (let turn = 0; turn < MAX_FORK_TURNS; turn++) {
|
|
522
|
+
const result = await aiService.callAgent({
|
|
523
|
+
gatewayConfig,
|
|
524
|
+
modelConfig,
|
|
525
|
+
messages: forkMessages,
|
|
526
|
+
sessionId,
|
|
527
|
+
abortSignal,
|
|
528
|
+
workdir,
|
|
529
|
+
tools: toolsConfig,
|
|
530
|
+
systemPrompt,
|
|
531
|
+
toolChoice: this.toolChoiceOverride,
|
|
532
|
+
// Stream so a slow reasoning model emits first bytes before the
|
|
533
|
+
// gateway's idle timeout fires (non-streaming waits for the full
|
|
534
|
+
// summary, which exceeds the timeout on large contexts).
|
|
535
|
+
stream: true,
|
|
536
|
+
});
|
|
537
|
+
if (result.usage) {
|
|
538
|
+
totalUsage = {
|
|
539
|
+
prompt_tokens: (totalUsage?.prompt_tokens ?? 0) + result.usage.prompt_tokens,
|
|
540
|
+
completion_tokens: (totalUsage?.completion_tokens ?? 0) +
|
|
541
|
+
result.usage.completion_tokens,
|
|
542
|
+
total_tokens: (totalUsage?.total_tokens ?? 0) + result.usage.total_tokens,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
if (result.content?.trim()) {
|
|
546
|
+
content = result.content;
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
if (result.tool_calls && result.tool_calls.length > 0) {
|
|
550
|
+
// Deny all tool calls locally and feed the rejections back so the
|
|
551
|
+
// model gets another turn to produce the summary text.
|
|
552
|
+
forkMessages.push({
|
|
553
|
+
role: "assistant",
|
|
554
|
+
content: result.content ?? null,
|
|
555
|
+
tool_calls: result.tool_calls,
|
|
556
|
+
});
|
|
557
|
+
for (const toolCall of result.tool_calls) {
|
|
558
|
+
forkMessages.push({
|
|
559
|
+
role: "tool",
|
|
560
|
+
tool_call_id: toolCall.id,
|
|
561
|
+
content: "Tool use is not allowed during compaction",
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
// Neither text nor tool calls: retrying the identical request is
|
|
567
|
+
// pointless, bail out and let the caller fail the compaction.
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
return { content, usage: totalUsage };
|
|
571
|
+
}
|
|
443
572
|
/**
|
|
444
573
|
* Build post-compact context restoration content.
|
|
445
574
|
* Restores file reads, working directory, plan mode, skills, and background tasks.
|
|
@@ -573,7 +702,7 @@ export class AIManager {
|
|
|
573
702
|
// has superseded us. setIsLoading(true) here also closes the "idle but
|
|
574
703
|
// previous turn not finished" race window by marking us busy immediately.
|
|
575
704
|
this.turnGeneration++;
|
|
576
|
-
|
|
705
|
+
let myGeneration = this.turnGeneration;
|
|
577
706
|
this.setIsLoading(true);
|
|
578
707
|
outer: while (true) {
|
|
579
708
|
let shouldRestart = false;
|
|
@@ -659,19 +788,10 @@ export class AIManager {
|
|
|
659
788
|
// Track if assistant message has been created
|
|
660
789
|
let assistantMessageCreated = false;
|
|
661
790
|
logger?.debug("modelConfig in sendAIMessage", this.getModelConfig());
|
|
662
|
-
const toolsConfig = this.
|
|
663
|
-
const toolNames = new Set(toolsConfig.map((t) => t.function.name));
|
|
664
|
-
const filteredToolPlugins = this.toolManager
|
|
665
|
-
.getTools()
|
|
666
|
-
.filter((t) => toolNames.has(t.name));
|
|
667
|
-
let autoMemoryOptions;
|
|
668
|
-
if (this.getAutoMemoryEnabled()) {
|
|
669
|
-
const directory = this.memoryService.getAutoMemoryDirectory(this.getWorkdir());
|
|
670
|
-
const content = await this.memoryService.getAutoMemoryContent(this.getWorkdir());
|
|
671
|
-
autoMemoryOptions = { directory, content };
|
|
672
|
-
}
|
|
791
|
+
const { toolsConfig, toolNames, filteredToolPlugins } = this.resolveFilteredTools();
|
|
673
792
|
// Get memory for message-array injection (not system prompt)
|
|
674
793
|
const { prependContent } = await this.messageManager.getMemoryForInjection();
|
|
794
|
+
const mainSystemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
675
795
|
// Call AI service with streaming callbacks if enabled
|
|
676
796
|
const callAgentOptions = {
|
|
677
797
|
gatewayConfig: this.getGatewayConfig(),
|
|
@@ -682,14 +802,7 @@ export class AIManager {
|
|
|
682
802
|
workdir: this.getWorkdir(), // Pass working directory
|
|
683
803
|
tools: toolsConfig, // Pass filtered tool configuration
|
|
684
804
|
model: model, // Use passed model
|
|
685
|
-
systemPrompt:
|
|
686
|
-
workdir: this.getWorkdir(),
|
|
687
|
-
originalWorkdir: this.getOriginalWorkdir(),
|
|
688
|
-
language: this.getLanguage(),
|
|
689
|
-
isSubagent: !!this.subagentType,
|
|
690
|
-
worktreeSession: this.getWorktreeSession(),
|
|
691
|
-
autoMemory: autoMemoryOptions,
|
|
692
|
-
}), // Pass custom system prompt
|
|
805
|
+
systemPrompt: mainSystemPrompt, // Pass custom system prompt
|
|
693
806
|
maxTokens: maxTokens, // Pass max tokens override
|
|
694
807
|
toolChoice: this.toolChoiceOverride, // Pass tool_choice override
|
|
695
808
|
};
|
|
@@ -697,7 +810,7 @@ export class AIManager {
|
|
|
697
810
|
if (prependContent.trim()) {
|
|
698
811
|
callAgentOptions.messages.unshift({
|
|
699
812
|
role: "user",
|
|
700
|
-
content:
|
|
813
|
+
content: wrapInSystemReminder(prependContent),
|
|
701
814
|
});
|
|
702
815
|
}
|
|
703
816
|
// Task reminder: persist as meta message (conditional rules already persisted above)
|
|
@@ -1087,6 +1200,14 @@ export class AIManager {
|
|
|
1087
1200
|
});
|
|
1088
1201
|
}
|
|
1089
1202
|
}
|
|
1203
|
+
// Re-assert loading state before restarting: if this turn was
|
|
1204
|
+
// aborted, abortAIMessage already reset loading to false, and the
|
|
1205
|
+
// restart below continues the conversation — the UI must show
|
|
1206
|
+
// streaming again (cursor, stop button, ESC handling).
|
|
1207
|
+
this.setIsLoading(true);
|
|
1208
|
+
// Adopt the current (possibly abort-bumped) generation so this
|
|
1209
|
+
// continued turn's end-of-turn cleanup is not skipped as superseded.
|
|
1210
|
+
myGeneration = this.turnGeneration;
|
|
1090
1211
|
// Restart outer loop to process the notifications
|
|
1091
1212
|
shouldRestart = true;
|
|
1092
1213
|
turnOffset = 0;
|
|
@@ -1156,7 +1277,7 @@ export class AIManager {
|
|
|
1156
1277
|
sessionCrons, // Stop-only: session cron jobs snapshot
|
|
1157
1278
|
lastAssistantMessage: lastAssistantText, // Stop/SubagentStop: last assistant message text
|
|
1158
1279
|
// Stop hooks don't need toolName, toolInput, toolResponse, or userPrompt
|
|
1159
|
-
env: Object.fromEntries(Object.entries(
|
|
1280
|
+
env: Object.fromEntries(Object.entries(this.mergedEnv).filter((e) => e[1] !== undefined)), // Include environment variables
|
|
1160
1281
|
};
|
|
1161
1282
|
const results = await this.hookManager.executeHooks(hookName, context);
|
|
1162
1283
|
// Process hook results to handle exit codes and appropriate responses
|
|
@@ -1311,7 +1432,7 @@ export class AIManager {
|
|
|
1311
1432
|
if (this.hookManager) {
|
|
1312
1433
|
const sessionId = this.messageManager.getSessionId();
|
|
1313
1434
|
const transcriptPath = this.messageManager.getTranscriptPath();
|
|
1314
|
-
const env = Object.fromEntries(Object.entries(
|
|
1435
|
+
const env = Object.fromEntries(Object.entries(this.mergedEnv).filter((e) => e[1] !== undefined));
|
|
1315
1436
|
await this.hookManager.executeCwdChangedHooks(oldCwd, newCwd, sessionId, transcriptPath, env);
|
|
1316
1437
|
}
|
|
1317
1438
|
},
|
|
@@ -1378,7 +1499,7 @@ export class AIManager {
|
|
|
1378
1499
|
cwd: this.getWorkdir(),
|
|
1379
1500
|
toolInput,
|
|
1380
1501
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
1381
|
-
env: Object.fromEntries(Object.entries(
|
|
1502
|
+
env: Object.fromEntries(Object.entries(this.mergedEnv).filter((e) => e[1] !== undefined)), // Include environment variables
|
|
1382
1503
|
};
|
|
1383
1504
|
const results = await this.hookManager.executeHooks("PreToolUse", context);
|
|
1384
1505
|
// Process hook results to handle exit codes and determine if tool should be blocked
|
|
@@ -1431,7 +1552,7 @@ export class AIManager {
|
|
|
1431
1552
|
toolResponse,
|
|
1432
1553
|
subagentType: this.subagentType, // Include subagent type in hook context
|
|
1433
1554
|
planFilePath: this.permissionManager?.getPlanFilePath(),
|
|
1434
|
-
env: Object.fromEntries(Object.entries(
|
|
1555
|
+
env: Object.fromEntries(Object.entries(this.mergedEnv).filter((e) => e[1] !== undefined)), // Include environment variables
|
|
1435
1556
|
};
|
|
1436
1557
|
const results = await this.hookManager.executeHooks("PostToolUse", context);
|
|
1437
1558
|
// Process hook results to handle exit codes and update tool results
|
|
@@ -15,6 +15,12 @@ export declare class BackgroundTaskManager {
|
|
|
15
15
|
private callbacks;
|
|
16
16
|
private workdir;
|
|
17
17
|
constructor(container: Container, options: BackgroundTaskManagerOptions);
|
|
18
|
+
/**
|
|
19
|
+
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
20
|
+
* background-task subprocesses, so settings `env` vars reach them without
|
|
21
|
+
* polluting other sessions in one `wave --stdio` process.
|
|
22
|
+
*/
|
|
23
|
+
private get sessionEnv();
|
|
18
24
|
/**
|
|
19
25
|
* Fire the onBackgroundTasksChange callback so UI consumers refresh.
|
|
20
26
|
* Public so other managers (e.g. WorkflowManager) can trigger a refresh
|
|
@@ -13,6 +13,16 @@ export class BackgroundTaskManager {
|
|
|
13
13
|
this.callbacks = options.callbacks || {};
|
|
14
14
|
this.workdir = options.workdir;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
18
|
+
* background-task subprocesses, so settings `env` vars reach them without
|
|
19
|
+
* polluting other sessions in one `wave --stdio` process.
|
|
20
|
+
*/
|
|
21
|
+
get sessionEnv() {
|
|
22
|
+
return (this.container
|
|
23
|
+
.get("ConfigurationService")
|
|
24
|
+
?.getMergedEnv?.() ?? process.env);
|
|
25
|
+
}
|
|
16
26
|
/**
|
|
17
27
|
* Fire the onBackgroundTasksChange callback so UI consumers refresh.
|
|
18
28
|
* Public so other managers (e.g. WorkflowManager) can trigger a refresh
|
|
@@ -44,6 +54,7 @@ export class BackgroundTaskManager {
|
|
|
44
54
|
cwd: cwd ?? this.workdir,
|
|
45
55
|
env: {
|
|
46
56
|
...process.env,
|
|
57
|
+
...this.sessionEnv,
|
|
47
58
|
},
|
|
48
59
|
});
|
|
49
60
|
// Create log file
|
|
@@ -14,6 +14,12 @@ export declare class BangManager {
|
|
|
14
14
|
onCommandRunningChange?: (running: boolean) => void;
|
|
15
15
|
constructor(container: Container, options: BangManagerOptions);
|
|
16
16
|
private get messageManager();
|
|
17
|
+
/**
|
|
18
|
+
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
19
|
+
* bang-command subprocesses, so settings `env` vars reach them without
|
|
20
|
+
* polluting other sessions in one `wave --stdio` process.
|
|
21
|
+
*/
|
|
22
|
+
private get sessionEnv();
|
|
17
23
|
private setCommandRunning;
|
|
18
24
|
executeCommand(command: string): Promise<number>;
|
|
19
25
|
abortCommand(): void;
|
|
@@ -10,6 +10,16 @@ export class BangManager {
|
|
|
10
10
|
get messageManager() {
|
|
11
11
|
return this.container.get("MessageManager");
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Merged env (OS env overlaid with this session's settings snapshot) for
|
|
15
|
+
* bang-command subprocesses, so settings `env` vars reach them without
|
|
16
|
+
* polluting other sessions in one `wave --stdio` process.
|
|
17
|
+
*/
|
|
18
|
+
get sessionEnv() {
|
|
19
|
+
return (this.container
|
|
20
|
+
.get("ConfigurationService")
|
|
21
|
+
?.getMergedEnv?.() ?? process.env);
|
|
22
|
+
}
|
|
13
23
|
setCommandRunning(isRunning) {
|
|
14
24
|
this.isCommandRunning = isRunning;
|
|
15
25
|
this.onCommandRunningChange?.(isRunning);
|
|
@@ -28,6 +38,7 @@ export class BangManager {
|
|
|
28
38
|
cwd: this.workdir,
|
|
29
39
|
env: {
|
|
30
40
|
...process.env,
|
|
41
|
+
...this.sessionEnv,
|
|
31
42
|
},
|
|
32
43
|
});
|
|
33
44
|
this.currentProcess = child;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Central orchestrator for the hooks system. Handles configuration loading,
|
|
5
5
|
* validation, and hook execution across all supported events.
|
|
6
6
|
*/
|
|
7
|
-
import { type HookEvent, type HookExecutionContext, type ExtendedHookExecutionContext, type HookExecutionResult, type HookValidationResult, type SessionEndSource } from "../types/hooks.js";
|
|
7
|
+
import { type HookEvent, type HookExecutionContext, type ExtendedHookExecutionContext, type HookExecutionResult, type HookValidationResult, type SessionEndSource, type SessionStartSource } from "../types/hooks.js";
|
|
8
8
|
import type { WaveConfiguration, PartialHookConfiguration } from "../types/configuration.js";
|
|
9
9
|
import { HookMatcher } from "../utils/hookMatcher.js";
|
|
10
10
|
import type { MessageManager } from "./messageManager.js";
|
|
@@ -18,6 +18,12 @@ export declare class HookManager {
|
|
|
18
18
|
private readonly matcher;
|
|
19
19
|
private readonly workdir;
|
|
20
20
|
constructor(container: Container, workdir: string, matcher?: HookMatcher);
|
|
21
|
+
/**
|
|
22
|
+
* Merged env for this session (OS env overlaid with the per-session settings
|
|
23
|
+
* snapshot). Hook subprocesses spawn with this env so settings.json `env`
|
|
24
|
+
* vars reach hooks without polluting other sessions in one stdio process.
|
|
25
|
+
*/
|
|
26
|
+
private get sessionEnv();
|
|
21
27
|
/**
|
|
22
28
|
* Load hook configuration from programmatic source (AgentOptions.hooks)
|
|
23
29
|
*/
|
|
@@ -118,7 +124,7 @@ export declare class HookManager {
|
|
|
118
124
|
* Execute SessionStart hooks during initialization.
|
|
119
125
|
* Collects additionalContext and initialUserMessage from hook stdout.
|
|
120
126
|
*/
|
|
121
|
-
executeSessionStartHooks(source:
|
|
127
|
+
executeSessionStartHooks(source: SessionStartSource, sessionId: string, transcriptPath: string, agentType?: string): Promise<{
|
|
122
128
|
results: HookExecutionResult[];
|
|
123
129
|
additionalContext?: string;
|
|
124
130
|
initialUserMessage?: string;
|
|
@@ -18,6 +18,16 @@ export class HookManager {
|
|
|
18
18
|
this.workdir = workdir;
|
|
19
19
|
this.matcher = matcher;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Merged env for this session (OS env overlaid with the per-session settings
|
|
23
|
+
* snapshot). Hook subprocesses spawn with this env so settings.json `env`
|
|
24
|
+
* vars reach hooks without polluting other sessions in one stdio process.
|
|
25
|
+
*/
|
|
26
|
+
get sessionEnv() {
|
|
27
|
+
return (this.container
|
|
28
|
+
.get("ConfigurationService")
|
|
29
|
+
?.getMergedEnv?.() ?? process.env);
|
|
30
|
+
}
|
|
21
31
|
/**
|
|
22
32
|
* Load hook configuration from programmatic source (AgentOptions.hooks)
|
|
23
33
|
*/
|
|
@@ -674,7 +684,7 @@ export class HookManager {
|
|
|
674
684
|
cwd: this.workdir,
|
|
675
685
|
source,
|
|
676
686
|
agentType,
|
|
677
|
-
env: Object.fromEntries(Object.entries(
|
|
687
|
+
env: Object.fromEntries(Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined)),
|
|
678
688
|
};
|
|
679
689
|
const results = await this.executeHooks("SessionStart", context);
|
|
680
690
|
let additionalContext;
|
|
@@ -718,7 +728,7 @@ export class HookManager {
|
|
|
718
728
|
transcriptPath,
|
|
719
729
|
cwd: this.workdir,
|
|
720
730
|
endSource: source,
|
|
721
|
-
env: Object.fromEntries(Object.entries(
|
|
731
|
+
env: Object.fromEntries(Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined)),
|
|
722
732
|
};
|
|
723
733
|
const results = await this.executeHooks("SessionEnd", context);
|
|
724
734
|
// Process results but never block shutdown
|
|
@@ -740,7 +750,7 @@ export class HookManager {
|
|
|
740
750
|
transcriptPath,
|
|
741
751
|
cwd: this.workdir,
|
|
742
752
|
compactInstructions: customInstructions,
|
|
743
|
-
env: Object.fromEntries(Object.entries(
|
|
753
|
+
env: Object.fromEntries(Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined)),
|
|
744
754
|
};
|
|
745
755
|
const results = await this.executeHooks("PreCompact", context);
|
|
746
756
|
let additionalInstructions;
|
|
@@ -767,7 +777,7 @@ export class HookManager {
|
|
|
767
777
|
transcriptPath,
|
|
768
778
|
cwd: this.workdir,
|
|
769
779
|
compactSummary,
|
|
770
|
-
env: Object.fromEntries(Object.entries(
|
|
780
|
+
env: Object.fromEntries(Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined)),
|
|
771
781
|
};
|
|
772
782
|
const results = await this.executeHooks("PostCompact", context);
|
|
773
783
|
if (results.length > 0) {
|
|
@@ -11,13 +11,21 @@ export interface McpManagerOptions {
|
|
|
11
11
|
/** Pre-configured MCP servers passed from constructor options */
|
|
12
12
|
mcpServers?: Record<string, McpServerConfig>;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Expand environment variables in a string value.
|
|
16
|
+
* Supports ${VAR} and ${VAR:-default} patterns.
|
|
17
|
+
*
|
|
18
|
+
* @param env - Merged env to resolve against (session snapshot over OS env).
|
|
19
|
+
* Defaults to `process.env` for backward compatibility with tests
|
|
20
|
+
* and standalone callers.
|
|
21
|
+
*/
|
|
22
|
+
export declare function expandEnvVars(value: string, env?: Record<string, string | undefined>): string;
|
|
15
23
|
/**
|
|
16
24
|
* Walk an MCP config and resolve environment variables in all string fields.
|
|
17
|
-
*
|
|
18
|
-
* handled at spawn time
|
|
25
|
+
* Expands ${VAR} against the session env snapshot (over OS env); falls back to
|
|
26
|
+
* process.env. WAVE_PLUGIN_ROOT is skipped — handled at spawn time.
|
|
19
27
|
*/
|
|
20
|
-
export declare function resolveMcpConfig(config: McpConfig): McpConfig;
|
|
28
|
+
export declare function resolveMcpConfig(config: McpConfig, env?: Record<string, string | undefined>): McpConfig;
|
|
21
29
|
export declare class McpManager {
|
|
22
30
|
private container;
|
|
23
31
|
private config;
|
|
@@ -30,6 +38,12 @@ export declare class McpManager {
|
|
|
30
38
|
private reconnectTimers;
|
|
31
39
|
private reconnectAttempts;
|
|
32
40
|
constructor(container: Container, options?: McpManagerOptions);
|
|
41
|
+
/**
|
|
42
|
+
* Merged env for this session: OS env overlaid with the per-session settings
|
|
43
|
+
* snapshot. MCP env-var expansion (${VAR}) resolves against this so multiple
|
|
44
|
+
* sessions in one `wave --stdio` process don't read each other's settings env.
|
|
45
|
+
*/
|
|
46
|
+
private get envSnapshot();
|
|
33
47
|
/**
|
|
34
48
|
* Initialize MCP manager with working directory and optionally auto-connect
|
|
35
49
|
*/
|