wave-agent-sdk 0.18.6 → 0.18.7
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/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +12 -5
- 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 +6 -1
- package/dist/managers/messageManager.d.ts +1 -1
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +10 -4
- package/dist/managers/toolManager.d.ts.map +1 -1
- package/dist/managers/toolManager.js +2 -0
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +2 -0
- package/dist/telemetry/sessionTracing.d.ts +17 -16
- package/dist/telemetry/sessionTracing.d.ts.map +1 -1
- package/dist/telemetry/sessionTracing.js +62 -58
- package/dist/tools/readTool.d.ts.map +1 -1
- package/dist/tools/readTool.js +6 -7
- package/dist/types/telemetry.d.ts +2 -0
- package/dist/types/telemetry.d.ts.map +1 -1
- 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/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/managers/aiManager.ts +14 -5
- package/src/managers/cronManager.ts +2 -0
- package/src/managers/liveConfigManager.ts +0 -12
- package/src/managers/mcpManager.ts +8 -1
- package/src/managers/messageManager.ts +10 -5
- package/src/managers/toolManager.ts +2 -0
- package/src/services/configurationService.ts +2 -0
- package/src/telemetry/sessionTracing.ts +64 -67
- package/src/tools/readTool.ts +6 -12
- package/src/types/telemetry.ts +2 -0
- package/src/utils/fileUtils.ts +0 -46
- package/src/utils/gitUtils.ts +54 -0
- package/src/utils/groupMessagesByApiRound.ts +7 -6
- package/src/utils/tokenEstimate.ts +34 -0
- package/src/utils/worktreeUtils.ts +8 -1
|
@@ -2,7 +2,12 @@
|
|
|
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";
|
|
@@ -16,12 +21,8 @@ import type { LLMRequestMetadata, ToolMetadata } from "../types/telemetry.js";
|
|
|
16
21
|
|
|
17
22
|
// -- AsyncLocalStorage for context propagation --
|
|
18
23
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
// -- LIFO stacks for nested span tracking --
|
|
22
|
-
|
|
23
|
-
const llmSpans: Span[] = [];
|
|
24
|
-
const toolSpans: Span[] = [];
|
|
24
|
+
const interactionContext = new AsyncLocalStorage<Span | undefined>();
|
|
25
|
+
const toolContext = new AsyncLocalStorage<Span | undefined>();
|
|
25
26
|
|
|
26
27
|
// -- Tracer accessor --
|
|
27
28
|
|
|
@@ -32,37 +33,19 @@ function getTracer() {
|
|
|
32
33
|
return otelApi.trace.getTracer("wave");
|
|
33
34
|
}
|
|
34
35
|
|
|
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
36
|
// -- Public API --
|
|
58
37
|
|
|
59
38
|
/**
|
|
60
39
|
* Creates an interaction span for a user turn.
|
|
40
|
+
* The span is stored in interactionContext for the duration of the turn.
|
|
61
41
|
*/
|
|
62
42
|
export function startInteractionSpan(
|
|
63
43
|
userPrompt: string,
|
|
64
44
|
sequence: number,
|
|
65
45
|
): Span | undefined {
|
|
46
|
+
const tracer = getTracer();
|
|
47
|
+
if (!tracer) return undefined;
|
|
48
|
+
|
|
66
49
|
const config = getCurrentConfig();
|
|
67
50
|
const attributes: Record<string, string | number> = {
|
|
68
51
|
"span.type": "interaction",
|
|
@@ -72,25 +55,33 @@ export function startInteractionSpan(
|
|
|
72
55
|
if (config?.logUserPrompts) {
|
|
73
56
|
attributes.user_prompt = userPrompt;
|
|
74
57
|
}
|
|
75
|
-
|
|
58
|
+
|
|
59
|
+
const span = tracer.startSpan("interaction", { attributes });
|
|
60
|
+
interactionContext.enterWith(span);
|
|
61
|
+
return span;
|
|
76
62
|
}
|
|
77
63
|
|
|
78
64
|
/**
|
|
79
|
-
* Ends the current
|
|
65
|
+
* Ends the current interaction span and clears the context.
|
|
80
66
|
*/
|
|
81
67
|
export function endInteractionSpan(): void {
|
|
82
|
-
const span =
|
|
68
|
+
const span = interactionContext.getStore();
|
|
83
69
|
if (!span) return;
|
|
84
70
|
span.end();
|
|
71
|
+
interactionContext.enterWith(undefined);
|
|
85
72
|
}
|
|
86
73
|
|
|
87
74
|
/**
|
|
88
|
-
* Creates an LLM request span as a child of the
|
|
75
|
+
* Creates an LLM request span as a child of the interaction span.
|
|
76
|
+
* Does NOT enter any ALS — the span must be passed explicitly to endLLMRequestSpan.
|
|
89
77
|
*/
|
|
90
78
|
export function startLLMRequestSpan(
|
|
91
79
|
model: string,
|
|
92
80
|
options?: { context?: string },
|
|
93
81
|
): Span | undefined {
|
|
82
|
+
const tracer = getTracer();
|
|
83
|
+
if (!tracer) return undefined;
|
|
84
|
+
|
|
94
85
|
const attributes: Record<string, string> = {
|
|
95
86
|
"span.type": "llm_request",
|
|
96
87
|
model,
|
|
@@ -99,18 +90,26 @@ export function startLLMRequestSpan(
|
|
|
99
90
|
attributes["llm_request.context"] = options.context;
|
|
100
91
|
}
|
|
101
92
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
const parent = interactionContext.getStore();
|
|
94
|
+
let span: Span;
|
|
95
|
+
if (parent) {
|
|
96
|
+
const otelApi = getOTELApi()!;
|
|
97
|
+
const ctx = otelApi.trace.setSpan(otelApi.context.active(), parent);
|
|
98
|
+
span = tracer.startSpan("llm.request", { attributes }, ctx);
|
|
99
|
+
} else {
|
|
100
|
+
span = tracer.startSpan("llm.request", { attributes });
|
|
105
101
|
}
|
|
106
102
|
return span;
|
|
107
103
|
}
|
|
108
104
|
|
|
109
105
|
/**
|
|
110
|
-
* Ends
|
|
106
|
+
* Ends an LLM request span with response metadata.
|
|
107
|
+
* The span is passed explicitly — no ALS is read or modified.
|
|
111
108
|
*/
|
|
112
|
-
export function endLLMRequestSpan(
|
|
113
|
-
|
|
109
|
+
export function endLLMRequestSpan(
|
|
110
|
+
span: Span | undefined,
|
|
111
|
+
metadata: LLMRequestMetadata,
|
|
112
|
+
): void {
|
|
114
113
|
if (!span) return;
|
|
115
114
|
|
|
116
115
|
if (metadata.inputTokens != null)
|
|
@@ -129,19 +128,19 @@ export function endLLMRequestSpan(metadata: LLMRequestMetadata): void {
|
|
|
129
128
|
span.setAttribute("has_tool_call", metadata.hasToolCall);
|
|
130
129
|
|
|
131
130
|
span.end();
|
|
132
|
-
|
|
133
|
-
if (llmSpans.length > 0) {
|
|
134
|
-
spanContext.enterWith(llmSpans[llmSpans.length - 1]);
|
|
135
|
-
}
|
|
136
131
|
}
|
|
137
132
|
|
|
138
133
|
/**
|
|
139
|
-
* Creates a tool execution span as a child of the
|
|
134
|
+
* Creates a tool execution span as a child of the interaction span.
|
|
135
|
+
* Enters toolContext with the new span.
|
|
140
136
|
*/
|
|
141
137
|
export function startToolSpan(
|
|
142
138
|
toolName: string,
|
|
143
139
|
input?: unknown,
|
|
144
140
|
): Span | undefined {
|
|
141
|
+
const tracer = getTracer();
|
|
142
|
+
if (!tracer) return undefined;
|
|
143
|
+
|
|
145
144
|
const config = getCurrentConfig();
|
|
146
145
|
const attributes: Record<string, string | number> = {
|
|
147
146
|
"span.type": "tool",
|
|
@@ -155,42 +154,40 @@ export function startToolSpan(
|
|
|
155
154
|
attributes.tool_input = inputStr;
|
|
156
155
|
}
|
|
157
156
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
const parent = interactionContext.getStore();
|
|
158
|
+
let span: Span;
|
|
159
|
+
if (parent) {
|
|
160
|
+
const otelApi = getOTELApi()!;
|
|
161
|
+
const ctx = otelApi.trace.setSpan(otelApi.context.active(), parent);
|
|
162
|
+
span = tracer.startSpan(`tool.${toolName}`, { attributes }, ctx);
|
|
163
|
+
} else {
|
|
164
|
+
span = tracer.startSpan(`tool.${toolName}`, { attributes });
|
|
161
165
|
}
|
|
166
|
+
toolContext.enterWith(span);
|
|
162
167
|
return span;
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
/**
|
|
166
|
-
* Ends
|
|
171
|
+
* Ends the current tool span with execution metadata.
|
|
172
|
+
* Reads the span from toolContext, then clears it.
|
|
167
173
|
*/
|
|
168
174
|
export function endToolSpan(metadata: ToolMetadata): void {
|
|
169
|
-
const span =
|
|
175
|
+
const span = toolContext.getStore();
|
|
170
176
|
if (!span) return;
|
|
171
177
|
|
|
172
178
|
span.setAttribute("success", metadata.success);
|
|
173
179
|
if (metadata.error) span.setAttribute("error", metadata.error);
|
|
174
180
|
span.setAttribute("duration_ms", metadata.durationMs);
|
|
175
181
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
const config = getCurrentConfig();
|
|
183
|
+
if (config?.logToolContent && metadata.output) {
|
|
184
|
+
let outputStr = metadata.output;
|
|
185
|
+
if (outputStr.length > 1000) {
|
|
186
|
+
outputStr = outputStr.substring(0, 1000);
|
|
187
|
+
}
|
|
188
|
+
span.setAttribute("tool_output", outputStr);
|
|
180
189
|
}
|
|
181
|
-
}
|
|
182
190
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
*/
|
|
186
|
-
export function getActiveInteractionSpan(): Span | undefined {
|
|
187
|
-
return spanContext.getStore();
|
|
188
|
-
}
|
|
189
|
-
|
|
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);
|
|
191
|
+
span.end();
|
|
192
|
+
toolContext.enterWith(undefined);
|
|
196
193
|
}
|
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/telemetry.ts
CHANGED
package/src/utils/fileUtils.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import { createReadStream } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { execSync } from "node:child_process";
|
|
5
|
-
import { homedir } from "node:os";
|
|
6
4
|
import { glob } from "glob";
|
|
7
5
|
|
|
8
6
|
/**
|
|
@@ -156,50 +154,6 @@ export async function getLastLine(
|
|
|
156
154
|
}
|
|
157
155
|
}
|
|
158
156
|
|
|
159
|
-
/**
|
|
160
|
-
* Ensures that a pattern is present in the global git ignore file.
|
|
161
|
-
*
|
|
162
|
-
* @param {string} pattern - The pattern to add to global git ignore.
|
|
163
|
-
*/
|
|
164
|
-
export async function ensureGlobalGitIgnore(pattern: string): Promise<void> {
|
|
165
|
-
try {
|
|
166
|
-
let globalIgnorePath: string;
|
|
167
|
-
try {
|
|
168
|
-
globalIgnorePath = execSync("git config --get core.excludesfile", {
|
|
169
|
-
encoding: "utf8",
|
|
170
|
-
}).trim();
|
|
171
|
-
} catch {
|
|
172
|
-
// If not set, use default paths
|
|
173
|
-
const xdgConfigHome =
|
|
174
|
-
process.env.XDG_CONFIG_HOME || path.join(homedir(), ".config");
|
|
175
|
-
globalIgnorePath = path.join(xdgConfigHome, "git", "ignore");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (!globalIgnorePath) return;
|
|
179
|
-
|
|
180
|
-
// Ensure directory exists
|
|
181
|
-
await fs.mkdir(path.dirname(globalIgnorePath), { recursive: true });
|
|
182
|
-
|
|
183
|
-
let content = "";
|
|
184
|
-
try {
|
|
185
|
-
content = await fs.readFile(globalIgnorePath, "utf8");
|
|
186
|
-
} catch {
|
|
187
|
-
// File doesn't exist
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const lines = content.split("\n").map((line) => line.trim());
|
|
191
|
-
if (!lines.includes(pattern)) {
|
|
192
|
-
const newContent =
|
|
193
|
-
content.endsWith("\n") || content === ""
|
|
194
|
-
? `${content}${pattern}\n`
|
|
195
|
-
: `${content}\n${pattern}\n`;
|
|
196
|
-
await fs.writeFile(globalIgnorePath, newContent, "utf8");
|
|
197
|
-
}
|
|
198
|
-
} catch {
|
|
199
|
-
// Ignore errors
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
157
|
/**
|
|
204
158
|
* Simple Levenshtein distance implementation
|
|
205
159
|
*/
|
package/src/utils/gitUtils.ts
CHANGED
|
@@ -222,6 +222,60 @@ export function getDefaultRemoteBranch(cwd: string): string {
|
|
|
222
222
|
return "main";
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Module-level set tracking git dirs already processed in this process,
|
|
227
|
+
* to avoid redundant I/O.
|
|
228
|
+
*/
|
|
229
|
+
const excludedGitDirs = new Set<string>();
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Ensure that Wave runtime files are excluded from git status by writing
|
|
233
|
+
* patterns to `.git/info/exclude` (per-repo, not global).
|
|
234
|
+
*
|
|
235
|
+
* Idempotent: if the marker `# wave-runtime` is already present in the
|
|
236
|
+
* exclude file, it skips writing. A module-level Set provides additional
|
|
237
|
+
* per-process dedup to avoid redundant file reads.
|
|
238
|
+
*
|
|
239
|
+
* @param cwd Working directory to start searching from
|
|
240
|
+
*/
|
|
241
|
+
export function ensureWaveRuntimeFilesExcluded(cwd: string): void {
|
|
242
|
+
try {
|
|
243
|
+
const gitDir = resolveGitDir(cwd);
|
|
244
|
+
if (!gitDir) return;
|
|
245
|
+
|
|
246
|
+
if (excludedGitDirs.has(gitDir)) return;
|
|
247
|
+
|
|
248
|
+
const excludePath = path.join(gitDir, "info", "exclude");
|
|
249
|
+
let content = "";
|
|
250
|
+
try {
|
|
251
|
+
content = fsSync.readFileSync(excludePath, "utf8");
|
|
252
|
+
} catch {
|
|
253
|
+
// File doesn't exist yet
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const marker = "# wave-runtime";
|
|
257
|
+
if (content.includes(marker)) {
|
|
258
|
+
excludedGitDirs.add(gitDir);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const block = [
|
|
263
|
+
marker,
|
|
264
|
+
"**/.wave/scheduled_tasks.lock",
|
|
265
|
+
"**/.wave/scheduled_tasks.json",
|
|
266
|
+
"**/.wave/worktrees/",
|
|
267
|
+
"**/.wave/settings.local.json",
|
|
268
|
+
"",
|
|
269
|
+
].join("\n");
|
|
270
|
+
|
|
271
|
+
fsSync.mkdirSync(path.join(gitDir, "info"), { recursive: true });
|
|
272
|
+
fsSync.appendFileSync(excludePath, block);
|
|
273
|
+
excludedGitDirs.add(gitDir);
|
|
274
|
+
} catch {
|
|
275
|
+
// Best-effort: ignore all errors
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
225
279
|
/**
|
|
226
280
|
* Check if there are uncommitted changes in the working directory
|
|
227
281
|
* @param cwd Working directory
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Message } from "../types/index.js";
|
|
2
|
+
import { estimateTokens as estimateStrTokens } from "./tokenEstimate.js";
|
|
2
3
|
|
|
3
4
|
export interface ApiRound {
|
|
4
5
|
messages: Message[];
|
|
@@ -95,26 +96,26 @@ export function getLastApiRounds(
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
/**
|
|
98
|
-
*
|
|
99
|
+
* Estimate token count from message blocks using CJK-aware estimation.
|
|
99
100
|
*/
|
|
100
101
|
function estimateTokens(messages: Message[]): number {
|
|
101
|
-
let
|
|
102
|
+
let combined = "";
|
|
102
103
|
for (const msg of messages) {
|
|
103
104
|
for (const block of msg.blocks) {
|
|
104
105
|
if ("content" in block && typeof block.content === "string") {
|
|
105
|
-
|
|
106
|
+
combined += block.content;
|
|
106
107
|
}
|
|
107
108
|
if (
|
|
108
109
|
block.type === "tool" &&
|
|
109
110
|
block.parameters &&
|
|
110
111
|
typeof block.parameters === "string"
|
|
111
112
|
) {
|
|
112
|
-
|
|
113
|
+
combined += block.parameters;
|
|
113
114
|
}
|
|
114
115
|
if (block.type === "tool" && block.result) {
|
|
115
|
-
|
|
116
|
+
combined += block.result;
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
|
-
return
|
|
120
|
+
return estimateStrTokens(combined);
|
|
120
121
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CJK-aware token estimation without external dependencies.
|
|
3
|
+
*
|
|
4
|
+
* The naive `length / 4` heuristic under-estimates CJK text by ~4x because
|
|
5
|
+
* each Chinese/Japanese/Korean character is typically 1-2 tokens, not 0.25.
|
|
6
|
+
* This function separates CJK characters from other text and applies
|
|
7
|
+
* different ratios:
|
|
8
|
+
*
|
|
9
|
+
* - CJK characters: 1 char ≈ 1 token
|
|
10
|
+
* - Other characters: 4 chars ≈ 1 token (2 for JSON/JSONL/JSONC)
|
|
11
|
+
*
|
|
12
|
+
* The estimate intentionally leans high for CJK (safe direction for limit
|
|
13
|
+
* checks — better to reject than to let oversized content through).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// CJK Unified Ideographs + Extension A + Hiragana + Katakana + Hangul Syllables
|
|
17
|
+
const CJK_REGEX =
|
|
18
|
+
/[\u4e00-\u9fff\u3400-\u4dbf\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Estimate token count for a string, with CJK-awareness.
|
|
22
|
+
*
|
|
23
|
+
* @param content - The text content to estimate
|
|
24
|
+
* @param ext - File extension (without dot), e.g. "ts", "json". JSON/JSONL/JSONC
|
|
25
|
+
* files use a tighter ratio (2 chars/token) for non-CJK text.
|
|
26
|
+
* @returns Estimated token count
|
|
27
|
+
*/
|
|
28
|
+
export function estimateTokens(content: string, ext?: string): number {
|
|
29
|
+
const cjkCount = (content.match(CJK_REGEX) || []).length;
|
|
30
|
+
const otherCount = content.length - cjkCount;
|
|
31
|
+
const bytesPerToken =
|
|
32
|
+
ext === "json" || ext === "jsonl" || ext === "jsonc" ? 2 : 4;
|
|
33
|
+
return Math.ceil(cjkCount + otherCount / bytesPerToken);
|
|
34
|
+
}
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getGitMainRepoRoot,
|
|
11
|
+
getDefaultRemoteBranch,
|
|
12
|
+
ensureWaveRuntimeFilesExcluded,
|
|
13
|
+
} from "./gitUtils.js";
|
|
10
14
|
import { logger } from "./globalLogger.js";
|
|
11
15
|
|
|
12
16
|
export interface WorktreeInfo {
|
|
@@ -105,6 +109,9 @@ export function createWorktree(name: string, cwd: string): WorktreeInfo {
|
|
|
105
109
|
const branchName = `worktree-${name}`;
|
|
106
110
|
const baseBranch = getDefaultRemoteBranch(cwd);
|
|
107
111
|
|
|
112
|
+
// Ensure Wave runtime files are git-excluded in this repo
|
|
113
|
+
ensureWaveRuntimeFilesExcluded(cwd);
|
|
114
|
+
|
|
108
115
|
// Ensure parent directory exists
|
|
109
116
|
const parentDir = path.dirname(worktreePath);
|
|
110
117
|
if (!fs.existsSync(parentDir)) {
|