pi-extended-teams 2.1.16

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.
Files changed (130) hide show
  1. package/README.md +105 -0
  2. package/assets/pi-extended-teams-agent-navigation.png +0 -0
  3. package/assets/pi-extended-teams-in-action.png +0 -0
  4. package/extensions/agents/read-agent-report.ts +181 -0
  5. package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
  6. package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
  7. package/extensions/agents/read-agent.test.ts +3077 -0
  8. package/extensions/agents/read-agent.ts +1119 -0
  9. package/extensions/agents/write-agent.test.ts +513 -0
  10. package/extensions/agents/write-agent.ts +392 -0
  11. package/extensions/events/register-events.test.ts +465 -0
  12. package/extensions/events/register-events.ts +409 -0
  13. package/extensions/index.test.ts +1659 -0
  14. package/extensions/index.ts +1228 -0
  15. package/extensions/internal/agent-session-files.test.ts +164 -0
  16. package/extensions/internal/agent-session-files.ts +320 -0
  17. package/extensions/internal/debug.ts +44 -0
  18. package/extensions/internal/model-selection.ts +82 -0
  19. package/extensions/internal/pi-command.test.ts +191 -0
  20. package/extensions/internal/pi-command.ts +224 -0
  21. package/extensions/internal/pi-runtime-api.test.ts +43 -0
  22. package/extensions/internal/pi-runtime-api.ts +78 -0
  23. package/extensions/internal/schema.ts +19 -0
  24. package/extensions/internal/session-context-reference.test.ts +290 -0
  25. package/extensions/internal/session-context-reference.ts +430 -0
  26. package/extensions/internal/session-files.test.ts +225 -0
  27. package/extensions/internal/session-files.ts +273 -0
  28. package/extensions/internal/session-usage.ts +59 -0
  29. package/extensions/resources/spawn-resource-plan.test.ts +233 -0
  30. package/extensions/resources/spawn-resource-plan.ts +247 -0
  31. package/extensions/runtime/active-agent-sleep.test.ts +157 -0
  32. package/extensions/runtime/active-agent-sleep.ts +117 -0
  33. package/extensions/runtime/nested-read-agents.ts +22 -0
  34. package/extensions/runtime/pending-child-controller.test.ts +169 -0
  35. package/extensions/runtime/pending-child-controller.ts +280 -0
  36. package/extensions/runtime/types.ts +68 -0
  37. package/extensions/team/contracts.test.ts +111 -0
  38. package/extensions/team/lifecycle.test.ts +1095 -0
  39. package/extensions/team/lifecycle.ts +502 -0
  40. package/extensions/team/recipient-closure.test.ts +97 -0
  41. package/extensions/team/recipient-closure.ts +120 -0
  42. package/extensions/team/roster.test.ts +129 -0
  43. package/extensions/team/roster.ts +165 -0
  44. package/extensions/team/team-contracts.json +43 -0
  45. package/extensions/team/writer-screens.test.ts +50 -0
  46. package/extensions/team/writer-screens.ts +156 -0
  47. package/extensions/tools/agent-communication-tools.test.ts +306 -0
  48. package/extensions/tools/agent-communication-tools.ts +183 -0
  49. package/extensions/tools/coordination-tools.test.ts +670 -0
  50. package/extensions/tools/coordination-tools.ts +304 -0
  51. package/extensions/tools/delegation-guard.test.ts +95 -0
  52. package/extensions/tools/delegation-guard.ts +65 -0
  53. package/extensions/tools/file-claim-tools.test.ts +104 -0
  54. package/extensions/tools/file-claim-tools.ts +68 -0
  55. package/extensions/tools/model-tools.ts +53 -0
  56. package/extensions/tools/predefined-tools.test.ts +647 -0
  57. package/extensions/tools/predefined-tools.ts +331 -0
  58. package/extensions/tools/read-helper.test.ts +35 -0
  59. package/extensions/tools/task-runtime-tools.test.ts +334 -0
  60. package/extensions/tools/task-runtime-tools.ts +227 -0
  61. package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
  62. package/extensions/tools/team-tools.ts +1270 -0
  63. package/extensions/ui/agent-follow-view.test.ts +520 -0
  64. package/extensions/ui/agent-follow-view.ts +779 -0
  65. package/extensions/ui/agent-navigation.test.ts +52 -0
  66. package/extensions/ui/agent-navigation.ts +55 -0
  67. package/extensions/ui/ansi.ts +16 -0
  68. package/extensions/ui/extensions-command.test.ts +336 -0
  69. package/extensions/ui/extensions-command.ts +282 -0
  70. package/extensions/ui/favorite-models-command.test.ts +280 -0
  71. package/extensions/ui/favorite-models-command.ts +423 -0
  72. package/extensions/ui/frame.ts +77 -0
  73. package/extensions/ui/input.ts +17 -0
  74. package/extensions/ui/read-agent-status.test.ts +117 -0
  75. package/extensions/ui/read-agent-status.ts +125 -0
  76. package/extensions/ui/renderers.ts +243 -0
  77. package/extensions/ui/status-widget.test.ts +143 -0
  78. package/extensions/ui/status-widget.ts +440 -0
  79. package/extensions/ui-frame.test.ts +69 -0
  80. package/package.json +75 -0
  81. package/skills/teams.md +256 -0
  82. package/src/adapters/terminal-registry.ts +78 -0
  83. package/src/adapters/tmux-adapter.test.ts +276 -0
  84. package/src/adapters/tmux-adapter.ts +200 -0
  85. package/src/orchestration/index.ts +437 -0
  86. package/src/orchestration/orchestrator.test.ts +396 -0
  87. package/src/orchestration/types.ts +125 -0
  88. package/src/utils/atomic-json.ts +21 -0
  89. package/src/utils/claims.test.ts +137 -0
  90. package/src/utils/claims.ts +169 -0
  91. package/src/utils/hooks.test.ts +75 -0
  92. package/src/utils/hooks.ts +35 -0
  93. package/src/utils/lifecycle-tombstone.test.ts +105 -0
  94. package/src/utils/lifecycle-tombstone.ts +268 -0
  95. package/src/utils/lock.race.child.ts +44 -0
  96. package/src/utils/lock.race.test.ts +198 -0
  97. package/src/utils/lock.test.ts +90 -0
  98. package/src/utils/lock.ts +186 -0
  99. package/src/utils/messaging.test.ts +337 -0
  100. package/src/utils/messaging.ts +441 -0
  101. package/src/utils/model-resolution.test.ts +231 -0
  102. package/src/utils/model-resolution.ts +322 -0
  103. package/src/utils/models.test.ts +8 -0
  104. package/src/utils/models.ts +115 -0
  105. package/src/utils/paths.ts +81 -0
  106. package/src/utils/predefined-teams/types.ts +48 -0
  107. package/src/utils/predefined-teams.save-template.test.ts +74 -0
  108. package/src/utils/predefined-teams.test.ts +441 -0
  109. package/src/utils/predefined-teams.ts +471 -0
  110. package/src/utils/read-helper-queue.ts +99 -0
  111. package/src/utils/report-events.test.ts +154 -0
  112. package/src/utils/report-events.ts +222 -0
  113. package/src/utils/runtime.test.ts +314 -0
  114. package/src/utils/runtime.ts +261 -0
  115. package/src/utils/security.test.ts +43 -0
  116. package/src/utils/settings.test.ts +480 -0
  117. package/src/utils/settings.ts +645 -0
  118. package/src/utils/shared-memory.test.ts +80 -0
  119. package/src/utils/shared-memory.ts +81 -0
  120. package/src/utils/tasks.race.test.ts +44 -0
  121. package/src/utils/tasks.test.ts +229 -0
  122. package/src/utils/tasks.ts +396 -0
  123. package/src/utils/teams.ts +231 -0
  124. package/src/utils/terminal-adapter.ts +103 -0
  125. package/src/utils/thinking-levels.test.ts +56 -0
  126. package/src/utils/thinking-levels.ts +47 -0
  127. package/src/utils/workflow-metadata.test.ts +11 -0
  128. package/src/utils/workflow-metadata.ts +88 -0
  129. package/src/utils/write-queue.test.ts +251 -0
  130. package/src/utils/write-queue.ts +202 -0
