wave-agent-sdk 0.14.1 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/builtin/skills/settings/HOOKS.md +69 -0
- package/builtin/skills/settings/PLUGINS.md +171 -0
- package/builtin/skills/settings/SKILL.md +8 -3
- package/dist/agent.d.ts +2 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -3
- package/dist/managers/aiManager.d.ts +6 -6
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +122 -59
- package/dist/managers/backgroundTaskManager.d.ts.map +1 -1
- package/dist/managers/backgroundTaskManager.js +28 -18
- package/dist/managers/hookManager.d.ts +16 -1
- package/dist/managers/hookManager.d.ts.map +1 -1
- package/dist/managers/hookManager.js +97 -8
- package/dist/managers/messageManager.d.ts +19 -4
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +63 -18
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +1 -1
- package/dist/services/MarketplaceService.d.ts +0 -11
- package/dist/services/MarketplaceService.d.ts.map +1 -1
- package/dist/services/MarketplaceService.js +21 -89
- package/dist/services/aiService.d.ts +3 -3
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +7 -7
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +15 -0
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +24 -1
- package/dist/services/interactionService.js +1 -1
- package/dist/services/pluginLoader.d.ts +0 -6
- package/dist/services/pluginLoader.d.ts.map +1 -1
- package/dist/services/pluginLoader.js +14 -53
- package/dist/services/session.d.ts +1 -1
- package/dist/services/session.js +7 -7
- package/dist/services/taskManager.d.ts +1 -1
- package/dist/services/taskManager.js +1 -1
- package/dist/types/core.d.ts +1 -1
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +9 -1
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/hooks.js +2 -0
- package/dist/types/marketplace.d.ts +1 -26
- package/dist/types/marketplace.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +3 -3
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/plugins.d.ts +1 -13
- package/dist/types/plugins.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.d.ts +1 -1
- package/dist/utils/convertMessagesForAPI.js +7 -7
- package/dist/utils/groupMessagesByApiRound.d.ts +1 -1
- package/dist/utils/groupMessagesByApiRound.js +6 -6
- package/dist/utils/messageOperations.d.ts.map +1 -1
- package/dist/utils/messageOperations.js +3 -3
- package/package.json +1 -1
- package/src/agent.ts +16 -3
- package/src/managers/aiManager.ts +142 -63
- package/src/managers/backgroundTaskManager.ts +32 -22
- package/src/managers/hookManager.ts +125 -10
- package/src/managers/messageManager.ts +76 -22
- package/src/prompts/index.ts +1 -1
- package/src/services/MarketplaceService.ts +26 -127
- package/src/services/aiService.ts +11 -11
- package/src/services/hook.ts +17 -0
- package/src/services/initializationService.ts +33 -1
- package/src/services/interactionService.ts +1 -1
- package/src/services/pluginLoader.ts +14 -67
- package/src/services/session.ts +7 -7
- package/src/services/taskManager.ts +1 -1
- package/src/types/core.ts +1 -1
- package/src/types/hooks.ts +16 -2
- package/src/types/marketplace.ts +1 -24
- package/src/types/messaging.ts +3 -3
- package/src/types/plugins.ts +1 -13
- package/src/utils/convertMessagesForAPI.ts +8 -8
- package/src/utils/groupMessagesByApiRound.ts +6 -6
- package/src/utils/messageOperations.ts +3 -5
|
@@ -30,7 +30,7 @@ import type { NotificationQueue } from "./notificationQueue.js";
|
|
|
30
30
|
import { logger } from "../utils/globalLogger.js";
|
|
31
31
|
|
|
32
32
|
export interface AIManagerCallbacks {
|
|
33
|
-
|
|
33
|
+
onCompactionStateChange?: (isCompacting: boolean) => void;
|
|
34
34
|
onUsageAdded?: (usage: Usage) => void;
|
|
35
35
|
onCwdChange?: (newCwd: string) => void;
|
|
36
36
|
}
|
|
@@ -58,7 +58,7 @@ export class AIManager {
|
|
|
58
58
|
private stream: boolean; // Streaming mode flag
|
|
59
59
|
private modelOverride?: string;
|
|
60
60
|
private _onCwdChange?: (newCwd: string) => void; // Store callback for CWD changes
|
|
61
|
-
private
|
|
61
|
+
private consecutiveCompactionFailures: number = 0;
|
|
62
62
|
|
|
63
63
|
// Service overrides
|
|
64
64
|
constructor(
|
|
@@ -177,7 +177,7 @@ export class AIManager {
|
|
|
177
177
|
this._onCwdChange = callback;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
private
|
|
180
|
+
private isCompacting: boolean = false;
|
|
181
181
|
private callbacks: AIManagerCallbacks;
|
|
182
182
|
|
|
183
183
|
/**
|
|
@@ -253,8 +253,8 @@ export class AIManager {
|
|
|
253
253
|
return "";
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
// Private method to handle token statistics and message
|
|
257
|
-
private async
|
|
256
|
+
// Private method to handle token statistics and message compaction
|
|
257
|
+
private async handleTokenUsageAndCompaction(
|
|
258
258
|
usage: Usage | undefined,
|
|
259
259
|
abortController: AbortController,
|
|
260
260
|
): Promise<void> {
|
|
@@ -272,30 +272,30 @@ export class AIManager {
|
|
|
272
272
|
this.getMaxInputTokens()
|
|
273
273
|
) {
|
|
274
274
|
logger?.debug(
|
|
275
|
-
`Token usage exceeded ${this.getMaxInputTokens()},
|
|
275
|
+
`Token usage exceeded ${this.getMaxInputTokens()}, compacting messages...`,
|
|
276
276
|
);
|
|
277
277
|
|
|
278
|
-
// Check if messages need
|
|
279
|
-
const
|
|
278
|
+
// Check if messages need compaction
|
|
279
|
+
const messagesToCompact = this.messageManager.getMessages();
|
|
280
280
|
|
|
281
|
-
// If there are messages to
|
|
282
|
-
if (
|
|
283
|
-
// Circuit breaker: skip
|
|
284
|
-
if (this.
|
|
281
|
+
// If there are messages to compact, perform compaction
|
|
282
|
+
if (messagesToCompact.length > 0) {
|
|
283
|
+
// Circuit breaker: skip compaction after 3 consecutive failures
|
|
284
|
+
if (this.consecutiveCompactionFailures >= 3) {
|
|
285
285
|
logger?.warn(
|
|
286
|
-
`Skipping
|
|
286
|
+
`Skipping compaction: ${this.consecutiveCompactionFailures} consecutive failures`,
|
|
287
287
|
);
|
|
288
288
|
return;
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
const recentChatMessages = convertMessagesForAPI(
|
|
291
|
+
const recentChatMessages = convertMessagesForAPI(messagesToCompact);
|
|
292
292
|
|
|
293
|
-
// Save session before
|
|
293
|
+
// Save session before compaction to preserve original messages
|
|
294
294
|
await this.messageManager.saveSession();
|
|
295
295
|
|
|
296
|
-
this.
|
|
296
|
+
this.setIsCompacting(true);
|
|
297
297
|
try {
|
|
298
|
-
const
|
|
298
|
+
const compactResult = await aiService.compactMessages({
|
|
299
299
|
gatewayConfig: this.getGatewayConfig(),
|
|
300
300
|
modelConfig: this.getModelConfig(),
|
|
301
301
|
messages: recentChatMessages,
|
|
@@ -303,15 +303,15 @@ export class AIManager {
|
|
|
303
303
|
model: this.getModelConfig().fastModel,
|
|
304
304
|
});
|
|
305
305
|
|
|
306
|
-
// Handle usage tracking for
|
|
307
|
-
let
|
|
308
|
-
if (
|
|
309
|
-
|
|
310
|
-
prompt_tokens:
|
|
311
|
-
completion_tokens:
|
|
312
|
-
total_tokens:
|
|
306
|
+
// Handle usage tracking for compaction operations
|
|
307
|
+
let compactUsage: Usage | undefined;
|
|
308
|
+
if (compactResult.usage) {
|
|
309
|
+
compactUsage = {
|
|
310
|
+
prompt_tokens: compactResult.usage.prompt_tokens,
|
|
311
|
+
completion_tokens: compactResult.usage.completion_tokens,
|
|
312
|
+
total_tokens: compactResult.usage.total_tokens,
|
|
313
313
|
model: this.getModelConfig().fastModel,
|
|
314
|
-
operation_type: "
|
|
314
|
+
operation_type: "compact",
|
|
315
315
|
};
|
|
316
316
|
}
|
|
317
317
|
|
|
@@ -364,73 +364,152 @@ export class AIManager {
|
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
// 4.
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
if (
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
367
|
+
// 4. Invoked skills context (with token budget, matching Claude Code)
|
|
368
|
+
const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000;
|
|
369
|
+
const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000;
|
|
370
|
+
const invokedSkillNames =
|
|
371
|
+
this.messageManager.getInvokedSkillNames(10);
|
|
372
|
+
if (invokedSkillNames.length > 0 && this.skillManager) {
|
|
373
|
+
const invokedSkillParts: string[] = [];
|
|
374
|
+
let skillsUsedTokens = 0;
|
|
375
|
+
for (const skillName of invokedSkillNames) {
|
|
376
|
+
try {
|
|
377
|
+
const skill = await this.skillManager.loadSkill(skillName);
|
|
378
|
+
if (!skill) continue;
|
|
379
|
+
|
|
380
|
+
// Extract content after frontmatter (matching prepareSkillContent pattern)
|
|
381
|
+
const contentMatch = skill.content.match(
|
|
382
|
+
/^---\n[\s\S]*?\n---\n([\s\S]*)$/,
|
|
383
|
+
);
|
|
384
|
+
let skillContent = contentMatch
|
|
385
|
+
? contentMatch[1].trim()
|
|
386
|
+
: skill.content;
|
|
387
|
+
|
|
388
|
+
// Per-skill token budget enforcement (~4 chars per token)
|
|
389
|
+
const maxSkillChars = POST_COMPACT_MAX_TOKENS_PER_SKILL * 4;
|
|
390
|
+
if (skillContent.length > maxSkillChars) {
|
|
391
|
+
skillContent =
|
|
392
|
+
skillContent.slice(0, maxSkillChars) +
|
|
393
|
+
"\n\n...[truncated]...";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const skillTokens = Math.ceil(skillContent.length / 4);
|
|
397
|
+
if (
|
|
398
|
+
skillsUsedTokens + skillTokens >
|
|
399
|
+
POST_COMPACT_SKILLS_TOKEN_BUDGET
|
|
400
|
+
)
|
|
401
|
+
break;
|
|
402
|
+
skillsUsedTokens += skillTokens;
|
|
403
|
+
|
|
404
|
+
invokedSkillParts.push(
|
|
405
|
+
`\n\n## ${skill.name}\n${skill.description ? `*${skill.description}*\n\n` : ""}\`\`\`\n${skillContent}\n\`\`\``,
|
|
406
|
+
);
|
|
407
|
+
} catch {
|
|
408
|
+
// Skip skills that can't be loaded
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (invokedSkillParts.length > 0) {
|
|
412
|
+
contextParts.push(
|
|
413
|
+
`\n\n[Invoked Skills]\n${invokedSkillParts.join("")}`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
377
416
|
}
|
|
378
417
|
|
|
379
|
-
// 5. Background
|
|
380
|
-
const agents =
|
|
418
|
+
// 5. Background subagent status (shell tasks excluded, matching Claude Code's createAsyncAgentAttachmentsIfNeeded)
|
|
419
|
+
const agents =
|
|
420
|
+
this.backgroundTaskManager
|
|
421
|
+
?.getAllTasks()
|
|
422
|
+
.filter((a) => a.type === "subagent") || [];
|
|
381
423
|
if (agents.length > 0) {
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
.
|
|
385
|
-
|
|
424
|
+
const agentParts: string[] = [];
|
|
425
|
+
for (const a of agents) {
|
|
426
|
+
if (a.status === "killed") {
|
|
427
|
+
agentParts.push(
|
|
428
|
+
`Task "${a.description}" (${a.id}) was stopped by the user.`,
|
|
429
|
+
);
|
|
430
|
+
} else if (a.status === "running") {
|
|
431
|
+
const parts = [
|
|
432
|
+
`Background agent "${a.description}" (${a.id}) is still running.`,
|
|
433
|
+
`Do NOT spawn a duplicate. You will be notified when it completes.`,
|
|
434
|
+
];
|
|
435
|
+
if (a.outputPath) {
|
|
436
|
+
parts.push(`You can read partial output at ${a.outputPath}.`);
|
|
437
|
+
}
|
|
438
|
+
agentParts.push(parts.join(" "));
|
|
439
|
+
} else {
|
|
440
|
+
// completed or failed
|
|
441
|
+
const parts = [
|
|
442
|
+
`Task ${a.id} (status: ${a.status}) (description: ${a.description}).`,
|
|
443
|
+
];
|
|
444
|
+
const deltaText = a.status === "failed" ? a.stderr : a.stdout;
|
|
445
|
+
if (deltaText && deltaText.length > 0) {
|
|
446
|
+
const summary =
|
|
447
|
+
deltaText.length > 500
|
|
448
|
+
? deltaText.slice(0, 500) + "..."
|
|
449
|
+
: deltaText;
|
|
450
|
+
parts.push(`Delta: ${summary}`);
|
|
451
|
+
}
|
|
452
|
+
if (a.outputPath) {
|
|
453
|
+
parts.push(
|
|
454
|
+
`Read the output file to retrieve the result: ${a.outputPath}.`,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
agentParts.push(parts.join(" "));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (agentParts.length > 0) {
|
|
461
|
+
contextParts.push(
|
|
462
|
+
`\n\n[Background Tasks]\n${agentParts.join("\n")}`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
386
465
|
}
|
|
387
466
|
|
|
388
467
|
// Merge context restoration into summary
|
|
389
468
|
const enhancedSummary =
|
|
390
|
-
|
|
469
|
+
compactResult.content +
|
|
391
470
|
(contextParts.length > 0
|
|
392
471
|
? `\n\n[Context Restoration]` + contextParts.join("")
|
|
393
472
|
: "");
|
|
394
473
|
|
|
395
|
-
// Execute message reconstruction and sessionId update after
|
|
396
|
-
this.messageManager.
|
|
474
|
+
// Execute message reconstruction and sessionId update after compaction
|
|
475
|
+
this.messageManager.compactMessagesAndUpdateSession(
|
|
397
476
|
enhancedSummary,
|
|
398
|
-
|
|
477
|
+
compactUsage,
|
|
399
478
|
);
|
|
400
479
|
|
|
401
480
|
// Notify Agent to add to usage tracking
|
|
402
|
-
if (
|
|
403
|
-
this.callbacks.onUsageAdded(
|
|
481
|
+
if (compactUsage && this.callbacks?.onUsageAdded) {
|
|
482
|
+
this.callbacks.onUsageAdded(compactUsage);
|
|
404
483
|
}
|
|
405
484
|
|
|
406
485
|
logger?.debug(
|
|
407
|
-
`Successfully
|
|
486
|
+
`Successfully compacted ${messagesToCompact.length} messages and updated session`,
|
|
408
487
|
);
|
|
409
|
-
this.
|
|
410
|
-
} catch (
|
|
411
|
-
this.
|
|
488
|
+
this.consecutiveCompactionFailures = 0;
|
|
489
|
+
} catch (compactError) {
|
|
490
|
+
this.consecutiveCompactionFailures++;
|
|
412
491
|
logger?.error(
|
|
413
|
-
`Failed to
|
|
414
|
-
|
|
492
|
+
`Failed to compact messages (${this.consecutiveCompactionFailures} consecutive):`,
|
|
493
|
+
compactError,
|
|
415
494
|
);
|
|
416
495
|
this.messageManager.addErrorBlock(
|
|
417
|
-
`Failed to
|
|
496
|
+
`Failed to compact conversation history: ${compactError instanceof Error ? compactError.message : String(compactError)}. You may encounter context limit issues.`,
|
|
418
497
|
);
|
|
419
498
|
} finally {
|
|
420
|
-
this.
|
|
499
|
+
this.setIsCompacting(false);
|
|
421
500
|
}
|
|
422
501
|
}
|
|
423
502
|
}
|
|
424
503
|
}
|
|
425
504
|
|
|
426
|
-
public
|
|
427
|
-
return this.
|
|
505
|
+
public getIsCompacting(): boolean {
|
|
506
|
+
return this.isCompacting;
|
|
428
507
|
}
|
|
429
508
|
|
|
430
|
-
public
|
|
431
|
-
if (this.
|
|
432
|
-
this.
|
|
433
|
-
this.callbacks.
|
|
509
|
+
public setIsCompacting(isCompacting: boolean): void {
|
|
510
|
+
if (this.isCompacting !== isCompacting) {
|
|
511
|
+
this.isCompacting = isCompacting;
|
|
512
|
+
this.callbacks.onCompactionStateChange?.(isCompacting);
|
|
434
513
|
}
|
|
435
514
|
}
|
|
436
515
|
|
|
@@ -914,8 +993,8 @@ export class AIManager {
|
|
|
914
993
|
await Promise.all(toolExecutionPromises);
|
|
915
994
|
}
|
|
916
995
|
|
|
917
|
-
// Handle token statistics and message
|
|
918
|
-
await this.
|
|
996
|
+
// Handle token statistics and message compaction
|
|
997
|
+
await this.handleTokenUsageAndCompaction(result.usage, abortController);
|
|
919
998
|
|
|
920
999
|
// Finalize text/reasoning blocks for the final response (no tools)
|
|
921
1000
|
this.messageManager.finalizeStreamingBlocks();
|
|
@@ -153,22 +153,27 @@ export class BackgroundTaskManager {
|
|
|
153
153
|
if (logStream.writable) {
|
|
154
154
|
logStream.end();
|
|
155
155
|
}
|
|
156
|
-
shell.status
|
|
156
|
+
const wasKilled = shell.status === "killed";
|
|
157
|
+
if (!wasKilled) {
|
|
158
|
+
shell.status = code === 0 ? "completed" : "failed";
|
|
159
|
+
}
|
|
157
160
|
shell.exitCode = code ?? 0;
|
|
158
161
|
shell.endTime = Date.now();
|
|
159
162
|
shell.runtime = shell.endTime - startTime;
|
|
160
163
|
this.notifyTasksChange();
|
|
161
164
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
165
|
+
// Skip notification if task was manually killed (user/agent-initiated stop)
|
|
166
|
+
if (!wasKilled) {
|
|
167
|
+
const notificationQueue = this.container.has("NotificationQueue")
|
|
168
|
+
? this.container.get<NotificationQueue>("NotificationQueue")
|
|
169
|
+
: undefined;
|
|
170
|
+
if (notificationQueue) {
|
|
171
|
+
const statusStr = shell.status;
|
|
172
|
+
const summary = `Command "${command}" ${statusStr} with exit code ${code ?? 0}`;
|
|
173
|
+
notificationQueue.enqueue(
|
|
174
|
+
`<task-notification>\n<task-id>${id}</task-id>\n<task-type>shell</task-type>\n<output-file>${logPath}</output-file>\n<status>${statusStr}</status>\n<summary>${summary}</summary>\n</task-notification>`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
172
177
|
}
|
|
173
178
|
};
|
|
174
179
|
|
|
@@ -301,22 +306,27 @@ export class BackgroundTaskManager {
|
|
|
301
306
|
if (logStream.writable) {
|
|
302
307
|
logStream.end();
|
|
303
308
|
}
|
|
304
|
-
shell.status
|
|
309
|
+
const wasKilled = shell.status === "killed";
|
|
310
|
+
if (!wasKilled) {
|
|
311
|
+
shell.status = code === 0 ? "completed" : "failed";
|
|
312
|
+
}
|
|
305
313
|
shell.exitCode = code ?? 0;
|
|
306
314
|
shell.endTime = Date.now();
|
|
307
315
|
shell.runtime = shell.endTime - startTime;
|
|
308
316
|
this.notifyTasksChange();
|
|
309
317
|
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
// Skip notification if task was manually killed (user/agent-initiated stop)
|
|
319
|
+
if (!wasKilled) {
|
|
320
|
+
const notificationQueue = this.container.has("NotificationQueue")
|
|
321
|
+
? this.container.get<NotificationQueue>("NotificationQueue")
|
|
322
|
+
: undefined;
|
|
323
|
+
if (notificationQueue) {
|
|
324
|
+
const statusStr = shell.status;
|
|
325
|
+
const summary = `Command "${command}" ${statusStr} with exit code ${code ?? 0}`;
|
|
326
|
+
notificationQueue.enqueue(
|
|
327
|
+
`<task-notification>\n<task-id>${id}</task-id>\n<task-type>shell</task-type>\n<output-file>${logPath}</output-file>\n<status>${statusStr}</status>\n<summary>${summary}</summary>\n</task-notification>`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
320
330
|
}
|
|
321
331
|
});
|
|
322
332
|
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type ExtendedHookExecutionContext,
|
|
13
13
|
type HookExecutionResult,
|
|
14
14
|
type HookValidationResult,
|
|
15
|
+
type SessionEndSource,
|
|
15
16
|
HookConfigurationError,
|
|
16
17
|
isValidHookEvent,
|
|
17
18
|
isValidHookEventConfig,
|
|
@@ -80,9 +81,8 @@ export class HookManager {
|
|
|
80
81
|
*/
|
|
81
82
|
loadConfigurationFromWaveConfig(waveConfig: WaveConfiguration | null): void {
|
|
82
83
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Validate the loaded configuration if it exists
|
|
84
|
+
// Merge Wave configuration hooks with existing plugin hooks
|
|
85
|
+
// (plugin hooks were registered earlier via registerPluginHooks)
|
|
86
86
|
if (waveConfig?.hooks) {
|
|
87
87
|
const validation = this.validatePartialConfiguration(waveConfig.hooks);
|
|
88
88
|
if (!validation.valid) {
|
|
@@ -91,17 +91,18 @@ export class HookManager {
|
|
|
91
91
|
validation.errors,
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
|
+
if (!this.configuration) {
|
|
95
|
+
this.configuration = {};
|
|
96
|
+
}
|
|
97
|
+
this.mergeHooksConfiguration(this.configuration, waveConfig.hooks);
|
|
94
98
|
}
|
|
95
99
|
} catch (error) {
|
|
96
|
-
// If loading fails, start with undefined configuration (no hooks)
|
|
97
|
-
this.configuration = undefined;
|
|
98
|
-
|
|
99
100
|
// Re-throw configuration errors, but handle other errors gracefully
|
|
100
101
|
if (error instanceof HookConfigurationError) {
|
|
101
102
|
throw error;
|
|
102
103
|
} else {
|
|
103
104
|
logger?.warn(
|
|
104
|
-
`[HookManager] Failed to load configuration, continuing with
|
|
105
|
+
`[HookManager] Failed to load configuration, continuing with existing hooks: ${(error as Error).message}`,
|
|
105
106
|
);
|
|
106
107
|
}
|
|
107
108
|
}
|
|
@@ -298,6 +299,8 @@ export class HookManager {
|
|
|
298
299
|
source: MessageSource.HOOK,
|
|
299
300
|
});
|
|
300
301
|
}
|
|
302
|
+
// For SessionStart, stdout is processed separately in executeSessionStartHooks
|
|
303
|
+
// For SessionEnd, stdout is ignored (fire-and-forget cleanup)
|
|
301
304
|
// For other hook types (PreToolUse, PostToolUse, Stop, PermissionRequest), ignore stdout
|
|
302
305
|
}
|
|
303
306
|
|
|
@@ -374,6 +377,16 @@ export class HookManager {
|
|
|
374
377
|
messageManager.addErrorBlock(errorMessage);
|
|
375
378
|
return { shouldBlock: false };
|
|
376
379
|
|
|
380
|
+
case "SessionStart":
|
|
381
|
+
// Non-blocking for startup, show error in error block
|
|
382
|
+
messageManager.addErrorBlock(errorMessage);
|
|
383
|
+
return { shouldBlock: false };
|
|
384
|
+
|
|
385
|
+
case "SessionEnd":
|
|
386
|
+
// Blocking error (exit code 2): show in error block, don't block shutdown
|
|
387
|
+
messageManager.addErrorBlock(errorMessage);
|
|
388
|
+
return { shouldBlock: false };
|
|
389
|
+
|
|
377
390
|
default:
|
|
378
391
|
return { shouldBlock: false };
|
|
379
392
|
}
|
|
@@ -577,7 +590,9 @@ export class HookManager {
|
|
|
577
590
|
(event === "UserPromptSubmit" ||
|
|
578
591
|
event === "Stop" ||
|
|
579
592
|
event === "SubagentStop" ||
|
|
580
|
-
event === "WorktreeCreate"
|
|
593
|
+
event === "WorktreeCreate" ||
|
|
594
|
+
event === "SessionStart" ||
|
|
595
|
+
event === "SessionEnd") &&
|
|
581
596
|
context.toolName !== undefined
|
|
582
597
|
) {
|
|
583
598
|
logger?.warn(
|
|
@@ -654,7 +669,9 @@ export class HookManager {
|
|
|
654
669
|
event === "Stop" ||
|
|
655
670
|
event === "SubagentStop" ||
|
|
656
671
|
event === "WorktreeCreate" ||
|
|
657
|
-
event === "CwdChanged"
|
|
672
|
+
event === "CwdChanged" ||
|
|
673
|
+
event === "SessionStart" ||
|
|
674
|
+
event === "SessionEnd"
|
|
658
675
|
) {
|
|
659
676
|
return true;
|
|
660
677
|
}
|
|
@@ -714,7 +731,9 @@ export class HookManager {
|
|
|
714
731
|
(event === "UserPromptSubmit" ||
|
|
715
732
|
event === "Stop" ||
|
|
716
733
|
event === "SubagentStop" ||
|
|
717
|
-
event === "WorktreeCreate"
|
|
734
|
+
event === "WorktreeCreate" ||
|
|
735
|
+
event === "SessionStart" ||
|
|
736
|
+
event === "SessionEnd") &&
|
|
718
737
|
config.matcher
|
|
719
738
|
) {
|
|
720
739
|
errors.push(`${prefix}: Event ${event} should not have a matcher`);
|
|
@@ -755,6 +774,8 @@ export class HookManager {
|
|
|
755
774
|
PermissionRequest: 0,
|
|
756
775
|
WorktreeCreate: 0,
|
|
757
776
|
CwdChanged: 0,
|
|
777
|
+
SessionStart: 0,
|
|
778
|
+
SessionEnd: 0,
|
|
758
779
|
},
|
|
759
780
|
};
|
|
760
781
|
}
|
|
@@ -768,6 +789,8 @@ export class HookManager {
|
|
|
768
789
|
PermissionRequest: 0,
|
|
769
790
|
WorktreeCreate: 0,
|
|
770
791
|
CwdChanged: 0,
|
|
792
|
+
SessionStart: 0,
|
|
793
|
+
SessionEnd: 0,
|
|
771
794
|
};
|
|
772
795
|
|
|
773
796
|
let totalConfigs = 0;
|
|
@@ -844,4 +867,96 @@ export class HookManager {
|
|
|
844
867
|
|
|
845
868
|
this.mergeHooksConfiguration(this.configuration, stampedHooks);
|
|
846
869
|
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Execute SessionStart hooks during initialization.
|
|
873
|
+
* Collects additionalContext and initialUserMessage from hook stdout.
|
|
874
|
+
*/
|
|
875
|
+
async executeSessionStartHooks(
|
|
876
|
+
source: "startup" | "resume" | "compact",
|
|
877
|
+
sessionId: string,
|
|
878
|
+
transcriptPath: string,
|
|
879
|
+
agentType?: string,
|
|
880
|
+
): Promise<{
|
|
881
|
+
results: HookExecutionResult[];
|
|
882
|
+
additionalContext?: string;
|
|
883
|
+
initialUserMessage?: string;
|
|
884
|
+
}> {
|
|
885
|
+
const context: ExtendedHookExecutionContext = {
|
|
886
|
+
event: "SessionStart",
|
|
887
|
+
projectDir: this.workdir,
|
|
888
|
+
timestamp: new Date(),
|
|
889
|
+
sessionId,
|
|
890
|
+
transcriptPath,
|
|
891
|
+
cwd: this.workdir,
|
|
892
|
+
source,
|
|
893
|
+
agentType,
|
|
894
|
+
env: Object.fromEntries(
|
|
895
|
+
Object.entries(process.env).filter((e) => e[1] !== undefined),
|
|
896
|
+
) as Record<string, string>,
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
const results = await this.executeHooks("SessionStart", context);
|
|
900
|
+
|
|
901
|
+
let additionalContext: string | undefined;
|
|
902
|
+
let initialUserMessage: string | undefined;
|
|
903
|
+
|
|
904
|
+
// Process stdout from successful hooks
|
|
905
|
+
for (const result of results) {
|
|
906
|
+
if (result.success && result.stdout?.trim()) {
|
|
907
|
+
const trimmed = result.stdout.trim();
|
|
908
|
+
// Try to parse as JSON for structured output
|
|
909
|
+
try {
|
|
910
|
+
const parsed = JSON.parse(trimmed);
|
|
911
|
+
if (parsed.hookSpecificOutput?.additionalContext) {
|
|
912
|
+
additionalContext =
|
|
913
|
+
(additionalContext ? additionalContext + "\n" : "") +
|
|
914
|
+
parsed.hookSpecificOutput.additionalContext;
|
|
915
|
+
}
|
|
916
|
+
if (parsed.initialUserMessage) {
|
|
917
|
+
initialUserMessage = parsed.initialUserMessage;
|
|
918
|
+
}
|
|
919
|
+
} catch {
|
|
920
|
+
// Not JSON, treat as additional context
|
|
921
|
+
additionalContext =
|
|
922
|
+
(additionalContext ? additionalContext + "\n" : "") + trimmed;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
return { results, additionalContext, initialUserMessage };
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Execute SessionEnd hooks during agent destruction.
|
|
932
|
+
* Non-blocking: always continues shutdown even if hooks fail.
|
|
933
|
+
* No stdout processing needed (SessionEnd hooks are fire-and-forget cleanup).
|
|
934
|
+
*/
|
|
935
|
+
async executeSessionEndHooks(
|
|
936
|
+
source: SessionEndSource,
|
|
937
|
+
sessionId: string,
|
|
938
|
+
transcriptPath: string,
|
|
939
|
+
): Promise<HookExecutionResult[]> {
|
|
940
|
+
const context: ExtendedHookExecutionContext = {
|
|
941
|
+
event: "SessionEnd",
|
|
942
|
+
projectDir: this.workdir,
|
|
943
|
+
timestamp: new Date(),
|
|
944
|
+
sessionId,
|
|
945
|
+
transcriptPath,
|
|
946
|
+
cwd: this.workdir,
|
|
947
|
+
endSource: source,
|
|
948
|
+
env: Object.fromEntries(
|
|
949
|
+
Object.entries(process.env).filter((e) => e[1] !== undefined),
|
|
950
|
+
) as Record<string, string>,
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const results = await this.executeHooks("SessionEnd", context);
|
|
954
|
+
|
|
955
|
+
// Process results but never block shutdown
|
|
956
|
+
if (results.length > 0) {
|
|
957
|
+
this.processHookResults("SessionEnd", results);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return results;
|
|
961
|
+
}
|
|
847
962
|
}
|