wave-agent-sdk 0.18.7 → 0.19.0

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 (99) hide show
  1. package/builtin/skills/settings/ENV.md +0 -1
  2. package/builtin/skills/settings/HOOKS.md +10 -0
  3. package/builtin/skills/settings/MODELS.md +37 -0
  4. package/builtin/skills/settings/SKILLS.md +6 -4
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +12 -5
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/managers/aiManager.d.ts.map +1 -1
  11. package/dist/managers/aiManager.js +20 -4
  12. package/dist/managers/mcpManager.d.ts.map +1 -1
  13. package/dist/managers/mcpManager.js +7 -2
  14. package/dist/managers/messageManager.d.ts +3 -5
  15. package/dist/managers/messageManager.d.ts.map +1 -1
  16. package/dist/managers/messageManager.js +17 -77
  17. package/dist/managers/skillManager.d.ts +1 -0
  18. package/dist/managers/skillManager.d.ts.map +1 -1
  19. package/dist/managers/skillManager.js +11 -2
  20. package/dist/prompts/index.d.ts +3 -2
  21. package/dist/prompts/index.d.ts.map +1 -1
  22. package/dist/prompts/index.js +5 -1
  23. package/dist/services/aiService.d.ts.map +1 -1
  24. package/dist/services/aiService.js +13 -9
  25. package/dist/services/configurationService.d.ts.map +1 -1
  26. package/dist/services/configurationService.js +22 -9
  27. package/dist/services/hook.d.ts.map +1 -1
  28. package/dist/services/hook.js +6 -0
  29. package/dist/services/initializationService.d.ts.map +1 -1
  30. package/dist/services/initializationService.js +1 -1
  31. package/dist/services/interactionService.js +1 -1
  32. package/dist/services/reversionService.d.ts +5 -1
  33. package/dist/services/reversionService.d.ts.map +1 -1
  34. package/dist/services/reversionService.js +50 -22
  35. package/dist/services/session.d.ts +4 -13
  36. package/dist/services/session.d.ts.map +1 -1
  37. package/dist/services/session.js +59 -215
  38. package/dist/telemetry/sessionTracing.d.ts +14 -4
  39. package/dist/telemetry/sessionTracing.d.ts.map +1 -1
  40. package/dist/telemetry/sessionTracing.js +134 -8
  41. package/dist/tools/bashTool.d.ts.map +1 -1
  42. package/dist/tools/bashTool.js +16 -1
  43. package/dist/types/config.d.ts +7 -0
  44. package/dist/types/config.d.ts.map +1 -1
  45. package/dist/types/hooks.d.ts +1 -0
  46. package/dist/types/hooks.d.ts.map +1 -1
  47. package/dist/types/messaging.d.ts +0 -1
  48. package/dist/types/messaging.d.ts.map +1 -1
  49. package/dist/types/skills.d.ts +1 -0
  50. package/dist/types/skills.d.ts.map +1 -1
  51. package/dist/types/telemetry.d.ts +14 -1
  52. package/dist/types/telemetry.d.ts.map +1 -1
  53. package/dist/utils/atomicWrite.d.ts +15 -0
  54. package/dist/utils/atomicWrite.d.ts.map +1 -0
  55. package/dist/utils/atomicWrite.js +59 -0
  56. package/dist/utils/cacheControlUtils.d.ts +3 -15
  57. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  58. package/dist/utils/cacheControlUtils.js +4 -35
  59. package/dist/utils/containerSetup.d.ts.map +1 -1
  60. package/dist/utils/containerSetup.js +5 -1
  61. package/dist/utils/convertMessagesForAPI.d.ts +9 -1
  62. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  63. package/dist/utils/convertMessagesForAPI.js +74 -55
  64. package/dist/utils/mcpUtils.d.ts.map +1 -1
  65. package/dist/utils/mcpUtils.js +2 -1
  66. package/dist/utils/modelCapabilities.d.ts +22 -0
  67. package/dist/utils/modelCapabilities.d.ts.map +1 -0
  68. package/dist/utils/modelCapabilities.js +24 -0
  69. package/dist/utils/skillParser.d.ts.map +1 -1
  70. package/dist/utils/skillParser.js +3 -1
  71. package/package.json +1 -1
  72. package/src/agent.ts +11 -5
  73. package/src/index.ts +1 -0
  74. package/src/managers/aiManager.ts +22 -2
  75. package/src/managers/mcpManager.ts +8 -2
  76. package/src/managers/messageManager.ts +22 -104
  77. package/src/managers/skillManager.ts +16 -2
  78. package/src/prompts/index.ts +6 -1
  79. package/src/services/aiService.ts +12 -5
  80. package/src/services/configurationService.ts +24 -24
  81. package/src/services/hook.ts +7 -0
  82. package/src/services/initializationService.ts +1 -3
  83. package/src/services/interactionService.ts +1 -1
  84. package/src/services/reversionService.ts +50 -27
  85. package/src/services/session.ts +63 -262
  86. package/src/telemetry/sessionTracing.ts +167 -12
  87. package/src/tools/bashTool.ts +23 -1
  88. package/src/types/config.ts +8 -0
  89. package/src/types/hooks.ts +1 -0
  90. package/src/types/messaging.ts +0 -1
  91. package/src/types/skills.ts +1 -0
  92. package/src/types/telemetry.ts +17 -1
  93. package/src/utils/atomicWrite.ts +61 -0
  94. package/src/utils/cacheControlUtils.ts +5 -38
  95. package/src/utils/containerSetup.ts +5 -1
  96. package/src/utils/convertMessagesForAPI.ts +87 -57
  97. package/src/utils/mcpUtils.ts +2 -1
  98. package/src/utils/modelCapabilities.ts +30 -0
  99. package/src/utils/skillParser.ts +3 -1
