@fradser/pi-agent-teams 0.5.3 → 0.7.0
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 +3 -1
- package/.memory/node-terminal-followups.md +3 -2
- package/.memory/team-centric-design.md +21 -0
- package/README.md +103 -63
- package/package.json +4 -4
- package/references/agent-roles.md +77 -0
- package/src/activity.ts +86 -0
- package/src/agents.ts +93 -24
- package/src/follow-up-queue.ts +94 -26
- package/src/guidance.ts +96 -61
- package/src/index.ts +114 -28
- package/src/spawner.ts +194 -350
- package/src/state.ts +252 -565
- package/src/statefile.ts +209 -46
- package/src/team-machine.ts +968 -0
- package/src/tools.ts +160 -175
- package/src/types.ts +153 -189
- package/src/ui.ts +459 -226
- package/src/worker.ts +174 -34
- package/src/worktree.ts +20 -8
- package/.memory/run-centric-design.md +0 -22
- package/agents/observer.md +0 -10
- package/agents/reviewer.md +0 -11
- package/agents/specialist.md +0 -10
- package/agents/worker.md +0 -11
- package/src/run-machine.ts +0 -606
- package/src/terminal.ts +0 -34
package/src/index.ts
CHANGED
|
@@ -1,38 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-agent-teams-fradser — Pi extension for
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* run-machine.ts; the passive widget and console live in ui.ts.
|
|
2
|
+
* pi-agent-teams-fradser — Pi extension for Claude-style resident agent
|
|
3
|
+
* teams. Composition only: worker capability registration, session lifecycle,
|
|
4
|
+
* and delegation to tools.ts. Coordination lives in team-machine.ts; the
|
|
5
|
+
* passive widget and console live in ui.ts.
|
|
7
6
|
*/
|
|
8
7
|
|
|
8
|
+
import { getMarkdownTheme, keyHint } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { buildTeamLeaderGuidance, WORKER_GUIDANCE } from "./guidance";
|
|
11
|
-
import {
|
|
12
|
-
import { cleanupExpiredStateDirs
|
|
13
|
-
import { resetState } from "./state";
|
|
14
|
-
import { ensureTeamWidget, refreshTeamUI, stopUiTimers } from "./ui";
|
|
15
|
-
import { registerLeaderTools, registerTeamCommand } from "./tools";
|
|
16
|
-
import { registerWorkerCapabilities,
|
|
17
|
-
import {
|
|
18
|
-
import { FollowUpQueue } from "./follow-up-queue";
|
|
10
|
+
import { buildTeamLeaderGuidance, WORKER_GUIDANCE } from "./guidance.ts";
|
|
11
|
+
import { initTeamMachine, removeRuntimeDir, shutdownTeamMachine, teardownTeammates } from "./team-machine.ts";
|
|
12
|
+
import { cleanupExpiredStateDirs } from "./statefile.ts";
|
|
13
|
+
import { resetState } from "./state.ts";
|
|
14
|
+
import { ensureTeamWidget, refreshTeamUI, stopUiTimers } from "./ui.ts";
|
|
15
|
+
import { registerLeaderTools, registerTeamCommand } from "./tools.ts";
|
|
16
|
+
import { registerWorkerCapabilities, workerBinding } from "./worker.ts";
|
|
17
|
+
import { formatAgentMessagePrefix, formatExpandHint } from "@fradser/pi-kit";
|
|
18
|
+
import { FollowUpQueue, groupReportsByTeammate, TEAMMATE_REPORT_MESSAGE_TYPE, type FollowUpReport } from "./follow-up-queue.ts";
|
|
19
|
+
import { Box, Markdown, Text } from "@earendil-works/pi-tui";
|
|
19
20
|
|
|
20
21
|
const STATE_DIR_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
21
22
|
|
|
22
23
|
let leaderPi: ExtensionAPI | undefined;
|
|
23
24
|
let leaderCtx: ExtensionContext | undefined;
|
|
24
25
|
let followUpQueue: FollowUpQueue | undefined;
|
|
26
|
+
// One "Teammate finished" announcement per spawn incarnation; repeated
|
|
27
|
+
// terminal reports from the same resident stay ordinary report rows.
|
|
28
|
+
const announcedFinishKeys = new Set<string>();
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
const REPORT_COLORS = ["success", "warning", "error", "mdLink"] as const;
|
|
31
|
+
export const TEAMMATE_FINISHED_ENTRY_TYPE = "agent-teams-teammate-finished";
|
|
32
|
+
|
|
33
|
+
function reportColor(name: string): (typeof REPORT_COLORS)[number] {
|
|
34
|
+
let hash = 0;
|
|
35
|
+
for (const char of name) hash = (hash * 31 + char.charCodeAt(0)) | 0;
|
|
36
|
+
return REPORT_COLORS[Math.abs(hash) % REPORT_COLORS.length];
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
function
|
|
31
|
-
|
|
39
|
+
function sendMainSessionFollowUp(report: FollowUpReport): void {
|
|
40
|
+
followUpQueue?.enqueue(report);
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
export default function (pi: ExtensionAPI) {
|
|
35
|
-
if (
|
|
44
|
+
if (workerBinding()) {
|
|
36
45
|
pi.on("before_agent_start", async (event) => ({
|
|
37
46
|
systemPrompt: event.systemPrompt + WORKER_GUIDANCE,
|
|
38
47
|
}));
|
|
@@ -41,23 +50,73 @@ export default function (pi: ExtensionAPI) {
|
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
leaderPi = pi;
|
|
53
|
+
pi.registerEntryRenderer(TEAMMATE_FINISHED_ENTRY_TYPE, (entry, _options, theme) => {
|
|
54
|
+
const data = entry.data as { teammate?: string; agent?: string } | undefined;
|
|
55
|
+
const name = data?.teammate ?? data?.agent ?? "teammate";
|
|
56
|
+
return new Text(theme.fg("success", `Teammate @${name} finished.`), 0, 0);
|
|
57
|
+
});
|
|
58
|
+
pi.registerMessageRenderer(TEAMMATE_REPORT_MESSAGE_TYPE, (message, { expanded, outputPad }, theme) => {
|
|
59
|
+
const reports = extractReports(message.details);
|
|
60
|
+
const box = new Box(outputPad, 1, (text) => theme.bg("customMessageBg", text));
|
|
61
|
+
if (reports.length === 0) {
|
|
62
|
+
box.addChild(new Markdown(String(message.content), 0, 0, getMarkdownTheme()));
|
|
63
|
+
return box;
|
|
64
|
+
}
|
|
65
|
+
if (!expanded) {
|
|
66
|
+
const groups = groupReportsByTeammate(reports);
|
|
67
|
+
const hint = formatExpandHint(keyHint("app.tools.expand", "to expand"), theme);
|
|
68
|
+
for (const group of groups) {
|
|
69
|
+
const prefix = theme.fg("customMessageLabel", theme.bold(formatAgentMessagePrefix("from", group.reports.length)));
|
|
70
|
+
const name = theme.fg(reportColor(group.teammate), `@${group.teammate}`);
|
|
71
|
+
box.addChild(new Text(`${prefix}${name}${hint}`, 0, 0));
|
|
72
|
+
}
|
|
73
|
+
return box;
|
|
74
|
+
}
|
|
75
|
+
for (const [index, report] of reports.entries()) {
|
|
76
|
+
const teammate = report.teammate ?? report.agent ?? "teammate";
|
|
77
|
+
const prefix = theme.fg("customMessageLabel", theme.bold(formatAgentMessagePrefix("from")));
|
|
78
|
+
const name = theme.fg(reportColor(teammate), `@${teammate}`);
|
|
79
|
+
box.addChild(new Text(`${prefix}${name}`, 0, 0));
|
|
80
|
+
box.addChild(new Markdown(report.body, 0, 0, getMarkdownTheme(), {
|
|
81
|
+
color: (text) => theme.fg("customMessageText", text),
|
|
82
|
+
}));
|
|
83
|
+
if (index < reports.length - 1) box.addChild(new Text("", 0, 0));
|
|
84
|
+
}
|
|
85
|
+
return box;
|
|
86
|
+
});
|
|
44
87
|
registerLeaderTools(pi);
|
|
45
88
|
registerTeamCommand(pi);
|
|
46
89
|
|
|
90
|
+
pi.on("message_end", async (event) => {
|
|
91
|
+
if (event.message.role !== "custom" || event.message.customType !== TEAMMATE_REPORT_MESSAGE_TYPE) return;
|
|
92
|
+
const reports = extractReports(event.message.details);
|
|
93
|
+
for (const report of reports) {
|
|
94
|
+
if (!markTeammateFinished(announcedFinishKeys, report)) continue;
|
|
95
|
+
pi.appendEntry(TEAMMATE_FINISHED_ENTRY_TYPE, {
|
|
96
|
+
teammate: report.teammate ?? report.agent,
|
|
97
|
+
agent: report.agent,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
47
102
|
pi.on("session_start", async (_event, ctx) => {
|
|
48
103
|
resetState();
|
|
49
104
|
followUpQueue?.reset();
|
|
105
|
+
announcedFinishKeys.clear();
|
|
50
106
|
leaderCtx = ctx;
|
|
51
|
-
|
|
107
|
+
followUpQueue = new FollowUpQueue({
|
|
52
108
|
isIdle: () => Boolean(leaderCtx?.isIdle()),
|
|
53
|
-
|
|
109
|
+
prepareOnDispatch: true,
|
|
110
|
+
dispatch: (reports, content) => leaderPi?.sendMessage({
|
|
111
|
+
customType: TEAMMATE_REPORT_MESSAGE_TYPE,
|
|
112
|
+
content,
|
|
113
|
+
display: true,
|
|
114
|
+
details: reports.length === 1 ? reports[0] : { reports },
|
|
115
|
+
}, { triggerTurn: true, deliverAs: "followUp" }),
|
|
54
116
|
onFailure: (message) => leaderCtx?.ui.notify(message, "warning"),
|
|
55
117
|
});
|
|
56
|
-
followUpQueue = sessionQueue;
|
|
57
|
-
const dispatchCtx = dispatchContext(ctx);
|
|
58
118
|
ensureTeamWidget(ctx);
|
|
59
|
-
|
|
60
|
-
initRunMachine(dispatchCtx, stateFile, {
|
|
119
|
+
initTeamMachine(ctx, {
|
|
61
120
|
sendUpdate: sendMainSessionFollowUp,
|
|
62
121
|
notifyChange: () => refreshTeamUI(leaderCtx),
|
|
63
122
|
});
|
|
@@ -83,14 +142,41 @@ export default function (pi: ExtensionAPI) {
|
|
|
83
142
|
|
|
84
143
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
85
144
|
stopUiTimers();
|
|
86
|
-
await
|
|
87
|
-
|
|
88
|
-
|
|
145
|
+
const diagnostics = await teardownTeammates();
|
|
146
|
+
for (const message of diagnostics) {
|
|
147
|
+
ctx.ui.notify(message, "warning");
|
|
148
|
+
}
|
|
149
|
+
shutdownTeamMachine();
|
|
150
|
+
removeRuntimeDir(ctx);
|
|
89
151
|
followUpQueue?.reset();
|
|
90
152
|
followUpQueue = undefined;
|
|
91
153
|
leaderPi = undefined;
|
|
92
154
|
leaderCtx = undefined;
|
|
93
155
|
resetState();
|
|
94
156
|
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractReports(details: unknown): FollowUpReport[] {
|
|
160
|
+
const typed = details as FollowUpReport | { reports?: FollowUpReport[] } | undefined;
|
|
161
|
+
if (typed && "reports" in typed && Array.isArray(typed.reports)) return typed.reports;
|
|
162
|
+
if (typed && "teammate" in typed) return [typed as FollowUpReport];
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
95
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Announce a finished entry only on the first terminal report of a spawn
|
|
168
|
+
* incarnation. Reports without a spawn identity key on the session scope so
|
|
169
|
+
* crash diagnostics always stay visible. Returns true when the caller should
|
|
170
|
+
* append the finished entry.
|
|
171
|
+
*/
|
|
172
|
+
export function markTeammateFinished(
|
|
173
|
+
seen: Set<string>,
|
|
174
|
+
report: Pick<FollowUpReport, "teammate" | "agent" | "spawnId" | "finished">,
|
|
175
|
+
): boolean {
|
|
176
|
+
if (!report.finished) return false;
|
|
177
|
+
const name = report.teammate ?? report.agent ?? "teammate";
|
|
178
|
+
const key = `${name}:${report.spawnId ?? "session"}`;
|
|
179
|
+
if (seen.has(key)) return false;
|
|
180
|
+
seen.add(key);
|
|
181
|
+
return true;
|
|
96
182
|
}
|