wave-agent-sdk 0.18.6 → 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 (129) 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 +31 -8
  12. package/dist/managers/cronManager.d.ts.map +1 -1
  13. package/dist/managers/cronManager.js +2 -0
  14. package/dist/managers/liveConfigManager.d.ts.map +1 -1
  15. package/dist/managers/liveConfigManager.js +0 -9
  16. package/dist/managers/mcpManager.d.ts.map +1 -1
  17. package/dist/managers/mcpManager.js +13 -3
  18. package/dist/managers/messageManager.d.ts +4 -6
  19. package/dist/managers/messageManager.d.ts.map +1 -1
  20. package/dist/managers/messageManager.js +27 -81
  21. package/dist/managers/skillManager.d.ts +1 -0
  22. package/dist/managers/skillManager.d.ts.map +1 -1
  23. package/dist/managers/skillManager.js +11 -2
  24. package/dist/managers/toolManager.d.ts.map +1 -1
  25. package/dist/managers/toolManager.js +2 -0
  26. package/dist/prompts/index.d.ts +3 -2
  27. package/dist/prompts/index.d.ts.map +1 -1
  28. package/dist/prompts/index.js +5 -1
  29. package/dist/services/aiService.d.ts.map +1 -1
  30. package/dist/services/aiService.js +13 -9
  31. package/dist/services/configurationService.d.ts.map +1 -1
  32. package/dist/services/configurationService.js +24 -9
  33. package/dist/services/hook.d.ts.map +1 -1
  34. package/dist/services/hook.js +6 -0
  35. package/dist/services/initializationService.d.ts.map +1 -1
  36. package/dist/services/initializationService.js +1 -1
  37. package/dist/services/interactionService.js +1 -1
  38. package/dist/services/reversionService.d.ts +5 -1
  39. package/dist/services/reversionService.d.ts.map +1 -1
  40. package/dist/services/reversionService.js +50 -22
  41. package/dist/services/session.d.ts +4 -13
  42. package/dist/services/session.d.ts.map +1 -1
  43. package/dist/services/session.js +59 -215
  44. package/dist/telemetry/sessionTracing.d.ts +31 -20
  45. package/dist/telemetry/sessionTracing.d.ts.map +1 -1
  46. package/dist/telemetry/sessionTracing.js +192 -62
  47. package/dist/tools/bashTool.d.ts.map +1 -1
  48. package/dist/tools/bashTool.js +16 -1
  49. package/dist/tools/readTool.d.ts.map +1 -1
  50. package/dist/tools/readTool.js +6 -7
  51. package/dist/types/config.d.ts +7 -0
  52. package/dist/types/config.d.ts.map +1 -1
  53. package/dist/types/hooks.d.ts +1 -0
  54. package/dist/types/hooks.d.ts.map +1 -1
  55. package/dist/types/messaging.d.ts +0 -1
  56. package/dist/types/messaging.d.ts.map +1 -1
  57. package/dist/types/skills.d.ts +1 -0
  58. package/dist/types/skills.d.ts.map +1 -1
  59. package/dist/types/telemetry.d.ts +16 -1
  60. package/dist/types/telemetry.d.ts.map +1 -1
  61. package/dist/utils/atomicWrite.d.ts +15 -0
  62. package/dist/utils/atomicWrite.d.ts.map +1 -0
  63. package/dist/utils/atomicWrite.js +59 -0
  64. package/dist/utils/cacheControlUtils.d.ts +3 -15
  65. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  66. package/dist/utils/cacheControlUtils.js +4 -35
  67. package/dist/utils/containerSetup.d.ts.map +1 -1
  68. package/dist/utils/containerSetup.js +5 -1
  69. package/dist/utils/convertMessagesForAPI.d.ts +9 -1
  70. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  71. package/dist/utils/convertMessagesForAPI.js +74 -55
  72. package/dist/utils/fileUtils.d.ts +0 -6
  73. package/dist/utils/fileUtils.d.ts.map +1 -1
  74. package/dist/utils/fileUtils.js +0 -43
  75. package/dist/utils/gitUtils.d.ts +11 -0
  76. package/dist/utils/gitUtils.d.ts.map +1 -1
  77. package/dist/utils/gitUtils.js +51 -0
  78. package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -1
  79. package/dist/utils/groupMessagesByApiRound.js +7 -6
  80. package/dist/utils/mcpUtils.d.ts.map +1 -1
  81. package/dist/utils/mcpUtils.js +2 -1
  82. package/dist/utils/modelCapabilities.d.ts +22 -0
  83. package/dist/utils/modelCapabilities.d.ts.map +1 -0
  84. package/dist/utils/modelCapabilities.js +24 -0
  85. package/dist/utils/skillParser.d.ts.map +1 -1
  86. package/dist/utils/skillParser.js +3 -1
  87. package/dist/utils/tokenEstimate.d.ts +24 -0
  88. package/dist/utils/tokenEstimate.d.ts.map +1 -0
  89. package/dist/utils/tokenEstimate.js +30 -0
  90. package/dist/utils/worktreeUtils.d.ts.map +1 -1
  91. package/dist/utils/worktreeUtils.js +3 -1
  92. package/package.json +1 -1
  93. package/src/agent.ts +11 -5
  94. package/src/index.ts +1 -0
  95. package/src/managers/aiManager.ts +36 -7
  96. package/src/managers/cronManager.ts +2 -0
  97. package/src/managers/liveConfigManager.ts +0 -12
  98. package/src/managers/mcpManager.ts +16 -3
  99. package/src/managers/messageManager.ts +32 -109
  100. package/src/managers/skillManager.ts +16 -2
  101. package/src/managers/toolManager.ts +2 -0
  102. package/src/prompts/index.ts +6 -1
  103. package/src/services/aiService.ts +12 -5
  104. package/src/services/configurationService.ts +26 -24
  105. package/src/services/hook.ts +7 -0
  106. package/src/services/initializationService.ts +1 -3
  107. package/src/services/interactionService.ts +1 -1
  108. package/src/services/reversionService.ts +50 -27
  109. package/src/services/session.ts +63 -262
  110. package/src/telemetry/sessionTracing.ts +225 -73
  111. package/src/tools/bashTool.ts +23 -1
  112. package/src/tools/readTool.ts +6 -12
  113. package/src/types/config.ts +8 -0
  114. package/src/types/hooks.ts +1 -0
  115. package/src/types/messaging.ts +0 -1
  116. package/src/types/skills.ts +1 -0
  117. package/src/types/telemetry.ts +19 -1
  118. package/src/utils/atomicWrite.ts +61 -0
  119. package/src/utils/cacheControlUtils.ts +5 -38
  120. package/src/utils/containerSetup.ts +5 -1
  121. package/src/utils/convertMessagesForAPI.ts +87 -57
  122. package/src/utils/fileUtils.ts +0 -46
  123. package/src/utils/gitUtils.ts +54 -0
  124. package/src/utils/groupMessagesByApiRound.ts +7 -6
  125. package/src/utils/mcpUtils.ts +2 -1
  126. package/src/utils/modelCapabilities.ts +30 -0
  127. package/src/utils/skillParser.ts +3 -1
  128. package/src/utils/tokenEstimate.ts +34 -0
  129. package/src/utils/worktreeUtils.ts +8 -1
