wave-agent-sdk 1.0.0 → 1.0.1

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 (41) hide show
  1. package/dist/agent.d.ts +9 -20
  2. package/dist/agent.js +23 -97
  3. package/dist/managers/aiManager.d.ts +63 -8
  4. package/dist/managers/aiManager.js +270 -80
  5. package/dist/managers/messageManager.d.ts +9 -5
  6. package/dist/managers/messageManager.js +36 -12
  7. package/dist/managers/subagentManager.d.ts +6 -0
  8. package/dist/managers/subagentManager.js +33 -22
  9. package/dist/prompts/index.d.ts +0 -1
  10. package/dist/prompts/index.js +0 -4
  11. package/dist/services/aiService.d.ts +1 -34
  12. package/dist/services/aiService.js +18 -130
  13. package/dist/services/autoMemoryService.d.ts +27 -2
  14. package/dist/services/autoMemoryService.js +124 -36
  15. package/dist/services/configurationService.js +14 -1
  16. package/dist/services/session.d.ts +13 -0
  17. package/dist/services/session.js +64 -0
  18. package/dist/types/agent.d.ts +0 -2
  19. package/dist/types/config.d.ts +7 -0
  20. package/dist/types/core.d.ts +1 -1
  21. package/dist/utils/containerSetup.js +12 -3
  22. package/package.json +1 -1
  23. package/src/agent.ts +36 -110
  24. package/src/managers/aiManager.ts +366 -105
  25. package/src/managers/messageManager.ts +51 -23
  26. package/src/managers/subagentManager.ts +36 -25
  27. package/src/prompts/index.ts +0 -4
  28. package/src/services/aiService.ts +25 -203
  29. package/src/services/autoMemoryService.ts +145 -39
  30. package/src/services/configurationService.ts +16 -1
  31. package/src/services/session.ts +68 -0
  32. package/src/types/agent.ts +0 -6
  33. package/src/types/config.ts +7 -0
  34. package/src/types/core.ts +1 -1
  35. package/src/utils/containerSetup.ts +12 -4
  36. package/dist/constants/goalPrompts.d.ts +0 -1
  37. package/dist/constants/goalPrompts.js +0 -10
  38. package/dist/managers/goalManager.d.ts +0 -42
  39. package/dist/managers/goalManager.js +0 -177
  40. package/src/constants/goalPrompts.ts +0 -10
  41. package/src/managers/goalManager.ts +0 -232
@@ -37,7 +37,6 @@ import { READ_TOOL_NAME } from "../constants/tools.js";
37
37
  import { Container } from "../utils/container.js";
38
38
 
