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
|
@@ -9,11 +9,13 @@ import { resolveWaitToolConfig } from "../runs/background/wait-config.ts";
|
|
|
9
9
|
import type { ChildRuntimeConfig } from "../runs/shared/child-runtime-config.ts";
|
|
10
10
|
import { readNestedControlRequests, resolveInheritedNestedRoute, type NestedRoute, writeNestedControlResult } from "../runs/shared/nested-events.ts";
|
|
11
11
|
import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
|
|
12
|
+
import { createNativeSupervisorChannel, NATIVE_SUPERVISOR_TOOL_NAME, resolveSupervisorChannelDir } from "../intercom/native-supervisor-channel.ts";
|
|
13
|
+
import { readStatus } from "../shared/utils.ts";
|
|
12
14
|
import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
|
|
13
15
|
import { createSubagentParamsSchema } from "./schemas.ts";
|
|
14
16
|
import { finalizeToolResult } from "./tool-result.ts";
|
|
15
17
|
import { loadConfig, resolveAsyncByDefault } from "./config.ts";
|
|
16
|
-
import { type Details, type SubagentState } from "../shared/types.ts";
|
|
18
|
+
import { SUBAGENT_ASYNC_STARTED_EVENT, type AsyncStartedEvent, type Details, type SubagentState } from "../shared/types.ts";
|
|
17
19
|
|
|
18
20
|
function getSubagentSessionRoot(parentSessionFile: string | null): string {
|
|
19
21
|
if (parentSessionFile) {
|
|
@@ -28,7 +30,7 @@ function expandTilde(p: string): string {
|
|
|
28
30
|
return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
function createChildSafeState(): SubagentState {
|
|
33
|
+
export function createChildSafeState(): SubagentState {
|
|
32
34
|
return {
|
|
33
35
|
baseCwd: "",
|
|
34
36
|
currentSessionId: null,
|
|
@@ -141,7 +143,7 @@ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState,
|
|
|
141
143
|
return () => clearInterval(timer);
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
/** Register
|
|
146
|
+
/** Register delegation and supervisor replies for fanout-authorized children. */
|
|
145
147
|
export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, childConfig: ChildRuntimeConfig): void {
|
|
146
148
|
if (!childConfig.fanoutChild) return;
|
|
147
149
|
|
|
@@ -156,7 +158,40 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
|
|
|
156
158
|
|
|
157
159
|
const config = loadConfig();
|
|
158
160
|
const waitToolConfig = resolveWaitToolConfig(config.waitTool);
|
|
159
|
-
const state = createChildSafeState();
|
|
161
|
+
const state = childConfig.runtimeState ?? createChildSafeState();
|
|
162
|
+
const asyncChildren = new Map<string, { dir: string; agents: string[] }>();
|
|
163
|
+
const foregroundChannels = new Set<string>();
|
|
164
|
+
const supervisorChannel = createNativeSupervisorChannel(pi, state, {
|
|
165
|
+
getChannelDirs: () => {
|
|
166
|
+
const dirs = new Set<string>();
|
|
167
|
+
for (const run of state.foregroundControls.values()) {
|
|
168
|
+
for (const child of run.activeChildren?.values() ?? []) dirs.add(resolveSupervisorChannelDir(run.runId, child.agent, child.index));
|
|
169
|
+
}
|
|
170
|
+
for (const run of state.foregroundRuns?.values() ?? []) {
|
|
171
|
+
for (const child of run.children) {
|
|
172
|
+
if (child.status === "detached") dirs.add(resolveSupervisorChannelDir(run.runId, child.agent, child.index));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const retiringForeground = [...foregroundChannels].filter(dir => !dirs.has(dir));
|
|
176
|
+
for (const dir of dirs) foregroundChannels.add(dir);
|
|
177
|
+
for (const dir of retiringForeground) dirs.add(dir);
|
|
178
|
+
const retiringAsync: string[] = [];
|
|
179
|
+
for (const [id, child] of asyncChildren) {
|
|
180
|
+
const status = readStatus(child.dir);
|
|
181
|
+
if (status && status.state !== "queued" && status.state !== "running") retiringAsync.push(id);
|
|
182
|
+
for (const [index, agent] of child.agents.entries()) dirs.add(resolveSupervisorChannelDir(id, agent, index));
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
dirs: [...dirs],
|
|
186
|
+
retire: () => {
|
|
187
|
+
for (const dir of retiringForeground) foregroundChannels.delete(dir);
|
|
188
|
+
for (const id of retiringAsync) asyncChildren.delete(id);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
const hasPendingSupervisorRequest = supervisorChannel.hasPendingRequests;
|
|
194
|
+
childConfig.hasPendingSupervisorRequest = hasPendingSupervisorRequest;
|
|
160
195
|
const executor = createSubagentExecutor({
|
|
161
196
|
pi,
|
|
162
197
|
state,
|
|
@@ -170,6 +205,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
|
|
|
170
205
|
discoverAgents,
|
|
171
206
|
allowMutatingManagementActions: false,
|
|
172
207
|
childRuntime: childConfig,
|
|
208
|
+
activateSupervisorTransport: supervisorChannel.activateTransport,
|
|
209
|
+
findPendingAsks: supervisorChannel.findPendingAsks,
|
|
173
210
|
});
|
|
174
211
|
|
|
175
212
|
const params = createSubagentParamsSchema();
|
|
@@ -188,6 +225,31 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
|
|
|
188
225
|
};
|
|
189
226
|
|
|
190
227
|
pi.registerTool(tool);
|
|
228
|
+
let unsubscribeAsyncStarted: (() => void) | undefined;
|
|
229
|
+
pi.on("session_start", (_event, ctx) => {
|
|
230
|
+
supervisorChannel.registerTools();
|
|
231
|
+
// The host applies the explicit allowlist to dynamic registration too.
|
|
232
|
+
if (!pi.getAllTools().some(tool => tool.name === NATIVE_SUPERVISOR_TOOL_NAME)) return;
|
|
233
|
+
// Downward asks belong to this coordinator, not its parent or persisted session file.
|
|
234
|
+
state.supervisorOwnerSessionId = ctx.sessionManager.getSessionId() || null;
|
|
235
|
+
unsubscribeAsyncStarted = pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, (payload: unknown) => {
|
|
236
|
+
const info = payload as AsyncStartedEvent;
|
|
237
|
+
if (!info.id || !info.asyncDir || info.sessionId !== state.currentSessionId) return;
|
|
238
|
+
const agents = info.agents ?? (info.agent ? [info.agent] : []);
|
|
239
|
+
asyncChildren.set(info.id, { dir: info.asyncDir, agents });
|
|
240
|
+
supervisorChannel.activateTransport();
|
|
241
|
+
});
|
|
242
|
+
supervisorChannel.start();
|
|
243
|
+
supervisorChannel.activateTransport();
|
|
244
|
+
});
|
|
245
|
+
pi.on("session_shutdown", () => {
|
|
246
|
+
unsubscribeAsyncStarted?.();
|
|
247
|
+
asyncChildren.clear();
|
|
248
|
+
foregroundChannels.clear();
|
|
249
|
+
supervisorChannel.dispose();
|
|
250
|
+
if (childConfig.hasPendingSupervisorRequest === hasPendingSupervisorRequest) childConfig.hasPendingSupervisorRequest = undefined;
|
|
251
|
+
state.supervisorOwnerSessionId = null;
|
|
252
|
+
});
|
|
191
253
|
const route = resolveNestedControlRoute(childConfig);
|
|
192
254
|
if (!route) return;
|
|
193
255
|
const listenerCleanupKey = "__piSubagentFanoutChildNestedControlInboxCleanups";
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as net from "node:net";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import packageJson from "../../package.json" with { type: "json" };
|
|
9
|
+
import { encodeHerdrPiFrame, HERDR_PI_MAX_FRAME_BYTES, HERDR_PI_PROTOCOL, HERDR_PI_RUN_ENV, HERDR_PI_RUNTIME_DIR_ENV, HerdrPiFrameDecoder, validateHerdrPiRunId, type HerdrPiFrame } from "../runs/shared/herdr-pi-protocol.ts";
|
|
10
|
+
import { discoverAgents } from "../agents/agents.ts";
|
|
11
|
+
import { buildSkillInjection, resolveSkills } from "../agents/skills.ts";
|
|
12
|
+
import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
|
|
13
|
+
import { appendAgentRefinementOverlay } from "../agents/agent-refinements.ts";
|
|
14
|
+
import { rewriteSubagentPrompt } from "../runs/shared/subagent-prompt-runtime.ts";
|
|
15
|
+
import { resolveExistingReadPaths } from "../shared/settings.ts";
|
|
16
|
+
|
|
17
|
+
interface PendingRequest { operation: "prompt" | "steer" | "follow-up" | "abort" | "supervisor-reply"; text?: string; supervisorId?: string }
|
|
18
|
+
interface BridgeContext {
|
|
19
|
+
isIdle(): boolean;
|
|
20
|
+
hasPendingMessages(): boolean;
|
|
21
|
+
getActiveTools?(): string[];
|
|
22
|
+
sessionManager?: { getSessionId?(): string; getSessionFile?(): string | undefined };
|
|
23
|
+
model?: { provider?: string; id?: string };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function gitEvidence(cwd: string): Record<string, unknown> | undefined {
|
|
27
|
+
const run = (args: string[]) => spawnSync("git", args, { cwd, encoding: "utf8", timeout: 5_000, maxBuffer: 128 * 1024 });
|
|
28
|
+
const inside = run(["rev-parse", "--is-inside-work-tree"]);
|
|
29
|
+
if (inside.status !== 0) return undefined;
|
|
30
|
+
const head = run(["rev-parse", "HEAD"]); const branch = run(["symbolic-ref", "--quiet", "--short", "HEAD"]); const dirty = run(["status", "--porcelain"]);
|
|
31
|
+
return { ...(head.status === 0 ? { head: head.stdout.trim() } : {}), ...(branch.status === 0 ? { branch: branch.stdout.trim() } : {}), dirty: Boolean(dirty.stdout.trim()) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveRemoteHerdrResources(cwd: string, resources: { agent: string; skills?: string[]; toolCeiling?: string[]; reads?: string[] | false }, remoteDefaultTools: string[] = []): { agent: string; skills: string[]; tools: string[]; systemPrompt: string; inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean } {
|
|
35
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || resources.skills?.some((name) => !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(name)) || resources.toolCeiling?.some((name) => !/^[A-Za-z0-9_-]{1,64}$/u.test(name)) || (resources.reads !== undefined && resources.reads !== false && (!Array.isArray(resources.reads) || resources.reads.some((name) => typeof name !== "string")))) throw new Error("Remote logical resource names are invalid.");
|
|
36
|
+
const discovered = discoverAgents(cwd, "both").agents.filter((agent) => agent.name === resources.agent || agent.aliases?.includes(resources.agent));
|
|
37
|
+
if (discovered.length !== 1) throw new Error(`Remote agent '${resources.agent}' was not found unambiguously in ${cwd}.`);
|
|
38
|
+
const agent = discovered[0]!; const names = resources.skills ?? agent.skills ?? [];
|
|
39
|
+
const skills = resolveSkills(names, cwd); if (skills.missing.length) throw new Error(`Remote Pi skills not found: ${skills.missing.join(", ")}. Configure them on the saved machine.`);
|
|
40
|
+
const skillPrompt = buildSkillInjection(skills.resolved);
|
|
41
|
+
const reads = resources.reads === undefined ? agent.defaultReads ?? false : resources.reads; const readPaths = Array.isArray(reads) ? resolveExistingReadPaths(reads, cwd) : [];
|
|
42
|
+
const systemPrompt = appendAgentRefinementOverlay(`${agent.systemPrompt}${buildAgentMemoryInjection(agent, cwd)}${skillPrompt ? `\n\n${skillPrompt}` : ""}${readPaths.length ? `\n\n[Read from: ${readPaths.join(", ")}]` : ""}`, { cwd, agentName: agent.name });
|
|
43
|
+
const denied = new Set(agent.excludeTools ?? []); const tools = (agent.tools === undefined ? remoteDefaultTools : agent.tools).filter((tool) => !denied.has(tool) && (!resources.toolCeiling || resources.toolCeiling.includes(tool))); if (agent.tools === undefined && !tools.length) throw new Error(`Remote agent '${agent.name}' resolved no default active tools within the inherited ceiling.`);
|
|
44
|
+
return { agent: agent.name, skills: names, tools, systemPrompt, inheritProjectContext: agent.inheritProjectContext, inheritGlobalContext: agent.inheritGlobalContext, inheritSkills: agent.inheritSkills };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function registerHerdrPiBridge(pi: ExtensionAPI): void {
|
|
48
|
+
const runId = validateHerdrPiRunId(process.env[HERDR_PI_RUN_ENV]);
|
|
49
|
+
const runDir = process.env[HERDR_PI_RUNTIME_DIR_ENV];
|
|
50
|
+
if (!runDir || !path.isAbsolute(runDir) || fs.lstatSync(runDir).isSymbolicLink()) throw new Error("Herdr bridge runtime directory was not authoritatively provisioned.");
|
|
51
|
+
fs.chmodSync(runDir, 0o700);
|
|
52
|
+
const socketPath = path.join(runDir, "bridge.sock");
|
|
53
|
+
const manifestPath = path.join(runDir, "manifest.json");
|
|
54
|
+
for (const candidate of [socketPath, manifestPath]) {
|
|
55
|
+
try { const stat = fs.lstatSync(candidate); if (stat.isSymbolicLink()) throw new Error(`Refusing symlink at '${candidate}'.`); fs.rmSync(candidate, { force: true }); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
|
|
56
|
+
}
|
|
57
|
+
let client: net.Socket | undefined;
|
|
58
|
+
let context: BridgeContext | undefined;
|
|
59
|
+
let nativeSessionId: string | undefined;
|
|
60
|
+
let finalAssistant: AgentMessage | undefined;
|
|
61
|
+
let activeRequestId: string | undefined;
|
|
62
|
+
let resolvedSystemPrompt: string | undefined;
|
|
63
|
+
let resolvedContextPolicy: { agent: string; inheritProjectContext: boolean; inheritGlobalContext: boolean; inheritSkills: boolean } | undefined;
|
|
64
|
+
let configured = false;
|
|
65
|
+
const pending = new Map<string, PendingRequest>();
|
|
66
|
+
const supervisor = new Map<string, { reason: "need_decision" | "interview_request" | "progress_update"; resolve(answer: string): void }>();
|
|
67
|
+
let evidenceCursor = 0; let journalBytes = 0; const journal: HerdrPiFrame[] = [];
|
|
68
|
+
const send = (frame: { type: string; [key: string]: unknown }) => {
|
|
69
|
+
const record = { protocol: HERDR_PI_PROTOCOL, runId, ...frame, cursor: ++evidenceCursor } as HerdrPiFrame; const size = Buffer.byteLength(JSON.stringify(record)); journal.push(record); journalBytes += size; while (journal.length > 512 || journalBytes > 8 * 1024 * 1024) { const removed = journal.shift(); if (removed) journalBytes -= Buffer.byteLength(JSON.stringify(removed)); }
|
|
70
|
+
if (!client || client.destroyed) return;
|
|
71
|
+
client.write(encodeHerdrPiFrame(record));
|
|
72
|
+
};
|
|
73
|
+
const sessionId = (ctx: BridgeContext) => ctx.sessionManager?.getSessionId?.();
|
|
74
|
+
const activeTools = (ctx: BridgeContext) => ctx.getActiveTools?.() ?? [];
|
|
75
|
+
|
|
76
|
+
pi.registerTool({
|
|
77
|
+
name: "contact_supervisor",
|
|
78
|
+
label: "Contact supervisor",
|
|
79
|
+
description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
|
|
80
|
+
parameters: Type.Object({ reason: Type.Union([Type.Literal("need_decision"), Type.Literal("interview_request"), Type.Literal("progress_update")]), message: Type.Optional(Type.String({ maxLength: 65_536 })), interview: Type.Optional(Type.Unknown()) }, { additionalProperties: false }),
|
|
81
|
+
execute: async (_toolCallId, input: { reason: "need_decision" | "interview_request" | "progress_update"; message?: string; interview?: unknown }) => {
|
|
82
|
+
if (input.reason !== "interview_request" && !input.message?.trim()) throw new Error("message is required for supervisor decisions and progress updates.");
|
|
83
|
+
const id = `sup_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
84
|
+
send({ type: "supervisor-request", requestId: id, reason: input.reason, message: input.message?.slice(0, 65_536) ?? "", ...(input.interview !== undefined ? { interview: input.interview } : {}), expectsReply: input.reason !== "progress_update", nativeSessionId });
|
|
85
|
+
if (input.reason === "progress_update") { await new Promise<string>((resolve) => supervisor.set(id, { reason: input.reason, resolve })); return { content: [{ type: "text", text: "Supervisor progress update queued." }], details: { delivered: true, requestId: id, reason: input.reason } }; }
|
|
86
|
+
const reason = input.reason as "need_decision" | "interview_request"; const answer = await new Promise<string>((resolve) => supervisor.set(id, { reason, resolve })); const details: Record<string, unknown> = { requestId: id, reason }; if (reason === "interview_request") try { details.structuredReply = JSON.parse(answer.replace(/^```(?:json)?\s*|\s*```$/giu, "")); } catch { details.structuredReplyParseError = "Supervisor interview reply was not valid JSON."; }
|
|
87
|
+
return { content: [{ type: "text", text: `**Reply from supervisor:**\n${answer}` }], details };
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const execute = async (requestId: string, ctx: BridgeContext) => {
|
|
92
|
+
const request = pending.get(requestId);
|
|
93
|
+
if (!request) throw new Error(`Unknown or already consumed bridge request '${requestId}'.`);
|
|
94
|
+
pending.delete(requestId);
|
|
95
|
+
if (sessionId(ctx) !== nativeSessionId) throw new Error("Native Pi session identity changed.");
|
|
96
|
+
send({ type: "accepted", requestId, operation: request.operation, nativeSessionId });
|
|
97
|
+
const api = pi as unknown as { sendUserMessage(text: string, options?: { deliverAs?: "steer" | "followUp" }): void; abort(): void };
|
|
98
|
+
if (request.operation === "prompt") { activeRequestId = requestId; finalAssistant = undefined; api.sendUserMessage(request.text ?? ""); }
|
|
99
|
+
else if (request.operation === "steer") api.sendUserMessage(request.text ?? "", { deliverAs: "steer" });
|
|
100
|
+
else if (request.operation === "follow-up") api.sendUserMessage(request.text ?? "", { deliverAs: "followUp" });
|
|
101
|
+
else if (request.operation === "abort") api.abort();
|
|
102
|
+
else {
|
|
103
|
+
const entry = request.supervisorId ? supervisor.get(request.supervisorId) : undefined;
|
|
104
|
+
if (!entry) throw new Error("Unknown supervisor request.");
|
|
105
|
+
supervisor.delete(request.supervisorId!); entry.resolve(request.text ?? "");
|
|
106
|
+
}
|
|
107
|
+
send({ type: "control-ack", requestId, operation: request.operation, nativeSessionId });
|
|
108
|
+
};
|
|
109
|
+
pi.registerCommand("pi-subagents-bridge", { description: "Internal pane-native request dispatch", handler: (args, ctx) => execute(args.trim(), ctx as unknown as BridgeContext) });
|
|
110
|
+
|
|
111
|
+
const on = pi.on as unknown as (name: string, handler: (event: Record<string, unknown>, ctx: BridgeContext) => unknown) => void;
|
|
112
|
+
on("before_agent_start", (event) => resolvedSystemPrompt && resolvedContextPolicy ? { systemPrompt: `${rewriteSubagentPrompt(typeof event.systemPrompt === "string" ? event.systemPrompt : "", { inheritProjectContext: resolvedContextPolicy.inheritProjectContext, inheritGlobalContext: resolvedContextPolicy.inheritGlobalContext, inheritSkills: resolvedContextPolicy.inheritSkills })}\n\n<active_agent name=${JSON.stringify(resolvedContextPolicy.agent)}/>\n\n${resolvedSystemPrompt}` } : undefined);
|
|
113
|
+
on("session_start", (_event, ctx) => {
|
|
114
|
+
context = ctx; nativeSessionId = sessionId(ctx);
|
|
115
|
+
if (!nativeSessionId) throw new Error("Pane-native Pi bridge requires a persisted native session identity.");
|
|
116
|
+
fs.writeFileSync(manifestPath, JSON.stringify({ protocol: HERDR_PI_PROTOCOL, packageVersion: packageJson.version, runId, socketPath, nativeSessionId, pid: process.pid }), { mode: 0o600, flag: "wx" });
|
|
117
|
+
send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: ctx.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(ctx), model: ctx.model?.provider && ctx.model.id ? `${ctx.model.provider}/${ctx.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
|
|
118
|
+
});
|
|
119
|
+
on("message_end", (event) => {
|
|
120
|
+
const message = event.message as AgentMessage | undefined;
|
|
121
|
+
if (message?.role === "assistant") finalAssistant = message;
|
|
122
|
+
send({ type: "event", requestId: activeRequestId, event: { ...event, message }, nativeSessionId });
|
|
123
|
+
});
|
|
124
|
+
on("agent_settled", (_event, ctx) => { const requestId = activeRequestId; send({ type: "settled", requestId, nativeSessionId, idle: ctx.isIdle(), pending: ctx.hasPendingMessages(), finalAssistant, finalGit: gitEvidence(process.cwd()) }); if (ctx.isIdle() && !ctx.hasPendingMessages()) activeRequestId = undefined; });
|
|
125
|
+
on("session_shutdown", () => { server.close(); client?.destroy(); try { fs.rmSync(runDir, { recursive: true, force: true }); } catch {} });
|
|
126
|
+
|
|
127
|
+
const server = net.createServer((socket) => {
|
|
128
|
+
if (client && !client.destroyed) { socket.destroy(); return; }
|
|
129
|
+
client = socket; const decoder = new HerdrPiFrameDecoder(); let total = 0;
|
|
130
|
+
for (const record of journal) socket.write(encodeHerdrPiFrame(record));
|
|
131
|
+
socket.on("data", (chunk: Buffer) => {
|
|
132
|
+
total += chunk.byteLength; if (total > HERDR_PI_MAX_FRAME_BYTES * 32) { socket.destroy(new Error("Bridge byte budget exceeded.")); return; }
|
|
133
|
+
try {
|
|
134
|
+
for (const frame of decoder.push(chunk)) {
|
|
135
|
+
if (frame.runId !== runId) throw new Error("Bridge run identity mismatch.");
|
|
136
|
+
if (frame.type === "supervisor-delivered" && typeof frame.requestId === "string") { const entry = supervisor.get(frame.requestId); if (!entry || entry.reason !== "progress_update") throw new Error("Unknown supervisor delivery acknowledgement."); supervisor.delete(frame.requestId); entry.resolve("delivered"); continue; }
|
|
137
|
+
if (frame.type === "configure") {
|
|
138
|
+
if (configured || typeof frame.requestId !== "string" || !frame.resources || typeof frame.resources !== "object") throw new Error("Invalid or duplicate bridge configuration.");
|
|
139
|
+
const resources = frame.resources as { agent?: unknown; skills?: unknown; toolCeiling?: unknown; reads?: unknown };
|
|
140
|
+
const invalidSkills = resources.skills !== undefined && (!Array.isArray(resources.skills) || resources.skills.some((name) => typeof name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(name)));
|
|
141
|
+
const invalidReads = resources.reads !== undefined && resources.reads !== false && (!Array.isArray(resources.reads) || resources.reads.some((name) => typeof name !== "string"));
|
|
142
|
+
if (typeof resources.agent !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(resources.agent) || invalidSkills || invalidReads) throw new Error("Remote logical resource names are invalid.");
|
|
143
|
+
const resolved = resolveRemoteHerdrResources(process.cwd(), { agent: resources.agent, ...(resources.skills ? { skills: resources.skills as string[] } : {}), ...(resources.toolCeiling ? { toolCeiling: resources.toolCeiling as string[] } : {}), ...(resources.reads !== undefined ? { reads: resources.reads as string[] | false } : {}) }, context ? activeTools(context) : []); resolvedSystemPrompt = resolved.systemPrompt; resolvedContextPolicy = resolved;
|
|
144
|
+
(pi as unknown as { setActiveTools?(tools: string[]): void }).setActiveTools?.(resolved.tools); const acknowledged = context ? activeTools(context) : []; if (acknowledged.some((tool) => !resolved.tools.includes(tool)) || resolved.tools.some((tool) => !acknowledged.includes(tool))) throw new Error("Remote Pi did not apply the resolved active-tool set exactly.");
|
|
145
|
+
configured = true; send({ type: "configured", requestId: frame.requestId, nativeSessionId, agent: resolved.agent, skills: resolved.skills, model: context?.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, tools: acknowledged }); continue;
|
|
146
|
+
}
|
|
147
|
+
if (frame.type !== "prepare" || typeof frame.requestId !== "string" || pending.has(frame.requestId) || !configured) throw new Error("Invalid, duplicate, or unconfigured bridge request.");
|
|
148
|
+
const operation = frame.operation;
|
|
149
|
+
if (!(["prompt", "steer", "follow-up", "abort", "supervisor-reply"] as unknown[]).includes(operation)) throw new Error("Unsupported bridge operation.");
|
|
150
|
+
const text = frame.text; if (text !== undefined && (typeof text !== "string" || Buffer.byteLength(text) > 256 * 1024)) throw new Error("Bridge request text is invalid or too large.");
|
|
151
|
+
pending.set(frame.requestId, { operation: operation as PendingRequest["operation"], ...(text !== undefined ? { text } : {}), ...(typeof frame.supervisorId === "string" ? { supervisorId: frame.supervisorId } : {}) });
|
|
152
|
+
send({ type: "prepared", requestId: frame.requestId, nativeSessionId });
|
|
153
|
+
}
|
|
154
|
+
} catch (error) { socket.destroy(error as Error); }
|
|
155
|
+
});
|
|
156
|
+
socket.on("close", () => { if (client === socket) client = undefined; });
|
|
157
|
+
if (context && nativeSessionId) send({ type: "ready", packageVersion: packageJson.version, nativeSessionId, sessionFile: context.sessionManager?.getSessionFile?.(), cwd: process.cwd(), tools: activeTools(context), model: context.model?.provider && context.model.id ? `${context.model.provider}/${context.model.id}` : undefined, configured, activeRequestId, evidenceCursor, evidenceFloor: journal[0]?.cursor ?? evidenceCursor + 1, initialGit: gitEvidence(process.cwd()) });
|
|
158
|
+
});
|
|
159
|
+
server.listen(socketPath, () => { fs.chmodSync(socketPath, 0o600); });
|
|
160
|
+
}
|
package/src/extension/index.ts
CHANGED
|
@@ -30,8 +30,9 @@ import { getAgentDir } from "../shared/utils.ts";
|
|
|
30
30
|
import { isStaleExtensionContextError, withCachedUiContext } from "../shared/extension-context.ts";
|
|
31
31
|
import { currentCompletionOwnerId } from "../shared/completion-owner.ts";
|
|
32
32
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
33
|
-
import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
|
|
33
|
+
import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary, setInlineWorkflowCoverage } from "../tui/render.ts";
|
|
34
34
|
import { openSubagentFleet } from "../tui/fleet.ts";
|
|
35
|
+
import { createBuiltinInspectorPlugins } from "../inspectors/plugins.ts";
|
|
35
36
|
import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-status.ts";
|
|
36
37
|
import { createSubagentParamsSchema } from "./schemas.ts";
|
|
37
38
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
@@ -70,7 +71,7 @@ import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/
|
|
|
70
71
|
import { disposeChildSessions } from "../runs/shared/child-session.ts";
|
|
71
72
|
import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
72
73
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
73
|
-
import {
|
|
74
|
+
import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
|
|
74
75
|
import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
|
|
75
76
|
import { formatWorkflowPreflightSummary, normalizeWorkflowPreflight } from "../workflows/workflow-preflight.ts";
|
|
76
77
|
import { finalizeToolResult } from "./tool-result.ts";
|
|
@@ -336,14 +337,18 @@ function createSlashResultComponent(
|
|
|
336
337
|
theme: ExtensionContext["ui"]["theme"],
|
|
337
338
|
rendererConfig?: MainWindowRendererConfig,
|
|
338
339
|
foregroundDetachShortcut?: string,
|
|
340
|
+
getCurrentTheme: () => ExtensionContext["ui"]["theme"] = () => theme,
|
|
339
341
|
): Container {
|
|
340
342
|
const container = new Container();
|
|
341
343
|
let lastVersion = -1;
|
|
344
|
+
let lastTheme: ExtensionContext["ui"]["theme"] | undefined;
|
|
342
345
|
container.render = (width: number): string[] => {
|
|
343
346
|
const snapshot = getSlashRenderableSnapshot(details);
|
|
344
|
-
|
|
347
|
+
const currentTheme = getCurrentTheme();
|
|
348
|
+
if (snapshot.version !== lastVersion || currentTheme !== lastTheme || isSlashResultRunning(snapshot.result)) {
|
|
345
349
|
lastVersion = snapshot.version;
|
|
346
|
-
|
|
350
|
+
lastTheme = currentTheme;
|
|
351
|
+
rebuildSlashResultContainer(container, snapshot.result, options, currentTheme, rendererConfig, foregroundDetachShortcut);
|
|
347
352
|
}
|
|
348
353
|
return Container.prototype.render.call(container, width);
|
|
349
354
|
};
|
|
@@ -426,8 +431,6 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
426
431
|
cleanupOldChainDirs();
|
|
427
432
|
|
|
428
433
|
const config = loadConfig();
|
|
429
|
-
// Apply the process-wide exclusion TTL before any child launch can record a model failure.
|
|
430
|
-
applyModelExclusionsConfig(config);
|
|
431
434
|
const waitToolConfig = resolveWaitToolConfig(config.waitTool);
|
|
432
435
|
const asyncByDefault = resolveAsyncByDefault(config);
|
|
433
436
|
const fleetViewEnabled = config.fleetView !== false;
|
|
@@ -437,14 +440,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
437
440
|
const tempArtifactsDir = getArtifactsDir(null);
|
|
438
441
|
const artifactCleanupDays = config.artifactConfig?.cleanupDays ?? DEFAULT_ARTIFACT_CONFIG.cleanupDays;
|
|
439
442
|
cleanupAllArtifactDirs(artifactCleanupDays);
|
|
440
|
-
|
|
441
|
-
try {
|
|
442
|
-
cleanupResultIndexes(DIRS.results);
|
|
443
|
-
} catch (error) {
|
|
444
|
-
console.error("Failed to clean stale subagent result indexes:", error);
|
|
445
|
-
}
|
|
446
|
-
}, 30_000);
|
|
447
|
-
resultIndexCleanupTimer.unref?.();
|
|
443
|
+
let resultIndexCleanupTimer: ReturnType<typeof setTimeout> | undefined;
|
|
448
444
|
|
|
449
445
|
const state: SubagentState = {
|
|
450
446
|
baseCwd: "",
|
|
@@ -503,7 +499,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
503
499
|
const ctx = withLastUiContext((current) => current);
|
|
504
500
|
if (!ctx) return;
|
|
505
501
|
try {
|
|
506
|
-
await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: config.fleetKeybindings });
|
|
502
|
+
await openSubagentFleet(ctx, state, { initialKey: itemKey, asyncDirRoot: DIRS.async, resultsDir: DIRS.results, fleetKeybindings: config.fleetKeybindings, inspectorPlugins: createBuiltinInspectorPlugins() });
|
|
507
503
|
} catch (error) {
|
|
508
504
|
if (isStaleExtensionContextError(error)) {
|
|
509
505
|
if (state.lastUiContext === ctx) state.lastUiContext = null;
|
|
@@ -511,7 +507,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
511
507
|
}
|
|
512
508
|
throw error;
|
|
513
509
|
}
|
|
514
|
-
}, { placement: fleetViewPlacement })
|
|
510
|
+
}, { placement: fleetViewPlacement, onWorkflowCoverageChange: setInlineWorkflowCoverage })
|
|
515
511
|
: undefined;
|
|
516
512
|
let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
|
|
517
513
|
let goalTurnId = 0;
|
|
@@ -590,23 +586,39 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
590
586
|
const { startResultWatcher, transitionResultDelivery, primeExistingResults, stopResultWatcher } = resultWatcher;
|
|
591
587
|
refreshResultDelivery = resultWatcher.refreshResultDelivery;
|
|
592
588
|
const asyncRetentionAbort = new AbortController();
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
589
|
+
let asyncRetentionTimer: ReturnType<typeof setTimeout> | undefined;
|
|
590
|
+
const startSessionMaintenance = () => {
|
|
591
|
+
if (!resultIndexCleanupTimer) {
|
|
592
|
+
resultIndexCleanupTimer = setTimeout(() => {
|
|
593
|
+
try {
|
|
594
|
+
cleanupResultIndexes(DIRS.results);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error("Failed to clean stale subagent result indexes:", error);
|
|
597
|
+
}
|
|
598
|
+
}, 30_000);
|
|
599
|
+
resultIndexCleanupTimer.unref?.();
|
|
600
|
+
}
|
|
601
|
+
waitSubscriptionManager.start();
|
|
602
|
+
if (!asyncRetentionTimer) {
|
|
603
|
+
asyncRetentionTimer = setTimeout(async () => {
|
|
604
|
+
try {
|
|
605
|
+
await cleanupAsyncRetention({
|
|
606
|
+
asyncDirRoot: DIRS.async,
|
|
607
|
+
resultsDir: DIRS.results,
|
|
608
|
+
signal: asyncRetentionAbort.signal,
|
|
609
|
+
protectedRunIds: new Set([
|
|
610
|
+
...state.asyncJobs.keys(),
|
|
611
|
+
...(state.workflowControllers?.keys() ?? []),
|
|
612
|
+
...scheduledRunManager.referencedAsyncRunIds(),
|
|
613
|
+
]),
|
|
614
|
+
});
|
|
615
|
+
} catch (error) {
|
|
616
|
+
console.error("Failed to clean retained async subagent state:", error);
|
|
617
|
+
}
|
|
618
|
+
}, ASYNC_RETENTION_DELAY_MS);
|
|
619
|
+
asyncRetentionTimer.unref?.();
|
|
607
620
|
}
|
|
608
|
-
}
|
|
609
|
-
asyncRetentionTimer.unref?.();
|
|
621
|
+
};
|
|
610
622
|
|
|
611
623
|
const executorDeps: Parameters<typeof createSubagentExecutor>[0] = {
|
|
612
624
|
pi,
|
|
@@ -648,7 +660,14 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
648
660
|
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
649
661
|
const details = resolveSlashMessageDetails(message.details);
|
|
650
662
|
if (!details) return undefined;
|
|
651
|
-
return createSlashResultComponent(
|
|
663
|
+
return createSlashResultComponent(
|
|
664
|
+
details,
|
|
665
|
+
options,
|
|
666
|
+
theme,
|
|
667
|
+
config.mainWindowRenderer,
|
|
668
|
+
config.foregroundDetachShortcut,
|
|
669
|
+
() => state.lastUiContext?.ui.theme ?? theme,
|
|
670
|
+
);
|
|
652
671
|
});
|
|
653
672
|
|
|
654
673
|
pi.registerMessageRenderer<undefined>(SLASH_TEXT_RESULT_TYPE, (message, _options, _theme) => {
|
|
@@ -800,9 +819,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
800
819
|
pi.registerTool(tool);
|
|
801
820
|
|
|
802
821
|
pi.on("before_agent_start", (event, ctx) => {
|
|
803
|
-
const selectedTools = event.systemPromptOptions
|
|
822
|
+
const selectedTools = event.systemPromptOptions?.selectedTools ?? (typeof pi.getActiveTools === "function" ? pi.getActiveTools() : []);
|
|
804
823
|
const sessionId = state.currentSessionId ?? resolveCurrentSessionId(ctx.sessionManager);
|
|
805
|
-
const advertisedPrompt = selectedTools.includes("subagent")
|
|
824
|
+
const advertisedPrompt = Array.isArray(selectedTools) && selectedTools.includes("subagent")
|
|
806
825
|
? buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(sessionId))
|
|
807
826
|
: undefined;
|
|
808
827
|
const systemPrompt = appendAdvertisedAgentPrompt(event.systemPrompt, advertisedPrompt);
|
|
@@ -812,7 +831,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
812
831
|
registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager, waitToolConfig.defaultTimeoutMs);
|
|
813
832
|
|
|
814
833
|
pi.on("agent_end", async (_event, ctx) => {
|
|
815
|
-
if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events });
|
|
834
|
+
if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events, hasPendingSupervisorRequest: supervisorChannel.hasPendingRequests });
|
|
816
835
|
const ownerSessionId = state.currentSessionId;
|
|
817
836
|
if (!ownerSessionId) return;
|
|
818
837
|
goalTurnId += 1;
|
|
@@ -1029,8 +1048,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1029
1048
|
state.workflowControllers?.clear();
|
|
1030
1049
|
state.workflowChildStops?.clear();
|
|
1031
1050
|
clearRuntimeAgentsForPi(pi);
|
|
1032
|
-
clearTimeout(resultIndexCleanupTimer);
|
|
1033
|
-
|
|
1051
|
+
if (resultIndexCleanupTimer) clearTimeout(resultIndexCleanupTimer);
|
|
1052
|
+
resultIndexCleanupTimer = undefined;
|
|
1053
|
+
if (asyncRetentionTimer) clearTimeout(asyncRetentionTimer);
|
|
1054
|
+
asyncRetentionTimer = undefined;
|
|
1034
1055
|
asyncRetentionAbort.abort();
|
|
1035
1056
|
stopResultWatcher();
|
|
1036
1057
|
resultDeliveryOwnership.clear();
|
|
@@ -1124,7 +1145,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1124
1145
|
if (event.reason !== "manual") suspendWidgetsForCompaction();
|
|
1125
1146
|
});
|
|
1126
1147
|
|
|
1127
|
-
pi.on("session_compact", () => {
|
|
1148
|
+
pi.on("session_compact", (event) => {
|
|
1149
|
+
if (event.reason !== "manual") return;
|
|
1128
1150
|
const hasActiveAsyncWork = [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
|
|
1129
1151
|
if (!hasActiveAsyncWork || !withLastUiContext(() => true)) return;
|
|
1130
1152
|
pi.sendMessage(
|
|
@@ -1139,6 +1161,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1139
1161
|
|
|
1140
1162
|
pi.on("session_start", (event, ctx) => {
|
|
1141
1163
|
installRuntime(ctx);
|
|
1164
|
+
startSessionMaintenance();
|
|
1142
1165
|
const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
|
|
1143
1166
|
resetSessionState(ctx, recovering, event.previousSessionFile);
|
|
1144
1167
|
releaseHostSessionLiveness();
|
|
@@ -89,16 +89,18 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
89
89
|
if (hasNamedWorkflow && (params.workflowScript !== undefined || params.workflowScriptPath !== undefined)) {
|
|
90
90
|
return { ok: false, error: "workflow is mutually exclusive with workflowScript and workflowScriptPath.", mode: "workflow" };
|
|
91
91
|
}
|
|
92
|
-
if (!hasNamedWorkflow && params.args !== undefined) {
|
|
93
|
-
return { ok: false, error: "args requires a named workflow resource.", mode: "workflow" };
|
|
94
|
-
}
|
|
95
92
|
const hasWorkflowInput = params.workflowScript !== undefined || params.workflowScriptPath !== undefined || hasNamedWorkflow;
|
|
93
|
+
if (!hasWorkflowInput && params.args !== undefined) {
|
|
94
|
+
return { ok: false, error: "args requires workflow, workflowScript, or workflowScriptPath.", mode: "workflow" };
|
|
95
|
+
}
|
|
96
96
|
const hasCapacityOverride = params.globalConcurrencyLimit !== undefined || params.maxSubagentSpawnsPerRun !== undefined;
|
|
97
97
|
if (hasCapacityOverride) {
|
|
98
98
|
const capacityOverrideError = validateWorkflowCapacityOverrides(params);
|
|
99
99
|
if (capacityOverrideError) return { ok: false, error: capacityOverrideError, mode: params.action === undefined ? "workflow" : "management" };
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const validatesSpawnBudget = typeof params.action === "string" && params.action.trim() === "validate"
|
|
101
|
+
&& params.globalConcurrencyLimit === undefined && params.maxSubagentSpawnsPerRun !== undefined;
|
|
102
|
+
if ((params.action !== undefined && !validatesSpawnBudget) || hasNamedWorkflow || (params.workflowScript === undefined && params.workflowScriptPath === undefined)) {
|
|
103
|
+
return { ok: false, error: "Workflow capacity overrides are only supported on top-level workflowScript or workflowScriptPath calls; validate accepts maxSubagentSpawnsPerRun for static budget checks.", mode: params.action === undefined ? "workflow" : "management" };
|
|
102
104
|
}
|
|
103
105
|
}
|
|
104
106
|
if (params.preflight !== undefined && !hasWorkflowInput) {
|
package/src/extension/rpc.ts
CHANGED
|
@@ -499,9 +499,13 @@ function manageParams(params: unknown): SubagentParamsLike {
|
|
|
499
499
|
if (requiresId && typeof input.id !== "string") {
|
|
500
500
|
throw new SubagentRpcError("invalid_params", `RPC manage ${action} requires id.`);
|
|
501
501
|
}
|
|
502
|
+
if (action === "schedule.run" && input.quiet !== undefined && typeof input.quiet !== "boolean") {
|
|
503
|
+
throw new SubagentRpcError("invalid_params", "RPC manage quiet must be a boolean.");
|
|
504
|
+
}
|
|
502
505
|
const output: SubagentParamsLike = {
|
|
503
506
|
action,
|
|
504
507
|
...(typeof input.id === "string" ? { id: input.id.trim() } : {}),
|
|
508
|
+
...(action === "schedule.run" && input.quiet === true ? { quiet: true } : {}),
|
|
505
509
|
};
|
|
506
510
|
assertSubagentParams(output, "RPC manage params");
|
|
507
511
|
return output;
|