@@ -2,26 +2,63 @@
2
2
  * Session Tracing -- OpenTelemetry Span Management
3
3
  *
4
4
  * Provides span creation/ending APIs for interactions, LLM requests, and tool
5
- * executions with AsyncLocalStorage context propagation and stale span cleanup.
5
+ * executions. Uses two independent AsyncLocalStorage contexts:
6
+ * - interactionContext: holds the interaction span for the entire turn
7
+ * - toolContext: holds the current tool span (cleared on end)
8
+ *
9
+ * LLM request spans do not enter any ALS; they are passed explicitly to
10
+ * endLLMRequestSpan, aligning with Claude Code's approach.
6
11
  */
7
12
 
8
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
+ import { createHash } from "node:crypto";
9
15
  import type { Span } from "@opentelemetry/api";
10
16
  import {
11
17
  getOTELApi,
12
18
  isInitialized,
13
19
  getCurrentConfig,
14
20
  } from "./instrumentation.js";
15
- 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;
16
32
 
17
33
  // -- AsyncLocalStorage for context propagation --
18
34
 
19
- const spanContext = new AsyncLocalStorage<Span>();
35
+ const interactionContext = new AsyncLocalStorage<Span | undefined>();
36
+ const toolContext = new AsyncLocalStorage<Span | undefined>();
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>();
20
46
 
