@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/worker.ts
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Teammate capabilities. A single addressed
|
|
2
|
+
* Teammate capabilities. A single addressed agent_event primitive covers
|
|
3
3
|
* leader reports and peer mail. Task-list registration is shared by leader
|
|
4
4
|
* and worker processes; claiming and submitting remain worker-only.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
+
import * as fs from "node:fs";
|
|
8
9
|
import * as path from "node:path";
|
|
9
|
-
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
11
|
-
import { emptyToolCall,
|
|
12
|
-
import {
|
|
10
|
+
import { truncateTail, type ExtensionAPI, type MessageEndEvent } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { messageRow, workerWorkRow } from "./tool-copy.ts";
|
|
12
|
+
import { emptyToolCall, renderCoordinationRow, textOf } from "./tool-render.ts";
|
|
13
|
+
import { resolveRecipient } from "./recipient.ts";
|
|
13
14
|
import { appendInboxMessage, appendWorkerEvent, createTaskIntent, readBoardFile, readRoster } from "./statefile.ts";
|
|
14
15
|
import {
|
|
16
|
+
AgentEventParams,
|
|
15
17
|
LEADER_RECIPIENT,
|
|
16
18
|
messageTitle,
|
|
17
|
-
|
|
18
|
-
TaskClaimParams,
|
|
19
|
-
TaskListParams,
|
|
20
|
-
TaskSubmitParams,
|
|
19
|
+
WorkerWorkToolParams,
|
|
21
20
|
type BoardTask,
|
|
22
21
|
} from "./types.ts";
|
|
23
22
|
|
|
24
|
-
const BOARD_DISCLOSURE_TOOLS = ["
|
|
23
|
+
const BOARD_DISCLOSURE_TOOLS = ["work"] as const;
|
|
25
24
|
type BoardDisclosure = "none" | "notice" | "claimed";
|
|
26
25
|
|
|
27
26
|
export interface WorkerToolDisclosure {
|
|
@@ -35,11 +34,7 @@ function createWorkerToolDisclosure(pi: ExtensionAPI): WorkerToolDisclosure {
|
|
|
35
34
|
if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
|
|
36
35
|
const active = pi.getActiveTools();
|
|
37
36
|
const withoutBoardControls = active.filter((tool) => !BOARD_DISCLOSURE_TOOLS.includes(tool as typeof BOARD_DISCLOSURE_TOOLS[number]));
|
|
38
|
-
const revealed = state === "notice"
|
|
39
|
-
? ["task_list", "task_claim"]
|
|
40
|
-
: state === "claimed"
|
|
41
|
-
? ["task_submit"]
|
|
42
|
-
: [];
|
|
37
|
+
const revealed = state === "notice" || state === "claimed" ? ["work"] : [];
|
|
43
38
|
pi.setActiveTools([...withoutBoardControls, ...revealed]);
|
|
44
39
|
};
|
|
45
40
|
return {
|
|
@@ -49,7 +44,7 @@ function createWorkerToolDisclosure(pi: ExtensionAPI): WorkerToolDisclosure {
|
|
|
49
44
|
? readRoster(binding.rosterFile).find((entry) => entry.name === binding.worker)
|
|
50
45
|
: undefined;
|
|
51
46
|
const assignment = rosterEntry?.assignment;
|
|
52
|
-
if (assignment
|
|
47
|
+
if (assignment && !assignment.closed) state = "claimed";
|
|
53
48
|
else if (!assignment && prompt.includes("=== BOARD NOTICE ===")) state = "notice";
|
|
54
49
|
else state = "none";
|
|
55
50
|
apply();
|
|
@@ -90,12 +85,6 @@ export function workerBinding(): WorkerBinding | undefined {
|
|
|
90
85
|
return { worker, spawnId, outbox, inbox, rosterFile, boardFile, claimsDir, submissionsDir };
|
|
91
86
|
}
|
|
92
87
|
|
|
93
|
-
function livingRecipients(binding: WorkerBinding): Set<string> {
|
|
94
|
-
return new Set(readRoster(binding.rosterFile)
|
|
95
|
-
.filter((entry) => entry.status !== "stopped")
|
|
96
|
-
.map((entry) => entry.name));
|
|
97
|
-
}
|
|
98
|
-
|
|
99
88
|
function loadBoardTasks(binding: WorkerBinding): BoardTask[] {
|
|
100
89
|
const board = readBoardFile(binding.boardFile);
|
|
101
90
|
return Object.values(board?.tasks ?? {}).sort((a, b) => a.id.localeCompare(b.id));
|
|
@@ -151,212 +140,290 @@ export function renderTaskBoard(tasks: BoardTask[]): string {
|
|
|
151
140
|
return lines.join("\n");
|
|
152
141
|
}
|
|
153
142
|
|
|
154
|
-
|
|
155
|
-
|
|
143
|
+
type AssistantResponse = Extract<MessageEndEvent["message"], { role: "assistant" }>;
|
|
144
|
+
type AutomaticResult = { status: "completed" | "failed"; body: string };
|
|
145
|
+
|
|
146
|
+
function executionResult(message?: AssistantResponse): AutomaticResult {
|
|
147
|
+
if (message?.stopReason === "error" || message?.stopReason === "aborted") {
|
|
148
|
+
return { status: "failed", body: message.errorMessage?.trim() || `Execution ${message.stopReason} without a final answer.` };
|
|
149
|
+
}
|
|
150
|
+
if (message?.stopReason === "length") return { status: "failed", body: "Execution reached the response length limit without a complete final answer." };
|
|
151
|
+
if (message && message.stopReason !== "stop" && message.stopReason !== "toolUse") {
|
|
152
|
+
return { status: "failed", body: `Execution settled with an incomplete response (${message.stopReason}) and no final answer.` };
|
|
153
|
+
}
|
|
154
|
+
const body = message?.content.filter((part) => part.type === "text").map((part) => part.text).join("\n") ?? "";
|
|
155
|
+
if (message?.stopReason === "stop" && !message.content.some((part) => part.type === "toolCall") && body.trim()) {
|
|
156
|
+
return { status: "completed", body };
|
|
157
|
+
}
|
|
158
|
+
return { status: "failed", body: "Execution settled without a final answer." };
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
161
|
+
function automaticResultBody(binding: WorkerBinding, assignmentId: string, body: string): string {
|
|
162
|
+
if (Buffer.byteLength(JSON.stringify(body), "utf-8") <= 48 * 1024) return body;
|
|
163
|
+
const key = createHash("sha256").update(binding.spawnId).update("\0").update(assignmentId).digest("hex");
|
|
164
|
+
const file = path.join(path.dirname(binding.outbox), `${key}.result.txt`);
|
|
165
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
166
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
167
|
+
try {
|
|
168
|
+
fs.writeFileSync(temporary, body, { encoding: "utf-8", mode: 0o600 });
|
|
169
|
+
fs.renameSync(temporary, file);
|
|
170
|
+
} finally {
|
|
171
|
+
fs.rmSync(temporary, { force: true });
|
|
172
|
+
}
|
|
173
|
+
const preview = truncateTail(body, { maxBytes: 8 * 1024, maxLines: 200 });
|
|
174
|
+
return `${preview.content}\n\n[Result truncated; ${preview.totalBytes} bytes total]\nFull result: ${file}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function createWorkerReports(pi: ExtensionAPI) {
|
|
178
|
+
const initial = workerBinding();
|
|
179
|
+
const initialAssignment = initial ? readRoster(initial.rosterFile).find((entry) => entry.name === initial.worker)?.assignment : undefined;
|
|
180
|
+
let assignmentId = initialAssignment?.id;
|
|
181
|
+
let reportClosed = initialAssignment?.closed === true;
|
|
182
|
+
let pending: { binding: WorkerBinding; assignmentId: string; timestamp: number; awaitingFinal?: boolean; outcome?: AutomaticResult } | undefined;
|
|
183
|
+
const finalizedMessages = new WeakSet<AssistantResponse>();
|
|
184
|
+
const currentAssignment = (binding: WorkerBinding) => {
|
|
185
|
+
const self = readRoster(binding.rosterFile).find((entry) => entry.name === binding.worker);
|
|
186
|
+
const assignment = self?.assignment;
|
|
187
|
+
return !reportClosed && self?.spawnId === binding.spawnId && self.status !== "stopped"
|
|
188
|
+
&& assignment && assignment.id === assignmentId && !assignment.closed ? assignment : undefined;
|
|
189
|
+
};
|
|
190
|
+
const close = () => {
|
|
191
|
+
reportClosed = true;
|
|
192
|
+
pending = undefined;
|
|
193
|
+
};
|
|
194
|
+
const reportAssignment = (binding: WorkerBinding) => {
|
|
195
|
+
const current = readRoster(binding.rosterFile).find((entry) => entry.name === binding.worker)?.assignment;
|
|
196
|
+
if (current?.kind === "board" && current.id !== assignmentId) {
|
|
197
|
+
assignmentId = current.id;
|
|
198
|
+
reportClosed = false;
|
|
199
|
+
pending = undefined;
|
|
200
|
+
}
|
|
201
|
+
if (reportClosed) throw new Error("Leader report rejected: this assignment already has a terminal report. Wait for an explicit leader assignment.");
|
|
202
|
+
return assignmentId;
|
|
203
|
+
};
|
|
204
|
+
pi.on("message_start", ({ message }) => {
|
|
205
|
+
const binding = workerBinding();
|
|
206
|
+
if (!binding) return;
|
|
207
|
+
if (message.role === "user") {
|
|
208
|
+
const content = message.content;
|
|
209
|
+
const text = typeof content === "string" ? content : content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
210
|
+
const marker = /^\[agent-teams-assignment:([^\n]+)\]\n/.exec(text);
|
|
211
|
+
const current = readRoster(binding.rosterFile).find((entry) => entry.name === binding.worker)?.assignment;
|
|
212
|
+
if (marker) {
|
|
213
|
+
const id = marker[1] === "none" ? undefined : marker[1];
|
|
214
|
+
if (id !== current?.id) return;
|
|
215
|
+
if (id !== assignmentId || current?.kind === "board") reportClosed = current?.closed === true;
|
|
216
|
+
assignmentId = id;
|
|
217
|
+
pending = undefined;
|
|
218
|
+
}
|
|
219
|
+
const assignment = currentAssignment(binding);
|
|
220
|
+
if (assignment) pending = { binding, assignmentId: assignment.id, timestamp: message.timestamp };
|
|
221
|
+
} else if (message.role === "assistant" && pending && message.timestamp >= pending.timestamp) {
|
|
222
|
+
pending.timestamp = message.timestamp;
|
|
223
|
+
pending.awaitingFinal = true;
|
|
224
|
+
pending.outcome = undefined;
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
pi.on("agent_start", () => {
|
|
228
|
+
if (pending) {
|
|
229
|
+
pending.outcome = undefined;
|
|
230
|
+
pending.awaitingFinal = false;
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
pi.on("message_end", ({ message }) => {
|
|
234
|
+
if (message.role !== "assistant" || !pending?.awaitingFinal || message.timestamp < pending.timestamp
|
|
235
|
+
|| finalizedMessages.has(message) || !currentAssignment(pending.binding)) return;
|
|
236
|
+
finalizedMessages.add(message);
|
|
237
|
+
pending.timestamp = message.timestamp;
|
|
238
|
+
pending.awaitingFinal = false;
|
|
239
|
+
pending.outcome = executionResult(message);
|
|
240
|
+
});
|
|
241
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
242
|
+
if (!pending || !ctx.isIdle()) return;
|
|
243
|
+
const binding = workerBinding();
|
|
244
|
+
if (!binding || binding.worker !== pending.binding.worker || binding.spawnId !== pending.binding.spawnId
|
|
245
|
+
|| binding.outbox !== pending.binding.outbox || pending.assignmentId !== assignmentId || !currentAssignment(binding)) return;
|
|
246
|
+
const result = pending.outcome ?? executionResult();
|
|
247
|
+
appendWorkerEvent(binding.outbox, {
|
|
248
|
+
id: randomUUID(), type: "message", worker: binding.worker, spawnId: binding.spawnId,
|
|
249
|
+
assignmentId: pending.assignmentId, timestamp: Date.now(), status: result.status,
|
|
250
|
+
body: automaticResultBody(binding, pending.assignmentId, result.body),
|
|
251
|
+
});
|
|
252
|
+
close();
|
|
196
253
|
});
|
|
254
|
+
return {
|
|
255
|
+
close,
|
|
256
|
+
send(binding: WorkerBinding, body: string, status: import("./types.ts").WorkerReportEvent["status"]) {
|
|
257
|
+
appendWorkerEvent(binding.outbox, {
|
|
258
|
+
assignmentId: reportAssignment(binding), id: randomUUID(), type: "message",
|
|
259
|
+
worker: binding.worker, spawnId: binding.spawnId, body, status, timestamp: Date.now(),
|
|
260
|
+
});
|
|
261
|
+
const isTerminal = status === "completed" || status === "failed";
|
|
262
|
+
if (isTerminal) close();
|
|
263
|
+
return isTerminal;
|
|
264
|
+
},
|
|
265
|
+
reset() { pending = undefined; },
|
|
266
|
+
};
|
|
197
267
|
}
|
|
198
268
|
|
|
199
269
|
export function registerWorkerCapabilities(pi: ExtensionAPI): WorkerToolDisclosure {
|
|
200
270
|
const disclosure = createWorkerToolDisclosure(pi);
|
|
271
|
+
const reports = createWorkerReports(pi);
|
|
201
272
|
pi.registerTool({
|
|
202
|
-
name: "
|
|
203
|
-
promptSnippet: "Send
|
|
204
|
-
label: "
|
|
205
|
-
description: "
|
|
206
|
-
parameters:
|
|
273
|
+
name: "agent_event",
|
|
274
|
+
promptSnippet: "Send an event or message to a participant",
|
|
275
|
+
label: "Agent Event",
|
|
276
|
+
description: "Communication-only interface across Leader, Worker, and Peers. Work completion is reported automatically after settlement or uses work action=submit.",
|
|
277
|
+
parameters: AgentEventParams,
|
|
207
278
|
renderShell: "self",
|
|
208
279
|
renderCall: emptyToolCall,
|
|
209
280
|
renderResult(result, options, theme, context) {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return renderLifecycleResult(result, options, theme, context, eventToolLifecycle(
|
|
281
|
+
return renderCoordinationRow(
|
|
282
|
+
result, options, theme, context,
|
|
213
283
|
"message",
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
));
|
|
284
|
+
messageRow(context.args as { to?: string; message?: string }, result.details, { isError: context.isError }),
|
|
285
|
+
);
|
|
217
286
|
},
|
|
218
287
|
async execute(_toolCallId, params) {
|
|
219
288
|
const binding = workerBinding();
|
|
220
289
|
if (!binding) throw new Error("This capability is available only inside a spawned teammate.");
|
|
221
|
-
if (params.to === LEADER_RECIPIENT) {
|
|
290
|
+
if (!params.to || params.to === LEADER_RECIPIENT) {
|
|
222
291
|
appendWorkerEvent(binding.outbox, {
|
|
223
|
-
id: randomUUID(),
|
|
224
|
-
|
|
225
|
-
worker: binding.worker,
|
|
226
|
-
spawnId: binding.spawnId,
|
|
227
|
-
body: params.message,
|
|
228
|
-
status: params.status,
|
|
229
|
-
timestamp: Date.now(),
|
|
292
|
+
assignmentId: readRoster(binding.rosterFile).find((entry) => entry.name === binding.worker)?.assignment?.id, id: randomUUID(), type: "message", worker: binding.worker, spawnId: binding.spawnId,
|
|
293
|
+
body: params.message, status: params.intent ?? "inform", timestamp: Date.now(),
|
|
230
294
|
});
|
|
231
295
|
return {
|
|
232
|
-
content: [{ type: "text", text: params.
|
|
233
|
-
|
|
234
|
-
: 'MESSAGING\nREPORT · to=leader · status=in_progress\nNEXT · send status="completed" or status="failed" to end the assignment' }],
|
|
235
|
-
details: { to: LEADER_RECIPIENT, status: params.status ?? "in_progress", outcome: "queued" },
|
|
236
|
-
terminate: params.status === "completed" || params.status === "failed",
|
|
296
|
+
content: [{ type: "text", text: `MESSAGING\nREPORT · to=leader · intent=${params.intent ?? "inform"}\nNEXT · harness will deliver this message; Work completion is automatic or uses work submit` }],
|
|
297
|
+
details: { to: LEADER_RECIPIENT, intent: params.intent ?? "inform", outcome: "queued" },
|
|
237
298
|
};
|
|
238
299
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
const recipientInbox = path.join(path.dirname(binding.inbox), `inbox-${encodeURIComponent(params.to)}.jsonl`);
|
|
300
|
+
const recipient = resolveRecipient(params.to, readRoster(binding.rosterFile));
|
|
301
|
+
const to = recipient.name;
|
|
302
|
+
if (to === binding.worker) throw new Error("You are already the recipient — no need to message yourself.");
|
|
303
|
+
const recipientInbox = path.join(path.dirname(binding.inbox), `inbox-${encodeURIComponent(to)}.jsonl`);
|
|
245
304
|
appendInboxMessage(recipientInbox, {
|
|
246
305
|
id: randomUUID(),
|
|
247
306
|
from: binding.worker,
|
|
248
307
|
subject: messageTitle(params.message),
|
|
249
308
|
body: params.message,
|
|
309
|
+
...(recipient.spawnId ? { toSpawnId: recipient.spawnId } : {}),
|
|
250
310
|
});
|
|
251
311
|
return {
|
|
252
|
-
content: [{ type: "text", text: `MESSAGING\nQUEUED · to=@${
|
|
253
|
-
details: { to
|
|
312
|
+
content: [{ type: "text", text: `MESSAGING\nQUEUED · to=@${to}\nNEXT · harness will route the inbox message into a recipient turn` }],
|
|
313
|
+
details: { to, outcome: "queued", intent: params.intent ?? "inform" },
|
|
254
314
|
};
|
|
255
315
|
},
|
|
256
316
|
});
|
|
257
317
|
|
|
258
|
-
|
|
318
|
+
async function queueWorkClaim(id: string | undefined, presentation: "board" | "work") {
|
|
319
|
+
const binding = workerBinding();
|
|
320
|
+
if (!binding) throw new Error("This capability is available only inside a spawned teammate.");
|
|
321
|
+
const label = presentation === "work" ? "Work Item" : "Task";
|
|
322
|
+
const empty = presentation === "work" ? "No claimable Work Item right now." : "No claimable task right now.";
|
|
323
|
+
const race = presentation === "work" ? "All candidate Work Items were claimed in the race. Check Work again." : "All candidate tasks were claimed in the race. Check the board again.";
|
|
324
|
+
const tasks = loadBoardTasks(binding);
|
|
325
|
+
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
326
|
+
const candidates = id
|
|
327
|
+
? tasks.filter((task) => task.id === id)
|
|
328
|
+
: tasks.filter((task) => task.status === "pending" && dependenciesMet(task, byId));
|
|
329
|
+
if (candidates.length === 0) {
|
|
330
|
+
throw new Error(id ? `${label} "${id}" was not found${presentation === "board" ? " on the board" : ""}.` : empty);
|
|
331
|
+
}
|
|
332
|
+
for (const task of candidates) {
|
|
333
|
+
if (task.status !== "pending" || !dependenciesMet(task, byId)) continue;
|
|
334
|
+
const rejected = claimRejection(binding, task);
|
|
335
|
+
if (rejected) {
|
|
336
|
+
if (id) throw new Error(rejected);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const won = createTaskIntent(binding.claimsDir, task.id, {
|
|
340
|
+
taskId: task.id, worker: binding.worker, spawnId: binding.spawnId, timestamp: Date.now(),
|
|
341
|
+
});
|
|
342
|
+
if (won) {
|
|
343
|
+
const content = presentation === "work"
|
|
344
|
+
? `WORK · current session\nCLAIM INTENT QUEUED · ${task.id} · ${task.subject}\nREQUESTER · @${binding.worker}\nNEXT · wait for harness claim acceptance before starting Work`
|
|
345
|
+
: `BOARD · current session\nCLAIM INTENT QUEUED · ${task.id} · ${task.subject}\nREQUESTER · @${binding.worker}\nNEXT · wait for harness Claim accepted feedback; do not start work until it arrives`;
|
|
346
|
+
const details = presentation === "work"
|
|
347
|
+
? { action: "claim", outcome: "queued", id: task.id, subject: task.subject, worker: binding.worker }
|
|
348
|
+
: { taskId: task.id, subject: task.subject, worker: binding.worker };
|
|
349
|
+
return { content: [{ type: "text" as const, text: content }], details };
|
|
350
|
+
}
|
|
351
|
+
if (id) throw new Error(`${label} "${task.id}" was claimed by someone else first.`);
|
|
352
|
+
}
|
|
353
|
+
throw new Error(race);
|
|
354
|
+
}
|
|
259
355
|
|
|
260
356
|
pi.registerTool({
|
|
261
|
-
name: "
|
|
357
|
+
name: "work",
|
|
262
358
|
renderShell: "self",
|
|
263
359
|
renderCall: emptyToolCall,
|
|
264
360
|
renderResult(result, options, theme, context) {
|
|
265
|
-
const
|
|
266
|
-
return
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
{
|
|
270
|
-
)
|
|
361
|
+
const params = context.args as { action?: string; id?: string };
|
|
362
|
+
return renderCoordinationRow(
|
|
363
|
+
result, options, theme, context,
|
|
364
|
+
"work",
|
|
365
|
+
workerWorkRow(params, result.details, textOf(result), { isError: context.isError }),
|
|
366
|
+
);
|
|
271
367
|
},
|
|
272
|
-
promptSnippet: "
|
|
273
|
-
label: "
|
|
274
|
-
description: "
|
|
275
|
-
parameters:
|
|
276
|
-
|
|
368
|
+
promptSnippet: "Claim pending Work or submit owned Work",
|
|
369
|
+
label: "Work",
|
|
370
|
+
description: "Queue a claim for pending Work or submit the current owned Work Item. The harness remains the authority for claim acceptance and completion.",
|
|
371
|
+
parameters: WorkerWorkToolParams,
|
|
372
|
+
execute(_toolCallId, params) {
|
|
277
373
|
const binding = workerBinding();
|
|
278
374
|
if (!binding) throw new Error("This capability is available only inside a spawned teammate.");
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
: tasks.filter((task) => task.status === "pending" && dependenciesMet(task, byId));
|
|
284
|
-
if (candidates.length === 0) {
|
|
285
|
-
throw new Error(params.taskId ? `Task "${params.taskId}" was not found on the board.` : "No claimable task right now.");
|
|
375
|
+
if (params.action === "list") {
|
|
376
|
+
const tasks = loadBoardTasks(binding);
|
|
377
|
+
const lines = tasks.map((task) => `- ${task.id} · ${task.status} · ${task.subject}`);
|
|
378
|
+
return Promise.resolve({ content: [{ type: "text", text: `WORK · current session\nWORK ITEMS\n${lines.join("\n") || "(none)"}` }], details: { action: "list", outcome: "listed", count: tasks.length } });
|
|
286
379
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
const won = createTaskIntent(binding.claimsDir, task.id, {
|
|
295
|
-
taskId: task.id,
|
|
296
|
-
worker: binding.worker,
|
|
297
|
-
spawnId: binding.spawnId,
|
|
298
|
-
timestamp: Date.now(),
|
|
299
|
-
});
|
|
300
|
-
if (won) {
|
|
301
|
-
return {
|
|
302
|
-
content: [{ type: "text", text: `BOARD · current session\nCLAIM INTENT QUEUED · ${task.id} · ${task.subject}\nREQUESTER · @${binding.worker}\nNEXT · wait for harness Claim accepted feedback; do not start work until it arrives` }],
|
|
303
|
-
details: { taskId: task.id, subject: task.subject, worker: binding.worker },
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
if (params.taskId) throw new Error(`Task "${task.id}" was claimed by someone else first.`);
|
|
380
|
+
if (params.action === "claim") return queueWorkClaim(params.id, "work");
|
|
381
|
+
const self = readRoster(binding.rosterFile).find((entry) => entry.name === binding.worker);
|
|
382
|
+
if (!self?.assignment || !self.currentTaskId) throw new Error("You do not own a claimed Work Item.");
|
|
383
|
+
if (params.action === "release") {
|
|
384
|
+
return queueWorkSubmission({ taskId: self.currentTaskId, status: "failed", result: params.result, presentation: "work" });
|
|
307
385
|
}
|
|
308
|
-
|
|
386
|
+
return queueWorkSubmission({ taskId: self.currentTaskId, status: params.outcome === "success" ? "completed" : "failed", result: params.result, presentation: "work" });
|
|
309
387
|
},
|
|
310
388
|
});
|
|
311
389
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
390
|
+
async function queueWorkSubmission(input: { taskId: string; status: "completed" | "failed"; result?: string; presentation: "board" | "work" }) {
|
|
391
|
+
const binding = workerBinding();
|
|
392
|
+
if (!binding) throw new Error("This capability is available only inside a spawned teammate.");
|
|
393
|
+
const assignmentId = readRoster(binding.rosterFile).find((entry) => entry.name === binding.worker)?.assignment?.id;
|
|
394
|
+
const won = createTaskIntent(binding.submissionsDir, input.taskId, {
|
|
395
|
+
taskId: input.taskId, worker: binding.worker, spawnId: binding.spawnId, assignmentId,
|
|
396
|
+
status: input.status, result: input.result, timestamp: Date.now(),
|
|
397
|
+
});
|
|
398
|
+
if (!won) throw new Error(`A submission for "${input.taskId}" is already pending.`);
|
|
399
|
+
reports.close();
|
|
400
|
+
disclosure.reset();
|
|
401
|
+
const task = loadBoardTasks(binding).find((candidate) => candidate.id === input.taskId);
|
|
402
|
+
const roleVerify = process.env.PI_TEAMMATE_VERIFY_DEFAULT?.trim();
|
|
403
|
+
const verify = input.status === "completed"
|
|
404
|
+
? task?.verify
|
|
405
|
+
? "VERIFY · queued (task gate)"
|
|
406
|
+
: roleVerify
|
|
407
|
+
? "VERIFY · queued (role gate)"
|
|
408
|
+
: "VERIFY · none configured"
|
|
409
|
+
: "VERIFY · skipped (failed submission)";
|
|
410
|
+
const next = input.status === "completed" ? "NEXT · wait for the harness result" : input.presentation === "work" ? "NEXT · Work returns to pending" : "NEXT · task returns to pending";
|
|
411
|
+
const content = input.presentation === "work"
|
|
412
|
+
? `WORK · current session\nSUBMISSION INTENT QUEUED · ${input.taskId} · ${input.status === "completed" ? "success" : "failed"}\n${verify}\n${next}`
|
|
413
|
+
: `BOARD · current session\nSUBMITTED · ${input.taskId} · ${input.status}\n${verify}\n${next}`;
|
|
414
|
+
// The subject rides along in details: a worker process never hydrates the
|
|
415
|
+
// leader's in-memory board, so its row cannot look the task up by itself.
|
|
416
|
+
const details = input.presentation === "work"
|
|
417
|
+
? { action: "submit", outcome: "queued", id: input.taskId, subject: task?.subject, status: input.status === "completed" ? "success" : "failed", verify: Boolean(task?.verify || roleVerify) }
|
|
418
|
+
: { taskId: input.taskId, subject: task?.subject, status: input.status, verify: Boolean(task?.verify || roleVerify) };
|
|
419
|
+
return { content: [{ type: "text" as const, text: content }], details, terminate: true };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
update: disclosure.update,
|
|
424
|
+
reset() {
|
|
341
425
|
disclosure.reset();
|
|
342
|
-
|
|
343
|
-
const roleVerify = process.env.PI_TEAMMATE_VERIFY_DEFAULT?.trim();
|
|
344
|
-
const verify = params.status === "completed"
|
|
345
|
-
? task?.verify
|
|
346
|
-
? "VERIFY · queued (task gate)"
|
|
347
|
-
: roleVerify
|
|
348
|
-
? "VERIFY · queued (role gate)"
|
|
349
|
-
: "VERIFY · none configured"
|
|
350
|
-
: "VERIFY · skipped (failed submission)";
|
|
351
|
-
const next = params.status === "completed"
|
|
352
|
-
? "NEXT · wait for the harness result"
|
|
353
|
-
: "NEXT · task returns to pending";
|
|
354
|
-
return {
|
|
355
|
-
content: [{ type: "text", text: `BOARD · current session\nSUBMITTED · ${params.taskId} · ${params.status}\n${verify}\n${next}` }],
|
|
356
|
-
details: { taskId: params.taskId, status: params.status, verify: Boolean(task?.verify || roleVerify) },
|
|
357
|
-
};
|
|
426
|
+
reports.reset();
|
|
358
427
|
},
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return disclosure;
|
|
428
|
+
};
|
|
362
429
|
}
|