Add 5 pi extensions: pi-subagents, pi-crew, rpiv-pi, pi-interactive-shell, pi-intercom
This commit is contained in:
141
extensions/pi-subagents/src/group-join.ts
Normal file
141
extensions/pi-subagents/src/group-join.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* group-join.ts — Manages grouped background agent completion notifications.
|
||||
*
|
||||
* Instead of each agent individually nudging the main agent on completion,
|
||||
* agents in a group are held until all complete (or a timeout fires),
|
||||
* then a single consolidated notification is sent.
|
||||
*/
|
||||
|
||||
import type { AgentRecord } from "./types.js";
|
||||
|
||||
export type DeliveryCallback = (records: AgentRecord[], partial: boolean) => void;
|
||||
|
||||
interface AgentGroup {
|
||||
groupId: string;
|
||||
agentIds: Set<string>;
|
||||
completedRecords: Map<string, AgentRecord>;
|
||||
timeoutHandle?: ReturnType<typeof setTimeout>;
|
||||
delivered: boolean;
|
||||
/** Shorter timeout for stragglers after a partial delivery. */
|
||||
isStraggler: boolean;
|
||||
}
|
||||
|
||||
/** Default timeout: 30s after first completion in a group. */
|
||||
const DEFAULT_TIMEOUT = 30_000;
|
||||
/** Straggler re-batch timeout: 15s. */
|
||||
const STRAGGLER_TIMEOUT = 15_000;
|
||||
|
||||
export class GroupJoinManager {
|
||||
private groups = new Map<string, AgentGroup>();
|
||||
private agentToGroup = new Map<string, string>();
|
||||
|
||||
constructor(
|
||||
private deliverCb: DeliveryCallback,
|
||||
private groupTimeout = DEFAULT_TIMEOUT,
|
||||
) {}
|
||||
|
||||
/** Register a group of agent IDs that should be joined. */
|
||||
registerGroup(groupId: string, agentIds: string[]): void {
|
||||
const group: AgentGroup = {
|
||||
groupId,
|
||||
agentIds: new Set(agentIds),
|
||||
completedRecords: new Map(),
|
||||
delivered: false,
|
||||
isStraggler: false,
|
||||
};
|
||||
this.groups.set(groupId, group);
|
||||
for (const id of agentIds) {
|
||||
this.agentToGroup.set(id, groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an agent completes.
|
||||
* Returns:
|
||||
* - 'pass' — agent is not grouped, caller should send individual nudge
|
||||
* - 'held' — result held, waiting for group completion
|
||||
* - 'delivered' — this completion triggered the group notification
|
||||
*/
|
||||
onAgentComplete(record: AgentRecord): 'delivered' | 'held' | 'pass' {
|
||||
const groupId = this.agentToGroup.get(record.id);
|
||||
if (!groupId) return 'pass';
|
||||
|
||||
const group = this.groups.get(groupId);
|
||||
if (!group || group.delivered) return 'pass';
|
||||
|
||||
group.completedRecords.set(record.id, record);
|
||||
|
||||
// All done — deliver immediately
|
||||
if (group.completedRecords.size >= group.agentIds.size) {
|
||||
this.deliver(group, false);
|
||||
return 'delivered';
|
||||
}
|
||||
|
||||
// First completion in this batch — start timeout
|
||||
if (!group.timeoutHandle) {
|
||||
const timeout = group.isStraggler ? STRAGGLER_TIMEOUT : this.groupTimeout;
|
||||
group.timeoutHandle = setTimeout(() => {
|
||||
this.onTimeout(group);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
return 'held';
|
||||
}
|
||||
|
||||
private onTimeout(group: AgentGroup): void {
|
||||
if (group.delivered) return;
|
||||
group.timeoutHandle = undefined;
|
||||
|
||||
// Partial delivery — some agents still running
|
||||
const remaining = new Set<string>();
|
||||
for (const id of group.agentIds) {
|
||||
if (!group.completedRecords.has(id)) remaining.add(id);
|
||||
}
|
||||
|
||||
// Clean up agentToGroup for delivered agents (they won't complete again)
|
||||
for (const id of group.completedRecords.keys()) {
|
||||
this.agentToGroup.delete(id);
|
||||
}
|
||||
|
||||
// Deliver what we have
|
||||
this.deliverCb([...group.completedRecords.values()], true);
|
||||
|
||||
// Set up straggler group for remaining agents
|
||||
group.completedRecords.clear();
|
||||
group.agentIds = remaining;
|
||||
group.isStraggler = true;
|
||||
// Timeout will be started when the next straggler completes
|
||||
}
|
||||
|
||||
private deliver(group: AgentGroup, partial: boolean): void {
|
||||
if (group.timeoutHandle) {
|
||||
clearTimeout(group.timeoutHandle);
|
||||
group.timeoutHandle = undefined;
|
||||
}
|
||||
group.delivered = true;
|
||||
this.deliverCb([...group.completedRecords.values()], partial);
|
||||
this.cleanupGroup(group.groupId);
|
||||
}
|
||||
|
||||
private cleanupGroup(groupId: string): void {
|
||||
const group = this.groups.get(groupId);
|
||||
if (!group) return;
|
||||
for (const id of group.agentIds) {
|
||||
this.agentToGroup.delete(id);
|
||||
}
|
||||
this.groups.delete(groupId);
|
||||
}
|
||||
|
||||
/** Check if an agent is in a group. */
|
||||
isGrouped(agentId: string): boolean {
|
||||
return this.agentToGroup.has(agentId);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const group of this.groups.values()) {
|
||||
if (group.timeoutHandle) clearTimeout(group.timeoutHandle);
|
||||
}
|
||||
this.groups.clear();
|
||||
this.agentToGroup.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user