21
- // -- LIFO stacks for nested span tracking --
47
+ // -- Helpers --
22
48
 
23
- const llmSpans: Span[] = [];
24
- const toolSpans: Span[] = [];
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
+ }
25
62
 
26
63
  // -- Tracer accessor --
27
64
 
@@ -32,37 +69,19 @@ function getTracer() {
32
69
  return otelApi.trace.getTracer("wave");
33
70
  }
34
71
 
35
- // -- Helper: create child span with parent context --
36
-
37
- function startChildSpan(
38
- name: string,
39
- attributes: Record<string, string | number>,
40
- ): Span | undefined {
41
- const tracer = getTracer();
42
- if (!tracer) return undefined;
43
-
44
- const parent = spanContext.getStore();
45
- let span: Span;
46
- if (parent) {
47
- const otelApi = getOTELApi()!;
48
- const ctx = otelApi.trace.setSpan(otelApi.context.active(), parent);
49
- span = tracer.startSpan(name, { attributes }, ctx);
50
- } else {
51
- span = tracer.startSpan(name, { attributes });
52
- }
53
- spanContext.enterWith(span);
54
- return span;
55
- }
56
-
57
72
  // -- Public API --
58
73
 
59
74
  /**
60
75
  * Creates an interaction span for a user turn.
76
+ * The span is stored in interactionContext for the duration of the turn.
61
77
  */