@@ -0,0 +1,1119 @@
1
+ import type { AgentSession } from "@mariozechner/pi-coding-agent";
2
+ import * as runtime from "../../src/utils/runtime";
3
+ import * as teams from "../../src/utils/teams";
4
+ import * as messaging from "../../src/utils/messaging";
5
+ import * as reportEvents from "../../src/utils/report-events";
6
+ import type { Member } from "../../src/utils/models";
7
+ import type { AgentReportSource, CompletedAgentReport, RunningReadAgent } from "../runtime/types";
8
+ import { extractTextParts, sanitizeTuiLine } from "../ui/renderers";
9
+ import { createAgentCommunicationTools, type SubmittedAgentReport } from "../tools/agent-communication-tools";
10
+ import { requireWriteAgentTeam } from "../team/roster";
11
+ import { isPiPromptPlanningMember, shouldSuppressLeadReportInjection } from "../../src/utils/workflow-metadata";
12
+ import { canonicalPersistedModelSlot, loadSettings, requireFavoriteModelLevel } from "../../src/utils/settings";
13
+ import { closePersistedRecipient } from "../team/recipient-closure";
14
+ import { generateExtensionInstanceId, generateLifecycleRunId } from "../../src/utils/lifecycle-tombstone";
15
+ import { createLifecycleRuntime, type ShutdownTeammateOptions } from "../team/lifecycle";
16
+ import {
17
+ createSpawnResourcePlan,
18
+ parentProjectTrustForSpawn,
19
+ type SpawnResourcePlan,
20
+ } from "../resources/spawn-resource-plan";
21
+ import { loadPiRuntimeApi } from "../internal/pi-runtime-api";
22
+ import { preparePrivateAgentSessionDirectory } from "../internal/agent-session-files";
23
+ import { isEligibleNestedReadParent, NESTED_DELEGATION_TOOL_NAMES } from "../runtime/nested-read-agents";
24
+ import type { NestedReadAgentToolBinding } from "../tools/team-tools";
25
+ import type { ParentRunIdentity, PendingChildController } from "../runtime/pending-child-controller";
26
+ import {
27
+ closeReadAgentMessageDelivery,
28
+ enqueueReadAgentMessageDelivery,
29
+ installReadAgentSessionLifecycle,
30
+ type ReadAgentDeliveryCloseResult,
31
+ type ReadAgentTeardownResult,
32
+ } from "./read-agent-session-lifecycle";
33
+ import {
34
+ EMPTY_REPORT_RECOVERY_PROMPT,
35
+ ReadAgentReportUnavailableError,
36
+ nonEmptyReportText,
37
+ persistedSessionMessages,
38
+ readAgentRecoveryReference,
39
+ resolveReadAgentReport,
40
+ type PersistedReadAgentMessages,
41
+ type ResolvedReadAgentReport,
42
+ } from "./read-agent-report";
43
+
44
+ export { closeReadAgentMessageDelivery } from "./read-agent-session-lifecycle";
45
+
46
+ const pendingParentWakeSignals = new WeakMap<RunningReadAgent, () => void>();
47
+
48
+ export interface RunReadAgentOptions {
49
+ isTeammate: boolean;
50
+ getTeamName(): string | null | undefined;
51
+ runningReadAgents: Map<string, RunningReadAgent>;
52
+ readAgentKey(teamName: string, agentName: string): string;
53
+ isCurrentReadAgentRun(key: string, state: RunningReadAgent): boolean;
54
+ ensureReadAgentStatusTicker(): void;
55
+ renderReadAgentStatus(): void;
56
+ rememberCompletedAgentReport(teamName: string, report: CompletedAgentReport): void;
57
+ emitAgentReport(teamName: string, name: string, startedAt: number, tokens: number, report: string, ok: boolean, suppressLeadInjection?: boolean): void;
58
+ emitAgentProgress?(teamName: string, name: string, status: string, updatedAt: number): void;
59
+ releaseAllClaimsForAgent(teamName: string, agentName: string): Promise<string[]>;
60
+ shutdownTeammate?(
61
+ teamName: string,
62
+ member: Member,
63
+ options?: ShutdownTeammateOptions
64
+ ): Promise<ReadAgentTeardownResult>;
65
+ agentName?: string;
66
+ renderLeadInboxStatus?(): Promise<void>;
67
+ notifyLeadOfInboxReports?(teamName: string): Promise<void>;
68
+ deliverMessageToActiveAgent?(
69
+ teamName: string,
70
+ recipient: string,
71
+ content: string,
72
+ expectedRecipientRunId?: string
73
+ ): Promise<boolean>;
74
+ createResourcePlan?(input: { cwd: string; projectTrusted: boolean }): SpawnResourcePlan | Promise<SpawnResourcePlan>;
75
+ extensionInstanceId?: string;
76
+ createNestedReadAgentTools?(binding: NestedReadAgentToolBinding): any[];
77
+ pendingChildController?: PendingChildController;
78
+ }
79
+
80
+ function pushReadAgentEvent(agent: RunningReadAgent, text: string): void {
81
+ agent.recentEvents.push(text);
82
+ agent.recentEvents = agent.recentEvents.slice(-12);
83
+ }
84
+
85
+ function markReadAgentActivity(
86
+ agent: RunningReadAgent,
87
+ text: string,
88
+ status: RunningReadAgent["status"],
89
+ activeToolName?: string
90
+ ): void {
91
+ agent.status = status;
92
+ agent.lastActivityAt = Date.now();
93
+ agent.activeToolName = activeToolName;
94
+ agent.idleNudgeLevel = undefined;
95
+ pushReadAgentEvent(agent, text);
96
+ }
97
+
98
+ function refreshReadAgentStats(agent: RunningReadAgent, session: AgentSession, activityAlreadyMarked = false): void {
99
+ const stats = session.getSessionStats();
100
+ const tokensUsed = stats.tokens.total;
101
+ if (tokensUsed !== agent.tokensUsed && !activityAlreadyMarked) {
102
+ agent.lastActivityAt = Date.now();
103
+ agent.idleNudgeLevel = undefined;
104
+ }
105
+ agent.tokensUsed = tokensUsed;
106
+ const contextUsage = stats.contextUsage ?? session.getContextUsage?.();
107
+ agent.contextUsage = tokensUsed > 0
108
+ ? contextUsage
109
+ : runtime.initialContextUsage(contextUsage?.contextWindow);
110
+ }
111
+
112
+ const MAX_ASSISTANT_PROGRESS_SNIPPET_CHARS = 180;
113
+ const MAX_ASSISTANT_PROGRESS_TAIL_CHARS = 384;
114
+
115
+ function clipAssistantProgressText(text: string): string | undefined {
116
+ const trimmed = text.trim();
117
+ if (!trimmed) return undefined;
118
+ return trimmed.length > MAX_ASSISTANT_PROGRESS_SNIPPET_CHARS
119
+ ? `…${trimmed.slice(-(MAX_ASSISTANT_PROGRESS_SNIPPET_CHARS - 1))}`
120
+ : trimmed;
121
+ }
122
+
123
+ function assistantProgressSnippetFromRawText(rawText: string): string | undefined {
124
+ return clipAssistantProgressText(sanitizeTuiLine(rawText));
125
+ }
126
+
127
+ function assistantProgressSnippetFromSafeTail(normalizedText: string): string | undefined {
128
+ return clipAssistantProgressText(normalizedText);
129
+ }
130
+
131
+ function normalizeIncrementalAssistantDelta(delta: string): string | null {
132
+ let normalized: string | undefined;
133
+ let segmentStart = 0;
134
+ for (let index = 0; index < delta.length; index++) {
135
+ const code = delta.charCodeAt(index);
136
+ if ((code <= 8) || (code >= 11 && code <= 31) || (code >= 127 && code <= 159)) return null;
137
+ if (code !== 9 && code !== 10) continue;
138
+ normalized = `${normalized || ""}${delta.slice(segmentStart, index)}${code === 9 ? " " : " "}`;
139
+ segmentStart = index + 1;
140
+ }
141
+ return normalized === undefined ? delta : `${normalized}${delta.slice(segmentStart)}`;
142
+ }
143
+
144
+ function assistantProgressSnippet(message: any): string | undefined {
145
+ if (message?.role !== "assistant") return undefined;
146
+ return assistantProgressSnippetFromRawText(extractTextParts(message.content));
147
+ }
148
+
149
+ function applyAssistantProgressSnippet(agent: RunningReadAgent, snippet: string | undefined, recordEvent: boolean): boolean {
150
+ if (!snippet || snippet === agent.latestAssistantSnippet) return false;
151
+ agent.latestAssistantSnippet = snippet;
152
+ agent.lastActivityAt = Date.now();
153
+ agent.idleNudgeLevel = undefined;
154
+ if (recordEvent) pushReadAgentEvent(agent, `assistant: ${snippet}`);
155
+ return true;
156
+ }
157
+
158
+ function updateAssistantProgress(agent: RunningReadAgent, message: any, recordEvent: boolean): boolean {
159
+ return applyAssistantProgressSnippet(agent, assistantProgressSnippet(message), recordEvent);
160
+ }
161
+
162
+ function resetIncrementalAssistantProgress(agent: RunningReadAgent): void {
163
+ agent.assistantProgressNormalizedTail = "";
164
+ agent.assistantProgressTailTruncated = false;
165
+ agent.assistantProgressContentIndex = undefined;
166
+ agent.assistantProgressNeedsSeparator = false;
167
+ agent.assistantProgressIncrementalUnsafe = false;
168
+ }
169
+
170
+ function updateAssistantProgressFromEvent(agent: RunningReadAgent, event: any): boolean {
171
+ const update = event.assistantMessageEvent;
172
+ if (!update || typeof update.type !== "string") {
173
+ return updateAssistantProgress(agent, event.message, false);
174
+ }
175
+
176
+ if (update.type === "text_start") {
177
+ agent.assistantProgressNeedsSeparator = agent.assistantProgressContentIndex !== undefined
178
+ && agent.assistantProgressContentIndex !== update.contentIndex;
179
+ agent.assistantProgressContentIndex = update.contentIndex;
180
+ return false;
181
+ }
182
+
183
+ if (update.type !== "text_delta") {
184
+ return update.type === "text_end"
185
+ ? updateAssistantProgress(agent, event.message, false)
186
+ : false;
187
+ }
188
+
189
+ const delta = String(update.delta || "");
190
+ if (agent.assistantProgressIncrementalUnsafe) return updateAssistantProgress(agent, event.message, false);
191
+ const normalizedDelta = normalizeIncrementalAssistantDelta(delta);
192
+ if (normalizedDelta === null) {
193
+ agent.assistantProgressIncrementalUnsafe = true;
194
+ return updateAssistantProgress(agent, event.message, false);
195
+ }
196
+
197
+ const needsSeparator = agent.assistantProgressNeedsSeparator
198
+ || (agent.assistantProgressContentIndex !== undefined && agent.assistantProgressContentIndex !== update.contentIndex);
199
+ agent.assistantProgressContentIndex = update.contentIndex;
200
+ agent.assistantProgressNeedsSeparator = false;
201
+ let normalizedTail = `${agent.assistantProgressNormalizedTail || ""}${needsSeparator ? " " : ""}${normalizedDelta}`;
202
+ if (normalizedTail.length > MAX_ASSISTANT_PROGRESS_TAIL_CHARS) {
203
+ normalizedTail = normalizedTail.slice(-MAX_ASSISTANT_PROGRESS_TAIL_CHARS);
204
+ agent.assistantProgressTailTruncated = true;
205
+ }
206
+ agent.assistantProgressNormalizedTail = normalizedTail;
207
+
208
+ const snippet = assistantProgressSnippetFromSafeTail(normalizedTail);
209
+ if (agent.assistantProgressTailTruncated && (!snippet || snippet.length < MAX_ASSISTANT_PROGRESS_SNIPPET_CHARS)) {
210
+ return updateAssistantProgress(agent, event.message, false);
211
+ }
212
+ return applyAssistantProgressSnippet(agent, snippet, false);
213
+ }
214
+
215
+ export function handleReadAgentSessionEvent(
216
+ state: RunningReadAgent,
217
+ session: AgentSession,
218
+ event: any,
219
+ renderReadAgentStatus: () => void
220
+ ): void {
221
+ const eventType = event.type;
222
+ if (eventType === "agent_start" || eventType === "turn_start") {
223
+ markReadAgentActivity(state, "thinking", "thinking");
224
+ }
225
+ if (eventType === "message_start" && event.message?.role === "assistant") {
226
+ resetIncrementalAssistantProgress(state);
227
+ markReadAgentActivity(state, "thinking", "thinking");
228
+ }
229
+ let assistantActivityMarked = false;
230
+ if (eventType === "message_update") {
231
+ assistantActivityMarked = updateAssistantProgressFromEvent(state, event);
232
+ if (assistantActivityMarked) {
233
+ state.status = "thinking";
234
+ state.activeToolName = undefined;
235
+ }
236
+ }
237
+ if (eventType === "message_end" && event.message?.role === "assistant") {
238
+ assistantActivityMarked = updateAssistantProgress(state, event.message, true);
239
+ resetIncrementalAssistantProgress(state);
240
+ }
241
+ if (eventType === "tool_execution_start") {
242
+ markReadAgentActivity(state, `working: ${event.toolName}`, "working", event.toolName);
243
+ }
244
+ if (eventType === "tool_execution_update") {
245
+ markReadAgentActivity(state, `working: ${event.toolName}`, "working", event.toolName);
246
+ }
247
+ if (eventType === "tool_execution_end") {
248
+ markReadAgentActivity(state, "thinking", "thinking");
249
+ }
250
+ if (eventType === "agent_end") pushReadAgentEvent(state, "agent complete");
251
+ if (eventType === "message_update" || eventType === "message_end" || eventType === "turn_end" || eventType === "agent_end") {
252
+ try {
253
+ refreshReadAgentStats(state, session, assistantActivityMarked);
254
+ renderReadAgentStatus();
255
+ } catch {
256
+ // Ignore stats races while the nested session is shutting down.
257
+ }
258
+ }
259
+ }
260
+
261
+ export async function sendMessageToRunningReadAgent(agent: RunningReadAgent | undefined, content: string): Promise<boolean> {
262
+ if (!agent) return false;
263
+ if (!agent.session || !agent.acceptingMessages || agent.messageDeliveryClosed || agent.stopRequested) {
264
+ throw new Error(`Cannot send message to ${agent.name}: agent is finishing.`);
265
+ }
266
+
267
+ const session = agent.session;
268
+ const delivery = session.isStreaming ? { deliverAs: "steer" as const } : undefined;
269
+ const deliveryResult = enqueueReadAgentMessageDelivery(
270
+ agent,
271
+ agent.name,
272
+ () => session.sendUserMessage(content, delivery)
273
+ );
274
+ pendingParentWakeSignals.get(agent)?.();
275
+ await deliveryResult;
276
+ markReadAgentActivity(agent, "received lead message", "thinking");
277
+ return true;
278
+ }
279
+
280
+ async function hasRecentMessageFrom(
281
+ teamName: string,
282
+ fromName: string,
283
+ toName: string,
284
+ sinceMs: number,
285
+ matches: (message: any) => boolean = () => true
286
+ ): Promise<boolean> {
287
+ const messages = await messaging.readInbox(teamName, toName, false, false).catch(() => []);
288
+ return messages.some((message: any) => {
289
+ const timestamp = message.timestamp ? new Date(message.timestamp).getTime() : 0;
290
+ return message.from === fromName
291
+ && timestamp >= sinceMs - 1000
292
+ && String(message.text || "").trim().length > 0
293
+ && matches(message);
294
+ });
295
+ }
296
+
297
+ function operationMetadataFromMember(member: Member): { operationId?: string; workflowRunId?: string } {
298
+ return {
299
+ operationId: member.metadata?.operationId || member.metadata?.orchestration?.operationId,
300
+ workflowRunId: member.metadata?.workflowRunId || member.metadata?.orchestration?.workflowRunId,
301
+ };
302
+ }
303
+
304
+ function reportPersistenceBlockReason(
305
+ description: string,
306
+ error: unknown,
307
+ recoveryReference: ReturnType<typeof readAgentRecoveryReference>,
308
+ ): string {
309
+ const persistenceError = error instanceof Error ? error.message : String(error);
310
+ return [
311
+ `${description}: ${persistenceError}`,
312
+ `Recovery pointer: ${recoveryReference.pointer}`,
313
+ recoveryReference.guidance,
314
+ ].join("\n");
315
+ }
316
+
317
+ async function recordReadAgentReportEvent(
318
+ teamName: string,
319
+ member: Member,
320
+ status: "completed" | "failed",
321
+ report: string,
322
+ summary: string,
323
+ startedAt: number,
324
+ tokensUsed: number,
325
+ costUsd?: number,
326
+ color?: string,
327
+ reportMetadata: Record<string, any> = {}
328
+ ): Promise<{ persisted: true } | { persisted: false; error: unknown }> {
329
+ const operation = operationMetadataFromMember(member);
330
+ const modelSlot = canonicalPersistedModelSlot(member.modelSlot);
331
+ try {
332
+ await reportEvents.appendTeamReportEvent(teamName, {
333
+ agentName: member.name,
334
+ role: member.role || "read",
335
+ status,
336
+ report,
337
+ summary,
338
+ startedAt,
339
+ elapsedMs: Date.now() - startedAt,
340
+ tokensUsed,
341
+ costUsd,
342
+ model: member.model,
343
+ thinking: member.thinking,
344
+ modelSlot,
345
+ color: color || member.color,
346
+ requestedBy: member.requestedBy,
347
+ source: "read-agent",
348
+ operationId: operation.operationId,
349
+ workflowRunId: operation.workflowRunId,
350
+ metadata: {
351
+ ...(member.prompt ? { initialPrompt: member.prompt } : {}),
352
+ ...(modelSlot ? { modelSlot } : {}),
353
+ ...reportMetadata,
354
+ },
355
+ });
356
+ return { persisted: true };
357
+ } catch (error) {
358
+ return { persisted: false, error };
359
+ }
360
+ }
361
+
362
+ async function ensureLeadCompletionMessage(
363
+ teamName: string,
364
+ member: Member,
365
+ startedAt: number,
366
+ report: string,
367
+ summary: string,
368
+ color: string | undefined,
369
+ metadata: Record<string, any>
370
+ ): Promise<void> {
371
+ const leadHasReport = await hasRecentMessageFrom(teamName, member.name, "team-lead", startedAt);
372
+ if (leadHasReport) return;
373
+
374
+ await messaging.sendPlainMessage(
375
+ teamName,
376
+ member.name,
377
+ "team-lead",
378
+ report,
379
+ summary,
380
+ color,
381
+ { metadata }
382
+ );
383
+ }
384
+
385
+ async function ensureReadHelperCompletionMessages(
386
+ teamName: string,
387
+ member: Member,
388
+ startedAt: number,
389
+ runId: string,
390
+ report: string,
391
+ outcome: "completed" | "failed" = "completed",
392
+ color = member.color,
393
+ deliverMessageToActiveAgent?: RunReadAgentOptions["deliverMessageToActiveAgent"]
394
+ ): Promise<void> {
395
+ if (!member.requestedBy) return;
396
+
397
+ const expectedRequesterRunId = member.parentAgentName === member.requestedBy
398
+ ? member.parentLifecycleRunId
399
+ : undefined;
400
+ let requesterReceivedReport = false;
401
+ try {
402
+ const deliveredDirectly = expectedRequesterRunId
403
+ ? await deliverMessageToActiveAgent?.(teamName, member.requestedBy, report, expectedRequesterRunId) === true
404
+ : await deliverMessageToActiveAgent?.(teamName, member.requestedBy, report) === true;
405
+ if (deliveredDirectly) {
406
+ requesterReceivedReport = true;
407
+ } else {
408
+ requesterReceivedReport = await hasRecentMessageFrom(
409
+ teamName,
410
+ member.name,
411
+ member.requestedBy,
412
+ startedAt,
413
+ message => message?.metadata?.helperReport === true && message?.metadata?.runId === runId
414
+ );
415
+ if (!requesterReceivedReport) {
416
+ await messaging.sendPlainMessageIfRunning(
417
+ teamName,
418
+ member.name,
419
+ member.requestedBy,
420
+ report,
421
+ outcome === "failed" ? `Read helper ${member.name} failed` : `Read helper ${member.name} report`,
422
+ color,
423
+ {
424
+ metadata: { helperReport: true, helperCompletion: true, runId, outcome, requestedBy: member.requestedBy },
425
+ ...(expectedRequesterRunId ? { expectedRecipientRunId: expectedRequesterRunId } : {}),
426
+ }
427
+ );
428
+ requesterReceivedReport = true;
429
+ }
430
+ }
431
+ } catch {
432
+ requesterReceivedReport = false;
433
+ }
434
+
435
+ const leadHasClassifiedNotice = await hasRecentMessageFrom(
436
+ teamName,
437
+ member.name,
438
+ "team-lead",
439
+ startedAt,
440
+ message => message?.metadata?.finalReport === true
441
+ && message?.metadata?.helperCompletion === true
442
+ && message?.metadata?.runId === runId
443
+ );
444
+ if (!leadHasClassifiedNotice) {
445
+ const delivery = requesterReceivedReport
446
+ ? `Report sent to ${member.requestedBy}.`
447
+ : `${member.requestedBy} is no longer running; the report is retained here.`;
448
+ await messaging.sendPlainMessage(
449
+ teamName,
450
+ member.name,
451
+ "team-lead",
452
+ outcome === "failed"
453
+ ? `Read helper ${member.name} failed for ${member.requestedBy}. ${delivery}`
454
+ : `Read helper ${member.name} completed for ${member.requestedBy}. ${delivery}`,
455
+ outcome === "failed" ? `Read helper ${member.name} failed` : `Read helper ${member.name} done`,
456
+ color,
457
+ { metadata: { finalReport: true, helperCompletion: true, runId, outcome, requestedBy: member.requestedBy } }
458
+ );
459
+ }
460
+ }
461
+
462
+ function assertMemberUsesConfiguredLevel(member: Member): void {
463
+ const settings = loadSettings({ projectDir: member.cwd });
464
+ const level = requireFavoriteModelLevel(settings, member.modelSlot);
465
+ const role = member.role || "read";
466
+ if (role !== level.role) {
467
+ throw new Error(`Agent ${member.name} level ${level.slot} resolves to role ${level.role}, not ${role}. Spawn agents by level only.`);
468
+ }
469
+ if (member.model !== level.model || member.thinking !== level.thinking) {
470
+ throw new Error(`Agent ${member.name} must use configured level ${level.slot}; direct model/thinking overrides are not allowed.`);
471
+ }
472
+ }
473
+
474
+ export async function runReadAgentInProcess(
475
+ readTeamName: string,
476
+ member: Member,
477
+ prompt: string,
478
+ ctx: any,
479
+ options: RunReadAgentOptions
480
+ ): Promise<void> {
481
+ assertMemberUsesConfiguredLevel(member);
482
+ const key = options.readAgentKey(readTeamName, member.name);
483
+ const role = member.role || "read";
484
+ const roleLabel = role === "write" ? "edit-allowed" : "read-only";
485
+ const modelSlot = canonicalPersistedModelSlot(member.modelSlot);
486
+ let resolveFinished!: () => void;
487
+ const finished = new Promise<void>((resolve) => { resolveFinished = resolve; });
488
+ let resolveSessionCreation!: (session: AgentSession | undefined) => void;
489
+ const sessionCreation = new Promise<AgentSession | undefined>((resolve) => {
490
+ resolveSessionCreation = resolve;
491
+ });
492
+ let lifecycleRunId = member.lifecycleRunId ?? generateLifecycleRunId();
493
+ if (teams.teamExists(readTeamName)) {
494
+ lifecycleRunId = await teams.ensureMemberLifecycleRunId(readTeamName, member.name, lifecycleRunId);
495
+ }
496
+ member.lifecycleRunId = lifecycleRunId;
497
+ const extensionInstanceId = options.extensionInstanceId ?? generateExtensionInstanceId();
498
+ const state: RunningReadAgent = {
499
+ runId: lifecycleRunId,
500
+ name: member.name,
501
+ teamName: readTeamName,
502
+ role,
503
+ startedAt: Date.now(),
504
+ tokensUsed: 0,
505
+ contextUsage: runtime.initialContextUsage(),
506
+ status: "starting",
507
+ recentEvents: [],
508
+ lastActivityAt: Date.now(),
509
+ model: member.model,
510
+ thinking: member.thinking,
511
+ modelSlot,
512
+ finished,
513
+ resolveFinished,
514
+ startupState: "pending",
515
+ sessionCreation,
516
+ teardownState: "active",
517
+ };
518
+ let sessionCreationSettled = false;
519
+ const settleSessionCreation = (session: AgentSession | undefined): void => {
520
+ if (sessionCreationSettled) return;
521
+ sessionCreationSettled = true;
522
+ state.startupState = session ? "session_created" : "failed";
523
+ resolveSessionCreation(session);
524
+ };
525
+ options.runningReadAgents.set(key, state);
526
+ options.ensureReadAgentStatusTicker();
527
+ // Production injects the shared lifecycle runtime. The fallback keeps direct
528
+ // library/test callers on that same owner instead of duplicating cleanup here.
529
+ const shutdownTeammate = options.shutdownTeammate ?? createLifecycleRuntime({
530
+ isTeammate: options.isTeammate,
531
+ terminal: null,
532
+ runningReadAgents: options.runningReadAgents,
533
+ readAgentKey: options.readAgentKey,
534
+ isCurrentReadAgentRun: options.isCurrentReadAgentRun,
535
+ renderReadAgentStatus: options.renderReadAgentStatus,
536
+ releaseAllClaimsForAgent: options.releaseAllClaimsForAgent,
537
+ drainWriteQueue: async () => {},
538
+ getSessionCwd: () => member.cwd,
539
+ getTeamName: () => readTeamName,
540
+ }).shutdownTeammate;
541
+
542
+ let submittedFinalReport: SubmittedAgentReport | undefined;
543
+ let finalReportSubmissionInProgress = false;
544
+ let childSessionManager: any;
545
+ let privateSessionDirectory: string | undefined;
546
+ let privateCompletedReportPersisted = false;
547
+ const pendingChildController = options.pendingChildController;
548
+ const pendingChildParent: ParentRunIdentity | undefined = isEligibleNestedReadParent(member)
549
+ ? {
550
+ teamName: readTeamName,
551
+ parentName: member.name,
552
+ parentRunId: state.runId,
553
+ }
554
+ : undefined;
555
+ let pendingChildGeneration: number | undefined;
556
+ if (pendingChildController && pendingChildParent) {
557
+ const snapshot = pendingChildController.observeParent(pendingChildParent);
558
+ pendingChildGeneration = snapshot.generation;
559
+ if (snapshot.cancelled) state.stopRequested = true;
560
+ pendingParentWakeSignals.set(state, () => {
561
+ pendingChildController.signalParentChange(pendingChildParent);
562
+ });
563
+ }
564
+
565
+ const closeRecipient = async (): Promise<ReadAgentDeliveryCloseResult> => {
566
+ if (pendingChildParent) pendingChildController?.cancelParent(pendingChildParent);
567
+ const deliveryClose = closeReadAgentMessageDelivery(state);
568
+ if (!state.recipientClosurePromise) {
569
+ state.recipientClosurePromise = closePersistedRecipient(
570
+ readTeamName,
571
+ member.name,
572
+ state.runId,
573
+ { removeOnFailure: true, role, reason: "quit", extensionInstanceId }
574
+ ).then(() => { state.persistedRecipientClosed = true; });
575
+ }
576
+ await state.recipientClosurePromise;
577
+ return deliveryClose;
578
+ };
579
+
580
+ const deliverCompletion = async (
581
+ resolution: ResolvedReadAgentReport,
582
+ completionSummary: string,
583
+ recoveryAttempted: boolean,
584
+ recoveryReference: ReturnType<typeof readAgentRecoveryReference>,
585
+ ): Promise<void> => {
586
+ const session = state.session;
587
+ if (!session) throw new Error(`Agent ${member.name} completed without a nested session.`);
588
+ const report = resolution.report!;
589
+ const completionStats = session.getSessionStats();
590
+ // Private child transcripts are deleted after teardown; never publish pointers
591
+ // that would outlive a successful run's recovery artifact.
592
+ const includeRecoveryReference = !privateSessionDirectory
593
+ && (recoveryAttempted || resolution.source?.startsWith("persisted-"));
594
+ const reportMetadata = {
595
+ reportSource: resolution.source,
596
+ recoveryAttempted,
597
+ ...(includeRecoveryReference && recoveryReference.sessionId
598
+ ? { recoverySessionId: recoveryReference.sessionId }
599
+ : {}),
600
+ ...(includeRecoveryReference && recoveryReference.sessionFile
601
+ ? { recoverySessionFile: recoveryReference.sessionFile }
602
+ : {}),
603
+ };
604
+ options.rememberCompletedAgentReport(readTeamName, {
605
+ name: member.name,
606
+ role,
607
+ status: "completed",
608
+ report,
609
+ summary: completionSummary,
610
+ completedAt: Date.now(),
611
+ startedAt: state.startedAt,
612
+ elapsedMs: Date.now() - state.startedAt,
613
+ tokensUsed: state.tokensUsed,
614
+ costUsd: completionStats.cost,
615
+ model: member.model,
616
+ thinking: member.thinking,
617
+ modelSlot,
618
+ color: member.color,
619
+ requestedBy: member.requestedBy,
620
+ initialPrompt: member.prompt || prompt,
621
+ ...reportMetadata,
622
+ source: "read-agent",
623
+ });
624
+ const completionMetadata = {
625
+ finalReport: true,
626
+ startedAt: state.startedAt,
627
+ elapsedMs: Date.now() - state.startedAt,
628
+ tokensUsed: state.tokensUsed,
629
+ costUsd: completionStats.cost,
630
+ model: member.model,
631
+ thinking: member.thinking,
632
+ modelSlot,
633
+ initialPrompt: member.prompt || prompt,
634
+ ...reportMetadata,
635
+ };
636
+ const reportEventPersistence = await recordReadAgentReportEvent(
637
+ readTeamName,
638
+ member,
639
+ "completed",
640
+ report,
641
+ completionSummary,
642
+ state.startedAt,
643
+ state.tokensUsed,
644
+ completionStats.cost,
645
+ undefined,
646
+ reportMetadata,
647
+ );
648
+ if (!reportEventPersistence.persisted) {
649
+ state.finalizationBlockedReason = reportPersistenceBlockReason(
650
+ "Could not durably persist the completed read-agent report",
651
+ reportEventPersistence.error,
652
+ recoveryReference,
653
+ );
654
+ throw new Error(state.finalizationBlockedReason);
655
+ }
656
+ // The lifecycle finalizer consumes this flag only after session disposal and
657
+ // successful lifecycle finalization, so every successful private run is removed.
658
+ if (privateSessionDirectory) {
659
+ state.cleanupPrivateSessionOnFinalize = true;
660
+ privateCompletedReportPersisted = true;
661
+ }
662
+ const suppressLeadReportInjection = shouldSuppressLeadReportInjection(member);
663
+ if (member.requestedBy) {
664
+ await ensureReadHelperCompletionMessages(
665
+ readTeamName,
666
+ member,
667
+ state.startedAt,
668
+ state.runId,
669
+ report,
670
+ "completed",
671
+ member.color,
672
+ options.deliverMessageToActiveAgent
673
+ );
674
+ await options.renderLeadInboxStatus?.().catch(() => {});
675
+ await options.notifyLeadOfInboxReports?.(readTeamName).catch(() => {});
676
+ } else if (suppressLeadReportInjection) {
677
+ if (isPiPromptPlanningMember(member)) {
678
+ options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed, report, true, true);
679
+ }
680
+ // Workflow orchestrators consume full reports from TeamReportEvent storage.
681
+ // Pi Prompt consumes its writer report through the private event without a lead turn.
682
+ } else if (!options.isTeammate && (options.getTeamName() === readTeamName || readTeamName.startsWith("prompt-build-"))) {
683
+ options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed, report, true);
684
+ } else {
685
+ await ensureLeadCompletionMessage(readTeamName, member, state.startedAt, report, completionSummary, member.color, completionMetadata);
686
+ }
687
+ };
688
+
689
+ try {
690
+ if (pendingChildParent && !pendingChildController) {
691
+ throw new Error(`Eligible nested read parent ${member.name} requires a pending child controller.`);
692
+ }
693
+ const [provider, modelId] = (member.model || "").split("/", 2);
694
+ const model = provider && modelId ? ctx.modelRegistry.find(provider, modelId) : undefined;
695
+ if (!model) {
696
+ throw new Error(`Read agent model "${member.model}" is not available.`);
697
+ }
698
+
699
+ await runtime.writeRuntimeStatus(readTeamName, member.name, state.runId, {
700
+ pid: process.pid,
701
+ startedAt: state.startedAt,
702
+ lastHeartbeatAt: Date.now(),
703
+ ready: true,
704
+ lastError: undefined,
705
+ });
706
+
707
+ if (!state.stopRequested) {
708
+ state.heartbeatTimer = setInterval(async () => {
709
+ try {
710
+ await runtime.writeRuntimeStatus(readTeamName, member.name, state.runId, {
711
+ lastHeartbeatAt: Date.now(),
712
+ });
713
+ } catch {
714
+ // Ignore heartbeat races during shutdown.
715
+ }
716
+ }, 5000);
717
+ }
718
+
719
+ const {
720
+ createAgentSession,
721
+ DefaultResourceLoader,
722
+ getAgentDir,
723
+ SessionManager,
724
+ SettingsManager,
725
+ } = await loadPiRuntimeApi();
726
+ const agentDir = getAgentDir();
727
+ const projectTrusted = parentProjectTrustForSpawn(ctx, member.cwd);
728
+ const resourcePlan = await (options.createResourcePlan ?? createSpawnResourcePlan)({
729
+ cwd: member.cwd,
730
+ projectTrusted,
731
+ });
732
+ const createSettingsManager = SettingsManager.create as unknown as (
733
+ cwd: string,
734
+ agentDir: string,
735
+ options: { projectTrusted: boolean },
736
+ ) => any;
737
+ const childSettingsManager = createSettingsManager(member.cwd, agentDir, {
738
+ projectTrusted: resourcePlan.trust.projectTrusted,
739
+ });
740
+ const nestedReadAgentTools = isEligibleNestedReadParent(member)
741
+ ? options.createNestedReadAgentTools?.({
742
+ teamName: readTeamName,
743
+ parent: member,
744
+ parentRunId: state.runId,
745
+ outerCtx: ctx,
746
+ }) ?? []
747
+ : [];
748
+ const nestedReadDelegationEnabled = nestedReadAgentTools.length > 0;
749
+ const loader = new DefaultResourceLoader({
750
+ cwd: member.cwd,
751
+ agentDir,
752
+ settingsManager: childSettingsManager,
753
+ noExtensions: true,
754
+ additionalExtensionPaths: [...resourcePlan.extensionPaths],
755
+ noSkills: false,
756
+ appendSystemPrompt: [
757
+ `You are ${roleLabel} agent '${member.name}' in Pi session '${readTeamName}', running in-process so the lead can follow and control you from Pi.`,
758
+ role === "write"
759
+ ? "You may use edit/write tools for the assigned scope only. Keep changes small, avoid unrelated cleanup, and report every file changed. Do not install or remove packages, start long-running services, commit, push, deploy, or make destructive changes unless the lead explicitly assigned that side effect."
760
+ : "You have the full toolset and may run any read-only shell command you need to investigate — git status/log/diff/show, grep/rg, ls, cat, running tests or builds, etc.",
761
+ role === "write"
762
+ ? "Use read/bash/edit/write as needed for the assignment. Prefer precise edits. Stop and report if you need broader product or architecture approval."
763
+ : "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.",
764
+ "Use send_message for direct communication and read_inbox only when you were told a reply is waiting. Do not coordinate a peer-agent society; the lead controls orchestration.",
765
+ "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.",
766
+ ...(member.requestedBy
767
+ ? [`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.`]
768
+ : nestedReadDelegationEnabled
769
+ ? [
770
+ "This opted-in depth-0 write-feature/write-critical run may use restricted spawn_agent or spawn_swarm_agents for depth-1 read-only helpers. Choose any canonical read-* tier and any helper count, subject to the team's global read capacity and queue. Children report to you and cannot delegate.",
771
+ "Use spawn_agent with only name (optional), prompt, and model_slot. Use spawn_swarm_agents with optional defaults.model_slot plus agents using only name (optional), prompt, and model_slot.",
772
+ "Do not request write tiers, cwd/team overrides, metadata, replacement of an active/queued name, or delegation outside your assigned scope.",
773
+ ]
774
+ : ["You cannot spawn or create other agents. If another agent is needed, use send_message to ask team-lead; only the lead decides and performs the spawn."]),
775
+ "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.",
776
+ "When finished, use report_and_exit with the complete required deliverable in content and only a short label in summary, then stop. Never replace required output with a summary. Do not wait for the lead to kill you — report and exit cleanly.",
777
+ ],
778
+ });
779
+ await loader.reload();
780
+
781
+ const communicationTools = createAgentCommunicationTools({
782
+ isTeammate: true,
783
+ agentName: member.name,
784
+ role,
785
+ getTeamName: () => readTeamName,
786
+ getLifecycleRunId: () => state.runId,
787
+ authorizeWriteMember: async (teamName, agentName) => {
788
+ await requireWriteAgentTeam(teamName, true, agentName);
789
+ },
790
+ onProgress: (status, updatedAt) => {
791
+ options.emitAgentProgress?.(readTeamName, member.name, status, updatedAt);
792
+ state.latestProgress = status;
793
+ state.progressUpdatedAt = updatedAt;
794
+ state.lastActivityAt = updatedAt;
795
+ state.idleNudgeLevel = undefined;
796
+ pushReadAgentEvent(state, status);
797
+ options.renderReadAgentStatus();
798
+ },
799
+ onReportAndExit: async report => {
800
+ const content = nonEmptyReportText(report.content);
801
+ if (!content) throw new Error("Final report content must not be empty.");
802
+ if (submittedFinalReport || finalReportSubmissionInProgress) return { accepted: false };
803
+ finalReportSubmissionInProgress = true;
804
+ try {
805
+ const deliveryClose = await closeRecipient();
806
+ submittedFinalReport = {
807
+ content,
808
+ summary: nonEmptyReportText(report.summary),
809
+ };
810
+ return {
811
+ accepted: true,
812
+ cancelledDeliveries: deliveryClose.cancelledDeliveries,
813
+ deliveryOutcome: deliveryClose.cancelledDeliveries > 0 ? "cancelled" : "none",
814
+ };
815
+ } finally {
816
+ finalReportSubmissionInProgress = false;
817
+ }
818
+ },
819
+ });
820
+ const communicationToolNames = communicationTools.map(tool => tool.name);
821
+ const communicationToolNameSet = new Set(communicationToolNames);
822
+ const delegationToolNameSet = new Set<string>(NESTED_DELEGATION_TOOL_NAMES);
823
+ const extensionToolNames = loader.getExtensions().extensions.flatMap(extension => {
824
+ return Array.from(extension.tools.keys()).filter(name => !communicationToolNameSet.has(name) && !delegationToolNameSet.has(name));
825
+ });
826
+ const nestedReadAgentToolNames = nestedReadAgentTools.map(tool => tool.name);
827
+ const activeToolNames = Array.from(new Set([
828
+ "read",
829
+ "bash",
830
+ "edit",
831
+ "write",
832
+ "grep",
833
+ "find",
834
+ "ls",
835
+ ...extensionToolNames,
836
+ ...communicationToolNames,
837
+ ...nestedReadAgentToolNames,
838
+ ]));
839
+
840
+ const agentSettings = loadSettings({ projectDir: member.cwd });
841
+ privateSessionDirectory = agentSettings.agentSessions.showInResume
842
+ ? undefined
843
+ : preparePrivateAgentSessionDirectory(readTeamName, member.name, state.runId);
844
+ childSessionManager = privateSessionDirectory
845
+ ? SessionManager.create(member.cwd, privateSessionDirectory)
846
+ : SessionManager.create(member.cwd);
847
+ const parentModelRuntime: unknown = Reflect.get(ctx.modelRegistry, "runtime");
848
+ const { session } = await createAgentSession({
849
+ cwd: member.cwd,
850
+ model,
851
+ thinkingLevel: member.thinking as any,
852
+ // Pi 0.82 consumes modelRuntime; reuse the parent's runtime so nested
853
+ // sessions retain custom providers and runtime-scoped credentials.
854
+ modelRuntime: parentModelRuntime,
855
+ modelRegistry: ctx.modelRegistry,
856
+ tools: activeToolNames,
857
+ customTools: [...communicationTools, ...nestedReadAgentTools],
858
+ resourceLoader: loader,
859
+ settingsManager: childSettingsManager,
860
+ sessionManager: childSessionManager,
861
+ } as Parameters<typeof createAgentSession>[0] & { modelRuntime?: unknown });
862
+
863
+ state.session = session;
864
+ installReadAgentSessionLifecycle(session);
865
+ try {
866
+ if (typeof session.bindExtensions === "function") {
867
+ await (session.bindExtensions as (bindings: { mode: "print" }) => Promise<void>)({ mode: "print" });
868
+ }
869
+ } finally {
870
+ // Selected extensions emit session_start while binding. Open the startup gate
871
+ // to teardown only after binding settles so session_shutdown cannot precede start.
872
+ settleSessionCreation(session);
873
+ }
874
+ if (state.stopRequested || !options.isCurrentReadAgentRun(key, state)) {
875
+ if (state.teardownState !== "persistence_failed") await state.teardownPromise;
876
+ return;
877
+ }
878
+ markReadAgentActivity(state, "started", "thinking");
879
+ options.renderReadAgentStatus();
880
+
881
+ session.subscribe((event: any) => {
882
+ handleReadAgentSessionEvent(state, session, event, options.renderReadAgentStatus);
883
+ });
884
+
885
+ let persistedSnapshot: PersistedReadAgentMessages = { messages: [] };
886
+ let completionResolution: ResolvedReadAgentReport | undefined;
887
+ let recoveryAttempted = false;
888
+ const resolveCurrentReport = (): ResolvedReadAgentReport => {
889
+ persistedSnapshot = persistedSessionMessages(
890
+ SessionManager,
891
+ nonEmptyReportText(childSessionManager.getSessionFile?.()),
892
+ );
893
+ return resolveReadAgentReport(submittedFinalReport, session.messages, persistedSnapshot.messages);
894
+ };
895
+ const unavailableReportError = (reason: string): ReadAgentReportUnavailableError => {
896
+ const reference = readAgentRecoveryReference(readTeamName, member.name, state.runId, childSessionManager);
897
+ const message = [
898
+ reason,
899
+ persistedSnapshot.error ? `The durable child session could not be read: ${persistedSnapshot.error}` : undefined,
900
+ `Recovery pointer: ${reference.pointer}`,
901
+ reference.guidance,
902
+ ].filter(Boolean).join("\n");
903
+ return new ReadAgentReportUnavailableError(
904
+ message,
905
+ recoveryAttempted,
906
+ reference.sessionId,
907
+ reference.sessionFile,
908
+ );
909
+ };
910
+
911
+ state.acceptingMessages = true;
912
+ try {
913
+ await session.prompt(prompt, { source: "extension" as any });
914
+ refreshReadAgentStats(state, session);
915
+ if (pendingChildController && pendingChildParent) {
916
+ while (!submittedFinalReport && !state.stopRequested) {
917
+ const snapshot = pendingChildController.observeParent(pendingChildParent);
918
+ if (snapshot.cancelled) break;
919
+
920
+ let change;
921
+ if (snapshot.generation !== pendingChildGeneration) {
922
+ change = { status: "changed" as const, generation: snapshot.generation };
923
+ } else {
924
+ change = await pendingChildController.waitForChangeOrCancelled(
925
+ pendingChildParent,
926
+ snapshot.generation
927
+ );
928
+ }
929
+ pendingChildGeneration = change.generation;
930
+ if (change.status === "cancelled" || submittedFinalReport || state.stopRequested) break;
931
+
932
+ // Every direct delivery and exact child transition advances the parent
933
+ // generation. Drain the current delivery, then observe again because
934
+ // its continuation may accept or settle another child wave.
935
+ const deliveryTail = state.messageDeliveryTail;
936
+ if (deliveryTail) await deliveryTail.catch(() => {});
937
+ }
938
+ } else if (!state.stopRequested && options.isCurrentReadAgentRun(key, state)) {
939
+ completionResolution = resolveCurrentReport();
940
+ if (completionResolution.terminalFailure) {
941
+ throw unavailableReportError(completionResolution.terminalFailure);
942
+ }
943
+ if (!completionResolution.report) {
944
+ recoveryAttempted = true;
945
+ markReadAgentActivity(state, "recovering missing report", "thinking");
946
+ options.renderReadAgentStatus();
947
+ try {
948
+ await session.prompt(EMPTY_REPORT_RECOVERY_PROMPT, { source: "extension" as any });
949
+ refreshReadAgentStats(state, session);
950
+ } catch (error) {
951
+ throw unavailableReportError(
952
+ `The report-only recovery turn failed: ${error instanceof Error ? error.message : String(error)}`,
953
+ );
954
+ }
955
+ completionResolution = resolveCurrentReport();
956
+ if (completionResolution.terminalFailure) {
957
+ throw unavailableReportError(completionResolution.terminalFailure);
958
+ }
959
+ if (!completionResolution.report) {
960
+ throw unavailableReportError(
961
+ completionResolution.recoveryReason
962
+ ?? "The agent completed a report-only recovery turn without producing any usable final report.",
963
+ );
964
+ }
965
+ }
966
+ }
967
+ } finally {
968
+ state.acceptingMessages = false;
969
+ }
970
+ await closeRecipient();
971
+ state.status = "finishing";
972
+ state.activeToolName = undefined;
973
+ refreshReadAgentStats(state, session);
974
+ markReadAgentActivity(state, "sending report", "finishing");
975
+ options.renderReadAgentStatus();
976
+
977
+ if (state.stopRequested || !options.isCurrentReadAgentRun(key, state)) return;
978
+ if (pendingChildParent && !submittedFinalReport) return;
979
+
980
+ completionResolution ??= resolveCurrentReport();
981
+ if (completionResolution.terminalFailure) {
982
+ throw unavailableReportError(completionResolution.terminalFailure);
983
+ }
984
+ if (!completionResolution.report) {
985
+ throw unavailableReportError(
986
+ completionResolution.recoveryReason ?? "The agent completed without producing any usable final report.",
987
+ );
988
+ }
989
+ const completionSummary = completionResolution.summary
990
+ ?? `${role === "write" ? "Edit" : "Read"} agent ${member.name} completed`;
991
+ const recoveryReference = readAgentRecoveryReference(readTeamName, member.name, state.runId, childSessionManager);
992
+ await deliverCompletion(completionResolution, completionSummary, recoveryAttempted, recoveryReference);
993
+ } catch (e) {
994
+ const lastError = runtime.createRuntimeError(e);
995
+ state.lastError = lastError;
996
+ options.renderReadAgentStatus();
997
+ settleSessionCreation(state.session);
998
+ await closeRecipient();
999
+ if (!state.stopRequested && options.isCurrentReadAgentRun(key, state) && !privateCompletedReportPersisted) {
1000
+ let failureReport = `${role === "write" ? "Edit" : "Read"} agent ${member.name} failed: ${e instanceof Error ? e.message : String(e)}`;
1001
+ const failureStats = state.session?.getSessionStats();
1002
+ const reportUnavailable = e instanceof ReadAgentReportUnavailableError ? e : undefined;
1003
+ const failureRecoveryReference = childSessionManager
1004
+ ? readAgentRecoveryReference(readTeamName, member.name, state.runId, childSessionManager)
1005
+ : undefined;
1006
+ const failureReportMetadata = {
1007
+ reportSource: reportUnavailable?.reportSource ?? "runtime-failure" as AgentReportSource,
1008
+ recoveryAttempted: reportUnavailable?.recoveryAttempted ?? false,
1009
+ ...((reportUnavailable?.recoverySessionId ?? failureRecoveryReference?.sessionId)
1010
+ ? { recoverySessionId: reportUnavailable?.recoverySessionId ?? failureRecoveryReference?.sessionId }
1011
+ : {}),
1012
+ ...((reportUnavailable?.recoverySessionFile ?? failureRecoveryReference?.sessionFile)
1013
+ ? { recoverySessionFile: reportUnavailable?.recoverySessionFile ?? failureRecoveryReference?.sessionFile }
1014
+ : {}),
1015
+ };
1016
+ const failureSummary = `${role === "write" ? "Edit" : "Read"} agent ${member.name} failed`;
1017
+ const failureEventPersistence = await recordReadAgentReportEvent(
1018
+ readTeamName,
1019
+ member,
1020
+ "failed",
1021
+ failureReport,
1022
+ failureSummary,
1023
+ state.startedAt,
1024
+ state.tokensUsed,
1025
+ failureStats?.cost,
1026
+ "red",
1027
+ failureReportMetadata,
1028
+ );
1029
+ if (!failureEventPersistence.persisted) {
1030
+ const recoveryReference = failureRecoveryReference
1031
+ ?? readAgentRecoveryReference(readTeamName, member.name, state.runId, childSessionManager);
1032
+ state.finalizationBlockedReason ||= reportPersistenceBlockReason(
1033
+ "Could not durably persist the failed read-agent report",
1034
+ failureEventPersistence.error,
1035
+ recoveryReference,
1036
+ );
1037
+ if (!failureReport.includes("Recovery pointer: pi-child-session/v1")) {
1038
+ failureReport = `${failureReport}\n\n${state.finalizationBlockedReason}`;
1039
+ }
1040
+ }
1041
+ options.rememberCompletedAgentReport(readTeamName, {
1042
+ name: member.name,
1043
+ role,
1044
+ status: "failed",
1045
+ report: failureReport,
1046
+ summary: failureSummary,
1047
+ completedAt: Date.now(),
1048
+ startedAt: state.startedAt,
1049
+ elapsedMs: Date.now() - state.startedAt,
1050
+ tokensUsed: state.tokensUsed,
1051
+ costUsd: failureStats?.cost,
1052
+ model: member.model,
1053
+ thinking: member.thinking,
1054
+ modelSlot,
1055
+ color: "red",
1056
+ requestedBy: member.requestedBy,
1057
+ initialPrompt: member.prompt || prompt,
1058
+ ...failureReportMetadata,
1059
+ source: "read-agent",
1060
+ });
1061
+ const failureMetadata = {
1062
+ finalReport: true,
1063
+ startedAt: state.startedAt,
1064
+ elapsedMs: Date.now() - state.startedAt,
1065
+ tokensUsed: state.tokensUsed,
1066
+ costUsd: failureStats?.cost,
1067
+ model: member.model,
1068
+ thinking: member.thinking,
1069
+ modelSlot,
1070
+ initialPrompt: member.prompt || prompt,
1071
+ ...failureReportMetadata,
1072
+ };
1073
+ const suppressLeadReportInjection = shouldSuppressLeadReportInjection(member);
1074
+ if (member.requestedBy) {
1075
+ await ensureReadHelperCompletionMessages(
1076
+ readTeamName,
1077
+ member,
1078
+ state.startedAt,
1079
+ state.runId,
1080
+ failureReport,
1081
+ "failed",
1082
+ "red",
1083
+ options.deliverMessageToActiveAgent
1084
+ );
1085
+ await options.renderLeadInboxStatus?.().catch(() => {});
1086
+ await options.notifyLeadOfInboxReports?.(readTeamName).catch(() => {});
1087
+ } else if (suppressLeadReportInjection) {
1088
+ if (isPiPromptPlanningMember(member)) {
1089
+ options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed, failureReport, false, true);
1090
+ }
1091
+ // Workflow orchestrators consume full failure reports from TeamReportEvent storage.
1092
+ } else if (!options.isTeammate && (options.getTeamName() === readTeamName || readTeamName.startsWith("prompt-build-"))) {
1093
+ options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed, failureReport, false);
1094
+ } else {
1095
+ await ensureLeadCompletionMessage(readTeamName, member, state.startedAt, failureReport, failureSummary, "red", failureMetadata);
1096
+ }
1097
+ try {
1098
+ await runtime.writeRuntimeStatus(readTeamName, member.name, state.runId, {
1099
+ lastHeartbeatAt: Date.now(),
1100
+ lastError,
1101
+ });
1102
+ } catch {
1103
+ // Ignore runtime cleanup races.
1104
+ }
1105
+ }
1106
+ } finally {
1107
+ pendingParentWakeSignals.delete(state);
1108
+ settleSessionCreation(state.session);
1109
+ // End run-owned activity even when persisted lifecycle cleanup is refused.
1110
+ if (state.heartbeatTimer) clearInterval(state.heartbeatTimer);
1111
+ state.heartbeatTimer = undefined;
1112
+ closeReadAgentMessageDelivery(state);
1113
+ const teardown = await shutdownTeammate(readTeamName, member, { reason: "quit" });
1114
+ if (pendingChildController && pendingChildParent) {
1115
+ pendingChildController.forgetParent(pendingChildParent);
1116
+ }
1117
+ if (!teardown.finalized) options.renderReadAgentStatus();
1118
+ }
1119
+ }