wave-agent-sdk 0.19.9 → 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 (89) hide show
  1. package/builtin/plugins/sdd/scripts/session-start.js +1 -1
  2. package/builtin/plugins/sdd/skills/specify/SKILL.md +3 -4
  3. package/builtin/skills/settings/ENV.md +15 -9
  4. package/builtin/skills/settings/HOOKS.md +27 -2
  5. package/dist/agent.d.ts +9 -20
  6. package/dist/agent.js +28 -99
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.js +1 -0
  9. package/dist/managers/aiManager.d.ts +71 -8
  10. package/dist/managers/aiManager.js +290 -85
  11. package/dist/managers/backgroundTaskManager.d.ts +6 -0
  12. package/dist/managers/backgroundTaskManager.js +11 -0
  13. package/dist/managers/bangManager.d.ts +6 -0
  14. package/dist/managers/bangManager.js +11 -0
  15. package/dist/managers/hookManager.d.ts +8 -2
  16. package/dist/managers/hookManager.js +14 -4
  17. package/dist/managers/mcpManager.d.ts +18 -4
  18. package/dist/managers/mcpManager.js +40 -18
  19. package/dist/managers/messageManager.d.ts +9 -5
  20. package/dist/managers/messageManager.js +36 -12
  21. package/dist/managers/subagentManager.d.ts +6 -0
  22. package/dist/managers/subagentManager.js +33 -22
  23. package/dist/managers/toolManager.js +5 -0
  24. package/dist/prompts/index.d.ts +0 -1
  25. package/dist/prompts/index.js +0 -4
  26. package/dist/services/aiService.d.ts +1 -34
  27. package/dist/services/aiService.js +18 -130
  28. package/dist/services/autoMemoryService.d.ts +27 -2
  29. package/dist/services/autoMemoryService.js +124 -36
  30. package/dist/services/configurationService.d.ts +21 -2
  31. package/dist/services/configurationService.js +86 -24
  32. package/dist/services/initializationService.js +14 -4
  33. package/dist/services/interactionService.js +35 -7
  34. package/dist/services/remoteSettingsService.d.ts +12 -0
  35. package/dist/services/remoteSettingsService.js +15 -1
  36. package/dist/services/session.d.ts +13 -0
  37. package/dist/services/session.js +64 -0
  38. package/dist/services/taskManager.js +7 -1
  39. package/dist/tools/bashTool.js +1 -0
  40. package/dist/tools/enterWorktreeTool.js +14 -3
  41. package/dist/tools/exitWorktreeTool.js +11 -10
  42. package/dist/tools/types.d.ts +7 -0
  43. package/dist/types/agent.d.ts +0 -2
  44. package/dist/types/config.d.ts +9 -0
  45. package/dist/types/core.d.ts +1 -1
  46. package/dist/types/hooks.d.ts +2 -2
  47. package/dist/utils/containerSetup.js +13 -4
  48. package/dist/utils/openaiClient.js +2 -1
  49. package/dist/utils/pathEncoder.js +7 -2
  50. package/dist/utils/worktreeUtils.d.ts +17 -0
  51. package/dist/utils/worktreeUtils.js +339 -1
  52. package/package.json +1 -1
  53. package/src/agent.ts +43 -112
  54. package/src/index.ts +1 -0
  55. package/src/managers/aiManager.ts +389 -110
  56. package/src/managers/backgroundTaskManager.ts +15 -0
  57. package/src/managers/bangManager.ts +15 -0
  58. package/src/managers/hookManager.ts +20 -5
  59. package/src/managers/mcpManager.ts +60 -18
  60. package/src/managers/messageManager.ts +51 -23
  61. package/src/managers/subagentManager.ts +36 -25
  62. package/src/managers/toolManager.ts +7 -0
  63. package/src/prompts/index.ts +0 -4
  64. package/src/services/aiService.ts +25 -203
  65. package/src/services/autoMemoryService.ts +145 -39
  66. package/src/services/configurationService.ts +100 -24
  67. package/src/services/initializationService.ts +17 -4
  68. package/src/services/interactionService.ts +49 -6
  69. package/src/services/remoteSettingsService.ts +16 -1
  70. package/src/services/session.ts +68 -0
  71. package/src/services/taskManager.ts +10 -1
  72. package/src/tools/bashTool.ts +1 -0
  73. package/src/tools/enterWorktreeTool.ts +19 -2
  74. package/src/tools/exitWorktreeTool.ts +15 -12
  75. package/src/tools/types.ts +7 -0
  76. package/src/types/agent.ts +0 -6
  77. package/src/types/config.ts +9 -0
  78. package/src/types/core.ts +1 -1
  79. package/src/types/hooks.ts +2 -2
  80. package/src/utils/containerSetup.ts +15 -5
  81. package/src/utils/openaiClient.ts +2 -0
  82. package/src/utils/pathEncoder.ts +7 -2
  83. package/src/utils/worktreeUtils.ts +401 -1
  84. package/dist/constants/goalPrompts.d.ts +0 -1
  85. package/dist/constants/goalPrompts.js +0 -10
  86. package/dist/managers/goalManager.d.ts +0 -42
  87. package/dist/managers/goalManager.js +0 -177
  88. package/src/constants/goalPrompts.ts +0 -10
  89. package/src/managers/goalManager.ts +0 -232
