wave-agent-sdk 0.17.1 → 0.17.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 (169) hide show
  1. package/builtin/skills/deep-research/SKILL.md +90 -0
  2. package/builtin/skills/settings/ENV.md +6 -3
  3. package/dist/agent.d.ts +28 -1
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +128 -34
  6. package/dist/constants/goalPrompts.d.ts +2 -0
  7. package/dist/constants/goalPrompts.d.ts.map +1 -0
  8. package/dist/constants/goalPrompts.js +10 -0
  9. package/dist/constants/tools.d.ts +1 -0
  10. package/dist/constants/tools.d.ts.map +1 -1
  11. package/dist/constants/tools.js +1 -0
  12. package/dist/managers/aiManager.d.ts +7 -0
  13. package/dist/managers/aiManager.d.ts.map +1 -1
  14. package/dist/managers/aiManager.js +77 -41
  15. package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
  16. package/dist/managers/backgroundTaskManager.js +10 -2
  17. package/dist/managers/goalManager.d.ts +43 -0
  18. package/dist/managers/goalManager.d.ts.map +1 -0
  19. package/dist/managers/goalManager.js +177 -0
  20. package/dist/managers/messageManager.d.ts +2 -2
  21. package/dist/managers/messageManager.d.ts.map +1 -1
  22. package/dist/managers/messageQueue.d.ts +10 -0
  23. package/dist/managers/messageQueue.d.ts.map +1 -1
  24. package/dist/managers/messageQueue.js +53 -1
  25. package/dist/managers/pluginManager.d.ts.map +1 -1
  26. package/dist/managers/pluginManager.js +7 -1
  27. package/dist/managers/skillManager.d.ts +2 -0
  28. package/dist/managers/skillManager.d.ts.map +1 -1
  29. package/dist/managers/skillManager.js +19 -9
  30. package/dist/managers/slashCommandManager.d.ts +6 -0
  31. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  32. package/dist/managers/slashCommandManager.js +105 -0
  33. package/dist/managers/toolManager.d.ts.map +1 -1
  34. package/dist/managers/toolManager.js +5 -0
  35. package/dist/managers/workflowManager.d.ts +65 -0
  36. package/dist/managers/workflowManager.d.ts.map +1 -0
  37. package/dist/managers/workflowManager.js +380 -0
  38. package/dist/prompts/index.d.ts +2 -1
  39. package/dist/prompts/index.d.ts.map +1 -1
  40. package/dist/prompts/index.js +3 -3
  41. package/dist/services/aiService.d.ts +23 -0
  42. package/dist/services/aiService.d.ts.map +1 -1
  43. package/dist/services/aiService.js +102 -9
  44. package/dist/services/configurationService.d.ts +1 -1
  45. package/dist/services/configurationService.d.ts.map +1 -1
  46. package/dist/services/configurationService.js +3 -16
  47. package/dist/services/hook.d.ts.map +1 -1
  48. package/dist/services/hook.js +4 -0
  49. package/dist/services/session.d.ts +9 -1
  50. package/dist/services/session.d.ts.map +1 -1
  51. package/dist/services/session.js +28 -1
  52. package/dist/tools/bashTool.d.ts.map +1 -1
  53. package/dist/tools/bashTool.js +49 -7
  54. package/dist/tools/readTool.d.ts.map +1 -1
  55. package/dist/tools/readTool.js +1 -1
  56. package/dist/tools/taskManagementTools.d.ts.map +1 -1
  57. package/dist/tools/taskManagementTools.js +103 -157
  58. package/dist/tools/types.d.ts +2 -0
  59. package/dist/tools/types.d.ts.map +1 -1
  60. package/dist/tools/webFetchTool.d.ts.map +1 -1
  61. package/dist/tools/webFetchTool.js +0 -9
  62. package/dist/tools/workflowTool.d.ts +11 -0
  63. package/dist/tools/workflowTool.d.ts.map +1 -0
  64. package/dist/tools/workflowTool.js +190 -0
  65. package/dist/types/agent.d.ts +2 -0
  66. package/dist/types/agent.d.ts.map +1 -1
  67. package/dist/types/commands.d.ts +4 -0
  68. package/dist/types/commands.d.ts.map +1 -1
  69. package/dist/types/config.d.ts +2 -2
  70. package/dist/types/config.d.ts.map +1 -1
  71. package/dist/types/core.d.ts +1 -1
  72. package/dist/types/core.d.ts.map +1 -1
  73. package/dist/types/hooks.d.ts +2 -0
  74. package/dist/types/hooks.d.ts.map +1 -1
  75. package/dist/types/index.d.ts +1 -0
  76. package/dist/types/index.d.ts.map +1 -1
  77. package/dist/types/index.js +1 -0
  78. package/dist/types/messaging.d.ts +2 -2
  79. package/dist/types/messaging.d.ts.map +1 -1
  80. package/dist/types/processes.d.ts +6 -2
  81. package/dist/types/processes.d.ts.map +1 -1
  82. package/dist/types/workflow.d.ts +2 -0
  83. package/dist/types/workflow.d.ts.map +1 -0
  84. package/dist/types/workflow.js +1 -0
  85. package/dist/utils/cacheControlUtils.d.ts +13 -8
  86. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  87. package/dist/utils/cacheControlUtils.js +73 -102
  88. package/dist/utils/containerSetup.d.ts.map +1 -1
  89. package/dist/utils/containerSetup.js +7 -0
  90. package/dist/utils/markdownParser.d.ts.map +1 -1
  91. package/dist/utils/markdownParser.js +21 -6
  92. package/dist/utils/messageOperations.d.ts +2 -2
  93. package/dist/utils/messageOperations.d.ts.map +1 -1
  94. package/dist/utils/notificationXml.d.ts.map +1 -1
  95. package/dist/workflow/budgetTracker.d.ts +12 -0
  96. package/dist/workflow/budgetTracker.d.ts.map +1 -0
  97. package/dist/workflow/budgetTracker.js +30 -0
  98. package/dist/workflow/concurrencyLimiter.d.ts +14 -0
  99. package/dist/workflow/concurrencyLimiter.d.ts.map +1 -0
  100. package/dist/workflow/concurrencyLimiter.js +39 -0
  101. package/dist/workflow/journal.d.ts +19 -0
  102. package/dist/workflow/journal.d.ts.map +1 -0
  103. package/dist/workflow/journal.js +74 -0
  104. package/dist/workflow/progressReporter.d.ts +21 -0
  105. package/dist/workflow/progressReporter.d.ts.map +1 -0
  106. package/dist/workflow/progressReporter.js +118 -0
  107. package/dist/workflow/runState.d.ts +16 -0
  108. package/dist/workflow/runState.d.ts.map +1 -0
  109. package/dist/workflow/runState.js +57 -0
  110. package/dist/workflow/scriptRuntime.d.ts +35 -0
  111. package/dist/workflow/scriptRuntime.d.ts.map +1 -0
  112. package/dist/workflow/scriptRuntime.js +196 -0
  113. package/dist/workflow/structuredOutput.d.ts +27 -0
  114. package/dist/workflow/structuredOutput.d.ts.map +1 -0
  115. package/dist/workflow/structuredOutput.js +106 -0
  116. package/dist/workflow/types.d.ts +81 -0
  117. package/dist/workflow/types.d.ts.map +1 -0
  118. package/dist/workflow/types.js +1 -0
  119. package/dist/workflow/workflowApis.d.ts +46 -0
  120. package/dist/workflow/workflowApis.d.ts.map +1 -0
  121. package/dist/workflow/workflowApis.js +280 -0
  122. package/package.json +1 -1
  123. package/src/agent.ts +144 -34
  124. package/src/constants/goalPrompts.ts +10 -0
  125. package/src/constants/tools.ts +1 -0
  126. package/src/managers/aiManager.ts +91 -47
  127. package/src/managers/backgroundTaskManager.ts +16 -4
  128. package/src/managers/goalManager.ts +232 -0
  129. package/src/managers/messageManager.ts +2 -2
  130. package/src/managers/messageQueue.ts +59 -1
  131. package/src/managers/pluginManager.ts +8 -1
  132. package/src/managers/skillManager.ts +20 -9
  133. package/src/managers/slashCommandManager.ts +119 -0
  134. package/src/managers/toolManager.ts +7 -0
  135. package/src/managers/workflowManager.ts +491 -0
  136. package/src/prompts/index.ts +4 -2
  137. package/src/services/aiService.ts +166 -12
  138. package/src/services/configurationService.ts +2 -22
  139. package/src/services/hook.ts +5 -0
  140. package/src/services/session.ts +42 -2
  141. package/src/tools/bashTool.ts +64 -9
  142. package/src/tools/readTool.ts +1 -2
  143. package/src/tools/taskManagementTools.ts +146 -195
  144. package/src/tools/types.ts +2 -0
  145. package/src/tools/webFetchTool.ts +0 -12
  146. package/src/tools/workflowTool.ts +205 -0
  147. package/src/types/agent.ts +6 -0
  148. package/src/types/commands.ts +4 -0
  149. package/src/types/config.ts +2 -2
  150. package/src/types/core.ts +3 -3
  151. package/src/types/hooks.ts +2 -0
  152. package/src/types/index.ts +1 -0
  153. package/src/types/messaging.ts +2 -2
  154. package/src/types/processes.ts +10 -2
  155. package/src/types/workflow.ts +5 -0
  156. package/src/utils/cacheControlUtils.ts +106 -131
  157. package/src/utils/containerSetup.ts +9 -0
  158. package/src/utils/markdownParser.ts +26 -8
  159. package/src/utils/messageOperations.ts +2 -2
  160. package/src/utils/notificationXml.ts +6 -1
  161. package/src/workflow/budgetTracker.ts +34 -0
  162. package/src/workflow/concurrencyLimiter.ts +47 -0
  163. package/src/workflow/journal.ts +95 -0
  164. package/src/workflow/progressReporter.ts +141 -0
  165. package/src/workflow/runState.ts +65 -0
  166. package/src/workflow/scriptRuntime.ts +274 -0
  167. package/src/workflow/structuredOutput.ts +123 -0
  168. package/src/workflow/types.ts +95 -0
  169. package/src/workflow/workflowApis.ts +412 -0
