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.
- package/builtin/skills/settings/ENV.md +0 -1
- package/builtin/skills/settings/HOOKS.md +10 -0
- package/builtin/skills/settings/MODELS.md +37 -0
- package/builtin/skills/settings/SKILLS.md +6 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +31 -8
- package/dist/managers/cronManager.d.ts.map +1 -1
- package/dist/managers/cronManager.js +2 -0
- package/dist/managers/liveConfigManager.d.ts.map +1 -1
- package/dist/managers/liveConfigManager.js +0 -9
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +13 -3
- package/dist/managers/messageManager.d.ts +4 -6
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +27 -81
- package/dist/managers/skillManager.d.ts +1 -0
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +11 -2
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +2 -0
- package/dist/prompts/index.d.ts +3 -2
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +5 -1
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +13 -9
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +24 -9
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +6 -0
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +1 -1
- package/dist/services/interactionService.js +1 -1
- package/dist/services/reversionService.d.ts +5 -1
- package/dist/services/reversionService.d.ts.map +1 -1
- package/dist/services/reversionService.js +50 -22
- package/dist/services/session.d.ts +4 -13
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +59 -215
- package/dist/telemetry/sessionTracing.d.ts +31 -20
- package/dist/telemetry/sessionTracing.d.ts.map +1 -1
- package/dist/telemetry/sessionTracing.js +192 -62
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +16 -1
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +6 -7
- package/dist/types/config.d.ts +7 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +0 -1
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/skills.d.ts +1 -0
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +16 -1
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/utils/atomicWrite.d.ts +15 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +59 -0
- package/dist/utils/cacheControlUtils.d.ts +3 -15
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +4 -35
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +5 -1
- package/dist/utils/convertMessagesForAPI.d.ts +9 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +74 -55
- package/dist/utils/fileUtils.d.ts +0 -6
- package/dist/utils/fileUtils.d.ts.map +1 -1
- package/dist/utils/fileUtils.js +0 -43
- package/dist/utils/gitUtils.d.ts +11 -0
- package/dist/utils/gitUtils.d.ts.map +1 -1
- package/dist/utils/gitUtils.js +51 -0
- package/dist/utils/groupMessagesByApiRound.d.ts.map +1 -1
- package/dist/utils/groupMessagesByApiRound.js +7 -6
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +2 -1
- package/dist/utils/modelCapabilities.d.ts +22 -0
- package/dist/utils/modelCapabilities.d.ts.map +1 -0
- package/dist/utils/modelCapabilities.js +24 -0
- package/dist/utils/skillParser.d.ts.map +1 -1
- package/dist/utils/skillParser.js +3 -1
- package/dist/utils/tokenEstimate.d.ts +24 -0
- package/dist/utils/tokenEstimate.d.ts.map +1 -0
- package/dist/utils/tokenEstimate.js +30 -0
- package/dist/utils/worktreeUtils.d.ts.map +1 -1
- package/dist/utils/worktreeUtils.js +3 -1
- package/package.json +1 -1
- package/src/agent.ts +11 -5
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +36 -7
- package/src/managers/cronManager.ts +2 -0
- package/src/managers/liveConfigManager.ts +0 -12
- package/src/managers/mcpManager.ts +16 -3
- package/src/managers/messageManager.ts +32 -109
- package/src/managers/skillManager.ts +16 -2
- package/src/managers/toolManager.ts +2 -0
- package/src/prompts/index.ts +6 -1
- package/src/services/aiService.ts +12 -5
- package/src/services/configurationService.ts +26 -24
- package/src/services/hook.ts +7 -0
- package/src/services/initializationService.ts +1 -3
- package/src/services/interactionService.ts +1 -1
- package/src/services/reversionService.ts +50 -27
- package/src/services/session.ts +63 -262
- package/src/telemetry/sessionTracing.ts +225 -73
- package/src/tools/bashTool.ts +23 -1
- package/src/tools/readTool.ts +6 -12
- package/src/types/config.ts +8 -0
- package/src/types/hooks.ts +1 -0
- package/src/types/messaging.ts +0 -1
- package/src/types/skills.ts +1 -0
- package/src/types/telemetry.ts +19 -1
- package/src/utils/atomicWrite.ts +61 -0
- package/src/utils/cacheControlUtils.ts +5 -38
- package/src/utils/containerSetup.ts +5 -1
- package/src/utils/convertMessagesForAPI.ts +87 -57
- package/src/utils/fileUtils.ts +0 -46
- package/src/utils/gitUtils.ts +54 -0
- package/src/utils/groupMessagesByApiRound.ts +7 -6
- package/src/utils/mcpUtils.ts +2 -1
- package/src/utils/modelCapabilities.ts +30 -0
- package/src/utils/skillParser.ts +3 -1
- package/src/utils/tokenEstimate.ts +34 -0
- 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
|
|
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
|
|
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
|
|
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
|
-
// --
|
|
47
|
+
// -- Helpers --
|
|
22
48
|
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
101
|
+
* Ends the current interaction span and clears the context.
|
|
80
102
|
*/
|
|
81
103
|
export function endInteractionSpan(): void {
|
|
82
|
-
const span =
|
|
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
|
|
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?:
|
|
131
|
+
options?: LLMRequestInput,
|
|
93
132
|
): Span | undefined {
|
|
94
|
-
const
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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(
|
|
113
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
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
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/tools/bashTool.ts
CHANGED
|
@@ -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 =
|
|
270
|
+
const wrappedCommand = wrapCommandForCwdTracking(
|
|
271
|
+
command,
|
|
272
|
+
tempCwdFileForBash,
|
|
273
|
+
);
|
|
252
274
|
|
|
253
275
|
const child: ChildProcess = spawn(wrappedCommand, {
|
|
254
276
|
shell: shellPath || true,
|
package/src/tools/readTool.ts
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
}
|
package/src/types/hooks.ts
CHANGED
|
@@ -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
|
package/src/types/messaging.ts
CHANGED
package/src/types/skills.ts
CHANGED
package/src/types/telemetry.ts
CHANGED
|
@@ -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
|
+
}
|