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
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
  /**
@@ -30,6 +30,7 @@ import { logger } from "../utils/globalLogger.js";
30
30
  export class SkillManager extends EventEmitter {
31
31
  private personalSkillsPath: string;
32
32
  private personalClaudeSkillsPath: string;
33
+ private personalAgentsSkillsPath: string;
33
34
  private scanTimeout: number;
34
35
  private workdir: string;
35
36
 
@@ -50,6 +51,8 @@ export class SkillManager extends EventEmitter {
50
51
  options.personalSkillsPath || join(homedir(), ".wave", "skills");
51
52
  this.personalClaudeSkillsPath =
52
53
  options.personalClaudeSkillsPath || join(homedir(), ".claude", "skills");
54
+ this.personalAgentsSkillsPath =
55
+ options.personalAgentsSkillsPath || join(homedir(), ".agents", "skills");
53
56
  this.scanTimeout = options.scanTimeout || 5000;
54
57
  this.workdir = options.workdir || process.cwd();
55
58
  this.watchEnabled = options.watch ?? false;
@@ -131,8 +134,10 @@ export class SkillManager extends EventEmitter {
131
134
  const pathsToWatch = [
132
135
  this.personalSkillsPath,
133
136
  this.personalClaudeSkillsPath,
137
+ this.personalAgentsSkillsPath,
134
138
  join(this.workdir, ".wave", "skills"),
135
139
  join(this.workdir, ".claude", "skills"),
140
+ join(this.workdir, ".agents", "skills"),
136
141
  ];
137
142
 
138
143
  logger?.debug(`Setting up skill watcher for: ${pathsToWatch.join(", ")}`);
@@ -225,6 +230,11 @@ export class SkillManager extends EventEmitter {
225
230
  "builtin",
226
231
  );
227
232
 
233
+ const personalAgentsCollection = await this.discoverSkillCollection(
234
+ this.personalAgentsSkillsPath,
235
+ "personal",
236
+ );
237
+
228
238
  const personalClaudeCollection = await this.discoverSkillCollection(
229
239
  this.personalClaudeSkillsPath,
230
240
  "personal",
@@ -243,12 +253,14 @@ export class SkillManager extends EventEmitter {
243
253
  return {
244
254
  builtinSkills: builtinCollection.skills,
245
255
  personalSkills: new Map([
246
- ...personalClaudeCollection.skills,
256
+ ...personalAgentsCollection.skills, // lowest priority
257
+ ...personalClaudeCollection.skills, // overrides .agents
247
258
  ...personalCollection.skills, // .wave overrides .claude
248
259
  ]),
249
260
  projectSkills: projectCollection.skills,
250
261
  errors: [
251
262
  ...builtinCollection.errors,
263
+ ...personalAgentsCollection.errors,
252
264
  ...personalClaudeCollection.errors,
253
265
  ...personalCollection.errors,
254
266
  ...projectCollection.errors,
@@ -271,9 +283,11 @@ export class SkillManager extends EventEmitter {
271
283
  };
272
284
 
273
285
  if (type === "project") {
274
- // Scan .claude/skills first, then .wave/skills (wave overrides)
286
+ // Scan .agents/skills first, then .claude/skills, then .wave/skills (wave overrides)
287
+ const agentsPath = join(basePath, ".agents", "skills");
275
288
  const claudePath = join(basePath, ".claude", "skills");
276
289
  const wavePath = join(basePath, ".wave", "skills");
290
+ await this.scanSkillPath(agentsPath, collection);
277
291
  await this.scanSkillPath(claudePath, collection);
278
292
  await this.scanSkillPath(wavePath, collection);
279
293
  } else {
@@ -19,7 +19,11 @@ import {
19
19
  GREP_TOOL_NAME,
20
20
  } from "../constants/tools.js";
21
21
 
22
- export const BASE_SYSTEM_PROMPT = `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
22
+ export const BASE_SYSTEM_PROMPT = `You are a coding agent developed by NetEase that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
23
+
24
+ export const MODEL_IDENTITY_PROMPT = `# Model identity
25
+
26
+ If asked about your model, model version, underlying architecture, or product name, identify yourself simply as a coding agent developed by NetEase. Do not reveal the specific model ID, model family, model provider, or product name. Do not mention or hint at any of these details.`;
23
27
 
24
28
  export const DOING_TASKS_PROMPT = `# Doing tasks
25
29
  - The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more. When given an unclear or generic instruction, consider it in the context of these software engineering tasks and the current working directory. For example, if the user asks you to change "methodName" to snake case, do not reply with just "method_name", instead find the method in the code and modify the code.
@@ -265,6 +269,7 @@ export function buildSystemPrompt(
265
269
 
266
270
  staticText += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
267
271
  staticText += `\n\n${TONE_AND_STYLE_PROMPT}`;
272
+ staticText += `\n\n${MODEL_IDENTITY_PROMPT}`;
268
273
 
269
274
  const blocks: SystemPromptBlock[] = [{ text: staticText, cacheable: true }];
270
275
 
@@ -13,11 +13,11 @@ import type { GatewayConfig, ModelConfig } from "../types/index.js";
13
13
  import { ConfigurationError, CONFIG_ERRORS } from "../types/index.js";
14
14
  import {
15
15
  transformMessagesForExplicitCache,
16
- supportsPromptCaching,
17
16
  extendUsageWithCacheMetrics,
18
17
  type ClaudeUsage,
19
18
  type ClaudeChatCompletionContentPartText,
20
19
  } from "../utils/cacheControlUtils.js";
20
+ import { supportsPromptCaching } from "../utils/modelCapabilities.js";
21
21
 
22
22
  import * as os from "os";
23
23
  import * as fs from "fs";
@@ -265,13 +265,12 @@ export async function callAgent(
265
265
  });
266
266
 
267
267
  // Determine model early (needed for system prompt construction)
268
- const currentModel = model || modelConfig.model;
269
268
  const resolvedMaxTokens = options.maxTokens ?? modelConfig.maxTokens;
270
269
 
271
270
  // Build system message content
272
271
  let systemMessage: ChatCompletionMessageParam;
273
272
  if (Array.isArray(systemPrompt)) {
274
- if (supportsPromptCaching(currentModel)) {
273
+ if (supportsPromptCaching(modelConfig.capabilities)) {
275
274
  // For Claude models, map blocks to content parts with cache_control on cacheable blocks
276
275
  const contentParts: ClaudeChatCompletionContentPartText[] =
277
276
  systemPrompt.map((block) => {
@@ -307,10 +306,10 @@ export async function callAgent(
307
306
 
308
307
  processedTools = tools;
309
308
 
310
- if (supportsPromptCaching(currentModel)) {
309
+ if (supportsPromptCaching(modelConfig.capabilities)) {
311
310
  openaiMessages = transformMessagesForExplicitCache(
312
311
  openaiMessages,
313
- currentModel,
312
+ modelConfig.capabilities,
314
313
  );
315
314
  }
316
315
 
@@ -320,12 +319,14 @@ export async function callAgent(
320
319
  fastModel: _fastModel,
321
320
  maxTokens: _maxTokens,
322
321
  permissionMode: _permissionMode,
322
+ capabilities: _capabilities,
323
323
  ...extraParams
324
324
  } = modelConfig;
325
325
  void _model;
326
326
  void _fastModel;
327
327
  void _maxTokens;
328
328
  void _permissionMode;
329
+ void _capabilities;
329
330
 
330
331
  const openaiModelConfig = getModelConfig(model || modelConfig.model, {
331
332
  max_tokens: resolvedMaxTokens,
@@ -866,6 +867,7 @@ export async function compactMessages(
866
867
  fastModel: _fastModel,
867
868
  maxTokens: _maxTokens,
868
869
  permissionMode: _permissionMode,
870
+ capabilities: _capabilities,
869
871
  fastModelConfig: _fastModelConfig,
870
872
  ...extraParams
871
873
  } = modelConfig;
@@ -873,6 +875,7 @@ export async function compactMessages(
873
875
  void _fastModel;
874
876
  void _maxTokens;
875
877
  void _permissionMode;
878
+ void _capabilities;
876
879
 
877
880
  // When a fast model override is provided, use the fast model's hyperparams
878
881
  // (if configured); otherwise fall back to the agent model's extraParams.
@@ -989,6 +992,7 @@ export async function processWebContent(
989
992
  fastModel: _fastModel,
990
993
  maxTokens: _maxTokens,
991
994
  permissionMode: _permissionMode,
995
+ capabilities: _capabilities,
992
996
  fastModelConfig: _fastModelConfig,
993
997
  ...extraParams
994
998
  } = modelConfig;
@@ -996,6 +1000,7 @@ export async function processWebContent(
996
1000
  void _fastModel;
997
1001
  void _maxTokens;
998
1002
  void _permissionMode;
1003
+ void _capabilities;
999
1004
 
1000
1005
  // When a fast model override is provided, use the fast model's hyperparams
1001
1006
  // (if configured); otherwise fall back to the agent model's extraParams.
@@ -1106,12 +1111,14 @@ export async function btw(options: BtwOptions): Promise<BtwResult> {
1106
1111
  fastModel: _fastModel,
1107
1112
  maxTokens: _maxTokens,
1108
1113
  permissionMode: _permissionMode,
1114
+ capabilities: _capabilities,
1109
1115
  ...extraParams
1110
1116
  } = modelConfig;
1111
1117
  void _model;
1112
1118
  void _fastModel;
1113
1119
  void _maxTokens;
1114
1120
  void _permissionMode;
1121
+ void _capabilities;
1115
1122
 
1116
1123
  const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
1117
1124
  temperature: 0.1,
@@ -48,6 +48,7 @@ import {
48
48
  } from "./remoteSettingsService.js";
49
49
  import { createAuthAwareFetch } from "./authService.js";
50
50
  import { ensureWaveRuntimeFilesExcluded } from "../utils/gitUtils.js";
51
+ import { atomicWriteFile } from "../utils/atomicWrite.js";
51
52
 
52
53
  /**
53
54
  * Default ConfigurationService implementation
@@ -545,12 +546,14 @@ export class ConfigurationService {
545
546
  fastModel: _ffm,
546
547
  maxTokens: _fmt,
547
548
  permissionMode: _fpm,
549
+ capabilities: _fcap,
548
550
  ...fastHyperparams
549
551
  } = fastModelConfigSource;
550
552
  void _fm;
551
553
  void _ffm;
552
554
  void _fmt;
553
555
  void _fpm;
556
+ void _fcap;
554
557
  baseConfig.fastModelConfig = fastHyperparams;
555
558
  }
556
559
 
@@ -729,7 +732,7 @@ export class ConfigurationService {
729
732
  }
730
733
 
731
734
  config.model = model;
732
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
735
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
733
736
  }
734
737
 
735
738
  /**
@@ -816,11 +819,7 @@ export class ConfigurationService {
816
819
 
817
820
  if (!config.permissions.allow.includes(rule)) {
818
821
  config.permissions.allow.push(rule);
819
- await fs.writeFile(
820
- localConfigPath,
821
- JSON.stringify(config, null, 2),
822
- "utf-8",
823
- );
822
+ await atomicWriteFile(localConfigPath, JSON.stringify(config, null, 2));
824
823
  }
825
824
  }
826
825
 
@@ -868,7 +867,7 @@ export class ConfigurationService {
868
867
 
869
868
  config.enabledPlugins[pluginId] = enabled;
870
869
 
871
- await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
870
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
872
871
  }
873
872
 
874
873
  /**
@@ -940,11 +939,7 @@ export class ConfigurationService {
940
939
  }
941
940
  fileConfig.marketplaces[name] = config;
942
941
 
943
- await fs.writeFile(
944
- configPath,
945
- JSON.stringify(fileConfig, null, 2),
946
- "utf-8",
947
- );
942
+ await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
948
943
  }
949
944
 
950
945
  /**
@@ -978,11 +973,7 @@ export class ConfigurationService {
978
973
 
979
974
  if (fileConfig.marketplaces && name in fileConfig.marketplaces) {
980
975
  delete fileConfig.marketplaces[name];
981
- await fs.writeFile(
982
- configPath,
983
- JSON.stringify(fileConfig, null, 2),
984
- "utf-8",
985
- );
976
+ await atomicWriteFile(configPath, JSON.stringify(fileConfig, null, 2));
986
977
  }
987
978
  } catch {
988
979
  // Ignore errors for corrupted or non-existent files
@@ -1020,11 +1011,7 @@ export class ConfigurationService {
1020
1011
 
1021
1012
  if (config.enabledPlugins && pluginId in config.enabledPlugins) {
1022
1013
  delete config.enabledPlugins[pluginId];
1023
- await fs.writeFile(
1024
- configPath,
1025
- JSON.stringify(config, null, 2),
1026
- "utf-8",
1027
- );
1014
+ await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
1028
1015
  }
1029
1016
  } catch {
1030
1017
  // Ignore errors for corrupted or non-existent files
@@ -1171,6 +1158,15 @@ export function loadWaveConfigFromFile(
1171
1158
 
1172
1159
  try {
1173
1160
  const content = readFileSync(filePath, "utf-8");
1161
+
1162
+ // Tolerate empty/whitespace-only files: a concurrent writer may have the
1163
+ // file truncated mid-write. Returning null lets callers skip this source
1164
+ // instead of crashing (consistent with the "file not found" path).
1165
+ if (content.trim() === "") {
1166
+ logger.warn(`Empty configuration file (likely mid-write): ${filePath}`);
1167
+ return null;
1168
+ }
1169
+
1174
1170
  const config = JSON.parse(content) as WaveConfiguration;
1175
1171
 
1176
1172
  return {
@@ -1189,10 +1185,14 @@ export function loadWaveConfigFromFile(
1189
1185
  };
1190
1186
  } catch (error) {
1191
1187
  if (error instanceof SyntaxError) {
1192
- throw new Error(`Invalid JSON syntax in ${filePath}: ${error.message}`);
1188
+ // Tolerate corrupt JSON: a concurrent writer may leave the file in a
1189
+ // partially-written state. Warn and return null so callers skip this
1190
+ // source instead of crashing.
1191
+ logger.warn(`Invalid JSON syntax in ${filePath}: ${error.message}`);
1192
+ return null;
1193
1193
  }
1194
1194
 
1195
- // Re-throw validation errors and other errors as-is
1195
+ // Re-throw other errors as-is
1196
1196
  throw error;
1197
1197
  }
1198
1198
  }