pi-extended-teams 2.3.2 → 2.4.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.
|
@@ -122,7 +122,7 @@ export function createAgentCommunicationTools(options: AgentCommunicationToolsOp
|
|
|
122
122
|
{
|
|
123
123
|
name: "read_inbox",
|
|
124
124
|
label: "Read Inbox",
|
|
125
|
-
description: "Read this agent's inbox
|
|
125
|
+
description: "Read this agent's inbox with current sender message-admission status. Status is a snapshot, not proof of process liveness; do not send to unavailable senders.",
|
|
126
126
|
parameters: Type.Object({
|
|
127
127
|
unread_only: Type.Optional(Type.Boolean({ default: true })),
|
|
128
128
|
mark_as_read: Type.Optional(Type.Boolean({ default: true, description: "Set false to peek without marking messages read." })),
|
|
@@ -131,7 +131,7 @@ export function createAgentCommunicationTools(options: AgentCommunicationToolsOp
|
|
|
131
131
|
const teamName = requireCurrentSession(options);
|
|
132
132
|
const markAsRead = params.mark_as_read !== false;
|
|
133
133
|
const unreadOnly = params.unread_only !== false;
|
|
134
|
-
const msgs = await messaging.
|
|
134
|
+
const msgs = await messaging.readInboxWithSenderStatus(teamName, options.agentName, unreadOnly, markAsRead);
|
|
135
135
|
const lifecycleRunId = options.getLifecycleRunId();
|
|
136
136
|
// Do not fail after read flags are persisted; telemetry is best-effort.
|
|
137
137
|
if (markAsRead && lifecycleRunId) {
|
|
@@ -333,7 +333,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
333
333
|
pi.registerTool({
|
|
334
334
|
name: "read_inbox",
|
|
335
335
|
label: "Read Inbox",
|
|
336
|
-
description: "Read messages from the current Pi session inbox. Defaults to
|
|
336
|
+
description: "Read messages from the current Pi session inbox with current sender message-admission status. Defaults to your inbox. Status is a snapshot, not proof of process liveness; do not send to unavailable senders.",
|
|
337
337
|
parameters: Type.Object({
|
|
338
338
|
agent_name: Type.Optional(Type.String({ description: "Whose inbox to read. Defaults to your own." })),
|
|
339
339
|
unread_only: Type.Optional(Type.Boolean({ default: true })),
|
|
@@ -345,7 +345,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
345
345
|
if (process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1" && targetAgent !== options.agentName) throw new Error("A resumed subagent can only read its own inbox.");
|
|
346
346
|
const markAsRead = params.mark_as_read !== false;
|
|
347
347
|
const unreadOnly = params.unread_only !== false;
|
|
348
|
-
const msgs = await messaging.
|
|
348
|
+
const msgs = await messaging.readInboxWithSenderStatus(targetTeamName, targetAgent, unreadOnly, markAsRead);
|
|
349
349
|
|
|
350
350
|
if (markAsRead && options.isTeammate && targetAgent === options.agentName) {
|
|
351
351
|
const config = await teams.readConfig(targetTeamName).catch(() => null);
|
|
@@ -28,7 +28,7 @@ export interface AgentFollowTranscriptOptions {
|
|
|
28
28
|
|
|
29
29
|
type TranscriptBlock =
|
|
30
30
|
| { kind: "section"; label: "user" | "thinking" | "assistant"; text: string }
|
|
31
|
-
| { kind: "tool"; id?: string; name: string; args: unknown; result?: string; details?: unknown; isError?: boolean };
|
|
31
|
+
| { kind: "tool"; id?: string; name: string; args: unknown; result?: string; readSummary?: string; details?: unknown; isError?: boolean };
|
|
32
32
|
|
|
33
33
|
function stringifyToolArgs(args: unknown): string {
|
|
34
34
|
if (args === undefined) return "";
|
|
@@ -42,6 +42,7 @@ function stringifyToolArgs(args: unknown): string {
|
|
|
42
42
|
function compactToolArgs(name: string, args: unknown): string {
|
|
43
43
|
if (!args || typeof args !== "object") return sanitizeTuiLine(stringifyToolArgs(args));
|
|
44
44
|
const values = args as Record<string, unknown>;
|
|
45
|
+
if (name === "ls") return compactTranscriptLine(String(values.path ?? "."));
|
|
45
46
|
const primary = name === "bash"
|
|
46
47
|
? values.command
|
|
47
48
|
: name === "read"
|
|
@@ -226,6 +227,14 @@ function renderToolBlock(theme: ExtendedTeamsTheme, block: Extract<TranscriptBlo
|
|
|
226
227
|
if (compactBlock) return compactBlock;
|
|
227
228
|
|
|
228
229
|
const header = renderToolHeader(theme, block);
|
|
230
|
+
if (!expandLargeToolResults && (block.name === "read" || block.name === "bash" || block.name === "ls")) {
|
|
231
|
+
const state = block.result === undefined
|
|
232
|
+
? pendingText(theme, "working")
|
|
233
|
+
: block.isError ? failureText(theme, "✗") : successText(theme, "✓");
|
|
234
|
+
const summary = block.name === "read" && !block.isError && block.readSummary
|
|
235
|
+
? mutedText(theme, ` · ${block.readSummary}`) : "";
|
|
236
|
+
return [boundTranscriptLine(`${header}${summary}${mutedText(theme, " · ")}${state}`, width)];
|
|
237
|
+
}
|
|
229
238
|
if (block.result === undefined) {
|
|
230
239
|
return [header, `${structuralText(theme, "│")} ${pendingText(theme, "waiting for result…")}`, `${structuralText(theme, "╰─")} ${pendingText(theme, "running")}`, ""];
|
|
231
240
|
}
|
|
@@ -290,28 +299,40 @@ export function formatAgentFollowTranscript(messages: any[], options: AgentFollo
|
|
|
290
299
|
const name = sanitizeTuiLine(String(message.toolName || "tool"));
|
|
291
300
|
const matchingTool = (id ? toolsById.get(id) : undefined)
|
|
292
301
|
?? blocks.slice().reverse().find((block): block is Extract<TranscriptBlock, { kind: "tool" }> => block.kind === "tool" && block.result === undefined && block.name === name);
|
|
302
|
+
let readSummary: string | undefined;
|
|
303
|
+
if ((matchingTool?.name ?? name) === "read") {
|
|
304
|
+
const text = typeof message.content === "string" ? message.content
|
|
305
|
+
: (Array.isArray(message.content) ? message.content : [])
|
|
306
|
+
.filter((part: any) => part?.type === "text" && typeof part.text === "string")
|
|
307
|
+
.map((part: any) => part.text).join("\n");
|
|
308
|
+
const lineCount = text ? text.replace(/\r\n?/g, "\n").replace(/\n$/, "").split("\n").length : 0;
|
|
309
|
+
readSummary = `${lineCount} line${lineCount === 1 ? "" : "s"} · ${formatResultSize(text)}`;
|
|
310
|
+
}
|
|
293
311
|
const result = sanitizeTuiText(extractTextParts(message.content));
|
|
294
312
|
const isError = typeof message.isError === "boolean" ? message.isError : undefined;
|
|
295
313
|
if (matchingTool) {
|
|
296
314
|
matchingTool.result = result;
|
|
315
|
+
matchingTool.readSummary = readSummary;
|
|
297
316
|
matchingTool.details = message.details;
|
|
298
317
|
matchingTool.isError = isError;
|
|
299
318
|
} else {
|
|
300
|
-
blocks.push({ kind: "tool", id, name, args: undefined, result, details: message.details, isError });
|
|
319
|
+
blocks.push({ kind: "tool", id, name, args: undefined, result, readSummary, details: message.details, isError });
|
|
301
320
|
}
|
|
302
321
|
}
|
|
303
322
|
}
|
|
304
323
|
|
|
305
|
-
const lines =
|
|
324
|
+
const lines: string[] = [];
|
|
325
|
+
for (const block of blocks) {
|
|
306
326
|
if (block.kind === "tool") {
|
|
307
|
-
|
|
327
|
+
lines.push(...renderToolBlock(theme, block, options.expandLargeToolResults === true, options.width));
|
|
328
|
+
} else if (block.label === "thinking") {
|
|
329
|
+
if (lines.length > 0 && lines[lines.length - 1] !== "") lines.push("");
|
|
330
|
+
lines.push(theme.fg("thinkingText", block.label), block.text.replace(/\*\*/g, ""), "");
|
|
331
|
+
} else {
|
|
332
|
+
const labelToken = block.label === "user" ? "customMessageLabel" : "accent";
|
|
333
|
+
lines.push(theme.fg(labelToken, block.label), block.text, "");
|
|
308
334
|
}
|
|
309
|
-
|
|
310
|
-
return [theme.fg("thinkingText", block.label), block.text.replace(/\*\*/g, ""), ""];
|
|
311
|
-
}
|
|
312
|
-
const labelToken = block.label === "user" ? "customMessageLabel" : "accent";
|
|
313
|
-
return [theme.fg(labelToken, block.label), block.text, ""];
|
|
314
|
-
});
|
|
335
|
+
}
|
|
315
336
|
return lines.length > 0 ? lines : [theme.fg("dim", "Waiting for the agent's first transcript event…")];
|
|
316
337
|
}
|
|
317
338
|
|
|
@@ -104,7 +104,11 @@ export function summarizeInboxMessage(message: any): string {
|
|
|
104
104
|
const from = message?.from || "unknown";
|
|
105
105
|
const summary = message?.summary || "message";
|
|
106
106
|
const timestamp = message?.timestamp ? new Date(message.timestamp).toLocaleTimeString() : "";
|
|
107
|
-
|
|
107
|
+
const sender = message?.senderStatus;
|
|
108
|
+
const availability = sender
|
|
109
|
+
? ` [sender now: ${sender.status}; can message: ${sender.canMessage === null ? "unknown" : sender.canMessage ? "yes" : "no"}]`
|
|
110
|
+
: "";
|
|
111
|
+
return `${from}${availability}${timestamp ? ` ${timestamp}` : ""}: ${summary}`;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
export function formatInboxMessagesForModel(messages: any[]): string {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extended-teams",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Control-first, session-connected subagents for Pi with live navigation and intent-tier routing",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
30
|
"test": "vitest run",
|
|
31
|
-
"test:focused": "vitest run src/utils/claims.test.ts src/utils/tasks.test.ts src/utils/model-resolution.test.ts src/utils/settings.test.ts src/utils/write-queue.test.ts src/utils/shared-memory.test.ts extensions/index.test.ts extensions/events/register-events.test.ts extensions/internal/session-files.test.ts extensions/team/contracts.test.ts extensions/tools/delegation-guard.test.ts extensions/tools/agent-communication-tools.test.ts extensions/tools/read-helper.test.ts extensions/agents/read-agent.test.ts extensions/agents/write-agent.test.ts extensions/tools/team-tools.read-agent.test.ts extensions/ui/agent-follow-view.test.ts extensions/tools/coordination-tools.test.ts",
|
|
31
|
+
"test:focused": "vitest run --maxWorkers=2 src/utils/claims.test.ts src/utils/tasks.test.ts src/utils/model-resolution.test.ts src/utils/settings.test.ts src/utils/write-queue.test.ts src/utils/shared-memory.test.ts extensions/index.test.ts extensions/events/register-events.test.ts extensions/internal/session-files.test.ts extensions/team/contracts.test.ts extensions/tools/delegation-guard.test.ts extensions/tools/agent-communication-tools.test.ts extensions/tools/read-helper.test.ts extensions/agents/read-agent.test.ts extensions/agents/write-agent.test.ts extensions/tools/team-tools.read-agent.test.ts extensions/ui/agent-follow-view.test.ts extensions/tools/coordination-tools.test.ts",
|
|
32
32
|
"build": "tsc --noEmit"
|
|
33
33
|
},
|
|
34
34
|
"main": "extensions/index.ts",
|
package/src/utils/messaging.ts
CHANGED
|
@@ -7,7 +7,12 @@ import { InboxMessage, TeamConfig } from "./models";
|
|
|
7
7
|
import { withLock } from "./lock";
|
|
8
8
|
import { configPath, inboxPath } from "./paths";
|
|
9
9
|
import { readConfig } from "./teams";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
assertLifecycleTombstoneAbsent,
|
|
12
|
+
readLifecycleTombstoneSnapshot,
|
|
13
|
+
withLifecycleTombstoneLock,
|
|
14
|
+
type LifecycleTombstonePhase,
|
|
15
|
+
} from "./lifecycle-tombstone";
|
|
11
16
|
|
|
12
17
|
export interface MessageMetadataOptions {
|
|
13
18
|
id?: string;
|
|
@@ -272,6 +277,57 @@ export async function readInbox(
|
|
|
272
277
|
});
|
|
273
278
|
}
|
|
274
279
|
|
|
280
|
+
export interface InboxSenderStatus {
|
|
281
|
+
status: "lead" | "active" | "not_running" | "quarantined" | "unknown" | LifecycleTombstonePhase;
|
|
282
|
+
canMessage: boolean | null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function readInboxWithSenderStatus(
|
|
286
|
+
teamName: string,
|
|
287
|
+
agentName: string,
|
|
288
|
+
unreadOnly = false,
|
|
289
|
+
markAsRead = true
|
|
290
|
+
): Promise<Array<InboxMessage & { senderStatus: InboxSenderStatus }>> {
|
|
291
|
+
const messages = await readInbox(teamName, agentName, unreadOnly, markAsRead);
|
|
292
|
+
if (messages.length === 0) return [];
|
|
293
|
+
|
|
294
|
+
// Advisory admission snapshot, not process liveness or a reservation to send.
|
|
295
|
+
// Enrich only returned copies: persisted inbox history must not cache live status.
|
|
296
|
+
let members: TeamConfig["members"] | undefined;
|
|
297
|
+
try {
|
|
298
|
+
const config = JSON.parse(fs.readFileSync(configPath(teamName), "utf-8")) as TeamConfig;
|
|
299
|
+
if (Array.isArray(config.members)) members = config.members;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") members = [];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const senders = new Map<string, InboxSenderStatus>();
|
|
305
|
+
for (const sender of new Set(messages.map(message => message.from))) {
|
|
306
|
+
let senderStatus: InboxSenderStatus = { status: "unknown", canMessage: null };
|
|
307
|
+
try {
|
|
308
|
+
if (sender === "team-lead") {
|
|
309
|
+
senderStatus = { status: "lead", canMessage: true };
|
|
310
|
+
} else {
|
|
311
|
+
const fence = readLifecycleTombstoneSnapshot(teamName, sender);
|
|
312
|
+
if (fence.status !== "absent") {
|
|
313
|
+
senderStatus = {
|
|
314
|
+
status: fence.status === "corrupt" ? "quarantined" : fence.tombstone.phase,
|
|
315
|
+
canMessage: false,
|
|
316
|
+
};
|
|
317
|
+
} else if (members) {
|
|
318
|
+
const member = members.find(member => member.name === sender);
|
|
319
|
+
const canMessage = !!member && member.isActive !== false;
|
|
320
|
+
senderStatus = { status: canMessage ? "active" : "not_running", canMessage };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
// Status lookup must not discard messages after read flags were saved.
|
|
325
|
+
}
|
|
326
|
+
senders.set(sender, senderStatus);
|
|
327
|
+
}
|
|
328
|
+
return messages.map(message => ({ ...message, senderStatus: senders.get(message.from)! }));
|
|
329
|
+
}
|
|
330
|
+
|
|
275
331
|
export async function markInboxMessagesRead(teamName: string, agentName: string, ids: string[]): Promise<void> {
|
|
276
332
|
const p = inboxPath(teamName, agentName);
|
|
277
333
|
if (!fs.existsSync(p)) throw new Error("Referenced inbox is unavailable.");
|