@@ -11,19 +11,55 @@
11
11
  */
12
12
 
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
+ import { createHash } from "node:crypto";
14
15
  import type { Span } from "@opentelemetry/api";
15
16
  import {
16
17
  getOTELApi,
17
18
  isInitialized,
18
19
  getCurrentConfig,
19
20
  } from "./instrumentation.js";
20
- import type { LLMRequestMetadata, ToolMetadata } from "../types/telemetry.js";
21
+ import { logOTelEvent } from "./events.js";
22
+ import type {
23
+ LLMRequestMetadata,
24
+ LLMRequestInput,
25
+ ToolMetadata,
26
+ } from "../types/telemetry.js";
27
+
28
+ // -- Constants --
29
+
30
+ /** Max content length for span attributes (60KB, aligning with Claude Code) */
31
+ const MAX_CONTENT_LENGTH = 60000;
21
32
 
22
33
  // -- AsyncLocalStorage for context propagation --
23
34
 
24
35
  const interactionContext = new AsyncLocalStorage<Span | undefined>();
25
36
  const toolContext = new AsyncLocalStorage<Span | undefined>();
26
37
 
38
+ // -- Incremental tracking state (module-level, per session) --
39
+
40
+ /** Number of messages already reported in new_context; only deltas are sent */
41
+ let lastReportedMessageCount = 0;
42
+ /** Set of system prompt hashes already emitted via logOTelEvent */
43
+ const seenSystemPromptHashes = new Set<string>();
44
+ /** Set of tool schema hashes already emitted via logOTelEvent */
45
+ const seenToolSchemaHashes = new Set<string>();
46
+
47
+ // -- Helpers --
48
+
49
+ /** Computes SHA-256 hash of content, returns first 12 hex characters */
50
+ function hashContent(content: string): string {
51
+ return createHash("sha256").update(content).digest("hex").substring(0, 12);
52
+ }
53
+
54
+ /** Truncates content to MAX_CONTENT_LENGTH, returns [truncated, originalLength] */
55
+ function truncateContent(content: string): [string, number] {
56
+ const originalLength = content.length;
57
+ if (originalLength > MAX_CONTENT_LENGTH) {
58
+ return [content.substring(0, MAX_CONTENT_LENGTH), originalLength];
59
+ }
60
+ return [content, originalLength];
61
+ }
62
+
27
63
  // -- Tracer accessor --
