wave-agent-sdk 1.0.0 → 1.0.2

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 (43) hide show
  1. package/dist/agent.d.ts +9 -20
  2. package/dist/agent.js +35 -97
  3. package/dist/managers/aiManager.d.ts +63 -8
  4. package/dist/managers/aiManager.js +274 -80
  5. package/dist/managers/messageManager.d.ts +9 -5
  6. package/dist/managers/messageManager.js +36 -12
  7. package/dist/managers/permissionManager.js +13 -11
  8. package/dist/managers/subagentManager.d.ts +6 -0
  9. package/dist/managers/subagentManager.js +33 -22
  10. package/dist/prompts/index.d.ts +0 -2
  11. package/dist/prompts/index.js +71 -47
  12. package/dist/services/aiService.d.ts +1 -34
  13. package/dist/services/aiService.js +18 -130
  14. package/dist/services/autoMemoryService.d.ts +27 -2
  15. package/dist/services/autoMemoryService.js +124 -36
  16. package/dist/services/configurationService.js +14 -1
  17. package/dist/services/session.d.ts +13 -0
  18. package/dist/services/session.js +64 -0
  19. package/dist/types/agent.d.ts +0 -2
  20. package/dist/types/config.d.ts +7 -0
  21. package/dist/types/core.d.ts +1 -1
  22. package/dist/utils/containerSetup.js +12 -3
  23. package/package.json +1 -1
  24. package/src/agent.ts +50 -110
  25. package/src/managers/aiManager.ts +370 -105
  26. package/src/managers/messageManager.ts +51 -23
  27. package/src/managers/permissionManager.ts +15 -13
  28. package/src/managers/subagentManager.ts +36 -25
  29. package/src/prompts/index.ts +75 -56
  30. package/src/services/aiService.ts +25 -203
  31. package/src/services/autoMemoryService.ts +145 -39
  32. package/src/services/configurationService.ts +16 -1
  33. package/src/services/session.ts +68 -0
  34. package/src/types/agent.ts +0 -6
  35. package/src/types/config.ts +7 -0
  36. package/src/types/core.ts +1 -1
  37. package/src/utils/containerSetup.ts +12 -4
  38. package/dist/constants/goalPrompts.d.ts +0 -1
  39. package/dist/constants/goalPrompts.js +0 -10
  40. package/dist/managers/goalManager.d.ts +0 -42
  41. package/dist/managers/goalManager.js +0 -177
  42. package/src/constants/goalPrompts.ts +0 -10
  43. 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
  /**
@@ -427,18 +427,8 @@ export class PermissionManager {
427
427
  }
428
428
  }
429
429
 
430
- // 1. If bypassPermissions mode, always allow
431
- // Exception: tools that require user interaction (e.g. AskUserQuestion)
432
- // must still prompt the user, matching Claude Code's requiresUserInteraction behavior.
433
- if (context.permissionMode === "bypassPermissions") {
434
- const requiresUserInteraction =
435
- context.toolName === ASK_USER_QUESTION_TOOL_NAME;
436
- if (!requiresUserInteraction) {
437
- return { behavior: "allow" };
438
- }
439
- }
440
-
441
- // 1.0 Check worktree safety for Write and Edit tools
430
+ // Check worktree safety for Write and Edit tools — unconditional safety
431
+ // check, applied regardless of permission mode (same as read-before-edit).
442
432
  // Support both CLI -w sessions (container-registered) and EnterWorktree mid-session
443
433
  // (per-agent WorktreeSession stored in this session's container)
444
434
  const worktreeSession = this.container.get<WorktreeSession | null>(
@@ -503,7 +493,19 @@ export class PermissionManager {
503
493
  }
504
494
  }
505
495
 
506
- // 1.1 If acceptEdits mode, allow Edit, Write, and mkdir in safe zone
496
+ // If bypassPermissions mode, always allow
497
+ // Exception: tools that require user interaction (e.g. AskUserQuestion)
498
+ // must still prompt the user, matching Claude Code's requiresUserInteraction behavior.
499
+ // Worktree safety check above runs unconditionally, so bypass never skips it.
500
+ if (context.permissionMode === "bypassPermissions") {
501
+ const requiresUserInteraction =
502
+ context.toolName === ASK_USER_QUESTION_TOOL_NAME;
503
+ if (!requiresUserInteraction) {
504
+ return { behavior: "allow" };
505
+ }
506
+ }
507
+
508
+ // If acceptEdits mode, allow Edit, Write, and mkdir in safe zone
507
509
  if (context.permissionMode === "acceptEdits") {
508
510
  const autoAcceptedTools = [EDIT_TOOL_NAME, WRITE_TOOL_NAME];
509
511
  if (autoAcceptedTools.includes(context.toolName)) {
@@ -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,46 @@ 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.`;
349
+
350
+ /**
351
+ * Notes block prepended to the subagent env section, aligned with Claude
352
+ * Code's enhanceSystemPromptWithEnvDetails().
353
+ */
354
+ const SUBAGENT_ENV_NOTES = `Notes:
355
+ - Agent threads always have their cwd reset between bash calls, as a result please only use absolute file paths.
356
+ - In your final response, share file paths (always absolute, never relative) that are relevant to the task. Include code snippets only when the exact text is load-bearing (e.g., a bug you found, a function signature the caller asked for) — do not recap code you merely read.
357
+ - For clear communication with the user the assistant MUST avoid using emojis.
358
+ - Do not use a colon before tool calls. Text like "Let me read the file:" followed by a read tool call should just be "Let me read the file." with a period.`;
359
+
360
+ /**
361
+ * Shell info line, aligned with Claude Code's getShellInfoLine(). On win32 an
362
+ * extra Unix-syntax hint is appended.
363
+ */
364
+ function getShellInfoLine(): string {
365
+ const shell = process.env.SHELL || "unknown";
366
+ const shellName = shell.includes("zsh")
367
+ ? "zsh"
368
+ : shell.includes("bash")
369
+ ? "bash"
370
+ : shell;
371
+ if (os.platform() === "win32") {
372
+ return `Shell: ${shellName} (use Unix shell syntax, not Windows — e.g., /dev/null not NUL, forward slashes in paths)`;
373
+ }
374
+ return `Shell: ${shellName}`;
375
+ }
376
+
377
+ /**
378
+ * OS Version value, aligned with Claude Code's getUnameSR(). os.type() and
379
+ * os.release() wrap uname(3) on POSIX, producing output byte-identical to
380
+ * `uname -sr`. Windows has no uname(3); os.type() returns "Windows_NT" there,
381
+ * but os.version() gives the friendlier "Windows 11 Pro", so use that instead.
382
+ */
383
+ function getUnameSR(): string {
384
+ if (os.platform() === "win32") {
385
+ return `${os.version()} ${os.release()}`;
386
+ }
387
+ return `${os.type()} ${os.release()}`;
388
+ }
353
389
 
