wave-agent-sdk 0.17.12 → 0.18.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 (33) hide show
  1. package/builtin/skills/code-review/SKILL.md +137 -0
  2. package/builtin/skills/simplify/SKILL.md +55 -0
  3. package/dist/managers/aiManager.d.ts.map +1 -1
  4. package/dist/managers/aiManager.js +2 -7
  5. package/dist/managers/messageManager.d.ts +25 -12
  6. package/dist/managers/messageManager.d.ts.map +1 -1
  7. package/dist/managers/messageManager.js +70 -48
  8. package/dist/managers/subagentManager.d.ts +17 -5
  9. package/dist/managers/subagentManager.d.ts.map +1 -1
  10. package/dist/managers/subagentManager.js +12 -6
  11. package/dist/services/authService.d.ts +2 -2
  12. package/dist/services/authService.d.ts.map +1 -1
  13. package/dist/services/authService.js +7 -7
  14. package/dist/services/remoteSettingsService.d.ts +6 -0
  15. package/dist/services/remoteSettingsService.d.ts.map +1 -1
  16. package/dist/services/remoteSettingsService.js +29 -0
  17. package/dist/utils/containerSetup.d.ts.map +1 -1
  18. package/dist/utils/containerSetup.js +7 -2
  19. package/dist/utils/messageOperations.d.ts +7 -1
  20. package/dist/utils/messageOperations.d.ts.map +1 -1
  21. package/dist/utils/messageOperations.js +6 -4
  22. package/package.json +1 -1
  23. package/src/managers/aiManager.ts +2 -7
  24. package/src/managers/messageManager.ts +103 -74
  25. package/src/managers/subagentManager.ts +41 -26
  26. package/src/services/authService.ts +13 -10
  27. package/src/services/remoteSettingsService.ts +35 -0
  28. package/src/utils/containerSetup.ts +8 -2
  29. package/src/utils/messageOperations.ts +15 -5
  30. package/dist/utils/microcompact.d.ts +0 -7
  31. package/dist/utils/microcompact.d.ts.map +0 -1
  32. package/dist/utils/microcompact.js +0 -78
  33. package/src/utils/microcompact.ts +0 -101