39
39
  export interface MessageManagerCallbacks {
40
- onMessagesChange?: (messages: Message[]) => void;
41
40
  onSessionIdChange?: (sessionId: string) => void;
42
41
  onLatestTotalTokensChange?: (latestTotalTokens: number) => void;
43
42
  onUsagesChange?: (usages: Usage[]) => void;
@@ -64,9 +63,17 @@ export interface MessageManagerCallbacks {
64
63
  onCompactBlockAdded?: (content: string) => void;
65
64
  onCompactionStateChange?: (isCompacting: boolean) => void;
66
65
  // Bang callback
67
- onAddBangMessage?: (command: string) => void;
68
- onUpdateBangMessage?: (command: string, output: string) => void;
69
- onCompleteBangMessage?: (command: string, exitCode: number) => void;
66
+ onAddBangMessage?: (command: string, messageId: string) => void;
67
+ onUpdateBangMessage?: (
68
+ command: string,
69
+ output: string,
70
+ messageId: string,
71
+ ) => void;
72
+ onCompleteBangMessage?: (
73
+ command: string,
74
+ exitCode: number,
75
+ messageId: string,
76
+ ) => void;
70
77
  onInfoBlockAdded?: (content: string) => void;
71
78
  // Rewind callbacks
72
79
  onShowRewind?: () => void;
@@ -322,8 +329,6 @@ export class MessageManager {
322
329
  this.extractFileReadsFromMessage(messages[messages.length - 1]);
323
330
  this.extractSkillInvocationsFromMessage(messages[messages.length - 1]);
324
331
  }
325
-
326
- this.callbacks.onMessagesChange?.([...messages]);
327
332
  }
328
333
 
329
334
  /**
@@ -339,6 +344,20 @@ export class MessageManager {
339
344
  return;
340
345
  }
341
346
 
347
+ // CC-aligned lazy materialization: when the session file has not been
348
+ // materialized yet (no saved messages) and every unsaved message is a
349
+ // meta message (isMeta: true, e.g. SessionStart hook context), skip
350
+ // persistence. Persisting meta-only sessions would create "0 tokens /
351
+ // No content" ghost entries in the resume list. The meta messages stay
352
+ // in memory and are flushed together with the first real user/assistant
353
+ // message (matches CC's pendingEntries buffering).
354
+ if (
355
+ this.savedMessageCount === 0 &&
356
+ unsavedMessages.every((m) => m.isMeta)
357
+ ) {
358
+ return;
359
+ }
360
+
342
361
  // Create session if needed (only when we have messages to save)
343
362
  if (this.savedMessageCount === 0) {
344
363
  // This is the first time saving messages, so create the session
@@ -622,7 +641,9 @@ export class MessageManager {
622
641
  command,
623
642
  });
624
643
  this.setMessages(updatedMessages);
625
- this.callbacks.onAddBangMessage?.(command);
644
+ // The bang message is appended as the last message
645
+ const messageId = this.messages[this.messages.length - 1]?.id ?? "";
646
+ this.callbacks.onAddBangMessage?.(command, messageId);
626
647
  }
627
648
 
628
649
  public updateBangMessage(command: string, output: string): void {
@@ -632,7 +653,8 @@ export class MessageManager {
632
653
  output,
633
654
  });
634
655
  this.setMessages(updatedMessages);
635
- this.callbacks.onUpdateBangMessage?.(command, output);
656
+ const messageId = this.findBangMessageId(command) ?? "";
657
+ this.callbacks.onUpdateBangMessage?.(command, output, messageId);
636
658
  }
637
659
 
638
660
  public completeBangMessage(
@@ -647,7 +669,25 @@ export class MessageManager {
647
669
  output,
648
670
  });
649
671
  this.setMessages(updatedMessages);
650
- this.callbacks.onCompleteBangMessage?.(command, exitCode);
672
+ const messageId = this.findBangMessageId(command) ?? "";
673
+ this.callbacks.onCompleteBangMessage?.(command, exitCode, messageId);
674
+ }
675
+
676
+ /**
677
+ * Find the message ID of the most recent message containing a bang block
678
+ * for the given command. Bang callbacks do not carry a block ID, so the
679
+ * message is located by matching the command against bang blocks.
680
+ */
681
+ private findBangMessageId(command: string): string | undefined {
682
+ for (let i = this.messages.length - 1; i >= 0; i--) {
683
+ const message = this.messages[i];
684
+ if (message.role !== "user") continue;
685
+ const hasBangBlock = message.blocks?.some(
686
+ (block) => block.type === "bang" && block.command === command,
687
+ );
688
+ if (hasBangBlock) return message.id;
689
+ }
690
+ return undefined;
651
691
  }
652
692
 
653
693
  public addNotificationMessage(
@@ -700,7 +740,6 @@ export class MessageManager {
700
740
  /**
701
741
  * Finalize a streaming block of the given type by setting its stage to "end".
702
742
  * Fires the corresponding incremental callback with chunk="" to signal finalization.
703
- * Does NOT call onMessagesChange — the caller is responsible for that.
704
743
  * Returns true if a block was finalized.
705
744
  */
706
745
  private finalizeStreamingBlock(
@@ -807,8 +846,6 @@ export class MessageManager {
807
846
  });
808
847
 
809
848
  // Note: Subagent-specific callbacks are now handled by SubagentManager
810
-
811
- this.callbacks.onMessagesChange?.([...this.messages]); // Still need to notify of changes
812
849
  }
813
850
 
814
851
  /**
@@ -871,8 +908,6 @@ export class MessageManager {
871
908
  accumulated: newAccumulatedReasoning,
872
909
  stage: "streaming",
873
910
  });
874
-
875
- this.callbacks.onMessagesChange?.([...this.messages]); // Still need to notify of changes
876
911
  }
877
912
 
878
913
  /**
@@ -885,15 +920,8 @@ export class MessageManager {
885
920
  const lastMessage = this.messages[this.messages.length - 1];
886
921
  if (lastMessage.role !== "assistant") return;
887
922
 
888
- const textFinalized = this.finalizeStreamingBlock(lastMessage, "text");
889
- const reasoningFinalized = this.finalizeStreamingBlock(
890
- lastMessage,
891
- "reasoning",
892
- );
893
-
894
- if (textFinalized || reasoningFinalized) {
895
- this.callbacks.onMessagesChange?.([...this.messages]);
896
- }
923
+ this.finalizeStreamingBlock(lastMessage, "text");
924
+ this.finalizeStreamingBlock(lastMessage, "reasoning");
897
925
  }
898
926
 
899
927
  /**
@@ -790,6 +790,7 @@ export class SubagentManager {
790
790
  private createSubagentCallbacks(subagentId: string) {
791
791
  return {
792
792
  onUserMessageAdded: (params: UserMessageParams) => {
793
+ this.refreshSubagentState(subagentId);
793
794
  // Forward user message events to parent via SubagentManager callbacks
794
795
  if (this.callbacks?.onSubagentUserMessageAdded) {
795
796
  this.callbacks.onSubagentUserMessageAdded(subagentId, params);
@@ -797,6 +798,7 @@ export class SubagentManager {
797
798
  },
798
799
 
799
800
  onAssistantMessageAdded: (messageId: string) => {
801
+ this.refreshSubagentState(subagentId);
800
802
  // Forward assistant message events to parent via SubagentManager callbacks
801
803
  if (this.callbacks?.onSubagentAssistantMessageAdded) {
802
804
  this.callbacks.onSubagentAssistantMessageAdded(subagentId, messageId);
@@ -809,6 +811,7 @@ export class SubagentManager {
809
811
  accumulated: string;
810
812
  stage: "streaming" | "end";
811
813
  }) => {
814
+ this.refreshSubagentState(subagentId);
812
815
  // Forward assistant content updates to parent via SubagentManager callbacks
813
816
  if (this.callbacks?.onSubagentAssistantContentUpdated) {
814
817
  this.callbacks.onSubagentAssistantContentUpdated({
@@ -823,6 +826,7 @@ export class SubagentManager {
823
826
  accumulated: string;
824
827
  stage: "streaming" | "end";
825
828
  }) => {
829
+ this.refreshSubagentState(subagentId);
826
830
  // Forward assistant reasoning updates to parent via SubagentManager callbacks
827
831
  if (this.callbacks?.onSubagentAssistantReasoningUpdated) {
828
832
  this.callbacks.onSubagentAssistantReasoningUpdated({
@@ -833,6 +837,7 @@ export class SubagentManager {
833
837
  },
834
838
 
835
839
  onToolBlockUpdated: (params: ToolBlockUpdateCallbackParams) => {
840
+ this.refreshSubagentState(subagentId);
836
841
  const instance = this.instances.get(subagentId);
837
842
  if (instance) {
838
843
  // Log tool execution to file only when finalized
@@ -852,31 +857,6 @@ export class SubagentManager {
852
857
  }
853
858
  },
854
859
 
855
- // These callbacks will be handled by the parent agent
856
- onMessagesChange: (messages: Message[]) => {
857
- const instance = this.instances.get(subagentId);
858
- if (instance) {
859
- instance.messages = messages;
860
- // Compute usedTools from messages (last 2 tool blocks)
861
- const toolBlocks = messages.flatMap(
862
- (m) => m.blocks?.filter((b) => b.type === "tool") ?? [],
863
- );
864
- const last2 = toolBlocks.slice(-2);
865
- instance.usedTools = last2.map((tb) => ({
866
- name: tb.name ?? "",
867
- parameters: tb.parameters ?? "",
868
- compactParams: tb.compactParams,
869
- stage: tb.stage,
870
- }));
871
- // Trigger the onUpdate callback if provided
872
- instance.onUpdate?.();
873
- // Forward subagent message changes to parent via callbacks
874
- if (this.callbacks?.onSubagentMessagesChange) {
875
- this.callbacks.onSubagentMessagesChange(subagentId, messages);
876
- }
877
- }
878
- },
879
-
880
860
  onLatestTotalTokensChange: (tokens: number) => {
881
861
  const instance = this.instances.get(subagentId);
882
862
  if (instance) {
@@ -890,6 +870,7 @@ export class SubagentManager {
890
870
  },
891
871
 
892
872
  onErrorBlockAdded: (error: string) => {
873
+ this.refreshSubagentState(subagentId);
893
874
  const instance = this.instances.get(subagentId);
894
875
  if (instance?.logStream) {
895
876
  instance.logStream.write(
@@ -899,4 +880,34 @@ export class SubagentManager {
899
880
  },
900
881
  };
901
882
  }
883
+
884
+ /**
885
+ * Pull the latest messages from the subagent instance's MessageManager and
886
+ * refresh the instance's cached messages, usedTools, onUpdate and the
887
+ * onSubagentMessagesChange forwarding. Triggered by incremental callbacks.
888
+ */
889
+ private refreshSubagentState(subagentId: string): void {
890
+ const instance = this.instances.get(subagentId);
891
+ if (!instance) return;
892
+
893
+ const messages = instance.messageManager.getMessages();
894
+ instance.messages = messages;
895
+ // Compute usedTools from messages (last 2 tool blocks)
896
+ const toolBlocks = messages.flatMap(
897
+ (m) => m.blocks?.filter((b) => b.type === "tool") ?? [],
898
+ );
899
+ const last2 = toolBlocks.slice(-2);
900
+ instance.usedTools = last2.map((tb) => ({
901
+ name: tb.name ?? "",
902
+ parameters: tb.parameters ?? "",
903
+ compactParams: tb.compactParams,
904
+ stage: tb.stage,
905
+ }));
906
+ // Trigger the onUpdate callback if provided
907
+ instance.onUpdate?.();
908
+ // Forward subagent message changes to parent via callbacks
909
+ if (this.callbacks?.onSubagentMessagesChange) {
910
+ this.callbacks.onSubagentMessagesChange(subagentId, messages);
911
+ }
912
+ }
902
913
  }
@@ -346,10 +346,6 @@ export function formatCompactSummary(summary: string): string {
346
346
  }
347
347
 
348
348
  export const WEB_CONTENT_SYSTEM_PROMPT = `You are a helpful assistant that extracts information from web content. The content is provided in Markdown format.`;
349
- export const BTW_SYSTEM_PROMPT = `You are a helpful assistant. Answer the user's side question based on the conversation history.
350
- Do NOT say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action.
351
- If you don't know the answer, say so - do not offer to look it up or investigate.
352
- Simply answer the question with the information you have.`;
353
349
 
354
350
  export function buildSystemPrompt(
355
351
  basePrompt: string | undefined,
@@ -25,10 +25,8 @@ import * as path from "path";
25
25
 
26
26
  import {
27
27
  WEB_CONTENT_SYSTEM_PROMPT,
28
- BTW_SYSTEM_PROMPT,
29
28
  type SystemPromptBlock,
30
29
  } from "../prompts/index.js";
31
- import { GOAL_EVALUATION_SYSTEM_PROMPT } from "../constants/goalPrompts.js";
32
30
 
33
31
  /**
34
32
  * Interface for debug data saved during 400 errors
@@ -148,6 +146,18 @@ function getModelConfig(
148
146
  return config;
149
147
  }
150
148
 
149
+ /**
150
+ * Effective disable-thinking params for a model config. No default: these
151
+ * params are only sent when the user explicitly configures
152
+ * `models[X].disableThinkingOptions` (an empty object clears them), so a
153
+ * gateway that doesn't understand the params is never hit with them.
154
+ */
155
+ function effectiveDisableThinkingOptions(
156
+ modelConfig: ModelConfig,
157
+ ): Record<string, unknown> | undefined {
158
+ return modelConfig.disableThinkingOptions;
159
+ }
160
+
151
161
  export interface CallAgentOptions {
152
162
  // Resolved configuration
153
163
  gatewayConfig: GatewayConfig;
@@ -184,6 +194,10 @@ export interface CallAgentOptions {
184
194
  stage?: "start" | "streaming" | "running" | "end";
185
195
  }) => void;
186
196
  onReasoningUpdate?: (content: string) => void;
197
+
198
+ // Disable-thinking params for fast-model subagent calls (merged into the
199
+ // request; never used in the agent loop).
200
+ disableThinkingOptions?: Record<string, unknown>;
187
201
  }
188
202
 
189
203
  export interface CallAgentResult {
@@ -238,6 +252,7 @@ export async function callAgent(
238
252
  onContentUpdate,
239
253
  onToolUpdate,
240
254
  onReasoningUpdate,
255
+ disableThinkingOptions,
241
256
  } = options;
242
257
 
243
258
  // Validate model config at call time
@@ -321,6 +336,7 @@ export async function callAgent(
321
336
  const openaiModelConfig = getModelConfig(model || modelConfig.model, {
322
337
  max_tokens: resolvedMaxTokens,
323
338
  ...(modelConfig.options || {}),
339
+ ...(disableThinkingOptions ?? {}),
324
340
  });
325
341
 
326
342
  // Determine if streaming is needed
@@ -842,10 +858,17 @@ export async function processWebContent(
842
858
  ? modelConfig.fastModelOptions || {}
843
859
  : modelConfig.options || {};
844
860
 
861
+ // Disable-thinking params only apply to the fast-model override path;
862
+ // the agent-model path is untouched.
863
+ const disableThinking = options.model
864
+ ? effectiveDisableThinkingOptions(modelConfig)
865
+ : undefined;
866
+
845
867
  const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
846
868
  temperature: 0.1,
847
869
  max_tokens: 4096,
848
870
  ...activeExtraParams,
871
+ ...(disableThinking || {}),
849
872
  });
850
873
 
851
874
  try {
@@ -894,204 +917,3 @@ export async function processWebContent(
894
917
  }
895
918
  }
896
919
 
897
- export interface BtwOptions {
898
- // Resolved configuration
899
- gatewayConfig: GatewayConfig;
900
- modelConfig: ModelConfig;
901
-
902
- // Parameters
903
- messages: ChatCompletionMessageParam[];
904
- question: string;
905
- abortSignal?: AbortSignal;
906
- model?: string;
907
- }
908
-
909
- export interface BtwResult {
910
- content: string;
911
- usage?: {
912
- prompt_tokens: number;
913
- completion_tokens: number;
914
- total_tokens: number;
915
- };
916
- }
917
-
918
- export async function btw(options: BtwOptions): Promise<BtwResult> {
919
- const { gatewayConfig, modelConfig, messages, question, abortSignal } =
920
- options;
921
-
922
- // Validate model config at call time
923
- validateModelConfig(modelConfig);
924
-
925
- // Apply global 1 QPS rate limit
926
- if (
927
- process.env.NODE_ENV !== "test" ||
928
- modelConfig.model === "rate-limit-test"
929
- ) {
930
- await acquireSlot(abortSignal);
931
- }
932
-
933
- // Create OpenAI client with injected configuration
934
- const openai = new OpenAIClient({
935
- apiKey: gatewayConfig.apiKey,
936
- baseURL: gatewayConfig.baseURL,
937
- defaultHeaders: gatewayConfig.defaultHeaders,
938
- fetchOptions: gatewayConfig.fetchOptions,
939
- fetch: gatewayConfig.fetch,
940
- });
941
-
942
- const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
943
- temperature: 0.1,
944
- max_tokens: 4096,
945
- ...(modelConfig.options || {}),
946
- });
947
-
948
- try {
949
- const response = await openai.chat.completions.create(
950
- {
951
- ...openaiModelConfig,
952
- messages: [
953
- {
954
- role: "system",
955
- content: BTW_SYSTEM_PROMPT,
956
- },
957
- ...messages,
958
- {
959
- role: "user",
960
- content: question,
961
- },
962
- ],
963
- },
964
- {
965
- signal: abortSignal,
966
- },
967
- );
968
-
969
- const result = response.choices[0]?.message?.content?.trim();
970
- if (!result) {
971
- throw new Error(
972
- "Failed to process side question: Empty response from AI",
973
- );
974
- }
975
- const usage = response.usage
976
- ? {
977
- prompt_tokens: response.usage.prompt_tokens,
978
- completion_tokens: response.usage.completion_tokens,
979
- total_tokens: response.usage.total_tokens,
980
- }
981
- : undefined;
982
-
983
- return {
984
- content: result,
985
- usage,
986
- };
987
- } catch (error) {
988
- if ((error as Error).name === "AbortError") {
989
- logger.info("Side question request was aborted");
990
- throw new Error("Side question request was aborted");
991
- }
992
- logger.error("Failed to process side question:", error);
993
- throw error;
994
- }
995
- }
996
-
997
- export interface EvaluateGoalOptions {
998
- gatewayConfig: GatewayConfig;
999
- modelConfig: ModelConfig;
1000
- model: string;
1001
- goalCondition: string;
1002
- messages: ChatCompletionMessageParam[];
1003
- abortSignal?: AbortSignal;
1004
- }
1005
-
1006
- export interface EvaluateGoalResult {
1007
- content: string;
1008
- usage?: {
1009
- prompt_tokens: number;
1010
- completion_tokens: number;
1011
- total_tokens: number;
1012
- };
1013
- }
1014
-
1015
- export async function evaluateGoal(
1016
- options: EvaluateGoalOptions,
1017
- ): Promise<EvaluateGoalResult> {
1018
- const {
1019
- gatewayConfig,
1020
- modelConfig,
1021
- model,
1022
- goalCondition,
1023
- messages,
1024
- abortSignal,
1025
- } = options;
1026
-
1027
- // Create OpenAI client with injected configuration (no rate limiter — bypasses 1 QPS)
1028
- const openai = new OpenAIClient({
1029
- apiKey: gatewayConfig.apiKey,
1030
- baseURL: gatewayConfig.baseURL,
1031
- defaultHeaders: gatewayConfig.defaultHeaders,
1032
- fetchOptions: gatewayConfig.fetchOptions,
1033
- fetch: gatewayConfig.fetch,
1034
- });
1035
-
1036
- const openaiModelConfig = getModelConfig(model, {
1037
- temperature: 0,
1038
- max_tokens: 200,
1039
- ...(modelConfig.fastModelOptions || {}),
1040
- });
1041
-
1042
- // Strip images from messages to reduce token usage (same as compact)
1043
- const cleanedMessages = messages.map((msg) => {
1044
- if (Array.isArray(msg.content)) {
1045
- const textParts = msg.content.filter(
1046
- (part) => part.type === "text",
1047
- ) as import("openai/resources.js").ChatCompletionContentPartText[];
1048
- const text = textParts.map((p) => p.text).join("\n");
1049
- return { ...msg, content: text || "(empty message)" };
1050
- }
1051
- return msg;
1052
- });
1053
-
1054
- try {
1055
- const response = await openai.chat.completions.create(
1056
- {
1057
- ...openaiModelConfig,
1058
- messages: [
1059
- {
1060
- role: "system",
1061
- content: GOAL_EVALUATION_SYSTEM_PROMPT,
1062
- },
1063
- ...cleanedMessages,
1064
- {
1065
- role: "user",
1066
- content: `Goal condition: ${goalCondition}\n\nHas this goal been achieved based on the conversation above?`,
1067
- },
1068
- ],
1069
- },
1070
- {
1071
- signal: abortSignal,
1072
- },
1073
- );
1074
-
1075
- const result = response.choices[0]?.message?.content?.trim();
1076
- if (!result) {
1077
- throw new Error("Goal evaluation returned empty response");
1078
- }
1079
-
1080
- const usage = response.usage
1081
- ? {
1082
- prompt_tokens: response.usage.prompt_tokens,
1083
- completion_tokens: response.usage.completion_tokens,
1084
- total_tokens: response.usage.total_tokens,
1085
- }
1086
- : undefined;
1087
-
1088
- return { content: result, usage };
1089
- } catch (error) {
1090
- if ((error as Error).name === "AbortError") {
1091
- logger.info("Goal evaluation was aborted");
1092
- throw new Error("Goal evaluation was aborted");
1093
- }
1094
- logger.error("Goal evaluation failed:", error);
1095
- throw error;
1096
- }
1097
- }