28
64
 
29
65
  function getTracer() {
@@ -71,18 +107,33 @@ export function endInteractionSpan(): void {
71
107
  interactionContext.enterWith(undefined);
72
108
  }
73
109
 
110
+ /**
111
+ * Resets incremental tracing state. Should be called after compaction to
112
+ * prevent incorrect delta calculations.
113
+ */
114
+ export function resetTracingState(): void {
115
+ lastReportedMessageCount = 0;
116
+ seenSystemPromptHashes.clear();
117
+ seenToolSchemaHashes.clear();
118
+ }
119
+
74
120
  /**
75
121
  * Creates an LLM request span as a child of the interaction span.
76
122
  * Does NOT enter any ALS — the span must be passed explicitly to endLLMRequestSpan.
123
+ *
124
+ * When logToolContent is enabled, captures:
125
+ * - system_prompt_hash / system_prompt_preview / system_prompt_length
126
+ * - new_context (incremental messages delta)
127
+ * - tools (JSON array of {name, hash}) / tools_count
77
128
  */
78
129
  export function startLLMRequestSpan(
79
130
  model: string,
80
- options?: { context?: string },
131
+ options?: LLMRequestInput,
81
132
  ): Span | undefined {
82
133
  const tracer = getTracer();
83
134
  if (!tracer) return undefined;
84
135
 
85
- const attributes: Record<string, string> = {
136
+ const attributes: Record<string, string | number | boolean> = {
86
137
  "span.type": "llm_request",
87
138
  model,
88
139
  };
@@ -90,6 +141,91 @@ export function startLLMRequestSpan(
90
141
  attributes["llm_request.context"] = options.context;
91
142
  }
92
143
 
144
+ const config = getCurrentConfig();
145
+
146
+ // Content capture gated by logToolContent
147
+ if (config?.logToolContent) {
148
+ // System prompt: hash + preview + length
149
+ if (options?.systemPrompt) {
150
+ const hash = hashContent(options.systemPrompt);
151
+ attributes.system_prompt_hash = hash;
152
+ attributes.system_prompt_length = options.systemPrompt.length;
153
+ const preview =
154
+ options.systemPrompt.length > 500
155
+ ? options.systemPrompt.substring(0, 500)
156
+ : options.systemPrompt;
157
+ attributes.system_prompt_preview = preview;
158
+
159
+ // Emit full system prompt via event only once per hash
160
+ if (!seenSystemPromptHashes.has(hash)) {
161
+ seenSystemPromptHashes.add(hash);
162
+ logOTelEvent("system_prompt", {
163
+ system_prompt_hash: hash,
164
+ system_prompt: options.systemPrompt,
165
+ }).catch(() => {});
166
+ }
167
+ }
168
+
169
+ // Incremental input messages: only send new messages since last report
170
+ if (options?.inputMessages && options.inputMessages.length > 0) {
171
+ const totalCount = options.inputMessages.length;
172
+ const deltaCount = totalCount - lastReportedMessageCount;
173
+ if (deltaCount > 0) {
174
+ const newMessages = options.inputMessages.slice(
175
+ lastReportedMessageCount,
176
+ );
177
+ const newContextStr = JSON.stringify(newMessages);
178
+ const [truncated, originalLength] = truncateContent(newContextStr);
179
+ attributes.new_context = truncated;
180
+ attributes.new_context_message_count = deltaCount;
181
+ if (originalLength > MAX_CONTENT_LENGTH) {
182
+ attributes.new_context_truncated = true;
183
+ attributes.new_context_original_length = originalLength;
184
+ }
185
+ }
186
+ lastReportedMessageCount = totalCount;
187
+ }
188
+
189
+ // Tool schemas: hash per tool + count
190
+ if (options?.toolsSchema) {
191
+ try {
192
+ const tools = JSON.parse(options.toolsSchema);
193
+ if (Array.isArray(tools)) {
194
+ const toolHashes = tools.map(
195
+ (tool: { function?: { name?: string } }) => {
196
+ const name = tool.function?.name || "unknown";
197
+ const toolStr = JSON.stringify(tool);
198
+ const hash = hashContent(toolStr);
199
+ return { name, hash };
200
+ },
201
+ );
202
+ attributes.tools = JSON.stringify(toolHashes);
203
+ attributes.tools_count = toolHashes.length;
204
+
205
+ // Emit full tool schema via event only once per hash
206
+ for (const { name, hash } of toolHashes) {
207
+ if (!seenToolSchemaHashes.has(hash)) {
208
+ seenToolSchemaHashes.add(hash);
209
+ const fullTool = tools.find(
210
+ (t: { function?: { name?: string } }) =>
211
+ t.function?.name === name,
212
+ );
213
+ if (fullTool) {
214
+ logOTelEvent("tool_schema", {
215
+ tool_name: name,
216
+ tool_hash: hash,
217
+ tool_schema: JSON.stringify(fullTool),
218
+ }).catch(() => {});
219
+ }
220
+ }
221
+ }
222
+ }
223
+ } catch {
224
+ // toolsSchema is not valid JSON — skip
225
+ }
226
+ }
227
+ }
228
+
93
229
  const parent = interactionContext.getStore();
94
230
  let span: Span;
95
231
  if (parent) {
@@ -105,6 +241,8 @@ export function startLLMRequestSpan(
105
241
  /**
106
242
  * Ends an LLM request span with response metadata.
107
243
  * The span is passed explicitly — no ALS is read or modified.
244
+ *
245
+ * When logToolContent is enabled, captures response.model_output.
108
246
  */
109
247
  export function endLLMRequestSpan(
110
248
  span: Span | undefined,
@@ -127,6 +265,20 @@ export function endLLMRequestSpan(
127
265
  if (metadata.hasToolCall != null)
128
266
  span.setAttribute("has_tool_call", metadata.hasToolCall);
129
267
 
268
+ // Content capture gated by logToolContent
269
+ const config = getCurrentConfig();
270
+ if (config?.logToolContent && metadata.modelOutput) {
271
+ const [truncated, originalLength] = truncateContent(metadata.modelOutput);
272
+ span.setAttribute("response.model_output", truncated);
273
+ if (originalLength > MAX_CONTENT_LENGTH) {
274
+ span.setAttribute("response.model_output_truncated", true);
275
+ span.setAttribute(
276
+ "response.model_output_original_length",
277
+ originalLength,
278
+ );
279
+ }
280
+ }
281
+
130
282
  span.end();
131
283
  }
132
284
 
@@ -142,16 +294,18 @@ export function startToolSpan(
142
294
  if (!tracer) return undefined;
143
295
 
144
296
  const config = getCurrentConfig();
145
- const attributes: Record<string, string | number> = {
297
+ const attributes: Record<string, string | number | boolean> = {
146
298
  "span.type": "tool",
147
299
  tool_name: toolName,
148
300
  };
149
301
  if (config?.logToolContent && input !== undefined) {
150
- let inputStr = typeof input === "string" ? input : JSON.stringify(input);
151
- if (inputStr.length > 1000) {
152
- inputStr = inputStr.substring(0, 1000);
302
+ const inputStr = typeof input === "string" ? input : JSON.stringify(input);
303
+ const [truncated, originalLength] = truncateContent(inputStr);
304
+ attributes.tool_input = truncated;
305
+ if (originalLength > MAX_CONTENT_LENGTH) {
306
+ attributes.tool_input_truncated = true;
307
+ attributes.tool_input_original_length = originalLength;
153
308
  }
154
- attributes.tool_input = inputStr;
155
309
  }
156
310
 
157
311
  const parent = interactionContext.getStore();
@@ -181,11 +335,12 @@ export function endToolSpan(metadata: ToolMetadata): void {
181
335
 
182
336
  const config = getCurrentConfig();
183
337
  if (config?.logToolContent && metadata.output) {
184
- let outputStr = metadata.output;
185
- if (outputStr.length > 1000) {
186
- outputStr = outputStr.substring(0, 1000);
338
+ const [truncated, originalLength] = truncateContent(metadata.output);
339
+ span.setAttribute("tool_output", truncated);
340
+ if (originalLength > MAX_CONTENT_LENGTH) {
341
+ span.setAttribute("tool_output_truncated", true);
342
+ span.setAttribute("tool_output_original_length", originalLength);
187
343
  }
188
- span.setAttribute("tool_output", outputStr);
189
344
  }
190
345
 
191
346
  span.end();
@@ -20,6 +20,25 @@ import {
20
20
 
21
21
  const BASH_DEFAULT_TIMEOUT_MS = 120000;
22
22
 
23
+ /**
24
+ * Wrap a user command so we can append CWD tracking (`&& pwd -P`) without the
25
+ * appended part being affected by trailing here-docs, unbalanced quotes, or
26
+ * multi-line syntax in the user command.
27
+ *
28
+ * The command is single-quoted (with embedded single quotes escaped via the
29
+ * classic `'"'"'` sequence) and run through `eval`. Because the entire user
30
+ * command becomes a single literal argument to `eval`, any here-doc inside it
31
+ * only opens/closes during eval's own second parse pass and cannot leak out to
32
+ * clobber the trailing `&& pwd -P`. Works on sh/dash/bash and Git Bash.
33
+ */
34
+ function wrapCommandForCwdTracking(
35
+ command: string,
36
+ cwdFileForBash: string,
37
+ ): string {
38
+ const escaped = command.replace(/'/g, `'"'"'`);
39
+ return `eval '${escaped}' && pwd -P >| ${cwdFileForBash}`;
40
+ }
41
+
23
42
  // Commands that should not be auto-backgrounded on timeout (e.g. sleep should just be killed)
24
43
  const DISALLOWED_AUTO_BACKGROUND_COMMANDS = ["sleep"];
25
44
 
@@ -248,7 +267,10 @@ The working directory persists between commands. Try to maintain your current wo
248
267
  `wave_cwd_${Date.now()}_${Math.random().toString(36).substring(2, 11)}.tmp`,
249
268
  );
250
269
  const tempCwdFileForBash = toPosixPath(tempCwdFile);
251
- const wrappedCommand = `${command} && pwd -P >| ${tempCwdFileForBash}`;
270
+ const wrappedCommand = wrapCommandForCwdTracking(
271
+ command,
272
+ tempCwdFileForBash,
273
+ );
252
274
 
253
275
  const child: ChildProcess = spawn(wrappedCommand, {
254
276
  shell: shellPath || true,
@@ -14,11 +14,19 @@ export interface GatewayConfig {
14
14
  fetch?: OpenAI["fetch"];
15
15
  }
16
16
 
17
+ export interface ModelCapabilities {
18
+ /** Whether the model supports image/vision input. Default: true. */
19
+ vision?: boolean;
20
+ /** Whether the model supports prompt caching (ephemeral cache_control markers). Default: false. */
21
+ promptCaching?: boolean;
22
+ }
23
+
17
24
  export interface ModelConfig {
18
25
  model?: string;
19
26
  fastModel?: string;
20
27
  maxTokens?: number;
21
28
  permissionMode?: PermissionMode;
22
29
  fastModelConfig?: Record<string, unknown>;
30
+ capabilities?: ModelCapabilities;
23
31
  [key: string]: unknown;
24
32
  }
@@ -188,6 +188,7 @@ export interface HookJsonInput {
188
188
  user_prompt?: string; // Present for UserPromptSubmit only
189
189
  subagent_type?: string; // Present when hook is executed by a subagent
190
190
  name?: string; // Present for WorktreeCreate events
191
+ worktree_path?: string; // Present for WorktreeRemove events
191
192
  old_cwd?: string; // Present for CwdChanged events
192
193
  new_cwd?: string; // Present for CwdChanged events
193
194
  source?: SessionStartSource; // Present for SessionStart events
@@ -89,7 +89,6 @@ export interface BangBlock {
89
89
  export interface CompactBlock {
90
90
  type: "compact";
91
91
  content: string;
92
- sessionId: string;
93
92
  }
94
93
 
95
94
  export interface ReasoningBlock {
@@ -74,6 +74,7 @@ export interface SkillToolArgs {
74
74
  export interface SkillManagerOptions {
75
75
  personalSkillsPath?: string;
76
76
  personalClaudeSkillsPath?: string;
77
+ personalAgentsSkillsPath?: string;
77
78
  scanTimeout?: number;
78
79
  workdir?: string;
79
80
  watch?: boolean;
@@ -58,6 +58,20 @@ export interface LLMRequestMetadata {
58
58
  error?: string;
59
59
  /** Response included tool calls */
60
60
  hasToolCall?: boolean;
61
+ /** Model text output (recorded when logToolContent is true) */
62
+ modelOutput?: string;
63
+ }
64
+
65
+ /** Input context for an LLM request span (content capture gated by logToolContent) */
66
+ export interface LLMRequestInput {
67
+ /** Scope context */
68
+ context?: "interaction" | "standalone";
69
+ /** System prompt string (when available) */
70
+ systemPrompt?: string;
71
+ /** Messages array sent to the API */
72
+ inputMessages?: unknown[];
73
+ /** Tool schemas JSON string */
74
+ toolsSchema?: string;
61
75
  }
62
76
 
63
77
  /** Metadata for tool execution spans */
@@ -79,4 +93,6 @@ export type OTelEventName =
79
93
  | "user_prompt"
80
94
  | "tool_decision"
81
95
  | "compaction"
82
- | "error";
96
+ | "error"
97
+ | "system_prompt"
98
+ | "tool_schema";
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Atomic file write utilities.
3
+ *
4
+ * Writes data to a temporary file first, then renames it to the target path.
5
+ * This prevents readers from observing a partially-written (truncated) file,
6
+ * which is critical for config files that may be read concurrently.
7
+ *
8
+ * Temp file naming: `${targetPath}.tmp.${process.pid}.${randomUUID()}`
9
+ * — avoids collisions when multiple processes write the same file.
10
+ */
11
+ import { writeFileSync, renameSync, unlinkSync, promises as fsp } from "fs";
12
+ import { randomUUID } from "crypto";
13
+
14
+ function tmpPathFor(targetPath: string): string {
15
+ return `${targetPath}.tmp.${process.pid}.${randomUUID()}`;
16
+ }
17
+
18
+ /**
19
+ * Atomically write `data` to `filePath` (async).
20
+ *
21
+ * Writes to a temp file then renames it into place. On any failure the temp
22
+ * file is removed and the error is re-thrown so callers can handle it.
23
+ */
24
+ export async function atomicWriteFile(
25
+ filePath: string,
26
+ data: string,
27
+ ): Promise<void> {
28
+ const tmpPath = tmpPathFor(filePath);
29
+ try {
30
+ await fsp.writeFile(tmpPath, data, "utf-8");
31
+ await fsp.rename(tmpPath, filePath);
32
+ } catch (error) {
33
+ try {
34
+ await fsp.unlink(tmpPath);
35
+ } catch {
36
+ // Ignore cleanup errors — the original error is more important
37
+ }
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Atomically write `data` to `filePath` (sync).
44
+ *
45
+ * Sync variant for callers that cannot await. Same temp-file-then-rename
46
+ * strategy as {@link atomicWriteFile}.
47
+ */
48
+ export function atomicWriteFileSync(filePath: string, data: string): void {
49
+ const tmpPath = tmpPathFor(filePath);
50
+ try {
51
+ writeFileSync(tmpPath, data, "utf-8");
52
+ renameSync(tmpPath, filePath);
53
+ } catch (error) {
54
+ try {
55
+ unlinkSync(tmpPath);
56
+ } catch {
57
+ // Ignore cleanup errors — the original error is more important
58
+ }
59
+ throw error;
60
+ }
61
+ }
@@ -13,6 +13,8 @@ import type {
13
13
  CompletionUsage,
14
14
  } from "openai/resources";
15
15
  import { logger } from "./globalLogger.js";
16
+ import { supportsPromptCaching } from "./modelCapabilities.js";
17
+ import type { ModelCapabilities } from "../types/config.js";
16
18
 
17
19
  // ============================================================================
18
20
  // Core Types
@@ -73,41 +75,6 @@ export interface ClaudeUsage extends CompletionUsage {
73
75
  // Utility Functions (Basic Structure - to be implemented)
74
76
  // ============================================================================
75
77
 
76
- /**
77
- * Determines if a model supports prompt caching
78
- * @param modelName - Model identifier
79
- * @returns True if model name matches the cache pattern (default: contains 'claude')
80
- */
81
- export function supportsPromptCaching(modelName: string): boolean {
82
- // Handle null, undefined, and non-string inputs
83
- if (!modelName || typeof modelName !== "string") {
84
- return false;
85
- }
86
-
87
- // Handle empty strings and whitespace-only strings
88
- const trimmed = modelName.trim();
89
- if (trimmed.length === 0) {
90
- return false;
91
- }
92
-
93
- const cachePattern = process.env.WAVE_PROMPT_CACHE_REGEX || "claude";
94
- try {
95
- const regex = new RegExp(cachePattern, "i");
96
- return regex.test(trimmed);
97
- } catch {
98
- // If regex is invalid, fall back to simple includes check with default
99
- return trimmed.toLowerCase().includes("claude");
100
- }
101
- }
102
-
103
- /**
104
- * Determines if a model supports cache control
105
- * @param modelName - Model identifier
106
- * @returns True if model name contains 'claude' (case-insensitive)
107
- * @deprecated Use supportsPromptCaching instead
108
- */
109
- export const isClaudeModel = supportsPromptCaching;
110
-
111
78
  /**
112
79
  * Validates cache control structure
113
80
  * @param control - Object to validate
@@ -254,12 +221,12 @@ export function countContentBlocks(
254
221
  * Tools are marked separately via addCacheControlToLastTool (called by aiService).
255
222
  *
256
223
  * @param messages - Original OpenAI message array
257
- * @param modelName - Model name for cache detection
224
+ * @param capabilities - Declarative model capabilities for cache detection
258
225
  * @returns Messages with cache control markers applied
259
226
  */
260
227
  export function transformMessagesForExplicitCache(
261
228
  messages: ChatCompletionMessageParam[],
262
- modelName: string,
229
+ capabilities?: ModelCapabilities,
263
230
  ): ChatCompletionMessageParam[] {
264
231
  // Validate inputs
265
232
  if (!messages || !Array.isArray(messages)) {
@@ -274,7 +241,7 @@ export function transformMessagesForExplicitCache(
274
241
  }
275
242
 
276
243
  // Only apply cache control for models that support prompt caching
277
- if (!supportsPromptCaching(modelName)) {
244
+ if (!supportsPromptCaching(capabilities)) {
278
245
  return messages;
279
246
  }
280
247
 
@@ -205,7 +205,11 @@ export function setupAgentContainer(
205
205
 
206
206
  const rootSessionId = messageManager.getRootSessionId();
207
207
 
208
- container.register("ReversionService", new ReversionService(rootSessionId));
208
+ const reversionService = new ReversionService(rootSessionId);
209
+ container.register("ReversionService", reversionService);
210
+ reversionService.cleanupOldSessions(30).catch((error) => {
211
+ logger.error("Failed to cleanup old file history:", error);
212
+ });
209
213
  const reversionManager = new ReversionManager(container);
210
214
  container.register("ReversionManager", reversionManager);
211
215