@@ -13,6 +13,7 @@ import {
13
13
  type HookExecutionResult,
14
14
  type HookValidationResult,
15
15
  type SessionEndSource,
16
+ type SessionStartSource,
16
17
  HookConfigurationError,
17
18
  isValidHookEvent,
18
19
  isValidHookEventConfig,
@@ -26,6 +27,7 @@ import { executeCommand, isCommandSafe } from "../services/hook.js";
26
27
  import { MessageSource } from "../types/index.js";
27
28
  import type { MessageManager } from "./messageManager.js";
28
29
  import { Container } from "../utils/container.js";
30
+ import type { ConfigurationService } from "../services/configurationService.js";
29
31
 
30
32
  import { logger } from "../utils/globalLogger.js";
31
33
 
@@ -46,6 +48,19 @@ export class HookManager {
46
48
  this.matcher = matcher;
47
49
  }
48
50
 
51
+ /**
52
+ * Merged env for this session (OS env overlaid with the per-session settings
53
+ * snapshot). Hook subprocesses spawn with this env so settings.json `env`
54
+ * vars reach hooks without polluting other sessions in one stdio process.
55
+ */
56
+ private get sessionEnv(): Record<string, string> {
57
+ return (
58
+ this.container
59
+ .get<ConfigurationService>("ConfigurationService")
60
+ ?.getMergedEnv?.() ?? (process.env as Record<string, string>)
61
+ );
62
+ }
63
+
49
64
  /**
50
65
  * Load hook configuration from programmatic source (AgentOptions.hooks)
51
66
  */
@@ -916,7 +931,7 @@ export class HookManager {
916
931
  * Collects additionalContext and initialUserMessage from hook stdout.
917
932
  */
918
933
  async executeSessionStartHooks(
919
- source: "startup" | "compact" | "clear",
934
+ source: SessionStartSource,
920
935
  sessionId: string,
921
936
  transcriptPath: string,
922
937
  agentType?: string,
@@ -935,7 +950,7 @@ export class HookManager {
935
950
  source,
936
951
  agentType,
937
952
  env: Object.fromEntries(
938
- Object.entries(process.env).filter((e) => e[1] !== undefined),
953
+ Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined),
939
954
  ) as Record<string, string>,
940
955
  };
941
956
 
@@ -989,7 +1004,7 @@ export class HookManager {
989
1004
  cwd: this.workdir,
990
1005
  endSource: source,
991
1006
  env: Object.fromEntries(
992
- Object.entries(process.env).filter((e) => e[1] !== undefined),
1007
+ Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined),
993
1008
  ) as Record<string, string>,
994
1009
  };
995
1010
 
@@ -1024,7 +1039,7 @@ export class HookManager {
1024
1039
  cwd: this.workdir,
1025
1040
  compactInstructions: customInstructions,
1026
1041
  env: Object.fromEntries(
1027
- Object.entries(process.env).filter((e) => e[1] !== undefined),
1042
+ Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined),
1028
1043
  ) as Record<string, string>,
1029
1044
  };
1030
1045
 
@@ -1061,7 +1076,7 @@ export class HookManager {
1061
1076
  cwd: this.workdir,
1062
1077
  compactSummary,
1063
1078
  env: Object.fromEntries(
1064
- Object.entries(process.env).filter((e) => e[1] !== undefined),
1079
+ Object.entries(this.sessionEnv).filter((e) => e[1] !== undefined),
1065
1080
  ) as Record<string, string>,
1066
1081
  };
1067
1082
 
@@ -9,6 +9,7 @@ import { ChatCompletionFunctionTool } from "openai/resources.js";
9
9
  import { createMcpToolPlugin, findToolServer } from "../utils/mcpUtils.js";
