wave-agent-sdk 0.18.5 → 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 +92 -87
- 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/openaiClient.d.ts.map +1 -1
- package/dist/utils/openaiClient.js +2 -0
- 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/scripts/install_ripgrep.js +1 -21
- package/src/managers/aiManager.ts +111 -102
- 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/openaiClient.ts +2 -0
- package/src/utils/tokenEstimate.ts +34 -0
- package/src/utils/worktreeUtils.ts +8 -1
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
|
@@ -3,6 +3,7 @@ import * as aiService from "../services/aiService.js";
|
|
|
3
3
|
import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
|
|
4
4
|
import { parseTaskNotificationXml } from "../utils/notificationXml.js";
|
|
5
5
|
import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
|
|
6
|
+
import { estimateTokens } from "../utils/tokenEstimate.js";
|
|
6
7
|
import {
|
|
7
8
|
getTaskReminderTurnCounts,
|
|
8
9
|
maybeInjectTaskReminder,
|
|
@@ -606,7 +607,7 @@ export class AIManager {
|
|
|
606
607
|
);
|
|
607
608
|
let usedTokens = 0;
|
|
608
609
|
for (const file of recentFiles) {
|
|
609
|
-
const fileTokens =
|
|
610
|
+
const fileTokens = estimateTokens(file.content);
|
|
610
611
|
if (usedTokens + fileTokens > POST_COMPACT_MAX_TOKENS_PER_FILE) continue;
|
|
611
612
|
if (fileTokens > 0) usedTokens += fileTokens;
|
|
612
613
|
contextParts.push(`\n\n## ${file.path}\n\`\`\`\n${file.content}\n\`\`\``);
|
|
@@ -639,7 +640,7 @@ export class AIManager {
|
|
|
639
640
|
skillContent.slice(0, maxSkillChars) + "\n\n...[truncated]...";
|
|
640
641
|
}
|
|
641
642
|
|
|
642
|
-
const skillTokens =
|
|
643
|
+
const skillTokens = estimateTokens(skillContent);
|
|
643
644
|
if (skillsUsedTokens + skillTokens > POST_COMPACT_SKILLS_TOKEN_BUDGET)
|
|
644
645
|
break;
|
|
645
646
|
skillsUsedTokens += skillTokens;
|
|
@@ -813,6 +814,7 @@ export class AIManager {
|
|
|
813
814
|
let turnDepth = turnOffset;
|
|
814
815
|
|
|
815
816
|
inner: while (true) {
|
|
817
|
+
let llmSpan: import("@opentelemetry/api").Span | undefined;
|
|
816
818
|
try {
|
|
817
819
|
// Save session in each iteration to ensure message persistence
|
|
818
820
|
await this.messageManager.saveSession();
|
|
@@ -949,15 +951,21 @@ export class AIManager {
|
|
|
949
951
|
};
|
|
950
952
|
}
|
|
951
953
|
|
|
952
|
-
|
|
954
|
+
llmSpan = startLLMRequestSpan(
|
|
955
|
+
model || this.getModelConfig().model || "",
|
|
956
|
+
);
|
|
953
957
|
|
|
954
958
|
const result = await aiService.callAgent(callAgentOptions);
|
|
955
959
|
|
|
956
960
|
// End LLM span with usage data
|
|
957
|
-
endLLMRequestSpan({
|
|
961
|
+
endLLMRequestSpan(llmSpan, {
|
|
958
962
|
model: model || this.getModelConfig().model || "",
|
|
959
963
|
success: true,
|
|
960
964
|
hasToolCall: !!(result.tool_calls && result.tool_calls.length > 0),
|
|
965
|
+
inputTokens: result.usage?.prompt_tokens,
|
|
966
|
+
outputTokens: result.usage?.completion_tokens,
|
|
967
|
+
cacheReadTokens: result.usage?.cache_read_input_tokens,
|
|
968
|
+
cacheCreationTokens: result.usage?.cache_creation_input_tokens,
|
|
961
969
|
});
|
|
962
970
|
|
|
963
971
|
const createdByStreaming = assistantMessageCreated;
|
|
@@ -1238,7 +1246,7 @@ export class AIManager {
|
|
|
1238
1246
|
break inner;
|
|
1239
1247
|
} catch (error) {
|
|
1240
1248
|
// End LLM span with error
|
|
1241
|
-
endLLMRequestSpan({
|
|
1249
|
+
endLLMRequestSpan(llmSpan, {
|
|
1242
1250
|
model: model || this.getModelConfig().model || "",
|
|
1243
1251
|
success: false,
|
|
1244
1252
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -1271,7 +1279,103 @@ export class AIManager {
|
|
|
1271
1279
|
// Set loading to false first
|
|
1272
1280
|
this.setIsLoading(false);
|
|
1273
1281
|
|
|
1274
|
-
//
|
|
1282
|
+
// Clear temporary rules
|
|
1283
|
+
this.permissionManager?.clearTemporaryRules();
|
|
1284
|
+
|
|
1285
|
+
// Clear abort controllers
|
|
1286
|
+
this.abortController = null;
|
|
1287
|
+
this.toolAbortController = null;
|
|
1288
|
+
|
|
1289
|
+
// Execute Stop/SubagentStop hooks only if the operation was not aborted
|
|
1290
|
+
const isCurrentlyAborted =
|
|
1291
|
+
abortController.signal.aborted || toolAbortController.signal.aborted;
|
|
1292
|
+
|
|
1293
|
+
if (!isCurrentlyAborted) {
|
|
1294
|
+
// Record committed snapshots to message history for the final turn
|
|
1295
|
+
if (this.reversionManager) {
|
|
1296
|
+
const snapshots =
|
|
1297
|
+
this.reversionManager.getAndClearCommittedSnapshots();
|
|
1298
|
+
if (snapshots.length > 0) {
|
|
1299
|
+
this.messageManager.addFileHistoryBlock(snapshots);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Goal evaluation — supersedes Stop hooks when active
|
|
1304
|
+
const goalManager = this.container.has("GoalManager")
|
|
1305
|
+
? this.container.get<import("./goalManager.js").GoalManager>(
|
|
1306
|
+
"GoalManager",
|
|
1307
|
+
)
|
|
1308
|
+
: undefined;
|
|
1309
|
+
|
|
1310
|
+
let goalContinuing = false;
|
|
1311
|
+
|
|
1312
|
+
if (goalManager?.isGoalActive() && !this.subagentType) {
|
|
1313
|
+
// 1. Increment turn count and check circuit breakers
|
|
1314
|
+
goalManager.incrementTurnCount();
|
|
1315
|
+
const circuitBreaker = goalManager.checkCircuitBreakers();
|
|
1316
|
+
|
|
1317
|
+
if (circuitBreaker) {
|
|
1318
|
+
goalManager.clearGoal();
|
|
1319
|
+
logger?.info(`[Goal] ${circuitBreaker}`);
|
|
1320
|
+
this.messageManager.addUserMessage({
|
|
1321
|
+
content: `<system-reminder>${circuitBreaker}</system-reminder>`,
|
|
1322
|
+
isMeta: true,
|
|
1323
|
+
});
|
|
1324
|
+
// Fall through to normal Stop hooks on the final turn
|
|
1325
|
+
} else {
|
|
1326
|
+
// 2. Evaluate goal
|
|
1327
|
+
const evaluation = await goalManager.evaluateGoal(
|
|
1328
|
+
abortController.signal,
|
|
1329
|
+
);
|
|
1330
|
+
|
|
1331
|
+
if (evaluation.isMet) {
|
|
1332
|
+
goalManager.clearGoal();
|
|
1333
|
+
logger?.info(`[Goal] Goal achieved: ${evaluation.reason}`);
|
|
1334
|
+
this.messageManager.addUserMessage({
|
|
1335
|
+
content: `<system-reminder>Goal achieved: ${evaluation.reason}</system-reminder>`,
|
|
1336
|
+
isMeta: true,
|
|
1337
|
+
});
|
|
1338
|
+
// Fall through to normal Stop hooks on the final turn
|
|
1339
|
+
} else {
|
|
1340
|
+
const goal = goalManager.getGoal()!;
|
|
1341
|
+
goal.lastReason = evaluation.reason;
|
|
1342
|
+
logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
|
|
1343
|
+
this.messageManager.addUserMessage({
|
|
1344
|
+
content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
|
|
1345
|
+
isMeta: true,
|
|
1346
|
+
});
|
|
1347
|
+
// Keep loading state active to prevent UI flicker
|
|
1348
|
+
this.setIsLoading(true);
|
|
1349
|
+
goalContinuing = true;
|
|
1350
|
+
// Restart outer loop to continue goal pursuit
|
|
1351
|
+
shouldRestart = true;
|
|
1352
|
+
turnOffset = 0;
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// Skip Stop hooks when goal evaluator is continuing the conversation
|
|
1358
|
+
if (goalContinuing) {
|
|
1359
|
+
// Goal evaluator supersedes Stop hooks
|
|
1360
|
+
} else {
|
|
1361
|
+
const shouldContinue = await this.executeStopHooks();
|
|
1362
|
+
|
|
1363
|
+
// If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
|
|
1364
|
+
// restart the AI conversation cycle
|
|
1365
|
+
if (shouldContinue) {
|
|
1366
|
+
logger?.info(
|
|
1367
|
+
`${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
|
|
1368
|
+
);
|
|
1369
|
+
|
|
1370
|
+
// Restart the conversation to let AI fix the issues
|
|
1371
|
+
shouldRestart = true;
|
|
1372
|
+
turnOffset = 0;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Inject pending notifications from background tasks (after Stop hooks,
|
|
1378
|
+
// aligned with Claude Code which fires Stop hooks unconditionally)
|
|
1275
1379
|
const notificationQueue = this.container.has("NotificationQueue")
|
|
1276
1380
|
? this.container.get<NotificationQueue>("NotificationQueue")
|
|
1277
1381
|
: undefined;
|
|
@@ -1292,102 +1396,6 @@ export class AIManager {
|
|
|
1292
1396
|
// Restart outer loop to process the notifications
|
|
1293
1397
|
shouldRestart = true;
|
|
1294
1398
|
turnOffset = 0;
|
|
1295
|
-
} else {
|
|
1296
|
-
// Clear temporary rules
|
|
1297
|
-
this.permissionManager?.clearTemporaryRules();
|
|
1298
|
-
|
|
1299
|
-
// Clear abort controllers
|
|
1300
|
-
this.abortController = null;
|
|
1301
|
-
this.toolAbortController = null;
|
|
1302
|
-
|
|
1303
|
-
// Execute Stop/SubagentStop hooks only if the operation was not aborted
|
|
1304
|
-
const isCurrentlyAborted =
|
|
1305
|
-
abortController.signal.aborted ||
|
|
1306
|
-
toolAbortController.signal.aborted;
|
|
1307
|
-
|
|
1308
|
-
if (!isCurrentlyAborted) {
|
|
1309
|
-
// Record committed snapshots to message history for the final turn
|
|
1310
|
-
if (this.reversionManager) {
|
|
1311
|
-
const snapshots =
|
|
1312
|
-
this.reversionManager.getAndClearCommittedSnapshots();
|
|
1313
|
-
if (snapshots.length > 0) {
|
|
1314
|
-
this.messageManager.addFileHistoryBlock(snapshots);
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// Goal evaluation — supersedes Stop hooks when active
|
|
1319
|
-
const goalManager = this.container.has("GoalManager")
|
|
1320
|
-
? this.container.get<import("./goalManager.js").GoalManager>(
|
|
1321
|
-
"GoalManager",
|
|
1322
|
-
)
|
|
1323
|
-
: undefined;
|
|
1324
|
-
|
|
1325
|
-
let goalContinuing = false;
|
|
1326
|
-
|
|
1327
|
-
if (goalManager?.isGoalActive() && !this.subagentType) {
|
|
1328
|
-
// 1. Increment turn count and check circuit breakers
|
|
1329
|
-
goalManager.incrementTurnCount();
|
|
1330
|
-
const circuitBreaker = goalManager.checkCircuitBreakers();
|
|
1331
|
-
|
|
1332
|
-
if (circuitBreaker) {
|
|
1333
|
-
goalManager.clearGoal();
|
|
1334
|
-
logger?.info(`[Goal] ${circuitBreaker}`);
|
|
1335
|
-
this.messageManager.addUserMessage({
|
|
1336
|
-
content: `<system-reminder>${circuitBreaker}</system-reminder>`,
|
|
1337
|
-
isMeta: true,
|
|
1338
|
-
});
|
|
1339
|
-
// Fall through to normal Stop hooks on the final turn
|
|
1340
|
-
} else {
|
|
1341
|
-
// 2. Evaluate goal
|
|
1342
|
-
const evaluation = await goalManager.evaluateGoal(
|
|
1343
|
-
abortController.signal,
|
|
1344
|
-
);
|
|
1345
|
-
|
|
1346
|
-
if (evaluation.isMet) {
|
|
1347
|
-
goalManager.clearGoal();
|
|
1348
|
-
logger?.info(`[Goal] Goal achieved: ${evaluation.reason}`);
|
|
1349
|
-
this.messageManager.addUserMessage({
|
|
1350
|
-
content: `<system-reminder>Goal achieved: ${evaluation.reason}</system-reminder>`,
|
|
1351
|
-
isMeta: true,
|
|
1352
|
-
});
|
|
1353
|
-
// Fall through to normal Stop hooks on the final turn
|
|
1354
|
-
} else {
|
|
1355
|
-
const goal = goalManager.getGoal()!;
|
|
1356
|
-
goal.lastReason = evaluation.reason;
|
|
1357
|
-
logger?.info(`[Goal] Not yet met: ${evaluation.reason}`);
|
|
1358
|
-
this.messageManager.addUserMessage({
|
|
1359
|
-
content: `<system-reminder>Goal not yet met: ${evaluation.reason}. Continue working toward: ${goal.condition}</system-reminder>`,
|
|
1360
|
-
isMeta: true,
|
|
1361
|
-
});
|
|
1362
|
-
// Keep loading state active to prevent UI flicker
|
|
1363
|
-
this.setIsLoading(true);
|
|
1364
|
-
goalContinuing = true;
|
|
1365
|
-
// Restart outer loop to continue goal pursuit
|
|
1366
|
-
shouldRestart = true;
|
|
1367
|
-
turnOffset = 0;
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
// Skip Stop hooks when goal evaluator is continuing the conversation
|
|
1373
|
-
if (goalContinuing) {
|
|
1374
|
-
// Goal evaluator supersedes Stop hooks
|
|
1375
|
-
} else {
|
|
1376
|
-
const shouldContinue = await this.executeStopHooks();
|
|
1377
|
-
|
|
1378
|
-
// If Stop/SubagentStop hooks indicate we should continue (due to blocking errors),
|
|
1379
|
-
// restart the AI conversation cycle
|
|
1380
|
-
if (shouldContinue) {
|
|
1381
|
-
logger?.info(
|
|
1382
|
-
`${this.subagentType ? "SubagentStop" : "Stop"} hooks indicate issues need fixing, continuing conversation...`,
|
|
1383
|
-
);
|
|
1384
|
-
|
|
1385
|
-
// Restart the conversation to let AI fix the issues
|
|
1386
|
-
shouldRestart = true;
|
|
1387
|
-
turnOffset = 0;
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
1399
|
}
|
|
1392
1400
|
}
|
|
1393
1401
|
|
|
@@ -1653,6 +1661,7 @@ export class AIManager {
|
|
|
1653
1661
|
shortResult: toolResult.shortResult,
|
|
1654
1662
|
isManuallyBackgrounded: toolResult.isManuallyBackgrounded,
|
|
1655
1663
|
startLineNumber: toolResult.startLineNumber,
|
|
1664
|
+
images: toolResult.images,
|
|
1656
1665
|
timestamp: Date.now(),
|
|
1657
1666
|
});
|
|
1658
1667
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
releaseSchedulerLock,
|
|
16
16
|
registerSchedulerLockCleanup,
|
|
17
17
|
} from "../utils/cronTasksLock.js";
|
|
18
|
+
import { ensureWaveRuntimeFilesExcluded } from "../utils/gitUtils.js";
|
|
18
19
|
|
|
19
20
|
export class CronManager {
|
|
20
21
|
private jobs = new Map<string, CronJob>();
|
|
@@ -100,6 +101,7 @@ export class CronManager {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
private tryLock(workdir: string): void {
|
|
104
|
+
ensureWaveRuntimeFilesExcluded(workdir);
|
|
103
105
|
tryAcquireSchedulerLock({ dir: workdir, sessionId: this.sessionId })
|
|
104
106
|
.then((acquired) => {
|
|
105
107
|
if (acquired) {
|
|
@@ -17,7 +17,6 @@ import type { HookManager } from "./hookManager.js";
|
|
|
17
17
|
import type { PermissionManager } from "./permissionManager.js";
|
|
18
18
|
import { isValidHookEvent } from "../types/hooks.js";
|
|
19
19
|
import { ConfigurationService } from "../services/configurationService.js";
|
|
20
|
-
import { ensureGlobalGitIgnore } from "../utils/fileUtils.js";
|
|
21
20
|
import { Container } from "../utils/container.js";
|
|
22
21
|
|
|
23
22
|
import type {
|
|
@@ -97,9 +96,6 @@ export class LiveConfigManager {
|
|
|
97
96
|
if (projectPaths) {
|
|
98
97
|
for (const projectPath of projectPaths) {
|
|
99
98
|
if (existsSync(projectPath)) {
|
|
100
|
-
if (projectPath.endsWith("settings.local.json")) {
|
|
101
|
-
await ensureGlobalGitIgnore("**/.wave/settings.local.json");
|
|
102
|
-
}
|
|
103
99
|
await this.fileWatcher.watchFile(projectPath, (event) =>
|
|
104
100
|
this.handleFileChange(event, "project"),
|
|
105
101
|
);
|
|
@@ -364,14 +360,6 @@ export class LiveConfigManager {
|
|
|
364
360
|
|
|
365
361
|
// Handle file creation or modification
|
|
366
362
|
if (event.type === "change" || event.type === "create") {
|
|
367
|
-
if (
|
|
368
|
-
source === "project" &&
|
|
369
|
-
event.path.endsWith("settings.local.json") &&
|
|
370
|
-
event.type === "create"
|
|
371
|
-
) {
|
|
372
|
-
await ensureGlobalGitIgnore("**/.wave/settings.local.json");
|
|
373
|
-
}
|
|
374
|
-
|
|
375
363
|
// Add small delay to ensure file write is complete
|
|
376
364
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
377
365
|
|
|
@@ -848,9 +848,16 @@ export class McpManager {
|
|
|
848
848
|
textContent.push(String(result.content));
|
|
849
849
|
}
|
|
850
850
|
|
|
851
|
+
const textContentStr =
|
|
852
|
+
textContent.length > 0
|
|
853
|
+
? textContent.join("\n")
|
|
854
|
+
: images.length > 0
|
|
855
|
+
? `Tool returned ${images.length} image(s).`
|
|
856
|
+
: "No content";
|
|
857
|
+
|
|
851
858
|
return {
|
|
852
859
|
success: true,
|
|
853
|
-
content:
|
|
860
|
+
content: textContentStr,
|
|
854
861
|
images: images.length > 0 ? images : undefined,
|
|
855
862
|
serverName,
|
|
856
863
|
};
|
|
@@ -31,6 +31,7 @@ import type { MemoryRuleManager } from "./MemoryRuleManager.js";
|
|
|
31
31
|
import type { MemoryRule } from "../types/memoryRule.js";
|
|
32
32
|
import type { MemoryService } from "../services/memory.js";
|
|
33
33
|
import { pathEncoder } from "../utils/pathEncoder.js";
|
|
34
|
+
import { estimateTokens } from "../utils/tokenEstimate.js";
|
|
34
35
|
import { READ_TOOL_NAME } from "../constants/tools.js";
|
|
35
36
|
|
|
36
37
|
import { Container } from "../utils/container.js";
|
|
@@ -1063,7 +1064,7 @@ export class MessageManager {
|
|
|
1063
1064
|
/**
|
|
1064
1065
|
* Get recent file read contents, sorted by timestamp (newest first).
|
|
1065
1066
|
* @param maxFiles - Maximum number of files to return
|
|
1066
|
-
* @param maxTokensPerFile - Maximum tokens per file (
|
|
1067
|
+
* @param maxTokensPerFile - Maximum tokens per file (CJK-aware estimation)
|
|
1067
1068
|
* @returns Array of { path, content } sorted by recency
|
|
1068
1069
|
*/
|
|
1069
1070
|
public getRecentFileReads(
|
|
@@ -1076,10 +1077,14 @@ export class MessageManager {
|
|
|
1076
1077
|
|
|
1077
1078
|
const result: Array<{ path: string; content: string }> = [];
|
|
1078
1079
|
for (const [path, { content }] of sorted) {
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1080
|
+
const tokenCount = estimateTokens(content);
|
|
1081
|
+
let truncated = content;
|
|
1082
|
+
if (tokenCount > maxTokensPerFile) {
|
|
1083
|
+
// Truncate proportionally to fit within the token budget
|
|
1084
|
+
const ratio = maxTokensPerFile / tokenCount;
|
|
1085
|
+
const cutIndex = Math.floor(content.length * ratio);
|
|
1086
|
+
truncated = content.slice(0, cutIndex);
|
|
1087
|
+
}
|
|
1083
1088
|
result.push({ path, content: truncated });
|
|
1084
1089
|
}
|
|
1085
1090
|
return result;
|
|
@@ -279,6 +279,7 @@ class ToolManager {
|
|
|
279
279
|
endToolSpan({
|
|
280
280
|
success: result.success,
|
|
281
281
|
durationMs: Date.now() - toolStartTime,
|
|
282
|
+
output: result.content,
|
|
282
283
|
});
|
|
283
284
|
return result;
|
|
284
285
|
} catch (error) {
|
|
@@ -303,6 +304,7 @@ class ToolManager {
|
|
|
303
304
|
endToolSpan({
|
|
304
305
|
success: result.success,
|
|
305
306
|
durationMs: Date.now() - toolStartTime,
|
|
307
|
+
output: result.content,
|
|
306
308
|
});
|
|
307
309
|
return result;
|
|
308
310
|
} catch (error) {
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
mergeRemoteSettings,
|
|
48
48
|
} from "./remoteSettingsService.js";
|
|
49
49
|
import { createAuthAwareFetch } from "./authService.js";
|
|
50
|
+
import { ensureWaveRuntimeFilesExcluded } from "../utils/gitUtils.js";
|
|
50
51
|
|
|
51
52
|
/**
|
|
52
53
|
* Default ConfigurationService implementation
|
|
@@ -791,6 +792,7 @@ export class ConfigurationService {
|
|
|
791
792
|
|
|
792
793
|
// Ensure .wave directory exists
|
|
793
794
|
const waveDir = path.join(workdir, ".wave");
|
|
795
|
+
ensureWaveRuntimeFilesExcluded(workdir);
|
|
794
796
|
if (!existsSync(waveDir)) {
|
|
795
797
|
await fs.mkdir(waveDir, { recursive: true });
|
|
796
798
|
}
|
|
@@ -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
|
}
|