62
78
  export function startInteractionSpan(
63
79
  userPrompt: string,
64
80
  sequence: number,
65
81
  ): Span | undefined {
82
+ const tracer = getTracer();
83
+ if (!tracer) return undefined;
84
+
66
85
  const config = getCurrentConfig();
67
86
  const attributes: Record<string, string | number> = {
68
87
  "span.type": "interaction",
@@ -72,26 +91,49 @@ export function startInteractionSpan(
72
91
  if (config?.logUserPrompts) {
73
92
  attributes.user_prompt = userPrompt;
74
93
  }
75
- return startChildSpan("interaction", attributes);
94
+
95
+ const span = tracer.startSpan("interaction", { attributes });
96
+ interactionContext.enterWith(span);
97
+ return span;
76
98
  }
77
99
 
78
100
  /**
79
- * Ends the current active interaction span.
101
+ * Ends the current interaction span and clears the context.
80
102
  */
81
103
  export function endInteractionSpan(): void {
82
- const span = spanContext.getStore();
104
+ const span = interactionContext.getStore();
83
105
  if (!span) return;
84
106
  span.end();
107
+ interactionContext.enterWith(undefined);
108
+ }
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();
85
118
  }
86
119
 
87
120
  /**
88
- * Creates an LLM request span as a child of the current active span.
121
+ * Creates an LLM request span as a child of the interaction span.
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
89
128
  */
90
129
  export function startLLMRequestSpan(
91
130
  model: string,
92
- options?: { context?: string },
131
+ options?: LLMRequestInput,
93
132
  ): Span | undefined {
94
- const attributes: Record<string, string> = {
133
+ const tracer = getTracer();
134
+ if (!tracer) return undefined;
135
+
136
+ const attributes: Record<string, string | number | boolean> = {
95
137
  "span.type": "llm_request",
96
138
  model,
97
139
  };
@@ -99,18 +141,113 @@ export function startLLMRequestSpan(
99
141
  attributes["llm_request.context"] = options.context;
100
142
  }
101
143
 
102
- const span = startChildSpan("llm.request", attributes);
103
- if (span) {
104
- llmSpans.push(span);
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
+
229
+ const parent = interactionContext.getStore();
230
+ let span: Span;
231
+ if (parent) {
232
+ const otelApi = getOTELApi()!;
233
+ const ctx = otelApi.trace.setSpan(otelApi.context.active(), parent);
234
+ span = tracer.startSpan("llm.request", { attributes }, ctx);
235
+ } else {
236
+ span = tracer.startSpan("llm.request", { attributes });
105
237
  }
106
238
  return span;
107
239
  }
108
240
 
109
241
  /**
110
- * Ends the most recent LLM request span with response metadata.
242
+ * Ends an LLM request span with response metadata.
243
+ * The span is passed explicitly — no ALS is read or modified.
244
+ *
245
+ * When logToolContent is enabled, captures response.model_output.
111
246
  */
112
- export function endLLMRequestSpan(metadata: LLMRequestMetadata): void {
113
- const span = llmSpans.pop();
247
+ export function endLLMRequestSpan(
248
+ span: Span | undefined,
249
+ metadata: LLMRequestMetadata,
250
+ ): void {
114
251
  if (!span) return;
115
252
 
116
253
  if (metadata.inputTokens != null)
@@ -128,69 +265,84 @@ export function endLLMRequestSpan(metadata: LLMRequestMetadata): void {
128
265
  if (metadata.hasToolCall != null)
129
266
  span.setAttribute("has_tool_call", metadata.hasToolCall);
130
267
 
131
- span.end();
132
-
133
- if (llmSpans.length > 0) {
134
- spanContext.enterWith(llmSpans[llmSpans.length - 1]);
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
+ }
135
280
  }
281
+
282
+ span.end();
136
283
  }
137
284
 
138
285
  /**
139
- * Creates a tool execution span as a child of the current active span.
286
+ * Creates a tool execution span as a child of the interaction span.
287
+ * Enters toolContext with the new span.
140
288
  */
141
289
  export function startToolSpan(
142
290
  toolName: string,
143
291
  input?: unknown,
144
292
  ): Span | undefined {
293
+ const tracer = getTracer();
294
+ if (!tracer) return undefined;
295
+
145
296
  const config = getCurrentConfig();
146
- const attributes: Record<string, string | number> = {
297
+ const attributes: Record<string, string | number | boolean> = {
147
298
  "span.type": "tool",
148
299
  tool_name: toolName,
149
300
  };
150
301
  if (config?.logToolContent && input !== undefined) {
151
- let inputStr = typeof input === "string" ? input : JSON.stringify(input);
152
- if (inputStr.length > 1000) {
153
- 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;
154
308
  }
155
- attributes.tool_input = inputStr;
156
309
  }
157
310
 
158
- const span = startChildSpan(`tool.${toolName}`, attributes);
159
- if (span) {
160
- toolSpans.push(span);
311
+ const parent = interactionContext.getStore();
312
+ let span: Span;
313
+ if (parent) {
314
+ const otelApi = getOTELApi()!;
315
+ const ctx = otelApi.trace.setSpan(otelApi.context.active(), parent);
316
+ span = tracer.startSpan(`tool.${toolName}`, { attributes }, ctx);
317
+ } else {
318
+ span = tracer.startSpan(`tool.${toolName}`, { attributes });
161
319
  }
320
+ toolContext.enterWith(span);
162
321
  return span;
163
322
  }
164
323
 
165
324
  /**
166
- * Ends a tool span with execution metadata.
325
+ * Ends the current tool span with execution metadata.
326
+ * Reads the span from toolContext, then clears it.
167
327
  */
168
328
  export function endToolSpan(metadata: ToolMetadata): void {
169
- const span = toolSpans.pop();
329
+ const span = toolContext.getStore();
170
330
  if (!span) return;
171
331
 
172
332
  span.setAttribute("success", metadata.success);
173
333
  if (metadata.error) span.setAttribute("error", metadata.error);
174
334
  span.setAttribute("duration_ms", metadata.durationMs);
175
335
 
176
- span.end();
177
-
178
- if (toolSpans.length > 0) {
179
- spanContext.enterWith(toolSpans[toolSpans.length - 1]);
336
+ const config = getCurrentConfig();
337
+ if (config?.logToolContent && metadata.output) {
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);
343
+ }
180
344
  }
181
- }
182
-
183
- /**
184
- * Returns the current active span from ALS context.
185
- */
186
- export function getActiveInteractionSpan(): Span | undefined {
187
- return spanContext.getStore();
188
- }
189
345
 
190
- /**
191
- * Executes `fn` with `span` as the active context via ALS.
192
- * Useful for parallel tool calls that each need their own span context.
193
- */
194
- export function withSpanContext<T>(span: Span, fn: () => T): T {
195
- return spanContext.run(span, fn);
346
+ span.end();
347
+ toolContext.enterWith(undefined);
196
348
  }
@@ -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,
@@ -5,6 +5,7 @@ import { logger } from "../utils/globalLogger.js";
5
5
  import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
6
6
  import { resolvePath, getDisplayPath } from "../utils/path.js";
7
7
  import { formatLineNumberPrefix } from "../utils/stringUtils.js";
8
+ import { estimateTokens } from "../utils/tokenEstimate.js";
8
9
  import {
9
10
  isBinaryDocument,
10
11
  getBinaryDocumentError,
@@ -267,14 +268,11 @@ Usage:
267
268
  }
268
269
  }
269
270
 
270
- // Resource Limits
271
+ // Resource Limits — align with Claude Code: 256KB default, bypassed only
272
+ // when the caller provides an explicit line limit (not offset alone).
271
273
  const maxSizeBytes =
272
- context.fileReadingLimits?.maxSizeBytes ?? 1024 * 1024; // Default 1MB
273
- if (
274
- stats.size > maxSizeBytes &&
275
- typeof offset !== "number" &&
276
- typeof limit !== "number"
277
- ) {
274
+ context.fileReadingLimits?.maxSizeBytes ?? 0.25 * 1024 * 1024; // Default 256KB
275
+ if (stats.size > maxSizeBytes && typeof limit !== "number") {
278
276
  return {
279
277
  success: false,
280
278
  content: "",
@@ -357,11 +355,7 @@ Usage:
357
355
  // Token-level validation: estimate tokens and reject if over limit
358
356
  const maxTokens = context.fileReadingLimits?.maxTokens ?? 25000; // Default 25000 tokens
359
357
  const ext = extname(actualFilePath).toLowerCase().slice(1);
360
- const bytesPerToken =
361
- ext === "json" || ext === "jsonl" || ext === "jsonc" ? 2 : 4;
362
- const estimatedTokens = Math.ceil(
363
- formattedContent.length / bytesPerToken,
364
- );
358
+ const estimatedTokens = estimateTokens(formattedContent, ext);
365
359
  if (estimatedTokens > maxTokens) {
366
360
  return {
367
361
  success: false,
@@ -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 */
@@ -68,6 +82,8 @@ export interface ToolMetadata {
68
82
  error?: string;
69
83
  /** Execution time (ms) */
70
84
  durationMs: number;
85
+ /** Tool output content (recorded when logToolContent is true) */
86
+ output?: string;
71
87
  }
72
88
 
73
89
  /** Event names for OTel structured logging */
@@ -77,4 +93,6 @@ export type OTelEventName =
77
93
  | "user_prompt"
78
94
  | "tool_decision"
79
95
  | "compaction"
80
- | "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
+ }