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.
Files changed (45) hide show
  1. package/builtin/plugins/sdd/.wave-plugin/plugin.json +8 -0
  2. package/builtin/plugins/sdd/hooks/hooks.json +14 -0
  3. package/builtin/plugins/sdd/scripts/session-start.js +24 -0
  4. package/builtin/plugins/sdd/scripts/spec-count.js +77 -0
  5. package/builtin/plugins/sdd/skills/specify/SKILL.md +48 -0
  6. package/builtin/plugins/sdd/skills/specify/templates/spec-template.md +47 -0
  7. package/dist/agent.d.ts +1 -0
  8. package/dist/agent.js +21 -10
  9. package/dist/managers/aiManager.d.ts +17 -0
  10. package/dist/managers/aiManager.js +152 -46
  11. package/dist/managers/permissionManager.d.ts +7 -0
  12. package/dist/managers/permissionManager.js +102 -142
  13. package/dist/managers/pluginManager.d.ts +7 -0
  14. package/dist/managers/pluginManager.js +31 -0
  15. package/dist/prompts/index.d.ts +12 -1
  16. package/dist/prompts/index.js +133 -45
  17. package/dist/services/aiService.d.ts +1 -17
  18. package/dist/services/aiService.js +3 -85
  19. package/dist/services/session.d.ts +3 -1
  20. package/dist/services/session.js +12 -4
  21. package/dist/services/taskManager.d.ts +1 -0
  22. package/dist/services/taskManager.js +34 -5
  23. package/dist/tools/editTool.js +24 -10
  24. package/dist/tools/grepTool.js +8 -2
  25. package/dist/tools/writeTool.js +36 -0
  26. package/dist/utils/bashParser.d.ts +25 -0
  27. package/dist/utils/bashParser.js +103 -0
  28. package/dist/utils/configPaths.d.ts +4 -0
  29. package/dist/utils/configPaths.js +6 -0
  30. package/dist/utils/fileSearch.js +4 -2
  31. package/package.json +1 -1
  32. package/src/agent.ts +19 -10
  33. package/src/managers/aiManager.ts +215 -61
  34. package/src/managers/permissionManager.ts +116 -168
  35. package/src/managers/pluginManager.ts +29 -0
  36. package/src/prompts/index.ts +144 -37
  37. package/src/services/aiService.ts +9 -128
  38. package/src/services/session.ts +18 -4
  39. package/src/services/taskManager.ts +46 -7
  40. package/src/tools/editTool.ts +29 -11
  41. package/src/tools/grepTool.ts +11 -2
  42. package/src/tools/writeTool.ts +43 -0
  43. package/src/utils/bashParser.ts +106 -0
  44. package/src/utils/configPaths.ts +7 -0
  45. 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 { ChatCompletionMessageFunctionToolCall } from "openai/resources.js";
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 { buildSystemPrompt } from "../prompts/index.js";
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 recentChatMessages = convertMessagesForAPI(messagesToCompact);
541
-
542
- // 4. Call compactMessages with optional custom instructions
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 (compactResult.usage) {
573
+ if (compactTokens) {
555
574
  compactUsage = {
556
- prompt_tokens: compactResult.usage.prompt_tokens,
557
- completion_tokens: compactResult.usage.completion_tokens,
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. Build post-compact context restoration
565
- const enhancedSummary = await this.buildPostCompactContext(
566
- compactResult.content,
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
- // 7. Execute message reconstruction
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
- // 8. Track usage
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
- // 9. Log OTEL event
623
+ // 10. Log OTEL event
605
624
  logOTelEvent("compaction", {
606
625
  beforeTokens: String(messagesToCompact.length),
607
626
  afterTokens: "1",
608
- model: this.getModelConfig().fastModel,
627
+ model: compactModel,
609
628
  }).catch(() => {});
610
629
 
611
- // 10. Run SessionStart hooks (existing behavior)
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
- // 11. Run PostCompact hooks
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
- compactResult.content,
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
- const myGeneration = this.turnGeneration;
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 = this.getFilteredToolsConfig();
942
- const toolNames = new Set(toolsConfig.map((t) => t.function.name));
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: buildSystemPrompt(
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: `<system-reminder>\n${prependContent}\n</system-reminder>`,
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;