354
390
  export function buildSystemPrompt(
355
391
  basePrompt: string | undefined,
@@ -391,29 +427,51 @@ export function buildSystemPrompt(
391
427
  if (options.workdir) {
392
428
  const isGitRepo = isGitRepository(options.workdir);
393
429
  const platform = os.platform();
394
- const osVersion = `${os.type()} ${os.release()}`;
395
- const today = new Date().toISOString().split("T")[0];
396
- const shell = process.env.SHELL || "unknown";
397
- const shellName = shell.includes("zsh")
398
- ? "zsh"
399
- : shell.includes("bash")
400
- ? "bash"
401
- : shell;
402
-
430
+ const shellInfo = getShellInfoLine();
431
+ const osVersion = getUnameSR();
432
+ const primaryWorkdir = options.originalWorkdir ?? options.workdir;
403
433
  const worktreeSession = options.worktreeSession;
404
434
 
405
- dynamicText += `
435
+ if (options.isSubagent) {
436
+ // Subagent env section, aligned with Claude Code's computeEnvInfo() +
437
+ // enhanceSystemPromptWithEnvDetails() (without the model description and
438
+ // knowledge cutoff lines, which Wave does not use).
439
+ dynamicText += `
440
+
441
+ ${SUBAGENT_ENV_NOTES}
406
442
 
407
443
  Here is useful information about the environment you are running in:
408
444
  <env>
409
- Primary working directory: ${options.originalWorkdir ?? options.workdir}${worktreeSession ? `\nThis is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root at ${worktreeSession.originalCwd}.` : ""}
445
+ Working directory: ${primaryWorkdir}
410
446
  Is directory a git repo: ${isGitRepo}
411
447
  Platform: ${platform}
412
- Shell: ${shellName}
448
+ ${shellInfo}
413
449
  OS Version: ${osVersion}
414
- Today's date: ${today}
415
450
  </env>
416
451
  `;
452
+ } else {
453
+ // Main agent env section, aligned with Claude Code's
454
+ // computeSimpleEnvInfo() (without the model description, knowledge
455
+ // cutoff, and marketing lines, which Wave does not use).
456
+ const envItems = [
457
+ `Primary working directory: ${primaryWorkdir}`,
458
+ worktreeSession
459
+ ? `This is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root.`
460
+ : null,
461
+ `Is a git repository: ${isGitRepo}`,
462
+ `Platform: ${platform}`,
463
+ shellInfo,
464
+ `OS Version: ${osVersion}`,
465
+ ].filter((item): item is string => item !== null);
466
+
467
+ const envBlock = [
468
+ `# Environment`,
469
+ `You have been invoked in the following environment: `,
470
+ ...envItems.map((item) => ` - ${item}`),
471
+ ].join("\n");
472
+
473
+ dynamicText += `\n\n${envBlock}`;
474
+ }
417
475
  }
