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.
Files changed (99) hide show
  1. package/builtin/skills/settings/ENV.md +0 -1
  2. package/builtin/skills/settings/HOOKS.md +10 -0
  3. package/builtin/skills/settings/MODELS.md +37 -0
  4. package/builtin/skills/settings/SKILLS.md +6 -4
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +12 -5
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/managers/aiManager.d.ts.map +1 -1
  11. package/dist/managers/aiManager.js +20 -4
  12. package/dist/managers/mcpManager.d.ts.map +1 -1
  13. package/dist/managers/mcpManager.js +7 -2
  14. package/dist/managers/messageManager.d.ts +3 -5
  15. package/dist/managers/messageManager.d.ts.map +1 -1
  16. package/dist/managers/messageManager.js +17 -77
  17. package/dist/managers/skillManager.d.ts +1 -0
  18. package/dist/managers/skillManager.d.ts.map +1 -1
  19. package/dist/managers/skillManager.js +11 -2
  20. package/dist/prompts/index.d.ts +3 -2
  21. package/dist/prompts/index.d.ts.map +1 -1
  22. package/dist/prompts/index.js +5 -1
  23. package/dist/services/aiService.d.ts.map +1 -1
  24. package/dist/services/aiService.js +13 -9
  25. package/dist/services/configurationService.d.ts.map +1 -1
  26. package/dist/services/configurationService.js +22 -9
  27. package/dist/services/hook.d.ts.map +1 -1
  28. package/dist/services/hook.js +6 -0
  29. package/dist/services/initializationService.d.ts.map +1 -1
  30. package/dist/services/initializationService.js +1 -1
  31. package/dist/services/interactionService.js +1 -1
  32. package/dist/services/reversionService.d.ts +5 -1
  33. package/dist/services/reversionService.d.ts.map +1 -1
  34. package/dist/services/reversionService.js +50 -22
  35. package/dist/services/session.d.ts +4 -13
  36. package/dist/services/session.d.ts.map +1 -1
  37. package/dist/services/session.js +59 -215
  38. package/dist/telemetry/sessionTracing.d.ts +14 -4
  39. package/dist/telemetry/sessionTracing.d.ts.map +1 -1
  40. package/dist/telemetry/sessionTracing.js +134 -8
  41. package/dist/tools/bashTool.d.ts.map +1 -1
  42. package/dist/tools/bashTool.js +16 -1
  43. package/dist/types/config.d.ts +7 -0
  44. package/dist/types/config.d.ts.map +1 -1
  45. package/dist/types/hooks.d.ts +1 -0
  46. package/dist/types/hooks.d.ts.map +1 -1
  47. package/dist/types/messaging.d.ts +0 -1
  48. package/dist/types/messaging.d.ts.map +1 -1
  49. package/dist/types/skills.d.ts +1 -0
  50. package/dist/types/skills.d.ts.map +1 -1
  51. package/dist/types/telemetry.d.ts +14 -1
  52. package/dist/types/telemetry.d.ts.map +1 -1
  53. package/dist/utils/atomicWrite.d.ts +15 -0
  54. package/dist/utils/atomicWrite.d.ts.map +1 -0
  55. package/dist/utils/atomicWrite.js +59 -0
  56. package/dist/utils/cacheControlUtils.d.ts +3 -15
  57. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  58. package/dist/utils/cacheControlUtils.js +4 -35
  59. package/dist/utils/containerSetup.d.ts.map +1 -1
  60. package/dist/utils/containerSetup.js +5 -1
  61. package/dist/utils/convertMessagesForAPI.d.ts +9 -1
  62. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  63. package/dist/utils/convertMessagesForAPI.js +74 -55
  64. package/dist/utils/mcpUtils.d.ts.map +1 -1
  65. package/dist/utils/mcpUtils.js +2 -1
  66. package/dist/utils/modelCapabilities.d.ts +22 -0
  67. package/dist/utils/modelCapabilities.d.ts.map +1 -0
  68. package/dist/utils/modelCapabilities.js +24 -0
  69. package/dist/utils/skillParser.d.ts.map +1 -1
  70. package/dist/utils/skillParser.js +3 -1
  71. package/package.json +1 -1
  72. package/src/agent.ts +11 -5
  73. package/src/index.ts +1 -0
  74. package/src/managers/aiManager.ts +22 -2
  75. package/src/managers/mcpManager.ts +8 -2
  76. package/src/managers/messageManager.ts +22 -104
  77. package/src/managers/skillManager.ts +16 -2
  78. package/src/prompts/index.ts +6 -1
  79. package/src/services/aiService.ts +12 -5
  80. package/src/services/configurationService.ts +24 -24
  81. package/src/services/hook.ts +7 -0
  82. package/src/services/initializationService.ts +1 -3
  83. package/src/services/interactionService.ts +1 -1
  84. package/src/services/reversionService.ts +50 -27
  85. package/src/services/session.ts +63 -262
  86. package/src/telemetry/sessionTracing.ts +167 -12
  87. package/src/tools/bashTool.ts +23 -1
  88. package/src/types/config.ts +8 -0
  89. package/src/types/hooks.ts +1 -0
  90. package/src/types/messaging.ts +0 -1
  91. package/src/types/skills.ts +1 -0
  92. package/src/types/telemetry.ts +17 -1
  93. package/src/utils/atomicWrite.ts +61 -0
  94. package/src/utils/cacheControlUtils.ts +5 -38
  95. package/src/utils/containerSetup.ts +5 -1
  96. package/src/utils/convertMessagesForAPI.ts +87 -57
  97. package/src/utils/mcpUtils.ts +2 -1
  98. package/src/utils/modelCapabilities.ts +30 -0
  99. package/src/utils/skillParser.ts +3 -1
