@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/tools.ts
CHANGED
|
@@ -1,213 +1,198 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { keyHint, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { formatAgentMessagePrefix, formatAgentTaskName, formatExpandHint, formatToolEventLabel, safeDisplayText } from "@fradser/pi-kit";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
cancelNodeAndTerminate,
|
|
7
|
-
cancelRunAndTerminate,
|
|
8
|
-
ensureRunContext,
|
|
4
|
+
createBoardTask,
|
|
5
|
+
formatSilenceDuration,
|
|
9
6
|
publishStateSnapshot,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
7
|
+
sendLeaderMessage,
|
|
8
|
+
shutdownTeammate,
|
|
9
|
+
spawnTeammate,
|
|
10
|
+
} from "./team-machine.ts";
|
|
11
|
+
import { listTasks, livingTeammates } from "./state.ts";
|
|
12
|
+
import { LEADER_RECIPIENT, SendMessageParams, TeammateShutdownParams, TeammateSpawnParams, TaskCreateParams } from "./types.ts";
|
|
13
|
+
import { registerTaskListTool } from "./worker.ts";
|
|
14
|
+
import { openTeamConsole, refreshTeamUI } from "./ui.ts";
|
|
15
|
+
import { discoverAgents } from "./agents.ts";
|
|
16
|
+
import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
17
17
|
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
function sleep(ms: number): Promise<void> {
|
|
23
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function gatherForeground(runId: string, signal: AbortSignal | undefined): Promise<string> {
|
|
27
|
-
const foregroundTimeoutMs = 5 * 60 * 1000;
|
|
28
|
-
const deadline = Date.now() + foregroundTimeoutMs;
|
|
29
|
-
while (true) {
|
|
30
|
-
applyWorkerEvents();
|
|
31
|
-
const run = getRun(runId);
|
|
32
|
-
if (!run || run.status !== "running") break;
|
|
33
|
-
if (signal?.aborted) {
|
|
34
|
-
run.background = true;
|
|
35
|
-
run.completionNotified = false;
|
|
36
|
-
publishStateSnapshot();
|
|
37
|
-
throw new Error(`Run [${runId}] continues in the background — workers will message team-leader upon completion.`);
|
|
38
|
-
}
|
|
39
|
-
if (Date.now() >= deadline) {
|
|
40
|
-
run.background = true;
|
|
41
|
-
run.completionNotified = false;
|
|
42
|
-
publishStateSnapshot();
|
|
43
|
-
return `Run [${runId}] is still running after ${Math.round(foregroundTimeoutMs / 1000)}s — detached to background. Workers will message team-leader upon completion.`;
|
|
44
|
-
}
|
|
45
|
-
await sleep(150);
|
|
46
|
-
}
|
|
47
|
-
markRunCompletionDelivered(runId);
|
|
48
|
-
publishStateSnapshot();
|
|
49
|
-
return buildRunSummary(runId);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function resolveTargetNode(target: string): { node: import("./types").Node } | undefined {
|
|
53
|
-
return listNodes().map((node) => ({ node })).find(({ node }) => node.id === target || node.workerKey === target);
|
|
18
|
+
function rosterSummary(): string {
|
|
19
|
+
const alive = livingTeammates();
|
|
20
|
+
if (alive.length === 0) return "No living teammates.";
|
|
21
|
+
return alive.map((t) => `@${t.name} (${t.agent}, ${t.status}${t.currentTaskId ? `, task ${t.currentTaskId}` : ""})`).join("\n");
|
|
54
22
|
}
|
|
55
23
|
|
|
56
24
|
export function registerLeaderTools(pi: ExtensionAPI): void {
|
|
57
25
|
pi.registerTool({
|
|
58
|
-
name: "
|
|
59
|
-
promptSnippet: "
|
|
60
|
-
label: "
|
|
61
|
-
description: "
|
|
62
|
-
parameters:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
throw new Error(`Agent "${task.agent}" not found in any scope. Available agents: ${available || "(none)"}.`);
|
|
72
|
-
}
|
|
26
|
+
name: "teammate_spawn",
|
|
27
|
+
promptSnippet: "Spawn a named resident teammate",
|
|
28
|
+
label: "Spawn Teammate",
|
|
29
|
+
description: "Spawn one named resident teammate. Model and worktree behavior come from its declarative agent definition; the kickoff prompt is optional.",
|
|
30
|
+
parameters: TeammateSpawnParams,
|
|
31
|
+
// Canonical lifecycle rows (same as packages/monitor): empty renderCall,
|
|
32
|
+
// ONE startup row owned by renderResult.
|
|
33
|
+
renderShell: "self",
|
|
34
|
+
renderCall: () => new Text("", 0, 0),
|
|
35
|
+
renderResult(result, _options, theme, context) {
|
|
36
|
+
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
37
|
+
if (context.isError) {
|
|
38
|
+
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to spawn teammate."), 0, 0);
|
|
73
39
|
}
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
prompt: task.prompt,
|
|
85
|
-
paths: task.paths,
|
|
86
|
-
access: task.access ?? "read",
|
|
87
|
-
model: task.model,
|
|
88
|
-
mode: task.mode,
|
|
89
|
-
turnBudget: task.turnBudget,
|
|
90
|
-
forkContext: task.forkContext,
|
|
91
|
-
inputBindings: task.inputBindings,
|
|
92
|
-
dependsOn: task.dependsOn ?? [],
|
|
93
|
-
})),
|
|
94
|
-
});
|
|
95
|
-
if (!created.ok) throw new Error(created.error ?? "Failed to create run.");
|
|
96
|
-
const run = created.run;
|
|
97
|
-
publishStateSnapshot();
|
|
40
|
+
const params = context.args as { name: string; agent: string; prompt?: string };
|
|
41
|
+
const line = `${theme.fg("toolTitle", theme.bold(formatToolEventLabel("started", "", "agent").trimEnd()))} ${theme.fg("accent", `@${params.name}`)} ${theme.fg("dim", "·")} ${theme.fg("customMessageText", theme.bold(formatAgentTaskName(params.prompt ?? "", params.name)))}`;
|
|
42
|
+
return {
|
|
43
|
+
render: (width: number) => width > 0 ? [truncateToWidth(line, width)] : [],
|
|
44
|
+
invalidate: () => {},
|
|
45
|
+
};
|
|
46
|
+
},
|
|
47
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
48
|
+
const result = spawnTeammate(params);
|
|
49
|
+
if (!result.ok) throw new Error(result.error);
|
|
98
50
|
refreshTeamUI(ctx);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
return { content: [{ type: "text", text: await gatherForeground(run.id, signal) }], details: {} };
|
|
51
|
+
const kickoffNote = params.prompt?.trim()
|
|
52
|
+
? "It received your kickoff prompt and is working on it."
|
|
53
|
+
: "It is idle: it wakes for inbox messages and claimable board tasks.";
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: `@${params.name} is alive as ${params.agent}.\n${kickoffNote}\n\n${rosterSummary()}` }],
|
|
56
|
+
details: { started: true },
|
|
57
|
+
};
|
|
108
58
|
},
|
|
109
59
|
});
|
|
110
60
|
|
|
111
61
|
pi.registerTool({
|
|
112
|
-
name: "
|
|
113
|
-
promptSnippet: "
|
|
114
|
-
label: "
|
|
115
|
-
description: "
|
|
116
|
-
parameters:
|
|
62
|
+
name: "teammate_shutdown",
|
|
63
|
+
promptSnippet: "Shut down a resident teammate",
|
|
64
|
+
label: "Shutdown Teammate",
|
|
65
|
+
description: "Gracefully stop one named teammate. Its claimed task returns to the board; its worktree diff is captured before teardown.",
|
|
66
|
+
parameters: TeammateShutdownParams,
|
|
67
|
+
renderShell: "self",
|
|
68
|
+
renderCall: () => new Text("", 0, 0),
|
|
69
|
+
renderResult(result, options, theme, context) {
|
|
70
|
+
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
71
|
+
if (context.isError) {
|
|
72
|
+
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to shut down teammate."), 0, 0);
|
|
73
|
+
}
|
|
74
|
+
const name = String((context.args as { name?: string }).name ?? "");
|
|
75
|
+
const title = theme.fg("toolTitle", theme.bold(formatToolEventLabel("event", `@${name} shut down`, "agent")));
|
|
76
|
+
const render = (width: number) => {
|
|
77
|
+
if (width <= 0) return [];
|
|
78
|
+
if (!options.expanded) {
|
|
79
|
+
return [truncateToWidth(`${title}${formatExpandHint(keyHint("app.tools.expand", "to expand"), theme)}`, width)];
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
truncateToWidth(title, width),
|
|
83
|
+
...text.split("\n").map((line) => truncateToWidth(theme.fg("customMessageText", safeDisplayText(line)), width)),
|
|
84
|
+
];
|
|
85
|
+
};
|
|
86
|
+
return { render, invalidate: () => {} };
|
|
87
|
+
},
|
|
117
88
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
118
|
-
const
|
|
119
|
-
if (!
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const cwd = ctx.cwd ?? process.cwd();
|
|
123
|
-
const dctx = dispatchContext(ctx);
|
|
124
|
-
ensureRunContext(dctx);
|
|
125
|
-
const tasks = source.structuredOutput.map((item, index) => ({
|
|
126
|
-
id: `${params.nodeId}-${index + 1}`,
|
|
127
|
-
agent: params.agent,
|
|
128
|
-
prompt: `${params.prompt}\n\nFanout item:\n${JSON.stringify(item)}`,
|
|
129
|
-
paths: params.paths,
|
|
130
|
-
access: params.access ?? "read" as const,
|
|
131
|
-
model: params.model,
|
|
132
|
-
mode: "json" as const,
|
|
133
|
-
turnBudget: params.turnBudget,
|
|
134
|
-
dependsOn: [],
|
|
135
|
-
}));
|
|
136
|
-
if (!resolveAgent(params.agent, cwd)) throw new Error(`Agent "${params.agent}" not found.`);
|
|
137
|
-
const created = createRun({ cwd, concurrency: params.concurrency ?? 4, worktree: false, background: params.background ?? true, summarize: true, nodes: tasks });
|
|
138
|
-
if (!created.ok) throw new Error(created.error ?? "Failed to create fanout run.");
|
|
139
|
-
publishStateSnapshot();
|
|
140
|
-
scheduleRun(created.run.id, dctx);
|
|
141
|
-
return { content: [{ type: "text", text: `Started fanout run [${created.run.id}] with ${tasks.length} item(s).` }], details: {} };
|
|
89
|
+
const result = await shutdownTeammate(params.name);
|
|
90
|
+
if (!result.ok) throw new Error(result.error);
|
|
91
|
+
refreshTeamUI(ctx);
|
|
92
|
+
return { content: [{ type: "text", text: result.body }], details: {} };
|
|
142
93
|
},
|
|
143
94
|
});
|
|
144
95
|
|
|
145
96
|
pi.registerTool({
|
|
146
|
-
name: "
|
|
147
|
-
promptSnippet: "Send a
|
|
148
|
-
label: "Message
|
|
149
|
-
description: "
|
|
150
|
-
parameters:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
97
|
+
name: "send_message",
|
|
98
|
+
promptSnippet: "Send a message to a teammate",
|
|
99
|
+
label: "Send Message",
|
|
100
|
+
description: "The only messaging primitive. Address a living teammate by name; working teammates receive a steer immediately and idle teammates wake with the message. status is reserved for worker reports to leader.",
|
|
101
|
+
parameters: SendMessageParams,
|
|
102
|
+
// Canonical lifecycle rows (same as packages/monitor): empty call slot,
|
|
103
|
+
// ONE delivery row owned by renderResult.
|
|
104
|
+
renderShell: "self",
|
|
105
|
+
renderCall: () => new Text("", 0, 0),
|
|
106
|
+
renderResult(result, _options, theme, context) {
|
|
107
|
+
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
108
|
+
if (context.isError) {
|
|
109
|
+
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to send message."), 0, 0);
|
|
110
|
+
}
|
|
111
|
+
const to = String((context.args as { to?: string }).to ?? "");
|
|
112
|
+
const details = result.details as { queued?: boolean; stalledMs?: number } | undefined;
|
|
113
|
+
const suffix = details?.queued ? "queued" : "delivered";
|
|
114
|
+
let line = `${theme.fg("toolTitle", theme.bold(formatAgentMessagePrefix("to")))}${theme.fg("accent", `@${to}`)}${theme.fg("dim", ` · ${suffix}`)}`;
|
|
115
|
+
if (!details?.queued && details?.stalledMs !== undefined) {
|
|
116
|
+
line += theme.fg("dim", ` · stalled ${formatSilenceDuration(details.stalledMs)}`);
|
|
156
117
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
118
|
+
return {
|
|
119
|
+
render: (width: number) => width > 0 ? [truncateToWidth(line, width)] : [],
|
|
120
|
+
invalidate: () => {},
|
|
121
|
+
};
|
|
160
122
|
},
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
promptSnippet: "Cancel a run and stop its workers",
|
|
166
|
-
label: "Cancel Run",
|
|
167
|
-
description: "Cancel a run or one node. Running workers receive SIGTERM with bounded SIGKILL escalation; pending nodes are cancelled.",
|
|
168
|
-
parameters: TeammateCancelParams,
|
|
169
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
170
|
-
const dctx = dispatchContext(ctx);
|
|
171
|
-
ensureRunContext(dctx);
|
|
172
|
-
const result = params.nodeId
|
|
173
|
-
? await cancelNodeAndTerminate(params.runId, params.nodeId, dctx)
|
|
174
|
-
: await cancelRunAndTerminate(params.runId, dctx);
|
|
123
|
+
async execute(_toolCallId, params) {
|
|
124
|
+
if (params.to === LEADER_RECIPIENT) throw new Error('The leader cannot send a message to itself.');
|
|
125
|
+
if (params.status) throw new Error('status is reserved for worker reports to="leader".');
|
|
126
|
+
const result = sendLeaderMessage(params.to, params.message);
|
|
175
127
|
if (!result.ok) throw new Error(result.error);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
128
|
+
const disposition = result.queued ? "Queued for its next wake-up." : "Delivered to its running turn.";
|
|
129
|
+
let text = `Message to @${params.to}: ${disposition}`;
|
|
130
|
+
if (!result.queued && result.stalledMs !== undefined) {
|
|
131
|
+
text += `\nWarning: @${params.to} has been stalled with no output for ${formatSilenceDuration(result.stalledMs)} — the control-stream write succeeded, but the teammate may be wedged. Consider teammate_shutdown if it stays silent.`;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
content: [{ type: "text", text }],
|
|
135
|
+
details: result.stalledMs !== undefined && !result.queued
|
|
136
|
+
? { queued: result.queued, stalledMs: result.stalledMs }
|
|
137
|
+
: { queued: result.queued },
|
|
138
|
+
};
|
|
179
139
|
},
|
|
180
140
|
});
|
|
181
141
|
|
|
182
142
|
pi.registerTool({
|
|
183
|
-
name: "
|
|
184
|
-
promptSnippet: "
|
|
185
|
-
label: "
|
|
186
|
-
description: "
|
|
187
|
-
parameters:
|
|
143
|
+
name: "task_create",
|
|
144
|
+
promptSnippet: "Create a shared board task",
|
|
145
|
+
label: "Create Task",
|
|
146
|
+
description: "Create one shared board task. Resident teammates self-claim it when dependencies are met; an optional verify command gates completion.",
|
|
147
|
+
parameters: TaskCreateParams,
|
|
148
|
+
// Canonical lifecycle rows (same as packages/monitor): empty call slot,
|
|
149
|
+
// ONE created row owned by renderResult.
|
|
150
|
+
renderShell: "self",
|
|
151
|
+
renderCall: () => new Text("", 0, 0),
|
|
152
|
+
renderResult(result, _options, theme, context) {
|
|
153
|
+
const text = result.content[0]?.type === "text" ? (result.content[0] as { type: string; text: string }).text : "";
|
|
154
|
+
if (context.isError) {
|
|
155
|
+
return new Text(theme.fg("error", text.split("\n")[0] || "Failed to create task."), 0, 0);
|
|
156
|
+
}
|
|
157
|
+
const subject = String((context.args as { subject?: string }).subject ?? "");
|
|
158
|
+
const line = theme.fg("toolTitle", theme.bold(formatToolEventLabel("created", subject, "board")));
|
|
159
|
+
return {
|
|
160
|
+
render: (width: number) => width > 0 ? [truncateToWidth(line, width)] : [],
|
|
161
|
+
invalidate: () => {},
|
|
162
|
+
};
|
|
163
|
+
},
|
|
188
164
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
165
|
+
const tasks = listTasks();
|
|
166
|
+
if (params.dependsOn?.some((dep) => !tasks.some((task) => task.id === dep))) {
|
|
167
|
+
throw new Error(`Unknown dependency id in [${params.dependsOn.join(", ")}].`);
|
|
168
|
+
}
|
|
169
|
+
const created = createBoardTask(params);
|
|
170
|
+
if (!created.ok) throw new Error(created.error);
|
|
195
171
|
refreshTeamUI(ctx);
|
|
196
|
-
return { content: [{ type: "text", text: `
|
|
172
|
+
return { content: [{ type: "text", text: `Created [${created.id}] "${params.subject}". Idle teammates are notified automatically.` }], details: {} };
|
|
197
173
|
},
|
|
198
174
|
});
|
|
175
|
+
|
|
176
|
+
registerTaskListTool(pi);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function teamStatusSummary(): string {
|
|
180
|
+
const roles = [...discoverAgents().keys()].map((name) => `@${name}`);
|
|
181
|
+
const rolesLine = roles.length > 0
|
|
182
|
+
? `${roles.length} persistent agent role${roles.length === 1 ? "" : "s"}: ${roles.join(", ")}`
|
|
183
|
+
: "No agent roles discovered.";
|
|
184
|
+
return `${rosterSummary()}\n${rolesLine}`;
|
|
199
185
|
}
|
|
200
186
|
|
|
201
187
|
export function registerTeamCommand(pi: ExtensionAPI): void {
|
|
202
|
-
pi.registerCommand("
|
|
203
|
-
description: "
|
|
188
|
+
pi.registerCommand("agent-teams", {
|
|
189
|
+
description: "Agent Teams management console: session teammates and persistent agent roles",
|
|
204
190
|
handler: async (_args, ctx) => {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
191
|
+
publishStateSnapshot();
|
|
192
|
+
if (ctx.mode !== "tui") { ctx.ui.notify(teamStatusSummary(), "info"); return; }
|
|
209
193
|
await openTeamConsole(ctx);
|
|
210
194
|
refreshTeamUI(ctx);
|
|
211
195
|
},
|
|
212
196
|
});
|
|
213
197
|
}
|
|
198
|
+
|