wave-agent-sdk 0.19.8 → 0.19.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/builtin/plugins/sdd/.wave-plugin/plugin.json +8 -0
- package/builtin/plugins/sdd/hooks/hooks.json +14 -0
- package/builtin/plugins/sdd/scripts/session-start.js +24 -0
- package/builtin/plugins/sdd/scripts/spec-count.js +77 -0
- package/builtin/plugins/sdd/skills/specify/SKILL.md +48 -0
- package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +47 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +21 -10
- package/dist/managers/aiManager.d.ts +17 -0
- package/dist/managers/aiManager.js +152 -46
- package/dist/managers/permissionManager.d.ts +7 -0
- package/dist/managers/permissionManager.js +102 -142
- package/dist/managers/pluginManager.d.ts +7 -0
- package/dist/managers/pluginManager.js +31 -0
- package/dist/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/session.d.ts +3 -1
- package/dist/services/session.js +12 -4
- package/dist/services/taskManager.d.ts +1 -0
- package/dist/services/taskManager.js +34 -5
- package/dist/tools/editTool.js +24 -10
- package/dist/tools/grepTool.js +8 -2
- package/dist/tools/writeTool.js +36 -0
- 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/fileSearch.js +4 -2
- package/package.json +1 -1
- package/src/agent.ts +19 -10
- package/src/managers/aiManager.ts +215 -61
- package/src/managers/permissionManager.ts +116 -168
- package/src/managers/pluginManager.ts +29 -0
- package/src/prompts/index.ts +144 -37
- package/src/services/aiService.ts +9 -128
- package/src/services/session.ts +18 -4
- package/src/services/taskManager.ts +46 -7
- package/src/tools/editTool.ts +29 -11
- package/src/tools/grepTool.ts +11 -2
- package/src/tools/writeTool.ts +43 -0
- package/src/utils/bashParser.ts +106 -0
- package/src/utils/configPaths.ts +7 -0
- package/src/utils/fileSearch.ts +6 -2
|
@@ -22,7 +22,10 @@ import type { ToolManager } from "./toolManager.js";
|
|
|
22
22
|
import type { ToolContext, ToolResult } from "../tools/types.js";
|
|
23
23
|
import type { MessageManager } from "./messageManager.js";
|
|
24
24
|
import type { BackgroundTaskManager } from "./backgroundTaskManager.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
ChatCompletionMessageFunctionToolCall,
|
|
27
|
+
type ChatCompletionMessageParam,
|
|
28
|
+
} from "openai/resources.js";
|
|
26
29
|
|
|
27
30
|
import type { HookManager } from "./hookManager.js";
|
|
28
31
|
import type { ExtendedHookExecutionContext } from "../types/hooks.js";
|
|
@@ -31,11 +34,16 @@ import type { PermissionManager } from "./permissionManager.js";
|
|
|
31
34
|
import type { SubagentManager } from "./subagentManager.js";
|
|
32
35
|
import type { CronManager } from "./cronManager.js";
|
|
33
36
|
import type { SkillManager } from "./skillManager.js";
|
|
34
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
buildSystemPrompt,
|
|
39
|
+
formatCompactSummary,
|
|
40
|
+
getCompactPrompt,
|
|
41
|
+
} from "../prompts/index.js";
|
|
35
42
|
import {
|
|
36
43
|
buildPlanModeReminder,
|
|
37
44
|
buildPlanModeReEntryReminder,
|
|
38
45
|
buildExitedPlanModeReminder,
|
|
46
|
+
wrapInSystemReminder,
|
|
39
47
|
} from "../prompts/planModeReminders.js";
|
|
40
48
|
import { Container } from "../utils/container.js";
|
|
41
49
|
import type { WorktreeSession } from "../utils/worktreeSession.js";
|
|
@@ -537,36 +545,47 @@ export class AIManager {
|
|
|
537
545
|
|
|
538
546
|
this.setIsCompacting(true);
|
|
539
547
|
try {
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const compactResult = await aiService.compactMessages({
|
|
544
|
-
gatewayConfig: this.getGatewayConfig(),
|
|
545
|
-
modelConfig: this.getModelConfig(),
|
|
546
|
-
messages: recentChatMessages,
|
|
547
|
-
abortSignal: options.abortSignal,
|
|
548
|
-
model: this.getModelConfig().fastModel,
|
|
549
|
-
customInstructions: mergedInstructions,
|
|
548
|
+
const modelConfig = this.getModelConfig();
|
|
549
|
+
const recentChatMessages = convertMessagesForAPI(messagesToCompact, {
|
|
550
|
+
supportsVision: supportsVision(modelConfig.capabilities),
|
|
550
551
|
});
|
|
552
|
+
const compactPrompt = getCompactPrompt(mergedInstructions);
|
|
553
|
+
|
|
554
|
+
// 4. Fork path: fork the conversation with the same system prompt,
|
|
555
|
+
// tools, model, and generation params as the main loop so the forked
|
|
556
|
+
// request prefix matches exactly and the prompt cache is reused.
|
|
557
|
+
const forkResult = await this.runCompactFork(
|
|
558
|
+
recentChatMessages,
|
|
559
|
+
compactPrompt,
|
|
560
|
+
options.abortSignal,
|
|
561
|
+
);
|
|
562
|
+
const summaryContent = forkResult.content;
|
|
563
|
+
const compactTokens = forkResult.usage;
|
|
564
|
+
if (!summaryContent) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
"Compaction failed: the model produced no summary output",
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
const compactModel = modelConfig.model;
|
|
551
570
|
|
|
552
571
|
// 5. Handle usage tracking
|
|
553
572
|
let compactUsage: Usage | undefined;
|
|
554
|
-
if (
|
|
573
|
+
if (compactTokens) {
|
|
555
574
|
compactUsage = {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
total_tokens: compactResult.usage.total_tokens,
|
|
559
|
-
model: this.getModelConfig().fastModel,
|
|
575
|
+
...compactTokens,
|
|
576
|
+
model: compactModel,
|
|
560
577
|
operation_type: "compact",
|
|
561
578
|
};
|
|
562
579
|
}
|
|
563
580
|
|
|
564
|
-
// 6.
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
581
|
+
// 6. Strip the <analysis> scratchpad and extract the <summary> body
|
|
582
|
+
const formattedSummary = formatCompactSummary(summaryContent);
|
|
583
|
+
|
|
584
|
+
// 7. Build post-compact context restoration
|
|
585
|
+
const enhancedSummary =
|
|
586
|
+
await this.buildPostCompactContext(formattedSummary);
|
|
568
587
|
|
|
569
|
-
//
|
|
588
|
+
// 8. Execute message reconstruction
|
|
570
589
|
await this.messageManager.compactMessagesAndUpdateSession(
|
|
571
590
|
enhancedSummary,
|
|
572
591
|
compactUsage,
|
|
@@ -591,7 +610,7 @@ export class AIManager {
|
|
|
591
610
|
}
|
|
592
611
|
}
|
|
593
612
|
|
|
594
|
-
//
|
|
613
|
+
// 9. Track usage
|
|
595
614
|
if (compactUsage && this.callbacks?.onUsageAdded) {
|
|
596
615
|
this.callbacks.onUsageAdded(compactUsage);
|
|
597
616
|
}
|
|
@@ -601,14 +620,14 @@ export class AIManager {
|
|
|
601
620
|
// Reset incremental tracing state after compaction
|
|
602
621
|
resetTracingState();
|
|
603
622
|
|
|
604
|
-
//
|
|
623
|
+
// 10. Log OTEL event
|
|
605
624
|
logOTelEvent("compaction", {
|
|
606
625
|
beforeTokens: String(messagesToCompact.length),
|
|
607
626
|
afterTokens: "1",
|
|
608
|
-
model:
|
|
627
|
+
model: compactModel,
|
|
609
628
|
}).catch(() => {});
|
|
610
629
|
|
|
611
|
-
//
|
|
630
|
+
// 11. Run SessionStart hooks (existing behavior)
|
|
612
631
|
if (this.hookManager) {
|
|
613
632
|
try {
|
|
614
633
|
const newSessionId = this.messageManager.getSessionId();
|
|
@@ -638,13 +657,13 @@ export class AIManager {
|
|
|
638
657
|
}
|
|
639
658
|
}
|
|
640
659
|
|
|
641
|
-
//
|
|
660
|
+
// 12. Run PostCompact hooks
|
|
642
661
|
if (this.hookManager) {
|
|
643
662
|
try {
|
|
644
663
|
await this.hookManager.executePostCompactHooks(
|
|
645
664
|
this.messageManager.getSessionId(),
|
|
646
665
|
this.messageManager.getTranscriptPath(),
|
|
647
|
-
|
|
666
|
+
formattedSummary,
|
|
648
667
|
);
|
|
649
668
|
} catch (error) {
|
|
650
669
|
logger?.warn(`PostCompact hooks failed: ${(error as Error).message}`);
|
|
@@ -668,6 +687,158 @@ export class AIManager {
|
|
|
668
687
|
}
|
|
669
688
|
}
|
|
670
689
|
|
|
690
|
+
/**
|
|
691
|
+
* Build the system prompt used by the main agent loop. Extracted so the
|
|
692
|
+
* compaction fork can mirror it exactly — the forked request prefix must
|
|
693
|
+
* match the main conversation's for the prompt cache to be reused.
|
|
694
|
+
*/
|
|
695
|
+
private async buildMainSystemPrompt(
|
|
696
|
+
filteredToolPlugins: ReturnType<ToolManager["getTools"]>,
|
|
697
|
+
) {
|
|
698
|
+
let autoMemoryOptions: { directory: string; content: string } | undefined;
|
|
699
|
+
|
|
700
|
+
if (this.getAutoMemoryEnabled()) {
|
|
701
|
+
const directory = this.memoryService.getAutoMemoryDirectory(
|
|
702
|
+
this.getWorkdir(),
|
|
703
|
+
);
|
|
704
|
+
const content = await this.memoryService.getAutoMemoryContent(
|
|
705
|
+
this.getWorkdir(),
|
|
706
|
+
);
|
|
707
|
+
autoMemoryOptions = { directory, content };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return buildSystemPrompt(this.systemPrompt, filteredToolPlugins, {
|
|
711
|
+
workdir: this.getWorkdir(),
|
|
712
|
+
originalWorkdir: this.getOriginalWorkdir(),
|
|
713
|
+
language: this.getLanguage(),
|
|
714
|
+
isSubagent: !!this.subagentType,
|
|
715
|
+
worktreeSession: this.getWorktreeSession(),
|
|
716
|
+
autoMemory: autoMemoryOptions,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private resolveFilteredTools() {
|
|
721
|
+
const toolsConfig = this.getFilteredToolsConfig();
|
|
722
|
+
const toolNames = new Set(toolsConfig.map((t) => t.function.name));
|
|
723
|
+
const filteredToolPlugins = this.toolManager
|
|
724
|
+
.getTools()
|
|
725
|
+
.filter((t) => toolNames.has(t.name));
|
|
726
|
+
return { toolsConfig, toolNames, filteredToolPlugins };
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Fork-path compaction: run a bounded agent loop over a copy of the
|
|
731
|
+
* conversation using the same system prompt, tools, model, and generation
|
|
732
|
+
* params as the main loop, so the forked request prefix matches exactly
|
|
733
|
+
* and the prompt cache is reused. Tool calls are denied locally (the model
|
|
734
|
+
* is told to summarize, not act) and their rejections are fed back for
|
|
735
|
+
* another turn. Returns undefined content when the model never produces
|
|
736
|
+
* text; the caller treats that as a compaction failure.
|
|
737
|
+
*/
|
|
738
|
+
private async runCompactFork(
|
|
739
|
+
historyMessages: ChatCompletionMessageParam[],
|
|
740
|
+
compactPrompt: string,
|
|
741
|
+
abortSignal?: AbortSignal,
|
|
742
|
+
): Promise<{
|
|
743
|
+
content?: string;
|
|
744
|
+
usage?: {
|
|
745
|
+
prompt_tokens: number;
|
|
746
|
+
completion_tokens: number;
|
|
747
|
+
total_tokens: number;
|
|
748
|
+
};
|
|
749
|
+
}> {
|
|
750
|
+
const MAX_FORK_TURNS = 3;
|
|
751
|
+
const modelConfig = this.getModelConfig();
|
|
752
|
+
const gatewayConfig = this.getGatewayConfig();
|
|
753
|
+
const sessionId = this.messageManager.getSessionId();
|
|
754
|
+
const workdir = this.getWorkdir();
|
|
755
|
+
|
|
756
|
+
const forkMessages: ChatCompletionMessageParam[] = [...historyMessages];
|
|
757
|
+
|
|
758
|
+
// Mirror the main loop's memory injection so the request prefix matches.
|
|
759
|
+
const { prependContent } =
|
|
760
|
+
await this.messageManager.getMemoryForInjection();
|
|
761
|
+
if (prependContent.trim()) {
|
|
762
|
+
forkMessages.unshift({
|
|
763
|
+
role: "user",
|
|
764
|
+
content: wrapInSystemReminder(prependContent),
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
forkMessages.push({ role: "user", content: compactPrompt });
|
|
769
|
+
|
|
770
|
+
const { toolsConfig, filteredToolPlugins } = this.resolveFilteredTools();
|
|
771
|
+
const systemPrompt = await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
772
|
+
|
|
773
|
+
let totalUsage:
|
|
774
|
+
| {
|
|
775
|
+
prompt_tokens: number;
|
|
776
|
+
completion_tokens: number;
|
|
777
|
+
total_tokens: number;
|
|
778
|
+
}
|
|
779
|
+
| undefined;
|
|
780
|
+
let content: string | undefined;
|
|
781
|
+
|
|
782
|
+
for (let turn = 0; turn < MAX_FORK_TURNS; turn++) {
|
|
783
|
+
const result = await aiService.callAgent({
|
|
784
|
+
gatewayConfig,
|
|
785
|
+
modelConfig,
|
|
786
|
+
messages: forkMessages,
|
|
787
|
+
sessionId,
|
|
788
|
+
abortSignal,
|
|
789
|
+
workdir,
|
|
790
|
+
tools: toolsConfig,
|
|
791
|
+
systemPrompt,
|
|
792
|
+
toolChoice: this.toolChoiceOverride,
|
|
793
|
+
// Stream so a slow reasoning model emits first bytes before the
|
|
794
|
+
// gateway's idle timeout fires (non-streaming waits for the full
|
|
795
|
+
// summary, which exceeds the timeout on large contexts).
|
|
796
|
+
stream: true,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
if (result.usage) {
|
|
800
|
+
totalUsage = {
|
|
801
|
+
prompt_tokens:
|
|
802
|
+
(totalUsage?.prompt_tokens ?? 0) + result.usage.prompt_tokens,
|
|
803
|
+
completion_tokens:
|
|
804
|
+
(totalUsage?.completion_tokens ?? 0) +
|
|
805
|
+
result.usage.completion_tokens,
|
|
806
|
+
total_tokens:
|
|
807
|
+
(totalUsage?.total_tokens ?? 0) + result.usage.total_tokens,
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (result.content?.trim()) {
|
|
812
|
+
content = result.content;
|
|
813
|
+
break;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (result.tool_calls && result.tool_calls.length > 0) {
|
|
817
|
+
// Deny all tool calls locally and feed the rejections back so the
|
|
818
|
+
// model gets another turn to produce the summary text.
|
|
819
|
+
forkMessages.push({
|
|
820
|
+
role: "assistant",
|
|
821
|
+
content: result.content ?? null,
|
|
822
|
+
tool_calls: result.tool_calls,
|
|
823
|
+
});
|
|
824
|
+
for (const toolCall of result.tool_calls) {
|
|
825
|
+
forkMessages.push({
|
|
826
|
+
role: "tool",
|
|
827
|
+
tool_call_id: toolCall.id,
|
|
828
|
+
content: "Tool use is not allowed during compaction",
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Neither text nor tool calls: retrying the identical request is
|
|
835
|
+
// pointless, bail out and let the caller fail the compaction.
|
|
836
|
+
break;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
return { content, usage: totalUsage };
|
|
840
|
+
}
|
|
841
|
+
|
|
671
842
|
/**
|
|
672
843
|
* Build post-compact context restoration content.
|
|
673
844
|
* Restores file reads, working directory, plan mode, skills, and background tasks.
|
|
@@ -833,7 +1004,7 @@ export class AIManager {
|
|
|
833
1004
|
// has superseded us. setIsLoading(true) here also closes the "idle but
|
|
834
1005
|
// previous turn not finished" race window by marking us busy immediately.
|
|
835
1006
|
this.turnGeneration++;
|
|
836
|
-
|
|
1007
|
+
let myGeneration = this.turnGeneration;
|
|
837
1008
|
this.setIsLoading(true);
|
|
838
1009
|
|
|
839
1010
|
outer: while (true) {
|
|
@@ -938,30 +1109,16 @@ export class AIManager {
|
|
|
938
1109
|
|
|
939
1110
|
logger?.debug("modelConfig in sendAIMessage", this.getModelConfig());
|
|
940
1111
|
|
|
941
|
-
const toolsConfig =
|
|
942
|
-
|
|
943
|
-
const filteredToolPlugins = this.toolManager
|
|
944
|
-
.getTools()
|
|
945
|
-
.filter((t) => toolNames.has(t.name));
|
|
946
|
-
|
|
947
|
-
let autoMemoryOptions:
|
|
948
|
-
| { directory: string; content: string }
|
|
949
|
-
| undefined;
|
|
950
|
-
|
|
951
|
-
if (this.getAutoMemoryEnabled()) {
|
|
952
|
-
const directory = this.memoryService.getAutoMemoryDirectory(
|
|
953
|
-
this.getWorkdir(),
|
|
954
|
-
);
|
|
955
|
-
const content = await this.memoryService.getAutoMemoryContent(
|
|
956
|
-
this.getWorkdir(),
|
|
957
|
-
);
|
|
958
|
-
autoMemoryOptions = { directory, content };
|
|
959
|
-
}
|
|
1112
|
+
const { toolsConfig, toolNames, filteredToolPlugins } =
|
|
1113
|
+
this.resolveFilteredTools();
|
|
960
1114
|
|
|
961
1115
|
// Get memory for message-array injection (not system prompt)
|
|
962
1116
|
const { prependContent } =
|
|
963
1117
|
await this.messageManager.getMemoryForInjection();
|
|
964
1118
|
|
|
1119
|
+
const mainSystemPrompt =
|
|
1120
|
+
await this.buildMainSystemPrompt(filteredToolPlugins);
|
|
1121
|
+
|
|
965
1122
|
// Call AI service with streaming callbacks if enabled
|
|
966
1123
|
const callAgentOptions: CallAgentOptions = {
|
|
967
1124
|
gatewayConfig: this.getGatewayConfig(),
|
|
@@ -972,18 +1129,7 @@ export class AIManager {
|
|
|
972
1129
|
workdir: this.getWorkdir(), // Pass working directory
|
|
973
1130
|
tools: toolsConfig, // Pass filtered tool configuration
|
|
974
1131
|
model: model, // Use passed model
|
|
975
|
-
systemPrompt:
|
|
976
|
-
this.systemPrompt,
|
|
977
|
-
filteredToolPlugins,
|
|
978
|
-
{
|
|
979
|
-
workdir: this.getWorkdir(),
|
|
980
|
-
originalWorkdir: this.getOriginalWorkdir(),
|
|
981
|
-
language: this.getLanguage(),
|
|
982
|
-
isSubagent: !!this.subagentType,
|
|
983
|
-
worktreeSession: this.getWorktreeSession(),
|
|
984
|
-
autoMemory: autoMemoryOptions,
|
|
985
|
-
},
|
|
986
|
-
), // Pass custom system prompt
|
|
1132
|
+
systemPrompt: mainSystemPrompt, // Pass custom system prompt
|
|
987
1133
|
maxTokens: maxTokens, // Pass max tokens override
|
|
988
1134
|
toolChoice: this.toolChoiceOverride, // Pass tool_choice override
|
|
989
1135
|
};
|
|
@@ -992,7 +1138,7 @@ export class AIManager {
|
|
|
992
1138
|
if (prependContent.trim()) {
|
|
993
1139
|
callAgentOptions.messages.unshift({
|
|
994
1140
|
role: "user",
|
|
995
|
-
content:
|
|
1141
|
+
content: wrapInSystemReminder(prependContent),
|
|
996
1142
|
});
|
|
997
1143
|
}
|
|
998
1144
|
|
|
@@ -1507,6 +1653,14 @@ export class AIManager {
|
|
|
1507
1653
|
});
|
|
1508
1654
|
}
|
|
1509
1655
|
}
|
|
1656
|
+
// Re-assert loading state before restarting: if this turn was
|
|
1657
|
+
// aborted, abortAIMessage already reset loading to false, and the
|
|
1658
|
+
// restart below continues the conversation — the UI must show
|
|
1659
|
+
// streaming again (cursor, stop button, ESC handling).
|
|
1660
|
+
this.setIsLoading(true);
|
|
1661
|
+
// Adopt the current (possibly abort-bumped) generation so this
|
|
1662
|
+
// continued turn's end-of-turn cleanup is not skipped as superseded.
|
|
1663
|
+
myGeneration = this.turnGeneration;
|
|
1510
1664
|
// Restart outer loop to process the notifications
|
|
1511
1665
|
shouldRestart = true;
|
|
1512
1666
|
turnOffset = 0;
|