pi-subagents 0.64.0 → 0.65.1
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/CHANGELOG.md +65 -0
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +19 -17
- package/docs/configuration.md +23 -15
- package/docs/extension-api.md +18 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +15 -8
- package/docs/watchdog.md +1 -1
- package/docs/workflows.md +13 -3
- package/package.json +5 -4
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +7 -3
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/src/agents/agent-management.ts +47 -15
- package/src/api/capability-ceiling.ts +0 -1
- package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/src/api/preflight.ts +7 -4
- package/src/extension/config.ts +4 -2
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +58 -7
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +3 -2
- package/src/extension/tool-description.ts +13 -8
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +159 -95
- package/src/intercom/supervisor-ui.ts +244 -0
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/async-execution.ts +51 -25
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +14 -3
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/notify.ts +2 -0
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +614 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +152 -0
- package/src/runs/background/runner-child-sessions.ts +31 -0
- package/src/runs/background/scheduled-runs.ts +18 -4
- package/src/runs/background/subagent-runner.ts +203 -901
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +204 -378
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +161 -91
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- package/src/runs/shared/acceptance.ts +14 -1
- package/src/runs/shared/capability-ceiling.ts +1 -2
- package/src/runs/shared/child-hooks.ts +25 -0
- package/src/runs/shared/child-identity.ts +13 -2
- package/src/runs/shared/child-launch.ts +314 -0
- package/src/runs/shared/child-lifecycle.ts +25 -0
- package/src/runs/shared/child-runtime-config.ts +126 -0
- package/src/runs/shared/child-session.ts +337 -0
- package/src/runs/shared/child-tool-plan.ts +530 -0
- package/src/runs/shared/claude-code-adapter.ts +5 -1
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/external-cli-preflight.ts +16 -0
- package/src/runs/shared/llm-intent-arbiter.ts +20 -11
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +84 -15
- package/src/runs/shared/model-fallback.ts +78 -6
- package/src/runs/shared/nested-events.ts +32 -48
- package/src/runs/shared/nested-path.ts +0 -14
- package/src/runs/shared/orca-progress-tabs.ts +12 -7
- package/src/runs/shared/parallel-utils.ts +1 -2
- package/src/runs/shared/permissions.ts +0 -13
- package/src/runs/shared/process-signal.ts +4 -1
- package/src/runs/shared/run-fanout-budget.ts +0 -13
- package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
- package/src/runs/shared/structured-output.ts +17 -4
- package/src/runs/shared/subagent-control.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +31 -9
- package/src/runs/shared/worktree.ts +192 -40
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +63 -28
- package/src/shared/utils.ts +6 -4
- package/src/shared/watch-strategy.ts +2 -0
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +254 -41
- package/src/watchdog/child-status.ts +0 -1
- package/src/watchdog/register-child.ts +12 -12
- package/src/workflows/scripted-workflow.ts +80 -7
- package/src/workflows/workflow-checklist.ts +4 -3
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1059
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -4,17 +4,17 @@ import * as path from "node:path";
|
|
|
4
4
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
5
|
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
SUBAGENT_CHILD_INDEX_ENV,
|
|
10
|
-
SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV,
|
|
11
|
-
SUBAGENT_ORCHESTRATOR_TARGET_ENV,
|
|
12
|
-
SUBAGENT_RUN_ID_ENV,
|
|
13
|
-
SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV,
|
|
14
|
-
} from "../runs/shared/pi-args.ts";
|
|
15
|
-
import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
|
|
7
|
+
import type { ChildSupervisorMetadata } from "../runs/shared/child-runtime-config.ts";
|
|
8
|
+
import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type ControlEvent, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
|
|
16
9
|
import { writeAtomicJson } from "../shared/atomic-json.ts";
|
|
17
10
|
import { shouldUseNativeFsWatch } from "../shared/watch-strategy.ts";
|
|
11
|
+
import {
|
|
12
|
+
SUPERVISOR_REQUEST_MESSAGE_TYPE,
|
|
13
|
+
SUPERVISOR_REPLY_ENTRY_TYPE,
|
|
14
|
+
supervisorReplyHint,
|
|
15
|
+
type SupervisorReason,
|
|
16
|
+
type SupervisorReplyEntryData,
|
|
17
|
+
} from "./supervisor-ui.ts";
|
|
18
18
|
|
|
19
19
|
const SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, "supervisor-channels");
|
|
20
20
|
const REQUESTS_DIR = "requests";
|
|
@@ -26,8 +26,10 @@ const CHANNEL_POLL_MS = Math.min(POLL_INTERVAL_MS, 500);
|
|
|
26
26
|
const CHANNEL_SAFETY_POLL_MS = 5000;
|
|
27
27
|
const STALE_EMPTY_CHANNEL_AGE_MS = 60 * 1000;
|
|
28
28
|
const STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
29
|
+
const MAX_SUPERVISOR_REQUEST_CORRELATIONS = 256;
|
|
30
|
+
const SUPERVISOR_REQUEST_CORRELATION_RETENTION_MS = 10 * 60 * 1000;
|
|
29
31
|
|
|
30
|
-
type
|
|
32
|
+
export type SupervisorRequestState = "pending" | "resolved" | "unknown";
|
|
31
33
|
|
|
32
34
|
interface SupervisorRequest {
|
|
33
35
|
type: "subagent.supervisor.request";
|
|
@@ -42,6 +44,7 @@ interface SupervisorRequest {
|
|
|
42
44
|
runId: string;
|
|
43
45
|
agent: string;
|
|
44
46
|
childIndex: number;
|
|
47
|
+
toolCallId?: string;
|
|
45
48
|
childTarget?: string;
|
|
46
49
|
interview?: unknown;
|
|
47
50
|
}
|
|
@@ -51,6 +54,12 @@ interface PendingSupervisorRequest extends SupervisorRequest {
|
|
|
51
54
|
requestFile: string;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
interface SupervisorRequestCorrelation {
|
|
58
|
+
request: PendingSupervisorRequest;
|
|
59
|
+
state: Exclude<SupervisorRequestState, "unknown">;
|
|
60
|
+
updatedAt: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
interface SupervisorReply {
|
|
55
64
|
type: "subagent.supervisor.reply";
|
|
56
65
|
requestId: string;
|
|
@@ -113,71 +122,12 @@ function replyPath(channelDir: string, requestId: string): string {
|
|
|
113
122
|
return path.join(channelDir, REPLIES_DIR, `${safeSegment(requestId)}.json`);
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
function readTextEnv(name: string): string | undefined {
|
|
117
|
-
const value = process.env[name]?.trim();
|
|
118
|
-
return value ? value : undefined;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function readChildMetadata(): {
|
|
122
|
-
channelDir: string;
|
|
123
|
-
runId: string;
|
|
124
|
-
agent: string;
|
|
125
|
-
childIndex: number;
|
|
126
|
-
orchestratorTarget?: string;
|
|
127
|
-
orchestratorSessionId?: string;
|
|
128
|
-
childTarget?: string;
|
|
129
|
-
} | undefined {
|
|
130
|
-
const channelDir = readTextEnv(SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV);
|
|
131
|
-
const runId = readTextEnv(SUBAGENT_RUN_ID_ENV);
|
|
132
|
-
const agent = readTextEnv(SUBAGENT_CHILD_AGENT_ENV);
|
|
133
|
-
const rawIndex = readTextEnv(SUBAGENT_CHILD_INDEX_ENV);
|
|
134
|
-
const orchestratorSessionId = readTextEnv(SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV);
|
|
135
|
-
if (!channelDir || !runId || !agent || !orchestratorSessionId || rawIndex === undefined || !/^\d+$/.test(rawIndex)) return undefined;
|
|
136
|
-
return {
|
|
137
|
-
channelDir,
|
|
138
|
-
runId,
|
|
139
|
-
agent,
|
|
140
|
-
childIndex: Number(rawIndex),
|
|
141
|
-
orchestratorTarget: readTextEnv(SUBAGENT_ORCHESTRATOR_TARGET_ENV),
|
|
142
|
-
orchestratorSessionId,
|
|
143
|
-
childTarget: readTextEnv("PI_SUBAGENT_INTERCOM_SESSION_NAME"),
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
125
|
function reasonHeading(reason: SupervisorReason): string {
|
|
148
126
|
if (reason === "interview_request") return "Subagent requests a structured supervisor interview.";
|
|
149
127
|
if (reason === "progress_update") return "Subagent progress update.";
|
|
150
128
|
return "Subagent needs a supervisor decision.";
|
|
151
129
|
}
|
|
152
130
|
|
|
153
|
-
function formatChildMessage(input: {
|
|
154
|
-
reason: SupervisorReason;
|
|
155
|
-
message?: string;
|
|
156
|
-
interview?: unknown;
|
|
157
|
-
runId: string;
|
|
158
|
-
agent: string;
|
|
159
|
-
childIndex: number;
|
|
160
|
-
childTarget?: string;
|
|
161
|
-
}): string {
|
|
162
|
-
const lines = [
|
|
163
|
-
reasonHeading(input.reason),
|
|
164
|
-
`Run: ${input.runId}`,
|
|
165
|
-
`Agent: ${input.agent}`,
|
|
166
|
-
`Child index: ${input.childIndex}`,
|
|
167
|
-
];
|
|
168
|
-
if (input.childTarget) lines.push(`Child intercom target: ${input.childTarget}`);
|
|
169
|
-
lines.push("");
|
|
170
|
-
if (input.message?.trim()) lines.push(input.message.trim());
|
|
171
|
-
if (input.reason === "interview_request") {
|
|
172
|
-
lines.push(
|
|
173
|
-
"",
|
|
174
|
-
"Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.",
|
|
175
|
-
);
|
|
176
|
-
if (input.interview !== undefined) lines.push(JSON.stringify(input.interview, null, "\t"));
|
|
177
|
-
}
|
|
178
|
-
return lines.join("\n").trimEnd();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
131
|
function parseStructuredReply(message: string): { value?: unknown; error?: string } {
|
|
182
132
|
const trimmed = message.trim();
|
|
183
133
|
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1]?.trim();
|
|
@@ -231,11 +181,9 @@ async function waitForReply(channelDir: string, requestId: string, deadline: num
|
|
|
231
181
|
throw new Error("Timed out waiting for supervisor reply.");
|
|
232
182
|
}
|
|
233
183
|
|
|
234
|
-
async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: AbortSignal): Promise<AgentToolResult<Record<string, unknown>>> {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (params.reason !== "progress_update" && !params.message?.trim() && params.reason !== "interview_request") {
|
|
238
|
-
throw new Error("message is required for supervisor decisions.");
|
|
184
|
+
async function sendSupervisorRequest(params: ContactSupervisorParams, metadata: ChildSupervisorMetadata, signal?: AbortSignal, toolCallId?: string): Promise<AgentToolResult<Record<string, unknown>>> {
|
|
185
|
+
if (!params.message?.trim() && params.reason !== "interview_request") {
|
|
186
|
+
throw new Error("message is required for supervisor decisions and progress updates.");
|
|
239
187
|
}
|
|
240
188
|
ensureSupervisorChannelDir(metadata.channelDir);
|
|
241
189
|
const requestId = randomUUID();
|
|
@@ -243,7 +191,8 @@ async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: A
|
|
|
243
191
|
const createdAt = Date.now();
|
|
244
192
|
const replyDeadline = createdAt + askTimeoutMs();
|
|
245
193
|
const expiresAt = expectsReply ? replyDeadline : undefined;
|
|
246
|
-
const message =
|
|
194
|
+
const message = params.message?.trim() ?? "";
|
|
195
|
+
const requestToolCallId = typeof toolCallId === "string" && toolCallId.length > 0 ? toolCallId : undefined;
|
|
247
196
|
const request: SupervisorRequest = {
|
|
248
197
|
type: "subagent.supervisor.request",
|
|
249
198
|
id: requestId,
|
|
@@ -257,6 +206,7 @@ async function sendSupervisorRequest(params: ContactSupervisorParams, signal?: A
|
|
|
257
206
|
runId: metadata.runId,
|
|
258
207
|
agent: metadata.agent,
|
|
259
208
|
childIndex: metadata.childIndex,
|
|
209
|
+
...(requestToolCallId ? { toolCallId: requestToolCallId } : {}),
|
|
260
210
|
...(metadata.childTarget ? { childTarget: metadata.childTarget } : {}),
|
|
261
211
|
...(params.interview !== undefined ? { interview: params.interview } : {}),
|
|
262
212
|
};
|
|
@@ -297,15 +247,19 @@ function hasTool(pi: ExtensionAPI, name: string): boolean {
|
|
|
297
247
|
}
|
|
298
248
|
}
|
|
299
249
|
|
|
300
|
-
|
|
301
|
-
|
|
250
|
+
/**
|
|
251
|
+
* Register the child-side `contact_supervisor` tool. The host passes the
|
|
252
|
+
* channel metadata in the child runtime config.
|
|
253
|
+
*/
|
|
254
|
+
export function registerNativeSupervisorClient(pi: ExtensionAPI, metadata: ChildSupervisorMetadata | undefined): void {
|
|
255
|
+
if (!metadata || hasTool(pi, "contact_supervisor")) return;
|
|
302
256
|
const tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {
|
|
303
257
|
name: "contact_supervisor",
|
|
304
258
|
label: "Contact Supervisor",
|
|
305
259
|
description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
|
|
306
260
|
parameters: ContactSupervisorParamsSchema,
|
|
307
|
-
execute(
|
|
308
|
-
return sendSupervisorRequest(params as ContactSupervisorParams, signal);
|
|
261
|
+
execute(id, params, signal) {
|
|
262
|
+
return sendSupervisorRequest(params as ContactSupervisorParams, metadata, signal, id);
|
|
309
263
|
},
|
|
310
264
|
};
|
|
311
265
|
pi.registerTool(tool);
|
|
@@ -317,9 +271,14 @@ function parseRequestFile(file: string, channelDir: string): PendingSupervisorRe
|
|
|
317
271
|
if (parsed.type !== "subagent.supervisor.request") return undefined;
|
|
318
272
|
if (typeof parsed.id !== "string" || !parsed.id) return undefined;
|
|
319
273
|
if (parsed.reason !== "need_decision" && parsed.reason !== "interview_request" && parsed.reason !== "progress_update") return undefined;
|
|
320
|
-
if (typeof parsed.message !== "string" || !parsed.message) return undefined;
|
|
274
|
+
if (typeof parsed.message !== "string" || (!parsed.message.trim() && parsed.reason !== "interview_request")) return undefined;
|
|
321
275
|
if (typeof parsed.runId !== "string" || typeof parsed.agent !== "string" || typeof parsed.childIndex !== "number") return undefined;
|
|
322
|
-
return {
|
|
276
|
+
return {
|
|
277
|
+
...parsed as SupervisorRequest,
|
|
278
|
+
...(typeof parsed.toolCallId === "string" && parsed.toolCallId.length > 0 ? { toolCallId: parsed.toolCallId } : { toolCallId: undefined }),
|
|
279
|
+
channelDir,
|
|
280
|
+
requestFile: file,
|
|
281
|
+
};
|
|
323
282
|
} catch {
|
|
324
283
|
return undefined;
|
|
325
284
|
}
|
|
@@ -483,6 +442,7 @@ function removeRequestFile(file: string): void {
|
|
|
483
442
|
}
|
|
484
443
|
|
|
485
444
|
type SupervisorRequestLifecycle = "pending" | "resolved" | "expired" | "inactive" | "missing" | "wrong-session";
|
|
445
|
+
type SupervisorRequestLifecycleObserver = (request: PendingSupervisorRequest, lifecycle: SupervisorRequestLifecycle) => void;
|
|
486
446
|
|
|
487
447
|
function requestExpiresAt(request: SupervisorRequest, now: number): number {
|
|
488
448
|
const expiresAt = (request as { expiresAt?: unknown }).expiresAt;
|
|
@@ -515,30 +475,44 @@ function cleanupRequestLifecycle(request: PendingSupervisorRequest, lifecycle: S
|
|
|
515
475
|
if (lifecycle === "resolved" || lifecycle === "expired" || lifecycle === "inactive") removeRequestFile(request.requestFile);
|
|
516
476
|
}
|
|
517
477
|
|
|
518
|
-
function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, ctx: ExtensionContext | undefined): void {
|
|
478
|
+
function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, ctx: ExtensionContext | undefined, onLifecycle: SupervisorRequestLifecycleObserver): void {
|
|
519
479
|
const now = Date.now();
|
|
520
480
|
for (const request of pending.values()) {
|
|
521
481
|
const lifecycle = requestLifecycle(request, state, ctx, now);
|
|
522
482
|
if (lifecycle === "pending") continue;
|
|
523
483
|
pending.delete(request.id);
|
|
484
|
+
onLifecycle(request, lifecycle);
|
|
524
485
|
cleanupRequestLifecycle(request, lifecycle);
|
|
525
486
|
}
|
|
526
487
|
}
|
|
527
488
|
|
|
528
489
|
function formatPendingLine(request: PendingSupervisorRequest): string {
|
|
529
|
-
const replyHint = request.expectsReply ? ` Reply: ${
|
|
490
|
+
const replyHint = request.expectsReply ? ` Reply: ${supervisorReplyHint(request.id)}` : "";
|
|
530
491
|
return `- ${request.id}: ${request.agent} [${request.runId}#${request.childIndex}] ${request.reason}.${replyHint}`;
|
|
531
492
|
}
|
|
532
493
|
|
|
533
494
|
function requestVisibleText(request: PendingSupervisorRequest): string {
|
|
534
|
-
const lines = [
|
|
535
|
-
|
|
536
|
-
|
|
495
|
+
const lines = [
|
|
496
|
+
reasonHeading(request.reason),
|
|
497
|
+
`Run: ${request.runId}`,
|
|
498
|
+
`Agent: ${request.agent}`,
|
|
499
|
+
`Child index: ${request.childIndex}`,
|
|
500
|
+
];
|
|
501
|
+
if (request.childTarget) lines.push(`Child intercom target: ${request.childTarget}`);
|
|
502
|
+
lines.push("");
|
|
503
|
+
if (request.message) lines.push(request.message);
|
|
504
|
+
if (request.reason === "interview_request") {
|
|
505
|
+
lines.push(
|
|
506
|
+
"",
|
|
507
|
+
"Structured response requested. Reply with JSON, optionally fenced in ```json, matching the requested interview shape.",
|
|
508
|
+
);
|
|
509
|
+
if (request.interview !== undefined) lines.push(JSON.stringify(request.interview, null, "\t"));
|
|
537
510
|
}
|
|
538
|
-
|
|
511
|
+
if (request.expectsReply) lines.push("", `Reply with: ${supervisorReplyHint(request.id)}`);
|
|
512
|
+
return lines.join("\n").trimEnd();
|
|
539
513
|
}
|
|
540
514
|
|
|
541
|
-
function writeReply(request: PendingSupervisorRequest, message: string):
|
|
515
|
+
function writeReply(request: PendingSupervisorRequest, message: string): SupervisorReply {
|
|
542
516
|
if (!message.trim()) throw new Error("message is required for supervisor replies.");
|
|
543
517
|
const reply: SupervisorReply = {
|
|
544
518
|
type: "subagent.supervisor.reply",
|
|
@@ -548,6 +522,29 @@ function writeReply(request: PendingSupervisorRequest, message: string): void {
|
|
|
548
522
|
};
|
|
549
523
|
writeAtomicJson(replyPath(request.channelDir, request.id), reply);
|
|
550
524
|
removeRequestFile(request.requestFile);
|
|
525
|
+
return reply;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function appendSupervisorReplyEntry(pi: ExtensionAPI, request: PendingSupervisorRequest, reply: SupervisorReply): void {
|
|
529
|
+
const appendEntry = (pi as unknown as {
|
|
530
|
+
appendEntry?: (customType: string, data?: SupervisorReplyEntryData) => void;
|
|
531
|
+
}).appendEntry;
|
|
532
|
+
if (typeof appendEntry !== "function") return;
|
|
533
|
+
try {
|
|
534
|
+
appendEntry.call(pi, SUPERVISOR_REPLY_ENTRY_TYPE, {
|
|
535
|
+
requestId: request.id,
|
|
536
|
+
reason: request.reason,
|
|
537
|
+
runId: request.runId,
|
|
538
|
+
agent: request.agent,
|
|
539
|
+
childIndex: request.childIndex,
|
|
540
|
+
...(request.childTarget ? { childTarget: request.childTarget } : {}),
|
|
541
|
+
message: reply.message,
|
|
542
|
+
createdAt: reply.createdAt,
|
|
543
|
+
});
|
|
544
|
+
} catch (error) {
|
|
545
|
+
// The reply file is authoritative; a UI journal failure must not undo a delivered reply.
|
|
546
|
+
console.error("Failed to journal native supervisor reply:", error);
|
|
547
|
+
}
|
|
551
548
|
}
|
|
552
549
|
|
|
553
550
|
function resolvePendingRequest(pending: Map<string, PendingSupervisorRequest>, params: IntercomParams): PendingSupervisorRequest {
|
|
@@ -583,14 +580,14 @@ function publicPendingRequests(pending: Map<string, PendingSupervisorRequest>):
|
|
|
583
580
|
}));
|
|
584
581
|
}
|
|
585
582
|
|
|
586
|
-
function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
|
|
583
|
+
function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, PendingSupervisorRequest>, state: SubagentState, onLifecycle: SupervisorRequestLifecycleObserver): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
|
|
587
584
|
return {
|
|
588
585
|
name: NATIVE_SUPERVISOR_TOOL_NAME,
|
|
589
586
|
label: "Subagent Supervisor",
|
|
590
587
|
description: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
|
|
591
588
|
parameters: IntercomParamsSchema,
|
|
592
589
|
async execute(_id, params) {
|
|
593
|
-
refreshPendingRequests(pending, state, state.lastUiContext ?? undefined);
|
|
590
|
+
refreshPendingRequests(pending, state, state.lastUiContext ?? undefined, onLifecycle);
|
|
594
591
|
const input = params as IntercomParams;
|
|
595
592
|
if (input.action === "status") {
|
|
596
593
|
return { content: [{ type: "text", text: `Native supervisor channel active. Pending replies: ${pending.size}.` }], details: { active: true, pending: pending.size, root: SUPERVISOR_CHANNEL_ROOT } };
|
|
@@ -601,7 +598,9 @@ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest
|
|
|
601
598
|
}
|
|
602
599
|
if (input.action === "reply") {
|
|
603
600
|
const request = resolvePendingRequest(pending, input);
|
|
604
|
-
writeReply(request, input.message ?? "");
|
|
601
|
+
const reply = writeReply(request, input.message ?? "");
|
|
602
|
+
appendSupervisorReplyEntry(pi, request, reply);
|
|
603
|
+
onLifecycle(request, "resolved");
|
|
605
604
|
pending.delete(request.id);
|
|
606
605
|
clearForegroundSupervisorAttention(request, pending, state);
|
|
607
606
|
return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
|
|
@@ -614,10 +613,66 @@ function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest
|
|
|
614
613
|
};
|
|
615
614
|
}
|
|
616
615
|
|
|
617
|
-
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
|
|
616
|
+
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
|
|
617
|
+
start: () => void;
|
|
618
|
+
activateTransport: () => void;
|
|
619
|
+
dispose: () => void;
|
|
620
|
+
pending: Map<string, PendingSupervisorRequest>;
|
|
621
|
+
getSupervisorRequestState: (event: ControlEvent) => SupervisorRequestState;
|
|
622
|
+
} {
|
|
618
623
|
const watch = deps.watch ?? fs.watch;
|
|
619
624
|
const timers = deps.timers ?? globalThis;
|
|
620
625
|
const pending = new Map<string, PendingSupervisorRequest>();
|
|
626
|
+
const requestCorrelations = new Map<string, SupervisorRequestCorrelation>();
|
|
627
|
+
const correlationKey = (request: { runId: string; agent: string; childIndex: number; toolCallId?: string }): string | undefined => {
|
|
628
|
+
if (!request.toolCallId) return undefined;
|
|
629
|
+
return JSON.stringify([request.runId, request.agent, request.childIndex, request.toolCallId]);
|
|
630
|
+
};
|
|
631
|
+
const pruneRequestCorrelations = (now = Date.now()): void => {
|
|
632
|
+
for (const [key, correlation] of requestCorrelations) {
|
|
633
|
+
if (correlation.state === "resolved" && now - correlation.updatedAt > SUPERVISOR_REQUEST_CORRELATION_RETENTION_MS) requestCorrelations.delete(key);
|
|
634
|
+
}
|
|
635
|
+
while (requestCorrelations.size > MAX_SUPERVISOR_REQUEST_CORRELATIONS) {
|
|
636
|
+
const oldest = requestCorrelations.keys().next().value;
|
|
637
|
+
if (oldest === undefined) break;
|
|
638
|
+
requestCorrelations.delete(oldest);
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const rememberPendingRequest = (request: PendingSupervisorRequest): void => {
|
|
642
|
+
const key = correlationKey(request);
|
|
643
|
+
if (!key || !request.expectsReply) return;
|
|
644
|
+
requestCorrelations.set(key, { request, state: "pending", updatedAt: Date.now() });
|
|
645
|
+
pruneRequestCorrelations();
|
|
646
|
+
};
|
|
647
|
+
const rememberResolvedRequest = (request: PendingSupervisorRequest): void => {
|
|
648
|
+
const key = correlationKey(request);
|
|
649
|
+
if (!key || !request.expectsReply) return;
|
|
650
|
+
requestCorrelations.set(key, { request, state: "resolved", updatedAt: Date.now() });
|
|
651
|
+
pruneRequestCorrelations();
|
|
652
|
+
};
|
|
653
|
+
const observeRequestLifecycle: SupervisorRequestLifecycleObserver = (request, lifecycle) => {
|
|
654
|
+
if (lifecycle !== "wrong-session") rememberResolvedRequest(request);
|
|
655
|
+
};
|
|
656
|
+
const getSupervisorRequestState = (event: ControlEvent): SupervisorRequestState => {
|
|
657
|
+
if (event.currentTool === "intercom" || event.index === undefined) return "unknown";
|
|
658
|
+
const toolCallId = typeof event.toolCallId === "string" && event.toolCallId.length > 0 ? event.toolCallId : undefined;
|
|
659
|
+
if (!toolCallId) return "unknown";
|
|
660
|
+
pruneRequestCorrelations();
|
|
661
|
+
const key = correlationKey({ runId: event.runId, agent: event.agent, childIndex: event.index, toolCallId });
|
|
662
|
+
if (!key) return "unknown";
|
|
663
|
+
const correlation = requestCorrelations.get(key);
|
|
664
|
+
if (!correlation) return "unknown";
|
|
665
|
+
if (correlation.state === "resolved") return "resolved";
|
|
666
|
+
const now = Date.now();
|
|
667
|
+
if (!fs.existsSync(correlation.request.requestFile)
|
|
668
|
+
|| fs.existsSync(replyPath(correlation.request.channelDir, correlation.request.id))
|
|
669
|
+
|| now > requestExpiresAt(correlation.request, now)
|
|
670
|
+
|| requestRunInactive(correlation.request, state)) {
|
|
671
|
+
rememberResolvedRequest(correlation.request);
|
|
672
|
+
return "resolved";
|
|
673
|
+
}
|
|
674
|
+
return "pending";
|
|
675
|
+
};
|
|
621
676
|
const seenFiles = new Set<string>();
|
|
622
677
|
const requestWatchers = new Map<string, fs.FSWatcher>();
|
|
623
678
|
let rootWatcher: fs.FSWatcher | undefined;
|
|
@@ -635,7 +690,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
635
690
|
};
|
|
636
691
|
|
|
637
692
|
const registerParentTools = (): void => {
|
|
638
|
-
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pending, state));
|
|
693
|
+
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pi, pending, state, observeRequestLifecycle));
|
|
639
694
|
};
|
|
640
695
|
|
|
641
696
|
const cleanupStaleChannelsIfDue = (): void => {
|
|
@@ -653,7 +708,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
653
708
|
cleanupStaleChannelsIfDue();
|
|
654
709
|
const ctx = state.lastUiContext;
|
|
655
710
|
if (!ctx) return;
|
|
656
|
-
refreshPendingRequests(pending, state, ctx);
|
|
711
|
+
refreshPendingRequests(pending, state, ctx, observeRequestLifecycle);
|
|
657
712
|
const now = Date.now();
|
|
658
713
|
for (const { channelDir, file } of listRequestFiles()) {
|
|
659
714
|
if (seenFiles.has(file)) continue;
|
|
@@ -662,11 +717,13 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
662
717
|
const lifecycle = requestLifecycle(request, state, undefined, now);
|
|
663
718
|
if (lifecycle !== "pending") {
|
|
664
719
|
seenFiles.add(file);
|
|
720
|
+
observeRequestLifecycle(request, lifecycle);
|
|
665
721
|
cleanupRequestLifecycle(request, lifecycle);
|
|
666
722
|
continue;
|
|
667
723
|
}
|
|
668
724
|
seenFiles.add(file);
|
|
669
725
|
if (request.expectsReply) {
|
|
726
|
+
rememberPendingRequest(request);
|
|
670
727
|
pending.set(request.id, request);
|
|
671
728
|
markForegroundSupervisorAttention(request, state);
|
|
672
729
|
}
|
|
@@ -674,16 +731,21 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
674
731
|
removeRequestFile(request.requestFile);
|
|
675
732
|
}
|
|
676
733
|
pi.sendMessage({
|
|
677
|
-
customType:
|
|
734
|
+
customType: SUPERVISOR_REQUEST_MESSAGE_TYPE,
|
|
678
735
|
content: requestVisibleText(request),
|
|
679
736
|
display: true,
|
|
680
737
|
details: {
|
|
681
738
|
id: request.id,
|
|
739
|
+
requestId: request.id,
|
|
682
740
|
reason: request.reason,
|
|
683
741
|
expectsReply: request.expectsReply,
|
|
684
742
|
runId: request.runId,
|
|
685
743
|
agent: request.agent,
|
|
686
744
|
childIndex: request.childIndex,
|
|
745
|
+
...(request.childTarget ? { childTarget: request.childTarget } : {}),
|
|
746
|
+
...(request.interview !== undefined ? { interview: request.interview } : {}),
|
|
747
|
+
requestBody: request.message,
|
|
748
|
+
...(request.expectsReply ? { replyHint: supervisorReplyHint(request.id) } : {}),
|
|
687
749
|
},
|
|
688
750
|
}, { triggerTurn: true });
|
|
689
751
|
if (request.expectsReply) {
|
|
@@ -805,8 +867,10 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
805
867
|
if (deferredWatcherRefresh) timers.clearImmediate(deferredWatcherRefresh);
|
|
806
868
|
deferredWatcherRefresh = undefined;
|
|
807
869
|
pending.clear();
|
|
870
|
+
requestCorrelations.clear();
|
|
808
871
|
seenFiles.clear();
|
|
809
872
|
},
|
|
810
873
|
pending,
|
|
874
|
+
getSupervisorRequestState,
|
|
811
875
|
};
|
|
812
876
|
}
|