pi-crew 0.5.5 → 0.5.7
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/CHANGELOG.md +153 -0
- package/README.md +17 -1
- package/docs/architecture.md +2 -0
- package/docs/migration-v0.4-v0.5.md +19 -2
- package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
- package/package.json +7 -5
- package/src/benchmark/benchmark-runner.ts +45 -0
- package/src/benchmark/feedback-loop.ts +5 -0
- package/src/config/config.ts +38 -4
- package/src/config/defaults.ts +5 -0
- package/src/config/suggestions.ts +8 -0
- package/src/extension/async-notifier.ts +10 -1
- package/src/extension/cross-extension-rpc.ts +1 -1
- package/src/extension/notification-router.ts +18 -0
- package/src/extension/register.ts +13 -17
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/team-tool/anchor.ts +201 -0
- package/src/extension/team-tool/api.ts +2 -1
- package/src/extension/team-tool/auto-summarize.ts +154 -0
- package/src/extension/team-tool/run.ts +37 -2
- package/src/extension/team-tool.ts +44 -2
- package/src/hooks/registry.ts +1 -3
- package/src/observability/event-bus.ts +13 -4
- package/src/observability/event-to-metric.ts +0 -2
- package/src/runtime/anchor-manager.ts +473 -0
- package/src/runtime/async-runner.ts +8 -4
- package/src/runtime/auto-summarize.ts +350 -0
- package/src/runtime/background-runner.ts +2 -1
- package/src/runtime/budget-tracker.ts +354 -0
- package/src/runtime/chain-runner.ts +507 -0
- package/src/runtime/child-pi.ts +24 -6
- package/src/runtime/crash-recovery.ts +5 -4
- package/src/runtime/crew-agent-records.ts +32 -1
- package/src/runtime/custom-tools/irc-tool.ts +13 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
- package/src/runtime/delivery-coordinator.ts +10 -3
- package/src/runtime/dynamic-script-runner.ts +482 -0
- package/src/runtime/handoff-manager.ts +589 -0
- package/src/runtime/hidden-handoff.ts +424 -0
- package/src/runtime/live-agent-manager.ts +20 -4
- package/src/runtime/live-session-runtime.ts +39 -4
- package/src/runtime/manifest-cache.ts +2 -1
- package/src/runtime/model-resolver.ts +16 -4
- package/src/runtime/phase-tracker.ts +373 -0
- package/src/runtime/pipeline-runner.ts +514 -0
- package/src/runtime/retry-runner.ts +354 -0
- package/src/runtime/sandbox.ts +252 -0
- package/src/runtime/scheduler.ts +7 -2
- package/src/runtime/subagent-manager.ts +1 -1
- package/src/runtime/task-graph.ts +11 -1
- package/src/runtime/task-runner.ts +15 -1
- package/src/runtime/team-runner.ts +4 -3
- package/src/schema/team-tool-schema.ts +31 -0
- package/src/skills/discover-skills.ts +5 -0
- package/src/state/active-run-registry.ts +19 -3
- package/src/state/contracts.ts +9 -0
- package/src/state/crew-init.ts +3 -3
- package/src/state/decision-ledger.ts +26 -32
- package/src/state/event-log-rotation.ts +2 -2
- package/src/state/event-log.ts +17 -4
- package/src/state/mailbox.ts +35 -1
- package/src/state/run-cache.ts +18 -8
- package/src/tools/safe-bash-extension.ts +1 -0
- package/src/tools/safe-bash.ts +153 -20
- package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
- package/src/ui/powerbar-publisher.ts +1 -0
- package/src/ui/transcript-cache.ts +13 -0
- package/src/utils/bm25-search.ts +16 -8
- package/src/utils/env-filter.ts +8 -5
- package/src/utils/redaction.ts +169 -15
- package/src/utils/sse-parser.ts +10 -1
- package/src/worktree/cleanup.ts +6 -1
- package/workflows/chain.workflow.md +252 -0
- package/workflows/pipeline.workflow.md +27 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
// HandoffManager defaults
|
|
2
|
+
const DEFAULT_SUMMARIZE_THRESHOLD = 5000;
|
|
3
|
+
const DEFAULT_HANDOVER_TIMEOUT_MS = 300000; // 5 minutes
|
|
4
|
+
const DEFAULT_CLEANUP_INTERVAL_MS = 60000; // 1 minute
|
|
5
|
+
const MAX_PENDING_HANDOFFS = 1000; // Prevent unbounded growth
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Type guard for HandoffSummary structure validation.
|
|
9
|
+
*/
|
|
10
|
+
export function isValidHandoffSummary(value: unknown): value is HandoffSummary {
|
|
11
|
+
if (!value || typeof value !== 'object') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const obj = value as Record<string, unknown>;
|
|
15
|
+
|
|
16
|
+
// Check required fields
|
|
17
|
+
if (typeof obj.taskId !== 'string' || !obj.taskId) return false;
|
|
18
|
+
if (typeof obj.runId !== 'string' || !obj.runId) return false;
|
|
19
|
+
if (typeof obj.timestamp !== 'number') return false;
|
|
20
|
+
if (typeof obj.task !== 'string' || !obj.task) return false;
|
|
21
|
+
if (typeof obj.outcome !== 'string') return false;
|
|
22
|
+
if (!['success', 'failure', 'partial'].includes(obj.outcome)) return false;
|
|
23
|
+
|
|
24
|
+
// Check arrays
|
|
25
|
+
if (!Array.isArray(obj.filesCreated)) return false;
|
|
26
|
+
if (!Array.isArray(obj.filesModified)) return false;
|
|
27
|
+
if (!Array.isArray(obj.filesDeleted)) return false;
|
|
28
|
+
if (!Array.isArray(obj.decisions)) return false;
|
|
29
|
+
if (!Array.isArray(obj.blockers)) return false;
|
|
30
|
+
if (!Array.isArray(obj.nextSteps)) return false;
|
|
31
|
+
|
|
32
|
+
// Check metrics object
|
|
33
|
+
if (!obj.metrics || typeof obj.metrics !== 'object') return false;
|
|
34
|
+
const metrics = obj.metrics as Record<string, unknown>;
|
|
35
|
+
if (typeof metrics.tokensUsed !== 'number') return false;
|
|
36
|
+
if (typeof metrics.duration !== 'number') return false;
|
|
37
|
+
if (typeof metrics.iterations !== 'number') return false;
|
|
38
|
+
if (!Array.isArray(metrics.toolsUsed)) return false;
|
|
39
|
+
|
|
40
|
+
// Check contextSnapshot
|
|
41
|
+
if (typeof obj.contextSnapshot !== 'string') return false;
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* HandoffManager - Generates structured summaries for agent handoffs.
|
|
48
|
+
*
|
|
49
|
+
* Based on pi-boomerang's session_before_tree hook pattern:
|
|
50
|
+
* - Detects task completion via agent_end hook
|
|
51
|
+
* - Generates structured summaries with token metrics, artifacts, decisions
|
|
52
|
+
* - Optionally collapses context to reduce token usage
|
|
53
|
+
*
|
|
54
|
+
* @see docs/pi-boomerang-integration-plan.md
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
import type { TeamEvent } from "../state/event-log.ts";
|
|
58
|
+
import { appendEventAsync } from "../state/event-log.ts";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Represents a key decision made during task execution.
|
|
62
|
+
*/
|
|
63
|
+
export interface Decision {
|
|
64
|
+
rationale: string;
|
|
65
|
+
outcome: string;
|
|
66
|
+
alternativesConsidered: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Structured handoff summary for passing context between agents.
|
|
71
|
+
*/
|
|
72
|
+
export interface HandoffSummary {
|
|
73
|
+
taskId: string;
|
|
74
|
+
runId: string;
|
|
75
|
+
timestamp: number;
|
|
76
|
+
|
|
77
|
+
// Core summary
|
|
78
|
+
task: string;
|
|
79
|
+
outcome: "success" | "failure" | "partial";
|
|
80
|
+
|
|
81
|
+
// Structured artifacts
|
|
82
|
+
filesCreated: string[];
|
|
83
|
+
filesModified: string[];
|
|
84
|
+
filesDeleted: string[];
|
|
85
|
+
|
|
86
|
+
// Key decisions made
|
|
87
|
+
decisions: Decision[];
|
|
88
|
+
|
|
89
|
+
// Open issues / next steps
|
|
90
|
+
blockers: string[];
|
|
91
|
+
nextSteps: string[];
|
|
92
|
+
|
|
93
|
+
// Metrics
|
|
94
|
+
metrics: {
|
|
95
|
+
tokensUsed: number;
|
|
96
|
+
duration: number;
|
|
97
|
+
iterations: number;
|
|
98
|
+
toolsUsed: string[];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Context snapshot
|
|
102
|
+
contextSnapshot: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Task result interface (simplified for handoff generation).
|
|
107
|
+
*/
|
|
108
|
+
export interface TaskResult {
|
|
109
|
+
outcome: "success" | "failure" | "partial";
|
|
110
|
+
usage?: {
|
|
111
|
+
inputTokens?: number;
|
|
112
|
+
outputTokens?: number;
|
|
113
|
+
totalTokens?: number;
|
|
114
|
+
};
|
|
115
|
+
duration?: number;
|
|
116
|
+
iterations?: number;
|
|
117
|
+
toolsUsed?: string[];
|
|
118
|
+
blockers?: string[];
|
|
119
|
+
nextSteps?: string[];
|
|
120
|
+
filesCreated?: string[];
|
|
121
|
+
filesModified?: string[];
|
|
122
|
+
filesDeleted?: string[];
|
|
123
|
+
decisions?: Decision[];
|
|
124
|
+
error?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Task packet interface (minimal for handoff generation).
|
|
129
|
+
*/
|
|
130
|
+
export interface TaskPacket {
|
|
131
|
+
taskId: string;
|
|
132
|
+
runId: string;
|
|
133
|
+
goal: string;
|
|
134
|
+
sessionId?: string;
|
|
135
|
+
summarizeThreshold?: number;
|
|
136
|
+
collapseContext?: boolean;
|
|
137
|
+
forceSummarize?: boolean;
|
|
138
|
+
context?: Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface HandoffManagerOptions {
|
|
142
|
+
/** Default token threshold for triggering summarization */
|
|
143
|
+
defaultSummarizeThreshold?: number;
|
|
144
|
+
/** Enable context collapse after handoff */
|
|
145
|
+
enableContextCollapse?: boolean;
|
|
146
|
+
/** Custom event emitter for handoff events */
|
|
147
|
+
eventEmitter?: HandoffEventEmitter;
|
|
148
|
+
/** Timeout for pending handoffs in ms (default: 300000 = 5 minutes) */
|
|
149
|
+
handoffTimeoutMs?: number;
|
|
150
|
+
/** Interval for cleanup of old pending handoffs in ms (default: 60000 = 1 minute) */
|
|
151
|
+
cleanupIntervalMs?: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface HandoffEventEmitter {
|
|
155
|
+
emit(event: string, data: unknown): void;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Result of shouldSummarize check.
|
|
160
|
+
*/
|
|
161
|
+
export interface SummarizeDecision {
|
|
162
|
+
shouldSummarize: boolean;
|
|
163
|
+
reason: string;
|
|
164
|
+
tokenCount: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* HandoffManager generates structured summaries when agents complete tasks,
|
|
169
|
+
* enabling efficient context passing to subsequent agents.
|
|
170
|
+
*
|
|
171
|
+
* H1: Includes memory management to prevent unbounded growth of pendingHandoffs Map.
|
|
172
|
+
*/
|
|
173
|
+
export class HandoffManager {
|
|
174
|
+
private pendingHandoffs = new Map<string, HandoffSummary>();
|
|
175
|
+
private options: HandoffManagerOptions;
|
|
176
|
+
private handoffTimestamps = new Map<string, number>(); // Track when handoffs were added
|
|
177
|
+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
178
|
+
private disposed = false;
|
|
179
|
+
|
|
180
|
+
constructor(options: HandoffManagerOptions = {}) {
|
|
181
|
+
this.options = {
|
|
182
|
+
defaultSummarizeThreshold: DEFAULT_SUMMARIZE_THRESHOLD,
|
|
183
|
+
handoffTimeoutMs: DEFAULT_HANDOVER_TIMEOUT_MS,
|
|
184
|
+
cleanupIntervalMs: DEFAULT_CLEANUP_INTERVAL_MS,
|
|
185
|
+
...options,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Start cleanup timer
|
|
189
|
+
this.startCleanupTimer();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Start the periodic cleanup timer for stale pending handoffs.
|
|
194
|
+
* H1: Prevents memory leak by clearing old entries.
|
|
195
|
+
*/
|
|
196
|
+
private startCleanupTimer(): void {
|
|
197
|
+
if (this.disposed) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (this.cleanupTimer) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.cleanupTimer = setInterval(() => {
|
|
204
|
+
this.cleanupStaleHandoffs();
|
|
205
|
+
}, this.options.cleanupIntervalMs);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Clean up stale pending handoffs that have exceeded the timeout.
|
|
210
|
+
* H1: Prevents memory leak by removing old entries.
|
|
211
|
+
* FIX: Iterate over entries() instead of mutating Map during iteration.
|
|
212
|
+
*/
|
|
213
|
+
private cleanupStaleHandoffs(): void {
|
|
214
|
+
if (this.disposed) return;
|
|
215
|
+
|
|
216
|
+
const now = Date.now();
|
|
217
|
+
const timeout = this.options.handoffTimeoutMs ?? DEFAULT_HANDOVER_TIMEOUT_MS;
|
|
218
|
+
const cutoff = now - timeout;
|
|
219
|
+
|
|
220
|
+
// Collect keys to delete to avoid mutation during iteration
|
|
221
|
+
const toDelete: string[] = [];
|
|
222
|
+
for (const [sessionId, timestamp] of this.handoffTimestamps.entries()) {
|
|
223
|
+
if (timestamp < cutoff) {
|
|
224
|
+
toDelete.push(sessionId);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
for (const sessionId of toDelete) {
|
|
228
|
+
this.pendingHandoffs.delete(sessionId);
|
|
229
|
+
this.handoffTimestamps.delete(sessionId);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Also enforce max handoffs limit
|
|
233
|
+
if (this.pendingHandoffs.size > MAX_PENDING_HANDOFFS) {
|
|
234
|
+
// Remove oldest entries
|
|
235
|
+
const sortedEntries = [...this.handoffTimestamps.entries()]
|
|
236
|
+
.sort((a, b) => a[1] - b[1]);
|
|
237
|
+
const removeCount = sortedEntries.length - MAX_PENDING_HANDOFFS;
|
|
238
|
+
for (let i = 0; i < removeCount; i++) {
|
|
239
|
+
const sessionId = sortedEntries[i][0];
|
|
240
|
+
this.pendingHandoffs.delete(sessionId);
|
|
241
|
+
this.handoffTimestamps.delete(sessionId);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Hook: agent_end
|
|
248
|
+
* Called when agent completes a task.
|
|
249
|
+
*
|
|
250
|
+
* @param packet - The task packet
|
|
251
|
+
* @param result - The task result
|
|
252
|
+
*/
|
|
253
|
+
async onAgentEnd(packet: TaskPacket, result: TaskResult): Promise<HandoffSummary | null> {
|
|
254
|
+
if (this.disposed) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check if summarization is needed
|
|
259
|
+
if (!this.shouldSummarize(packet, result).shouldSummarize) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Generate handoff summary
|
|
264
|
+
const summary = await this.generateSummary(packet, result);
|
|
265
|
+
|
|
266
|
+
// H7: Validate generated summary structure
|
|
267
|
+
if (!isValidHandoffSummary(summary)) {
|
|
268
|
+
this.options.eventEmitter?.emit("handoff:validation_failed", {
|
|
269
|
+
packet,
|
|
270
|
+
error: "Generated summary failed structure validation",
|
|
271
|
+
});
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Store pending handoff for tree navigation
|
|
276
|
+
if (packet.sessionId) {
|
|
277
|
+
this.pendingHandoffs.set(packet.sessionId, summary);
|
|
278
|
+
this.handoffTimestamps.set(packet.sessionId, Date.now());
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Emit handoff event
|
|
282
|
+
this.options.eventEmitter?.emit("handoff:generated", { packet, summary });
|
|
283
|
+
|
|
284
|
+
// Optionally collapse context
|
|
285
|
+
if (packet.collapseContext) {
|
|
286
|
+
await this.collapseContext(packet, summary);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return summary;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Hook: session_before_tree
|
|
294
|
+
* Called before navigating to tree view.
|
|
295
|
+
* Injects pending handoff summaries into the tree.
|
|
296
|
+
*
|
|
297
|
+
* @param sessionId - The session ID
|
|
298
|
+
* @param targetId - The target tree node ID
|
|
299
|
+
*/
|
|
300
|
+
async onBeforeTreeNavigation(sessionId: string, targetId: string): Promise<HandoffSummary | null> {
|
|
301
|
+
if (this.disposed) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const pendingHandoff = this.pendingHandoffs.get(sessionId);
|
|
306
|
+
|
|
307
|
+
if (pendingHandoff) {
|
|
308
|
+
// Clear the pending handoff after injection
|
|
309
|
+
this.pendingHandoffs.delete(sessionId);
|
|
310
|
+
this.handoffTimestamps.delete(sessionId);
|
|
311
|
+
return pendingHandoff;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Check if summarization should be performed.
|
|
319
|
+
* H7: Validates input parameters before generating summary.
|
|
320
|
+
*/
|
|
321
|
+
shouldSummarize(packet: TaskPacket, result: TaskResult): SummarizeDecision {
|
|
322
|
+
// H7: Validate packet structure
|
|
323
|
+
if (!packet || typeof packet.taskId !== 'string' || !packet.taskId) {
|
|
324
|
+
return {
|
|
325
|
+
shouldSummarize: false,
|
|
326
|
+
reason: 'Invalid task packet structure',
|
|
327
|
+
tokenCount: 0,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// H7: Validate result structure
|
|
332
|
+
if (!result || typeof result.outcome !== 'string') {
|
|
333
|
+
return {
|
|
334
|
+
shouldSummarize: false,
|
|
335
|
+
reason: 'Invalid task result structure',
|
|
336
|
+
tokenCount: 0,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const threshold = packet.summarizeThreshold ?? this.options.defaultSummarizeThreshold ?? DEFAULT_SUMMARIZE_THRESHOLD;
|
|
341
|
+
const tokenCount = result.usage?.totalTokens ?? 0;
|
|
342
|
+
|
|
343
|
+
// Summarize if:
|
|
344
|
+
// 1. Task exceeded threshold tokens
|
|
345
|
+
if (tokenCount > threshold) {
|
|
346
|
+
return {
|
|
347
|
+
shouldSummarize: true,
|
|
348
|
+
reason: `Token count ${tokenCount} exceeds threshold ${threshold}`,
|
|
349
|
+
tokenCount,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 2. Task completed with significant work (3 or more tools used)
|
|
354
|
+
if (result.outcome === "success" && (result.toolsUsed?.length ?? 0) >= 3) {
|
|
355
|
+
return {
|
|
356
|
+
shouldSummarize: true,
|
|
357
|
+
reason: `Task used ${result.toolsUsed?.length ?? 0} tools, exceeding minimum of 3`,
|
|
358
|
+
tokenCount,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 3. Explicitly requested
|
|
363
|
+
if (packet.forceSummarize === true) {
|
|
364
|
+
return {
|
|
365
|
+
shouldSummarize: true,
|
|
366
|
+
reason: "Forced summarization requested",
|
|
367
|
+
tokenCount,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 4. Task has significant artifacts or decisions
|
|
372
|
+
const hasArtifacts = (result.filesCreated?.length ?? 0) > 0 ||
|
|
373
|
+
(result.filesModified?.length ?? 0) > 0;
|
|
374
|
+
const hasDecisions = (result.decisions?.length ?? 0) > 0;
|
|
375
|
+
|
|
376
|
+
if (hasArtifacts || hasDecisions) {
|
|
377
|
+
return {
|
|
378
|
+
shouldSummarize: true,
|
|
379
|
+
reason: "Task produced significant artifacts or decisions",
|
|
380
|
+
tokenCount,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// 5. Task outcome is not success (failure or partial)
|
|
385
|
+
if (result.outcome !== "success") {
|
|
386
|
+
return {
|
|
387
|
+
shouldSummarize: true,
|
|
388
|
+
reason: `Task outcome is ${result.outcome}`,
|
|
389
|
+
tokenCount,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
shouldSummarize: false,
|
|
395
|
+
reason: "Task below summarization threshold",
|
|
396
|
+
tokenCount,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Generate a structured handoff summary.
|
|
402
|
+
*/
|
|
403
|
+
async generateSummary(packet: TaskPacket, result: TaskResult): Promise<HandoffSummary> {
|
|
404
|
+
const artifacts = this.extractArtifacts(result);
|
|
405
|
+
// Use extractDecisionsFromResult to handle empty array and generate defaults
|
|
406
|
+
const decisions = this.extractDecisionsFromResult(result);
|
|
407
|
+
const contextSnapshot = await this.generateContextSnapshot(
|
|
408
|
+
packet.runId,
|
|
409
|
+
packet.taskId,
|
|
410
|
+
result
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
return {
|
|
414
|
+
taskId: packet.taskId,
|
|
415
|
+
runId: packet.runId,
|
|
416
|
+
timestamp: Date.now(),
|
|
417
|
+
|
|
418
|
+
task: packet.goal,
|
|
419
|
+
outcome: result.outcome,
|
|
420
|
+
|
|
421
|
+
filesCreated: artifacts.created,
|
|
422
|
+
filesModified: artifacts.modified,
|
|
423
|
+
filesDeleted: artifacts.deleted,
|
|
424
|
+
|
|
425
|
+
decisions,
|
|
426
|
+
blockers: result.blockers ?? [],
|
|
427
|
+
nextSteps: result.nextSteps ?? [],
|
|
428
|
+
|
|
429
|
+
metrics: {
|
|
430
|
+
tokensUsed: result.usage?.totalTokens ?? 0,
|
|
431
|
+
duration: result.duration ?? 0,
|
|
432
|
+
iterations: result.iterations ?? 1,
|
|
433
|
+
toolsUsed: result.toolsUsed ?? [],
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
contextSnapshot,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Collapse context after handoff.
|
|
442
|
+
* Signals to other extensions not to prompt during collapse.
|
|
443
|
+
*/
|
|
444
|
+
async collapseContext(packet: TaskPacket, summary: HandoffSummary): Promise<void> {
|
|
445
|
+
// Set global flag to signal collapse in progress
|
|
446
|
+
(globalThis as Record<string, unknown>).__boomerangCollapseInProgress = true;
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
// Emit event that context will be collapsed
|
|
450
|
+
this.options.eventEmitter?.emit("handoff:context_collapse", {
|
|
451
|
+
sessionId: packet.sessionId,
|
|
452
|
+
taskId: packet.taskId,
|
|
453
|
+
summary,
|
|
454
|
+
});
|
|
455
|
+
} finally {
|
|
456
|
+
// Clear the flag
|
|
457
|
+
(globalThis as Record<string, unknown>).__boomerangCollapseInProgress = false;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Get pending handoff for a session.
|
|
463
|
+
*/
|
|
464
|
+
getPendingHandoff(sessionId: string): HandoffSummary | undefined {
|
|
465
|
+
return this.pendingHandoffs.get(sessionId);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Clear pending handoff for a session.
|
|
470
|
+
*/
|
|
471
|
+
clearPendingHandoff(sessionId: string): void {
|
|
472
|
+
this.pendingHandoffs.delete(sessionId);
|
|
473
|
+
this.handoffTimestamps.delete(sessionId);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Clear all pending handoffs.
|
|
478
|
+
* H1: Manual cleanup method for memory management.
|
|
479
|
+
*/
|
|
480
|
+
clearAllPendingHandoffs(): void {
|
|
481
|
+
this.pendingHandoffs.clear();
|
|
482
|
+
this.handoffTimestamps.clear();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get the count of pending handoffs.
|
|
487
|
+
* Useful for monitoring and debugging.
|
|
488
|
+
*/
|
|
489
|
+
getPendingCount(): number {
|
|
490
|
+
return this.pendingHandoffs.size;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Extract file artifacts from task result.
|
|
495
|
+
*/
|
|
496
|
+
private extractArtifacts(result: TaskResult): {
|
|
497
|
+
created: string[];
|
|
498
|
+
modified: string[];
|
|
499
|
+
deleted: string[];
|
|
500
|
+
} {
|
|
501
|
+
return {
|
|
502
|
+
created: result.filesCreated ?? [],
|
|
503
|
+
modified: result.filesModified ?? [],
|
|
504
|
+
deleted: result.filesDeleted ?? [],
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Extract decisions from task result.
|
|
510
|
+
*/
|
|
511
|
+
private extractDecisionsFromResult(result: TaskResult): Decision[] {
|
|
512
|
+
if (result.decisions && result.decisions.length > 0) {
|
|
513
|
+
return result.decisions;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Generate a default decision for failure outcomes
|
|
517
|
+
if (result.outcome === "failure") {
|
|
518
|
+
return [{
|
|
519
|
+
rationale: "Task failed",
|
|
520
|
+
outcome: result.error ?? "Unknown error",
|
|
521
|
+
alternativesConsidered: [],
|
|
522
|
+
}];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return [];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Generate context snapshot for handoff.
|
|
530
|
+
*/
|
|
531
|
+
private async generateContextSnapshot(
|
|
532
|
+
runId: string,
|
|
533
|
+
taskId: string,
|
|
534
|
+
result: TaskResult
|
|
535
|
+
): Promise<string> {
|
|
536
|
+
const parts: string[] = [];
|
|
537
|
+
|
|
538
|
+
parts.push(`Task: ${taskId}`);
|
|
539
|
+
parts.push(`Outcome: ${result.outcome}`);
|
|
540
|
+
|
|
541
|
+
if (result.usage?.totalTokens) {
|
|
542
|
+
parts.push(`Tokens: ${result.usage.totalTokens}`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (result.toolsUsed?.length) {
|
|
546
|
+
parts.push(`Tools: ${result.toolsUsed.join(", ")}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (result.blockers?.length) {
|
|
550
|
+
parts.push(`Blockers: ${result.blockers.join("; ")}`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (result.nextSteps?.length) {
|
|
554
|
+
parts.push(`Next Steps: ${result.nextSteps.join("; ")}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return parts.join("\n");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* H8: Dispose of resources. Call when manager is no longer needed.
|
|
562
|
+
* Clears all pending handoffs and stops cleanup timer.
|
|
563
|
+
*/
|
|
564
|
+
dispose(): void {
|
|
565
|
+
this.disposed = true;
|
|
566
|
+
|
|
567
|
+
if (this.cleanupTimer) {
|
|
568
|
+
clearInterval(this.cleanupTimer);
|
|
569
|
+
this.cleanupTimer = null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
this.pendingHandoffs.clear();
|
|
573
|
+
this.handoffTimestamps.clear();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Check if the manager has been disposed.
|
|
578
|
+
*/
|
|
579
|
+
isDisposed(): boolean {
|
|
580
|
+
return this.disposed;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Create a HandoffManager with default options.
|
|
586
|
+
*/
|
|
587
|
+
export function createHandoffManager(options?: HandoffManagerOptions): HandoffManager {
|
|
588
|
+
return new HandoffManager(options);
|
|
589
|
+
}
|