pi-subagents 0.65.1 → 0.67.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/CHANGELOG.md +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +27 -19
- package/src/tui/render.ts +172 -33
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -2,13 +2,13 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as readline from "node:readline";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { parseMissionRecord } from "
|
|
6
|
-
import type { MissionRecord } from "
|
|
7
|
-
import { requestAsyncSteer, requestAsyncStop } from "
|
|
8
|
-
import { formatAsyncRunTranscript } from "
|
|
9
|
-
import { steeringReceipt } from "
|
|
10
|
-
import type { AsyncStatus } from "
|
|
11
|
-
import { readStatus } from "
|
|
5
|
+
import { parseMissionRecord } from "../missions/store.ts";
|
|
6
|
+
import type { MissionRecord } from "../missions/types.ts";
|
|
7
|
+
import { requestAsyncSteer, requestAsyncStop } from "../runs/background/control-channel.ts";
|
|
8
|
+
import { formatAsyncRunTranscript } from "../runs/background/fleet-view.ts";
|
|
9
|
+
import { steeringReceipt } from "../runs/background/steering.ts";
|
|
10
|
+
import type { AsyncStatus } from "../shared/types.ts";
|
|
11
|
+
import { readStatus } from "../shared/utils.ts";
|
|
12
12
|
import { decodeSessionRoots } from "./session-roots-codec.ts";
|
|
13
13
|
|
|
14
14
|
export interface RunnerOptions {
|
|
@@ -31,7 +31,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
|
|
|
31
31
|
const { status, asyncDir, mission } = input;
|
|
32
32
|
const lines = [
|
|
33
33
|
`pi-subagents inspector for ${status.runId}`,
|
|
34
|
-
"This
|
|
34
|
+
"This inspector mirrors lifecycle artifacts; closing it does not stop the run.",
|
|
35
35
|
"",
|
|
36
36
|
];
|
|
37
37
|
if (mission) {
|
|
@@ -43,7 +43,7 @@ export function formatInspectorDashboard(input: { status: AsyncStatus; asyncDir:
|
|
|
43
43
|
lines.push(formatAsyncRunTranscript(status, asyncDir, { index: input.index, lines: 60, sessionRoots: input.sessionRoots }));
|
|
44
44
|
const acceptsPlainGuidance = input.index !== undefined || status.mode === "single";
|
|
45
45
|
const controls = [input.allowSteer === false || !acceptsPlainGuidance ? undefined : "type guidance", input.allowSteer === false ? undefined : "steer <message>", input.allowStop === false ? undefined : "stop", "status"].filter(Boolean);
|
|
46
|
-
lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom).");
|
|
46
|
+
lines.push("", `Controls: ${controls.join(" | ")}`, "Supervisor replies remain in the parent Pi session (subagent_supervisor/intercom); this inspector is read-only.");
|
|
47
47
|
return lines.join("\n");
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -91,7 +91,7 @@ function queueInspectorSteer(options: RunnerOptions, status: AsyncStatus, messag
|
|
|
91
91
|
requestAsyncSteer(options.asyncDir, {
|
|
92
92
|
message,
|
|
93
93
|
...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
|
|
94
|
-
source: "
|
|
94
|
+
source: "inspector-runner",
|
|
95
95
|
});
|
|
96
96
|
return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
|
|
97
97
|
}
|
|
@@ -104,7 +104,7 @@ export function submitInspectorControl(options: RunnerOptions, line: string): st
|
|
|
104
104
|
if (command === "stop") {
|
|
105
105
|
if (options.allowStop === false) throw new Error("Authority policy does not allow stop from this inspector.");
|
|
106
106
|
if (isTerminal(status)) throw new Error(`Run '${options.runId}' is ${status.state} and cannot be stopped.`);
|
|
107
|
-
requestAsyncStop(options.asyncDir, { source: "
|
|
107
|
+
requestAsyncStop(options.asyncDir, { source: "inspector-runner" });
|
|
108
108
|
return `Stop requested for run ${options.runId}.`;
|
|
109
109
|
}
|
|
110
110
|
if (command.startsWith("steer ")) {
|
|
@@ -147,7 +147,7 @@ export function runInspector(argv = process.argv.slice(2)): void {
|
|
|
147
147
|
|
|
148
148
|
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
|
|
149
149
|
try { runInspector(); } catch (cause) {
|
|
150
|
-
process.stderr.write(`
|
|
150
|
+
process.stderr.write(`Inspector failed: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
151
151
|
process.exitCode = 1;
|
|
152
152
|
}
|
|
153
153
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createHerdrInspectorPlugin } from "./herdr/plugin.ts";
|
|
2
|
+
import { createGhosttyInspectorPlugin } from "./ghostty/plugin.ts";
|
|
3
|
+
import type { InspectorPlugin } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
/** Built-in inspector plugins, ordered by host preference. */
|
|
6
|
+
export function createBuiltinInspectorPlugins(): readonly InspectorPlugin[] {
|
|
7
|
+
return [createHerdrInspectorPlugin(), createGhosttyInspectorPlugin()];
|
|
8
|
+
}
|
|
@@ -19,24 +19,13 @@ function parseStringArray(value: unknown): string[] | undefined {
|
|
|
19
19
|
return value;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Decodes a `--session-roots` argument produced by {@link encodeSessionRoots}.
|
|
24
|
-
* Falls back to parsing the value as raw JSON so any externally-launched
|
|
25
|
-
* inspector runner (a cached copy, or a manual invocation) that still passes
|
|
26
|
-
* the legacy unencoded form keeps working.
|
|
27
|
-
*/
|
|
22
|
+
/** Decodes a `--session-roots` argument produced by {@link encodeSessionRoots}. */
|
|
28
23
|
export function decodeSessionRoots(raw: string): string[] {
|
|
29
24
|
try {
|
|
30
25
|
const decoded = parseStringArray(JSON.parse(Buffer.from(raw, "base64").toString("utf-8")));
|
|
31
26
|
if (decoded) return decoded;
|
|
32
27
|
} catch {
|
|
33
|
-
//
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
const parsed = parseStringArray(JSON.parse(raw));
|
|
37
|
-
if (parsed) return parsed;
|
|
38
|
-
} catch {
|
|
39
|
-
// fall through to the shared error below
|
|
28
|
+
// Report one stable validation error below.
|
|
40
29
|
}
|
|
41
|
-
throw new Error("--session-roots must be a base64-encoded
|
|
30
|
+
throw new Error("--session-roots must be a base64-encoded JSON array of strings.");
|
|
42
31
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { Details } from "../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
export const INSPECTOR_ACTIONS = ["inspector.open", "inspector.command", "inspector.status", "inspector.close"] as const;
|
|
5
|
+
export type InspectorAction = typeof INSPECTOR_ACTIONS[number];
|
|
6
|
+
|
|
7
|
+
export interface InspectorParams {
|
|
8
|
+
id?: string;
|
|
9
|
+
runId?: string;
|
|
10
|
+
dir?: string;
|
|
11
|
+
index?: number;
|
|
12
|
+
focus?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface InspectorTarget {
|
|
16
|
+
runId: string;
|
|
17
|
+
asyncDir: string;
|
|
18
|
+
index?: number;
|
|
19
|
+
status: {
|
|
20
|
+
cwd?: string;
|
|
21
|
+
state: string;
|
|
22
|
+
steps?: unknown[];
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface InspectorContext {
|
|
27
|
+
cwd: string;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
env: NodeJS.ProcessEnv;
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
target: InspectorTarget;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface InspectorLaunch {
|
|
35
|
+
executable: string;
|
|
36
|
+
argv: string[];
|
|
37
|
+
displayCommand: string;
|
|
38
|
+
mission?: { id: string; path: string };
|
|
39
|
+
allowSteer: boolean;
|
|
40
|
+
allowStop: boolean;
|
|
41
|
+
sessionRoots: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface InspectorPlugin {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
available(context: InspectorContext): Promise<boolean> | boolean;
|
|
47
|
+
owns(context: InspectorContext): boolean;
|
|
48
|
+
open(context: InspectorContext, launch: InspectorLaunch, params: InspectorParams): Promise<AgentToolResult<Details>>;
|
|
49
|
+
status?(context: InspectorContext): Promise<AgentToolResult<Details>>;
|
|
50
|
+
close?(context: InspectorContext): Promise<AgentToolResult<Details>>;
|
|
51
|
+
}
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "../agents/agents.ts";
|
|
5
|
+
import { agentDefinitionDigest } from "../shared/launch-contract.ts";
|
|
5
6
|
import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
|
|
6
7
|
import { getAgentDir } from "../shared/utils.ts";
|
|
7
8
|
|
|
@@ -18,9 +19,13 @@ function defaultSubagentConfigDir(agentDir = defaultAgentDir()): string {
|
|
|
18
19
|
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
19
20
|
export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
|
|
20
21
|
export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
22
|
+
const ORCHESTRATOR_TARGET_PLACEHOLDER = "{orchestratorTarget}";
|
|
23
|
+
// The default template must stay session-independent: the child reads the
|
|
24
|
+
// supervisor target from its runtime config, and a prompt that names the
|
|
25
|
+
// parent session would make the launch digest vary per session (#2127).
|
|
21
26
|
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the supervisor in normal assistant text.
|
|
22
27
|
|
|
23
|
-
Use contact_supervisor first. It resolves the supervisor session
|
|
28
|
+
Use contact_supervisor first. It resolves the supervisor session and run metadata automatically.
|
|
24
29
|
- Need a decision, blocked, approval, or product/API/scope ambiguity: contact_supervisor({ reason: "need_decision", message: "<question>" })
|
|
25
30
|
- Need structured supervisor input rather than a freeform reply: contact_supervisor({ reason: "interview_request", message: "<what input is needed>", interview: { title: "...", questions: [] } })
|
|
26
31
|
- After contact_supervisor with reason "need_decision" or "interview_request", stay alive and continue only after the reply arrives. Do not finish your final response with a choose-one question.
|
|
@@ -36,6 +41,32 @@ export interface IntercomBridgeState {
|
|
|
36
41
|
orchestratorTarget?: string;
|
|
37
42
|
extensionDir: string;
|
|
38
43
|
instruction: string;
|
|
44
|
+
/** True when the instruction template names the supervisor session, which ties the child prompt to the parent session. */
|
|
45
|
+
interpolatesOrchestratorTarget: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type IntercomBridgeConfigValidation =
|
|
49
|
+
| { ok: true; value: IntercomBridgeConfig }
|
|
50
|
+
| { ok: false; error: string };
|
|
51
|
+
|
|
52
|
+
/** Validates untrusted bridge config from descriptors or delegation requests; `label` prefixes each error. */
|
|
53
|
+
export function validateIntercomBridgeConfig({ value, label }: { value: unknown; label: string }): IntercomBridgeConfigValidation {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, error: `${label} must be an object.` };
|
|
55
|
+
const bridge = value as Record<string, unknown>;
|
|
56
|
+
for (const field of Object.keys(bridge)) {
|
|
57
|
+
if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") return { ok: false, error: `${label}.${field} is not supported.` };
|
|
58
|
+
}
|
|
59
|
+
if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") return { ok: false, error: `${label}.mode is invalid.` };
|
|
60
|
+
if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") return { ok: false, error: `${label}.instructionFile must be a string.` };
|
|
61
|
+
if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") return { ok: false, error: `${label}.resultDelivery must be a boolean.` };
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
value: {
|
|
65
|
+
...(bridge.mode !== undefined ? { mode: bridge.mode as IntercomBridgeMode } : {}),
|
|
66
|
+
...(bridge.instructionFile !== undefined ? { instructionFile: bridge.instructionFile as string } : {}),
|
|
67
|
+
...(bridge.resultDelivery !== undefined ? { resultDelivery: bridge.resultDelivery as boolean } : {}),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
39
70
|
}
|
|
40
71
|
|
|
41
72
|
export interface IntercomBridgeDiagnostic {
|
|
@@ -114,7 +145,7 @@ function resolveInstructionTemplate(instructionFile: string, settingsDir: string
|
|
|
114
145
|
}
|
|
115
146
|
|
|
116
147
|
function buildIntercomBridgeInstruction(orchestratorTarget: string, template: string): string {
|
|
117
|
-
const instruction = template.replaceAll(
|
|
148
|
+
const instruction = template.replaceAll(ORCHESTRATOR_TARGET_PLACEHOLDER, orchestratorTarget).trim();
|
|
118
149
|
if (instruction.startsWith(INTERCOM_BRIDGE_MARKER)) return instruction;
|
|
119
150
|
return `${INTERCOM_BRIDGE_MARKER}\n${instruction}`;
|
|
120
151
|
}
|
|
@@ -149,24 +180,34 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
|
|
|
149
180
|
const orchestratorTarget = input.orchestratorTarget?.trim();
|
|
150
181
|
const agentDir = path.resolve(input.agentDir ?? defaultAgentDir());
|
|
151
182
|
const settingsDir = path.resolve(input.settingsDir ?? defaultSubagentConfigDir(agentDir));
|
|
152
|
-
const defaultInstruction = buildIntercomBridgeInstruction(
|
|
153
|
-
orchestratorTarget || "{orchestratorTarget}",
|
|
154
|
-
DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
|
|
155
|
-
);
|
|
156
183
|
const reason = inactiveReason(mode, input.context, orchestratorTarget);
|
|
157
184
|
if (reason || !orchestratorTarget) {
|
|
158
|
-
return {
|
|
185
|
+
return {
|
|
186
|
+
active: false,
|
|
187
|
+
mode,
|
|
188
|
+
resultDelivery: config.resultDelivery,
|
|
189
|
+
extensionDir: NATIVE_INTERCOM_EXTENSION_DIR,
|
|
190
|
+
instruction: buildIntercomBridgeInstruction(ORCHESTRATOR_TARGET_PLACEHOLDER, DEFAULT_INTERCOM_BRIDGE_TEMPLATE),
|
|
191
|
+
interpolatesOrchestratorTarget: false,
|
|
192
|
+
};
|
|
159
193
|
}
|
|
194
|
+
const template = resolveInstructionTemplate(config.instructionFile, settingsDir);
|
|
160
195
|
return {
|
|
161
196
|
active: true,
|
|
162
197
|
mode,
|
|
163
198
|
resultDelivery: config.resultDelivery,
|
|
164
199
|
orchestratorTarget,
|
|
165
200
|
extensionDir: NATIVE_INTERCOM_EXTENSION_DIR,
|
|
166
|
-
instruction: buildIntercomBridgeInstruction(orchestratorTarget,
|
|
201
|
+
instruction: buildIntercomBridgeInstruction(orchestratorTarget, template),
|
|
202
|
+
interpolatesOrchestratorTarget: template.includes(ORCHESTRATOR_TARGET_PLACEHOLDER),
|
|
167
203
|
};
|
|
168
204
|
}
|
|
169
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Rewrites the launch prompt and tools for an active bridge. The parsed
|
|
208
|
+
* definition digest is captured first so launch identity keeps describing the
|
|
209
|
+
* agent file rather than this runtime overlay.
|
|
210
|
+
*/
|
|
170
211
|
export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomBridgeState): AgentConfig {
|
|
171
212
|
if (!bridge.active || !bridge.orchestratorTarget) return agent;
|
|
172
213
|
|
|
@@ -185,6 +226,7 @@ export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomB
|
|
|
185
226
|
if (tools === agent.tools && systemPrompt === agent.systemPrompt) return agent;
|
|
186
227
|
return {
|
|
187
228
|
...agent,
|
|
229
|
+
definitionDigest: agentDefinitionDigest(agent),
|
|
188
230
|
tools,
|
|
189
231
|
systemPrompt,
|
|
190
232
|
};
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
5
|
-
import type { ExtensionAPI,
|
|
5
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
import type { ChildSupervisorMetadata } from "../runs/shared/child-runtime-config.ts";
|
|
8
8
|
import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type ControlEvent, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
|
|
@@ -74,7 +74,7 @@ interface ContactSupervisorParams {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
interface IntercomParams {
|
|
77
|
-
action: "list" | "
|
|
77
|
+
action: "list" | "pending" | "status" | "reply";
|
|
78
78
|
to?: string;
|
|
79
79
|
message?: string;
|
|
80
80
|
replyTo?: string;
|
|
@@ -83,6 +83,10 @@ interface IntercomParams {
|
|
|
83
83
|
type SupervisorWatch = (filename: fs.PathLike, listener: fs.WatchListener<string>) => fs.FSWatcher;
|
|
84
84
|
|
|
85
85
|
interface NativeSupervisorChannelDeps {
|
|
86
|
+
/** Owned live/final-drain mailboxes. Only a completed poll retires the snapshot, never a demand probe. */
|
|
87
|
+
getChannelDirs?: () => { dirs: string[]; retire?: () => void };
|
|
88
|
+
/** Retained scheduled states for the current runtime owner, never foreign owners. */
|
|
89
|
+
getCurrentOwnerStates?: () => Iterable<SubagentState>;
|
|
86
90
|
platform?: NodeJS.Platform;
|
|
87
91
|
watch?: SupervisorWatch;
|
|
88
92
|
timers?: Pick<typeof globalThis, "setInterval" | "clearInterval" | "setImmediate" | "clearImmediate">;
|
|
@@ -95,7 +99,7 @@ const ContactSupervisorParamsSchema = Type.Object({
|
|
|
95
99
|
}, { additionalProperties: false });
|
|
96
100
|
|
|
97
101
|
const IntercomParamsSchema = Type.Object({
|
|
98
|
-
action: Type.String({ enum: ["list", "
|
|
102
|
+
action: Type.String({ enum: ["list", "pending", "status", "reply"] }),
|
|
99
103
|
to: Type.Optional(Type.String()),
|
|
100
104
|
message: Type.Optional(Type.String()),
|
|
101
105
|
replyTo: Type.Optional(Type.String()),
|
|
@@ -284,18 +288,18 @@ function parseRequestFile(file: string, channelDir: string): PendingSupervisorRe
|
|
|
284
288
|
}
|
|
285
289
|
}
|
|
286
290
|
|
|
287
|
-
function listRequestFiles(): Array<{ channelDir: string; file: string }> {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
function listRequestFiles(channelDirs?: string[]): Array<{ channelDir: string; file: string }> {
|
|
292
|
+
if (!channelDirs) {
|
|
293
|
+
try {
|
|
294
|
+
channelDirs = fs.readdirSync(SUPERVISOR_CHANNEL_ROOT, { withFileTypes: true })
|
|
295
|
+
.filter(entry => entry.isDirectory()).map(entry => path.join(SUPERVISOR_CHANNEL_ROOT, entry.name));
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
294
300
|
}
|
|
295
301
|
const files: Array<{ channelDir: string; file: string }> = [];
|
|
296
|
-
for (const
|
|
297
|
-
if (!entry.isDirectory()) continue;
|
|
298
|
-
const channelDir = path.join(SUPERVISOR_CHANNEL_ROOT, entry.name);
|
|
302
|
+
for (const channelDir of channelDirs) {
|
|
299
303
|
const requestsDir = path.join(channelDir, REQUESTS_DIR);
|
|
300
304
|
let requestEntries: fs.Dirent[];
|
|
301
305
|
try {
|
|
@@ -381,18 +385,8 @@ function cleanupStaleEmptySupervisorChannels(nowMs = Date.now()): number {
|
|
|
381
385
|
return removed;
|
|
382
386
|
}
|
|
383
387
|
|
|
384
|
-
function
|
|
385
|
-
|
|
386
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
387
|
-
if (sessionId) return sessionId;
|
|
388
|
-
} catch {
|
|
389
|
-
// Fall through to the last known identity.
|
|
390
|
-
}
|
|
391
|
-
return state.currentSessionId ?? undefined;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function requestMatchesContext(request: SupervisorRequest, state: Pick<SubagentState, "currentSessionId">, ctx: ExtensionContext): boolean {
|
|
395
|
-
const currentSessionId = currentContextSessionId(state, ctx);
|
|
388
|
+
function requestMatchesOwner(request: SupervisorRequest, state: Pick<SubagentState, "supervisorOwnerSessionId">): boolean {
|
|
389
|
+
const currentSessionId = state.supervisorOwnerSessionId;
|
|
396
390
|
return Boolean(currentSessionId && request.orchestratorSessionId === currentSessionId);
|
|
397
391
|
}
|
|
398
392
|
|
|
@@ -462,12 +456,12 @@ function requestRunInactive(request: SupervisorRequest, state: SubagentState): b
|
|
|
462
456
|
return stepStatus === "complete" || stepStatus === "completed" || stepStatus === "failed" || stepStatus === "paused";
|
|
463
457
|
}
|
|
464
458
|
|
|
465
|
-
function requestLifecycle(request: PendingSupervisorRequest, state: SubagentState,
|
|
466
|
-
if (
|
|
459
|
+
function requestLifecycle(request: PendingSupervisorRequest, state: SubagentState, now: number, runState: SubagentState): SupervisorRequestLifecycle {
|
|
460
|
+
if (!requestMatchesOwner(request, state)) return "wrong-session";
|
|
467
461
|
if (!fs.existsSync(request.requestFile)) return "missing";
|
|
468
462
|
if (request.expectsReply && fs.existsSync(replyPath(request.channelDir, request.id))) return "resolved";
|
|
469
463
|
if (request.expectsReply && now > requestExpiresAt(request, now)) return "expired";
|
|
470
|
-
if (request.expectsReply && requestRunInactive(request,
|
|
464
|
+
if (request.expectsReply && requestRunInactive(request, runState)) return "inactive";
|
|
471
465
|
return "pending";
|
|
472
466
|
}
|
|
473
467
|
|
|
@@ -475,10 +469,10 @@ function cleanupRequestLifecycle(request: PendingSupervisorRequest, lifecycle: S
|
|
|
475
469
|
if (lifecycle === "resolved" || lifecycle === "expired" || lifecycle === "inactive") removeRequestFile(request.requestFile);
|
|
476
470
|
}
|
|
477
471
|
|
|
478
|
-
function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState,
|
|
472
|
+
function refreshPendingRequests(pending: Map<string, PendingSupervisorRequest>, state: SubagentState, onLifecycle: SupervisorRequestLifecycleObserver, runState: (request: SupervisorRequest) => SubagentState): void {
|
|
479
473
|
const now = Date.now();
|
|
480
474
|
for (const request of pending.values()) {
|
|
481
|
-
const lifecycle = requestLifecycle(request, state,
|
|
475
|
+
const lifecycle = requestLifecycle(request, state, now, runState(request));
|
|
482
476
|
if (lifecycle === "pending") continue;
|
|
483
477
|
pending.delete(request.id);
|
|
484
478
|
onLifecycle(request, lifecycle);
|
|
@@ -498,7 +492,6 @@ function requestVisibleText(request: PendingSupervisorRequest): string {
|
|
|
498
492
|
`Agent: ${request.agent}`,
|
|
499
493
|
`Child index: ${request.childIndex}`,
|
|
500
494
|
];
|
|
501
|
-
if (request.childTarget) lines.push(`Child intercom target: ${request.childTarget}`);
|
|
502
495
|
lines.push("");
|
|
503
496
|
if (request.message) lines.push(request.message);
|
|
504
497
|
if (request.reason === "interview_request") {
|
|
@@ -509,6 +502,7 @@ function requestVisibleText(request: PendingSupervisorRequest): string {
|
|
|
509
502
|
if (request.interview !== undefined) lines.push(JSON.stringify(request.interview, null, "\t"));
|
|
510
503
|
}
|
|
511
504
|
if (request.expectsReply) lines.push("", `Reply with: ${supervisorReplyHint(request.id)}`);
|
|
505
|
+
lines.push("", `Live guidance: subagent({ action: "steer", id: ${JSON.stringify(request.runId)}, index: ${request.childIndex}, message: "..." })${request.expectsReply ? " (Reply to the pending request first.)" : ""}`);
|
|
512
506
|
return lines.join("\n").trimEnd();
|
|
513
507
|
}
|
|
514
508
|
|
|
@@ -563,6 +557,7 @@ function resolvePendingRequest(pending: Map<string, PendingSupervisorRequest>, p
|
|
|
563
557
|
);
|
|
564
558
|
if (matches.length === 1) return matches[0]!;
|
|
565
559
|
if (matches.length > 1) throw new Error(`Multiple pending supervisor requests match '${params.to}'. Use replyTo.`);
|
|
560
|
+
throw new Error(`No pending supervisor request matches '${params.to}'. Use replyTo.`);
|
|
566
561
|
}
|
|
567
562
|
if (requests.length === 1) return requests[0]!;
|
|
568
563
|
if (requests.length === 0) throw new Error("No pending supervisor requests need a reply.");
|
|
@@ -580,14 +575,16 @@ function publicPendingRequests(pending: Map<string, PendingSupervisorRequest>):
|
|
|
580
575
|
}));
|
|
581
576
|
}
|
|
582
577
|
|
|
583
|
-
function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, PendingSupervisorRequest>, state: SubagentState, onLifecycle: SupervisorRequestLifecycleObserver): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
|
|
578
|
+
function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, PendingSupervisorRequest>, state: SubagentState, onLifecycle: SupervisorRequestLifecycleObserver, discover: () => void, runState: (request: SupervisorRequest) => SubagentState): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
|
|
584
579
|
return {
|
|
585
580
|
name: NATIVE_SUPERVISOR_TOOL_NAME,
|
|
586
581
|
label: "Subagent Supervisor",
|
|
587
582
|
description: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
|
|
588
583
|
parameters: IntercomParamsSchema,
|
|
589
584
|
async execute(_id, params) {
|
|
590
|
-
|
|
585
|
+
// Discover new request files even when demand-gated polling is idle.
|
|
586
|
+
discover();
|
|
587
|
+
refreshPendingRequests(pending, state, onLifecycle, runState);
|
|
591
588
|
const input = params as IntercomParams;
|
|
592
589
|
if (input.action === "status") {
|
|
593
590
|
return { content: [{ type: "text", text: `Native supervisor channel active. Pending replies: ${pending.size}.` }], details: { active: true, pending: pending.size, root: SUPERVISOR_CHANNEL_ROOT } };
|
|
@@ -605,23 +602,28 @@ function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, Pendin
|
|
|
605
602
|
clearForegroundSupervisorAttention(request, pending, state);
|
|
606
603
|
return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
|
|
607
604
|
}
|
|
608
|
-
if (input.action === "send" || input.action === "ask") {
|
|
609
|
-
throw new Error("The native subagent supervisor handles replies only. Child agents initiate asks with contact_supervisor.");
|
|
610
|
-
}
|
|
611
605
|
throw new Error(`Unsupported supervisor action: ${input.action}`);
|
|
612
606
|
},
|
|
613
607
|
};
|
|
614
608
|
}
|
|
615
609
|
|
|
616
610
|
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
|
|
611
|
+
registerTools: () => void;
|
|
617
612
|
start: () => void;
|
|
618
613
|
activateTransport: () => void;
|
|
614
|
+
findPendingAsks: (target: { runId: string; agent: string; childIndex: number }) => string[];
|
|
619
615
|
dispose: () => void;
|
|
620
616
|
pending: Map<string, PendingSupervisorRequest>;
|
|
621
617
|
getSupervisorRequestState: (event: ControlEvent) => SupervisorRequestState;
|
|
622
618
|
} {
|
|
623
619
|
const watch = deps.watch ?? fs.watch;
|
|
624
620
|
const timers = deps.timers ?? globalThis;
|
|
621
|
+
const runState = (request: SupervisorRequest): SubagentState => {
|
|
622
|
+
for (const ownerState of deps.getCurrentOwnerStates?.() ?? []) {
|
|
623
|
+
if (ownerState.asyncJobs.has(request.runId)) return ownerState;
|
|
624
|
+
}
|
|
625
|
+
return state;
|
|
626
|
+
};
|
|
625
627
|
const pending = new Map<string, PendingSupervisorRequest>();
|
|
626
628
|
const requestCorrelations = new Map<string, SupervisorRequestCorrelation>();
|
|
627
629
|
const correlationKey = (request: { runId: string; agent: string; childIndex: number; toolCallId?: string }): string | undefined => {
|
|
@@ -667,7 +669,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
667
669
|
if (!fs.existsSync(correlation.request.requestFile)
|
|
668
670
|
|| fs.existsSync(replyPath(correlation.request.channelDir, correlation.request.id))
|
|
669
671
|
|| now > requestExpiresAt(correlation.request, now)
|
|
670
|
-
|| requestRunInactive(correlation.request,
|
|
672
|
+
|| requestRunInactive(correlation.request, runState(correlation.request))) {
|
|
671
673
|
rememberResolvedRequest(correlation.request);
|
|
672
674
|
return "resolved";
|
|
673
675
|
}
|
|
@@ -682,18 +684,26 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
682
684
|
let started = false;
|
|
683
685
|
let lastStaleCleanupAt = 0;
|
|
684
686
|
const platform = deps.platform ?? process.platform;
|
|
685
|
-
const useNativeWatcher = () => shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
|
|
687
|
+
const useNativeWatcher = () => !deps.getChannelDirs && shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
|
|
686
688
|
const hasTransportDemand = () => {
|
|
687
689
|
if (pending.size > 0) return true;
|
|
688
690
|
if (state.foregroundControls.size > 0) return true;
|
|
689
|
-
|
|
691
|
+
if (deps.getChannelDirs?.().dirs.length) return true;
|
|
692
|
+
if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
|
|
693
|
+
for (const ownerState of deps.getCurrentOwnerStates?.() ?? []) {
|
|
694
|
+
for (const job of ownerState.asyncJobs.values()) {
|
|
695
|
+
if (job.status === "queued" || job.status === "running") return true;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return false;
|
|
690
699
|
};
|
|
691
700
|
|
|
692
701
|
const registerParentTools = (): void => {
|
|
693
|
-
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pi, pending, state, observeRequestLifecycle));
|
|
702
|
+
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pi, pending, state, observeRequestLifecycle, () => poll(), runState));
|
|
694
703
|
};
|
|
695
704
|
|
|
696
705
|
const cleanupStaleChannelsIfDue = (): void => {
|
|
706
|
+
if (deps.getChannelDirs) return; // The root owns global retention cleanup.
|
|
697
707
|
const nowMs = Date.now();
|
|
698
708
|
if (nowMs - lastStaleCleanupAt < STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS) return;
|
|
699
709
|
lastStaleCleanupAt = nowMs;
|
|
@@ -706,15 +716,15 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
706
716
|
|
|
707
717
|
const poll = (): void => {
|
|
708
718
|
cleanupStaleChannelsIfDue();
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
refreshPendingRequests(pending, state, ctx, observeRequestLifecycle);
|
|
719
|
+
// Only display notifications require a live UI context, not request registration.
|
|
720
|
+
refreshPendingRequests(pending, state, observeRequestLifecycle, runState);
|
|
712
721
|
const now = Date.now();
|
|
713
|
-
|
|
722
|
+
const channels = deps.getChannelDirs?.();
|
|
723
|
+
for (const { channelDir, file } of listRequestFiles(channels?.dirs)) {
|
|
714
724
|
if (seenFiles.has(file)) continue;
|
|
715
725
|
const request = parseRequestFile(file, channelDir);
|
|
716
|
-
if (!request || !
|
|
717
|
-
const lifecycle = requestLifecycle(request, state,
|
|
726
|
+
if (!request || !requestMatchesOwner(request, state)) continue;
|
|
727
|
+
const lifecycle = requestLifecycle(request, state, now, runState(request));
|
|
718
728
|
if (lifecycle !== "pending") {
|
|
719
729
|
seenFiles.add(file);
|
|
720
730
|
observeRequestLifecycle(request, lifecycle);
|
|
@@ -727,27 +737,34 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
727
737
|
pending.set(request.id, request);
|
|
728
738
|
markForegroundSupervisorAttention(request, state);
|
|
729
739
|
}
|
|
730
|
-
|
|
731
|
-
|
|
740
|
+
// The ask is already queued above. A sendMessage failure (no UI, stale context) must not
|
|
741
|
+
// lose it, and must not abort the loop before the remaining asks register.
|
|
742
|
+
try {
|
|
743
|
+
pi.sendMessage({
|
|
744
|
+
customType: SUPERVISOR_REQUEST_MESSAGE_TYPE,
|
|
745
|
+
content: requestVisibleText(request),
|
|
746
|
+
display: true,
|
|
747
|
+
details: {
|
|
748
|
+
id: request.id,
|
|
749
|
+
requestId: request.id,
|
|
750
|
+
reason: request.reason,
|
|
751
|
+
expectsReply: request.expectsReply,
|
|
752
|
+
runId: request.runId,
|
|
753
|
+
agent: request.agent,
|
|
754
|
+
childIndex: request.childIndex,
|
|
755
|
+
...(request.childTarget ? { childTarget: request.childTarget } : {}),
|
|
756
|
+
...(request.interview !== undefined ? { interview: request.interview } : {}),
|
|
757
|
+
requestBody: request.message,
|
|
758
|
+
...(request.expectsReply ? { replyHint: supervisorReplyHint(request.id) } : {}),
|
|
759
|
+
},
|
|
760
|
+
}, { triggerTurn: true });
|
|
761
|
+
// sendMessage accepts synchronously; one-way updates stay on disk until it returns.
|
|
762
|
+
if (!request.expectsReply) removeRequestFile(request.requestFile);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
// Allow an existing later scan to retry an unaccepted one-way update.
|
|
765
|
+
if (!request.expectsReply) seenFiles.delete(file);
|
|
766
|
+
console.error(`Failed to surface supervisor request ${request.id} as a user turn:`, error);
|
|
732
767
|
}
|
|
733
|
-
pi.sendMessage({
|
|
734
|
-
customType: SUPERVISOR_REQUEST_MESSAGE_TYPE,
|
|
735
|
-
content: requestVisibleText(request),
|
|
736
|
-
display: true,
|
|
737
|
-
details: {
|
|
738
|
-
id: request.id,
|
|
739
|
-
requestId: request.id,
|
|
740
|
-
reason: request.reason,
|
|
741
|
-
expectsReply: request.expectsReply,
|
|
742
|
-
runId: request.runId,
|
|
743
|
-
agent: request.agent,
|
|
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) } : {}),
|
|
749
|
-
},
|
|
750
|
-
}, { triggerTurn: true });
|
|
751
768
|
if (request.expectsReply) {
|
|
752
769
|
(pi as { events?: IntercomEventBus }).events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
|
|
753
770
|
requestId: request.id,
|
|
@@ -758,13 +775,14 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
758
775
|
if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
|
|
759
776
|
}
|
|
760
777
|
}
|
|
778
|
+
channels?.retire?.();
|
|
761
779
|
};
|
|
762
780
|
|
|
763
781
|
const startPolling = (): void => {
|
|
764
782
|
if (poller) return;
|
|
765
783
|
poller = timers.setInterval(() => {
|
|
766
784
|
poll();
|
|
767
|
-
if (!useNativeWatcher() && platform === "darwin" && !hasTransportDemand()) {
|
|
785
|
+
if (!useNativeWatcher() && (platform === "darwin" || deps.getChannelDirs) && !hasTransportDemand()) {
|
|
768
786
|
if (poller) timers.clearInterval(poller);
|
|
769
787
|
poller = undefined;
|
|
770
788
|
}
|
|
@@ -819,16 +837,35 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
819
837
|
};
|
|
820
838
|
|
|
821
839
|
return {
|
|
840
|
+
registerTools: registerParentTools,
|
|
822
841
|
activateTransport: () => {
|
|
823
842
|
if (!started) return;
|
|
824
843
|
poll();
|
|
825
844
|
if (!useNativeWatcher() && hasTransportDemand()) startPolling();
|
|
826
845
|
},
|
|
846
|
+
findPendingAsks: (target) => {
|
|
847
|
+
// Receipt-only discovery: no registration, notification, cleanup or reply writes.
|
|
848
|
+
const channelDir = resolveSupervisorChannelDir(target.runId, target.agent, target.childIndex);
|
|
849
|
+
let files: string[];
|
|
850
|
+
try { files = fs.readdirSync(path.join(channelDir, REQUESTS_DIR)); }
|
|
851
|
+
catch (error) {
|
|
852
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
const now = Date.now();
|
|
856
|
+
return files.filter(file => file.endsWith(".json")).flatMap(file => {
|
|
857
|
+
const request = parseRequestFile(path.join(channelDir, REQUESTS_DIR, file), channelDir);
|
|
858
|
+
return request?.expectsReply && request.runId === target.runId
|
|
859
|
+
&& request.agent === target.agent && request.childIndex === target.childIndex
|
|
860
|
+
&& requestLifecycle(request, state, now, runState(request)) === "pending" ? [request.id] : [];
|
|
861
|
+
}).sort();
|
|
862
|
+
},
|
|
827
863
|
start: () => {
|
|
828
864
|
if (started) return;
|
|
829
865
|
started = true;
|
|
830
866
|
registerParentTools();
|
|
831
867
|
poll();
|
|
868
|
+
if (deps.getChannelDirs) return; // Child polling starts only after a descendant launch.
|
|
832
869
|
try {
|
|
833
870
|
fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
|
|
834
871
|
if (!useNativeWatcher()) {
|