pi-extended-teams 2.2.9 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -2
- package/extensions/agents/read-agent-report.ts +11 -2
- package/extensions/agents/read-agent-session-lifecycle.ts +25 -10
- package/extensions/agents/read-agent.ts +387 -65
- package/extensions/agents/write-agent.ts +3 -0
- package/extensions/events/register-events.ts +22 -10
- package/extensions/index.ts +132 -38
- package/extensions/internal/pi-check-operations.ts +24 -0
- package/extensions/internal/session-files.ts +10 -2
- package/extensions/runtime/teammate-interrupt.ts +11 -5
- package/extensions/runtime/types.ts +4 -0
- package/extensions/team/lifecycle.ts +26 -0
- package/extensions/team/session-cost.ts +155 -0
- package/extensions/tools/agent-communication-tools.ts +24 -8
- package/extensions/tools/agent-status-tool.ts +50 -7
- package/extensions/tools/coordination-tools.ts +78 -8
- package/extensions/tools/task-runtime-tools.ts +23 -4
- package/extensions/tools/team-tools.ts +287 -40
- package/extensions/ui/activity-colors.ts +59 -0
- package/extensions/ui/agent-follow-view.ts +22 -12
- package/extensions/ui/agent-navigation.ts +35 -1
- package/extensions/ui/checkpoints-command.ts +28 -0
- package/extensions/ui/extensions-command.ts +5 -1
- package/extensions/ui/onboarding-command.ts +218 -0
- package/extensions/ui/status-widget.ts +22 -14
- package/package.json +1 -1
- package/skills/teams.md +16 -0
- package/src/orchestration/index.ts +19 -1
- package/src/orchestration/status-projection.ts +2 -3
- package/src/orchestration/types.ts +9 -1
- package/src/results/check-journal.ts +149 -0
- package/src/results/check-policy.ts +86 -0
- package/src/results/check-runner.ts +110 -0
- package/src/results/checkpoint-assignment.ts +60 -0
- package/src/results/checkpoint-policy.ts +20 -0
- package/src/results/checkpoint-report.ts +109 -0
- package/src/results/checkpoint-retention.ts +15 -0
- package/src/results/completion-group-delivery.ts +66 -0
- package/src/results/completion-group-recovery.ts +61 -0
- package/src/results/completion-group-wake.ts +62 -0
- package/src/results/completion-group.ts +290 -0
- package/src/results/continuation-recipient.ts +37 -0
- package/src/results/durable-json.ts +29 -0
- package/src/results/repair-policy.ts +24 -0
- package/src/results/report-result.ts +77 -0
- package/src/results/source-identity.ts +115 -0
- package/src/results/specialist-checkpoint.ts +227 -0
- package/src/results/verification-controller.ts +308 -0
- package/src/utils/messaging.ts +47 -10
- package/src/utils/models.ts +13 -0
- package/src/utils/paths.ts +4 -0
- package/src/utils/report-events.ts +95 -10
- package/src/utils/teams.ts +3 -1
- package/src/utils/write-queue.ts +3 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const COST_ENTRY_TYPE = "pi-extended-teams-cost-v1";
|
|
4
|
+
export type CostOutcome = "completed" | "failed" | "cancelled";
|
|
5
|
+
export interface AgentCostSnapshot { childSessionId: string; costUsd: number | null }
|
|
6
|
+
export interface CostRun {
|
|
7
|
+
settle(snapshot: AgentCostSnapshot | undefined, outcome: CostOutcome): void;
|
|
8
|
+
exclude(): void;
|
|
9
|
+
}
|
|
10
|
+
interface Receipt {
|
|
11
|
+
rootSessionId: string;
|
|
12
|
+
teamName: string;
|
|
13
|
+
lifecycleRunId: string;
|
|
14
|
+
phase: "pending" | "final" | "excluded";
|
|
15
|
+
childSessionId?: string;
|
|
16
|
+
costUsd?: number | null;
|
|
17
|
+
outcome?: CostOutcome;
|
|
18
|
+
}
|
|
19
|
+
const object = (value: unknown): Record<string, unknown> =>
|
|
20
|
+
value !== null && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
21
|
+
const validCost = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
22
|
+
const identity = (receipt: Receipt) => JSON.stringify([receipt.rootSessionId, receipt.teamName, receipt.lifecycleRunId]);
|
|
23
|
+
|
|
24
|
+
export function recordedSessionCost(entries: readonly unknown[]): { usd: number; complete: boolean } {
|
|
25
|
+
let usd = 0;
|
|
26
|
+
let complete = true;
|
|
27
|
+
for (const item of entries) {
|
|
28
|
+
const entry = object(item);
|
|
29
|
+
const message = object(entry.message);
|
|
30
|
+
const summary = entry.type === "compaction" || entry.type === "branch_summary";
|
|
31
|
+
const assistant = entry.type === "message" && message.role === "assistant";
|
|
32
|
+
const tool = entry.type === "message" && message.role === "toolResult";
|
|
33
|
+
if (!summary && !assistant && !tool) continue;
|
|
34
|
+
const usage = summary ? entry.usage : message.usage;
|
|
35
|
+
if (tool && usage === undefined) continue; // Optional native nested-tool accounting.
|
|
36
|
+
const cost = object(object(usage).cost).total;
|
|
37
|
+
if (validCost(cost) && validCost(usd + cost)) usd += cost;
|
|
38
|
+
else complete = false; // Includes legacy summaries without recorded usage.
|
|
39
|
+
}
|
|
40
|
+
return { usd, complete };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createCombinedSessionCost(pi: Pick<ExtensionAPI, "on" | "appendEntry" | "events">) {
|
|
44
|
+
let scope: { root: string; ctx: ExtensionContext; failed: Set<string>; pending: Map<string, Receipt> } | undefined;
|
|
45
|
+
const receipts = (entries: readonly unknown[]) => {
|
|
46
|
+
const runs = new Map<string, Receipt>();
|
|
47
|
+
let complete = true;
|
|
48
|
+
for (const item of entries) {
|
|
49
|
+
const entry = object(item);
|
|
50
|
+
if (entry.type !== "custom" || entry.customType !== COST_ENTRY_TYPE) continue;
|
|
51
|
+
const data = object(entry.data);
|
|
52
|
+
if (![data.rootSessionId, data.teamName, data.lifecycleRunId].every(value => typeof value === "string" && value.length > 0)
|
|
53
|
+
|| !["pending", "final", "excluded"].includes(String(data.phase))
|
|
54
|
+
|| (data.phase === "final" && (!["completed", "failed", "cancelled"].includes(String(data.outcome))
|
|
55
|
+
|| !(data.costUsd === null || (validCost(data.costUsd) && typeof data.childSessionId === "string" && data.childSessionId.length > 0))))) {
|
|
56
|
+
complete = false;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const receipt = data as unknown as Receipt;
|
|
60
|
+
const key = identity(receipt);
|
|
61
|
+
if (!runs.has(key) || runs.get(key)?.phase === "pending") runs.set(key, receipt);
|
|
62
|
+
}
|
|
63
|
+
return { runs, complete };
|
|
64
|
+
};
|
|
65
|
+
const total = () => {
|
|
66
|
+
try {
|
|
67
|
+
if (!scope || scope.ctx.sessionManager.getSessionId() !== scope.root) return { usd: 0, complete: false };
|
|
68
|
+
const entries = scope.ctx.sessionManager.getEntries();
|
|
69
|
+
const own = recordedSessionCost(entries);
|
|
70
|
+
const replay = receipts(entries);
|
|
71
|
+
let complete = own.complete && replay.complete && scope.failed.size === 0;
|
|
72
|
+
let usd = own.usd;
|
|
73
|
+
const sessions = new Set([scope.root]);
|
|
74
|
+
for (const [key, receipt] of new Map([...replay.runs, ...scope.pending])) {
|
|
75
|
+
if (receipt.phase === "excluded") continue;
|
|
76
|
+
if (scope.failed.has(key) || receipt.phase !== "final" || !validCost(receipt.costUsd) || !receipt.childSessionId) {
|
|
77
|
+
complete = false;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (sessions.has(receipt.childSessionId)) continue;
|
|
81
|
+
sessions.add(receipt.childSessionId);
|
|
82
|
+
if (validCost(usd + receipt.costUsd)) usd += receipt.costUsd;
|
|
83
|
+
else complete = false;
|
|
84
|
+
}
|
|
85
|
+
return { usd, complete };
|
|
86
|
+
} catch { return { usd: 0, complete: false }; }
|
|
87
|
+
};
|
|
88
|
+
let displayTotal: ReturnType<typeof recordedSessionCost> | undefined;
|
|
89
|
+
// Synchronous display-only query: native usage and durable receipts stay untouched.
|
|
90
|
+
const unsubscribe = pi.events.on("pi-extended-teams:cost-request", (value) => {
|
|
91
|
+
const request = object(value);
|
|
92
|
+
if (scope && request.sessionId === scope.root && scope.ctx.sessionManager.getSessionId() === scope.root) {
|
|
93
|
+
request.result = displayTotal;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
const refresh = () => {
|
|
97
|
+
displayTotal = scope ? total() : undefined;
|
|
98
|
+
try { pi.events.emit("pi-extended-teams:cost-changed", undefined); }
|
|
99
|
+
catch { /* Display observers must not affect lifecycle cleanup. */ }
|
|
100
|
+
};
|
|
101
|
+
pi.on("session_start", (_event, ctx) => {
|
|
102
|
+
scope = { root: ctx.sessionManager.getSessionId(), ctx, failed: new Set(), pending: new Map() };
|
|
103
|
+
refresh();
|
|
104
|
+
});
|
|
105
|
+
pi.on("turn_end", refresh);
|
|
106
|
+
pi.on("agent_end", refresh);
|
|
107
|
+
pi.on("session_compact", refresh);
|
|
108
|
+
pi.on("session_tree", refresh);
|
|
109
|
+
return {
|
|
110
|
+
total,
|
|
111
|
+
deactivate() {
|
|
112
|
+
scope = undefined;
|
|
113
|
+
unsubscribe();
|
|
114
|
+
refresh();
|
|
115
|
+
},
|
|
116
|
+
begin(rootSessionId: string, teamName: string, lifecycleRunId: string): CostRun {
|
|
117
|
+
const origin = scope;
|
|
118
|
+
if (!origin || origin.root !== rootSessionId) return { settle() {}, exclude() {} };
|
|
119
|
+
const pending: Receipt = { rootSessionId, teamName, lifecycleRunId, phase: "pending" };
|
|
120
|
+
const key = identity(pending);
|
|
121
|
+
const active = () => scope === origin && origin.ctx.sessionManager.getSessionId() === rootSessionId;
|
|
122
|
+
const append = (receipt: Receipt) => {
|
|
123
|
+
try {
|
|
124
|
+
if (!active()) return false;
|
|
125
|
+
pi.appendEntry(COST_ENTRY_TYPE, receipt);
|
|
126
|
+
origin.failed.delete(key);
|
|
127
|
+
origin.pending.delete(key);
|
|
128
|
+
refresh();
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
origin.failed.add(key);
|
|
132
|
+
origin.pending.set(key, pending);
|
|
133
|
+
refresh();
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
try {
|
|
138
|
+
if (!receipts(origin.ctx.sessionManager.getEntries()).runs.has(key)) append(pending);
|
|
139
|
+
} catch { origin.failed.add(key); origin.pending.set(key, pending); refresh(); }
|
|
140
|
+
const finish = (receipt: Receipt) => {
|
|
141
|
+
try {
|
|
142
|
+
if (!active()) return;
|
|
143
|
+
const previous = origin.pending.get(key) ?? receipts(origin.ctx.sessionManager.getEntries()).runs.get(key);
|
|
144
|
+
if (previous?.phase !== "pending") return;
|
|
145
|
+
append(receipt);
|
|
146
|
+
} catch { origin.failed.add(key); refresh(); }
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
settle: (snapshot, outcome) => finish({ ...pending, phase: "final", outcome,
|
|
150
|
+
childSessionId: snapshot?.childSessionId, costUsd: snapshot?.costUsd ?? null }),
|
|
151
|
+
exclude: () => finish({ ...pending, phase: "excluded" }),
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -4,16 +4,26 @@ import * as runtime from "../../src/utils/runtime";
|
|
|
4
4
|
import * as claims from "../../src/utils/claims";
|
|
5
5
|
import { formatInboxMessagesForModel, sanitizePlainTuiLine } from "../ui/renderers";
|
|
6
6
|
import { createFileClaimTools } from "./file-claim-tools";
|
|
7
|
+
import { normalizeReportedTaskDetails, ReportedTaskDetailsSchema, type ReportedTaskDetails, type ReportResult } from "../../src/results/report-result";
|
|
8
|
+
import type { RepairRequest } from "../../src/results/verification-controller";
|
|
7
9
|
|
|
8
|
-
export interface SubmittedAgentReport {
|
|
10
|
+
export interface SubmittedAgentReport extends ReportedTaskDetails {
|
|
9
11
|
content: string;
|
|
10
12
|
summary?: string;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
+
export type AgentReportSubmissionResult = {
|
|
16
|
+
verification?: ReportResult["verification"];
|
|
15
17
|
cancelledDeliveries?: number;
|
|
16
18
|
deliveryOutcome?: "cancelled" | "none";
|
|
19
|
+
} & ({ accepted: boolean; repairRequest?: never } | { accepted: false; repairRequest: RepairRequest });
|
|
20
|
+
|
|
21
|
+
export function formatRepairRequest(request: RepairRequest): string {
|
|
22
|
+
return [
|
|
23
|
+
`Final report not accepted. Repair attempt ${request.attempt} is reserved (${request.id}).`,
|
|
24
|
+
"Use the observed failures below to repair only your assigned scope. Do not add checks or broaden side effects. If you cannot repair safely, report blocked or failed. Submit a new complete report when done; do not exit yet.",
|
|
25
|
+
...request.checks.map(check => `${check.assignment.name}: ${check.state}; exit ${check.exitCode ?? "unknown"}; full log: ${check.logPath}${check.error ? `; ${check.error}` : ""}`),
|
|
26
|
+
].join("\n");
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
export interface AgentCommunicationToolsOptions {
|
|
@@ -24,7 +34,8 @@ export interface AgentCommunicationToolsOptions {
|
|
|
24
34
|
getLifecycleRunId(): string | undefined;
|
|
25
35
|
authorizeWriteMember(teamName: string, agentName: string): Promise<void>;
|
|
26
36
|
onProgress?(status: string, updatedAt: number): void;
|
|
27
|
-
|
|
37
|
+
repairEnabled?: boolean;
|
|
38
|
+
onReportAndExit(report: SubmittedAgentReport, signal?: AbortSignal, submissionId?: string): Promise<AgentReportSubmissionResult>;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
function requireCurrentSession(options: Pick<AgentCommunicationToolsOptions, "getTeamName">): string {
|
|
@@ -139,17 +150,20 @@ export function createAgentCommunicationTools(options: AgentCommunicationToolsOp
|
|
|
139
150
|
const reportAndExitTool = {
|
|
140
151
|
name: "report_and_exit",
|
|
141
152
|
label: "Report and Exit",
|
|
142
|
-
description:
|
|
153
|
+
description: options.repairEnabled
|
|
154
|
+
? "Submit the complete report for verification. If repair is requested, stay in this run and follow the returned feedback; finish only after acceptance."
|
|
155
|
+
: "Submit the complete final report to the lead and finish this nested agent run.",
|
|
143
156
|
parameters: Type.Object({
|
|
144
157
|
content: Type.String({ minLength: 1, description: "Complete non-empty final report to send to the lead; do not replace required output with a summary." }),
|
|
145
158
|
summary: Type.Optional(Type.String({ description: "Short report summary." })),
|
|
159
|
+
...ReportedTaskDetailsSchema.properties,
|
|
146
160
|
}),
|
|
147
|
-
async execute(_toolCallId: string, params: SubmittedAgentReport) {
|
|
161
|
+
async execute(_toolCallId: string, params: SubmittedAgentReport, signal?: AbortSignal) {
|
|
148
162
|
const teamName = requireCurrentSession(options);
|
|
149
163
|
const content = normalizeFinalReportContent(params.content);
|
|
150
164
|
const summary = typeof params.summary === "string" && params.summary.trim() ? params.summary.trim() : undefined;
|
|
151
|
-
const result = await options.onReportAndExit({ content, summary });
|
|
152
|
-
const text = result.accepted
|
|
165
|
+
const result = await options.onReportAndExit({ content, summary, ...normalizeReportedTaskDetails(params) }, signal, _toolCallId);
|
|
166
|
+
const text = result.repairRequest ? formatRepairRequest(result.repairRequest) : result.accepted
|
|
153
167
|
? "Final report accepted. Finish immediately; the outer runner will release claims and stop this nested session."
|
|
154
168
|
: "A final report was already accepted for this run. This duplicate was ignored; finish immediately.";
|
|
155
169
|
return {
|
|
@@ -157,6 +171,8 @@ export function createAgentCommunicationTools(options: AgentCommunicationToolsOp
|
|
|
157
171
|
details: {
|
|
158
172
|
session: teamName,
|
|
159
173
|
accepted: result.accepted,
|
|
174
|
+
...(result.verification ? { verification: result.verification } : {}),
|
|
175
|
+
...(result.repairRequest ? { repairRequest: result.repairRequest } : {}),
|
|
160
176
|
...(result.cancelledDeliveries === undefined ? {} : {
|
|
161
177
|
cancelledDeliveries: result.cancelledDeliveries,
|
|
162
178
|
deliveryOutcome: result.deliveryOutcome,
|
|
@@ -2,9 +2,11 @@ import { Type } from "@sinclair/typebox";
|
|
|
2
2
|
import * as teams from "../../src/utils/teams";
|
|
3
3
|
import * as runtime from "../../src/utils/runtime";
|
|
4
4
|
import * as reportEvents from "../../src/utils/report-events";
|
|
5
|
-
import { readLifecycleTombstone } from "../../src/utils/lifecycle-tombstone";
|
|
6
|
-
import { projectAgentStatus, type ActiveAgentPhase } from "../../src/orchestration/status-projection";
|
|
5
|
+
import { listLifecycleTombstones, readLifecycleTombstone } from "../../src/utils/lifecycle-tombstone";
|
|
6
|
+
import { projectAgentStatus, projectLifecycleFence, type ActiveAgentPhase } from "../../src/orchestration/status-projection";
|
|
7
7
|
import type { Member, TeamReportEvent } from "../../src/utils/models";
|
|
8
|
+
import { createReportResult, effectiveTaskOutcome, type ReportResult } from "../../src/results/report-result";
|
|
9
|
+
import { VerificationController } from "../../src/results/verification-controller";
|
|
8
10
|
import type { RunningReadAgent } from "../runtime/types";
|
|
9
11
|
import { isWriteMemberAlive } from "../team/roster";
|
|
10
12
|
import { formatElapsed } from "../ui/renderers";
|
|
@@ -30,6 +32,7 @@ export interface AgentStatusScope {
|
|
|
30
32
|
|
|
31
33
|
export interface AgentStatusSnapshot {
|
|
32
34
|
name: string;
|
|
35
|
+
runId?: string;
|
|
33
36
|
role: string;
|
|
34
37
|
phase: AgentStatusPhase;
|
|
35
38
|
progress?: string;
|
|
@@ -40,6 +43,13 @@ export interface AgentStatusSnapshot {
|
|
|
40
43
|
queuePosition?: number;
|
|
41
44
|
queuedAgeMs?: number;
|
|
42
45
|
completedAgeMs?: number;
|
|
46
|
+
taskId?: string;
|
|
47
|
+
reportId?: string;
|
|
48
|
+
outcome?: ReportResult["outcome"];
|
|
49
|
+
effectiveOutcome?: ReportResult["outcome"];
|
|
50
|
+
repair?: ReportResult["repair"];
|
|
51
|
+
verification?: ReportResult["verification"]["state"];
|
|
52
|
+
acceptance?: ReportResult["acceptance"]["state"];
|
|
43
53
|
summary?: string;
|
|
44
54
|
error?: string;
|
|
45
55
|
}
|
|
@@ -85,6 +95,7 @@ async function activeStatus(
|
|
|
85
95
|
member: Member,
|
|
86
96
|
options: AgentStatusToolOptions,
|
|
87
97
|
now: number,
|
|
98
|
+
report?: TeamReportEvent,
|
|
88
99
|
): Promise<AgentStatusSnapshot> {
|
|
89
100
|
const candidateState = options.runningReadAgents.get(options.readAgentKey(teamName, member.name));
|
|
90
101
|
const [candidateRuntimeStatus, lifecycleResult] = await Promise.all([
|
|
@@ -102,6 +113,9 @@ async function activeStatus(
|
|
|
102
113
|
const { state, runtime: runtimeStatus } = projected;
|
|
103
114
|
const progress = state?.latestProgress || runtimeStatus?.latestProgress;
|
|
104
115
|
const progressUpdatedAt = state?.progressUpdatedAt || runtimeStatus?.progressUpdatedAt;
|
|
116
|
+
const result = report?.result ?? (member.lifecycleRunId
|
|
117
|
+
? (await VerificationController.observe(teamName, createReportResult(teamName, member.name, member.lifecycleRunId, {}))).result
|
|
118
|
+
: undefined);
|
|
105
119
|
return {
|
|
106
120
|
name: member.name,
|
|
107
121
|
role: member.role || state?.role || "read",
|
|
@@ -111,7 +125,17 @@ async function activeStatus(
|
|
|
111
125
|
activeTool: state?.activeToolName || runtimeStatus?.activeToolName,
|
|
112
126
|
activityAgeMs: age(now, state?.lastActivityAt),
|
|
113
127
|
heartbeatAgeMs: age(now, runtimeStatus?.lastHeartbeatAt),
|
|
114
|
-
|
|
128
|
+
...(result?.repair ? resultStatus(result) : {}),
|
|
129
|
+
error: projected.error ?? result?.verification.error,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resultStatus(result: ReportResult) {
|
|
134
|
+
return {
|
|
135
|
+
taskId: result.taskId, runId: result.runId, reportId: result.reportId,
|
|
136
|
+
outcome: result.outcome, verification: result.verification.state, acceptance: result.acceptance.state,
|
|
137
|
+
error: result.verification.error,
|
|
138
|
+
...(result.repair ? { repair: result.repair, effectiveOutcome: effectiveTaskOutcome(result) } : {}),
|
|
115
139
|
};
|
|
116
140
|
}
|
|
117
141
|
|
|
@@ -131,6 +155,7 @@ function completedStatus(report: TeamReportEvent, now: number): AgentStatusSnaps
|
|
|
131
155
|
name: report.agentName,
|
|
132
156
|
role: report.role || "read",
|
|
133
157
|
phase: report.status,
|
|
158
|
+
...(report.result ? resultStatus(report.result) : {}),
|
|
134
159
|
completedAgeMs: age(now, report.createdAt),
|
|
135
160
|
summary: report.summary,
|
|
136
161
|
};
|
|
@@ -145,6 +170,7 @@ export function formatAgentStatusesForModel(statuses: AgentStatusSnapshot[]): st
|
|
|
145
170
|
|
|
146
171
|
const blocks = statuses.map(status => {
|
|
147
172
|
const lines = [`${status.name}: ${status.phase} (${status.role})`];
|
|
173
|
+
if (status.runId) lines.push(` run: ${status.runId}`);
|
|
148
174
|
if (status.progress) lines.push(` progress: ${status.progress}${status.progressAgeMs === undefined ? "" : ` (${formatElapsed(status.progressAgeMs)} ago)`}`);
|
|
149
175
|
if (status.activeTool) lines.push(` tool: ${status.activeTool}`);
|
|
150
176
|
const activity = formatAge("activity", status.activityAgeMs)
|
|
@@ -155,6 +181,8 @@ export function formatAgentStatusesForModel(statuses: AgentStatusSnapshot[]): st
|
|
|
155
181
|
if (queued) lines.push(` ${queued}`);
|
|
156
182
|
const completed = formatAge(status.phase === "failed" ? "failed" : "completed", status.completedAgeMs);
|
|
157
183
|
if (completed) lines.push(` ${completed}`);
|
|
184
|
+
if (status.reportId) lines.push(` task: ${status.outcome ?? "unspecified"}; verification: ${status.verification}; acceptance: ${status.acceptance}`);
|
|
185
|
+
if (status.repair) lines.push(` repair: ${status.repair.state}; effective task: ${status.effectiveOutcome ?? "unspecified"}; ledger: ${status.repair.journalPath}`);
|
|
158
186
|
if (status.summary) lines.push(` summary: ${status.summary}`);
|
|
159
187
|
if (status.error) lines.push(` error: ${status.error}`);
|
|
160
188
|
return lines.join("\n");
|
|
@@ -188,9 +216,24 @@ export function createAgentStatusTool(options: AgentStatusToolOptions): any {
|
|
|
188
216
|
}).catch(() => []))
|
|
189
217
|
.filter(report => ownsReport(report, options.scope));
|
|
190
218
|
|
|
191
|
-
const activeStatuses = await Promise.all(activeMembers.map(member => activeStatus(teamName, member, options, now
|
|
192
|
-
|
|
193
|
-
|
|
219
|
+
const activeStatuses = await Promise.all(activeMembers.map(member => activeStatus(teamName, member, options, now,
|
|
220
|
+
reports.filter(report => report.agentName === member.name && report.result?.runId === member.lifecycleRunId).at(-1))));
|
|
221
|
+
// Orphan fences lack parent metadata; only the lead can inspect them.
|
|
222
|
+
const fencedStatuses: AgentStatusSnapshot[] = options.scope ? [] : (await listLifecycleTombstones(teamName))
|
|
223
|
+
.filter(({ agentName }) => !rosterNames.has(agentName))
|
|
224
|
+
.map(({ agentName, result }) => {
|
|
225
|
+
const pending = queued.find(item => item.name === agentName);
|
|
226
|
+
return {
|
|
227
|
+
...(pending ? queuedStatus(pending, now) : {}),
|
|
228
|
+
name: agentName,
|
|
229
|
+
runId: result.status === "occupied" ? result.tombstone.runId : undefined,
|
|
230
|
+
role: result.status === "occupied" ? result.tombstone.role : pending?.role || "unknown",
|
|
231
|
+
...projectLifecycleFence({}, result),
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
const fencedNames = new Set(fencedStatuses.map(status => status.name));
|
|
235
|
+
const queuedStatuses = queued.filter(item => !fencedNames.has(item.name)).map(item => queuedStatus(item, now));
|
|
236
|
+
const currentNames = new Set([...activeStatuses, ...fencedStatuses, ...queuedStatuses].map(status => status.name));
|
|
194
237
|
const completedByName = new Map<string, TeamReportEvent>();
|
|
195
238
|
for (const report of reports) {
|
|
196
239
|
if (currentNames.has(report.agentName)) continue;
|
|
@@ -198,7 +241,7 @@ export function createAgentStatusTool(options: AgentStatusToolOptions): any {
|
|
|
198
241
|
completedByName.set(report.agentName, report);
|
|
199
242
|
}
|
|
200
243
|
const completedStatuses = Array.from(completedByName.values()).slice(-20).map(report => completedStatus(report, now));
|
|
201
|
-
const statuses = [...activeStatuses, ...queuedStatuses, ...completedStatuses];
|
|
244
|
+
const statuses = [...activeStatuses, ...fencedStatuses, ...queuedStatuses, ...completedStatuses];
|
|
202
245
|
const selected = params.agent_name
|
|
203
246
|
? statuses.filter(status => status.name === params.agent_name)
|
|
204
247
|
: statuses;
|
|
@@ -7,7 +7,15 @@ import * as messaging from "../../src/utils/messaging";
|
|
|
7
7
|
import * as runtime from "../../src/utils/runtime";
|
|
8
8
|
import * as claims from "../../src/utils/claims";
|
|
9
9
|
import * as reportEvents from "../../src/utils/report-events";
|
|
10
|
+
import { checkpointReference, saveReportCheckpoint, resyncReportCheckpoint } from "../../src/results/checkpoint-report";
|
|
11
|
+
import { createReportResult, effectiveTaskOutcome, normalizeReportedTaskDetails, ReportedTaskDetailsSchema, type ReportResult } from "../../src/results/report-result";
|
|
10
12
|
import { canonicalPersistedModelSlot } from "../../src/utils/settings";
|
|
13
|
+
import { normalizeCheckPolicy } from "../../src/results/check-policy";
|
|
14
|
+
import { VerificationController } from "../../src/results/verification-controller";
|
|
15
|
+
import { deliverCompletionGroupReport } from "../../src/results/completion-group-delivery";
|
|
16
|
+
import { formatRepairRequest } from "./agent-communication-tools";
|
|
17
|
+
import type { CheckRunnerOptions } from "../../src/results/check-runner";
|
|
18
|
+
import { loadNativeCheckOperations } from "../internal/pi-check-operations";
|
|
11
19
|
import { createFileClaimTools } from "./file-claim-tools";
|
|
12
20
|
import { formatInboxMessagesForModel, renderInboxMessages } from "../ui/renderers";
|
|
13
21
|
import { unlinkPidFile } from "../internal/session-files";
|
|
@@ -33,6 +41,7 @@ export interface CoordinationToolsOptions {
|
|
|
33
41
|
resetLeadWakeNotifiedCount(): void;
|
|
34
42
|
deliverMessageToActiveAgent?(teamName: string, recipient: string, content: string): Promise<boolean>;
|
|
35
43
|
extensionInstanceId?: string;
|
|
44
|
+
loadCheckOperations?: CheckRunnerOptions["loadOperations"];
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
export function buildReadHelperPrompt(teamName: string, requester: string, prompt: string): string {
|
|
@@ -53,9 +62,26 @@ function requireCurrentSession(options: CoordinationToolsOptions): string {
|
|
|
53
62
|
|
|
54
63
|
export function registerCoordinationTools(pi: any, options: CoordinationToolsOptions): void {
|
|
55
64
|
const extensionInstanceId = options.extensionInstanceId ?? generateExtensionInstanceId();
|
|
56
|
-
const pendingWriterFinalization = new Map<string, { teamName: string; agentName: string; runId: string }>();
|
|
65
|
+
const pendingWriterFinalization = new Map<string, { teamName: string; agentName: string; runId: string; checkpointId?: string }>();
|
|
66
|
+
const verifyingReports = new Set<string>();
|
|
67
|
+
const blockedReports = new Set<string>();
|
|
68
|
+
const pendingRepairs = new Map<string, { controller: VerificationController; result: ReportResult }>();
|
|
69
|
+
let repairCancellation: Promise<void> | undefined;
|
|
70
|
+
const cancelPendingRepairs = () => repairCancellation ??= (async () => {
|
|
71
|
+
for (const [reportId, { controller }] of pendingRepairs) {
|
|
72
|
+
try {
|
|
73
|
+
await controller.cancel();
|
|
74
|
+
pendingRepairs.delete(reportId);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
blockedReports.add(reportId);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
})().finally(() => { repairCancellation = undefined; });
|
|
57
81
|
|
|
82
|
+
pi.on?.("agent_settled", cancelPendingRepairs);
|
|
58
83
|
pi.on?.("session_shutdown", async () => {
|
|
84
|
+
await cancelPendingRepairs();
|
|
59
85
|
const pending = Array.from(pendingWriterFinalization.values());
|
|
60
86
|
for (const item of pending) {
|
|
61
87
|
let cleared = false;
|
|
@@ -77,6 +103,11 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
77
103
|
throw new Error(`Refusing to remove replacement runtime run ${currentRuntime.lifecycleRunId || "unknown"} of ${item.agentName}.`);
|
|
78
104
|
}
|
|
79
105
|
|
|
106
|
+
if (item.checkpointId) {
|
|
107
|
+
const report = await reportEvents.readStoredTeamReportEvent(item.teamName, createReportResult(item.teamName, item.agentName, item.runId, {}).reportId);
|
|
108
|
+
if (report?.checkpoint?.id !== item.checkpointId) throw new Error("Checkpoint report provenance is unavailable.");
|
|
109
|
+
await resyncReportCheckpoint(report);
|
|
110
|
+
}
|
|
80
111
|
const pidFile = path.join(paths.teamDir(item.teamName), `${item.agentName}.pid`);
|
|
81
112
|
const pidFileExisted = fs.existsSync(pidFile);
|
|
82
113
|
const pidFileUnlinked = unlinkPidFile(pidFile);
|
|
@@ -143,10 +174,12 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
143
174
|
parameters: Type.Object({
|
|
144
175
|
content: Type.String({ description: "Final report to send to the lead." }),
|
|
145
176
|
summary: Type.Optional(Type.String({ description: "Short report summary." })),
|
|
177
|
+
...ReportedTaskDetailsSchema.properties,
|
|
146
178
|
}),
|
|
147
179
|
async execute(_toolCallId: string, params: any, _signal: AbortSignal, _onUpdate: any, ctx: any) {
|
|
148
180
|
const targetTeamName = requireCurrentSession(options);
|
|
149
181
|
if (!options.isTeammate) throw new Error("report_and_exit is only available to spawned agents.");
|
|
182
|
+
const reported = normalizeReportedTaskDetails(params);
|
|
150
183
|
if (process.env.PI_EXTENDED_TEAMS_HERDR_RESUME === "1" && !params.content.trim()) throw new Error("Final report content must not be empty.");
|
|
151
184
|
|
|
152
185
|
const config = await teams.readConfig(targetTeamName);
|
|
@@ -158,6 +191,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
158
191
|
throw new Error(`Refusing stale report run ${processRunId} for ${options.agentName}; current roster run is ${runId}.`);
|
|
159
192
|
}
|
|
160
193
|
member.lifecycleRunId = runId;
|
|
194
|
+
const result = createReportResult(targetTeamName, options.agentName, runId, reported);
|
|
161
195
|
let runtimeStatus = await runtime.readRuntimeStatus(targetTeamName, options.agentName).catch(() => null);
|
|
162
196
|
if (runtimeStatus?.lifecycleRunId && runtimeStatus.lifecycleRunId !== runId) {
|
|
163
197
|
throw new Error(`Refusing to report from stale run ${runId}; runtime status belongs to ${runtimeStatus.lifecycleRunId}.`);
|
|
@@ -165,17 +199,40 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
165
199
|
if (runtimeStatus && !runtimeStatus.lifecycleRunId) {
|
|
166
200
|
runtimeStatus = await runtime.writeRuntimeStatus(targetTeamName, options.agentName, runId, {});
|
|
167
201
|
}
|
|
202
|
+
const checks = normalizeCheckPolicy(member.assignedChecks);
|
|
203
|
+
if (repairCancellation) await repairCancellation;
|
|
204
|
+
if (blockedReports.has(result.reportId)) throw new Error("Final report verification is blocked by uncertain persistence for this run.");
|
|
205
|
+
if (verifyingReports.has(result.reportId)) throw new Error("Final report verification is already in progress for this run.");
|
|
206
|
+
const previous = pendingRepairs.get(result.reportId)?.result;
|
|
207
|
+
if (previous) Object.assign(result, { verification: previous.verification, repair: previous.repair });
|
|
208
|
+
const controller = new VerificationController({ teamName: targetTeamName, result, cwd: member.cwd, checks, repair: member.repairPolicy });
|
|
209
|
+
verifyingReports.add(result.reportId);
|
|
210
|
+
try {
|
|
211
|
+
const decision = await controller.verify(`tool:${_toolCallId}`, {
|
|
212
|
+
loadOperations: options.loadCheckOperations ?? loadNativeCheckOperations, signal: _signal,
|
|
213
|
+
}).catch(error => { blockedReports.add(result.reportId); throw error; });
|
|
214
|
+
Object.assign(result, decision.result);
|
|
215
|
+
if (decision.request) pendingRepairs.set(result.reportId, { controller, result: structuredClone(result) });
|
|
216
|
+
else pendingRepairs.delete(result.reportId);
|
|
217
|
+
if (checks?.length && _signal?.aborted) {
|
|
218
|
+
await controller.cancel().catch(error => { blockedReports.add(result.reportId); throw error; });
|
|
219
|
+
pendingRepairs.delete(result.reportId);
|
|
220
|
+
throw new Error("Assigned check verification was cancelled.");
|
|
221
|
+
}
|
|
222
|
+
if (result.verification.state === "pending") throw new Error("Assigned checks have an unresolved execution claim; inspect the durable journal before finalizing this run.");
|
|
223
|
+
if (decision.request) return {
|
|
224
|
+
content: [{ type: "text", text: formatRepairRequest(decision.request) }],
|
|
225
|
+
details: { accepted: false, session: targetTeamName, repairRequest: decision.request, result },
|
|
226
|
+
};
|
|
227
|
+
} finally {
|
|
228
|
+
verifyingReports.delete(result.reportId);
|
|
229
|
+
}
|
|
168
230
|
await closePersistedRecipient(targetTeamName, options.agentName, runId, {
|
|
169
231
|
removeOnFailure: true,
|
|
170
232
|
role: (member.role ?? "write") === "read" ? "read" : "write",
|
|
171
233
|
reason: "quit",
|
|
172
234
|
extensionInstanceId,
|
|
173
235
|
});
|
|
174
|
-
pendingWriterFinalization.set(`${targetTeamName}:${options.agentName}`, {
|
|
175
|
-
teamName: targetTeamName,
|
|
176
|
-
agentName: options.agentName,
|
|
177
|
-
runId,
|
|
178
|
-
});
|
|
179
236
|
const sessionUsage = summarizeSessionUsage(ctx);
|
|
180
237
|
const tokensUsed = typeof sessionUsage.tokensUsed === "number" ? sessionUsage.tokensUsed : runtimeStatus?.tokensUsed;
|
|
181
238
|
const costUsd = sessionUsage.costUsd;
|
|
@@ -198,8 +255,11 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
198
255
|
try {
|
|
199
256
|
const persistedReport = await reportEvents.appendTeamReportEvent(targetTeamName, {
|
|
200
257
|
agentName: options.agentName,
|
|
258
|
+
checkpoint: checkpointReference(targetTeamName, member),
|
|
259
|
+
completionGroup: member.completionGroup,
|
|
201
260
|
role: member?.role || "write",
|
|
202
261
|
status: "completed",
|
|
262
|
+
result,
|
|
203
263
|
report: params.content,
|
|
204
264
|
summary: params.summary || "Final report",
|
|
205
265
|
startedAt: runtimeStatus?.startedAt,
|
|
@@ -215,8 +275,18 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
215
275
|
});
|
|
216
276
|
reportPath = persistedReport.reportPath || "";
|
|
217
277
|
if (!reportPath) throw new Error("The persisted report did not provide its standalone file path.");
|
|
218
|
-
await
|
|
278
|
+
await saveReportCheckpoint(targetTeamName, member, persistedReport, _signal);
|
|
279
|
+
if (persistedReport.checkpoint) result.checkpointId = persistedReport.checkpoint.id;
|
|
280
|
+
const leadReport = (checks?.length
|
|
281
|
+
? `${params.content}\n\nHarness verification: ${result.verification.state}; lead acceptance: ${result.acceptance.state}${result.repair ? `; repair: ${result.repair.state}; effective task: ${effectiveTaskOutcome(result) ?? "unspecified"}` : ""}. Evidence: ${result.reportId}.`
|
|
282
|
+
: params.content) + (result.checkpointId ? `\n\nCheckpoint reference: ${result.checkpointId}.` : "");
|
|
283
|
+
const grouped = await deliverCompletionGroupReport(persistedReport);
|
|
284
|
+
if (member.completionGroup && !grouped) throw new Error("Grouped report provenance is unavailable.");
|
|
285
|
+
if (!grouped) {
|
|
286
|
+
await messaging.sendPlainMessage(targetTeamName, options.agentName, "team-lead", leadReport, params.summary || "Final report", undefined, { metadata: reportMetadata });
|
|
287
|
+
}
|
|
219
288
|
releasedClaims = await options.releaseAllClaimsForAgent(targetTeamName, options.agentName);
|
|
289
|
+
pendingWriterFinalization.set(`${targetTeamName}:${options.agentName}`, { teamName: targetTeamName, agentName: options.agentName, runId, checkpointId: result.checkpointId });
|
|
220
290
|
} catch (error) {
|
|
221
291
|
pendingWriterFinalization.delete(`${targetTeamName}:${options.agentName}`);
|
|
222
292
|
await withLifecycleTombstoneLock(targetTeamName, options.agentName, async lifecycleLock => {
|
|
@@ -232,7 +302,7 @@ export function registerCoordinationTools(pi: any, options: CoordinationToolsOpt
|
|
|
232
302
|
try { ctx.shutdown(); } catch { process.exit(0); }
|
|
233
303
|
}, 250);
|
|
234
304
|
|
|
235
|
-
return { content: [{ type: "text", text: `Final report sent. Released ${releasedClaims.length} file claim(s). Exiting.` }], details: { session: targetTeamName, releasedClaims, reportPath } };
|
|
305
|
+
return { content: [{ type: "text", text: `Final report ${member.completionGroup ? "saved to its completion group" : "sent"}. Released ${releasedClaims.length} file claim(s). Exiting.` }], details: { accepted: true, session: targetTeamName, releasedClaims, reportPath, result } };
|
|
236
306
|
},
|
|
237
307
|
});
|
|
238
308
|
|
|
@@ -11,6 +11,7 @@ import type { ReadAgentTeardownResult } from "../agents/read-agent-session-lifec
|
|
|
11
11
|
import type { ShutdownTeammateOptions } from "../team/lifecycle";
|
|
12
12
|
import { readLifecycleTombstone } from "../../src/utils/lifecycle-tombstone";
|
|
13
13
|
import type { TeammateInterruptResult } from "../runtime/teammate-interrupt";
|
|
14
|
+
import { effectiveTaskOutcome } from "../../src/results/report-result";
|
|
14
15
|
|
|
15
16
|
export interface TaskRuntimeToolsOptions {
|
|
16
17
|
isTeammate: boolean;
|
|
@@ -18,7 +19,7 @@ export interface TaskRuntimeToolsOptions {
|
|
|
18
19
|
runningReadAgents: Map<string, RunningReadAgent>;
|
|
19
20
|
readAgentKey(teamName: string, agentName: string): string;
|
|
20
21
|
interruptTeammate?(agentName: string): Promise<TeammateInterruptResult>;
|
|
21
|
-
cancelQueuedAgent?(teamName: string, agentName: string): boolean
|
|
22
|
+
cancelQueuedAgent?(teamName: string, agentName: string): boolean | Promise<boolean>;
|
|
22
23
|
shutdownTeammate(teamName: string, member: Member, options?: ShutdownTeammateOptions): Promise<ReadAgentTeardownResult>;
|
|
23
24
|
getTeamName(): string | null | undefined;
|
|
24
25
|
}
|
|
@@ -56,7 +57,7 @@ export function registerTaskRuntimeTools(pi: any, options: TaskRuntimeToolsOptio
|
|
|
56
57
|
const teamName = options.getTeamName();
|
|
57
58
|
if (!teamName) throw new Error("No active agent session. Spawn an agent first.");
|
|
58
59
|
|
|
59
|
-
if (options.cancelQueuedAgent?.(teamName, params.agent_name)) {
|
|
60
|
+
if (await options.cancelQueuedAgent?.(teamName, params.agent_name)) {
|
|
60
61
|
return {
|
|
61
62
|
content: [{ type: "text", text: `Cancelled queued agent ${params.agent_name}.` }],
|
|
62
63
|
details: { session: teamName, agentName: params.agent_name, stopped: true, queued: true, reason: params.reason },
|
|
@@ -147,6 +148,10 @@ export function registerTaskRuntimeTools(pi: any, options: TaskRuntimeToolsOptio
|
|
|
147
148
|
if (!member) {
|
|
148
149
|
const fence = await readLifecycleTombstone(teamName, params.agent_name);
|
|
149
150
|
if (fence.status !== "absent") {
|
|
151
|
+
const completedReport = !options.isTeammate && fence.status === "occupied"
|
|
152
|
+
? (await reportEvents.listTeamReportEvents(teamName, { agentName: params.agent_name }))
|
|
153
|
+
.filter(report => report.result?.runId === fence.tombstone.runId && report.result.repair).at(-1)
|
|
154
|
+
: undefined;
|
|
150
155
|
const details = {
|
|
151
156
|
agentName: params.agent_name,
|
|
152
157
|
alive: false,
|
|
@@ -157,12 +162,17 @@ export function registerTaskRuntimeTools(pi: any, options: TaskRuntimeToolsOptio
|
|
|
157
162
|
startupStalled: false,
|
|
158
163
|
runtime: await runtime.readRuntimeStatus(teamName, params.agent_name).catch(() => null),
|
|
159
164
|
teardownState: "quarantined",
|
|
165
|
+
...(completedReport ? { completedReport } : {}),
|
|
160
166
|
removedMember: false,
|
|
161
167
|
releasedClaims: [],
|
|
162
168
|
tombstone: fence.status === "occupied" ? fence.tombstone : undefined,
|
|
163
169
|
error: fence.status === "corrupt" ? fence.error : undefined,
|
|
164
170
|
};
|
|
165
|
-
|
|
171
|
+
const text = [formatTeammateStatusForModel(params.agent_name, details),
|
|
172
|
+
completedReport ? `Full report: ${completedReport.reportPath}; repair ledger: ${completedReport.result!.repair!.journalPath}` : undefined,
|
|
173
|
+
...(completedReport?.checks?.map(check => `Check ${check.checkId}: ${check.state}; full log: ${check.logPath}`) ?? []),
|
|
174
|
+
].filter(Boolean).join("\n\n");
|
|
175
|
+
return { content: [{ type: "text", text }], details };
|
|
166
176
|
}
|
|
167
177
|
if (options.isTeammate) throw new Error(`Agent ${params.agent_name} not found`);
|
|
168
178
|
const latestReport = (await reportEvents.listTeamReportEvents(teamName, {
|
|
@@ -177,6 +187,14 @@ export function registerTaskRuntimeTools(pi: any, options: TaskRuntimeToolsOptio
|
|
|
177
187
|
: undefined;
|
|
178
188
|
const text = [
|
|
179
189
|
`Agent ${params.agent_name} is no longer active. Recovered its latest persisted ${latestReport.status} report from ${reportPath}.`,
|
|
190
|
+
latestReport.result
|
|
191
|
+
? `Task: ${latestReport.result.outcome ?? "unspecified"}; verification: ${latestReport.result.verification.state}; acceptance: ${latestReport.result.acceptance.state}. Report ID: ${latestReport.result.reportId}.`
|
|
192
|
+
: undefined,
|
|
193
|
+
latestReport.result?.repair
|
|
194
|
+
? `Repair: ${latestReport.result.repair.state}; effective task: ${effectiveTaskOutcome(latestReport.result) ?? "unspecified"}; ledger: ${latestReport.result.repair.journalPath}.`
|
|
195
|
+
: undefined,
|
|
196
|
+
latestReport.checks?.map(check => `Check ${check.checkId}: ${check.state}; exit ${check.exitCode ?? "unknown"}; full log: ${check.logPath}`).join("\n"),
|
|
197
|
+
latestReport.result?.verification.error,
|
|
180
198
|
recoverySessionFile
|
|
181
199
|
? `Its durable child transcript is available at ${recoverySessionFile}. Use the read tool on that path if the report is incomplete.`
|
|
182
200
|
: undefined,
|
|
@@ -246,7 +264,8 @@ export function registerTaskRuntimeTools(pi: any, options: TaskRuntimeToolsOptio
|
|
|
246
264
|
removedMember: teardown?.removedMember ?? false,
|
|
247
265
|
error: teardown?.error ?? lifecycleError,
|
|
248
266
|
};
|
|
249
|
-
|
|
267
|
+
const text = [formatTeammateStatusForModel(params.agent_name, details), details.error].filter(Boolean).join("\n\n");
|
|
268
|
+
return { content: [{ type: "text", text }], details };
|
|
250
269
|
},
|
|
251
270
|
renderResult(result: any, { expanded }: any, theme: any) {
|
|
252
271
|
return renderTeammateStatus(result, expanded, theme);
|