pi-crew 0.5.5 → 0.5.6
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 +116 -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 +10 -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 +1 -1
- package/src/runtime/crash-recovery.ts +5 -4
- 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 +1 -1
- package/src/runtime/team-runner.ts +4 -3
- package/src/schema/team-tool-schema.ts +30 -0
- package/src/skills/discover-skills.ts +5 -0
- package/src/state/active-run-registry.ts +9 -2
- 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 +9 -1
- package/src/state/mailbox.ts +10 -0
- package/src/state/run-cache.ts +18 -8
- package/src/tools/safe-bash-extension.ts +1 -0
- package/src/tools/safe-bash.ts +152 -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,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HiddenHandoffService - Sends hidden boomerang-handoff messages to parent agents.
|
|
3
|
+
*
|
|
4
|
+
* Based on pi-boomerang's triggerHiddenOrchestratorHandoff() pattern:
|
|
5
|
+
* - Sends customType: "boomerang-handoff" messages
|
|
6
|
+
* - Full handoff details in hidden message to parent
|
|
7
|
+
* - Orchestrator immediately reads summary
|
|
8
|
+
*
|
|
9
|
+
* @see docs/pi-boomerang-integration-plan.md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { HandoffSummary } from "./handoff-manager.ts";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Type of hidden handoff message.
|
|
16
|
+
*/
|
|
17
|
+
export type HiddenHandoffType = "boomerang-handoff" | "task-complete" | "context-ready";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hidden handoff message sent to parent agent.
|
|
21
|
+
*/
|
|
22
|
+
export interface HiddenHandoff {
|
|
23
|
+
type: HiddenHandoffType;
|
|
24
|
+
hidden: true;
|
|
25
|
+
content: HandoffContent;
|
|
26
|
+
metadata: HiddenHandoffMetadata;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Metadata for hidden handoff message.
|
|
31
|
+
*/
|
|
32
|
+
export interface HiddenHandoffMetadata {
|
|
33
|
+
taskId: string;
|
|
34
|
+
runId: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
priority: HandoffPriority;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Priority level for hidden handoffs.
|
|
41
|
+
*/
|
|
42
|
+
export type HandoffPriority = "low" | "normal" | "high";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Content of a hidden handoff message.
|
|
46
|
+
*/
|
|
47
|
+
export interface HandoffContent {
|
|
48
|
+
summary: string;
|
|
49
|
+
files: {
|
|
50
|
+
created: string[];
|
|
51
|
+
modified: string[];
|
|
52
|
+
deleted: string[];
|
|
53
|
+
};
|
|
54
|
+
decisions: {
|
|
55
|
+
rationale: string;
|
|
56
|
+
outcome: string;
|
|
57
|
+
}[];
|
|
58
|
+
nextSteps: string[];
|
|
59
|
+
metrics: {
|
|
60
|
+
tokens: number;
|
|
61
|
+
duration: number;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Options for HiddenHandoffService.
|
|
67
|
+
*/
|
|
68
|
+
export interface HiddenHandoffServiceOptions {
|
|
69
|
+
/** Custom mailbox service for sending messages */
|
|
70
|
+
mailbox?: HiddenHandoffMailbox;
|
|
71
|
+
/** Event emitter for handoff events */
|
|
72
|
+
eventEmitter?: HiddenHandoffEventEmitter;
|
|
73
|
+
/** Get parent agent ID callback */
|
|
74
|
+
getParentAgentId?: () => string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Mailbox interface for sending hidden handoffs.
|
|
79
|
+
*/
|
|
80
|
+
export interface HiddenHandoffMailbox {
|
|
81
|
+
send(recipient: string, message: HiddenHandoff): void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Event emitter for hidden handoff events.
|
|
86
|
+
*/
|
|
87
|
+
export interface HiddenHandoffEventEmitter {
|
|
88
|
+
emit(event: string, data: unknown): void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Event data for hidden handoff sent event.
|
|
93
|
+
*/
|
|
94
|
+
export interface HiddenHandoffSentEventData {
|
|
95
|
+
summary: HandoffSummary;
|
|
96
|
+
recipient: string;
|
|
97
|
+
priority: HandoffPriority;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* HiddenHandoffService sends hidden boomerang-handoff messages to parent agents.
|
|
102
|
+
* This enables agents to communicate progress and context without explicit user-visible output.
|
|
103
|
+
*/
|
|
104
|
+
export class HiddenHandoffService {
|
|
105
|
+
private mailbox: HiddenHandoffMailbox | null = null;
|
|
106
|
+
private eventEmitter: HiddenHandoffEventEmitter | null = null;
|
|
107
|
+
private getParentAgentIdFn: (() => string) | null = null;
|
|
108
|
+
private enabled = true;
|
|
109
|
+
// C7: Track rate limiting per recipient
|
|
110
|
+
private sendTimestamps = new Map<string, number[]>();
|
|
111
|
+
private readonly RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
|
|
112
|
+
private readonly RATE_LIMIT_MAX_SENDS = 10; // Max handoffs per window
|
|
113
|
+
|
|
114
|
+
constructor(options: HiddenHandoffServiceOptions = {}) {
|
|
115
|
+
if (options.mailbox) {
|
|
116
|
+
this.mailbox = options.mailbox;
|
|
117
|
+
}
|
|
118
|
+
if (options.eventEmitter) {
|
|
119
|
+
this.eventEmitter = options.eventEmitter;
|
|
120
|
+
}
|
|
121
|
+
if (options.getParentAgentId) {
|
|
122
|
+
this.getParentAgentIdFn = options.getParentAgentId;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if hidden handoff service is enabled.
|
|
128
|
+
*/
|
|
129
|
+
isEnabled(): boolean {
|
|
130
|
+
return this.enabled;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Enable or disable hidden handoff service.
|
|
135
|
+
*/
|
|
136
|
+
setEnabled(enabled: boolean): void {
|
|
137
|
+
this.enabled = enabled;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Set the mailbox service for sending messages.
|
|
142
|
+
*/
|
|
143
|
+
setMailbox(mailbox: HiddenHandoffMailbox): void {
|
|
144
|
+
this.mailbox = mailbox;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Set the event emitter for events.
|
|
149
|
+
*/
|
|
150
|
+
setEventEmitter(eventEmitter: HiddenHandoffEventEmitter): void {
|
|
151
|
+
this.eventEmitter = eventEmitter;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Set the function to get parent agent ID.
|
|
156
|
+
*/
|
|
157
|
+
setGetParentAgentId(fn: () => string): void {
|
|
158
|
+
this.getParentAgentIdFn = fn;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Send a hidden handoff to the parent agent or specified recipient.
|
|
163
|
+
*
|
|
164
|
+
* @param summary - The handoff summary to send
|
|
165
|
+
* @param options - Send options
|
|
166
|
+
*/
|
|
167
|
+
sendHandoff(
|
|
168
|
+
summary: HandoffSummary,
|
|
169
|
+
options: SendHandoffOptions = {},
|
|
170
|
+
): void {
|
|
171
|
+
if (!this.enabled) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const priority = options.priority ?? this.inferPriority(summary);
|
|
176
|
+
const content = this.buildContent(summary);
|
|
177
|
+
let recipient = options.to ?? this.getParentAgentId();
|
|
178
|
+
|
|
179
|
+
if (!recipient) {
|
|
180
|
+
// No parent to send to, but we still emit the event
|
|
181
|
+
this.eventEmitter?.emit("handoff:sent_no_recipient", {
|
|
182
|
+
summary,
|
|
183
|
+
priority,
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// C7: Validate recipient is a reasonable agent ID
|
|
189
|
+
if (!this.isValidRecipient(recipient)) {
|
|
190
|
+
this.eventEmitter?.emit("handoff:invalid_recipient", {
|
|
191
|
+
recipient,
|
|
192
|
+
summary,
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// C7: Check rate limit
|
|
198
|
+
if (this.isRateLimited(recipient)) {
|
|
199
|
+
this.eventEmitter?.emit("handoff:rate_limited", {
|
|
200
|
+
recipient,
|
|
201
|
+
summary,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const message: HiddenHandoff = {
|
|
207
|
+
type: options.customType ?? "boomerang-handoff",
|
|
208
|
+
hidden: true,
|
|
209
|
+
content,
|
|
210
|
+
metadata: {
|
|
211
|
+
taskId: summary.taskId,
|
|
212
|
+
runId: summary.runId,
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
priority,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (this.mailbox) {
|
|
219
|
+
this.mailbox.send(recipient, message);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// C7: Record send for rate limiting
|
|
223
|
+
this.recordSend(recipient);
|
|
224
|
+
|
|
225
|
+
this.eventEmitter?.emit("handoff:sent", {
|
|
226
|
+
summary,
|
|
227
|
+
recipient,
|
|
228
|
+
priority,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Send a hidden handoff immediately (fire and forget).
|
|
234
|
+
*/
|
|
235
|
+
sendHandoffAsync(
|
|
236
|
+
summary: HandoffSummary,
|
|
237
|
+
options?: SendHandoffOptions,
|
|
238
|
+
): void {
|
|
239
|
+
// Fire and forget - no await
|
|
240
|
+
try {
|
|
241
|
+
this.sendHandoff(summary, options);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
// Log but don't throw
|
|
244
|
+
console.error("Hidden handoff failed:", error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Infer priority based on summary outcome.
|
|
250
|
+
*/
|
|
251
|
+
private inferPriority(summary: HandoffSummary): HandoffPriority {
|
|
252
|
+
if (summary.outcome === "failure") {
|
|
253
|
+
return "high";
|
|
254
|
+
}
|
|
255
|
+
if (summary.blockers.length > 0) {
|
|
256
|
+
return "normal";
|
|
257
|
+
}
|
|
258
|
+
if (summary.metrics.tokensUsed > 10000) {
|
|
259
|
+
return "normal";
|
|
260
|
+
}
|
|
261
|
+
return "low";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Build handoff content from summary.
|
|
266
|
+
*/
|
|
267
|
+
private buildContent(summary: HandoffSummary): HandoffContent {
|
|
268
|
+
return {
|
|
269
|
+
summary: this.buildSummaryText(summary),
|
|
270
|
+
files: {
|
|
271
|
+
created: summary.filesCreated,
|
|
272
|
+
modified: summary.filesModified,
|
|
273
|
+
deleted: summary.filesDeleted,
|
|
274
|
+
},
|
|
275
|
+
decisions: summary.decisions.map((d) => ({
|
|
276
|
+
rationale: d.rationale,
|
|
277
|
+
outcome: d.outcome,
|
|
278
|
+
})),
|
|
279
|
+
nextSteps: summary.nextSteps,
|
|
280
|
+
metrics: {
|
|
281
|
+
tokens: summary.metrics.tokensUsed,
|
|
282
|
+
duration: summary.metrics.duration,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Build summary text from summary.
|
|
289
|
+
*/
|
|
290
|
+
private buildSummaryText(summary: HandoffSummary): string {
|
|
291
|
+
const parts: string[] = [
|
|
292
|
+
`Completed: ${summary.task}`,
|
|
293
|
+
`Outcome: ${summary.outcome}`,
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
if (summary.filesCreated.length > 0) {
|
|
297
|
+
parts.push(`Files created: ${summary.filesCreated.join(", ")}`);
|
|
298
|
+
}
|
|
299
|
+
if (summary.filesModified.length > 0) {
|
|
300
|
+
parts.push(`Files modified: ${summary.filesModified.join(", ")}`);
|
|
301
|
+
}
|
|
302
|
+
if (summary.decisions.length > 0) {
|
|
303
|
+
parts.push(`Decisions: ${summary.decisions.length}`);
|
|
304
|
+
}
|
|
305
|
+
if (summary.blockers.length > 0) {
|
|
306
|
+
parts.push(`Blockers: ${summary.blockers.join("; ")}`);
|
|
307
|
+
}
|
|
308
|
+
if (summary.nextSteps.length > 0) {
|
|
309
|
+
parts.push(`Next steps: ${summary.nextSteps.join("; ")}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
parts.push(
|
|
313
|
+
`Tokens: ${summary.metrics.tokensUsed}`,
|
|
314
|
+
`Duration: ${Math.round(summary.metrics.duration / 1000)}s`,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return parts.join("\n");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get parent agent ID from callback or context.
|
|
322
|
+
*/
|
|
323
|
+
private getParentAgentId(): string | undefined {
|
|
324
|
+
if (this.getParentAgentIdFn) {
|
|
325
|
+
return this.getParentAgentIdFn();
|
|
326
|
+
}
|
|
327
|
+
// Fallback: try to get from global context
|
|
328
|
+
const ctx = (globalThis as Record<string, unknown>).__piCrewContext;
|
|
329
|
+
if (ctx && typeof ctx === "object") {
|
|
330
|
+
return (ctx as Record<string, unknown>).parentAgentId as string | undefined;
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* C7: Validate recipient is a reasonable agent ID.
|
|
337
|
+
*/
|
|
338
|
+
private isValidRecipient(recipient: string): boolean {
|
|
339
|
+
if (!recipient || typeof recipient !== "string") {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
// Reasonable length for an agent ID
|
|
343
|
+
if (recipient.length < 1 || recipient.length > 256) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
// Only allow alphanumeric, hyphen, underscore, colon, and period
|
|
347
|
+
// This prevents injection in mailbox routing
|
|
348
|
+
if (!/^[a-zA-Z0-9_:.-]+$/.test(recipient)) {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* C7: Check if recipient is rate limited.
|
|
356
|
+
* Also cleans up empty recipient entries to prevent unbounded Map growth.
|
|
357
|
+
*/
|
|
358
|
+
private isRateLimited(recipient: string): boolean {
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
const timestamps = this.sendTimestamps.get(recipient) ?? [];
|
|
361
|
+
|
|
362
|
+
// Filter out old timestamps outside the window
|
|
363
|
+
const recentTimestamps = timestamps.filter(
|
|
364
|
+
(t) => now - t < this.RATE_LIMIT_WINDOW_MS,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
// HIGH-11: If no recent timestamps, remove the empty key to prevent unbounded growth
|
|
368
|
+
if (recentTimestamps.length === 0) {
|
|
369
|
+
this.sendTimestamps.delete(recipient);
|
|
370
|
+
} else {
|
|
371
|
+
this.sendTimestamps.set(recipient, recentTimestamps);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return recentTimestamps.length >= this.RATE_LIMIT_MAX_SENDS;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* C7: Record a send for rate limiting.
|
|
379
|
+
*/
|
|
380
|
+
private recordSend(recipient: string): void {
|
|
381
|
+
const now = Date.now();
|
|
382
|
+
const timestamps = this.sendTimestamps.get(recipient) ?? [];
|
|
383
|
+
timestamps.push(now);
|
|
384
|
+
|
|
385
|
+
// MEDIUM-14: Use RATE_LIMIT_WINDOW_MS consistently for both filter and record
|
|
386
|
+
const recentTimestamps = timestamps.filter(
|
|
387
|
+
(t) => now - t < this.RATE_LIMIT_WINDOW_MS,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
this.sendTimestamps.set(recipient, recentTimestamps);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Dispose of resources.
|
|
395
|
+
* Call this when the service is no longer needed.
|
|
396
|
+
*/
|
|
397
|
+
dispose(): void {
|
|
398
|
+
this.mailbox = null;
|
|
399
|
+
this.eventEmitter = null;
|
|
400
|
+
this.getParentAgentIdFn = null;
|
|
401
|
+
this.sendTimestamps.clear();
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Options for sending hidden handoffs.
|
|
407
|
+
*/
|
|
408
|
+
export interface SendHandoffOptions {
|
|
409
|
+
/** Recipient agent ID (defaults to parent) */
|
|
410
|
+
to?: string;
|
|
411
|
+
/** Priority level */
|
|
412
|
+
priority?: HandoffPriority;
|
|
413
|
+
/** Custom handoff type */
|
|
414
|
+
customType?: HiddenHandoffType;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Create a HiddenHandoffService with default options.
|
|
419
|
+
*/
|
|
420
|
+
export function createHiddenHandoffService(
|
|
421
|
+
options?: HiddenHandoffServiceOptions,
|
|
422
|
+
): HiddenHandoffService {
|
|
423
|
+
return new HiddenHandoffService(options);
|
|
424
|
+
}
|
|
@@ -3,6 +3,8 @@ import type { IrcMessage } from "./live-irc.ts";
|
|
|
3
3
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
4
4
|
import type { appendEvent } from "../state/event-log.ts";
|
|
5
5
|
|
|
6
|
+
const MAX_PENDING_MESSAGES = 1000;
|
|
7
|
+
|
|
6
8
|
type LiveSessionHandle = {
|
|
7
9
|
steer?: (text: string) => Promise<void>;
|
|
8
10
|
prompt?: (text: string, options?: Record<string, unknown>) => Promise<void>;
|
|
@@ -103,12 +105,12 @@ export function registerLiveAgent(input: Omit<LiveAgentHandle, "createdAt" | "up
|
|
|
103
105
|
if (handle.pendingSteers.length && typeof handle.session.steer === "function") {
|
|
104
106
|
const pending = [...handle.pendingSteers];
|
|
105
107
|
handle.pendingSteers.length = 0;
|
|
106
|
-
for (const message of pending) void handle.session.steer(message).catch(() => {});
|
|
108
|
+
for (const message of pending) void handle.session.steer(message).catch((error) => logInternalError("live-agent-manager.steer", error, `agentId=${handle.agentId}`));
|
|
107
109
|
}
|
|
108
110
|
if (handle.pendingFollowUps.length && typeof handle.session.prompt === "function") {
|
|
109
111
|
const pending = [...handle.pendingFollowUps];
|
|
110
112
|
handle.pendingFollowUps.length = 0;
|
|
111
|
-
for (const message of pending) void handle.session.prompt(message, { source: "api", expandPromptTemplates: false }).catch(() => {});
|
|
113
|
+
for (const message of pending) void handle.session.prompt(message, { source: "api", expandPromptTemplates: false }).catch((error) => logInternalError("live-agent-manager.prompt", error, `agentId=${handle.agentId}`));
|
|
112
114
|
}
|
|
113
115
|
return handle;
|
|
114
116
|
}
|
|
@@ -262,6 +264,14 @@ export async function resumeLiveAgent(agentIdOrTaskId: string, prompt: string):
|
|
|
262
264
|
export function trackLiveAgentToolStart(agentIdOrTaskId: string, toolName: string): void {
|
|
263
265
|
const handle = getLiveAgent(agentIdOrTaskId);
|
|
264
266
|
if (!handle) return;
|
|
267
|
+
// Evict oldest entries if at capacity
|
|
268
|
+
const MAX_TRACKED_TOOLS = 1000;
|
|
269
|
+
if (handle.activity.activeTools.size >= MAX_TRACKED_TOOLS) {
|
|
270
|
+
const firstKey = handle.activity.activeTools.keys().next().value;
|
|
271
|
+
if (firstKey !== undefined) {
|
|
272
|
+
handle.activity.activeTools.delete(firstKey);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
265
275
|
handle.activity.activeTools.set(toolName, toolName);
|
|
266
276
|
handle.activity.toolUses++;
|
|
267
277
|
handle.updatedAt = new Date().toISOString();
|
|
@@ -310,6 +320,9 @@ export function clearLiveAgentsForTest(): void {
|
|
|
310
320
|
export function sendIrcMessage(targetAgentId: string, message: IrcMessage): void {
|
|
311
321
|
const handle = getLiveAgent(targetAgentId);
|
|
312
322
|
if (!handle) return;
|
|
323
|
+
if (handle.pendingMessages.length >= MAX_PENDING_MESSAGES) {
|
|
324
|
+
handle.pendingMessages.shift();
|
|
325
|
+
}
|
|
313
326
|
handle.pendingMessages.push(message);
|
|
314
327
|
handle.updatedAt = new Date().toISOString();
|
|
315
328
|
// G4: Try non-blocking delivery via sendCustomMessage
|
|
@@ -328,7 +341,7 @@ export function sendIrcMessage(targetAgentId: string, message: IrcMessage): void
|
|
|
328
341
|
// Fallback: inject as prompt (blocking)
|
|
329
342
|
if (typeof handle.session.prompt === "function") {
|
|
330
343
|
const ircPrompt = `[Message from ${message.from}] ${message.content}`;
|
|
331
|
-
void handle.session.prompt(ircPrompt, { source: "api", expandPromptTemplates: false }).catch(() => {});
|
|
344
|
+
void handle.session.prompt(ircPrompt, { source: "api", expandPromptTemplates: false }).catch((error) => logInternalError("live-agent-manager.irc-deliver", error, `agentId=${handle.agentId}`));
|
|
332
345
|
}
|
|
333
346
|
}
|
|
334
347
|
|
|
@@ -341,6 +354,9 @@ export function broadcastIrcMessage(fromAgentId: string, message: IrcMessage): s
|
|
|
341
354
|
for (const handle of liveAgents.values()) {
|
|
342
355
|
if (handle.agentId === fromAgentId) continue;
|
|
343
356
|
if (handle.status !== "running" && handle.status !== "queued") continue;
|
|
357
|
+
if (handle.pendingMessages.length >= MAX_PENDING_MESSAGES) {
|
|
358
|
+
handle.pendingMessages.shift();
|
|
359
|
+
}
|
|
344
360
|
handle.pendingMessages.push(message);
|
|
345
361
|
handle.updatedAt = new Date().toISOString();
|
|
346
362
|
// G4: Try non-blocking delivery
|
|
@@ -360,7 +376,7 @@ export function broadcastIrcMessage(fromAgentId: string, message: IrcMessage): s
|
|
|
360
376
|
// Fallback: inject as prompt
|
|
361
377
|
if (typeof handle.session.prompt === "function") {
|
|
362
378
|
const ircPrompt = `[Broadcast from ${message.from}] ${message.content}`;
|
|
363
|
-
void handle.session.prompt(ircPrompt, { source: "api", expandPromptTemplates: false }).catch(() => {});
|
|
379
|
+
void handle.session.prompt(ircPrompt, { source: "api", expandPromptTemplates: false }).catch((error) => logInternalError("live-agent-manager.irc-broadcast", error, `agentId=${handle.agentId}`));
|
|
364
380
|
}
|
|
365
381
|
recipients.push(handle.agentId);
|
|
366
382
|
}
|
|
@@ -105,6 +105,41 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
105
105
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
function isString(value: unknown): value is string {
|
|
109
|
+
return typeof value === "string";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Type-safe extractor for message role from streaming events.
|
|
114
|
+
* Handles the case where message may be a Record with a role field.
|
|
115
|
+
*/
|
|
116
|
+
function extractMessageRole(obj: Record<string, unknown> | undefined): string | undefined {
|
|
117
|
+
const message = obj?.message ? asRecord(obj.message) : undefined;
|
|
118
|
+
return isString(message?.role) ? message.role : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Type-safe extractor for message usage from streaming events.
|
|
123
|
+
* Handles the case where message may be a Record with usage data.
|
|
124
|
+
*/
|
|
125
|
+
function extractMessageUsage(obj: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
126
|
+
const message = obj?.message ? asRecord(obj.message) : undefined;
|
|
127
|
+
return message?.usage && typeof message.usage === "object" ? message.usage as Record<string, unknown> : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Type-safe extractor for tool name from streaming events.
|
|
132
|
+
* Handles various event structures: { tool: { name } }, { toolName }, { name }.
|
|
133
|
+
*/
|
|
134
|
+
function extractToolName(obj: Record<string, unknown> | undefined): string | undefined {
|
|
135
|
+
if (!obj) return undefined;
|
|
136
|
+
const tool = asRecord(obj.tool);
|
|
137
|
+
if (isString(tool?.name)) return tool.name;
|
|
138
|
+
if (isString(obj.toolName)) return obj.toolName;
|
|
139
|
+
if (isString(obj.name)) return obj.name;
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
108
143
|
function textFromContent(content: unknown): string[] {
|
|
109
144
|
if (typeof content === "string") return [content];
|
|
110
145
|
if (!Array.isArray(content)) return [];
|
|
@@ -477,8 +512,8 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
477
512
|
}
|
|
478
513
|
}
|
|
479
514
|
// Accumulate lifetime usage that survives compaction
|
|
480
|
-
if (obj?.type === "message_end" && (obj
|
|
481
|
-
const u = (obj
|
|
515
|
+
if (obj?.type === "message_end" && extractMessageRole(obj) === "assistant") {
|
|
516
|
+
const u = extractMessageUsage(obj);
|
|
482
517
|
if (u) {
|
|
483
518
|
trackTaskUsage(input.task.id, {
|
|
484
519
|
input: typeof u.input === "number" ? u.input : 0,
|
|
@@ -497,11 +532,11 @@ export async function runLiveSessionTask(input: LiveSessionSpawnInput): Promise<
|
|
|
497
532
|
}
|
|
498
533
|
// G2: Track tool start/end for activity display
|
|
499
534
|
if (obj?.type === "tool_use" || obj?.type === "tool_execution_start") {
|
|
500
|
-
const toolName = (obj
|
|
535
|
+
const toolName = extractToolName(obj) ?? "unknown";
|
|
501
536
|
trackLiveAgentToolStart(agentId, toolName);
|
|
502
537
|
}
|
|
503
538
|
if (obj?.type === "tool_result" || obj?.type === "tool_execution_end") {
|
|
504
|
-
const toolName = (obj
|
|
539
|
+
const toolName = extractToolName(obj) ?? "unknown";
|
|
505
540
|
trackLiveAgentToolEnd(agentId, toolName);
|
|
506
541
|
}
|
|
507
542
|
// Phase 1: collect events for yield detection
|
|
@@ -152,10 +152,11 @@ export function createManifestCache(cwd: string, options: ManifestCacheOptions =
|
|
|
152
152
|
clearTimeout(listTimer);
|
|
153
153
|
}
|
|
154
154
|
listTimer = setTimeout(() => {
|
|
155
|
+
const timer = listTimer;
|
|
155
156
|
listTimer = undefined;
|
|
156
157
|
listCache.clear();
|
|
158
|
+
timer?.unref();
|
|
157
159
|
}, ttlMs);
|
|
158
|
-
listTimer.unref();
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
function loadManifest(runId: string, rootsToCheck: string[]): CachedManifest | undefined {
|
|
@@ -4,10 +4,22 @@ export interface ModelEntry {
|
|
|
4
4
|
provider: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Core Model interface representing a resolved model instance.
|
|
9
|
+
* Used by resolveModel return type to ensure proper typing.
|
|
10
|
+
*/
|
|
11
|
+
export interface Model {
|
|
12
|
+
id: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
provider?: string;
|
|
15
|
+
// Allow additional properties from the registry
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
7
19
|
export interface ModelRegistry {
|
|
8
|
-
find(provider: string, modelId: string):
|
|
9
|
-
getAll():
|
|
10
|
-
getAvailable?():
|
|
20
|
+
find(provider: string, modelId: string): Model | undefined;
|
|
21
|
+
getAll(): Model[];
|
|
22
|
+
getAvailable?(): Model[];
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
/**
|
|
@@ -15,7 +27,7 @@ export interface ModelRegistry {
|
|
|
15
27
|
* Exact match first ("provider/modelId"), then fuzzy match.
|
|
16
28
|
* Returns Model on success, error message string on failure.
|
|
17
29
|
*/
|
|
18
|
-
export function resolveModel(input: string, registry: ModelRegistry):
|
|
30
|
+
export function resolveModel(input: string, registry: ModelRegistry): Model | string {
|
|
19
31
|
const all = (registry.getAvailable?.() ?? registry.getAll()) as ModelEntry[];
|
|
20
32
|
const availableSet = new Set(all.map((m) => `${m.provider}/${m.id}`.toLowerCase()));
|
|
21
33
|
|