10
10
  import type { ToolPlugin, ToolResult, ToolContext } from "../tools/types.js";
11
11
  import { Container } from "../utils/container.js";
12
+ import type { ConfigurationService } from "../services/configurationService.js";
12
13
  import type {
13
14
  Logger,
14
15
  McpServerConfig,
@@ -42,7 +43,21 @@ export interface McpManagerOptions {
42
43
  */
43
44
  const WAVE_TEMPLATE_VARS = ["WAVE_PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT"];
44
45
 
45
- export function expandEnvVars(value: string): string {
46
+ /**
47
+ * Expand environment variables in a string value.
48
+ * Supports ${VAR} and ${VAR:-default} patterns.
49
+ *
50
+ * @param env - Merged env to resolve against (session snapshot over OS env).
51
+ * Defaults to `process.env` for backward compatibility with tests
52
+ * and standalone callers.
53
+ */
54
+ export function expandEnvVars(
55
+ value: string,
56
+ env: Record<string, string | undefined> = process.env as Record<
57
+ string,
58
+ string | undefined
59
+ >,
60
+ ): string {
46
61
  return value.replace(/\$\{([^}]+)\}/g, (_match, expr: string) => {
47
62
  const [varName, ...rest] = expr.split(":-");
48
63
  const defaultValue = rest.join(":-");
@@ -50,45 +65,53 @@ export function expandEnvVars(value: string): string {
50
65
  if (WAVE_TEMPLATE_VARS.includes(varName)) {
51
66
  return _match; // return original ${...} string untouched
52
67
  }
53
- return process.env[varName] ?? defaultValue;
68
+ return env[varName] ?? process.env[varName] ?? defaultValue;
54
69
  });
55
70
  }
56
71
 
57
72
  /**
58
73
  * Walk an MCP config and resolve environment variables in all string fields.
59
- * Only expands ${VAR} from process.env (skipping WAVE_PLUGIN_ROOT which is
60
- * handled at spawn time).
74
+ * Expands ${VAR} against the session env snapshot (over OS env); falls back to
75
+ * process.env. WAVE_PLUGIN_ROOT is skipped — handled at spawn time.
61
76
  */
62
- export function resolveMcpConfig(config: McpConfig): McpConfig {
77
+ export function resolveMcpConfig(
78
+ config: McpConfig,
79
+ env: Record<string, string | undefined> = process.env as Record<
80
+ string,
81
+ string | undefined
82
+ >,
83
+ ): McpConfig {
63
84
  const resolved: McpConfig = { mcpServers: {} };
64
85
 
65
86
  for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
66
87
  const resolvedServer: McpServerConfig = { ...serverConfig };
67
88
 
68
89
  if (resolvedServer.command) {
69
- resolvedServer.command = expandEnvVars(resolvedServer.command);
90
+ resolvedServer.command = expandEnvVars(resolvedServer.command, env);
70
91
  }
71
92
 
72
93
  if (resolvedServer.args) {
73
- resolvedServer.args = resolvedServer.args.map(expandEnvVars);
94
+ resolvedServer.args = resolvedServer.args.map((a) =>
95
+ expandEnvVars(a, env),
96
+ );
74
97
  }
75
98
 
76
99
  if (resolvedServer.env) {
77
100
  const resolvedEnv: Record<string, string> = {};
78
101
  for (const [key, val] of Object.entries(resolvedServer.env)) {
79
- resolvedEnv[key] = expandEnvVars(val);
102
+ resolvedEnv[key] = expandEnvVars(val, env);
80
103
  }
81
104
  resolvedServer.env = resolvedEnv;
82
105
  }
83
106
 
84
107
  if (resolvedServer.url) {
85
- resolvedServer.url = expandEnvVars(resolvedServer.url);
108
+ resolvedServer.url = expandEnvVars(resolvedServer.url, env);
86
109
  }
87
110
 
88
111
  if (resolvedServer.headers) {
89
112
  const resolvedHeaders: Record<string, string> = {};
90
113
  for (const [key, val] of Object.entries(resolvedServer.headers)) {
91
- resolvedHeaders[key] = expandEnvVars(val);
114
+ resolvedHeaders[key] = expandEnvVars(val, env);
92
115
  }
93
116
  resolvedServer.headers = resolvedHeaders;
94
117
  }
@@ -119,6 +142,19 @@ export class McpManager {
119
142
  this.mcpServers = options.mcpServers;
120
143
  }
121
144
 
145
+ /**
146
+ * Merged env for this session: OS env overlaid with the per-session settings
147
+ * snapshot. MCP env-var expansion (${VAR}) resolves against this so multiple
148
+ * sessions in one `wave --stdio` process don't read each other's settings env.
149
+ */
150
+ private get envSnapshot(): Record<string, string> {
151
+ return (
152
+ this.container
153
+ .get<ConfigurationService>("ConfigurationService")
154
+ ?.getMergedEnv?.() ?? (process.env as Record<string, string>)
155
+ );
156
+ }
157
+
122
158
  /**
123
159
  * Initialize MCP manager with working directory and optionally auto-connect
124
160
  */
@@ -189,7 +225,7 @@ export class McpManager {
189
225
  try {
190
226
  const configContent = await fs.readFile(this.configPath, "utf-8");
191
227
  const rawConfig: McpConfig = JSON.parse(configContent);
192
- const workspaceConfig = resolveMcpConfig(rawConfig);
228
+ const workspaceConfig = resolveMcpConfig(rawConfig, this.envSnapshot);
193
229
 
194
230
  // Extract original (pre-resolution) URLs for safe display
195
231
  const originalUrls: Record<string, string | undefined> = {};
@@ -284,28 +320,31 @@ export class McpManager {
284
320
  // Capture original URL before any resolution for safe display
285
321
  const originalUrl = config.url;
286
322
 
287
- // Expand env vars from process.env (e.g. ${TAVILY_API_KEY})
323
+ // Expand env vars against the session snapshot (over OS env, e.g. ${TAVILY_API_KEY})
324
+ const env = this.envSnapshot;
288
325
  const resolvedConfig: McpServerConfig = { ...config };
289
326
  if (resolvedConfig.command) {
290
- resolvedConfig.command = expandEnvVars(resolvedConfig.command);
327
+ resolvedConfig.command = expandEnvVars(resolvedConfig.command, env);
291
328
  }
292
329
  if (resolvedConfig.args) {
293
- resolvedConfig.args = resolvedConfig.args.map(expandEnvVars);
330
+ resolvedConfig.args = resolvedConfig.args.map((a) =>
331
+ expandEnvVars(a, env),
332
+ );
294
333
  }
295
334
  if (resolvedConfig.env) {
296
335
  const resolvedEnv: Record<string, string> = {};
297
336
  for (const [key, val] of Object.entries(resolvedConfig.env)) {
298
- resolvedEnv[key] = expandEnvVars(val);
337
+ resolvedEnv[key] = expandEnvVars(val, env);
299
338
  }
300
339
  resolvedConfig.env = resolvedEnv;
301
340
  }
302
341
  if (resolvedConfig.url) {
303
- resolvedConfig.url = expandEnvVars(resolvedConfig.url);
342
+ resolvedConfig.url = expandEnvVars(resolvedConfig.url, env);
304
343
  }
305
344
  if (resolvedConfig.headers) {
306
345
  const resolvedHeaders: Record<string, string> = {};
307
346
  for (const [key, val] of Object.entries(resolvedConfig.headers)) {
308
- resolvedHeaders[key] = expandEnvVars(val);
347
+ resolvedHeaders[key] = expandEnvVars(val, env);
309
348
  }
310
349
  resolvedConfig.headers = resolvedHeaders;
311
350
  }
@@ -433,8 +472,11 @@ export class McpManager {
433
472
  );
434
473
  }
435
474
 
475
+ // Base env = OS env overlaid with this session's settings snapshot, so
476
+ // custom env vars from settings.json reach the MCP server subprocess
477
+ // without polluting other sessions sharing one `wave --stdio` process.
436
478
  const env: Record<string, string> = {
437
- ...(process.env as Record<string, string>),
479
+ ...this.envSnapshot,
438
480
  ...(server.config.env || {}),
439
481
  };
440
482
 
@@ -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
  }
@@ -253,6 +253,13 @@ class ToolManager {
253
253
  "WorkflowManager",
254
254
  )
255
255
  : undefined,
256
+ sessionEnv: this.container.has("ConfigurationService")
257
+ ? this.container
258
+ .get<
259
+ import("../services/configurationService.js").ConfigurationService
260
+ >("ConfigurationService")
261
+ ?.getMergedEnv?.()
262
+ : undefined,
256
263
  sessionId: context.sessionId,
257
264
  toolCallId: context.toolCallId,
258
265
  };
@@ -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,