pi-subagents 0.66.0 → 0.68.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 +138 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +43 -15
- package/docs/configuration.md +67 -23
- package/docs/extension-api.md +38 -19
- package/docs/missions.md +10 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +22 -12
- package/docs/standalone-background.md +59 -0
- package/docs/tool-reference.md +33 -20
- package/docs/watchdog.md +39 -10
- package/docs/workflows.md +37 -13
- package/index.ts +5 -2
- package/inspector-runner.mjs +2 -2
- package/package.json +4 -2
- package/prompts/parallel-review.md +1 -1
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +32 -0
- package/skills/pi-subagents/SKILL.md +32 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +11 -9
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +190 -71
- package/src/agents/builtin-names.ts +1 -0
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +94 -59
- package/src/api/required-child-extensions.ts +6 -0
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +66 -4
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +62 -39
- package/src/extension/public-execution.ts +7 -5
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +81 -81
- package/src/extension/tool-description.ts +30 -82
- 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 +44 -31
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +348 -176
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +17 -12
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +23 -10
- package/src/runs/background/binary-bootstrap.ts +38 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +105 -7
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +81 -33
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +12 -33
- package/src/runs/background/runner-child-launch.ts +6 -1
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +51 -18
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +441 -304
- package/src/runs/background/subagent-wait.ts +176 -28
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +134 -248
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +374 -178
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +38 -11
- package/src/runs/shared/async-status-projection.ts +127 -33
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +28 -5
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +8 -1
- package/src/runs/shared/child-session.ts +127 -52
- package/src/runs/shared/child-tool-plan.ts +142 -11
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -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 +27 -4
- package/src/runs/shared/worktree.ts +45 -15
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +68 -8
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +74 -30
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -7
- package/src/slash/subagents-admin.ts +24 -13
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +168 -37
- package/src/watchdog/child-status.ts +28 -28
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +21 -1
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +10 -2
- package/src/watchdog/register-main.ts +39 -35
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +123 -74
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +100 -27
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +3 -0
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +5 -3
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +279 -10
- package/src/workflows/workflow-checklist.ts +2 -2
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/runner-server-preload.mjs +0 -13
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
- /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
|
};
|
|
@@ -83,6 +83,8 @@ 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 };
|
|
86
88
|
/** Retained scheduled states for the current runtime owner, never foreign owners. */
|
|
87
89
|
getCurrentOwnerStates?: () => Iterable<SubagentState>;
|
|
88
90
|
platform?: NodeJS.Platform;
|
|
@@ -286,18 +288,18 @@ function parseRequestFile(file: string, channelDir: string): PendingSupervisorRe
|
|
|
286
288
|
}
|
|
287
289
|
}
|
|
288
290
|
|
|
289
|
-
function listRequestFiles(): Array<{ channelDir: string; file: string }> {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
+
}
|
|
296
300
|
}
|
|
297
301
|
const files: Array<{ channelDir: string; file: string }> = [];
|
|
298
|
-
for (const
|
|
299
|
-
if (!entry.isDirectory()) continue;
|
|
300
|
-
const channelDir = path.join(SUPERVISOR_CHANNEL_ROOT, entry.name);
|
|
302
|
+
for (const channelDir of channelDirs) {
|
|
301
303
|
const requestsDir = path.join(channelDir, REQUESTS_DIR);
|
|
302
304
|
let requestEntries: fs.Dirent[];
|
|
303
305
|
try {
|
|
@@ -606,9 +608,11 @@ function buildParentSupervisorTool(pi: ExtensionAPI, pending: Map<string, Pendin
|
|
|
606
608
|
}
|
|
607
609
|
|
|
608
610
|
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): {
|
|
611
|
+
registerTools: () => void;
|
|
609
612
|
start: () => void;
|
|
610
613
|
activateTransport: () => void;
|
|
611
614
|
findPendingAsks: (target: { runId: string; agent: string; childIndex: number }) => string[];
|
|
615
|
+
hasPendingRequests: () => boolean;
|
|
612
616
|
dispose: () => void;
|
|
613
617
|
pending: Map<string, PendingSupervisorRequest>;
|
|
614
618
|
getSupervisorRequestState: (event: ControlEvent) => SupervisorRequestState;
|
|
@@ -681,10 +685,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
681
685
|
let started = false;
|
|
682
686
|
let lastStaleCleanupAt = 0;
|
|
683
687
|
const platform = deps.platform ?? process.platform;
|
|
684
|
-
const useNativeWatcher = () => shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
|
|
688
|
+
const useNativeWatcher = () => !deps.getChannelDirs && shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
|
|
685
689
|
const hasTransportDemand = () => {
|
|
686
690
|
if (pending.size > 0) return true;
|
|
687
691
|
if (state.foregroundControls.size > 0) return true;
|
|
692
|
+
if (deps.getChannelDirs?.().dirs.length) return true;
|
|
688
693
|
if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
|
|
689
694
|
for (const ownerState of deps.getCurrentOwnerStates?.() ?? []) {
|
|
690
695
|
for (const job of ownerState.asyncJobs.values()) {
|
|
@@ -699,6 +704,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
699
704
|
};
|
|
700
705
|
|
|
701
706
|
const cleanupStaleChannelsIfDue = (): void => {
|
|
707
|
+
if (deps.getChannelDirs) return; // The root owns global retention cleanup.
|
|
702
708
|
const nowMs = Date.now();
|
|
703
709
|
if (nowMs - lastStaleCleanupAt < STALE_EMPTY_CHANNEL_CLEANUP_INTERVAL_MS) return;
|
|
704
710
|
lastStaleCleanupAt = nowMs;
|
|
@@ -714,7 +720,8 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
714
720
|
// Only display notifications require a live UI context, not request registration.
|
|
715
721
|
refreshPendingRequests(pending, state, observeRequestLifecycle, runState);
|
|
716
722
|
const now = Date.now();
|
|
717
|
-
|
|
723
|
+
const channels = deps.getChannelDirs?.();
|
|
724
|
+
for (const { channelDir, file } of listRequestFiles(channels?.dirs)) {
|
|
718
725
|
if (seenFiles.has(file)) continue;
|
|
719
726
|
const request = parseRequestFile(file, channelDir);
|
|
720
727
|
if (!request || !requestMatchesOwner(request, state)) continue;
|
|
@@ -726,11 +733,15 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
726
733
|
continue;
|
|
727
734
|
}
|
|
728
735
|
seenFiles.add(file);
|
|
729
|
-
if (request.expectsReply) {
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
736
|
+
if (!request.expectsReply) {
|
|
737
|
+
// Progress is already visible through child activity; do not inject a
|
|
738
|
+
// parent message or trigger a parent model turn.
|
|
739
|
+
removeRequestFile(request.requestFile);
|
|
740
|
+
continue;
|
|
733
741
|
}
|
|
742
|
+
rememberPendingRequest(request);
|
|
743
|
+
pending.set(request.id, request);
|
|
744
|
+
markForegroundSupervisorAttention(request, state);
|
|
734
745
|
// The ask is already queued above. A sendMessage failure (no UI, stale context) must not
|
|
735
746
|
// lose it, and must not abort the loop before the remaining asks register.
|
|
736
747
|
try {
|
|
@@ -749,33 +760,28 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
749
760
|
...(request.childTarget ? { childTarget: request.childTarget } : {}),
|
|
750
761
|
...(request.interview !== undefined ? { interview: request.interview } : {}),
|
|
751
762
|
requestBody: request.message,
|
|
752
|
-
|
|
763
|
+
replyHint: supervisorReplyHint(request.id),
|
|
753
764
|
},
|
|
754
765
|
}, { triggerTurn: true });
|
|
755
|
-
// sendMessage accepts synchronously; one-way updates stay on disk until it returns.
|
|
756
|
-
if (!request.expectsReply) removeRequestFile(request.requestFile);
|
|
757
766
|
} catch (error) {
|
|
758
|
-
// Allow an existing later scan to retry an unaccepted one-way update.
|
|
759
|
-
if (!request.expectsReply) seenFiles.delete(file);
|
|
760
767
|
console.error(`Failed to surface supervisor request ${request.id} as a user turn:`, error);
|
|
761
768
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
|
|
770
|
-
}
|
|
769
|
+
(pi as { events?: IntercomEventBus }).events?.emit(INTERCOM_DETACH_REQUEST_EVENT, {
|
|
770
|
+
requestId: request.id,
|
|
771
|
+
runId: request.runId,
|
|
772
|
+
agent: request.agent,
|
|
773
|
+
childIndex: request.childIndex,
|
|
774
|
+
});
|
|
775
|
+
if (pending.has(request.id)) markForegroundSupervisorAttention(request, state);
|
|
771
776
|
}
|
|
777
|
+
channels?.retire?.();
|
|
772
778
|
};
|
|
773
779
|
|
|
774
780
|
const startPolling = (): void => {
|
|
775
781
|
if (poller) return;
|
|
776
782
|
poller = timers.setInterval(() => {
|
|
777
783
|
poll();
|
|
778
|
-
if (!useNativeWatcher() && platform === "darwin" && !hasTransportDemand()) {
|
|
784
|
+
if (!useNativeWatcher() && (platform === "darwin" || deps.getChannelDirs) && !hasTransportDemand()) {
|
|
779
785
|
if (poller) timers.clearInterval(poller);
|
|
780
786
|
poller = undefined;
|
|
781
787
|
}
|
|
@@ -830,6 +836,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
830
836
|
};
|
|
831
837
|
|
|
832
838
|
return {
|
|
839
|
+
registerTools: registerParentTools,
|
|
833
840
|
activateTransport: () => {
|
|
834
841
|
if (!started) return;
|
|
835
842
|
poll();
|
|
@@ -852,11 +859,17 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
852
859
|
&& requestLifecycle(request, state, now, runState(request)) === "pending" ? [request.id] : [];
|
|
853
860
|
}).sort();
|
|
854
861
|
},
|
|
862
|
+
hasPendingRequests: () => {
|
|
863
|
+
if (!started) return false;
|
|
864
|
+
poll();
|
|
865
|
+
return pending.size > 0;
|
|
866
|
+
},
|
|
855
867
|
start: () => {
|
|
856
868
|
if (started) return;
|
|
857
869
|
started = true;
|
|
858
870
|
registerParentTools();
|
|
859
871
|
poll();
|
|
872
|
+
if (deps.getChannelDirs) return; // Child polling starts only after a descendant launch.
|
|
860
873
|
try {
|
|
861
874
|
fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
|
|
862
875
|
if (!useNativeWatcher()) {
|
package/src/policy/authority.ts
CHANGED
|
@@ -5,6 +5,8 @@ export const AUTHORITY_ACTIONS = [
|
|
|
5
5
|
"scheduleCreate",
|
|
6
6
|
"stopRun",
|
|
7
7
|
"steerRun",
|
|
8
|
+
"inspectorOpen",
|
|
9
|
+
"projectOpen",
|
|
8
10
|
] as const;
|
|
9
11
|
|
|
10
12
|
export type AuthorityAction = typeof AUTHORITY_ACTIONS[number];
|
|
@@ -18,6 +20,8 @@ const DEFAULT_AUTHORITY_POLICY: Record<AuthorityAction, AuthorityDecision> = {
|
|
|
18
20
|
scheduleCreate: "auto",
|
|
19
21
|
stopRun: "auto",
|
|
20
22
|
steerRun: "auto",
|
|
23
|
+
inspectorOpen: "auto",
|
|
24
|
+
projectOpen: "confirm",
|
|
21
25
|
};
|
|
22
26
|
|
|
23
27
|
export function resolveAuthorityDecision(input: {
|
package/src/profiles/profiles.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type RecommendedRoleTier = "cheap" | "medium" | "strong";
|
|
|
20
20
|
interface ProfileAgentOverride {
|
|
21
21
|
model?: string;
|
|
22
22
|
thinking?: string | false;
|
|
23
|
-
|
|
23
|
+
machine?: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface SubagentProfileFile {
|
|
@@ -144,10 +144,7 @@ function validateSubagentProfile(filePath: string, parsed: Record<string, unknow
|
|
|
144
144
|
if (thinking !== undefined && thinking !== false && typeof thinking !== "string") {
|
|
145
145
|
throw new Error(`Profile '${filePath}' has invalid thinking for '${name}'; expected a string or false.`);
|
|
146
146
|
}
|
|
147
|
-
|
|
148
|
-
if (fallbackModels !== undefined && fallbackModels !== false && (!Array.isArray(fallbackModels) || fallbackModels.some((item) => typeof item !== "string"))) {
|
|
149
|
-
throw new Error(`Profile '${filePath}' has invalid fallbackModels for '${name}'; expected an array of strings or false.`);
|
|
150
|
-
}
|
|
147
|
+
if ((override as Record<string, unknown>).fallbackModels !== undefined) throw new Error(`Profile '${filePath}' uses removed field fallbackModels for '${name}'; configure one model instead.`);
|
|
151
148
|
}
|
|
152
149
|
const disableBuiltins = (subagents as Record<string, unknown>).disableBuiltins;
|
|
153
150
|
if (disableBuiltins !== undefined && typeof disableBuiltins !== "boolean") {
|
|
@@ -489,10 +486,19 @@ export function applySubagentProfile(name: string): { filePath: string; settings
|
|
|
489
486
|
: {};
|
|
490
487
|
// A profile owns the complete agent mapping, but unrelated subagent settings
|
|
491
488
|
// (notably disableBuiltins, modelScope, watchdog, etc.) survive profile switches.
|
|
489
|
+
// Machine placement is not a model choice, so an existing pin survives a profile switch too.
|
|
490
|
+
const agentOverrides: Record<string, ProfileAgentOverride> = { ...profile.subagents.agentOverrides };
|
|
491
|
+
const existingOverrides = existing.agentOverrides && typeof existing.agentOverrides === "object" && !Array.isArray(existing.agentOverrides)
|
|
492
|
+
? existing.agentOverrides as Record<string, unknown>
|
|
493
|
+
: {};
|
|
494
|
+
for (const [name, value] of Object.entries(existingOverrides)) {
|
|
495
|
+
const machine = value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>).machine : undefined;
|
|
496
|
+
if (typeof machine === "string" && agentOverrides[name]?.machine === undefined) agentOverrides[name] = { ...agentOverrides[name], machine };
|
|
497
|
+
}
|
|
492
498
|
settings.subagents = {
|
|
493
499
|
...existing,
|
|
494
500
|
...profile.subagents,
|
|
495
|
-
agentOverrides
|
|
501
|
+
agentOverrides,
|
|
496
502
|
};
|
|
497
503
|
writeJsonFile(settingsPath, settings);
|
|
498
504
|
return { filePath, settingsPath };
|
|
@@ -280,6 +280,10 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncSt
|
|
|
280
280
|
if (!childStatus) return { state: "retained", reason: `async workflow child ${label} status is missing or unreadable` };
|
|
281
281
|
if (!terminalState(childStatus.state)) return { state: "retained", reason: `async workflow child ${label} is still ${childStatus.state}` };
|
|
282
282
|
if (!childStatus.processTerminal?.runnerProcessInstanceId) return { state: "retained", reason: `async workflow child ${label} has no runner process identity` };
|
|
283
|
+
if (childStatus.processTerminal.state === "not-started"
|
|
284
|
+
&& childStatus.processTerminal.runId === step.runId
|
|
285
|
+
&& typeof childStatus.error === "string"
|
|
286
|
+
&& childStatus.error) continue;
|
|
283
287
|
const proof = readProcessTerminal(childDir, {
|
|
284
288
|
runId: step.runId,
|
|
285
289
|
runnerProcessInstanceId: childStatus.processTerminal.runnerProcessInstanceId,
|
|
@@ -76,7 +76,7 @@ export function releaseActiveRunIndex(asyncDir: string): void {
|
|
|
76
76
|
releaseToolCallAliases(asyncDir);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string, options: { retryCapacityErrors?: boolean } = {}): void {
|
|
79
|
+
export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state"], toolCallId?: string, options: { retryCapacityErrors?: boolean; terminalIndexBeforeRelease?: boolean } = {}): void {
|
|
80
80
|
const marker = markerPath(asyncDir);
|
|
81
81
|
if (isActiveAsyncState(state)) {
|
|
82
82
|
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
@@ -94,6 +94,22 @@ export function updateActiveRunIndex(asyncDir: string, state: AsyncStatus["state
|
|
|
94
94
|
}
|
|
95
95
|
return;
|
|
96
96
|
}
|
|
97
|
+
if (options.terminalIndexBeforeRelease) {
|
|
98
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
99
|
+
fs.writeFileSync(marker, "", { flag: "a" });
|
|
100
|
+
const status = readStatus(asyncDir);
|
|
101
|
+
if (status?.state === state) {
|
|
102
|
+
try {
|
|
103
|
+
updateTerminalRunIndex(asyncDir, status);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (options.retryCapacityErrors) throw error;
|
|
106
|
+
console.error(`Failed to write async terminal-run index for '${asyncDir}':`, error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
releaseActiveRunIndex(asyncDir);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
97
113
|
releaseActiveRunIndex(asyncDir);
|
|
98
114
|
const status = readStatus(asyncDir);
|
|
99
115
|
if (status && status.state === state) {
|