pi-extended-teams 2.4.0 → 2.4.2
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/extensions/agents/read-agent-session-lifecycle.ts +9 -3
- package/extensions/agents/read-agent.ts +25 -17
- package/extensions/events/register-events.ts +5 -1
- package/extensions/prompts/spawned-agent-communication.md +1 -0
- package/extensions/tools/agent-communication-tools.ts +3 -1
- package/extensions/tools/coordination-tools.ts +3 -1
- package/extensions/tools/spawned-agent-policy.ts +22 -0
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ export interface ReadAgentDeliveryState {
|
|
|
16
16
|
acceptingMessages?: boolean;
|
|
17
17
|
messageDeliveryClosed?: boolean;
|
|
18
18
|
messageDeliveryTail?: Promise<void>;
|
|
19
|
+
messageAdmissionTail?: Promise<void>;
|
|
19
20
|
messageDeliveryGeneration?: number;
|
|
20
21
|
messageDeliveryCancellation?: Promise<void>;
|
|
21
22
|
cancelMessageDelivery?: () => void;
|
|
@@ -163,7 +164,8 @@ export function closeReadAgentMessageDelivery(state: ReadAgentDeliveryState): Re
|
|
|
163
164
|
export async function enqueueReadAgentMessageDelivery(
|
|
164
165
|
state: ReadAgentDeliveryState,
|
|
165
166
|
agentName: string,
|
|
166
|
-
send: () => Promise<void
|
|
167
|
+
send: () => Promise<void>,
|
|
168
|
+
received?: Promise<void>
|
|
167
169
|
): Promise<DeliveryOutcome> {
|
|
168
170
|
if (!state.acceptingMessages || state.messageDeliveryClosed) {
|
|
169
171
|
throw new Error(`Cannot send message to ${agentName}: agent is finishing.`);
|
|
@@ -175,7 +177,8 @@ export async function enqueueReadAgentMessageDelivery(
|
|
|
175
177
|
state.pendingMessageDeliveries = (state.pendingMessageDeliveries ?? 0) + 1;
|
|
176
178
|
|
|
177
179
|
const previousDelivery = state.messageDeliveryTail ?? Promise.resolve();
|
|
178
|
-
const
|
|
180
|
+
const previousAdmission = state.messageAdmissionTail ?? previousDelivery;
|
|
181
|
+
const rawOutcome = previousAdmission.catch(() => {}).then(async (): Promise<DeliveryOutcome> => {
|
|
179
182
|
if (state.messageDeliveryClosed || state.messageDeliveryGeneration !== generation) {
|
|
180
183
|
return { status: "cancelled" };
|
|
181
184
|
}
|
|
@@ -192,7 +195,10 @@ export async function enqueueReadAgentMessageDelivery(
|
|
|
192
195
|
await rawDelivery;
|
|
193
196
|
return { status: "delivered" };
|
|
194
197
|
});
|
|
195
|
-
|
|
198
|
+
const settled = rawOutcome.then(() => {}, () => {});
|
|
199
|
+
// Receipt releases the next send, not ownership of the model run it started.
|
|
200
|
+
state.messageAdmissionTail = received ? Promise.race([received, settled]) : settled;
|
|
201
|
+
state.messageDeliveryTail = Promise.all([previousDelivery, settled]).then(() => {});
|
|
196
202
|
|
|
197
203
|
try {
|
|
198
204
|
const outcome = await Promise.race([
|
|
@@ -20,6 +20,7 @@ import { loadNativeCheckOperations } from "../internal/pi-check-operations";
|
|
|
20
20
|
import type { AgentReportSource, CompletedAgentReport, RunningReadAgent } from "../runtime/types";
|
|
21
21
|
import { extractTextParts, sanitizeTuiLine } from "../ui/renderers";
|
|
22
22
|
import { createAgentCommunicationTools, formatRepairRequest, type SubmittedAgentReport } from "../tools/agent-communication-tools";
|
|
23
|
+
import { registerSpawnedAgentCommunicationGuard, SPAWNED_AGENT_COMMUNICATION_GUIDANCE, USER_INTERACTION_TOOLS } from "../tools/spawned-agent-policy";
|
|
23
24
|
import { requireWriteAgentTeam } from "../team/roster";
|
|
24
25
|
import { isPiPromptPlanningMember, shouldSuppressLeadReportInjection } from "../../src/utils/workflow-metadata";
|
|
25
26
|
import { canonicalPersistedModelSlot, loadSettings, requireFavoriteModelLevel } from "../../src/utils/settings";
|
|
@@ -389,31 +390,33 @@ export async function sendMessageToRunningReadAgent(
|
|
|
389
390
|
const pendingInterrupt = agent.operationInterruptPromise;
|
|
390
391
|
if (pendingInterrupt) await pendingInterrupt.catch(() => {});
|
|
391
392
|
if (agent.messageDeliveryClosed || agent.stopRequested) throw new ReadAgentDeliveryCancelledError(agent.name);
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
}
|
|
393
|
+
if (agent.completedOperationError) throw agent.completedOperationError;
|
|
394
|
+
unsubscribe = session.subscribe(event => {
|
|
395
|
+
if (event.type !== "message_end" || event.message.role !== "user") return;
|
|
396
|
+
const body = event.message.content;
|
|
397
|
+
const text = typeof body === "string"
|
|
398
|
+
? body
|
|
399
|
+
: body.filter(part => part.type === "text").map(part => part.text).join("\n");
|
|
400
|
+
if (text === content) {
|
|
401
|
+
received = true;
|
|
402
|
+
acknowledge();
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
405
|
if (session.isStreaming) {
|
|
406
406
|
await session.sendUserMessage(content, { deliverAs: "steer" as const });
|
|
407
407
|
} else {
|
|
408
408
|
await runReadAgentSessionOperation(agent, () => session.sendUserMessage(content, undefined));
|
|
409
409
|
if (requireReceipt && !received) throw new Error(`Requester ${agent.name} finished without receiving the report.`);
|
|
410
410
|
}
|
|
411
|
-
}
|
|
411
|
+
},
|
|
412
|
+
receipt
|
|
412
413
|
);
|
|
413
414
|
signalReadAgentWake(agent);
|
|
414
415
|
pendingParentWakeSignals.get(agent)?.();
|
|
415
416
|
try {
|
|
416
|
-
|
|
417
|
+
// A normal send acknowledges receipt, not completion of the recipient's work.
|
|
418
|
+
// The delivery tail still owns that work for finalization and error handling.
|
|
419
|
+
await (requireReceipt ? deliveryResult : Promise.race([receipt, deliveryResult]));
|
|
417
420
|
if (requireReceipt && !received) {
|
|
418
421
|
// Keep the receipt wait outside the raw tail so parent finalization can cancel an unconsumed steer.
|
|
419
422
|
await Promise.race([receipt, agent.messageDeliveryCancellation!.then(() => {
|
|
@@ -1056,6 +1059,7 @@ export async function runReadAgentInProcess(
|
|
|
1056
1059
|
settingsManager: childSettingsManager,
|
|
1057
1060
|
noExtensions: true,
|
|
1058
1061
|
additionalExtensionPaths: [...resourcePlan.extensionPaths],
|
|
1062
|
+
extensionFactories: [registerSpawnedAgentCommunicationGuard],
|
|
1059
1063
|
noSkills: false,
|
|
1060
1064
|
appendSystemPrompt: [
|
|
1061
1065
|
`You are ${roleLabel} agent '${member.name}' in Pi session '${readTeamName}', running in-process so the lead can follow and control you from Pi.`,
|
|
@@ -1065,7 +1069,7 @@ export async function runReadAgentInProcess(
|
|
|
1065
1069
|
role === "write"
|
|
1066
1070
|
? "Use read/bash/edit/write as needed for the assignment. Prefer precise edits. Stop and report if you need broader product or architecture approval."
|
|
1067
1071
|
: "Even though the edit/write tools are available, do not use them: do not edit or write files, install or remove packages, start long-running services, commit, push, deploy, or make any other mutating or destructive change. Investigate and report; if a change is needed, recommend it to the lead instead of applying it.",
|
|
1068
|
-
|
|
1072
|
+
SPAWNED_AGENT_COMMUNICATION_GUIDANCE,
|
|
1069
1073
|
"Progress reporting is required, not optional UI polish. Call report_progress before your first work tool with a concise phrase describing what you are starting. Call it again whenever you change phase or evidence source, hit a blocker, or begin synthesis; never make more than 3 work-tool calls without a fresh progress update. Use a new phrase describing what you are doing now. It updates the activity widget without messaging or waking the lead; do not use it as a heartbeat.",
|
|
1070
1074
|
...(member.requestedBy
|
|
1071
1075
|
? [`You are a depth-1 read helper requested by '${member.requestedBy}'. Your report_and_exit deliverable goes to that requesting writer; the lead receives only a classified completion notice. You cannot delegate.`]
|
|
@@ -1138,7 +1142,9 @@ export async function runReadAgentInProcess(
|
|
|
1138
1142
|
const communicationToolNameSet = new Set(communicationToolNames);
|
|
1139
1143
|
const delegationToolNameSet = new Set<string>(NESTED_DELEGATION_TOOL_NAMES);
|
|
1140
1144
|
const extensionToolNames = loader.getExtensions().extensions.flatMap(extension => {
|
|
1141
|
-
return Array.from(extension.tools.keys()).filter(name =>
|
|
1145
|
+
return Array.from(extension.tools.keys()).filter(name =>
|
|
1146
|
+
!communicationToolNameSet.has(name) && !delegationToolNameSet.has(name) && !USER_INTERACTION_TOOLS.includes(name)
|
|
1147
|
+
);
|
|
1142
1148
|
});
|
|
1143
1149
|
const nestedReadAgentToolNames = nestedReadAgentTools.map(tool => tool.name);
|
|
1144
1150
|
const activeToolNames = Array.from(new Set([
|
|
@@ -1179,6 +1185,7 @@ export async function runReadAgentInProcess(
|
|
|
1179
1185
|
modelRuntime: parentModelRuntime,
|
|
1180
1186
|
modelRegistry: ctx.modelRegistry,
|
|
1181
1187
|
tools: activeToolNames,
|
|
1188
|
+
excludeTools: USER_INTERACTION_TOOLS,
|
|
1182
1189
|
customTools: [...communicationTools, ...nestedReadAgentTools],
|
|
1183
1190
|
resourceLoader: loader,
|
|
1184
1191
|
settingsManager: childSettingsManager,
|
|
@@ -1418,6 +1425,7 @@ export async function runReadAgentInProcess(
|
|
|
1418
1425
|
// its continuation may accept or settle another child wave.
|
|
1419
1426
|
const deliveryTail = state.messageDeliveryTail;
|
|
1420
1427
|
if (deliveryTail) await deliveryTail.catch(() => {});
|
|
1428
|
+
if (state.completedOperationError) throw state.completedOperationError;
|
|
1421
1429
|
}
|
|
1422
1430
|
} else if (!state.stopRequested && options.isCurrentReadAgentRun(key, state)) {
|
|
1423
1431
|
completionResolution = resolveCurrentReport();
|
|
@@ -15,6 +15,7 @@ import { generateLifecycleRunId } from "../../src/utils/lifecycle-tombstone";
|
|
|
15
15
|
import { cleanupStaleSessionContextReferences } from "../internal/session-context-reference";
|
|
16
16
|
import { cleanupStalePrivateAgentSessions } from "../internal/agent-session-files";
|
|
17
17
|
import { expireCheckpoints } from "../../src/results/checkpoint-retention";
|
|
18
|
+
import { registerSpawnedAgentCommunicationGuard, SPAWNED_AGENT_COMMUNICATION_GUIDANCE } from "../tools/spawned-agent-policy";
|
|
18
19
|
|
|
19
20
|
export const LEAD_ORCHESTRATION_GUIDANCE = `\n\npi-extended-teams lead orchestration rules:\n- Choose tiers by the agent's intended outcome, not by vague task importance. read-review is the normal default for focused review, verification, and bounded synthesis.\n- Use read-collect when the lane gathers bounded facts without owning the conclusion. Use read-analyze when it must explain behavior or root cause across connected evidence. Reserve read-critical for irreducible high-stakes security, architecture, concurrency, migration, or data-correctness reasoning.\n- For edits, use write-patch for a narrow localized change, write-feature for a bounded feature with a known design, write-system for a cross-cutting integration/refactor within explicitly claimed files, and write-critical only for high-risk security, concurrency, recovery, migration, or data-integrity changes.\n- Prefer the canonical read-*/write-* tiers. Legacy reading-*/writing-* names are compatibility aliases for this minor release, not intent guidance.\n- A spawned agent owns its assigned lane until it reports, blocks, fails, or the user cancels it. Do not duplicate, take over, test, edit, or synthesize that same lane in parallel; work only on clearly unrelated lanes.\n- When no unrelated work remains, end the turn. The extension resumes you when a report arrives. One get_agent_status snapshot is allowed when current status is needed; do not repeatedly call it, sleep, busy-wait, loop on inbox/status, send nudges, do dummy work, or treat healthy silence as failure.\n- Wait for the actual report before synthesizing. Intervene only on a reported blocker/error, actual health failure, or explicit user cancellation/change.\n- For durable bug, security, or testing claims from an agent report or backlog, concrete, reproducible findings with file/line evidence or a focused failing regression may proceed directly to TDD repair.
|
|
20
21
|
- Use a separate read-only confirmation only when evidence is missing or weak, the claim is disputed, or irreducible high-risk uncertainty remains; never reconfirm an already confirmed finding.`;
|
|
@@ -66,6 +67,7 @@ export function registerAgentReportRenderer(pi: any): void {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export function registerExtensionEvents(pi: any, options: RegisterEventsOptions): void {
|
|
70
|
+
if (options.isTeammate) registerSpawnedAgentCommunicationGuard(pi);
|
|
69
71
|
let teammateWakeIfUnread: (() => Promise<void>) | null = null;
|
|
70
72
|
let teammatePendingInboxWake = false;
|
|
71
73
|
let teammateInboxWakeTimer: NodeJS.Timeout | null = null;
|
|
@@ -387,6 +389,7 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
|
|
|
387
389
|
if (!options.isTeammate) {
|
|
388
390
|
return { systemPrompt: event.systemPrompt + LEAD_ORCHESTRATION_GUIDANCE };
|
|
389
391
|
}
|
|
392
|
+
const systemPrompt = `${event.systemPrompt}\n\n${SPAWNED_AGENT_COMMUNICATION_GUIDANCE}`;
|
|
390
393
|
if (firstTurn) {
|
|
391
394
|
firstTurn = false;
|
|
392
395
|
|
|
@@ -430,8 +433,9 @@ export function registerExtensionEvents(pi: any, options: RegisterEventsOptions)
|
|
|
430
433
|
}
|
|
431
434
|
|
|
432
435
|
return {
|
|
433
|
-
systemPrompt:
|
|
436
|
+
systemPrompt: systemPrompt + `\n\nYou are spawned agent '${options.agentName}' in Pi session '${teamName}'.\nYour lead is 'team-lead'.${modelInfo}\n\nCore rules for every spawned agent:\n- NEVER sleep, busy-wait, or poll. Do not use bash sleep, while-true, or any wait/poll loop. The extension wakes you when messages arrive.\n- You cannot spawn, promote, or create other agents. If another agent is needed, use send_message to ask team-lead to decide and spawn.\n- Use send_message for direct communication and read_inbox when the extension wakes you or you expect a reply.\n- Progress reporting is required, not optional UI polish. After reading your initial instructions, call report_progress before your first work tool with a concise phrase describing what you are starting. Call it again whenever you change phase or evidence source, hit a blocker, or begin synthesis; never make more than 3 work-tool calls without a fresh progress update. Use a new phrase describing what you are doing now. It updates the activity widget without messaging or waking the lead; do not use it as a heartbeat.\n- When your work is done, report and exit cleanly. Do not wait for the lead to shut you down.${roleSpecificGuidance}${rosterInfo}\nStart by calling read_inbox to get your initial instructions.`,
|
|
434
437
|
};
|
|
435
438
|
}
|
|
439
|
+
return { systemPrompt };
|
|
436
440
|
});
|
|
437
441
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
You are a spawned agent. Communicate only with team-lead through send_message. Never ask or address the user directly, open a user-question dialog, or use Orb to contact them. If you need a decision or permission, send the question and relevant context to team-lead. Do not treat a tool error or timeout as approval. Continue independent assigned work if possible. If you cannot continue without an answer, use report_and_exit with outcome blocked and include your questions; do not claim success or poll for a reply. Use report_and_exit for your final deliverable; nested helper reports still go to the requesting writer.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { requireLeadRecipient } from "./spawned-agent-policy";
|
|
2
3
|
import * as messaging from "../../src/utils/messaging";
|
|
3
4
|
import * as runtime from "../../src/utils/runtime";
|
|
4
5
|
import * as claims from "../../src/utils/claims";
|
|
@@ -104,7 +105,7 @@ export function createAgentCommunicationTools(options: AgentCommunicationToolsOp
|
|
|
104
105
|
{
|
|
105
106
|
name: "send_message",
|
|
106
107
|
label: "Send Message",
|
|
107
|
-
description: "Send a direct message in the current Pi session. Spawned agents
|
|
108
|
+
description: "Send a direct message in the current Pi session. Spawned agents may message only team-lead.",
|
|
108
109
|
parameters: Type.Object({
|
|
109
110
|
recipient: Type.Optional(Type.String({ description: "Recipient agent name. Defaults to team-lead for spawned agents." })),
|
|
110
111
|
content: Type.String(),
|
|
@@ -114,6 +115,7 @@ export function createAgentCommunicationTools(options: AgentCommunicationToolsOp
|
|
|
114
115
|
const teamName = requireCurrentSession(options);
|
|
115
116
|
const recipient = params.recipient || (options.isTeammate ? "team-lead" : undefined);
|
|
116
117
|
if (!recipient) throw new Error("recipient is required when the lead sends a message.");
|
|
118
|
+
if (options.isTeammate) requireLeadRecipient(recipient);
|
|
117
119
|
await messaging.sendPlainMessageIfRunning(teamName, options.agentName, recipient, params.content, params.summary || "Message");
|
|
118
120
|
return { content: [{ type: "text", text: `Message sent to ${recipient}.` }], details: { session: teamName, recipient } };
|
|
119
121
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import { requireLeadRecipient } from "./spawned-agent-policy";
|
|
4
5
|
import * as paths from "../../src/utils/paths";
|
|
5
6
|
import * as teams from "../../src/utils/teams";
|
|
6
7
|
import * as messaging from "../../src/utils/messaging";
|
|
@@ -309,7 +310,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
309
310
|
pi.registerTool({
|
|
310
311
|
name: "send_message",
|
|
311
312
|
label: "Send Message",
|
|
312
|
-
description: "Send a direct message in the current Pi session. Spawned agents
|
|
313
|
+
description: "Send a direct message in the current Pi session. Spawned agents may message only team-lead.",
|
|
313
314
|
parameters: Type.Object({
|
|
314
315
|
recipient: Type.Optional(Type.String({ description: "Recipient agent name. Defaults to team-lead for spawned agents." })),
|
|
315
316
|
content: Type.String(),
|
|
@@ -319,6 +320,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
319
320
|
const targetTeamName = requireCurrentSession(options);
|
|
320
321
|
const recipient = params.recipient || (options.isTeammate ? "team-lead" : undefined);
|
|
321
322
|
if (!recipient) throw new Error("recipient is required when the lead sends a message.");
|
|
323
|
+
if (options.isTeammate) requireLeadRecipient(recipient);
|
|
322
324
|
const deliveredDirectly = await options.deliverMessageToActiveAgent?.(targetTeamName, recipient, params.content) === true;
|
|
323
325
|
if (!deliveredDirectly) {
|
|
324
326
|
await messaging.sendPlainMessageIfRunning(targetTeamName, options.agentName, recipient, params.content, params.summary || "Message");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export const USER_INTERACTION_TOOLS = ["ask_user", "ask_user_batch", "orb_ask", "orb_say"];
|
|
6
|
+
export const SPAWNED_AGENT_COMMUNICATION_GUIDANCE = readFileSync(
|
|
7
|
+
path.join(__dirname, "../prompts/spawned-agent-communication.md"), "utf8"
|
|
8
|
+
).trim();
|
|
9
|
+
|
|
10
|
+
export function requireLeadRecipient(recipient: string): void {
|
|
11
|
+
if (recipient !== "team-lead") {
|
|
12
|
+
throw new Error("Spawned agents may send messages only to team-lead.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function registerSpawnedAgentCommunicationGuard(pi: Pick<ExtensionAPI, "on">): void {
|
|
17
|
+
pi.on("tool_call", event => {
|
|
18
|
+
if (USER_INTERACTION_TOOLS.includes(event.toolName)) {
|
|
19
|
+
return { block: true, reason: "Spawned agents cannot contact the user. Send your question to team-lead using send_message." };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|