@@ -28,8 +28,9 @@ let _anonymousId: string | undefined;
28
28
  export class AuthService {
29
29
  private static instance: AuthService;
30
30
  private _serverUrl: string | undefined;
31
- private onAuthChangeCallbacks: Array<(event: "login" | "logout") => void> =
32
- [];
31
+ private onAuthChangeCallbacks: Array<
32
+ (event: "login" | "logout") => void | Promise<void>
33
+ > = [];
33
34
  private _refreshPromise: Promise<boolean> | null = null;
34
35
  private _authFileMtime: number = 0;
35
36
  private static readonly REFRESH_BUFFER_MS = 5 * 60 * 1000;
@@ -53,7 +54,9 @@ export class AuthService {
53
54
  * Register a callback for auth state changes.
54
55
  * Returns an unsubscribe function.
55
56
  */
56
- onAuthChange(callback: (event: "login" | "logout") => void): () => void {
57
+ onAuthChange(
58
+ callback: (event: "login" | "logout") => void | Promise<void>,
59
+ ): () => void {
57
60
  this.onAuthChangeCallbacks.push(callback);
58
61
  return () => {
59
62
  this.onAuthChangeCallbacks = this.onAuthChangeCallbacks.filter(
@@ -62,10 +65,10 @@ export class AuthService {
62
65
  };
63
66
  }
64
67
 
65
- private notifyAuthChange(event: "login" | "logout"): void {
68
+ private async notifyAuthChange(event: "login" | "logout"): Promise<void> {
66
69
  for (const cb of this.onAuthChangeCallbacks) {
67
70
  try {
68
- cb(event);
71
+ await cb(event);
69
72
  } catch {
70
73
  // Don't let callback errors break auth flow
71
74
  }
@@ -112,7 +115,7 @@ export class AuthService {
112
115
  }
113
116
  }
114
117
 
115
- clearAuth(): void {
118
+ async clearAuth(): Promise<void> {
116
119
  const config = this.loadAuth();
117
120
  delete config.SSO_TOKEN;
118
121
  delete config.SSO_REFRESH_TOKEN;
@@ -125,7 +128,7 @@ export class AuthService {
125
128
  } else {
126
129
  this.saveAuth(config);
127
130
  }
128
- this.notifyAuthChange("logout");
131
+ await this.notifyAuthChange("logout");
129
132
  }
130
133
 
131
134
  getSSOToken(): string | undefined {
@@ -177,7 +180,7 @@ export class AuthService {
177
180
  user,
178
181
  });
179
182
 
180
- this.notifyAuthChange("login");
183
+ await this.notifyAuthChange("login");
181
184
 
182
185
  return token;
183
186
  }
@@ -385,7 +388,7 @@ export class AuthService {
385
388
  logger.info(
386
389
  `[Auth] Refresh token rejected (${response.status}), clearing auth`,
387
390
  );
388
- this.clearAuth();
391
+ await this.clearAuth();
389
392
  return false;
390
393
  }
391
394
 
@@ -412,7 +415,7 @@ export class AuthService {
412
415
  logger.info(
413
416
  `[Auth] Token refreshed successfully, new token expires at ${newExpiresAt ? new Date(newExpiresAt).toISOString() : "never"}`,
414
417
  );
415
- this.notifyAuthChange("login");
418
+ await this.notifyAuthChange("login");
416
419
  return true;
417
420
  } catch (err) {
418
421
  // Network error — don't clear auth (might be transient)
@@ -18,6 +18,32 @@ const FETCH_TIMEOUT_MS = 10_000;
18
18
 
19
19
  let _cachedSettings: RemoteSettingsCache | null = null;
20
20
  let _pollingTimer: ReturnType<typeof setInterval> | null = null;
21
+ let _onSettingsUpdateCallbacks: Array<() => void | Promise<void>> = [];
22
+
23
+ /**
24
+ * Register a callback invoked when remote settings change (checksum differs).
25
+ * Returns an unsubscribe function.
26
+ */
27
+ export function onSettingsUpdate(
28
+ callback: () => void | Promise<void>,
29
+ ): () => void {
30
+ _onSettingsUpdateCallbacks.push(callback);
31
+ return () => {
32
+ _onSettingsUpdateCallbacks = _onSettingsUpdateCallbacks.filter(
33
+ (cb) => cb !== callback,
34
+ );
35
+ };
36
+ }
37
+
38
+ async function notifySettingsUpdate(): Promise<void> {
39
+ for (const cb of _onSettingsUpdateCallbacks) {
40
+ try {
41
+ await cb();
42
+ } catch {
43
+ // Don't let callback errors break the fetch flow
44
+ }
45
+ }
46
+ }
21
47
 
22
48
  function loadCacheFromDisk(): void {
23
49
  try {
@@ -77,6 +103,8 @@ async function fetchRemoteSettings(): Promise<RemoteSettingsFetchResult> {
77
103
  return { success: false, error: "Missing SSO token or server URL" };
78
104
  }
79
105
 
106
+ const oldChecksum = _cachedSettings?.checksum;
107
+
80
108
  const headers: Record<string, string> = {
81
109
  Authorization: `Bearer ${token}`,
82
110
  };
@@ -107,6 +135,9 @@ async function fetchRemoteSettings(): Promise<RemoteSettingsFetchResult> {
107
135
  logger.debug("remoteSettings: 404 not configured — clearing stale cache");
108
136
  _cachedSettings = null;
109
137
  removeCacheFromDisk();
138
+ if (oldChecksum) {
139
+ await notifySettingsUpdate();
140
+ }
110
141
  return { success: true, notConfigured: true, settings: null };
111
142
  }
112
143
 
@@ -134,6 +165,9 @@ async function fetchRemoteSettings(): Promise<RemoteSettingsFetchResult> {
134
165
  logger.debug("remoteSettings: fetched new settings", {
135
166
  checksum: data.checksum,
136
167
  });
168
+ if (data.checksum !== oldChecksum) {
169
+ await notifySettingsUpdate();
170
+ }
137
171
  return {
138
172
  success: true,
139
173
  settings: data.settings,
@@ -312,4 +346,5 @@ export const remoteSettingsService = {
312
346
  clear,
313
347
  shutdown,
314
348
  mergeRemoteSettings,
349
+ onSettingsUpdate,
315
350
  } as const;
@@ -161,9 +161,9 @@ export function setupAgentContainer(
161
161
  container.register("McpManager", mcpManager);
162
162
 
163
163
  // Wire up auth change callback to refresh/clear remote settings
164
- authService.onAuthChange((event) => {
164
+ authService.onAuthChange(async (event) => {
165
165
  if (event === "login") {
166
- remoteSettingsService.refresh();
166
+ await remoteSettingsService.refresh();
167
167
  } else if (event === "logout") {
168
168
  remoteSettingsService.clear();
169
169
  }
@@ -300,6 +300,12 @@ export function setupAgentContainer(
300
300
  });
301
301
  container.register("LiveConfigManager", liveConfigManager);
302
302
 
303
+ // Wire up remote settings hot-update: when polling detects changed settings,
304
+ // reload configuration so admin changes propagate to the running agent.
305
+ remoteSettingsService.onSettingsUpdate(async () => {
306
+ await liveConfigManager.reload();
307
+ });
308
+
303
309
  const subagentManager = new SubagentManager(container, {
304
310
  workdir,
305
311
  callbacks: {
@@ -58,6 +58,14 @@ export type AgentToolBlockUpdateParams = Omit<
58
58
  "messages"
59
59
  >;
60
60
 
61
+ // Callback payload type — SDK always fills messageId (required)
62
+ export type ToolBlockUpdateCallbackParams = Omit<
63
+ AgentToolBlockUpdateParams,
64
+ "messageId"
65
+ > & {
66
+ messageId: string;
67
+ };
68
+
61
69
  export interface AddErrorBlockParams {
62
70
  messages: Message[];
63
71
  error: string;
@@ -289,7 +297,7 @@ export const updateToolBlockInMessage = ({
289
297
  compactParams,
290
298
  parametersChunk,
291
299
  isManuallyBackgrounded,
292
- }: UpdateToolBlockParams): Message[] => {
300
+ }: UpdateToolBlockParams): { messages: Message[]; messageId?: string } => {
293
301
  const newMessages = [...messages];
294
302
 
295
303
  // If messageId is provided, target that specific message
@@ -321,7 +329,7 @@ export const updateToolBlockInMessage = ({
321
329
  }
322
330
  }
323
331
  }
324
- return newMessages;
332
+ return { messages: newMessages, messageId };
325
333
  }
326
334
 
327
335
  // Find the last assistant or user message
@@ -350,7 +358,8 @@ export const updateToolBlockInMessage = ({
350
358
  if (isManuallyBackgrounded !== undefined)
351
359
  toolBlock.isManuallyBackgrounded = isManuallyBackgrounded;
352
360
  }
353
- break; // Found and updated, stop searching
361
+ const foundMessageId = newMessages[i].id;
362
+ return { messages: newMessages, messageId: foundMessageId };
354
363
  } else if (newMessages[i].role === "assistant") {
355
364
  // If existing block not found in assistant message, create new one
356
365
  // This handles cases where we're streaming tool parameters before execution
@@ -370,11 +379,12 @@ export const updateToolBlockInMessage = ({
370
379
  parametersChunk: parametersChunk,
371
380
  isManuallyBackgrounded: isManuallyBackgrounded,
372
381
  });
373
- break; // Created and added, stop searching
382
+ const foundMessageId = newMessages[i].id;
383
+ return { messages: newMessages, messageId: foundMessageId };
374
384
  }
375
385
  }
376
386
  }
377
- return newMessages;
387
+ return { messages: newMessages };
378
388
  };
379
389
 
380
390
  // Add Error Block to the last assistant message
@@ -1,7 +0,0 @@
1
- import type { Message } from "../types/messaging.js";
2
- export interface MicrocompactOptions {
3
- timeThresholdMS: number;
4
- recentResultsToKeep: number;
5
- }
6
- export declare function microcompactMessages(messages: Message[], options: MicrocompactOptions): Message[];
7
- //# sourceMappingURL=microcompact.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"microcompact.d.ts","sourceRoot":"","sources":["../../src/utils/microcompact.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,MAAM,uBAAuB,CAAC;AAEhE,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;CAC7B;AAID,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,EAAE,CAwFX"}
@@ -1,78 +0,0 @@
1
- const CLEARED_RESULT = "[Old tool result content cleared]";
2
- export function microcompactMessages(messages, options) {
3
- const { timeThresholdMS, recentResultsToKeep } = options;
4
- // 1. Find the latest tool block timestamp across all assistant messages
5
- let lastAssistantTime = 0;
6
- for (const msg of messages) {
7
- if (msg.role === "assistant") {
8
- for (const block of msg.blocks) {
9
- if (block.type === "tool" && block.stage === "end" && block.timestamp) {
10
- if (block.timestamp > lastAssistantTime) {
11
- lastAssistantTime = block.timestamp;
12
- }
13
- }
14
- }
15
- }
16
- }
17
- // 2. If no prior assistant messages with completed tools, return unchanged
18
- if (lastAssistantTime === 0) {
19
- return messages;
20
- }
21
- // 3. If within threshold, return unchanged
22
- if (Date.now() - lastAssistantTime < timeThresholdMS) {
23
- return messages;
24
- }
25
- const toolRefs = [];
26
- for (let mi = 0; mi < messages.length; mi++) {
27
- const msg = messages[mi];
28
- if (msg.role === "assistant") {
29
- for (let bi = 0; bi < msg.blocks.length; bi++) {
30
- const block = msg.blocks[bi];
31
- if (block.type === "tool" && block.stage === "end" && block.timestamp) {
32
- toolRefs.push({
33
- msgIndex: mi,
34
- blockIndex: bi,
35
- timestamp: block.timestamp,
36
- });
37
- }
38
- }
39
- }
40
- }
41
- toolRefs.sort((a, b) => b.timestamp - a.timestamp);
42
- // 5. Mark the top N as "keep"
43
- const keepSet = new Set();
44
- for (let i = 0; i < Math.min(recentResultsToKeep, toolRefs.length); i++) {
45
- const ref = toolRefs[i];
46
- keepSet.add(`${ref.msgIndex}:${ref.blockIndex}`);
47
- }
48
- // 6. Deep-copy messages and clear result + shortResult on non-kept blocks
49
- const result = messages.map((msg) => ({
50
- ...msg,
51
- blocks: msg.blocks.map((block) => {
52
- if (block.type === "tool" && block.stage === "end" && block.timestamp) {
53
- return { ...block };
54
- }
55
- return block;
56
- }),
57
- }));
58
- // Clear non-kept tool blocks
59
- for (const ref of toolRefs) {
60
- const key = `${ref.msgIndex}:${ref.blockIndex}`;
61
- if (!keepSet.has(key)) {
62
- result[ref.msgIndex] = {
63
- ...result[ref.msgIndex],
64
- blocks: result[ref.msgIndex].blocks.map((b, idx) => {
65
- if (idx === ref.blockIndex && b.type === "tool") {
66
- return {
67
- ...b,
68
- result: CLEARED_RESULT,
69
- shortResult: undefined,
70
- };
71
- }
72
- return b;
73
- }),
74
- };
75
- }
76
- }
77
- return result;
78
- }
@@ -1,101 +0,0 @@
1
- import type { Message, ToolBlock } from "../types/messaging.js";
2
-
3
- export interface MicrocompactOptions {
4
- timeThresholdMS: number;
5
- recentResultsToKeep: number;
6
- }
7
-
8
- const CLEARED_RESULT = "[Old tool result content cleared]";
9
-
10
- export function microcompactMessages(
11
- messages: Message[],
12
- options: MicrocompactOptions,
13
- ): Message[] {
14
- const { timeThresholdMS, recentResultsToKeep } = options;
15
-
16
- // 1. Find the latest tool block timestamp across all assistant messages
17
- let lastAssistantTime = 0;
18
- for (const msg of messages) {
19
- if (msg.role === "assistant") {
20
- for (const block of msg.blocks) {
21
- if (block.type === "tool" && block.stage === "end" && block.timestamp) {
22
- if (block.timestamp > lastAssistantTime) {
23
- lastAssistantTime = block.timestamp;
24
- }
25
- }
26
- }
27
- }
28
- }
29
-
30
- // 2. If no prior assistant messages with completed tools, return unchanged
31
- if (lastAssistantTime === 0) {
32
- return messages;
33
- }
34
-
35
- // 3. If within threshold, return unchanged
36
- if (Date.now() - lastAssistantTime < timeThresholdMS) {
37
- return messages;
38
- }
39
-
40
- // 4. Collect all completed tool results with timestamps, sorted newest-first
41
- type ToolRef = { msgIndex: number; blockIndex: number; timestamp: number };
42
-
43
- const toolRefs: ToolRef[] = [];
44
- for (let mi = 0; mi < messages.length; mi++) {
45
- const msg = messages[mi];
46
- if (msg.role === "assistant") {
47
- for (let bi = 0; bi < msg.blocks.length; bi++) {
48
- const block = msg.blocks[bi];
49
- if (block.type === "tool" && block.stage === "end" && block.timestamp) {
50
- toolRefs.push({
51
- msgIndex: mi,
52
- blockIndex: bi,
53
- timestamp: block.timestamp,
54
- });
55
- }
56
- }
57
- }
58
- }
59
-
60
- toolRefs.sort((a, b) => b.timestamp - a.timestamp);
61
-
62
- // 5. Mark the top N as "keep"
63
- const keepSet = new Set<string>();
64
- for (let i = 0; i < Math.min(recentResultsToKeep, toolRefs.length); i++) {
65
- const ref = toolRefs[i];
66
- keepSet.add(`${ref.msgIndex}:${ref.blockIndex}`);
67
- }
68
-
69
- // 6. Deep-copy messages and clear result + shortResult on non-kept blocks
70
- const result: Message[] = messages.map((msg) => ({
71
- ...msg,
72
- blocks: msg.blocks.map((block) => {
73
- if (block.type === "tool" && block.stage === "end" && block.timestamp) {
74
- return { ...block } as ToolBlock;
75
- }
76
- return block;
77
- }),
78
- }));
79
-
80
- // Clear non-kept tool blocks
81
- for (const ref of toolRefs) {
82
- const key = `${ref.msgIndex}:${ref.blockIndex}`;
83
- if (!keepSet.has(key)) {
84
- result[ref.msgIndex] = {
85
- ...result[ref.msgIndex],
86
- blocks: result[ref.msgIndex].blocks.map((b, idx) => {
87
- if (idx === ref.blockIndex && b.type === "tool") {
88
- return {
89
- ...b,
90
- result: CLEARED_RESULT,
91
- shortResult: undefined,
92
- } as ToolBlock;
93
- }
94
- return b;
95
- }),
96
- };
97
- }
98
- }
99
-
100
- return result;
101
- }