@@ -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", rootSessionId, parentSessionId) {
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 messages = await jsonlHandler.read(filePath);
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, // No longer stored in metadata
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 the fallback/rebuild case
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
- const sortedSessions = sessions.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
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
- // Try to read from index first
466
- const indexPath = join(projectPath, SESSION_INDEX_FILENAME);
467
- try {
468
- const indexContent = await fs.readFile(indexPath, "utf8");
469
- const index = JSON.parse(indexContent);
470
- for (const [id, meta] of Object.entries(index.sessions)) {
471
- const lastActiveAt = new Date(meta.lastActiveAt);
472
- if (meta.sessionType === "main" && lastActiveAt >= sevenDaysAgo) {
473
- allSessions.push({
474
- id,
475
- ...meta,
476
- createdAt: new Date(meta.createdAt),
477
- lastActiveAt,
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 by following parentSessionId links
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 sessionIds = [];
788
- let currentId = currentSessionId;
789
- const allMessages = [];
790
- while (currentId) {
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;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAM/C,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAkB9E;;;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,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7B,IAAI,GAAG,SAAS,CAsBlB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,IAAI,GAAG,SAAS,EACtB,QAAQ,EAAE,kBAAkB,GAC3B,IAAI,CAmBN;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,KAAK,CAAC,EAAE,OAAO,GACd,IAAI,GAAG,SAAS,CA4BlB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAmBxD"}
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
- let inputStr = typeof input === "string" ? input : JSON.stringify(input);
126
- if (inputStr.length > 1000) {
127
- inputStr = inputStr.substring(0, 1000);
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
- let outputStr = metadata.output;
159
- if (outputStr.length > 1000) {
160
- outputStr = outputStr.substring(0, 1000);
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;AAuBtE;;GAEG;AACH,eAAO,MAAM,QAAQ,EAAE,UA2iBtB,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"}
@@ -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 = `${command} && pwd -P >| ${tempCwdFileForBash}`;
231
+ const wrappedCommand = wrapCommandForCwdTracking(command, tempCwdFileForBash);
217
232
  const child = spawn(wrappedCommand, {
218
233
  shell: shellPath || true,
219
234
  stdio: "pipe",
@@ -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