wave-agent-sdk 0.18.7 → 0.19.1

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 (100) 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 +55 -10
  4. package/builtin/skills/settings/SKILL.md +8 -4
  5. package/builtin/skills/settings/SKILLS.md +6 -4
  6. package/dist/agent.d.ts.map +1 -1
  7. package/dist/agent.js +12 -5
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -0
  11. package/dist/managers/aiManager.d.ts.map +1 -1
  12. package/dist/managers/aiManager.js +20 -4
  13. package/dist/managers/mcpManager.d.ts.map +1 -1
  14. package/dist/managers/mcpManager.js +7 -2
  15. package/dist/managers/messageManager.d.ts +3 -5
  16. package/dist/managers/messageManager.d.ts.map +1 -1
  17. package/dist/managers/messageManager.js +17 -77
  18. package/dist/managers/skillManager.d.ts +1 -0
  19. package/dist/managers/skillManager.d.ts.map +1 -1
  20. package/dist/managers/skillManager.js +11 -2
  21. package/dist/prompts/index.d.ts +3 -2
  22. package/dist/prompts/index.d.ts.map +1 -1
  23. package/dist/prompts/index.js +5 -1
  24. package/dist/services/aiService.d.ts.map +1 -1
  25. package/dist/services/aiService.js +16 -47
  26. package/dist/services/configurationService.d.ts.map +1 -1
  27. package/dist/services/configurationService.js +25 -19
  28. package/dist/services/hook.d.ts.map +1 -1
  29. package/dist/services/hook.js +6 -0
  30. package/dist/services/initializationService.d.ts.map +1 -1
  31. package/dist/services/initializationService.js +1 -1
  32. package/dist/services/interactionService.js +1 -1
  33. package/dist/services/reversionService.d.ts +5 -1
  34. package/dist/services/reversionService.d.ts.map +1 -1
  35. package/dist/services/reversionService.js +50 -22
  36. package/dist/services/session.d.ts +4 -13
  37. package/dist/services/session.d.ts.map +1 -1
  38. package/dist/services/session.js +59 -215
  39. package/dist/telemetry/sessionTracing.d.ts +14 -4
  40. package/dist/telemetry/sessionTracing.d.ts.map +1 -1
  41. package/dist/telemetry/sessionTracing.js +134 -8
  42. package/dist/tools/bashTool.d.ts.map +1 -1
  43. package/dist/tools/bashTool.js +16 -1
  44. package/dist/types/config.d.ts +11 -2
  45. package/dist/types/config.d.ts.map +1 -1
  46. package/dist/types/hooks.d.ts +1 -0
  47. package/dist/types/hooks.d.ts.map +1 -1
  48. package/dist/types/messaging.d.ts +0 -1
  49. package/dist/types/messaging.d.ts.map +1 -1
  50. package/dist/types/skills.d.ts +1 -0
  51. package/dist/types/skills.d.ts.map +1 -1
  52. package/dist/types/telemetry.d.ts +14 -1
  53. package/dist/types/telemetry.d.ts.map +1 -1
  54. package/dist/utils/atomicWrite.d.ts +15 -0
  55. package/dist/utils/atomicWrite.d.ts.map +1 -0
  56. package/dist/utils/atomicWrite.js +59 -0
  57. package/dist/utils/cacheControlUtils.d.ts +3 -15
  58. package/dist/utils/cacheControlUtils.d.ts.map +1 -1
  59. package/dist/utils/cacheControlUtils.js +4 -35
  60. package/dist/utils/containerSetup.d.ts.map +1 -1
  61. package/dist/utils/containerSetup.js +5 -1
  62. package/dist/utils/convertMessagesForAPI.d.ts +9 -1
  63. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  64. package/dist/utils/convertMessagesForAPI.js +74 -55
  65. package/dist/utils/mcpUtils.d.ts.map +1 -1
  66. package/dist/utils/mcpUtils.js +2 -1
  67. package/dist/utils/modelCapabilities.d.ts +22 -0
  68. package/dist/utils/modelCapabilities.d.ts.map +1 -0
  69. package/dist/utils/modelCapabilities.js +24 -0
  70. package/dist/utils/skillParser.d.ts.map +1 -1
  71. package/dist/utils/skillParser.js +3 -1
  72. package/package.json +1 -1
  73. package/src/agent.ts +11 -5
  74. package/src/index.ts +1 -0
  75. package/src/managers/aiManager.ts +22 -2
  76. package/src/managers/mcpManager.ts +8 -2
  77. package/src/managers/messageManager.ts +22 -104
  78. package/src/managers/skillManager.ts +16 -2
  79. package/src/prompts/index.ts +6 -1
  80. package/src/services/aiService.ts +15 -85
  81. package/src/services/configurationService.ts +27 -41
  82. package/src/services/hook.ts +7 -0
  83. package/src/services/initializationService.ts +1 -3
  84. package/src/services/interactionService.ts +1 -1
  85. package/src/services/reversionService.ts +50 -27
  86. package/src/services/session.ts +63 -262
  87. package/src/telemetry/sessionTracing.ts +167 -12
  88. package/src/tools/bashTool.ts +23 -1
  89. package/src/types/config.ts +12 -2
  90. package/src/types/hooks.ts +1 -0
  91. package/src/types/messaging.ts +0 -1
  92. package/src/types/skills.ts +1 -0
  93. package/src/types/telemetry.ts +17 -1
  94. package/src/utils/atomicWrite.ts +61 -0
  95. package/src/utils/cacheControlUtils.ts +5 -38
  96. package/src/utils/containerSetup.ts +5 -1
  97. package/src/utils/convertMessagesForAPI.ts +87 -57
  98. package/src/utils/mcpUtils.ts +2 -1
  99. package/src/utils/modelCapabilities.ts +30 -0
  100. package/src/utils/skillParser.ts +3 -1
