@fradser/pi-agent-teams 0.8.3 → 0.9.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.
- package/.memory/MEMORY.md +1 -1
- package/.memory/follow-up-queue.md +8 -11
- package/.memory/team-centric-design.md +18 -14
- package/LICENSE +21 -0
- package/README.md +119 -157
- package/package.json +2 -2
- package/references/agent-roles.md +57 -12
- package/src/activity.ts +12 -33
- package/src/agent-actions.ts +101 -0
- package/src/agents.ts +1 -6
- package/src/guidance.ts +114 -199
- package/src/index.ts +117 -101
- package/src/leader-reports.ts +92 -0
- package/src/recipient.ts +44 -0
- package/src/spawner.ts +293 -38
- package/src/state.ts +250 -69
- package/src/statefile.ts +74 -31
- package/src/team-machine.ts +984 -424
- package/src/tool-copy.ts +374 -0
- package/src/tool-render.ts +47 -22
- package/src/tools.ts +182 -161
- package/src/types.ts +104 -65
- package/src/ui.ts +32 -56
- package/src/work-context.ts +88 -0
- package/src/worker-tools.ts +4 -0
- package/src/worker.ts +251 -184
- package/src/follow-up-queue.ts +0 -301
package/src/follow-up-queue.ts
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
export const TEAMMATE_REPORT_MESSAGE_TYPE = "agent-teams-report";
|
|
2
|
-
export const TEAMMATE_HARNESS_MESSAGE_TYPE = "agent-teams-harness";
|
|
3
|
-
|
|
4
|
-
export interface FollowUpReport {
|
|
5
|
-
teammate?: string;
|
|
6
|
-
agent?: string;
|
|
7
|
-
spawnId?: string;
|
|
8
|
-
body: string;
|
|
9
|
-
/** Worker-authored reports are the default; harness events use a separate envelope. */
|
|
10
|
-
origin?: "teammate" | "harness";
|
|
11
|
-
harnessEvent?: {
|
|
12
|
-
type: string;
|
|
13
|
-
subject: string;
|
|
14
|
-
};
|
|
15
|
-
finished?: boolean;
|
|
16
|
-
/** Original append-only outbox event identifier, retained for session forensics. */
|
|
17
|
-
eventId?: string;
|
|
18
|
-
/** Original worker status, including omitted status, retained for session forensics. */
|
|
19
|
-
status?: "in_progress" | "completed" | "failed";
|
|
20
|
-
health?: {
|
|
21
|
-
state: "stalled";
|
|
22
|
-
silenceMs: number;
|
|
23
|
-
};
|
|
24
|
-
runId?: string;
|
|
25
|
-
/** Wall-clock time the message was authored; rendered in the envelope so a
|
|
26
|
-
* report delivered after a busy period keeps its original timestamp. */
|
|
27
|
-
timestamp?: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface FollowUpReportGroup {
|
|
31
|
-
teammate: string;
|
|
32
|
-
reports: FollowUpReport[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function groupReportsByTeammate(reports: FollowUpReport[]): FollowUpReportGroup[] {
|
|
36
|
-
const groups = new Map<string, FollowUpReportGroup>();
|
|
37
|
-
for (const report of reports) {
|
|
38
|
-
const teammate = report.teammate ?? report.agent ?? "teammate";
|
|
39
|
-
if (report.health) {
|
|
40
|
-
groups.set(`health:${groups.size}:${teammate}`, { teammate, reports: [report] });
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (report.origin === "harness" || report.harnessEvent) {
|
|
44
|
-
groups.set(`harness:${groups.size}:${teammate}`, { teammate, reports: [report] });
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
47
|
-
const key = `message:${teammate}`;
|
|
48
|
-
const group = groups.get(key);
|
|
49
|
-
if (group) {
|
|
50
|
-
group.reports.push(report);
|
|
51
|
-
} else {
|
|
52
|
-
groups.set(key, { teammate, reports: [report] });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return [...groups.values()];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface FollowUpQueueOptions {
|
|
59
|
-
isIdle: () => boolean;
|
|
60
|
-
dispatch: (reports: FollowUpReport[], content: string) => void;
|
|
61
|
-
/** Set when dispatch itself starts the agent run without before_agent_start. */
|
|
62
|
-
prepareOnDispatch?: boolean;
|
|
63
|
-
onFailure?: (message: string) => void;
|
|
64
|
-
agentStartTimeoutMs?: number;
|
|
65
|
-
retryBaseDelayMs?: number;
|
|
66
|
-
retryMaxDelayMs?: number;
|
|
67
|
-
maxAttempts?: number;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
interface PendingBatch {
|
|
71
|
-
reports: FollowUpReport[];
|
|
72
|
-
attempts: number;
|
|
73
|
-
retrying: boolean;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
interface PendingDispatch {
|
|
77
|
-
reports: FollowUpReport[];
|
|
78
|
-
content: string;
|
|
79
|
-
attempts: number;
|
|
80
|
-
prepared: boolean;
|
|
81
|
-
started: boolean;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Serializes automatic reports sent through Pi's void sendMessage API.
|
|
86
|
-
*
|
|
87
|
-
* The API reports asynchronous preflight failures through the runtime error
|
|
88
|
-
* channel, so this queue uses agent lifecycle events for success and a bounded
|
|
89
|
-
* watchdog for attempts that never reach agent_start. A report arriving during
|
|
90
|
-
* an active leader run is dispatched to Pi's native follow-up queue; this
|
|
91
|
-
* queue still serializes later reports until that run settles.
|
|
92
|
-
*/
|
|
93
|
-
export class FollowUpQueue {
|
|
94
|
-
private readonly isIdle: FollowUpQueueOptions["isIdle"];
|
|
95
|
-
private readonly dispatch: FollowUpQueueOptions["dispatch"];
|
|
96
|
-
private readonly onFailure: (message: string) => void;
|
|
97
|
-
private readonly prepareOnDispatch: boolean;
|
|
98
|
-
private readonly agentStartTimeoutMs: number;
|
|
99
|
-
private readonly retryBaseDelayMs: number;
|
|
100
|
-
private readonly retryMaxDelayMs: number;
|
|
101
|
-
private readonly maxAttempts: number;
|
|
102
|
-
private pending: PendingBatch[] = [];
|
|
103
|
-
private active: PendingDispatch | undefined;
|
|
104
|
-
private pumpScheduled = false;
|
|
105
|
-
private retryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
106
|
-
private watchdogTimer: ReturnType<typeof setTimeout> | undefined;
|
|
107
|
-
private generation = 0;
|
|
108
|
-
private deadLetter: FollowUpReport[] = [];
|
|
109
|
-
/** Reports suppressed after their teammate was intentionally stopped. They
|
|
110
|
-
* remain inspectable in runtime diagnostics but never begin a leader turn. */
|
|
111
|
-
private archived: FollowUpReport[] = [];
|
|
112
|
-
private archivedSpawnIds = new Set<string>();
|
|
113
|
-
|
|
114
|
-
constructor(options: FollowUpQueueOptions) {
|
|
115
|
-
this.isIdle = options.isIdle;
|
|
116
|
-
this.dispatch = options.dispatch;
|
|
117
|
-
this.onFailure = options.onFailure ?? (() => {});
|
|
118
|
-
this.prepareOnDispatch = options.prepareOnDispatch ?? false;
|
|
119
|
-
this.agentStartTimeoutMs = options.agentStartTimeoutMs ?? 30_000;
|
|
120
|
-
this.retryBaseDelayMs = options.retryBaseDelayMs ?? 1_000;
|
|
121
|
-
this.retryMaxDelayMs = options.retryMaxDelayMs ?? 30_000;
|
|
122
|
-
this.maxAttempts = Math.max(1, options.maxAttempts ?? 5);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
enqueue(report: FollowUpReport): void {
|
|
126
|
-
if (report.spawnId && this.archivedSpawnIds.has(report.spawnId)) {
|
|
127
|
-
this.archived.push(report);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
this.pending.push({ reports: [report], attempts: 0, retrying: false });
|
|
131
|
-
this.schedulePump();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Archive pending reports from one stopped spawn before they can wake the leader.
|
|
135
|
-
* A dispatch already accepted by Pi cannot be retracted, so it is deliberately
|
|
136
|
-
* left alone; this method only changes transitions the harness still owns. */
|
|
137
|
-
archiveSpawn(spawnId: string): FollowUpReport[] {
|
|
138
|
-
this.archivedSpawnIds.add(spawnId);
|
|
139
|
-
const moved: FollowUpReport[] = [];
|
|
140
|
-
for (const batch of this.pending) {
|
|
141
|
-
const retained = batch.reports.filter((report) => report.spawnId !== spawnId);
|
|
142
|
-
const archived = batch.reports.filter((report) => report.spawnId === spawnId);
|
|
143
|
-
this.archived.push(...archived);
|
|
144
|
-
moved.push(...archived);
|
|
145
|
-
batch.reports = retained;
|
|
146
|
-
}
|
|
147
|
-
this.pending = this.pending.filter((batch) => batch.reports.length > 0);
|
|
148
|
-
return moved;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Match the active dispatch to the prompt that Pi is about to start. */
|
|
152
|
-
onBeforeAgentStart(prompt: string): void {
|
|
153
|
-
if (!this.active || this.active.prepared || prompt !== this.active.content) return;
|
|
154
|
-
this.active.prepared = true;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Mark the matching dispatch started after Pi accepts the message. */
|
|
158
|
-
onAgentStart(): void {
|
|
159
|
-
if (!this.active || !this.active.prepared || this.active.started) return;
|
|
160
|
-
this.active.started = true;
|
|
161
|
-
this.clearWatchdog();
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/** Release a completed dispatch; unrelated settled events are ignored. */
|
|
165
|
-
onAgentSettled(): void {
|
|
166
|
-
if (this.active && !this.active.started) return;
|
|
167
|
-
if (this.active) this.active = undefined;
|
|
168
|
-
this.schedulePump();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Drop all reports and invalidate callbacks from the previous session. */
|
|
172
|
-
reset(): void {
|
|
173
|
-
this.generation++;
|
|
174
|
-
this.pending = [];
|
|
175
|
-
this.deadLetter = [];
|
|
176
|
-
this.archived = [];
|
|
177
|
-
this.archivedSpawnIds.clear();
|
|
178
|
-
this.active = undefined;
|
|
179
|
-
this.pumpScheduled = false;
|
|
180
|
-
this.clearRetryTimer();
|
|
181
|
-
this.clearWatchdog();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
get pendingCount(): number {
|
|
185
|
-
return this.pending.reduce((count, batch) => count + batch.reports.length, 0)
|
|
186
|
-
+ (this.active?.reports.length ?? 0);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
get deadLetterCount(): number {
|
|
190
|
-
return this.deadLetter.length;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
get archivedCount(): number {
|
|
194
|
-
return this.archived.length;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
archivedReportsFor(spawnId: string): FollowUpReport[] {
|
|
198
|
-
return this.archived.filter((report) => report.spawnId === spawnId);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private schedulePump(): void {
|
|
202
|
-
if (this.pumpScheduled) return;
|
|
203
|
-
this.pumpScheduled = true;
|
|
204
|
-
const generation = this.generation;
|
|
205
|
-
setTimeout(() => {
|
|
206
|
-
this.pumpScheduled = false;
|
|
207
|
-
if (generation !== this.generation) return;
|
|
208
|
-
this.pump();
|
|
209
|
-
}, 0);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private pump(): void {
|
|
213
|
-
if (this.active || this.retryTimer || this.pending.length === 0) return;
|
|
214
|
-
const first = this.pending.shift();
|
|
215
|
-
if (!first) return;
|
|
216
|
-
// One report per dispatch: every teammate-authored message becomes its own
|
|
217
|
-
// leader turn, in arrival order. Coalescing would blur when each message
|
|
218
|
-
// was sent and what the leader was expected to do about each one.
|
|
219
|
-
const reports = first.reports;
|
|
220
|
-
const content = formatReports(reports);
|
|
221
|
-
const queuedIntoActiveRun = !this.isIdle();
|
|
222
|
-
this.active = {
|
|
223
|
-
reports,
|
|
224
|
-
content,
|
|
225
|
-
attempts: first.attempts,
|
|
226
|
-
prepared: queuedIntoActiveRun || this.prepareOnDispatch,
|
|
227
|
-
started: queuedIntoActiveRun,
|
|
228
|
-
};
|
|
229
|
-
const generation = this.generation;
|
|
230
|
-
if (!queuedIntoActiveRun) {
|
|
231
|
-
this.watchdogTimer = setTimeout(() => {
|
|
232
|
-
if (generation !== this.generation || !this.active || this.active.started) return;
|
|
233
|
-
this.failActive(generation, "Pi did not start the automatic teammate follow-up.");
|
|
234
|
-
}, this.agentStartTimeoutMs);
|
|
235
|
-
this.watchdogTimer.unref?.();
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
this.dispatch(reports, content);
|
|
239
|
-
} catch (error) {
|
|
240
|
-
this.failActive(generation, error instanceof Error ? error.message : String(error));
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
private failActive(generation: number, message: string): void {
|
|
245
|
-
if (generation !== this.generation || !this.active) return;
|
|
246
|
-
const failed = this.active;
|
|
247
|
-
this.active = undefined;
|
|
248
|
-
this.clearWatchdog();
|
|
249
|
-
failed.attempts++;
|
|
250
|
-
if (failed.attempts >= this.maxAttempts) {
|
|
251
|
-
this.deadLetter.push(...failed.reports);
|
|
252
|
-
this.onFailure(`${message} Maximum retry attempts (${this.maxAttempts}) exhausted; report moved to dead letter.`);
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
this.pending.unshift({ reports: failed.reports, attempts: failed.attempts, retrying: true });
|
|
256
|
-
const delay = Math.min(
|
|
257
|
-
this.retryMaxDelayMs,
|
|
258
|
-
this.retryBaseDelayMs * 2 ** Math.max(0, failed.attempts - 1),
|
|
259
|
-
);
|
|
260
|
-
this.onFailure(`${message} Retrying in ${Math.ceil(delay / 1000)}s.`);
|
|
261
|
-
this.retryTimer = setTimeout(() => {
|
|
262
|
-
this.retryTimer = undefined;
|
|
263
|
-
if (generation === this.generation) this.schedulePump();
|
|
264
|
-
}, delay);
|
|
265
|
-
this.retryTimer.unref?.();
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private clearRetryTimer(): void {
|
|
269
|
-
if (!this.retryTimer) return;
|
|
270
|
-
clearTimeout(this.retryTimer);
|
|
271
|
-
this.retryTimer = undefined;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
private clearWatchdog(): void {
|
|
275
|
-
if (!this.watchdogTimer) return;
|
|
276
|
-
clearTimeout(this.watchdogTimer);
|
|
277
|
-
this.watchdogTimer = undefined;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export function formatReports(reports: FollowUpReport[]): string {
|
|
282
|
-
return reports
|
|
283
|
-
.map((report) => {
|
|
284
|
-
const { teammate, body, timestamp, origin, harnessEvent } = report;
|
|
285
|
-
const at = timestamp !== undefined && Number.isFinite(timestamp)
|
|
286
|
-
? ` at="${new Date(timestamp).toISOString()}"`
|
|
287
|
-
: "";
|
|
288
|
-
if (origin === "harness" || harnessEvent) {
|
|
289
|
-
const type = escapeAttribute(harnessEvent?.type ?? "event");
|
|
290
|
-
const subject = escapeAttribute(harnessEvent?.subject ?? "Agent Teams event");
|
|
291
|
-
return `<harness-event type="${type}" subject="${subject}"${at}>\n${body}\n</harness-event>`;
|
|
292
|
-
}
|
|
293
|
-
const name = teammate ?? "teammate";
|
|
294
|
-
return `<agent-message from="${escapeAttribute(name)}"${at}>\n${body}\n</agent-message>`;
|
|
295
|
-
})
|
|
296
|
-
.join("\n\n");
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function escapeAttribute(value: string): string {
|
|
300
|
-
return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
301
|
-
}
|