@@ -236,6 +236,7 @@ export function buildSystemPrompt(
236
236
  tools: ToolPlugin[],
237
237
  options: {
238
238
  workdir?: string;
239
+ originalWorkdir?: string;
239
240
  memory?: string;
240
241
  language?: string;
241
242
  isSubagent?: boolean;
@@ -283,7 +284,7 @@ export function buildSystemPrompt(
283
284
 
284
285
  Here is useful information about the environment you are running in:
285
286
  <env>
286
- Working directory: ${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}.` : ""}
287
+ 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}.` : ""}
287
288
  Is directory a git repo: ${isGitRepo}
288
289
  Platform: ${platform}
289
290
  Shell: ${shellName}
@@ -310,6 +311,7 @@ Today's date: ${today}
310
311
  export function enhanceSystemPromptWithEnvDetails(
311
312
  existingSystemPrompt: string,
312
313
  workdir: string,
314
+ originalWorkdir?: string,
313
315
  ): string {
314
316
  const isGitRepo = isGitRepository(workdir);
315
317
  const platform = os.platform();
@@ -336,7 +338,7 @@ ${notes}
336
338
 
337
339
  Here is useful information about the environment you are running in:
338
340
  <env>
339
- Working directory: ${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}.` : ""}
341
+ 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}.` : ""}
340
342
  Is directory a git repo: ${isGitRepo}
341
343
  Platform: ${platform}
342
344
  Shell: ${shellName}
@@ -10,6 +10,7 @@ import { OpenAIClient } from "../utils/openaiClient.js";
10
10
  import { logger } from "../utils/globalLogger.js";
11
11
  import { addOnceAbortListener } from "../utils/abortUtils.js";
12
12
  import type { GatewayConfig, ModelConfig } from "../types/index.js";
13
+ import { ConfigurationError, CONFIG_ERRORS } from "../types/index.js";
13
14
  import {
14
15
  transformMessagesForClaudeCache,
15
16
  addCacheControlToLastTool,
@@ -27,6 +28,7 @@ import {
27
28
  WEB_CONTENT_SYSTEM_PROMPT,
28
29
  BTW_SYSTEM_PROMPT,
29
30
  } from "../prompts/index.js";
31
+ import { GOAL_EVALUATION_SYSTEM_PROMPT } from "../constants/goalPrompts.js";
30
32
 
31
33
  /**
32
34
  * Interface for debug data saved during 400 errors
@@ -160,6 +162,11 @@ export interface CallAgentOptions {
160
162
  model?: string; // Custom model
161
163
  systemPrompt?: string; // Custom system prompt
162
164
  maxTokens?: number; // Maximum output tokens
165
+ toolChoice?:
166
+ | "auto"
167
+ | "none"
168
+ | "required"
169
+ | { type: "function"; function: { name: string } }; // Force tool selection
163
170
 
164
171
  // NEW: Streaming callbacks
165
172
  onContentUpdate?: (content: string) => void;
@@ -189,6 +196,27 @@ export interface CallAgentResult {
189
196
  additionalFields?: Record<string, unknown>;
190
197
  }
191
198
 
199
+ function validateModelConfig(
200
+ modelConfig: ModelConfig,
201
+ ): asserts modelConfig is ModelConfig & { model: string; fastModel: string } {
202
+ if (!modelConfig.model) {
203
+ throw new ConfigurationError(CONFIG_ERRORS.MISSING_MODEL, "model", {
204
+ constructor: undefined,
205
+ environment: process.env.WAVE_MODEL,
206
+ });
207
+ }
208
+ if (!modelConfig.fastModel) {
209
+ throw new ConfigurationError(
210
+ CONFIG_ERRORS.MISSING_FAST_MODEL,
211
+ "fastModel",
212
+ {
213
+ constructor: undefined,
214
+ environment: process.env.WAVE_FAST_MODEL,
215
+ },
216
+ );
217
+ }
218
+ }
219
+
192
220
  export async function callAgent(
193
221
  options: CallAgentOptions,
194
222
  ): Promise<CallAgentResult> {
@@ -206,6 +234,9 @@ export async function callAgent(
206
234
  onReasoningUpdate,
207
235
  } = options;
208
236
 
237
+ // Validate model config at call time
238
+ validateModelConfig(modelConfig);
239
+
209
240
  // Apply global 1 QPS rate limit
210
241
  if (
211
242
  process.env.NODE_ENV !== "test" ||
@@ -301,6 +332,11 @@ export async function callAgent(
301
332
  createParams.tools = processedTools;
302
333
  }
303
334
 
335
+ // Add tool_choice if specified
336
+ if (options.toolChoice) {
337
+ createParams.tool_choice = options.toolChoice;
338
+ }
339
+
304
340
  if (isStreaming) {
305
341
  // Handle streaming response
306
342
  const { data: stream, response } = await openai.chat.completions
@@ -322,7 +358,6 @@ export async function callAgent(
322
358
  onReasoningUpdate,
323
359
  abortSignal,
324
360
  responseHeaders,
325
- currentModel,
326
361
  );
327
362
  } else {
328
363
  // Handle non-streaming response
@@ -350,8 +385,8 @@ export async function callAgent(
350
385
  }
351
386
  : undefined;
352
387
 
353
- // Extend usage with cache metrics for Claude models
354
- if (totalUsage && supportsPromptCaching(currentModel) && response.usage) {
388
+ // Extend usage with cache metrics (Claude top-level + OpenAI prompt_tokens_details)
389
+ if (totalUsage && response.usage) {
355
390
  totalUsage = extendUsageWithCacheMetrics(
356
391
  totalUsage,
357
392
  response.usage as Partial<ClaudeUsage>,
@@ -521,7 +556,6 @@ export async function callAgent(
521
556
  * @param onToolUpdate Callback for tool updates
522
557
  * @param abortSignal Optional abort signal
523
558
  * @param responseHeaders Response headers from the initial request
524
- * @param modelName Model name for cache control processing
525
559
  * @returns Final result with accumulated content and tool calls
526
560
  */
527
561
  async function processStreamingResponse(
@@ -537,7 +571,6 @@ async function processStreamingResponse(
537
571
  onReasoningUpdate?: (content: string) => void,
538
572
  abortSignal?: AbortSignal,
539
573
  responseHeaders?: Record<string, string>,
540
- modelName?: string,
541
574
  ): Promise<CallAgentResult> {
542
575
  let accumulatedContent = "";
543
576
  let accumulatedReasoningContent = "";
@@ -569,13 +602,11 @@ async function processStreamingResponse(
569
602
  total_tokens: chunk.usage.total_tokens,
570
603
  };
571
604
 
572
- // Extend usage with cache metrics for Claude models
573
- if (modelName && supportsPromptCaching(modelName)) {
574
- chunkUsage = extendUsageWithCacheMetrics(
575
- chunkUsage,
576
- chunk.usage as Partial<ClaudeUsage>,
577
- );
578
- }
605
+ // Extend usage with cache metrics (Claude top-level + OpenAI prompt_tokens_details)
606
+ chunkUsage = extendUsageWithCacheMetrics(
607
+ chunkUsage,
608
+ chunk.usage as Partial<ClaudeUsage>,
609
+ );
579
610
 
580
611
  usage = chunkUsage;
581
612
  }
@@ -775,6 +806,9 @@ export async function compactMessages(
775
806
  ): Promise<CompactMessagesResult> {
776
807
  const { gatewayConfig, modelConfig, messages, abortSignal } = options;
777
808
 
809
+ // Validate model config at call time
810
+ validateModelConfig(modelConfig);
811
+
778
812
  // Apply global 1 QPS rate limit
779
813
  if (
780
814
  process.env.NODE_ENV !== "test" ||
@@ -901,6 +935,9 @@ export async function processWebContent(
901
935
  ): Promise<ProcessWebContentResult> {
902
936
  const { gatewayConfig, modelConfig, content, prompt, abortSignal } = options;
903
937
 
938
+ // Validate model config at call time
939
+ validateModelConfig(modelConfig);
940
+
904
941
  // Apply global 1 QPS rate limit
905
942
  if (
906
943
  process.env.NODE_ENV !== "test" ||
@@ -1008,6 +1045,9 @@ export async function btw(options: BtwOptions): Promise<BtwResult> {
1008
1045
  const { gatewayConfig, modelConfig, messages, question, abortSignal } =
1009
1046
  options;
1010
1047
 
1048
+ // Validate model config at call time
1049
+ validateModelConfig(modelConfig);
1050
+
1011
1051
  // Apply global 1 QPS rate limit
1012
1052
  if (
1013
1053
  process.env.NODE_ENV !== "test" ||
@@ -1092,3 +1132,117 @@ export async function btw(options: BtwOptions): Promise<BtwResult> {
1092
1132
  throw error;
1093
1133
  }
1094
1134
  }
1135
+
1136
+ export interface EvaluateGoalOptions {
1137
+ gatewayConfig: GatewayConfig;
1138
+ modelConfig: ModelConfig;
1139
+ model: string;
1140
+ goalCondition: string;
1141
+ messages: ChatCompletionMessageParam[];
1142
+ abortSignal?: AbortSignal;
1143
+ }
1144
+
1145
+ export interface EvaluateGoalResult {
1146
+ content: string;
1147
+ usage?: {
1148
+ prompt_tokens: number;
1149
+ completion_tokens: number;
1150
+ total_tokens: number;
1151
+ };
1152
+ }
1153
+
1154
+ export async function evaluateGoal(
1155
+ options: EvaluateGoalOptions,
1156
+ ): Promise<EvaluateGoalResult> {
1157
+ const {
1158
+ gatewayConfig,
1159
+ modelConfig,
1160
+ model,
1161
+ goalCondition,
1162
+ messages,
1163
+ abortSignal,
1164
+ } = options;
1165
+
1166
+ // Create OpenAI client with injected configuration (no rate limiter — bypasses 1 QPS)
1167
+ const openai = new OpenAIClient({
1168
+ apiKey: gatewayConfig.apiKey,
1169
+ baseURL: gatewayConfig.baseURL,
1170
+ defaultHeaders: gatewayConfig.defaultHeaders,
1171
+ fetchOptions: gatewayConfig.fetchOptions,
1172
+ fetch: gatewayConfig.fetch,
1173
+ });
1174
+
1175
+ const {
1176
+ model: _model,
1177
+ fastModel: _fastModel,
1178
+ maxTokens: _maxTokens,
1179
+ permissionMode: _permissionMode,
1180
+ ...extraParams
1181
+ } = modelConfig;
1182
+ void _model;
1183
+ void _fastModel;
1184
+ void _maxTokens;
1185
+ void _permissionMode;
1186
+
1187
+ const openaiModelConfig = getModelConfig(model, {
1188
+ temperature: 0,
1189
+ max_tokens: 200,
1190
+ ...extraParams,
1191
+ });
1192
+
1193
+ // Strip images from messages to reduce token usage (same as compact)
1194
+ const cleanedMessages = messages.map((msg) => {
1195
+ if (Array.isArray(msg.content)) {
1196
+ const textParts = msg.content.filter(
1197
+ (part) => part.type === "text",
1198
+ ) as import("openai/resources.js").ChatCompletionContentPartText[];
1199
+ const text = textParts.map((p) => p.text).join("\n");
1200
+ return { ...msg, content: text || "(empty message)" };
1201
+ }
1202
+ return msg;
1203
+ });
1204
+
1205
+ try {
1206
+ const response = await openai.chat.completions.create(
1207
+ {
1208
+ ...openaiModelConfig,
1209
+ messages: [
1210
+ {
1211
+ role: "system",
1212
+ content: GOAL_EVALUATION_SYSTEM_PROMPT,
1213
+ },
1214
+ ...cleanedMessages,
1215
+ {
1216
+ role: "user",
1217
+ content: `Goal condition: ${goalCondition}\n\nHas this goal been achieved based on the conversation above?`,
1218
+ },
1219
+ ],
1220
+ },
1221
+ {
1222
+ signal: abortSignal,
1223
+ },
1224
+ );
1225
+
1226
+ const result = response.choices[0]?.message?.content?.trim();
1227
+ if (!result) {
1228
+ throw new Error("Goal evaluation returned empty response");
1229
+ }
1230
+
1231
+ const usage = response.usage
1232
+ ? {
1233
+ prompt_tokens: response.usage.prompt_tokens,
1234
+ completion_tokens: response.usage.completion_tokens,
1235
+ total_tokens: response.usage.total_tokens,
1236
+ }
1237
+ : undefined;
1238
+
1239
+ return { content: result, usage };
1240
+ } catch (error) {
1241
+ if ((error as Error).name === "AbortError") {
1242
+ logger.info("Goal evaluation was aborted");
1243
+ throw new Error("Goal evaluation was aborted");
1244
+ }
1245
+ logger.error("Goal evaluation failed:", error);
1246
+ throw error;
1247
+ }
1248
+ }
@@ -33,8 +33,6 @@ import {
33
33
  import {
34
34
  GatewayConfig,
35
35
  ModelConfig,
36
- ConfigurationError,
37
- CONFIG_ERRORS,
38
36
  PermissionMode,
39
37
  AgentOptions,
40
38
  } from "../types/index.js";
@@ -410,7 +408,7 @@ export class ConfigurationService {
410
408
  * @param fetchOptions - Fetch options override (optional)
411
409
  * @param fetch - Custom fetch implementation override (optional)
412
410
  * @returns Resolved gateway configuration
413
- * @throws ConfigurationError if required configuration is missing after fallbacks
411
+ * @returns Resolved model configuration (model/fastModel may be undefined if not yet configured)
414
412
  */
415
413
  resolveGatewayConfig(
416
414
  apiKey?: string,
@@ -524,25 +522,6 @@ export class ConfigurationService {
524
522
  const resolvedFastModel =
525
523
  fastModel || this.options.fastModel || process.env.WAVE_FAST_MODEL;
526
524
 
527
- // Validate required fields
528
- if (!resolvedAgentModel) {
529
- throw new ConfigurationError(CONFIG_ERRORS.MISSING_MODEL, "model", {
530
- constructor: model,
531
- environment: process.env.WAVE_MODEL,
532
- });
533
- }
534
-
535
- if (!resolvedFastModel) {
536
- throw new ConfigurationError(
537
- CONFIG_ERRORS.MISSING_FAST_MODEL,
538
- "fastModel",
539
- {
540
- constructor: fastModel,
541
- environment: process.env.WAVE_FAST_MODEL,
542
- },
543
- );
544
- }
545
-
546
525
  // Resolve max output tokens
547
526
  const resolvedMaxTokens = this.resolveMaxOutputTokens(maxTokens);
548
527
 
@@ -555,6 +534,7 @@ export class ConfigurationService {
555
534
 
556
535
  // Merge model-specific settings from configuration
557
536
  const modelSpecificConfig =
537
+ resolvedAgentModel &&
558
538
  this.currentConfiguration?.models?.[resolvedAgentModel];
559
539
 
560
540
  if (modelSpecificConfig) {
@@ -72,6 +72,11 @@ async function buildHookJsonInput(
72
72
  jsonInput.user_prompt = context.userPrompt;
73
73
  }
74
74
 
75
+ // Add plan_file_path if present
76
+ if (context.planFilePath !== undefined) {
77
+ jsonInput.plan_file_path = context.planFilePath;
78
+ }
79
+
75
80
  // Add subagent_type if present
76
81
  if (context.subagentType !== undefined) {
77
82
  jsonInput.subagent_type = context.subagentType;
@@ -45,6 +45,7 @@ export interface SessionMetadata {
45
45
  sessionType: "main" | "subagent";
46
46
  subagentType?: string;
47
47
  workdir: string;
48
+ createdAt: Date;
48
49
  lastActiveAt: Date;
49
50
  latestTotalTokens: number;
50
51
  firstMessage?: string;
@@ -53,7 +54,10 @@ export interface SessionMetadata {
53
54
  export interface SessionIndex {
54
55
  sessions: Record<
55
56
  string,
56
- Omit<SessionMetadata, "id" | "lastActiveAt"> & { lastActiveAt: string }
57
+ Omit<SessionMetadata, "id" | "lastActiveAt" | "createdAt"> & {
58
+ lastActiveAt: string;
59
+ createdAt: string;
60
+ }
57
61
  >;
58
62
  lastUpdated: string;
59
63
  }
@@ -77,6 +81,27 @@ export function generateSessionId(): string {
77
81
  return `${ts}-${shortId}`;
78
82
  }
79
83
 
84
+ /**
85
+ * Parse the timestamp prefix from a session ID back into a Date
86
+ * Format: {YYYYMMDDHHmmss}-{8hex} (e.g. 20260527143025-a1b2c3d4)
87
+ * @returns Date from the session ID timestamp prefix
88
+ */
89
+ export function parseSessionIdTimestamp(sessionId: string): Date {
90
+ const match = sessionId.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/);
91
+ if (!match) {
92
+ return new Date(); // Fallback to now for legacy UUID-format session IDs
93
+ }
94
+ const [, year, month, day, hour, minute, second] = match;
95
+ return new Date(
96
+ Number(year),
97
+ Number(month) - 1,
98
+ Number(day),
99
+ Number(hour),
100
+ Number(minute),
101
+ Number(second),
102
+ );
103
+ }
104
+
80
105
  /**
81
106
  * Generate filename for subagent sessions
82
107
  * @param sessionId - UUID session identifier
@@ -114,6 +139,7 @@ async function updateSessionIndex(
114
139
  const { id, ...rest } = metadata;
115
140
  index.sessions[id] = {
116
141
  ...rest,
142
+ createdAt: metadata.createdAt.toISOString(),
117
143
  lastActiveAt: metadata.lastActiveAt.toISOString(),
118
144
  firstMessage: metadata.firstMessage || index.sessions[id]?.firstMessage,
119
145
  parentSessionId: metadata.parentSessionId,
@@ -247,8 +273,9 @@ export async function appendMessages(
247
273
  const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
248
274
  const lastMessage = newMessages[newMessages.length - 1];
249
275
 
250
- // Get first message content if it's a new session or we don't have it
276
+ // Get first message content and createdAt from existing index
251
277
  let firstMessage: string | undefined;
278
+ let createdAt: Date | undefined;
252
279
  try {
253
280
  const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
254
281
  const content = await fs.readFile(indexPath, "utf8");
@@ -257,18 +284,27 @@ export async function appendMessages(
257
284
  firstMessage =
258
285
  (await getFirstMessageContent(sessionId, workdir)) || undefined;
259
286
  }
287
+ if (index.sessions[sessionId]?.createdAt) {
288
+ createdAt = new Date(index.sessions[sessionId].createdAt);
289
+ }
260
290
  } catch {
261
291
  // If index doesn't exist, this might be the first message
262
292
  firstMessage =
263
293
  (await getFirstMessageContent(sessionId, workdir)) || undefined;
264
294
  }
265
295
 
296
+ // Derive createdAt from session ID timestamp if not in index
297
+ if (!createdAt) {
298
+ createdAt = new Date(parseSessionIdTimestamp(sessionId));
299
+ }
300
+
266
301
  await updateSessionIndex(projectDir.encodedPath, {
267
302
  id: sessionId,
268
303
  rootSessionId,
269
304
  parentSessionId,
270
305
  sessionType,
271
306
  workdir,
307
+ createdAt,
272
308
  lastActiveAt: new Date(lastMessage.timestamp),
273
309
  latestTotalTokens: lastMessage.usage
274
310
  ? extractLatestTotalTokens([lastMessage])
@@ -447,6 +483,7 @@ export async function listSessionsFromJsonl(
447
483
  .map(([id, meta]) => ({
448
484
  id,
449
485
  ...meta,
486
+ createdAt: new Date(meta.createdAt),
450
487
  lastActiveAt: new Date(meta.lastActiveAt),
451
488
  }));
452
489
 
@@ -520,6 +557,7 @@ export async function listSessionsFromJsonl(
520
557
  sessionType: "main",
521
558
  subagentType: undefined, // No longer stored in metadata
522
559
  workdir: projectDir.originalPath,
560
+ createdAt: new Date(parseSessionIdTimestamp(sessionId)),
523
561
  lastActiveAt,
524
562
  latestTotalTokens: lastMessage?.usage
525
563
  ? extractLatestTotalTokens([lastMessage])
@@ -558,6 +596,7 @@ export async function listSessionsFromJsonl(
558
596
  const { id, ...rest } = session;
559
597
  index.sessions[id] = {
560
598
  ...rest,
599
+ createdAt: session.createdAt.toISOString(),
561
600
  lastActiveAt: session.lastActiveAt.toISOString(),
562
601
  };
563
602
  }
@@ -613,6 +652,7 @@ export async function listAllSessions(): Promise<SessionMetadata[]> {
613
652
  allSessions.push({
614
653
  id,
615
654
  ...meta,
655
+ createdAt: new Date(meta.createdAt),
616
656
  lastActiveAt,
617
657
  });
618
658
  }
@@ -18,6 +18,17 @@ import {
18
18
 
19
19
  const BASH_DEFAULT_TIMEOUT_MS = 120000;
20
20
 
21
+ // Commands that should not be auto-backgrounded on timeout (e.g. sleep should just be killed)
22
+ const DISALLOWED_AUTO_BACKGROUND_COMMANDS = ["sleep"];
23
+
24
+ function isAutobackgroundingAllowed(command: string): boolean {
25
+ const trimmed = command.trim();
26
+ // Get the first word of the command
27
+ const baseCommand = trimmed.split(/\s+/)[0];
28
+ if (!baseCommand) return true;
29
+ return !DISALLOWED_AUTO_BACKGROUND_COMMANDS.includes(baseCommand);
30
+ }
31
+
21
32
  /**
22
33
  * Bash command execution tool - supports both foreground and background execution
23
34
  */
@@ -125,9 +136,12 @@ The working directory persists between commands. Try to maintain your current wo
125
136
  const runInBackground = args.run_in_background as boolean | undefined;
126
137
  const description = args.description as string | undefined;
127
138
  // Set default timeout: BASH_DEFAULT_TIMEOUT_MS for foreground, no timeout for background
128
- const timeout =
129
- (args.timeout as number | undefined) ??
130
- (runInBackground ? undefined : BASH_DEFAULT_TIMEOUT_MS);
139
+ // When run_in_background is explicitly set, cancel any timeout — the intent is to
140
+ // let the process run to completion (matching Claude Code behavior where background()
141
+ // clears the timeout via cleanupListeners).
142
+ const timeout = runInBackground
143
+ ? undefined
144
+ : ((args.timeout as number | undefined) ?? BASH_DEFAULT_TIMEOUT_MS);
131
145
 
132
146
  if (!command || typeof command !== "string") {
133
147
  return {
@@ -195,7 +209,7 @@ The working directory persists between commands. Try to maintain your current wo
195
209
  };
196
210
  }
197
211
 
198
- const { id: taskId } = backgroundTaskManager.startShell(command, timeout);
212
+ const { id: taskId } = backgroundTaskManager.startShell(command);
199
213
  const task = backgroundTaskManager.getTask(taskId);
200
214
  const outputPath = task?.outputPath;
201
215
  const backgroundMsg = [
@@ -300,12 +314,47 @@ The working directory persists between commands. Try to maintain your current wo
300
314
  });
301
315
  }
302
316
 
303
- // Set up timeout
317
+ // Set up timeout — auto-background if allowed, otherwise kill
304
318
  let timeoutHandle: NodeJS.Timeout | undefined;
319
+ const shouldAutoBackground =
320
+ !!context.backgroundTaskManager && isAutobackgroundingAllowed(command);
305
321
  if (timeout && timeout > 0) {
306
322
  timeoutHandle = setTimeout(() => {
307
- if (!isAborted) {
308
- handleAbort("Command timed out");
323
+ if (!isAborted && !isBackgrounded) {
324
+ if (shouldAutoBackground) {
325
+ // Auto-background: move the process to background task manager instead of killing it
326
+ isBackgrounded = true;
327
+ if (timeoutHandle) {
328
+ clearTimeout(timeoutHandle);
329
+ }
330
+
331
+ // Unregister foreground task since it's now backgrounded
332
+ if (context.foregroundTaskManager) {
333
+ context.foregroundTaskManager.unregisterForegroundTask(
334
+ foregroundTaskId,
335
+ );
336
+ }
337
+
338
+ const backgroundTaskManager = context.backgroundTaskManager!;
339
+ const taskId = backgroundTaskManager.adoptProcess(
340
+ child,
341
+ command,
342
+ outputBuffer,
343
+ errorBuffer,
344
+ );
345
+ const task = backgroundTaskManager.getTask(taskId);
346
+ const outputPath = task?.outputPath;
347
+ logger.info(
348
+ `[Bash] Command timed out after ${timeout}ms, auto-backgrounded as ${taskId}`,
349
+ );
350
+ resolve({
351
+ success: true,
352
+ content: `Command timed out after ${timeout / 1000} seconds and was moved to background with ID: ${taskId}.${outputPath ? ` Real-time output: ${outputPath}` : ""}`,
353
+ shortResult: `Process ${taskId} auto-backgrounded (timeout)`,
354
+ });
355
+ } else {
356
+ handleAbort("Command timed out");
357
+ }
309
358
  }
310
359
  }, timeout);
311
360
  }
@@ -330,8 +379,14 @@ The working directory persists between commands. Try to maintain your current wo
330
379
  if (child.pid && !child.killed) {
331
380
  try {
332
381
  process.kill(-child.pid, "SIGKILL");
333
- } catch (killError) {
334
- logger.error("Failed to force kill process:", killError);
382
+ } catch (killError: unknown) {
383
+ // ESRCH means the process already exited — not an error
384
+ if (
385
+ !(killError instanceof Error) ||
386
+ (killError as NodeJS.ErrnoException).code !== "ESRCH"
387
+ ) {
388
+ logger.error("Failed to force kill process:", killError);
389
+ }
335
390
  }
336
391
  }
337
392
  }, 1000);
@@ -295,8 +295,7 @@ Usage:
295
295
  logger.warn(`File ${filePath} exists but has empty contents`);
296
296
  return {
297
297
  success: true,
298
- content:
299
- "⚠️ System reminder: This file exists but has empty contents.",
298
+ content: "System reminder: This file exists but has empty contents.",
300
299
  shortResult: "Empty file",
301
300
  metadata: {
302
301
  type: "text",