@@ -36,9 +36,11 @@ function safeToolArguments(args) {
36
36
  * Convert message format to API call format, stopping when a compacted message is encountered.
37
37
  * Messages with no meaningful content or tool calls are filtered out.
38
38
  * @param messages Message list
39
+ * @param options Optional conversion options (e.g. supportsVision)
39
40
  * @returns Converted API message format list
40
41
  */
41
- export function convertMessagesForAPI(messages) {
42
+ export function convertMessagesForAPI(messages, options) {
43
+ const supportsVision = options?.supportsVision !== false;
42
44
  const recentMessages = [];
43
45
  const startIndex = messages.length - 1;
44
46
  for (let i = startIndex; i >= 0; i--) {
@@ -66,49 +68,57 @@ export function convertMessagesForAPI(messages) {
66
68
  const toolBlocks = message.blocks.filter((block) => block.type === "tool");
67
69
  const completedToolIds = new Set(); // Record completed tool IDs
68
70
  if (toolBlocks.length > 0) {
71
+ // Collect image user messages to place after all tool messages
72
+ const imageUserMessages = [];
69
73
  toolBlocks.forEach((toolBlock) => {
70
74
  // Only add completed tool blocks (i.e., stage is 'end')
71
75
  if (toolBlock.id && toolBlock.stage === "end") {
72
76
  completedToolIds.add(toolBlock.id);
73
- // Check for image data
77
+ // Always add a proper tool message to satisfy tool_call_id pairing
78
+ recentMessages.unshift({
79
+ tool_call_id: toolBlock.id,
80
+ role: "tool",
81
+ content: stripAnsiColors(toolBlock.result || ""),
82
+ });
83
+ // If there are images, also prepare a user message with image content
74
84
  if (toolBlock.images && toolBlock.images.length > 0) {
75
- // If there is an image, create a user message instead of a tool message
76
- const contentParts = [];
77
- // Add tool result as text
78
- const toolResultText = `Tool result for ${toolBlock.name || "unknown tool"}:\n${stripAnsiColors(toolBlock.result || "")}`;
79
- contentParts.push({
80
- type: "text",
81
- text: toolResultText,
82
- });
83
- // Add image
84
- toolBlock.images.forEach((image) => {
85
- const imageUrl = image.data.startsWith("data:")
86
- ? image.data
87
- : `data:${image.mediaType || "image/png"};base64,${image.data}`;
88
- contentParts.push({
89
- type: "image_url",
90
- image_url: {
91
- url: imageUrl,
92
- detail: "auto",
93
- },
85
+ if (supportsVision) {
86
+ const contentParts = [];
87
+ toolBlock.images.forEach((image) => {
88
+ const imageUrl = image.data.startsWith("data:")
89
+ ? image.data
90
+ : `data:${image.mediaType || "image/png"};base64,${image.data}`;
91
+ contentParts.push({
92
+ type: "image_url",
93
+ image_url: {
94
+ url: imageUrl,
95
+ detail: "auto",
96
+ },
97
+ });
94
98
  });
95
- });
96
- // Add user message
97
- recentMessages.unshift({
98
- role: "user",
99
- content: contentParts,
100
- });
101
- }
102
- else {
103
- // Normal tool message
104
- recentMessages.unshift({
105
- tool_call_id: toolBlock.id,
106
- role: "tool",
107
- content: stripAnsiColors(toolBlock.result || ""),
108
- });
99
+ imageUserMessages.push({
100
+ role: "user",
101
+ content: contentParts,
102
+ });
103
+ }
104
+ else {
105
+ // Non-vision model: replace images with a text placeholder
106
+ imageUserMessages.push({
107
+ role: "user",
108
+ content: "[Tool returned an image, but the current model does not support image recognition]",
109
+ });
110
+ }
109
111
  }
110
112
  }
111
113
  });
114
+ // Insert image user messages after all tool messages but before the
115
+ // assistant message (which will be unshifted next). Since tool messages
116
+ // were unshifted to the front, we splice images right after them.
117
+ if (imageUserMessages.length > 0) {
118
+ // Tool messages are at indices 0..N-1, existing messages start at N
119
+ const toolMsgCount = completedToolIds.size;
120
+ recentMessages.splice(toolMsgCount, 0, ...imageUserMessages);
121
+ }
112
122
  }
113
123
  // Construct the content of the assistant message
114
124
  let content = "";
@@ -180,28 +190,37 @@ export function convertMessagesForAPI(messages) {
180
190
  if (block.type === "image" &&
181
191
  block.imageUrls &&
182
192
  block.imageUrls.length > 0) {
183
- block.imageUrls.forEach((imageUrl) => {
184
- // Check if it's already base64, convert if not
185
- let finalImageUrl = imageUrl;
186
- if (!imageUrl.startsWith("data:image/")) {
187
- // If it's a file path, it needs to be converted to base64
188
- try {
189
- finalImageUrl = convertImageToBase64(imageUrl);
190
- }
191
- catch (error) {
192
- logger.error("Failed to convert image path to base64:", imageUrl, error);
193
- // Skip this image, do not add to content
194
- return;
195
- }
196
- }
193
+ if (!supportsVision) {
194
+ // Non-vision model: replace images with a text placeholder
197
195
  contentParts.push({
198
- type: "image_url",
199
- image_url: {
200
- url: finalImageUrl,
201
- detail: "auto",
202
- },
196
+ type: "text",
197
+ text: "[User shared an image, but the current model does not support image recognition]",
203
198
  });
204
- });
199
+ }
200
+ else {
201
+ block.imageUrls.forEach((imageUrl) => {
202
+ // Check if it's already base64, convert if not
203
+ let finalImageUrl = imageUrl;
204
+ if (!imageUrl.startsWith("data:image/")) {
205
+ // If it's a file path, it needs to be converted to base64
206
+ try {
207
+ finalImageUrl = convertImageToBase64(imageUrl);
208
+ }
209
+ catch (error) {
210
+ logger.error("Failed to convert image path to base64:", imageUrl, error);
211
+ // Skip this image, do not add to content
212
+ return;
213
+ }
214
+ }
215
+ contentParts.push({
216
+ type: "image_url",
217
+ image_url: {
218
+ url: finalImageUrl,
219
+ detail: "auto",
220
+ },
221
+ });
222
+ });
223
+ }
205
224
  }
206
225
  // If there is a tool block in user message, add its result
207
226
  if (block.type === "tool" && block.stage === "end" && block.result) {
@@ -1 +1 @@
1
- {"version":3,"file":"mcpUtils.d.ts","sourceRoot":"","sources":["../../src/utils/mcpUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAgDlE;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,GACjB,0BAA0B,CAgB5B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,CACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,WAAW,KAClB,OAAO,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtD,CAAC,GACD,UAAU,CAiCZ;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,eAAe,EAAE,GACzB,eAAe,GAAG,SAAS,CAK7B"}
1
+ {"version":3,"file":"mcpUtils.d.ts","sourceRoot":"","sources":["../../src/utils/mcpUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAgDlE;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,GACjB,0BAA0B,CAgB5B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,CACX,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,WAAW,KAClB,OAAO,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACtD,CAAC,GACD,UAAU,CAiCZ;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,eAAe,EAAE,GACzB,eAAe,GAAG,SAAS,CAM7B"}
@@ -84,5 +84,6 @@ export function createMcpToolPlugin(mcpTool, serverName, executeTool) {
84
84
  * Find which server a tool belongs to
85
85
  */
86
86
  export function findToolServer(toolName, servers) {
87
- return servers.find((s) => s.status === "connected" && s.tools?.some((t) => t.name === toolName));
87
+ return servers.find((s) => (s.status === "connected" || s.status === "reconnecting") &&
88
+ s.tools?.some((t) => t.name === toolName));
88
89
  }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Model Capability Detection
3
+ *
4
+ * Pure declarative capability accessors. Capabilities are configured per-model
5
+ * via the `capabilities` field in `ModelConfig` (settings.json), not via regex
6
+ * or environment variables.
7
+ */
8
+ import type { ModelCapabilities } from "../types/config.js";
9
+ /**
10
+ * Determines if a model supports prompt caching.
11
+ * @param capabilities - Declarative model capabilities
12
+ * @returns True if promptCaching is explicitly enabled (default: false)
13
+ */
14
+ export declare function supportsPromptCaching(capabilities?: ModelCapabilities): boolean;
15
+ /**
16
+ * Determines if a model supports image/vision recognition.
17
+ * Opt-out semantics: by default all models are assumed to support vision.
18
+ * @param capabilities - Declarative model capabilities
19
+ * @returns True unless vision is explicitly disabled (default: true)
20
+ */
21
+ export declare function supportsVision(capabilities?: ModelCapabilities): boolean;
22
+ //# sourceMappingURL=modelCapabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelCapabilities.d.ts","sourceRoot":"","sources":["../../src/utils/modelCapabilities.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,CAAC,EAAE,iBAAiB,GAC/B,OAAO,CAET;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,YAAY,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAExE"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Model Capability Detection
3
+ *
4
+ * Pure declarative capability accessors. Capabilities are configured per-model
5
+ * via the `capabilities` field in `ModelConfig` (settings.json), not via regex
6
+ * or environment variables.
7
+ */
8
+ /**
9
+ * Determines if a model supports prompt caching.
10
+ * @param capabilities - Declarative model capabilities
11
+ * @returns True if promptCaching is explicitly enabled (default: false)
12
+ */
13
+ export function supportsPromptCaching(capabilities) {
14
+ return capabilities?.promptCaching ?? false;
15
+ }
16
+ /**
17
+ * Determines if a model supports image/vision recognition.
18
+ * Opt-out semantics: by default all models are assumed to support vision.
19
+ * @param capabilities - Declarative model capabilities
20
+ * @returns True unless vision is explicitly disabled (default: true)
21
+ */
22
+ export function supportsVision(capabilities) {
23
+ return capabilities?.vision ?? true;
24
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"skillParser.d.ts","sourceRoot":"","sources":["../../src/utils/skillParser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EAEjB,aAAa,EACd,MAAM,mBAAmB,CAAC;AAE3B;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,iBAAsB,GAC9B,eAAe,CAkGjB;AAyDD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,EAAE,CAwCvE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAa5E"}
1
+ {"version":3,"file":"skillParser.d.ts","sourceRoot":"","sources":["../../src/utils/skillParser.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,eAAe,EACf,iBAAiB,EAEjB,aAAa,EACd,MAAM,mBAAmB,CAAC;AAE3B;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,iBAAsB,GAC9B,eAAe,CAoGjB;AAyDD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,EAAE,CAwCvE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAa5E"}
@@ -38,7 +38,9 @@ export function parseSkillFile(filePath, options = {}) {
38
38
  const skillType = skillPath.includes("/.wave/skills") ||
39
39
  skillPath.includes("\\.wave\\skills") ||
40
40
  skillPath.includes("/.claude/skills") ||
41
- skillPath.includes("\\.claude\\skills")
41
+ skillPath.includes("\\.claude\\skills") ||
42
+ skillPath.includes("/.agents/skills") ||
43
+ skillPath.includes("\\.agents\\skills")
42
44
  ? "project"
43
45
  : "personal";
44
46
  // Extract allowed tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-agent-sdk",
3
- "version": "0.18.7",
3
+ "version": "0.19.1",
4
4
  "description": "SDK for building AI-powered development tools and agents",
5
5
  "keywords": [
6
6
  "ai",
package/src/agent.ts CHANGED
@@ -41,6 +41,7 @@ import { SkillManager } from "./managers/skillManager.js";
41
41
  import { TaskManager } from "./services/taskManager.js";
42
42
  import { btw } from "./services/aiService.js";
43
43
  import { convertMessagesForAPI } from "./utils/convertMessagesForAPI.js";
44
+ import { supportsVision } from "./utils/modelCapabilities.js";
44
45
  import { parseTaskNotificationXml } from "./utils/notificationXml.js";
45
46
  import { InitializationService } from "./services/initializationService.js";
46
47
  import { InteractionService } from "./services/interactionService.js";
@@ -598,17 +599,20 @@ export class Agent {
598
599
  options,
599
600
  );
600
601
 
601
- // Initialize OpenTelemetry (non-blocking, graceful degradation)
602
+ // Initialize OpenTelemetry (awaited to ensure ALS context is ready before
603
+ // startInteractionSpan is called, preventing trace context fragmentation)
602
604
  const telemetryConfig = this.configurationService.resolveTelemetryConfig();
603
- initializeTelemetry(telemetryConfig).catch((error) => {
605
+ try {
606
+ await initializeTelemetry(telemetryConfig);
607
+ } catch (error) {
604
608
  this.logger?.warn("Telemetry initialization failed:", error);
605
- });
609
+ }
606
610
 
607
611
  // Log session_start event
608
612
  try {
609
613
  const modelConfig = this.getModelConfig();
610
614
  if (modelConfig.model) {
611
- logOTelEvent("session_start", {
615
+ await logOTelEvent("session_start", {
612
616
  sessionId: this.messageManager.getSessionId(),
613
617
  model: modelConfig.model,
614
618
  workdir: this.workdir,
@@ -992,7 +996,9 @@ export class Agent {
992
996
  * @returns Promise that resolves to the AI's answer
993
997
  */
994
998
  public async askBtw(question: string): Promise<string> {
995
- const messages = convertMessagesForAPI(this.messageManager.getMessages());
999
+ const messages = convertMessagesForAPI(this.messageManager.getMessages(), {
1000
+ supportsVision: supportsVision(this.getModelConfig().capabilities),
1001
+ });
996
1002
  const result = await btw({
997
1003
  gatewayConfig: this.getGatewayConfig(),
998
1004
  modelConfig: this.getModelConfig(),
package/src/index.ts CHANGED
@@ -14,6 +14,7 @@ export * from "./managers/messageQueue.js";
14
14
  // Export all utilities
15
15
  export * from "./utils/bashParser.js";
16
16
  export * from "./utils/convertMessagesForAPI.js";
17
+ export * from "./utils/modelCapabilities.js";
17
18
  export * from "./utils/fileSearch.js";
18
19
  export * from "./utils/globalLogger.js";
19
20
  export * from "./utils/mcpUtils.js";
@@ -1,6 +1,7 @@
1
1
  import { type CallAgentOptions } from "../services/aiService.js";
2
2
  import * as aiService from "../services/aiService.js";
3
3
  import { convertMessagesForAPI } from "../utils/convertMessagesForAPI.js";
4
+ import { supportsVision } from "../utils/modelCapabilities.js";
4
5
  import { parseTaskNotificationXml } from "../utils/notificationXml.js";
5
6
  import { calculateComprehensiveTotalTokens } from "../utils/tokenCalculation.js";
6
7
  import { estimateTokens } from "../utils/tokenEstimate.js";
@@ -45,6 +46,7 @@ import {
45
46
  endInteractionSpan,
46
47
  startLLMRequestSpan,
47
48
  endLLMRequestSpan,
49
+ resetTracingState,
48
50
  } from "../telemetry/sessionTracing.js";
49
51
  import { logOTelEvent } from "../telemetry/events.js";
50
52
 
@@ -492,7 +494,7 @@ export class AIManager {
492
494
  );
493
495
 
494
496
  // 7. Execute message reconstruction
495
- this.messageManager.compactMessagesAndUpdateSession(
497
+ await this.messageManager.compactMessagesAndUpdateSession(
496
498
  enhancedSummary,
497
499
  compactUsage,
498
500
  );
@@ -523,6 +525,9 @@ export class AIManager {
523
525
 
524
526
  this.consecutiveCompactionFailures = 0;
525
527
 
528
+ // Reset incremental tracing state after compaction
529
+ resetTracingState();
530
+
526
531
  // 9. Log OTEL event
527
532
  logOTelEvent("compaction", {
528
533
  beforeTokens: String(messagesToCompact.length),
@@ -838,7 +843,10 @@ export class AIManager {
838
843
 
839
844
  // Get recent message history
840
845
  const rawMessages = this.messageManager.getMessages();
841
- const recentMessages = convertMessagesForAPI(rawMessages);
846
+ const currentModelConfig = this.getModelConfig();
847
+ const recentMessages = convertMessagesForAPI(rawMessages, {
848
+ supportsVision: supportsVision(currentModelConfig.capabilities),
849
+ });
842
850
 
843
851
  // Track if assistant message has been created
844
852
  let assistantMessageCreated = false;
@@ -953,6 +961,17 @@ export class AIManager {
953
961
 
954
962
  llmSpan = startLLMRequestSpan(
955
963
  model || this.getModelConfig().model || "",
964
+ {
965
+ context: "interaction",
966
+ systemPrompt:
967
+ typeof callAgentOptions.systemPrompt === "string"
968
+ ? callAgentOptions.systemPrompt
969
+ : undefined,
970
+ inputMessages: callAgentOptions.messages,
971
+ toolsSchema: callAgentOptions.tools
972
+ ? JSON.stringify(callAgentOptions.tools)
973
+ : undefined,
974
+ },
956
975
  );
957
976
 
958
977
  const result = await aiService.callAgent(callAgentOptions);
@@ -966,6 +985,7 @@ export class AIManager {
966
985
  outputTokens: result.usage?.completion_tokens,
967
986
  cacheReadTokens: result.usage?.cache_read_input_tokens,
968
987
  cacheCreationTokens: result.usage?.cache_creation_input_tokens,
988
+ modelOutput: result.content,
969
989
  });
970
990
 
971
991
  const createdByStreaming = assistantMessageCreated;
@@ -725,11 +725,17 @@ export class McpManager {
725
725
  }
726
726
  }
727
727
 
728
- // Get all tools from connected servers
728
+ // Get all tools from connected servers.
729
+ // "reconnecting" servers retain their last-known tool snapshot so that
730
+ // transient SSE disconnects don't churn the tool list (which would bust
731
+ // the prompt cache). Tools are only dropped once the server truly fails
732
+ // (status "error"/"disconnected").
729
733
  getAllConnectedTools(): McpTool[] {
730
734
  const allTools: McpTool[] = [];
731
735
  for (const server of this.servers.values()) {
732
- if (server.status === "connected" && server.tools) {
736
+ const usable =
737
+ server.status === "connected" || server.status === "reconnecting";
738
+ if (usable && server.tools) {
733
739
  allTools.push(...server.tools);
734
740
  }
735
741
  }
@@ -94,8 +94,6 @@ export interface MessageManagerOptions {
94
94
  export class MessageManager {
95
95
  // Private state properties
96
96
  private sessionId: string;
97
- private rootSessionId: string;
98
- private parentSessionId?: string;
99
97
  private messages: Message[];
100
98
  private latestTotalTokens: number;
101
99
  private workdir: string;
@@ -118,7 +116,6 @@ export class MessageManager {
118
116
  options: MessageManagerOptions,
119
117
  ) {
120
118
  this.sessionId = generateSessionId();
121
- this.rootSessionId = this.sessionId;
122
119
  this.messages = [];
123
120
  this.latestTotalTokens = 0;
124
121
  this.workdir = options.workdir;
@@ -152,11 +149,7 @@ export class MessageManager {
152
149
  }
153
150
 
154
151
  public getRootSessionId(): string {
155
- return this.rootSessionId;
156
- }
157
-
158
- public getParentSessionId(): string | undefined {
159
- return this.parentSessionId;
152
+ return this.sessionId;
160
153
  }
161
154
 
162
155
  public getMessages(): Message[] {
@@ -358,8 +351,6 @@ export class MessageManager {
358
351
  unsavedMessages,
359
352
  this.workdir,
360
353
  this.sessionType,
361
- this.rootSessionId,
362
- this.parentSessionId,
363
354
  );
364
355
 
365
356
  // Update the saved message count
@@ -383,7 +374,6 @@ export class MessageManager {
383
374
  public clearMessages(): void {
384
375
  this.setMessages([]);
385
376
  const newSessionId = generateSessionId();
386
- this.rootSessionId = newSessionId;
387
377
  this.setSessionId(newSessionId);
388
378
  this.setlatestTotalTokens(0);
389
379
  this.savedMessageCount = 0; // Reset saved message count
@@ -399,8 +389,6 @@ export class MessageManager {
399
389
  // Initialize state from session data
400
390
  public initializeFromSession(sessionData: SessionData): void {
401
391
  this.setSessionId(sessionData.id);
402
- this.rootSessionId = sessionData.rootSessionId || sessionData.id;
403
- this.parentSessionId = sessionData.parentSessionId;
404
392
  this.setMessages([...sessionData.messages]);
405
393
  this.setlatestTotalTokens(sessionData.metadata.latestTotalTokens);
406
394
 
@@ -563,12 +551,13 @@ export class MessageManager {
563
551
  }
564
552
 
565
553
  /**
566
- * Compact messages and update session, delete compacted messages, only keep compacted messages and last 3 messages
554
+ * Compact messages and update session append compact message to same file (append-only).
555
+ * After compaction, in-memory messages are [compactMessage, ...lastThreeMessages].
567
556
  */
568
- public compactMessagesAndUpdateSession(
557
+ public async compactMessagesAndUpdateSession(
569
558
  compactedContent: string,
570
559
  usage?: Usage,
571
- ): void {
560
+ ): Promise<void> {
572
561
  // Get last 2 API rounds to preserve (structurally safe boundary)
573
562
  const lastThreeMessages = getLastApiRounds(this.messages, 2);
574
563
 
@@ -580,23 +569,27 @@ export class MessageManager {
580
569
  {
581
570
  type: "compact",
582
571
  content: compactedContent,
583
- sessionId: this.sessionId,
584
572
  },
585
573
  ],
586
574
  timestamp: new Date().toISOString(),
587
575
  ...(usage && { usage }),
588
576
  };
589
577
 
590
- // Build new message array: keep the compacted message and last 3 messages
578
+ // Build new message array: keep the compacted message and last messages
591
579
  const newMessages: Message[] = [compactMessage, ...lastThreeMessages];
592
580
 
593
- // Update sessionId and parentSessionId
594
- const oldSessionId = this.sessionId;
595
- this.setSessionId(generateSessionId());
596
- this.parentSessionId = oldSessionId;
581
+ // APPEND to the same session file (append-only, like Claude Code)
582
+ const { appendMessages } = await import("../services/session.js");
583
+ await appendMessages(
584
+ this.sessionId,
585
+ newMessages,
586
+ this.workdir,
587
+ this.sessionType,
588
+ );
597
589
 
598
- // Set new message list
590
+ // Update in-memory state
599
591
  this.setMessages(newMessages);
592
+ this.savedMessageCount = newMessages.length;
600
593
 
601
594
  // Clear and rebuild loaded rule IDs from remaining meta messages
602
595
  this.clearLoadedRuleIds();
@@ -911,101 +904,26 @@ export class MessageManager {
911
904
  index: number,
912
905
  reversionManager?: import("./reversionManager.js").ReversionManager,
913
906
  ): Promise<void> {
914
- const { messages, sessionIds } = await this.getFullMessageThread();
907
+ const { messages } = await this.getFullMessageThread();
915
908
 
916
909
  if (index < 0 || index >= messages.length) {
917
910
  throw new Error(`Invalid message index: ${index}`);
918
911
  }
919
912
 
920
- // Find which session the index belongs to
921
- let targetSessionId = this.sessionId;
922
- let targetIndexInSession = index;
923
-
924
- // We need to be careful here because loadFullMessageThread might have removed "compact" blocks
925
- // Let's re-calculate based on the actual messages returned.
926
- // Actually, it's easier to just load sessions one by one again or keep track of counts.
927
-
928
- // For simplicity, let's assume we want to truncate the WHOLE thread.
929
- // If the index is in a previous session, we need to:
930
- // 1. Load that session.
931
- // 2. Truncate it.
932
- // 3. Make it the current session.
933
- // 4. Delete/Invalidate subsequent sessions.
934
-
935
- // To correctly map 'index' to a session, we need to know the message count of each session
936
- // as they appear in the concatenated 'messages' array.
937
-
938
- let remainingIndex = index;
939
- const { loadSessionFromJsonl } = await import("../services/session.js");
940
-
941
- for (const sid of sessionIds) {
942
- const sessionData = await loadSessionFromJsonl(sid, this.workdir);
943
- if (!sessionData) continue;
944
-
945
- const sessionMessages = sessionData.messages;
946
- // If this is not the first session in the thread, it might have a compact block at the start
947
- // that was removed in getFullMessageThread.
948
- const hasCompactBlock = sessionMessages[0]?.blocks.some(
949
- (b) => b.type === "compact",
950
- );
951
- const effectiveMessages =
952
- hasCompactBlock && sid !== sessionIds[0]
953
- ? sessionMessages.slice(1)
954
- : sessionMessages;
955
-
956
- if (remainingIndex < effectiveMessages.length) {
957
- targetSessionId = sid;
958
- targetIndexInSession = hasCompactBlock
959
- ? remainingIndex + 1
960
- : remainingIndex;
961
- break;
962
- }
963
- remainingIndex -= effectiveMessages.length;
964
- }
965
-
966
- // Load the target session to perform truncation
967
- const targetSessionData = await loadSessionFromJsonl(
968
- targetSessionId,
969
- this.workdir,
970
- );
971
- if (!targetSessionData)
972
- throw new Error(`Target session ${targetSessionId} not found`);
973
-
974
- // Identify messages to be removed (from the whole thread)
913
+ const newMessages = messages.slice(0, index);
975
914
  const messagesToRemove = messages.slice(index);
976
915
  const messageIdsToRemove = messagesToRemove
977
916
  .map((m) => m.id as string)
978
917
  .filter((id) => !!id);
979
918
 
980
- // Revert file changes if manager is provided
981
919
  if (reversionManager && messageIdsToRemove.length > 0) {
982
920
  await reversionManager.revertTo(messageIdsToRemove, messages);
983
921
  }
984
922
 
985
- // Truncate messages in the target session
986
- const newMessagesInSession = targetSessionData.messages.slice(
987
- 0,
988
- targetIndexInSession,
989
- );
990
-
991
- // Update target session file
992
- this.sessionId = targetSessionId;
993
- this.rootSessionId = targetSessionData.rootSessionId || targetSessionId;
994
- this.parentSessionId = targetSessionData.parentSessionId;
995
- this.transcriptPath = this.computeTranscriptPath();
996
-
997
- await this.rewriteSessionFile(newMessagesInSession);
998
-
999
- // Update in-memory messages to the truncated session messages
1000
- // We do NOT include ancestor messages here to avoid exceeding context limits.
1001
- // The 'compact' block at the start of the session (if any) already summarizes them.
1002
- this.setMessages(newMessagesInSession);
1003
-
1004
- // Update saved message count
1005
- this.savedMessageCount = newMessagesInSession.length;
1006
-
1007
- // Notify session ID change if it changed
1008
- this.callbacks.onSessionIdChange?.(this.sessionId);
923
+ // Rewrite file with truncated messages
924
+ await this.rewriteSessionFile(newMessages);
925
+ this.setMessages(newMessages);
926
+ this.savedMessageCount = newMessages.length;
1009
927
  }
1010
928
 
1011
929
  /**