wave-agent-sdk 0.18.7 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +20 -4
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +7 -2
- package/dist/managers/messageManager.d.ts +3 -5
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +17 -77
- 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/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 +22 -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 +14 -4
- package/dist/telemetry/sessionTracing.d.ts.map +1 -1
- package/dist/telemetry/sessionTracing.js +134 -8
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +16 -1
- 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 +14 -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/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/package.json +1 -1
- package/src/agent.ts +11 -5
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +22 -2
- package/src/managers/mcpManager.ts +8 -2
- package/src/managers/messageManager.ts +22 -104
- package/src/managers/skillManager.ts +16 -2
- package/src/prompts/index.ts +6 -1
- package/src/services/aiService.ts +12 -5
- package/src/services/configurationService.ts +24 -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 +167 -12
- package/src/tools/bashTool.ts +23 -1
- 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 +17 -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/mcpUtils.ts +2 -1
- package/src/utils/modelCapabilities.ts +30 -0
- package/src/utils/skillParser.ts +3 -1
package/dist/services/session.js
CHANGED
|
@@ -41,34 +41,6 @@ export function generateSubagentFilename(sessionId) {
|
|
|
41
41
|
// Constants
|
|
42
42
|
export const SESSION_DIR = join(homedir(), ".wave", "projects");
|
|
43
43
|
const MAX_SESSION_AGE_DAYS = 14;
|
|
44
|
-
const SESSION_INDEX_FILENAME = "sessions-index.json";
|
|
45
|
-
/**
|
|
46
|
-
* Update the session index for a project directory
|
|
47
|
-
*/
|
|
48
|
-
async function updateSessionIndex(projectDirPath, metadata) {
|
|
49
|
-
const indexPath = join(projectDirPath, SESSION_INDEX_FILENAME);
|
|
50
|
-
let index = {
|
|
51
|
-
sessions: {},
|
|
52
|
-
lastUpdated: new Date().toISOString(),
|
|
53
|
-
};
|
|
54
|
-
try {
|
|
55
|
-
const content = await fs.readFile(indexPath, "utf8");
|
|
56
|
-
index = JSON.parse(content);
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
// Index doesn't exist or is invalid, start fresh
|
|
60
|
-
}
|
|
61
|
-
const { id, ...rest } = metadata;
|
|
62
|
-
index.sessions[id] = {
|
|
63
|
-
...rest,
|
|
64
|
-
createdAt: metadata.createdAt.toISOString(),
|
|
65
|
-
lastActiveAt: metadata.lastActiveAt.toISOString(),
|
|
66
|
-
firstMessage: metadata.firstMessage || index.sessions[id]?.firstMessage,
|
|
67
|
-
parentSessionId: metadata.parentSessionId,
|
|
68
|
-
};
|
|
69
|
-
index.lastUpdated = new Date().toISOString();
|
|
70
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
71
|
-
}
|
|
72
44
|
/**
|
|
73
45
|
* Ensure session directory exists
|
|
74
46
|
*/
|
|
@@ -129,7 +101,7 @@ export async function createSession(sessionId, workdir, sessionType = "main") {
|
|
|
129
101
|
* @param workdir - Working directory for the session
|
|
130
102
|
* @param sessionType - Type of session ("main" or "subagent", defaults to "main")
|
|
131
103
|
*/
|
|
132
|
-
export async function appendMessages(sessionId, newMessages, workdir, sessionType = "main"
|
|
104
|
+
export async function appendMessages(sessionId, newMessages, workdir, sessionType = "main") {
|
|
133
105
|
// Do not save session files in test environment
|
|
134
106
|
if (process.env.NODE_ENV === "test") {
|
|
135
107
|
return;
|
|
@@ -151,47 +123,6 @@ export async function appendMessages(sessionId, newMessages, workdir, sessionTyp
|
|
|
151
123
|
await jsonlHandler.append(filePath, newMessages, {
|
|
152
124
|
atomic: false,
|
|
153
125
|
});
|
|
154
|
-
// Update index
|
|
155
|
-
const encoder = new PathEncoder();
|
|
156
|
-
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
157
|
-
const lastMessage = newMessages[newMessages.length - 1];
|
|
158
|
-
// Get first message content and createdAt from existing index
|
|
159
|
-
let firstMessage;
|
|
160
|
-
let createdAt;
|
|
161
|
-
try {
|
|
162
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
163
|
-
const content = await fs.readFile(indexPath, "utf8");
|
|
164
|
-
const index = JSON.parse(content);
|
|
165
|
-
if (!index.sessions[sessionId]?.firstMessage) {
|
|
166
|
-
firstMessage =
|
|
167
|
-
(await getFirstMessageContent(sessionId, workdir)) || undefined;
|
|
168
|
-
}
|
|
169
|
-
if (index.sessions[sessionId]?.createdAt) {
|
|
170
|
-
createdAt = new Date(index.sessions[sessionId].createdAt);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
catch {
|
|
174
|
-
// If index doesn't exist, this might be the first message
|
|
175
|
-
firstMessage =
|
|
176
|
-
(await getFirstMessageContent(sessionId, workdir)) || undefined;
|
|
177
|
-
}
|
|
178
|
-
// Derive createdAt from session ID if not in index
|
|
179
|
-
if (!createdAt) {
|
|
180
|
-
createdAt = new Date();
|
|
181
|
-
}
|
|
182
|
-
await updateSessionIndex(projectDir.encodedPath, {
|
|
183
|
-
id: sessionId,
|
|
184
|
-
rootSessionId,
|
|
185
|
-
parentSessionId,
|
|
186
|
-
sessionType,
|
|
187
|
-
workdir,
|
|
188
|
-
createdAt,
|
|
189
|
-
lastActiveAt: new Date(lastMessage.timestamp),
|
|
190
|
-
latestTotalTokens: lastMessage.usage
|
|
191
|
-
? extractLatestTotalTokens([lastMessage])
|
|
192
|
-
: 0,
|
|
193
|
-
firstMessage,
|
|
194
|
-
});
|
|
195
126
|
}
|
|
196
127
|
/**
|
|
197
128
|
* Load session data from JSONL file (new approach)
|
|
@@ -216,28 +147,20 @@ export async function loadSessionFromJsonl(sessionId, workdir, sessionType = "ma
|
|
|
216
147
|
}
|
|
217
148
|
throw error;
|
|
218
149
|
}
|
|
219
|
-
const
|
|
150
|
+
const allMessages = await jsonlHandler.read(filePath);
|
|
151
|
+
// Find the last compact boundary — only return messages from there forward
|
|
152
|
+
let lastCompactIndex = -1;
|
|
153
|
+
for (let i = allMessages.length - 1; i >= 0; i--) {
|
|
154
|
+
if (allMessages[i].blocks?.some((b) => b.type === "compact")) {
|
|
155
|
+
lastCompactIndex = i;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const messages = lastCompactIndex >= 0 ? allMessages.slice(lastCompactIndex) : allMessages;
|
|
220
160
|
// Extract metadata from messages
|
|
221
161
|
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
222
|
-
// Try to get rootSessionId and parentSessionId from index
|
|
223
|
-
let rootSessionId;
|
|
224
|
-
let parentSessionId;
|
|
225
|
-
try {
|
|
226
|
-
const encoder = new PathEncoder();
|
|
227
|
-
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
228
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
229
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
230
|
-
const index = JSON.parse(indexContent);
|
|
231
|
-
rootSessionId = index.sessions[sessionId]?.rootSessionId;
|
|
232
|
-
parentSessionId = index.sessions[sessionId]?.parentSessionId;
|
|
233
|
-
}
|
|
234
|
-
catch {
|
|
235
|
-
// Ignore index errors
|
|
236
|
-
}
|
|
237
162
|
const sessionData = {
|
|
238
163
|
id: sessionId,
|
|
239
|
-
rootSessionId: rootSessionId || sessionId,
|
|
240
|
-
parentSessionId,
|
|
241
164
|
messages,
|
|
242
165
|
metadata: {
|
|
243
166
|
workdir,
|
|
@@ -311,29 +234,8 @@ export async function listSessionsFromJsonl(workdir) {
|
|
|
311
234
|
const encoder = new PathEncoder();
|
|
312
235
|
const baseDir = SESSION_DIR;
|
|
313
236
|
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
314
|
-
// Try to read from index first
|
|
315
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
316
237
|
const sevenDaysAgo = new Date();
|
|
317
238
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
318
|
-
try {
|
|
319
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
320
|
-
const index = JSON.parse(indexContent);
|
|
321
|
-
const sessions = Object.entries(index.sessions)
|
|
322
|
-
.filter(([, meta]) => {
|
|
323
|
-
const lastActiveAt = new Date(meta.lastActiveAt);
|
|
324
|
-
return meta.sessionType === "main" && lastActiveAt >= sevenDaysAgo;
|
|
325
|
-
})
|
|
326
|
-
.map(([id, meta]) => ({
|
|
327
|
-
id,
|
|
328
|
-
...meta,
|
|
329
|
-
createdAt: new Date(meta.createdAt),
|
|
330
|
-
lastActiveAt: new Date(meta.lastActiveAt),
|
|
331
|
-
}));
|
|
332
|
-
return sessions.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
// Fallback to manual listing if index fails
|
|
336
|
-
}
|
|
337
239
|
let files;
|
|
338
240
|
try {
|
|
339
241
|
files = await fs.readdir(projectDir.encodedPath);
|
|
@@ -382,7 +284,7 @@ export async function listSessionsFromJsonl(workdir) {
|
|
|
382
284
|
const sessionMeta = {
|
|
383
285
|
id: sessionId,
|
|
384
286
|
sessionType: "main",
|
|
385
|
-
subagentType: undefined,
|
|
287
|
+
subagentType: undefined,
|
|
386
288
|
workdir: projectDir.originalPath,
|
|
387
289
|
createdAt: new Date(),
|
|
388
290
|
lastActiveAt,
|
|
@@ -390,7 +292,7 @@ export async function listSessionsFromJsonl(workdir) {
|
|
|
390
292
|
? extractLatestTotalTokens([lastMessage])
|
|
391
293
|
: 0,
|
|
392
294
|
};
|
|
393
|
-
// Try to get first message content for
|
|
295
|
+
// Try to get first message content for display
|
|
394
296
|
try {
|
|
395
297
|
const firstContent = await getFirstMessageContent(sessionId, workdir);
|
|
396
298
|
if (firstContent) {
|
|
@@ -408,27 +310,7 @@ export async function listSessionsFromJsonl(workdir) {
|
|
|
408
310
|
}
|
|
409
311
|
}
|
|
410
312
|
// Sort by last active time (most recently active first)
|
|
411
|
-
|
|
412
|
-
// Rebuild index if we had to fall back
|
|
413
|
-
try {
|
|
414
|
-
const index = {
|
|
415
|
-
sessions: {},
|
|
416
|
-
lastUpdated: new Date().toISOString(),
|
|
417
|
-
};
|
|
418
|
-
for (const session of sessions) {
|
|
419
|
-
const { id, ...rest } = session;
|
|
420
|
-
index.sessions[id] = {
|
|
421
|
-
...rest,
|
|
422
|
-
createdAt: session.createdAt.toISOString(),
|
|
423
|
-
lastActiveAt: session.lastActiveAt.toISOString(),
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
427
|
-
}
|
|
428
|
-
catch (error) {
|
|
429
|
-
logger.warn(`Failed to rebuild session index for ${workdir}:`, error);
|
|
430
|
-
}
|
|
431
|
-
return sortedSessions;
|
|
313
|
+
return sessions.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
|
|
432
314
|
}
|
|
433
315
|
catch (error) {
|
|
434
316
|
throw new Error(`Failed to list sessions: ${error}`);
|
|
@@ -462,30 +344,49 @@ export async function listAllSessions() {
|
|
|
462
344
|
if (!stat.isDirectory()) {
|
|
463
345
|
continue;
|
|
464
346
|
}
|
|
465
|
-
//
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
347
|
+
// Scan .jsonl files in the project directory
|
|
348
|
+
const files = await fs.readdir(projectPath);
|
|
349
|
+
for (const file of files) {
|
|
350
|
+
if (!file.endsWith(".jsonl") || file.startsWith("subagent-")) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const filePath = join(projectPath, file);
|
|
355
|
+
const uuidMatch = file.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
|
|
356
|
+
if (!uuidMatch)
|
|
357
|
+
continue;
|
|
358
|
+
const sessionId = uuidMatch[1];
|
|
359
|
+
const jsonlHandler = new JsonlHandler();
|
|
360
|
+
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
361
|
+
let lastActiveAt;
|
|
362
|
+
if (lastMessage) {
|
|
363
|
+
lastActiveAt = new Date(lastMessage.timestamp);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const stats = await fs.stat(filePath);
|
|
367
|
+
lastActiveAt = stats.mtime;
|
|
479
368
|
}
|
|
369
|
+
if (lastActiveAt < sevenDaysAgo)
|
|
370
|
+
continue;
|
|
371
|
+
allSessions.push({
|
|
372
|
+
id: sessionId,
|
|
373
|
+
sessionType: "main",
|
|
374
|
+
subagentType: undefined,
|
|
375
|
+
workdir: projectDirName,
|
|
376
|
+
createdAt: new Date(),
|
|
377
|
+
lastActiveAt,
|
|
378
|
+
latestTotalTokens: lastMessage?.usage
|
|
379
|
+
? extractLatestTotalTokens([lastMessage])
|
|
380
|
+
: 0,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
continue;
|
|
480
385
|
}
|
|
481
|
-
}
|
|
482
|
-
catch {
|
|
483
|
-
// If index fails, we skip this project directory
|
|
484
|
-
// In the future, we could scan for .jsonl files here
|
|
485
386
|
}
|
|
486
387
|
}
|
|
487
388
|
catch {
|
|
488
|
-
// Skip if stat fails
|
|
389
|
+
// Skip if stat/readdir fails
|
|
489
390
|
}
|
|
490
391
|
}
|
|
491
392
|
return allSessions.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
|
|
@@ -523,26 +424,6 @@ export async function cleanupExpiredSessionsFromJsonl(workdir) {
|
|
|
523
424
|
if (fileAge > maxAge) {
|
|
524
425
|
await fs.unlink(filePath);
|
|
525
426
|
deletedCount++;
|
|
526
|
-
// Remove from index if it exists
|
|
527
|
-
try {
|
|
528
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
529
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
530
|
-
const index = JSON.parse(indexContent);
|
|
531
|
-
const uuidMatch = file.match(/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
|
|
532
|
-
const subagentMatch = file.match(/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/);
|
|
533
|
-
const sessionId = uuidMatch
|
|
534
|
-
? uuidMatch[1]
|
|
535
|
-
: subagentMatch
|
|
536
|
-
? subagentMatch[1]
|
|
537
|
-
: null;
|
|
538
|
-
if (sessionId && index.sessions[sessionId]) {
|
|
539
|
-
delete index.sessions[sessionId];
|
|
540
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
catch {
|
|
544
|
-
// Ignore index update errors during cleanup
|
|
545
|
-
}
|
|
546
427
|
}
|
|
547
428
|
}
|
|
548
429
|
catch {
|
|
@@ -697,21 +578,6 @@ export async function deleteSession(sessionId, workdir, sessionType = "main") {
|
|
|
697
578
|
throw error;
|
|
698
579
|
}
|
|
699
580
|
}
|
|
700
|
-
// Also remove from index
|
|
701
|
-
try {
|
|
702
|
-
const encoder = new PathEncoder();
|
|
703
|
-
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
704
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
705
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
706
|
-
const index = JSON.parse(indexContent);
|
|
707
|
-
if (index.sessions[sessionId]) {
|
|
708
|
-
delete index.sessions[sessionId];
|
|
709
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
catch {
|
|
713
|
-
// Ignore index errors
|
|
714
|
-
}
|
|
715
581
|
}
|
|
716
582
|
/**
|
|
717
583
|
* Truncate content to a maximum length, adding ellipsis if truncated.
|
|
@@ -778,38 +644,16 @@ export async function handleSessionRestoration(restoreSessionId, continueLastSes
|
|
|
778
644
|
}
|
|
779
645
|
}
|
|
780
646
|
/**
|
|
781
|
-
* Load the full message thread
|
|
647
|
+
* Load the full message thread for a session.
|
|
648
|
+
* With append-only compaction, all messages are in a single file.
|
|
649
|
+
* Returns the active messages (post-compact boundary).
|
|
782
650
|
* @param currentSessionId - The ID of the current session
|
|
783
651
|
* @param workdir - Working directory for the session
|
|
784
652
|
* @returns Promise that resolves to an array of all messages in the thread
|
|
785
653
|
*/
|
|
786
654
|
export async function loadFullMessageThread(currentSessionId, workdir) {
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const sessionData = await loadSessionFromJsonl(currentId, workdir);
|
|
792
|
-
if (!sessionData)
|
|
793
|
-
break;
|
|
794
|
-
sessionIds.unshift(currentId);
|
|
795
|
-
// Add messages from this session to the beginning of the list
|
|
796
|
-
// But skip the "compact" block if it's not the first session in our traversal (which is the latest)
|
|
797
|
-
// Actually, we should probably keep all messages and let the UI/logic handle it.
|
|
798
|
-
// But wait, if we are concatenating, the "compact" block in session N summarizes session N-1.
|
|
799
|
-
// So if we have session N-1 and session N, we should probably skip the compact block in session N.
|
|
800
|
-
const messages = sessionData.messages;
|
|
801
|
-
if (allMessages.length > 0) {
|
|
802
|
-
// If we already have messages (from "later" sessions),
|
|
803
|
-
// we are now adding messages from an "earlier" session.
|
|
804
|
-
// The later session's first message might be a "compact" block.
|
|
805
|
-
if (allMessages[0].blocks.some((b) => b.type === "compact")) {
|
|
806
|
-
// Remove the compact block from the later session's messages
|
|
807
|
-
// because we are now providing the actual messages it summarized.
|
|
808
|
-
allMessages.shift();
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
allMessages.unshift(...messages);
|
|
812
|
-
currentId = sessionData.parentSessionId;
|
|
813
|
-
}
|
|
814
|
-
return { messages: allMessages, sessionIds };
|
|
655
|
+
const sessionData = await loadSessionFromJsonl(currentSessionId, workdir);
|
|
656
|
+
if (!sessionData)
|
|
657
|
+
return { messages: [], sessionIds: [] };
|
|
658
|
+
return { messages: sessionData.messages, sessionIds: [currentSessionId] };
|
|
815
659
|
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* endLLMRequestSpan, aligning with Claude Code's approach.
|
|
11
11
|
*/
|
|
12
12
|
import type { Span } from "@opentelemetry/api";
|
|
13
|
-
import type { LLMRequestMetadata, ToolMetadata } from "../types/telemetry.js";
|
|
13
|
+
import type { LLMRequestMetadata, LLMRequestInput, ToolMetadata } from "../types/telemetry.js";
|
|
14
14
|
/**
|
|
15
15
|
* Creates an interaction span for a user turn.
|
|
16
16
|
* The span is stored in interactionContext for the duration of the turn.
|
|
@@ -20,16 +20,26 @@ export declare function startInteractionSpan(userPrompt: string, sequence: numbe
|
|
|
20
20
|
* Ends the current interaction span and clears the context.
|
|
21
21
|
*/
|
|
22
22
|
export declare function endInteractionSpan(): void;
|
|
23
|
+
/**
|
|
24
|
+
* Resets incremental tracing state. Should be called after compaction to
|
|
25
|
+
* prevent incorrect delta calculations.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resetTracingState(): void;
|
|
23
28
|
/**
|
|
24
29
|
* Creates an LLM request span as a child of the interaction span.
|
|
25
30
|
* Does NOT enter any ALS — the span must be passed explicitly to endLLMRequestSpan.
|
|
31
|
+
*
|
|
32
|
+
* When logToolContent is enabled, captures:
|
|
33
|
+
* - system_prompt_hash / system_prompt_preview / system_prompt_length
|
|
34
|
+
* - new_context (incremental messages delta)
|
|
35
|
+
* - tools (JSON array of {name, hash}) / tools_count
|
|
26
36
|
*/
|
|
27
|
-
export declare function startLLMRequestSpan(model: string, options?:
|
|
28
|
-
context?: string;
|
|
29
|
-
}): Span | undefined;
|
|
37
|
+
export declare function startLLMRequestSpan(model: string, options?: LLMRequestInput): Span | undefined;
|
|
30
38
|
/**
|
|
31
39
|
* Ends an LLM request span with response metadata.
|
|
32
40
|
* The span is passed explicitly — no ALS is read or modified.
|
|
41
|
+
*
|
|
42
|
+
* When logToolContent is enabled, captures response.model_output.
|
|
33
43
|
*/
|
|
34
44
|
export declare function endLLMRequestSpan(span: Span | undefined, metadata: LLMRequestMetadata): void;
|
|
35
45
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sessionTracing.d.ts","sourceRoot":"","sources":["../../src/telemetry/sessionTracing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"sessionTracing.d.ts","sourceRoot":"","sources":["../../src/telemetry/sessionTracing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAO/C,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,YAAY,EACb,MAAM,uBAAuB,CAAC;AAgD/B;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACf,IAAI,GAAG,SAAS,CAiBlB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAIxC;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,eAAe,GACxB,IAAI,GAAG,SAAS,CA2GlB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,QAAQ,EAAE,kBAAkB,GAC3B,IAAI,CAiCN;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,OAAO,GACd,IAAI,GAAG,SAAS,CA8BlB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAoBxD"}
|
|
@@ -10,10 +10,35 @@
|
|
|
10
10
|
* endLLMRequestSpan, aligning with Claude Code's approach.
|
|
11
11
|
*/
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
13
14
|
import { getOTELApi, isInitialized, getCurrentConfig, } from "./instrumentation.js";
|
|
15
|
+
import { logOTelEvent } from "./events.js";
|
|
16
|
+
// -- Constants --
|
|
17
|
+
/** Max content length for span attributes (60KB, aligning with Claude Code) */
|
|
18
|
+
const MAX_CONTENT_LENGTH = 60000;
|
|
14
19
|
// -- AsyncLocalStorage for context propagation --
|
|
15
20
|
const interactionContext = new AsyncLocalStorage();
|
|
16
21
|
const toolContext = new AsyncLocalStorage();
|
|
22
|
+
// -- Incremental tracking state (module-level, per session) --
|
|
23
|
+
/** Number of messages already reported in new_context; only deltas are sent */
|
|
24
|
+
let lastReportedMessageCount = 0;
|
|
25
|
+
/** Set of system prompt hashes already emitted via logOTelEvent */
|
|
26
|
+
const seenSystemPromptHashes = new Set();
|
|
27
|
+
/** Set of tool schema hashes already emitted via logOTelEvent */
|
|
28
|
+
const seenToolSchemaHashes = new Set();
|
|
29
|
+
// -- Helpers --
|
|
30
|
+
/** Computes SHA-256 hash of content, returns first 12 hex characters */
|
|
31
|
+
function hashContent(content) {
|
|
32
|
+
return createHash("sha256").update(content).digest("hex").substring(0, 12);
|
|
33
|
+
}
|
|
34
|
+
/** Truncates content to MAX_CONTENT_LENGTH, returns [truncated, originalLength] */
|
|
35
|
+
function truncateContent(content) {
|
|
36
|
+
const originalLength = content.length;
|
|
37
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
38
|
+
return [content.substring(0, MAX_CONTENT_LENGTH), originalLength];
|
|
39
|
+
}
|
|
40
|
+
return [content, originalLength];
|
|
41
|
+
}
|
|
17
42
|
// -- Tracer accessor --
|
|
18
43
|
function getTracer() {
|
|
19
44
|
if (!isInitialized())
|
|
@@ -55,9 +80,23 @@ export function endInteractionSpan() {
|
|
|
55
80
|
span.end();
|
|
56
81
|
interactionContext.enterWith(undefined);
|
|
57
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Resets incremental tracing state. Should be called after compaction to
|
|
85
|
+
* prevent incorrect delta calculations.
|
|
86
|
+
*/
|
|
87
|
+
export function resetTracingState() {
|
|
88
|
+
lastReportedMessageCount = 0;
|
|
89
|
+
seenSystemPromptHashes.clear();
|
|
90
|
+
seenToolSchemaHashes.clear();
|
|
91
|
+
}
|
|
58
92
|
/**
|
|
59
93
|
* Creates an LLM request span as a child of the interaction span.
|
|
60
94
|
* Does NOT enter any ALS — the span must be passed explicitly to endLLMRequestSpan.
|
|
95
|
+
*
|
|
96
|
+
* When logToolContent is enabled, captures:
|
|
97
|
+
* - system_prompt_hash / system_prompt_preview / system_prompt_length
|
|
98
|
+
* - new_context (incremental messages delta)
|
|
99
|
+
* - tools (JSON array of {name, hash}) / tools_count
|
|
61
100
|
*/
|
|
62
101
|
export function startLLMRequestSpan(model, options) {
|
|
63
102
|
const tracer = getTracer();
|
|
@@ -70,6 +109,78 @@ export function startLLMRequestSpan(model, options) {
|
|
|
70
109
|
if (options?.context) {
|
|
71
110
|
attributes["llm_request.context"] = options.context;
|
|
72
111
|
}
|
|
112
|
+
const config = getCurrentConfig();
|
|
113
|
+
// Content capture gated by logToolContent
|
|
114
|
+
if (config?.logToolContent) {
|
|
115
|
+
// System prompt: hash + preview + length
|
|
116
|
+
if (options?.systemPrompt) {
|
|
117
|
+
const hash = hashContent(options.systemPrompt);
|
|
118
|
+
attributes.system_prompt_hash = hash;
|
|
119
|
+
attributes.system_prompt_length = options.systemPrompt.length;
|
|
120
|
+
const preview = options.systemPrompt.length > 500
|
|
121
|
+
? options.systemPrompt.substring(0, 500)
|
|
122
|
+
: options.systemPrompt;
|
|
123
|
+
attributes.system_prompt_preview = preview;
|
|
124
|
+
// Emit full system prompt via event only once per hash
|
|
125
|
+
if (!seenSystemPromptHashes.has(hash)) {
|
|
126
|
+
seenSystemPromptHashes.add(hash);
|
|
127
|
+
logOTelEvent("system_prompt", {
|
|
128
|
+
system_prompt_hash: hash,
|
|
129
|
+
system_prompt: options.systemPrompt,
|
|
130
|
+
}).catch(() => { });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Incremental input messages: only send new messages since last report
|
|
134
|
+
if (options?.inputMessages && options.inputMessages.length > 0) {
|
|
135
|
+
const totalCount = options.inputMessages.length;
|
|
136
|
+
const deltaCount = totalCount - lastReportedMessageCount;
|
|
137
|
+
if (deltaCount > 0) {
|
|
138
|
+
const newMessages = options.inputMessages.slice(lastReportedMessageCount);
|
|
139
|
+
const newContextStr = JSON.stringify(newMessages);
|
|
140
|
+
const [truncated, originalLength] = truncateContent(newContextStr);
|
|
141
|
+
attributes.new_context = truncated;
|
|
142
|
+
attributes.new_context_message_count = deltaCount;
|
|
143
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
144
|
+
attributes.new_context_truncated = true;
|
|
145
|
+
attributes.new_context_original_length = originalLength;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
lastReportedMessageCount = totalCount;
|
|
149
|
+
}
|
|
150
|
+
// Tool schemas: hash per tool + count
|
|
151
|
+
if (options?.toolsSchema) {
|
|
152
|
+
try {
|
|
153
|
+
const tools = JSON.parse(options.toolsSchema);
|
|
154
|
+
if (Array.isArray(tools)) {
|
|
155
|
+
const toolHashes = tools.map((tool) => {
|
|
156
|
+
const name = tool.function?.name || "unknown";
|
|
157
|
+
const toolStr = JSON.stringify(tool);
|
|
158
|
+
const hash = hashContent(toolStr);
|
|
159
|
+
return { name, hash };
|
|
160
|
+
});
|
|
161
|
+
attributes.tools = JSON.stringify(toolHashes);
|
|
162
|
+
attributes.tools_count = toolHashes.length;
|
|
163
|
+
// Emit full tool schema via event only once per hash
|
|
164
|
+
for (const { name, hash } of toolHashes) {
|
|
165
|
+
if (!seenToolSchemaHashes.has(hash)) {
|
|
166
|
+
seenToolSchemaHashes.add(hash);
|
|
167
|
+
const fullTool = tools.find((t) => t.function?.name === name);
|
|
168
|
+
if (fullTool) {
|
|
169
|
+
logOTelEvent("tool_schema", {
|
|
170
|
+
tool_name: name,
|
|
171
|
+
tool_hash: hash,
|
|
172
|
+
tool_schema: JSON.stringify(fullTool),
|
|
173
|
+
}).catch(() => { });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// toolsSchema is not valid JSON — skip
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
73
184
|
const parent = interactionContext.getStore();
|
|
74
185
|
let span;
|
|
75
186
|
if (parent) {
|
|
@@ -85,6 +196,8 @@ export function startLLMRequestSpan(model, options) {
|
|
|
85
196
|
/**
|
|
86
197
|
* Ends an LLM request span with response metadata.
|
|
87
198
|
* The span is passed explicitly — no ALS is read or modified.
|
|
199
|
+
*
|
|
200
|
+
* When logToolContent is enabled, captures response.model_output.
|
|
88
201
|
*/
|
|
89
202
|
export function endLLMRequestSpan(span, metadata) {
|
|
90
203
|
if (!span)
|
|
@@ -106,6 +219,16 @@ export function endLLMRequestSpan(span, metadata) {
|
|
|
106
219
|
span.setAttribute("error", metadata.error);
|
|
107
220
|
if (metadata.hasToolCall != null)
|
|
108
221
|
span.setAttribute("has_tool_call", metadata.hasToolCall);
|
|
222
|
+
// Content capture gated by logToolContent
|
|
223
|
+
const config = getCurrentConfig();
|
|
224
|
+
if (config?.logToolContent && metadata.modelOutput) {
|
|
225
|
+
const [truncated, originalLength] = truncateContent(metadata.modelOutput);
|
|
226
|
+
span.setAttribute("response.model_output", truncated);
|
|
227
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
228
|
+
span.setAttribute("response.model_output_truncated", true);
|
|
229
|
+
span.setAttribute("response.model_output_original_length", originalLength);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
109
232
|
span.end();
|
|
110
233
|
}
|
|
111
234
|
/**
|
|
@@ -122,11 +245,13 @@ export function startToolSpan(toolName, input) {
|
|
|
122
245
|
tool_name: toolName,
|
|
123
246
|
};
|
|
124
247
|
if (config?.logToolContent && input !== undefined) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
248
|
+
const inputStr = typeof input === "string" ? input : JSON.stringify(input);
|
|
249
|
+
const [truncated, originalLength] = truncateContent(inputStr);
|
|
250
|
+
attributes.tool_input = truncated;
|
|
251
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
252
|
+
attributes.tool_input_truncated = true;
|
|
253
|
+
attributes.tool_input_original_length = originalLength;
|
|
128
254
|
}
|
|
129
|
-
attributes.tool_input = inputStr;
|
|
130
255
|
}
|
|
131
256
|
const parent = interactionContext.getStore();
|
|
132
257
|
let span;
|
|
@@ -155,11 +280,12 @@ export function endToolSpan(metadata) {
|
|
|
155
280
|
span.setAttribute("duration_ms", metadata.durationMs);
|
|
156
281
|
const config = getCurrentConfig();
|
|
157
282
|
if (config?.logToolContent && metadata.output) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
283
|
+
const [truncated, originalLength] = truncateContent(metadata.output);
|
|
284
|
+
span.setAttribute("tool_output", truncated);
|
|
285
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
286
|
+
span.setAttribute("tool_output_truncated", true);
|
|
287
|
+
span.setAttribute("tool_output_original_length", originalLength);
|
|
161
288
|
}
|
|
162
|
-
span.setAttribute("tool_output", outputStr);
|
|
163
289
|
}
|
|
164
290
|
span.end();
|
|
165
291
|
toolContext.enterWith(undefined);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashTool.d.ts","sourceRoot":"","sources":["../../src/tools/bashTool.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"bashTool.d.ts","sourceRoot":"","sources":["../../src/tools/bashTool.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAA2B,MAAM,YAAY,CAAC;AA0CtE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,UA8iBtB,CAAC"}
|
package/dist/tools/bashTool.js
CHANGED
|
@@ -10,6 +10,21 @@ import { processToolResult } from "../utils/toolResultStorage.js";
|
|
|
10
10
|
import { BASH_MAX_OUTPUT_CHARS } from "../constants/toolLimits.js";
|
|
11
11
|
import { BASH_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, READ_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, } from "../constants/tools.js";
|
|
12
12
|
const BASH_DEFAULT_TIMEOUT_MS = 120000;
|
|
13
|
+
/**
|
|
14
|
+
* Wrap a user command so we can append CWD tracking (`&& pwd -P`) without the
|
|
15
|
+
* appended part being affected by trailing here-docs, unbalanced quotes, or
|
|
16
|
+
* multi-line syntax in the user command.
|
|
17
|
+
*
|
|
18
|
+
* The command is single-quoted (with embedded single quotes escaped via the
|
|
19
|
+
* classic `'"'"'` sequence) and run through `eval`. Because the entire user
|
|
20
|
+
* command becomes a single literal argument to `eval`, any here-doc inside it
|
|
21
|
+
* only opens/closes during eval's own second parse pass and cannot leak out to
|
|
22
|
+
* clobber the trailing `&& pwd -P`. Works on sh/dash/bash and Git Bash.
|
|
23
|
+
*/
|
|
24
|
+
function wrapCommandForCwdTracking(command, cwdFileForBash) {
|
|
25
|
+
const escaped = command.replace(/'/g, `'"'"'`);
|
|
26
|
+
return `eval '${escaped}' && pwd -P >| ${cwdFileForBash}`;
|
|
27
|
+
}
|
|
13
28
|
// Commands that should not be auto-backgrounded on timeout (e.g. sleep should just be killed)
|
|
14
29
|
const DISALLOWED_AUTO_BACKGROUND_COMMANDS = ["sleep"];
|
|
15
30
|
function isAutobackgroundingAllowed(command) {
|
|
@@ -213,7 +228,7 @@ The working directory persists between commands. Try to maintain your current wo
|
|
|
213
228
|
// Create a temporary file to store the CWD
|
|
214
229
|
const tempCwdFile = path.join(os.tmpdir(), `wave_cwd_${Date.now()}_${Math.random().toString(36).substring(2, 11)}.tmp`);
|
|
215
230
|
const tempCwdFileForBash = toPosixPath(tempCwdFile);
|
|
216
|
-
const wrappedCommand =
|
|
231
|
+
const wrappedCommand = wrapCommandForCwdTracking(command, tempCwdFileForBash);
|
|
217
232
|
const child = spawn(wrappedCommand, {
|
|
218
233
|
shell: shellPath || true,
|
|
219
234
|
stdio: "pipe",
|
package/dist/types/config.d.ts
CHANGED
|
@@ -11,12 +11,19 @@ export interface GatewayConfig {
|
|
|
11
11
|
fetchOptions?: OpenAI["fetchOptions"];
|
|
12
12
|
fetch?: OpenAI["fetch"];
|
|
13
13
|
}
|
|
14
|
+
export interface ModelCapabilities {
|
|
15
|
+
/** Whether the model supports image/vision input. Default: true. */
|
|
16
|
+
vision?: boolean;
|
|
17
|
+
/** Whether the model supports prompt caching (ephemeral cache_control markers). Default: false. */
|
|
18
|
+
promptCaching?: boolean;
|
|
19
|
+
}
|
|
14
20
|
export interface ModelConfig {
|
|
15
21
|
model?: string;
|
|
16
22
|
fastModel?: string;
|
|
17
23
|
maxTokens?: number;
|
|
18
24
|
permissionMode?: PermissionMode;
|
|
19
25
|
fastModelConfig?: Record<string, unknown>;
|
|
26
|
+
capabilities?: ModelCapabilities;
|
|
20
27
|
[key: string]: unknown;
|
|
21
28
|
}
|
|
22
29
|
//# sourceMappingURL=config.d.ts.map
|