418
476
 
419
477
  if (options.autoMemory) {
@@ -429,42 +487,3 @@ Today's date: ${today}
429
487
 
430
488
  return blocks;
431
489
  }
432
-
433
- export function enhanceSystemPromptWithEnvDetails(
434
- existingSystemPrompt: string,
435
- workdir: string,
436
- originalWorkdir?: string,
437
- worktreeSession?: WorktreeSession | null,
438
- ): string {
439
- const isGitRepo = isGitRepository(workdir);
440
- const platform = os.platform();
441
- const osVersion = `${os.type()} ${os.release()}`;
442
- const today = new Date().toISOString().split("T")[0];
443
- const shell = process.env.SHELL || "unknown";
444
- const shellName = shell.includes("zsh")
445
- ? "zsh"
446
- : shell.includes("bash")
447
- ? "bash"
448
- : shell;
449
-
450
- const notes = `Notes:
451
- - Agent threads always have their cwd reset between bash calls, as a result please only use absolute file paths.${worktreeSession ? `\n- You are in a git worktree at ${worktreeSession.worktreePath} (branch: ${worktreeSession.worktreeBranch}). Absolute paths from prior context may refer to the original repo at ${worktreeSession.originalCwd}; translate them to your worktree. Do NOT edit files outside this worktree.` : ""}
452
- - In your final response, share file paths (always absolute, never relative) that are relevant to the task. Include code snippets only when the exact text is load-bearing (e.g., a bug you found, a function signature the caller asked for) — do not recap code you merely read.
453
- - For clear communication with the user the assistant MUST avoid using emojis.
454
- - Do not use a colon before tool calls. Text like "Let me read the file:" followed by a read tool call should just be "Let me read the file." with a period.`;
455
-
456
- return `${existingSystemPrompt}
457
-
458
- ${notes}
459
-
460
- Here is useful information about the environment you are running in:
461
- <env>
462
- Primary working directory: ${originalWorkdir ?? workdir}${worktreeSession ? `\nThis is a git worktree — an isolated copy of the repository. Run all commands from this directory. Do NOT \`cd\` to the original repository root at ${worktreeSession.originalCwd}.` : ""}
463
- Is directory a git repo: ${isGitRepo}
464
- Platform: ${platform}
465
- Shell: ${shellName}
466
- OS Version: ${osVersion}
467
- Today's date: ${today}
468
- </env>
469
- `;
470
- }