@fradser/pi-agent-teams 0.9.2 → 1.0.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 +1 -0
- package/.memory/attempt-bound-authority.md +19 -0
- package/README.md +137 -58
- package/package.json +13 -5
- package/references/agent-roles.md +23 -13
- package/src/activity.ts +20 -11
- package/src/agent-actions.ts +61 -36
- package/src/agent-host.ts +64 -0
- package/src/capability-tools.ts +34 -0
- package/src/guidance.ts +49 -37
- package/src/index.ts +149 -30
- package/src/recipient.ts +4 -4
- package/src/state.ts +163 -607
- package/src/statefile.ts +28 -244
- package/src/team-machine.ts +827 -535
- package/src/tool-copy.ts +26 -34
- package/src/tools.ts +57 -210
- package/src/types.ts +56 -157
- package/src/ui.ts +85 -59
- package/src/worker.ts +152 -79
- package/src/agents.ts +0 -296
- package/src/leader-reports.ts +0 -92
- package/src/spawner.ts +0 -751
- package/src/work-context.ts +0 -88
- package/src/worker-tools.ts +0 -4
- package/src/worktree.ts +0 -164
package/src/tool-copy.ts
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { contentDetailLines, detailField, displayText, fieldBlock, fieldLine, safeDisplayText, scrubHandles } from "@fradser/pi-kit";
|
|
12
|
-
import { resolveAgent } from "
|
|
12
|
+
import { resolveAgent } from "@fradser/pi-subagents";
|
|
13
13
|
import { runningTeammateActivity } from "./activity.ts";
|
|
14
14
|
import { parseExactSessionRoute } from "./recipient.ts";
|
|
15
|
-
import { getTask,
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
15
|
+
import { getTask, getAgent } from "./state.ts";
|
|
16
|
+
import { resolveAgentTools } from "@fradser/pi-subagents";
|
|
17
|
+
import { WORKER_CAPABILITY_TOOLS } from "./capability-tools.ts";
|
|
18
|
+
import type { Task, Agent } from "./types.ts";
|
|
18
19
|
|
|
19
20
|
/** One rendered lifecycle row: the collapsed line plus its expanded body. */
|
|
20
21
|
export interface CoordinationRow {
|
|
@@ -24,10 +25,6 @@ export interface CoordinationRow {
|
|
|
24
25
|
body: string[];
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const COLLAPSED_TASK_LIMIT = 90;
|
|
28
|
-
/** A kickoff prompt is shown in full up to this size, then clipped. */
|
|
29
|
-
const FIELD_LIMIT = 2000;
|
|
30
|
-
|
|
31
28
|
/** Package resolver: a known Work handle becomes its subject. Only prefixed
|
|
32
29
|
* handles are machine identifiers; a bare UUID can be a person's own text
|
|
33
30
|
* (a commit, an id under review) and survives verbatim inside kit. */
|
|
@@ -55,11 +52,12 @@ function clipLines(value: unknown): string {
|
|
|
55
52
|
return displayText(value).split("\n").map((line) => plainText(line)).join("\n");
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
/** The first non-empty line, on one line: a title. The row's own width fit
|
|
56
|
+
* bounds it and expansion reveals the whole value, so no character cap here. */
|
|
57
|
+
function headline(value: unknown): string {
|
|
59
58
|
const text = displayText(value);
|
|
60
59
|
const firstLine = text.split("\n").map((l) => plainText(l).trim()).find(Boolean) ?? "";
|
|
61
|
-
|
|
62
|
-
return line.length <= COLLAPSED_TASK_LIMIT ? line : `${line.slice(0, COLLAPSED_TASK_LIMIT - 1).trimEnd()}…`;
|
|
60
|
+
return collapse(firstLine);
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
/** Array fields also arrive untyped from a model call. */
|
|
@@ -72,8 +70,8 @@ function labeled(label: string, value: unknown): string {
|
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
/** Multi-line field: the label heads the first line, the rest follow verbatim. */
|
|
75
|
-
function labeledBlock(label: string, value: unknown
|
|
76
|
-
return fieldBlock(label, clipLines(value)
|
|
73
|
+
function labeledBlock(label: string, value: unknown): string[] {
|
|
74
|
+
return fieldBlock(label, clipLines(value));
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
function bodyLines(value: unknown): string[] {
|
|
@@ -94,7 +92,7 @@ const STATUS_WORDS: Record<string, string> = {
|
|
|
94
92
|
idle: "idle",
|
|
95
93
|
stopped: "stopped",
|
|
96
94
|
pending: "pending",
|
|
97
|
-
|
|
95
|
+
in_progress: "in progress",
|
|
98
96
|
completed: "completed",
|
|
99
97
|
superseded: "superseded",
|
|
100
98
|
sent: "sent",
|
|
@@ -114,11 +112,11 @@ function stateWord(value: unknown): string {
|
|
|
114
112
|
return STATUS_WORDS[key] ?? key.replace(/[_-]+/g, " ");
|
|
115
113
|
}
|
|
116
114
|
|
|
117
|
-
function workOf(teammate:
|
|
115
|
+
function workOf(teammate: Agent | undefined, task?: Task): string | undefined {
|
|
118
116
|
const source = task ?? (teammate?.workId ? getTask(teammate.workId) : undefined);
|
|
119
117
|
if (!source) return undefined;
|
|
120
118
|
const owner = source.claimedBy ? ` · @${source.claimedBy}` : "";
|
|
121
|
-
return `${
|
|
119
|
+
return `${headline(source.subject)} · ${stateWord(source.status)}${owner}`;
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
// ── agent ─────────────────────────────────────────────────────────
|
|
@@ -137,9 +135,6 @@ export interface AgentRowArgs {
|
|
|
137
135
|
tools?: string[];
|
|
138
136
|
model?: string;
|
|
139
137
|
verify?: string;
|
|
140
|
-
worktree?: boolean;
|
|
141
|
-
persist?: boolean;
|
|
142
|
-
persistScope?: string;
|
|
143
138
|
};
|
|
144
139
|
}
|
|
145
140
|
|
|
@@ -152,15 +147,13 @@ function agentName(args: AgentRowArgs, details: unknown): string {
|
|
|
152
147
|
function roleLine(args: AgentRowArgs, name: string): string | undefined {
|
|
153
148
|
const generated = args.definition;
|
|
154
149
|
if (generated?.description) {
|
|
155
|
-
return generated.
|
|
156
|
-
? `${oneLine(generated.description)} · saved as ${generated.persistScope ?? "project-local"} role`
|
|
157
|
-
: `${oneLine(generated.description)} · new role for this session`;
|
|
150
|
+
return `${oneLine(generated.description)} · new role for this session`;
|
|
158
151
|
}
|
|
159
152
|
const resolved = resolveAgent(name);
|
|
160
153
|
return resolved ? `${oneLine(resolved.description) || "role"} · ${resolved.scope} role` : undefined;
|
|
161
154
|
}
|
|
162
155
|
|
|
163
|
-
function agentStateWord(args: AgentRowArgs, teammate:
|
|
156
|
+
function agentStateWord(args: AgentRowArgs, teammate: Agent | undefined, details: unknown, flags: { isError?: boolean; isPartial?: boolean }): string {
|
|
164
157
|
if (flags.isError) return "failed";
|
|
165
158
|
const action = args.action ?? "agent";
|
|
166
159
|
const live = teammate ? stateWord(teammate.status) : undefined;
|
|
@@ -187,7 +180,7 @@ export function agentRow(
|
|
|
187
180
|
flags: { isError?: boolean; isPartial?: boolean } = {},
|
|
188
181
|
): CoordinationRow {
|
|
189
182
|
const name = agentName(args, details);
|
|
190
|
-
const teammate =
|
|
183
|
+
const teammate = getAgent(name);
|
|
191
184
|
const action = args.action ?? "agent";
|
|
192
185
|
let task: string | undefined;
|
|
193
186
|
if (action === "delegate") {
|
|
@@ -203,7 +196,7 @@ export function agentRow(
|
|
|
203
196
|
task = getTask(workId)?.subject;
|
|
204
197
|
}
|
|
205
198
|
}
|
|
206
|
-
const subject = [`@${name}`, agentStateWord(args, teammate, details, flags), task ?
|
|
199
|
+
const subject = [`@${name}`, agentStateWord(args, teammate, details, flags), task ? headline(task) : ""]
|
|
207
200
|
.filter(Boolean)
|
|
208
201
|
.join(" · ");
|
|
209
202
|
return { subject, body: agentBody(args, name, teammate, details, flags) };
|
|
@@ -212,7 +205,7 @@ export function agentRow(
|
|
|
212
205
|
function agentBody(
|
|
213
206
|
args: AgentRowArgs,
|
|
214
207
|
name: string,
|
|
215
|
-
teammate:
|
|
208
|
+
teammate: Agent | undefined,
|
|
216
209
|
details: unknown,
|
|
217
210
|
flags: { isError?: boolean },
|
|
218
211
|
): string[] {
|
|
@@ -230,7 +223,7 @@ function agentBody(
|
|
|
230
223
|
// field would duplicate the header, and could contradict it when the live
|
|
231
224
|
// teammate and the inspected snapshot disagree.
|
|
232
225
|
return [
|
|
233
|
-
...(isWorking && teammate ? [labeled("now",
|
|
226
|
+
...(isWorking && teammate ? [labeled("now", headline(runningTeammateActivity(teammate)))] : []),
|
|
234
227
|
...(work && isWorking ? [labeled("work", work)] : []),
|
|
235
228
|
...(sessionTools ? [labeled("tools", sessionTools.join(", "))] : []),
|
|
236
229
|
...(warning ? [labeled("warning", warning)] : []),
|
|
@@ -241,7 +234,7 @@ function agentBody(
|
|
|
241
234
|
}
|
|
242
235
|
const role = roleLine(args, name);
|
|
243
236
|
const resources = strings(args.resources);
|
|
244
|
-
const granted = teammate?.tools ??
|
|
237
|
+
const granted = teammate?.tools ?? resolveAgentTools(strings(args.definition?.tools).length > 0 ? strings(args.definition?.tools) : resolveAgent(name)?.tools, WORKER_CAPABILITY_TOOLS);
|
|
245
238
|
// The spawn receipt carries the same coordination-only warning as inspection,
|
|
246
239
|
// so delegate and start rows state the narrow grant they actually produced.
|
|
247
240
|
const sessionWarning = detailField<string>(detailField(details, "session"), "warning");
|
|
@@ -251,7 +244,6 @@ function agentBody(
|
|
|
251
244
|
labeled("model", teammate?.model ?? args.model ?? args.definition?.model ?? "team default"),
|
|
252
245
|
labeled("tools", granted.join(", ")),
|
|
253
246
|
...(sessionWarning ? [labeled("warning", sessionWarning)] : []),
|
|
254
|
-
...(teammate?.isolation === "worktree" ? [labeled("isolation", "dedicated git worktree")] : []),
|
|
255
247
|
...(teammate?.context === "fork" || args.fork ? [labeled("context", "forked from this session")] : []),
|
|
256
248
|
...(resources.length > 0 ? [labeled("resources", resources.join(", "))] : []),
|
|
257
249
|
...(() => {
|
|
@@ -276,7 +268,7 @@ export function leaderWorkRow(
|
|
|
276
268
|
const items = Array.isArray(works) ? works : [];
|
|
277
269
|
return {
|
|
278
270
|
subject: `${items.length} work item${items.length === 1 ? "" : "s"}`,
|
|
279
|
-
body: items.map((item) => `- ${
|
|
271
|
+
body: items.map((item) => `- ${headline(item.subject)} · ${stateWord(item.state)}${item.claimedBy ? ` · @${item.claimedBy}` : ""}`),
|
|
280
272
|
};
|
|
281
273
|
}
|
|
282
274
|
const work = detailField<{ subject?: string; state?: string; resources?: string[] }>(details, "work");
|
|
@@ -293,7 +285,7 @@ export function leaderWorkRow(
|
|
|
293
285
|
const workResources = strings(detailField<unknown>(work, "resources"));
|
|
294
286
|
const stillRunning = detailField<string>(details, "holderStillRunning");
|
|
295
287
|
return {
|
|
296
|
-
subject: `${
|
|
288
|
+
subject: `${headline(subject)} · ${flags.isError ? "failed" : stateWord(outcome)}`,
|
|
297
289
|
body: [
|
|
298
290
|
...(work?.state ? [labeled("state", stateWord(work.state))] : []),
|
|
299
291
|
...(stillRunning ? [labeled("risk", `@${stillRunning} was still working; an in-flight batch may still write inside the released scope`)] : []),
|
|
@@ -314,7 +306,7 @@ export function leaderWorkRow(
|
|
|
314
306
|
|
|
315
307
|
/** One `agent_event` row: who receives it and what was actually said. */
|
|
316
308
|
export function messageRow(
|
|
317
|
-
args: { to?: string;
|
|
309
|
+
args: { to?: string; body?: string; kind?: string },
|
|
318
310
|
details: unknown,
|
|
319
311
|
options: { isError?: boolean } = {},
|
|
320
312
|
): CoordinationRow {
|
|
@@ -322,7 +314,7 @@ export function messageRow(
|
|
|
322
314
|
const outcome = detailField<string>(details, "outcome");
|
|
323
315
|
// Messages are literal readbacks, not prose to clean up. Only terminal
|
|
324
316
|
// controls and runtime handles change; the shared renderer owns truncation.
|
|
325
|
-
const message = scrubHandles(safeDisplayText(displayText(args.
|
|
317
|
+
const message = scrubHandles(safeDisplayText(displayText(args.body)), workHandle);
|
|
326
318
|
const prefix = [`to @${to}`, options.isError ? "failed" : outcome ? stateWord(outcome) : undefined]
|
|
327
319
|
.filter(Boolean).join(" · ");
|
|
328
320
|
return {
|
|
@@ -351,7 +343,7 @@ export function workerWorkRow(
|
|
|
351
343
|
? "current Work"
|
|
352
344
|
: undefined;
|
|
353
345
|
return {
|
|
354
|
-
subject: `${
|
|
346
|
+
subject: `${headline(subject ?? leads ?? "Work")} · ${flags.isError ? "failed" : workerStateWord(action, outcome)}`,
|
|
355
347
|
body: bodyLines(content).slice(1),
|
|
356
348
|
};
|
|
357
349
|
}
|
package/src/tools.ts
CHANGED
|
@@ -1,88 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { CoordinationToolResult } from "./types.ts";
|
|
2
3
|
import { notifyPi } from "@fradser/pi-kit";
|
|
3
4
|
import {
|
|
4
|
-
assignExistingWork,
|
|
5
|
-
releaseExistingWork,
|
|
6
|
-
reopenExistingWork,
|
|
7
|
-
createBoardTask,
|
|
8
5
|
publishStateSnapshot,
|
|
9
6
|
sendLeaderMessage,
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
shutdownAgentExact,
|
|
8
|
+
spawnAgent,
|
|
12
9
|
} from "./team-machine.ts";
|
|
13
|
-
import {
|
|
10
|
+
import { livingAgents } from "./state.ts";
|
|
14
11
|
|
|
15
|
-
import {
|
|
12
|
+
import { AgentEventParams, LEADER_RECIPIENT } from "./types.ts";
|
|
16
13
|
import { openTeamConsole, refreshTeamUI } from "./ui.ts";
|
|
17
|
-
import { discoverAgents } from "
|
|
18
|
-
import {
|
|
14
|
+
import { discoverAgents } from "@fradser/pi-subagents";
|
|
15
|
+
import { messageRow } from "./tool-copy.ts";
|
|
19
16
|
import { emptyToolCall, renderCoordinationRow } from "./tool-render.ts";
|
|
20
|
-
import {
|
|
17
|
+
import type { AgentActionRuntime } from "./agent-actions.ts";
|
|
21
18
|
import { resolveRecipient } from "./recipient.ts";
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const dependencies = task.dependsOn.length > 0 ? ` · depends=${task.dependsOn.join(",")}` : "";
|
|
26
|
-
const holder = task.claimedBy ? ` · owner=@${task.claimedBy}` : "";
|
|
27
|
-
const result = task.result ? truncateHead(task.result, { maxBytes: 4096, maxLines: 40 }) : undefined;
|
|
28
|
-
const evidence = result ? `\n RESULT · ${result.content}${result.truncated ? "\n [Result preview truncated; complete evidence is retained in Work details.]" : ""}` : "";
|
|
29
|
-
return `- ${task.id} · ${task.status} · ${task.subject}${holder}${dependencies}${evidence}`;
|
|
30
|
-
});
|
|
31
|
-
return [
|
|
32
|
-
"WORK · current session",
|
|
33
|
-
`SUMMARY · ${tasks.length} work item${tasks.length === 1 ? "" : "s"}`,
|
|
34
|
-
"WORK ITEMS",
|
|
35
|
-
...(works.length > 0 ? works : ["(none)"]),
|
|
36
|
-
].join("\n");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function formatWorkCreation(subject: string, created: {
|
|
40
|
-
id: string;
|
|
41
|
-
claimable: boolean;
|
|
42
|
-
resourceBlocked: boolean;
|
|
43
|
-
notifiedTeammates: string[];
|
|
44
|
-
}): string {
|
|
45
|
-
const availability = created.claimable ? "pending/claimable" : "pending/blocked";
|
|
46
|
-
const routing = created.notifiedTeammates.length > 0
|
|
47
|
-
? `eligible residents notified: ${created.notifiedTeammates.map((name) => `@${name}`).join(", ")}`
|
|
48
|
-
: created.resourceBlocked
|
|
49
|
-
? "resource conflict blocks assignment"
|
|
50
|
-
: "no eligible resident notified";
|
|
51
|
-
return [
|
|
52
|
-
"WORK · current session",
|
|
53
|
-
`CREATED · ${created.id} · ${availability} · ${subject}`,
|
|
54
|
-
`ROUTING · ${routing}`,
|
|
55
|
-
].join("\n");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntime & { sendLeaderMessage: typeof sendLeaderMessage } = { spawnTeammate, shutdownTeammateExact, sendLeaderMessage }): void {
|
|
59
|
-
pi.registerTool({
|
|
60
|
-
name: "agent",
|
|
61
|
-
promptSnippet: "Delegate, start, inspect, or stop an Agent session",
|
|
62
|
-
label: "Agent",
|
|
63
|
-
description: "Strict Agent lifecycle interface. Delegate creates independent Work; start creates an unassigned resident; inspect and stop use incarnation-bound session handles. Results arrive automatically; inspect is for deliberate diagnosis, not waiting.",
|
|
64
|
-
promptGuidelines: ["After agent delegation, results arrive automatically. Continue independent work or end the turn; do not wait with sleep, repeated agent inspect, or work list polling."],
|
|
65
|
-
parameters: AgentActionParams,
|
|
66
|
-
renderShell: "self",
|
|
67
|
-
renderCall: emptyToolCall,
|
|
68
|
-
renderResult(result, options, theme, context) {
|
|
69
|
-
return renderCoordinationRow(
|
|
70
|
-
result, options, theme, context,
|
|
71
|
-
"agent",
|
|
72
|
-
agentRow(context.args as AgentRowArgs, result.details, { isError: context.isError, isPartial: options.isPartial }),
|
|
73
|
-
);
|
|
74
|
-
},
|
|
75
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
76
|
-
const result = await runAgentAction(params as Parameters<typeof runAgentAction>[0], ctx.cwd, runtime, ctx.sessionManager);
|
|
77
|
-
if (params.action !== "inspect") {
|
|
78
|
-
refreshTeamUI(ctx);
|
|
79
|
-
}
|
|
80
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }], details: result };
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
|
|
20
|
+
/** Point-to-point communication. Stays with @fradser/pi-agent-teams. */
|
|
21
|
+
export function registerMessageTool(pi: ExtensionAPI, runtime: { sendLeaderMessage: typeof sendLeaderMessage } = { sendLeaderMessage }): void {
|
|
84
22
|
pi.registerTool({
|
|
85
|
-
name: "
|
|
23
|
+
name: "message",
|
|
86
24
|
promptSnippet: "Send an event or message to a participant",
|
|
87
25
|
label: "Agent Event",
|
|
88
26
|
description: "Shared communication interface across Leader, Worker, and Peers. Leader-to-Agent messages carry only new evidence, changed constraints, or decisions — never kickoff echoes, progress requests, completion requests, or confirmation probes.",
|
|
@@ -93,152 +31,61 @@ export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntim
|
|
|
93
31
|
return renderCoordinationRow(
|
|
94
32
|
result, options, theme, context,
|
|
95
33
|
"message",
|
|
96
|
-
messageRow(context.args as { to?: string;
|
|
34
|
+
messageRow(context.args as { to?: string; body?: string }, result.details, { isError: context.isError }),
|
|
97
35
|
);
|
|
98
36
|
},
|
|
99
37
|
async execute(_toolCallId, params) {
|
|
38
|
+
// A refusal is returned, not thrown, so every tool in the coordination
|
|
39
|
+
// surface reports failure the same way and a caller reads one shape
|
|
40
|
+
// whatever went wrong. Throwing here would make `message` the only tool
|
|
41
|
+
// that needs a try/catch.
|
|
42
|
+
const refuse = (error: string): CoordinationToolResult => ({
|
|
43
|
+
content: [{ type: "text", text: error }],
|
|
44
|
+
details: { ok: false, error },
|
|
45
|
+
isError: true,
|
|
46
|
+
});
|
|
100
47
|
if (!params.to) {
|
|
101
|
-
|
|
48
|
+
return refuse("No bound reply route exists. Please specify 'to' explicitly.");
|
|
102
49
|
}
|
|
103
|
-
if (params.to === LEADER_RECIPIENT)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
content: [{ type: "text", text: `EVENT ROUTING · ${result.outcome} · to=@${to}\nINTENT · ${params.intent ?? "inform"}${recorded}${next}` }],
|
|
111
|
-
details: { to, outcome: result.outcome, intent: params.intent ?? "inform" },
|
|
112
|
-
};
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
pi.registerTool({
|
|
117
|
-
name: "work",
|
|
118
|
-
promptSnippet: "Create, list, assign, release, reopen, or supersede Work Items",
|
|
119
|
-
label: "Work",
|
|
120
|
-
description: "Manage Work Items: create (subject), list, assign (id, target.session), release (id, reason), reopen (id, reason), or supersede (subject, supersedes). Creation uses the session's single-writer Work state and never starts a resident.",
|
|
121
|
-
parameters: WorkToolParams,
|
|
122
|
-
renderShell: "self",
|
|
123
|
-
renderCall: emptyToolCall,
|
|
124
|
-
renderResult(result, options, theme, context) {
|
|
125
|
-
return renderCoordinationRow(
|
|
126
|
-
result, options, theme, context,
|
|
127
|
-
"work",
|
|
128
|
-
leaderWorkRow(context.args as Record<string, unknown>, result.details, { isError: context.isError }),
|
|
129
|
-
);
|
|
130
|
-
},
|
|
131
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
132
|
-
// Union-root schema: some harnesses deliver target/arrays as JSON strings.
|
|
133
|
-
params = requireParsedParams(normalizeCoordinationParams(params as Record<string, unknown>, ["target", "dependsOn", "resources", "supersedes"]), ["target", "dependsOn", "resources", "supersedes"]) as typeof params;
|
|
134
|
-
if (params.action === "supersede") {
|
|
135
|
-
const tasks = listTasks();
|
|
136
|
-
const referenced = [...((params.dependsOn as string[] | undefined) ?? []), ...(params.supersedes as string[])];
|
|
137
|
-
if (referenced.some((id) => !tasks.some((task) => task.id === id))) {
|
|
138
|
-
throw new Error(`Unknown work id in [${referenced.join(", ")}].`);
|
|
139
|
-
}
|
|
140
|
-
const created = createBoardTask(params as Parameters<typeof createBoardTask>[0]);
|
|
141
|
-
if (!created.ok) throw new Error(created.error);
|
|
142
|
-
const work = listTasks().find((task) => task.id === created.id);
|
|
143
|
-
if (!work) throw new Error(`Replacement Work Item "${created.id}" is unavailable.`);
|
|
144
|
-
refreshTeamUI(ctx);
|
|
145
|
-
return {
|
|
146
|
-
content: [{ type: "text", text: `WORK · current session\nSUPERSEDED · ${work.id} · ${work.status}\nREPLACED · ${created.supersededTaskIds.join(", ")}\nROUTING · ${created.notifiedTeammates.length > 0 ? `eligible residents notified: ${created.notifiedTeammates.map((name) => `@${name}`).join(", ")}` : "no eligible resident notified"}` }],
|
|
147
|
-
details: {
|
|
148
|
-
action: "supersede", outcome: "superseded", state: work.status,
|
|
149
|
-
work: { id: work.id, subject: work.subject, ...(work.description ? { description: work.description } : {}), dependsOn: work.dependsOn, resources: work.resources, ...(work.verify ? { verify: work.verify } : {}), state: work.status },
|
|
150
|
-
supersededWorkIds: created.supersededTaskIds, notifiedTeammates: created.notifiedTeammates, claimable: created.claimable,
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
if (params.action === "reopen") {
|
|
155
|
-
const reopened = reopenExistingWork(params.id);
|
|
156
|
-
if (!reopened.ok) throw new Error(reopened.error);
|
|
157
|
-
refreshTeamUI(ctx);
|
|
158
|
-
return {
|
|
159
|
-
content: [{ type: "text", text: `WORK · current session\nREOPENED · ${reopened.workId} · pending\nREASON · ${params.reason}` }],
|
|
160
|
-
details: { action: "reopen", outcome: "reopened", state: "pending", work: { id: reopened.workId, subject: reopened.subject, resources: reopened.resources, state: "pending" }, reason: params.reason },
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
if (params.action === "release") {
|
|
164
|
-
const released = releaseExistingWork(params.id, params.reason);
|
|
165
|
-
if (!released.ok) throw new Error(released.error);
|
|
166
|
-
refreshTeamUI(ctx);
|
|
167
|
-
const residual = released.holderStillRunning
|
|
168
|
-
? `\nRISK · @${released.holderStillRunning} was still working; an in-flight tool batch may still write inside ${released.resources.join(", ") || "the released scope"}.`
|
|
169
|
-
: "";
|
|
170
|
-
return {
|
|
171
|
-
content: [{ type: "text", text: `WORK · current session\nRELEASED · ${released.workId} · pending\nREASON · ${params.reason}${residual}` }],
|
|
172
|
-
details: { action: "release", outcome: "released", state: "pending", work: { id: released.workId, subject: released.subject, resources: released.resources, state: "pending" }, reason: params.reason, ...(released.holderStillRunning ? { holderStillRunning: released.holderStillRunning } : {}) },
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
if (params.action === "assign") {
|
|
176
|
-
const assigned = assignExistingWork(params.id, (params.target as { session: string }).session);
|
|
177
|
-
if (!assigned.ok) throw new Error(assigned.error);
|
|
178
|
-
const task = listTasks().find((entry) => entry.id === assigned.workId);
|
|
179
|
-
if (!task) throw new Error(`Assigned Work Item "${assigned.workId}" is unavailable.`);
|
|
180
|
-
refreshTeamUI(ctx);
|
|
181
|
-
return {
|
|
182
|
-
content: [{ type: "text", text: `WORK · current session\nASSIGNED · ${task.id} · claimed · owner=@${assigned.owner}\nATTEMPT · ${assigned.assignmentId} · fresh-session-pending` }],
|
|
183
|
-
details: {
|
|
184
|
-
action: "assign", outcome: "assigned", state: task.status,
|
|
185
|
-
work: { id: task.id, subject: task.subject, resources: task.resources, state: task.status, claimedBy: task.claimedBy },
|
|
186
|
-
assignment: { id: assigned.assignmentId, owner: assigned.owner, kind: "direct" },
|
|
187
|
-
target: params.target as { session: string }, delivery: "fresh-session-pending",
|
|
188
|
-
},
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
if (params.action === "list") {
|
|
192
|
-
const works = listTasks().map((task) => ({
|
|
193
|
-
id: task.id,
|
|
194
|
-
subject: task.subject,
|
|
195
|
-
...(task.description ? { description: task.description } : {}),
|
|
196
|
-
dependsOn: task.dependsOn,
|
|
197
|
-
resources: task.resources,
|
|
198
|
-
...(task.verify ? { verify: task.verify } : {}),
|
|
199
|
-
state: task.status,
|
|
200
|
-
...(task.claimedBy ? { claimedBy: task.claimedBy } : {}),
|
|
201
|
-
...(task.result ? { result: task.result } : {}),
|
|
202
|
-
}));
|
|
203
|
-
return {
|
|
204
|
-
content: [{ type: "text", text: formatWorkList(listTasks()) }],
|
|
205
|
-
details: { action: "list", outcome: "listed", works, count: works.length },
|
|
206
|
-
};
|
|
50
|
+
if (params.to === LEADER_RECIPIENT) return refuse("The leader cannot send a message to itself.");
|
|
51
|
+
let to: string;
|
|
52
|
+
try {
|
|
53
|
+
to = resolveRecipient(params.to, livingAgents()).name;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
207
56
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
throw new Error(`Unknown work id in [${referenced.join(", ")}].`);
|
|
212
|
-
}
|
|
213
|
-
const created = createBoardTask(params as Parameters<typeof createBoardTask>[0]);
|
|
214
|
-
if (!created.ok) throw new Error(created.error);
|
|
215
|
-
const work = listTasks().find((task) => task.id === created.id);
|
|
216
|
-
if (!work) throw new Error(`Created Work Item "${created.id}" is unavailable.`);
|
|
217
|
-
refreshTeamUI(ctx);
|
|
57
|
+
const result = runtime.sendLeaderMessage(to, params.body, {});
|
|
58
|
+
if (!result.ok) return refuse(result.error);
|
|
59
|
+
const recorded = result.outcome === "not-sent" ? `\nRECORDED TERMINAL REPORT · ${result.terminalReport}` : "";
|
|
218
60
|
return {
|
|
219
|
-
content: [{ type: "text", text:
|
|
220
|
-
details: {
|
|
221
|
-
action: "create",
|
|
222
|
-
outcome: "created",
|
|
223
|
-
state: work.status,
|
|
224
|
-
work: {
|
|
225
|
-
id: work.id,
|
|
226
|
-
subject: work.subject,
|
|
227
|
-
...(work.description ? { description: work.description } : {}),
|
|
228
|
-
dependsOn: work.dependsOn,
|
|
229
|
-
resources: work.resources,
|
|
230
|
-
...(work.verify ? { verify: work.verify } : {}),
|
|
231
|
-
state: work.status,
|
|
232
|
-
},
|
|
233
|
-
notifiedTeammates: created.notifiedTeammates,
|
|
234
|
-
claimable: created.claimable,
|
|
235
|
-
supersededWorkIds: created.supersededTaskIds,
|
|
236
|
-
},
|
|
61
|
+
content: [{ type: "text", text: `MESSAGE ROUTING · ${result.outcome} · to=@${to}\nKIND · ${params.kind ?? "inform"}${recorded}` }],
|
|
62
|
+
details: { to, outcome: result.outcome, kind: params.kind ?? "inform", ok: true },
|
|
237
63
|
};
|
|
238
64
|
},
|
|
239
65
|
});
|
|
66
|
+
}
|
|
240
67
|
|
|
68
|
+
// The `task` tool is registered by @fradser/pi-tasks' own extension entry. This
|
|
69
|
+
// package loads it through its manifest rather than calling its registrar, so
|
|
70
|
+
// there is exactly one registrant of that tool in every install combination.
|
|
71
|
+
// The leader learns about tasks transitions on `pi-tasks:*` events instead.
|
|
241
72
|
|
|
73
|
+
/** Temporary seam. Each registrar moves to its owning package; this keeps the
|
|
74
|
+
* existing entry point and its test fixtures intact until that lands. */
|
|
75
|
+
/** Register this package's own tool, and only that one.
|
|
76
|
+
*
|
|
77
|
+
* `agent` is registered by @fradser/pi-subagents and `task` by
|
|
78
|
+
* @fradser/pi-tasks. This package's manifest loads those two extension entries
|
|
79
|
+
* alongside its own, which is how one install ends up with three tools and each
|
|
80
|
+
* tool still has exactly one registrant. A tool with two registrants either
|
|
81
|
+
* collides or is silently dropped depending on load order, and neither failure is
|
|
82
|
+
* visible from here.
|
|
83
|
+
*
|
|
84
|
+
* The leader learns about tasks transitions on `pi-tasks:*` events rather than
|
|
85
|
+
* being called by pi-tasks, so the domain never has to know a resident exists.
|
|
86
|
+
*/
|
|
87
|
+
export function registerLeaderTools(pi: ExtensionAPI, runtime: AgentActionRuntime & { sendLeaderMessage: typeof sendLeaderMessage } = { spawnAgent, shutdownAgentExact, sendLeaderMessage }): void {
|
|
88
|
+
registerMessageTool(pi, runtime);
|
|
242
89
|
}
|
|
243
90
|
|
|
244
91
|
function teamStatusSummary(): string {
|
|
@@ -246,13 +93,13 @@ function teamStatusSummary(): string {
|
|
|
246
93
|
const rolesLine = roles.length > 0
|
|
247
94
|
? `${roles.length} persistent agent role${roles.length === 1 ? "" : "s"}: ${roles.join(", ")}`
|
|
248
95
|
: "No agent roles discovered.";
|
|
249
|
-
const roster =
|
|
96
|
+
const roster = livingAgents().map((t) => `@${t.name} (${t.role}, ${t.status})`).join("\n") || "No living sessions.";
|
|
250
97
|
return `${roster}\n${rolesLine}`;
|
|
251
98
|
}
|
|
252
99
|
|
|
253
100
|
export function registerTeamCommand(pi: ExtensionAPI): void {
|
|
254
101
|
pi.registerCommand("agent-teams", {
|
|
255
|
-
description: "Agent Teams management console: session
|
|
102
|
+
description: "Agent Teams management console: session agents and persistent agent roles",
|
|
256
103
|
handler: async (_args, ctx) => {
|
|
257
104
|
publishStateSnapshot();
|
|
258
105
|
if (ctx.mode !== "tui") { notifyPi(ctx.ui, teamStatusSummary(), "info"); return; }
|