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
|
@@ -8,10 +8,18 @@ import * as runtime from "../../src/utils/runtime";
|
|
|
8
8
|
import * as teams from "../../src/utils/teams";
|
|
9
9
|
import * as messaging from "../../src/utils/messaging";
|
|
10
10
|
import * as reportEvents from "../../src/utils/report-events";
|
|
11
|
-
import
|
|
11
|
+
import { checkpointReference, preflightReportCheckpoint, saveReportCheckpoint } from "../../src/results/checkpoint-report";
|
|
12
|
+
import type { Member, TeamReportEvent } from "../../src/utils/models";
|
|
13
|
+
import { deliverCompletionGroupReport } from "../../src/results/completion-group-delivery";
|
|
14
|
+
import { createReportResult, effectiveTaskOutcome, normalizeReportedTaskDetails, type ReportResult, type ReportedTaskDetails } from "../../src/results/report-result";
|
|
15
|
+
import { assignedCheckIds, normalizeCheckPolicy, type CheckDefinition } from "../../src/results/check-policy";
|
|
16
|
+
import { normalizeRepairPolicy, type RepairPolicy } from "../../src/results/repair-policy";
|
|
17
|
+
import { VerificationController, type VerificationDecision } from "../../src/results/verification-controller";
|
|
18
|
+
import type { CheckRunnerOptions } from "../../src/results/check-runner";
|
|
19
|
+
import { loadNativeCheckOperations } from "../internal/pi-check-operations";
|
|
12
20
|
import type { AgentReportSource, CompletedAgentReport, RunningReadAgent } from "../runtime/types";
|
|
13
21
|
import { extractTextParts, sanitizeTuiLine } from "../ui/renderers";
|
|
14
|
-
import { createAgentCommunicationTools, type SubmittedAgentReport } from "../tools/agent-communication-tools";
|
|
22
|
+
import { createAgentCommunicationTools, formatRepairRequest, type SubmittedAgentReport } from "../tools/agent-communication-tools";
|
|
15
23
|
import { requireWriteAgentTeam } from "../team/roster";
|
|
16
24
|
import { isPiPromptPlanningMember, shouldSuppressLeadReportInjection } from "../../src/utils/workflow-metadata";
|
|
17
25
|
import { canonicalPersistedModelSlot, loadSettings, requireFavoriteModelLevel } from "../../src/utils/settings";
|
|
@@ -19,6 +27,7 @@ import { parseQualifiedModel } from "../../src/utils/model-resolution";
|
|
|
19
27
|
import { closePersistedRecipient } from "../team/recipient-closure";
|
|
20
28
|
import { generateExtensionInstanceId, generateLifecycleRunId, withLifecycleTombstoneLock } from "../../src/utils/lifecycle-tombstone";
|
|
21
29
|
import { createLifecycleRuntime, type ShutdownTeammateOptions } from "../team/lifecycle";
|
|
30
|
+
import { recordedSessionCost, type AgentCostSnapshot, type CostOutcome, type CostRun } from "../team/session-cost";
|
|
22
31
|
import {
|
|
23
32
|
createSpawnResourcePlan,
|
|
24
33
|
parentProjectTrustForSpawn,
|
|
@@ -27,12 +36,13 @@ import {
|
|
|
27
36
|
import { loadPiRuntimeApi } from "../internal/pi-runtime-api";
|
|
28
37
|
import { preparePrivateAgentSessionDirectory } from "../internal/agent-session-files";
|
|
29
38
|
import { isEligibleNestedReadParent, NESTED_DELEGATION_TOOL_NAMES } from "../runtime/nested-read-agents";
|
|
30
|
-
import type
|
|
39
|
+
import { CHILD_AGENT_LIFECYCLE_PROBE, type NestedReadAgentToolBinding } from "../tools/team-tools";
|
|
31
40
|
import type { ParentRunIdentity, PendingChildController } from "../runtime/pending-child-controller";
|
|
32
41
|
import {
|
|
33
42
|
closeReadAgentMessageDelivery,
|
|
34
43
|
enqueueReadAgentMessageDelivery,
|
|
35
44
|
installReadAgentSessionLifecycle,
|
|
45
|
+
ReadAgentDeliveryCancelledError,
|
|
36
46
|
type ReadAgentDeliveryCloseResult,
|
|
37
47
|
type ReadAgentTeardownResult,
|
|
38
48
|
} from "./read-agent-session-lifecycle";
|
|
@@ -50,6 +60,7 @@ import {
|
|
|
50
60
|
export { closeReadAgentMessageDelivery } from "./read-agent-session-lifecycle";
|
|
51
61
|
|
|
52
62
|
const pendingParentWakeSignals = new WeakMap<RunningReadAgent, () => void>();
|
|
63
|
+
const repairCancellationHandlers = new WeakMap<RunningReadAgent, () => Promise<void>>();
|
|
53
64
|
|
|
54
65
|
interface ReadAgentWakeState {
|
|
55
66
|
generation: number;
|
|
@@ -114,12 +125,16 @@ export interface RunReadAgentOptions {
|
|
|
114
125
|
teamName: string,
|
|
115
126
|
recipient: string,
|
|
116
127
|
content: string,
|
|
117
|
-
expectedRecipientRunId?: string
|
|
128
|
+
expectedRecipientRunId?: string,
|
|
129
|
+
requireReceipt?: boolean
|
|
118
130
|
): Promise<boolean>;
|
|
119
131
|
createResourcePlan?(input: { cwd: string; projectTrusted: boolean }): SpawnResourcePlan | Promise<SpawnResourcePlan>;
|
|
120
132
|
extensionInstanceId?: string;
|
|
121
133
|
createNestedReadAgentTools?(binding: NestedReadAgentToolBinding): any[];
|
|
134
|
+
nestedChildSnapshot?(binding: NestedReadAgentToolBinding): { running: number; queued: number };
|
|
122
135
|
pendingChildController?: PendingChildController;
|
|
136
|
+
loadCheckOperations?: CheckRunnerOptions["loadOperations"];
|
|
137
|
+
beginCostRun?(rootSessionId: string, teamName: string, lifecycleRunId: string): CostRun;
|
|
123
138
|
}
|
|
124
139
|
|
|
125
140
|
function pushReadAgentEvent(agent: RunningReadAgent, text: string): void {
|
|
@@ -165,8 +180,9 @@ async function runReadAgentSessionOperation(
|
|
|
165
180
|
failure = error;
|
|
166
181
|
}
|
|
167
182
|
|
|
168
|
-
if (state.activeOperationGeneration === generation) state.activeOperationGeneration = undefined;
|
|
169
183
|
const interrupted = state.interruptRequestedGeneration === generation;
|
|
184
|
+
if (interrupted || failure || state.stopRequested) await repairCancellationHandlers.get(state)?.();
|
|
185
|
+
if (state.activeOperationGeneration === generation) state.activeOperationGeneration = undefined;
|
|
170
186
|
if (interrupted) state.interruptRequestedGeneration = undefined;
|
|
171
187
|
state.completedOperationGeneration = generation;
|
|
172
188
|
state.completedOperationInterrupted = interrupted;
|
|
@@ -349,30 +365,68 @@ export function handleReadAgentSessionEvent(
|
|
|
349
365
|
}
|
|
350
366
|
}
|
|
351
367
|
|
|
352
|
-
export async function sendMessageToRunningReadAgent(
|
|
368
|
+
export async function sendMessageToRunningReadAgent(
|
|
369
|
+
agent: RunningReadAgent | undefined,
|
|
370
|
+
content: string,
|
|
371
|
+
requireReceipt = false
|
|
372
|
+
): Promise<boolean> {
|
|
353
373
|
if (!agent) return false;
|
|
354
374
|
if (!agent.session || !agent.acceptingMessages || agent.messageDeliveryClosed || agent.stopRequested) {
|
|
355
375
|
throw new Error(`Cannot send message to ${agent.name}: agent is finishing.`);
|
|
356
376
|
}
|
|
357
377
|
|
|
358
378
|
const session = agent.session;
|
|
379
|
+
let received = false;
|
|
380
|
+
let acknowledge!: () => void;
|
|
381
|
+
const receipt = new Promise<void>(resolve => { acknowledge = resolve; });
|
|
382
|
+
let unsubscribe: (() => void) | undefined;
|
|
359
383
|
const deliveryResult = enqueueReadAgentMessageDelivery(
|
|
360
384
|
agent,
|
|
361
385
|
agent.name,
|
|
362
386
|
async () => {
|
|
387
|
+
const check = agent.checkOperation;
|
|
388
|
+
if (check || !session.isStreaming) await (agent.activeOperationSettlementPromise ?? check?.settled);
|
|
363
389
|
const pendingInterrupt = agent.operationInterruptPromise;
|
|
364
390
|
if (pendingInterrupt) await pendingInterrupt.catch(() => {});
|
|
391
|
+
if (agent.messageDeliveryClosed || agent.stopRequested) throw new ReadAgentDeliveryCancelledError(agent.name);
|
|
392
|
+
if (requireReceipt) {
|
|
393
|
+
unsubscribe = session.subscribe(event => {
|
|
394
|
+
if (event.type !== "message_end" || event.message.role !== "user") return;
|
|
395
|
+
const body = event.message.content;
|
|
396
|
+
const text = typeof body === "string"
|
|
397
|
+
? body
|
|
398
|
+
: body.filter(part => part.type === "text").map(part => part.text).join("\n");
|
|
399
|
+
if (text === content) {
|
|
400
|
+
received = true;
|
|
401
|
+
acknowledge();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
365
405
|
if (session.isStreaming) {
|
|
366
406
|
await session.sendUserMessage(content, { deliverAs: "steer" as const });
|
|
367
|
-
|
|
407
|
+
} else {
|
|
408
|
+
await runReadAgentSessionOperation(agent, () => session.sendUserMessage(content, undefined));
|
|
409
|
+
if (requireReceipt && !received) throw new Error(`Requester ${agent.name} finished without receiving the report.`);
|
|
368
410
|
}
|
|
369
|
-
await runReadAgentSessionOperation(agent, () => session.sendUserMessage(content, undefined));
|
|
370
411
|
}
|
|
371
412
|
);
|
|
372
413
|
signalReadAgentWake(agent);
|
|
373
414
|
pendingParentWakeSignals.get(agent)?.();
|
|
374
|
-
|
|
375
|
-
|
|
415
|
+
try {
|
|
416
|
+
await deliveryResult;
|
|
417
|
+
if (requireReceipt && !received) {
|
|
418
|
+
// Keep the receipt wait outside the raw tail so parent finalization can cancel an unconsumed steer.
|
|
419
|
+
await Promise.race([receipt, agent.messageDeliveryCancellation!.then(() => {
|
|
420
|
+
throw new ReadAgentDeliveryCancelledError(agent.name);
|
|
421
|
+
})]);
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
// Reporting closes admission while the idle SDK send still awaits the model turn.
|
|
425
|
+
if (!(received && error instanceof ReadAgentDeliveryCancelledError)) throw error;
|
|
426
|
+
} finally {
|
|
427
|
+
unsubscribe?.();
|
|
428
|
+
}
|
|
429
|
+
if (!agent.checkOperation && !agent.messageDeliveryClosed) markReadAgentActivity(agent, "received lead message", "thinking");
|
|
376
430
|
return true;
|
|
377
431
|
}
|
|
378
432
|
|
|
@@ -414,6 +468,8 @@ function reportPersistenceBlockReason(
|
|
|
414
468
|
}
|
|
415
469
|
|
|
416
470
|
async function recordReadAgentReportEvent(
|
|
471
|
+
state: RunningReadAgent,
|
|
472
|
+
isCurrent: () => boolean,
|
|
417
473
|
teamName: string,
|
|
418
474
|
member: Member,
|
|
419
475
|
status: "completed" | "failed",
|
|
@@ -421,17 +477,29 @@ async function recordReadAgentReportEvent(
|
|
|
421
477
|
summary: string,
|
|
422
478
|
startedAt: number,
|
|
423
479
|
tokensUsed: number,
|
|
480
|
+
result: ReportResult,
|
|
424
481
|
costUsd?: number,
|
|
425
482
|
color?: string,
|
|
426
483
|
reportMetadata: Record<string, any> = {}
|
|
427
|
-
): Promise<{ persisted: true } | { persisted: false; error: unknown }> {
|
|
484
|
+
): Promise<{ persisted: true; event: TeamReportEvent } | { persisted: false; error: unknown }> {
|
|
428
485
|
const operation = operationMetadataFromMember(member);
|
|
429
486
|
const modelSlot = canonicalPersistedModelSlot(member.modelSlot);
|
|
487
|
+
let checkpointOperation: RunningReadAgent["checkpointOperation"];
|
|
488
|
+
let settle = () => {};
|
|
430
489
|
try {
|
|
431
|
-
|
|
490
|
+
if (member.checkpointAssignment) {
|
|
491
|
+
if (state.stopRequested || !isCurrent()) throw new Error("Checkpoint publication cancelled: agent run is closing.");
|
|
492
|
+
if (state.checkpointOperation) throw new Error("Checkpoint publication is already active for this run.");
|
|
493
|
+
checkpointOperation = { controller: new AbortController(), settled: new Promise<void>(resolve => { settle = resolve; }) };
|
|
494
|
+
state.checkpointOperation = checkpointOperation;
|
|
495
|
+
}
|
|
496
|
+
const event = await reportEvents.appendTeamReportEvent(teamName, {
|
|
432
497
|
agentName: member.name,
|
|
498
|
+
checkpoint: checkpointReference(teamName, member),
|
|
499
|
+
completionGroup: member.completionGroup,
|
|
433
500
|
role: member.role || "read",
|
|
434
501
|
status,
|
|
502
|
+
result,
|
|
435
503
|
report,
|
|
436
504
|
summary,
|
|
437
505
|
startedAt,
|
|
@@ -453,9 +521,16 @@ async function recordReadAgentReportEvent(
|
|
|
453
521
|
...reportMetadata,
|
|
454
522
|
},
|
|
455
523
|
});
|
|
456
|
-
|
|
524
|
+
await saveReportCheckpoint(teamName, member, event, checkpointOperation?.controller.signal);
|
|
525
|
+
if (checkpointOperation && (checkpointOperation.controller.signal.aborted || state.stopRequested || !isCurrent())) throw new Error("Checkpoint publication cancelled: agent run is closing.");
|
|
526
|
+
if (event.checkpoint) result.checkpointId = event.checkpoint.id;
|
|
527
|
+
return { persisted: true, event };
|
|
457
528
|
} catch (error) {
|
|
529
|
+
if (member.checkpointAssignment) state.finalizationBlockedReason ||= `Checkpoint publication failed: ${String(error)}`;
|
|
458
530
|
return { persisted: false, error };
|
|
531
|
+
} finally {
|
|
532
|
+
if (checkpointOperation && state.checkpointOperation === checkpointOperation) state.checkpointOperation = undefined;
|
|
533
|
+
settle();
|
|
459
534
|
}
|
|
460
535
|
}
|
|
461
536
|
|
|
@@ -490,44 +565,43 @@ async function ensureReadHelperCompletionMessages(
|
|
|
490
565
|
report: string,
|
|
491
566
|
outcome: "completed" | "failed" = "completed",
|
|
492
567
|
color = member.color,
|
|
493
|
-
|
|
568
|
+
options: Pick<RunReadAgentOptions, "deliverMessageToActiveAgent" | "notifyLeadOfInboxReports"> = {}
|
|
494
569
|
): Promise<void> {
|
|
495
|
-
|
|
570
|
+
const requestedBy = member.requestedBy;
|
|
571
|
+
if (!requestedBy) return;
|
|
496
572
|
|
|
497
|
-
const expectedRequesterRunId = member.parentAgentName === member.requestedBy
|
|
498
|
-
? member.parentLifecycleRunId
|
|
499
|
-
: undefined;
|
|
500
573
|
let requesterReceivedReport = false;
|
|
574
|
+
// Direct session delivery has no inbox sender envelope. Keep identity outside the report body.
|
|
575
|
+
const attributedReport = `Read helper report ${JSON.stringify({ agentName: member.name, runId, runtimeStatus: outcome })}\n\n${report}`;
|
|
501
576
|
try {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
message => message?.metadata?.helperReport === true && message?.metadata?.runId === runId
|
|
514
|
-
);
|
|
515
|
-
if (!requesterReceivedReport) {
|
|
516
|
-
await messaging.sendPlainMessageIfRunning(
|
|
517
|
-
teamName,
|
|
518
|
-
member.name,
|
|
519
|
-
member.requestedBy,
|
|
520
|
-
report,
|
|
521
|
-
outcome === "failed" ? `Read helper ${member.name} failed` : `Read helper ${member.name} report`,
|
|
522
|
-
color,
|
|
523
|
-
{
|
|
524
|
-
metadata: { helperReport: true, helperCompletion: true, runId, outcome, requestedBy: member.requestedBy },
|
|
525
|
-
...(expectedRequesterRunId ? { expectedRecipientRunId: expectedRequesterRunId } : {}),
|
|
526
|
-
}
|
|
527
|
-
);
|
|
528
|
-
requesterReceivedReport = true;
|
|
577
|
+
const requester = (await teams.readConfig(teamName)).members.find(item => item.name === member.requestedBy);
|
|
578
|
+
const expectedRequesterRunId = member.parentAgentName === member.requestedBy
|
|
579
|
+
? member.parentLifecycleRunId : requester?.lifecycleRunId;
|
|
580
|
+
await messaging.sendPlainMessageOnceIfRunning(
|
|
581
|
+
teamName, member.name, requestedBy, report,
|
|
582
|
+
outcome === "failed" ? `Read helper ${member.name} failed` : `Read helper ${member.name} report`,
|
|
583
|
+
{
|
|
584
|
+
color,
|
|
585
|
+
operationId: `helper-report:${runId}`,
|
|
586
|
+
expectedRecipientRunId: expectedRequesterRunId,
|
|
587
|
+
metadata: { helperReport: true, helperCompletion: true, runId, outcome, requestedBy: member.requestedBy },
|
|
529
588
|
}
|
|
530
|
-
|
|
589
|
+
);
|
|
590
|
+
requesterReceivedReport = true;
|
|
591
|
+
// Durability, not the requester's complete idle model run, gates helper cleanup.
|
|
592
|
+
// Even an already persisted report must wake its exact requester automatically.
|
|
593
|
+
const wake = Promise.resolve().then(() => options.deliverMessageToActiveAgent?.(
|
|
594
|
+
teamName, requestedBy, attributedReport, expectedRequesterRunId, true
|
|
595
|
+
));
|
|
596
|
+
void wake.then(delivered => {
|
|
597
|
+
if (!delivered) throw new Error("requester unavailable for automatic delivery");
|
|
598
|
+
}).catch(async error => {
|
|
599
|
+
await messaging.sendPlainMessage(teamName, member.name, "team-lead",
|
|
600
|
+
`Automatic report delivery to ${member.requestedBy} could not be confirmed: ${String(error)}. The full report is retained below; do not replay it automatically into a replacement run.\n\n${attributedReport}`,
|
|
601
|
+
`Read helper ${member.name} wake incomplete`, color,
|
|
602
|
+
{ metadata: { helperWakeFailed: true, runId, requestedBy: member.requestedBy, expectedRecipientRunId: expectedRequesterRunId } });
|
|
603
|
+
await options.notifyLeadOfInboxReports?.(teamName);
|
|
604
|
+
}).catch(() => { console.warn(`Could not record helper wake outcome for ${member.name} (${runId}).`); });
|
|
531
605
|
} catch {
|
|
532
606
|
requesterReceivedReport = false;
|
|
533
607
|
}
|
|
@@ -549,9 +623,7 @@ async function ensureReadHelperCompletionMessages(
|
|
|
549
623
|
teamName,
|
|
550
624
|
member.name,
|
|
551
625
|
"team-lead",
|
|
552
|
-
outcome
|
|
553
|
-
? `Read helper ${member.name} failed for ${member.requestedBy}. ${delivery}`
|
|
554
|
-
: `Read helper ${member.name} completed for ${member.requestedBy}. ${delivery}`,
|
|
626
|
+
`Read helper ${member.name} ${outcome} for ${requestedBy}. ${delivery}${requesterReceivedReport ? "" : `\n\n${attributedReport}`}`,
|
|
555
627
|
outcome === "failed" ? `Read helper ${member.name} failed` : `Read helper ${member.name} done`,
|
|
556
628
|
color,
|
|
557
629
|
{ metadata: { finalReport: true, helperCompletion: true, runId, outcome, requestedBy: member.requestedBy } }
|
|
@@ -597,6 +669,12 @@ export async function runReadAgentInProcess(
|
|
|
597
669
|
}
|
|
598
670
|
member.lifecycleRunId = lifecycleRunId;
|
|
599
671
|
const extensionInstanceId = options.extensionInstanceId ?? generateExtensionInstanceId();
|
|
672
|
+
let costRun: CostRun | undefined;
|
|
673
|
+
let costSnapshot: AgentCostSnapshot | undefined;
|
|
674
|
+
let costOutcome: CostOutcome = "cancelled";
|
|
675
|
+
try {
|
|
676
|
+
costRun = options.beginCostRun?.(ctx.sessionManager.getSessionId(), readTeamName, lifecycleRunId);
|
|
677
|
+
} catch { /* Additive telemetry must not prevent an otherwise valid launch. */ }
|
|
600
678
|
const state: RunningReadAgent = {
|
|
601
679
|
runId: lifecycleRunId,
|
|
602
680
|
name: member.name,
|
|
@@ -616,6 +694,7 @@ export async function runReadAgentInProcess(
|
|
|
616
694
|
startupState: "pending",
|
|
617
695
|
sessionCreation,
|
|
618
696
|
teardownState: "active",
|
|
697
|
+
onCostSettled: () => costRun?.settle(costSnapshot, costOutcome),
|
|
619
698
|
};
|
|
620
699
|
let sessionCreationSettled = false;
|
|
621
700
|
const settleSessionCreation = (session: AgentSession | undefined): void => {
|
|
@@ -645,8 +724,30 @@ export async function runReadAgentInProcess(
|
|
|
645
724
|
let submittedFinalReport: SubmittedAgentReport | undefined;
|
|
646
725
|
let finalReportSubmissionInProgress = false;
|
|
647
726
|
let childSessionManager: any;
|
|
727
|
+
let childLifecycleProbeUnsubscribe = (): void => {};
|
|
648
728
|
let privateSessionDirectory: string | undefined;
|
|
649
|
-
let
|
|
729
|
+
let completedReportPersisted = false;
|
|
730
|
+
let resolvedTaskResult: ReportResult | undefined;
|
|
731
|
+
let assignedChecks: CheckDefinition[] | undefined;
|
|
732
|
+
let repairPolicy: RepairPolicy | undefined;
|
|
733
|
+
const deliveredRepairRequests = new Set<string>();
|
|
734
|
+
let repairCancellation: Promise<void> | undefined;
|
|
735
|
+
const cancelAutomaticRepair = async (): Promise<void> => {
|
|
736
|
+
const result = resolvedTaskResult;
|
|
737
|
+
if (!repairPolicy || result?.repair?.state !== "requested") return;
|
|
738
|
+
repairCancellation ??= (async () => {
|
|
739
|
+
try {
|
|
740
|
+
await new VerificationController({ teamName: readTeamName, result, cwd: member.cwd, checks: assignedChecks, repair: repairPolicy }).cancel();
|
|
741
|
+
result.repair = { ...result.repair!, state: "cancelled", outcome: "blocked" };
|
|
742
|
+
} catch (error) {
|
|
743
|
+
state.finalizationBlockedReason = `Repair cancellation could not be persisted: ${error instanceof Error ? error.message : String(error)}`;
|
|
744
|
+
result.verification.error = state.finalizationBlockedReason;
|
|
745
|
+
closeReadAgentMessageDelivery(state);
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
})();
|
|
749
|
+
await repairCancellation;
|
|
750
|
+
};
|
|
650
751
|
const pendingChildController = options.pendingChildController;
|
|
651
752
|
const pendingChildParent: ParentRunIdentity | undefined = isEligibleNestedReadParent(member)
|
|
652
753
|
? {
|
|
@@ -681,6 +782,79 @@ export async function runReadAgentInProcess(
|
|
|
681
782
|
return deliveryClose;
|
|
682
783
|
};
|
|
683
784
|
|
|
785
|
+
const verifyTaskResult = async (reported: ReportedTaskDetails, signal?: AbortSignal, submissionId?: string): Promise<VerificationDecision> => {
|
|
786
|
+
if (state.checkOperation) throw new Error("Check verification is already active for this run.");
|
|
787
|
+
if (assignedChecks?.length && (state.stopRequested || state.messageDeliveryClosed || state.finalizationBlockedReason || !options.isCurrentReadAgentRun(key, state))) {
|
|
788
|
+
throw new Error("Check verification cancelled: agent run is closing.");
|
|
789
|
+
}
|
|
790
|
+
const result = createReportResult(readTeamName, member.name, state.runId, reported);
|
|
791
|
+
if (resolvedTaskResult?.repair) Object.assign(result, { verification: resolvedTaskResult.verification, repair: resolvedTaskResult.repair });
|
|
792
|
+
resolvedTaskResult = result;
|
|
793
|
+
if (!assignedChecks?.length) return { result, checks: [] };
|
|
794
|
+
let decision: VerificationDecision = { result, checks: [] };
|
|
795
|
+
const verificationController = new VerificationController({
|
|
796
|
+
teamName: readTeamName, result, cwd: member.cwd, checks: assignedChecks, repair: repairPolicy,
|
|
797
|
+
});
|
|
798
|
+
result.verification = { state: "pending", checkIds: assignedCheckIds(result, assignedChecks) };
|
|
799
|
+
const controller = new AbortController();
|
|
800
|
+
const abort = () => controller.abort();
|
|
801
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
802
|
+
if (signal?.aborted) abort();
|
|
803
|
+
let settle!: () => void;
|
|
804
|
+
const operation = { controller, settled: new Promise<void>(resolve => { settle = resolve; }) };
|
|
805
|
+
state.checkOperation = operation;
|
|
806
|
+
markReadAgentActivity(state, "running assigned checks", "working", "assigned-check");
|
|
807
|
+
options.renderReadAgentStatus();
|
|
808
|
+
try {
|
|
809
|
+
try {
|
|
810
|
+
decision = await verificationController.verify(submissionId ?? "", {
|
|
811
|
+
loadOperations: options.loadCheckOperations ?? loadNativeCheckOperations,
|
|
812
|
+
signal: controller.signal,
|
|
813
|
+
});
|
|
814
|
+
Object.assign(result, decision.result);
|
|
815
|
+
decision.result = result;
|
|
816
|
+
} catch (error) {
|
|
817
|
+
state.finalizationBlockedReason = `Assigned check evidence could not be persisted: ${error instanceof Error ? error.message : String(error)}`;
|
|
818
|
+
result.verification.error = state.finalizationBlockedReason;
|
|
819
|
+
const repair = verificationController.pendingRepair(error);
|
|
820
|
+
if (repair) result.repair = repair;
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
if (result.verification.state === "pending") {
|
|
824
|
+
state.finalizationBlockedReason = "Assigned checks have an unresolved execution claim; inspect the durable journal before finalizing this run.";
|
|
825
|
+
throw new Error(state.finalizationBlockedReason);
|
|
826
|
+
}
|
|
827
|
+
if (controller.signal.aborted || state.stopRequested || !options.isCurrentReadAgentRun(key, state)) {
|
|
828
|
+
await cancelAutomaticRepair();
|
|
829
|
+
throw new Error("Assigned check verification was cancelled.");
|
|
830
|
+
}
|
|
831
|
+
return decision;
|
|
832
|
+
} finally {
|
|
833
|
+
signal?.removeEventListener("abort", abort);
|
|
834
|
+
if (state.checkOperation === operation) state.checkOperation = undefined;
|
|
835
|
+
if (controller.signal.aborted && !state.messageDeliveryClosed && !state.stopRequested && options.isCurrentReadAgentRun(key, state)) {
|
|
836
|
+
state.acceptingMessages = true;
|
|
837
|
+
}
|
|
838
|
+
settle();
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const deliverGroupedReport = async (event: TeamReportEvent, leadReport: string): Promise<void> => {
|
|
843
|
+
const planning = isPiPromptPlanningMember(member);
|
|
844
|
+
const suppressed = shouldSuppressLeadReportInjection(member);
|
|
845
|
+
const emitsEvent = planning || (!suppressed && !options.isTeammate
|
|
846
|
+
&& (options.getTeamName() === readTeamName || readTeamName.startsWith("prompt-build-")));
|
|
847
|
+
const grouped = await deliverCompletionGroupReport(event, () => {
|
|
848
|
+
if (emitsEvent) options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed,
|
|
849
|
+
planning ? event.report : leadReport, event.status === "completed", true);
|
|
850
|
+
});
|
|
851
|
+
if (!grouped) throw new Error("Grouped report provenance is unavailable.");
|
|
852
|
+
if (!suppressed && !readTeamName.startsWith("prompt-build-")) {
|
|
853
|
+
await options.renderLeadInboxStatus?.().catch(() => {});
|
|
854
|
+
await options.notifyLeadOfInboxReports?.(readTeamName).catch(() => {});
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
|
|
684
858
|
const deliverCompletion = async (
|
|
685
859
|
resolution: ResolvedReadAgentReport,
|
|
686
860
|
completionSummary: string,
|
|
@@ -690,6 +864,13 @@ export async function runReadAgentInProcess(
|
|
|
690
864
|
const session = state.session;
|
|
691
865
|
if (!session) throw new Error(`Agent ${member.name} completed without a nested session.`);
|
|
692
866
|
const report = resolution.report!;
|
|
867
|
+
const result = resolvedTaskResult ?? createReportResult(readTeamName, member.name, state.runId, resolution);
|
|
868
|
+
resolvedTaskResult = result;
|
|
869
|
+
const checkpoint = checkpointReference(readTeamName, member);
|
|
870
|
+
if (checkpoint) result.checkpointId = checkpoint.id;
|
|
871
|
+
const leadReport = (assignedChecks?.length
|
|
872
|
+
? `${report}\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}.`
|
|
873
|
+
: report) + (checkpoint ? `\n\nCheckpoint reference: ${checkpoint.id}.` : "");
|
|
693
874
|
const completionStats = session.getSessionStats();
|
|
694
875
|
// Private child transcripts are deleted after teardown; never publish pointers
|
|
695
876
|
// that would outlive a successful run's recovery artifact.
|
|
@@ -709,6 +890,7 @@ export async function runReadAgentInProcess(
|
|
|
709
890
|
name: member.name,
|
|
710
891
|
role,
|
|
711
892
|
status: "completed",
|
|
893
|
+
result,
|
|
712
894
|
report,
|
|
713
895
|
summary: completionSummary,
|
|
714
896
|
completedAt: Date.now(),
|
|
@@ -738,6 +920,8 @@ export async function runReadAgentInProcess(
|
|
|
738
920
|
...reportMetadata,
|
|
739
921
|
};
|
|
740
922
|
const reportEventPersistence = await recordReadAgentReportEvent(
|
|
923
|
+
state,
|
|
924
|
+
() => options.isCurrentReadAgentRun(key, state),
|
|
741
925
|
readTeamName,
|
|
742
926
|
member,
|
|
743
927
|
"completed",
|
|
@@ -745,6 +929,7 @@ export async function runReadAgentInProcess(
|
|
|
745
929
|
completionSummary,
|
|
746
930
|
state.startedAt,
|
|
747
931
|
state.tokensUsed,
|
|
932
|
+
result,
|
|
748
933
|
completionStats.cost,
|
|
749
934
|
undefined,
|
|
750
935
|
reportMetadata,
|
|
@@ -757,14 +942,17 @@ export async function runReadAgentInProcess(
|
|
|
757
942
|
);
|
|
758
943
|
throw new Error(state.finalizationBlockedReason);
|
|
759
944
|
}
|
|
945
|
+
if (checkpoint && (state.stopRequested || !options.isCurrentReadAgentRun(key, state))) return;
|
|
946
|
+
completedReportPersisted = true;
|
|
760
947
|
// The lifecycle finalizer consumes this flag only after session disposal and
|
|
761
948
|
// successful lifecycle finalization, so every successful private run is removed.
|
|
762
949
|
if (privateSessionDirectory) {
|
|
763
950
|
state.cleanupPrivateSessionOnFinalize = true;
|
|
764
|
-
privateCompletedReportPersisted = true;
|
|
765
951
|
}
|
|
766
952
|
const suppressLeadReportInjection = shouldSuppressLeadReportInjection(member);
|
|
767
|
-
if (member.requestedBy) {
|
|
953
|
+
if (member.completionGroup && !member.requestedBy) {
|
|
954
|
+
await deliverGroupedReport(reportEventPersistence.event, leadReport);
|
|
955
|
+
} else if (member.requestedBy) {
|
|
768
956
|
await ensureReadHelperCompletionMessages(
|
|
769
957
|
readTeamName,
|
|
770
958
|
member,
|
|
@@ -773,7 +961,7 @@ export async function runReadAgentInProcess(
|
|
|
773
961
|
report,
|
|
774
962
|
"completed",
|
|
775
963
|
member.color,
|
|
776
|
-
options
|
|
964
|
+
options
|
|
777
965
|
);
|
|
778
966
|
await options.renderLeadInboxStatus?.().catch(() => {});
|
|
779
967
|
await options.notifyLeadOfInboxReports?.(readTeamName).catch(() => {});
|
|
@@ -784,13 +972,17 @@ export async function runReadAgentInProcess(
|
|
|
784
972
|
// Workflow orchestrators consume full reports from TeamReportEvent storage.
|
|
785
973
|
// Pi Prompt consumes its writer report through the private event without a lead turn.
|
|
786
974
|
} else if (!options.isTeammate && (options.getTeamName() === readTeamName || readTeamName.startsWith("prompt-build-"))) {
|
|
787
|
-
options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed,
|
|
975
|
+
options.emitAgentReport(readTeamName, member.name, state.startedAt, state.tokensUsed, leadReport, true);
|
|
788
976
|
} else {
|
|
789
|
-
await ensureLeadCompletionMessage(readTeamName, member, state.startedAt,
|
|
977
|
+
await ensureLeadCompletionMessage(readTeamName, member, state.startedAt, leadReport, completionSummary, member.color, completionMetadata);
|
|
790
978
|
}
|
|
791
979
|
};
|
|
792
980
|
|
|
793
981
|
try {
|
|
982
|
+
assignedChecks = normalizeCheckPolicy(member.assignedChecks);
|
|
983
|
+
repairPolicy = normalizeRepairPolicy(member.repairPolicy);
|
|
984
|
+
if (repairPolicy && !assignedChecks?.length) throw new Error("Repair policy requires explicitly assigned checks.");
|
|
985
|
+
if (repairPolicy) repairCancellationHandlers.set(state, cancelAutomaticRepair);
|
|
794
986
|
if (pendingChildParent && !pendingChildController) {
|
|
795
987
|
throw new Error(`Eligible nested read parent ${member.name} requires a pending child controller.`);
|
|
796
988
|
}
|
|
@@ -824,6 +1016,7 @@ export async function runReadAgentInProcess(
|
|
|
824
1016
|
|
|
825
1017
|
const {
|
|
826
1018
|
createAgentSession,
|
|
1019
|
+
createEventBus,
|
|
827
1020
|
DefaultResourceLoader,
|
|
828
1021
|
getAgentDir,
|
|
829
1022
|
SessionManager,
|
|
@@ -843,16 +1036,21 @@ export async function runReadAgentInProcess(
|
|
|
843
1036
|
const childSettingsManager = createSettingsManager(member.cwd, agentDir, {
|
|
844
1037
|
projectTrusted: resourcePlan.trust.projectTrusted,
|
|
845
1038
|
});
|
|
846
|
-
const
|
|
847
|
-
?
|
|
1039
|
+
const nestedReadBinding = isEligibleNestedReadParent(member)
|
|
1040
|
+
? {
|
|
848
1041
|
teamName: readTeamName,
|
|
849
1042
|
parent: member,
|
|
850
1043
|
parentRunId: state.runId,
|
|
851
1044
|
outerCtx: ctx,
|
|
852
|
-
}
|
|
1045
|
+
}
|
|
1046
|
+
: undefined;
|
|
1047
|
+
const nestedReadAgentTools = nestedReadBinding
|
|
1048
|
+
? options.createNestedReadAgentTools?.(nestedReadBinding) ?? []
|
|
853
1049
|
: [];
|
|
854
1050
|
const nestedReadDelegationEnabled = nestedReadAgentTools.length > 0;
|
|
1051
|
+
const childEventBus = createEventBus();
|
|
855
1052
|
const loader = new DefaultResourceLoader({
|
|
1053
|
+
eventBus: childEventBus,
|
|
856
1054
|
cwd: member.cwd,
|
|
857
1055
|
agentDir,
|
|
858
1056
|
settingsManager: childSettingsManager,
|
|
@@ -880,7 +1078,9 @@ export async function runReadAgentInProcess(
|
|
|
880
1078
|
]
|
|
881
1079
|
: ["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."]),
|
|
882
1080
|
"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.",
|
|
883
|
-
|
|
1081
|
+
repairPolicy
|
|
1082
|
+
? `Use report_and_exit with the complete deliverable in content. The lead authorized at most ${repairPolicy.maxAttempts} additional repair attempts for assigned checks. If the tool requests repair, stay active, inspect its observed evidence and repair only within your role and assigned scope. Report blocked or failed if you cannot continue safely. Stop only after the report is accepted; never replace the deliverable with a summary.`
|
|
1083
|
+
: "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.",
|
|
884
1084
|
],
|
|
885
1085
|
});
|
|
886
1086
|
await loader.reload();
|
|
@@ -903,19 +1103,29 @@ export async function runReadAgentInProcess(
|
|
|
903
1103
|
pushReadAgentEvent(state, status);
|
|
904
1104
|
options.renderReadAgentStatus();
|
|
905
1105
|
},
|
|
906
|
-
|
|
1106
|
+
repairEnabled: !!repairPolicy,
|
|
1107
|
+
onReportAndExit: async (report, signal, submissionId) => {
|
|
907
1108
|
const content = nonEmptyReportText(report.content);
|
|
908
1109
|
if (!content) throw new Error("Final report content must not be empty.");
|
|
909
1110
|
if (submittedFinalReport || finalReportSubmissionInProgress) return { accepted: false };
|
|
910
1111
|
finalReportSubmissionInProgress = true;
|
|
911
1112
|
try {
|
|
1113
|
+
if (member.checkpointAssignment) preflightReportCheckpoint(readTeamName, member, createReportResult(readTeamName, member.name, state.runId, report));
|
|
1114
|
+
const decision = await verifyTaskResult(report, signal, submissionId ? `tool:${submissionId}` : undefined);
|
|
1115
|
+
if (decision.request) {
|
|
1116
|
+
deliveredRepairRequests.add(decision.request.id);
|
|
1117
|
+
return { accepted: false, verification: decision.result.verification, repairRequest: decision.request };
|
|
1118
|
+
}
|
|
1119
|
+
const result = decision.result;
|
|
912
1120
|
const deliveryClose = await closeRecipient();
|
|
913
1121
|
submittedFinalReport = {
|
|
1122
|
+
...normalizeReportedTaskDetails(report),
|
|
914
1123
|
content,
|
|
915
1124
|
summary: nonEmptyReportText(report.summary),
|
|
916
1125
|
};
|
|
917
1126
|
return {
|
|
918
1127
|
accepted: true,
|
|
1128
|
+
...(assignedChecks?.length ? { verification: result.verification } : {}),
|
|
919
1129
|
cancelledDeliveries: deliveryClose.cancelledDeliveries,
|
|
920
1130
|
deliveryOutcome: deliveryClose.cancelledDeliveries > 0 ? "cancelled" : "none",
|
|
921
1131
|
};
|
|
@@ -951,6 +1161,14 @@ export async function runReadAgentInProcess(
|
|
|
951
1161
|
childSessionManager = privateSessionDirectory
|
|
952
1162
|
? SessionManager.create(member.cwd, privateSessionDirectory)
|
|
953
1163
|
: SessionManager.create(member.cwd);
|
|
1164
|
+
try { costSnapshot = { childSessionId: childSessionManager.getSessionId(), costUsd: null }; } catch { /* Unknown provenance. */ }
|
|
1165
|
+
if (nestedReadBinding && options.nestedChildSnapshot) {
|
|
1166
|
+
const childSessionId = childSessionManager.getSessionId();
|
|
1167
|
+
childLifecycleProbeUnsubscribe = childEventBus.on(CHILD_AGENT_LIFECYCLE_PROBE, (payload: any) => {
|
|
1168
|
+
if (payload?.sessionId !== childSessionId || typeof payload.respond !== "function") return;
|
|
1169
|
+
payload.respond({ sessionId: childSessionId, ...options.nestedChildSnapshot!(nestedReadBinding) });
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
954
1172
|
const parentModelRuntime: unknown = Reflect.get(ctx.modelRegistry, "runtime");
|
|
955
1173
|
const { session } = await createAgentSession({
|
|
956
1174
|
cwd: member.cwd,
|
|
@@ -968,7 +1186,23 @@ export async function runReadAgentInProcess(
|
|
|
968
1186
|
} as Parameters<typeof createAgentSession>[0] & { modelRuntime?: unknown });
|
|
969
1187
|
|
|
970
1188
|
state.session = session;
|
|
971
|
-
|
|
1189
|
+
// Current hosts capture this public hook when a prompt starts; legacy hosts
|
|
1190
|
+
// retain their existing loop behavior. Never abort from the reporting tool.
|
|
1191
|
+
if (session.agent && "shouldStopAfterTurn" in session.agent) {
|
|
1192
|
+
const agent = session.agent as typeof session.agent & {
|
|
1193
|
+
shouldStopAfterTurn?: (...args: unknown[]) => boolean | Promise<boolean>;
|
|
1194
|
+
};
|
|
1195
|
+
const previous = agent.shouldStopAfterTurn;
|
|
1196
|
+
agent.shouldStopAfterTurn = async (...args) => {
|
|
1197
|
+
const stopped = await previous?.(...args);
|
|
1198
|
+
return submittedFinalReport !== undefined || stopped === true;
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
const sessionLifecycle = installReadAgentSessionLifecycle(session, () => {
|
|
1202
|
+
if (handoffRequested) return;
|
|
1203
|
+
const own = recordedSessionCost(childSessionManager.getEntries());
|
|
1204
|
+
costSnapshot = { childSessionId: childSessionManager.getSessionId(), costUsd: own.complete ? own.usd : null };
|
|
1205
|
+
});
|
|
972
1206
|
try {
|
|
973
1207
|
if (typeof session.bindExtensions === "function") {
|
|
974
1208
|
await (session.bindExtensions as (bindings: { mode: "print" }) => Promise<void>)({ mode: "print" });
|
|
@@ -1001,12 +1235,23 @@ export async function runReadAgentInProcess(
|
|
|
1001
1235
|
}
|
|
1002
1236
|
await teams.updateMember(readTeamName, member.name, { herdrPaneId: id, tmuxPaneId: "" });
|
|
1003
1237
|
});
|
|
1238
|
+
const rejectActiveCheck = (): void => {
|
|
1239
|
+
if (state.checkOperation) {
|
|
1240
|
+
throw new Error("Cannot move agent to Herdr while an assigned check is active. Try again after it settles.");
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1004
1243
|
state.moveToHerdr = () => moving ??= (async () => {
|
|
1005
1244
|
if (!options.isCurrentReadAgentRun(key, state) || (!handoffRequested && !state.acceptingMessages)) {
|
|
1006
1245
|
throw new Error("The agent is already finishing.");
|
|
1007
1246
|
}
|
|
1008
1247
|
if (handoffRequested && !released) throw new Error("The current operation is still stopping. Try h after it settles.");
|
|
1009
|
-
if (paneId) {
|
|
1248
|
+
if (paneId) {
|
|
1249
|
+
rejectActiveCheck();
|
|
1250
|
+
herdrCommand("pane", "close", paneId);
|
|
1251
|
+
await recordPane(undefined);
|
|
1252
|
+
paneId = undefined;
|
|
1253
|
+
}
|
|
1254
|
+
rejectActiveCheck();
|
|
1010
1255
|
const sessionFile = childSessionManager.getSessionFile();
|
|
1011
1256
|
if (!sessionFile || !fs.existsSync(sessionFile)) throw new Error("The agent has no saved session yet.");
|
|
1012
1257
|
if (!handoffRequested) {
|
|
@@ -1037,6 +1282,7 @@ export async function runReadAgentInProcess(
|
|
|
1037
1282
|
try {
|
|
1038
1283
|
if (!handoffRequested) {
|
|
1039
1284
|
handoffRequested = true;
|
|
1285
|
+
try { costRun?.exclude(); } catch { /* Terminal work is outside this total. */ }
|
|
1040
1286
|
state.stopRequested = true;
|
|
1041
1287
|
const delivery = closeReadAgentMessageDelivery(state);
|
|
1042
1288
|
const pending = session.clearQueue();
|
|
@@ -1214,6 +1460,65 @@ export async function runReadAgentInProcess(
|
|
|
1214
1460
|
}
|
|
1215
1461
|
}
|
|
1216
1462
|
}
|
|
1463
|
+
while (assignedChecks?.length && !submittedFinalReport && !pendingChildParent
|
|
1464
|
+
&& !state.stopRequested && options.isCurrentReadAgentRun(key, state)) {
|
|
1465
|
+
const wakeGeneration = readAgentWakeGeneration(state);
|
|
1466
|
+
// A delivery may have started before checks owned the operation gate.
|
|
1467
|
+
// Drain every admitted turn before capturing or verifying its final report.
|
|
1468
|
+
let deliveryTail: Promise<void> | undefined;
|
|
1469
|
+
do {
|
|
1470
|
+
deliveryTail = state.messageDeliveryTail;
|
|
1471
|
+
if (deliveryTail) await deliveryTail.catch(() => {});
|
|
1472
|
+
} while (deliveryTail !== state.messageDeliveryTail);
|
|
1473
|
+
if (state.stopRequested || !options.isCurrentReadAgentRun(key, state)) break;
|
|
1474
|
+
completionResolution = resolveCurrentReport();
|
|
1475
|
+
if (submittedFinalReport) break;
|
|
1476
|
+
if (state.completedOperationError) throw state.completedOperationError;
|
|
1477
|
+
if (state.completedOperationInterrupted) {
|
|
1478
|
+
if (!await waitForPostInterruptOperation(state.completedOperationGeneration!, wakeGeneration)) return;
|
|
1479
|
+
continue;
|
|
1480
|
+
}
|
|
1481
|
+
if (completionResolution.terminalFailure || !completionResolution.report) {
|
|
1482
|
+
throw unavailableReportError(completionResolution.terminalFailure ?? "No usable report is available for verification.");
|
|
1483
|
+
}
|
|
1484
|
+
const submissionId = `fallback:${state.completedOperationGeneration ?? 0}`;
|
|
1485
|
+
let decision: VerificationDecision | undefined;
|
|
1486
|
+
const verification = await runReadAgentSessionOperation(state, async () => {
|
|
1487
|
+
decision = await verifyTaskResult(completionResolution!, undefined, submissionId);
|
|
1488
|
+
});
|
|
1489
|
+
if (verification.interrupted) {
|
|
1490
|
+
if (!await waitForPostInterruptOperation(verification.generation, wakeGeneration)) return;
|
|
1491
|
+
} else {
|
|
1492
|
+
let deliveryTail: Promise<void> | undefined;
|
|
1493
|
+
do {
|
|
1494
|
+
deliveryTail = state.messageDeliveryTail;
|
|
1495
|
+
if (deliveryTail) await deliveryTail.catch(() => {});
|
|
1496
|
+
} while (deliveryTail !== state.messageDeliveryTail);
|
|
1497
|
+
state.acceptingMessages = false;
|
|
1498
|
+
const latestGeneration = state.completedOperationGeneration ?? verification.generation;
|
|
1499
|
+
if (latestGeneration === verification.generation) {
|
|
1500
|
+
const request = decision?.request;
|
|
1501
|
+
if (!request || deliveredRepairRequests.has(request.id)) break;
|
|
1502
|
+
deliveredRepairRequests.add(request.id);
|
|
1503
|
+
if (state.messageDeliveryClosed || state.stopRequested || !options.isCurrentReadAgentRun(key, state)) return;
|
|
1504
|
+
state.acceptingMessages = true;
|
|
1505
|
+
markReadAgentActivity(state, `repairing assigned checks (attempt ${request.attempt})`, "thinking");
|
|
1506
|
+
options.renderReadAgentStatus();
|
|
1507
|
+
const repairWake = readAgentWakeGeneration(state);
|
|
1508
|
+
const repairOperation = await runReadAgentSessionOperation(state, () => session.prompt(formatRepairRequest(request), { source: "extension" as any }));
|
|
1509
|
+
refreshReadAgentStats(state, session);
|
|
1510
|
+
if (repairOperation.interrupted && !await waitForPostInterruptOperation(repairOperation.generation, repairWake)) return;
|
|
1511
|
+
completionResolution = resolveCurrentReport();
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
if (state.completedOperationError) throw state.completedOperationError;
|
|
1515
|
+
if (state.completedOperationInterrupted) {
|
|
1516
|
+
state.acceptingMessages = !state.messageDeliveryClosed && !state.stopRequested && options.isCurrentReadAgentRun(key, state);
|
|
1517
|
+
if (!await waitForPostInterruptOperation(latestGeneration, wakeGeneration)) return;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
completionResolution = resolveCurrentReport();
|
|
1521
|
+
}
|
|
1217
1522
|
} finally {
|
|
1218
1523
|
state.acceptingMessages = false;
|
|
1219
1524
|
}
|
|
@@ -1240,13 +1545,16 @@ export async function runReadAgentInProcess(
|
|
|
1240
1545
|
?? `${role === "write" ? "Edit" : "Read"} agent ${member.name} completed`;
|
|
1241
1546
|
const recoveryReference = readAgentRecoveryReference(readTeamName, member.name, state.runId, childSessionManager);
|
|
1242
1547
|
await deliverCompletion(completionResolution, completionSummary, recoveryAttempted, recoveryReference);
|
|
1548
|
+
costOutcome = "completed";
|
|
1243
1549
|
} catch (e) {
|
|
1550
|
+
await cancelAutomaticRepair().catch(() => {});
|
|
1551
|
+
if (!state.stopRequested) costOutcome = "failed";
|
|
1244
1552
|
const lastError = runtime.createRuntimeError(e);
|
|
1245
1553
|
state.lastError = lastError;
|
|
1246
1554
|
options.renderReadAgentStatus();
|
|
1247
1555
|
settleSessionCreation(state.session);
|
|
1248
1556
|
await closeRecipient();
|
|
1249
|
-
if (!state.stopRequested && options.isCurrentReadAgentRun(key, state) && !
|
|
1557
|
+
if (!state.stopRequested && options.isCurrentReadAgentRun(key, state) && !completedReportPersisted) {
|
|
1250
1558
|
let failureReport = `${role === "write" ? "Edit" : "Read"} agent ${member.name} failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
1251
1559
|
const failureStats = state.session?.getSessionStats();
|
|
1252
1560
|
const reportUnavailable = e instanceof ReadAgentReportUnavailableError ? e : undefined;
|
|
@@ -1264,7 +1572,13 @@ export async function runReadAgentInProcess(
|
|
|
1264
1572
|
: {}),
|
|
1265
1573
|
};
|
|
1266
1574
|
const failureSummary = `${role === "write" ? "Edit" : "Read"} agent ${member.name} failed`;
|
|
1575
|
+
const result = resolvedTaskResult ?? createReportResult(readTeamName, member.name, state.runId, {});
|
|
1576
|
+
if (!resolvedTaskResult && assignedChecks?.length) {
|
|
1577
|
+
result.verification = { state: "pending", checkIds: assignedCheckIds(result, assignedChecks) };
|
|
1578
|
+
}
|
|
1267
1579
|
const failureEventPersistence = await recordReadAgentReportEvent(
|
|
1580
|
+
state,
|
|
1581
|
+
() => options.isCurrentReadAgentRun(key, state),
|
|
1268
1582
|
readTeamName,
|
|
1269
1583
|
member,
|
|
1270
1584
|
"failed",
|
|
@@ -1272,6 +1586,7 @@ export async function runReadAgentInProcess(
|
|
|
1272
1586
|
failureSummary,
|
|
1273
1587
|
state.startedAt,
|
|
1274
1588
|
state.tokensUsed,
|
|
1589
|
+
result,
|
|
1275
1590
|
failureStats?.cost,
|
|
1276
1591
|
"red",
|
|
1277
1592
|
failureReportMetadata,
|
|
@@ -1288,10 +1603,12 @@ export async function runReadAgentInProcess(
|
|
|
1288
1603
|
failureReport = `${failureReport}\n\n${state.finalizationBlockedReason}`;
|
|
1289
1604
|
}
|
|
1290
1605
|
}
|
|
1606
|
+
if (member.checkpointAssignment && (state.stopRequested || !options.isCurrentReadAgentRun(key, state))) return;
|
|
1291
1607
|
options.rememberCompletedAgentReport(readTeamName, {
|
|
1292
1608
|
name: member.name,
|
|
1293
1609
|
role,
|
|
1294
1610
|
status: "failed",
|
|
1611
|
+
result,
|
|
1295
1612
|
report: failureReport,
|
|
1296
1613
|
summary: failureSummary,
|
|
1297
1614
|
completedAt: Date.now(),
|
|
@@ -1321,7 +1638,9 @@ export async function runReadAgentInProcess(
|
|
|
1321
1638
|
...failureReportMetadata,
|
|
1322
1639
|
};
|
|
1323
1640
|
const suppressLeadReportInjection = shouldSuppressLeadReportInjection(member);
|
|
1324
|
-
if (member.requestedBy) {
|
|
1641
|
+
if (member.completionGroup && !member.requestedBy && failureEventPersistence.persisted) {
|
|
1642
|
+
await deliverGroupedReport(failureEventPersistence.event, failureReport);
|
|
1643
|
+
} else if (member.requestedBy) {
|
|
1325
1644
|
await ensureReadHelperCompletionMessages(
|
|
1326
1645
|
readTeamName,
|
|
1327
1646
|
member,
|
|
@@ -1330,7 +1649,7 @@ export async function runReadAgentInProcess(
|
|
|
1330
1649
|
failureReport,
|
|
1331
1650
|
"failed",
|
|
1332
1651
|
"red",
|
|
1333
|
-
options
|
|
1652
|
+
options
|
|
1334
1653
|
);
|
|
1335
1654
|
await options.renderLeadInboxStatus?.().catch(() => {});
|
|
1336
1655
|
await options.notifyLeadOfInboxReports?.(readTeamName).catch(() => {});
|
|
@@ -1354,6 +1673,9 @@ export async function runReadAgentInProcess(
|
|
|
1354
1673
|
}
|
|
1355
1674
|
}
|
|
1356
1675
|
} finally {
|
|
1676
|
+
childLifecycleProbeUnsubscribe();
|
|
1677
|
+
if (state.stopRequested) await cancelAutomaticRepair().catch(() => {});
|
|
1678
|
+
repairCancellationHandlers.delete(state);
|
|
1357
1679
|
pendingParentWakeSignals.delete(state);
|
|
1358
1680
|
disposeReadAgentWake(state);
|
|
1359
1681
|
settleSessionCreation(state.session);
|