pi-subagents 0.67.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 +70 -0
- package/README.md +1 -1
- package/docs/agents.md +37 -12
- package/docs/configuration.md +61 -19
- package/docs/extension-api.md +5 -1
- package/docs/missions.md +2 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +18 -8
- package/docs/standalone-background.md +13 -3
- package/docs/tool-reference.md +15 -12
- package/docs/watchdog.md +10 -12
- package/docs/workflows.md +11 -1
- package/index.ts +5 -2
- package/package.json +4 -2
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +25 -11
- package/skills/pi-subagents/SKILL.md +18 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +4 -4
- 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 +16 -12
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +184 -71
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/preflight.ts +20 -16
- package/src/api/required-child-extensions.ts +6 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +3 -0
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +42 -31
- package/src/extension/public-execution.ts +3 -3
- package/src/extension/schemas.ts +16 -5
- package/src/extension/tool-description.ts +8 -7
- package/src/intercom/native-supervisor-channel.ts +22 -18
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +309 -126
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +13 -4
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +20 -10
- package/src/runs/background/binary-bootstrap.ts +5 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/notify.ts +74 -6
- 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 +20 -29
- package/src/runs/background/runner-aliases.ts +4 -33
- package/src/runs/background/runner-child-launch.ts +4 -1
- package/src/runs/background/runner-child-sessions.ts +2 -2
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +11 -5
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/subagent-runner.ts +396 -275
- package/src/runs/background/subagent-wait.ts +128 -23
- 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 +4 -2
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +91 -218
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/subagent-executor.ts +266 -80
- package/src/runs/shared/acceptance.ts +34 -10
- package/src/runs/shared/async-status-projection.ts +123 -33
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +19 -6
- package/src/runs/shared/child-runtime-config.ts +5 -0
- package/src/runs/shared/child-session.ts +94 -50
- package/src/runs/shared/child-tool-plan.ts +28 -16
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- 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/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +3 -3
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/launch-contract.ts +6 -9
- 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 +70 -29
- package/src/slash/slash-commands.ts +0 -6
- package/src/slash/subagents-admin.ts +13 -9
- package/src/tui/render.ts +20 -10
- package/src/watchdog/child-status.ts +28 -36
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +1 -1
- package/src/watchdog/register-child.ts +10 -3
- package/src/watchdog/register-main.ts +20 -20
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +14 -30
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +23 -12
- package/src/watchdog/settings.ts +3 -6
- package/src/watchdog/types.ts +3 -5
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +42 -3
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- 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
|
@@ -22,6 +22,10 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
|
|
|
22
22
|
const key = keyValue.trim().toLowerCase();
|
|
23
23
|
const rawValue = rawValueValue.trim();
|
|
24
24
|
|
|
25
|
+
if (key === "machine") {
|
|
26
|
+
if (rawValue) step.machine = rawValue;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
25
29
|
if (key === "output") {
|
|
26
30
|
if (rawValue === "false") step.output = false;
|
|
27
31
|
else if (rawValue) step.output = rawValue;
|
|
@@ -257,6 +261,7 @@ export function serializeChain(config: ChainConfig): string {
|
|
|
257
261
|
for (let i = 0; i < config.steps.length; i++) {
|
|
258
262
|
const step = config.steps[i]!;
|
|
259
263
|
lines.push(`## ${step.agent}`);
|
|
264
|
+
if (step.machine) lines.push(`machine: ${step.machine}`);
|
|
260
265
|
if (step.output === false) lines.push("output: false");
|
|
261
266
|
else if (step.output) lines.push(`output: ${step.output}`);
|
|
262
267
|
if (step.phase) lines.push(`phase: ${step.phase}`);
|
|
@@ -24,7 +24,6 @@ export interface RuntimeAgentDefinition {
|
|
|
24
24
|
allowNestedSubagents?: boolean;
|
|
25
25
|
mcpDirectTools?: readonly string[];
|
|
26
26
|
model?: string;
|
|
27
|
-
fallbackModels?: readonly string[];
|
|
28
27
|
thinking?: string | false;
|
|
29
28
|
systemPromptMode?: "append" | "replace";
|
|
30
29
|
inheritProjectContext?: boolean;
|
|
@@ -37,6 +36,7 @@ export interface RuntimeAgentDefinition {
|
|
|
37
36
|
defaultAcceptance?: AcceptanceInput;
|
|
38
37
|
acceptanceRole?: AcceptanceRole;
|
|
39
38
|
runner?: AgentRunnerConfig;
|
|
39
|
+
machine?: string;
|
|
40
40
|
skills?: readonly string[];
|
|
41
41
|
skillPath?: readonly string[];
|
|
42
42
|
extensions?: readonly string[];
|
|
@@ -199,9 +199,9 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
199
199
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Runtime agent definition must be an object.");
|
|
200
200
|
const definition = value as Record<string, unknown>;
|
|
201
201
|
const supported = new Set([
|
|
202
|
-
"description", "systemPrompt", "aliases", "tools", "excludeTools", "allowNestedSubagents", "mcpDirectTools", "model", "
|
|
202
|
+
"description", "systemPrompt", "aliases", "tools", "excludeTools", "allowNestedSubagents", "mcpDirectTools", "model", "thinking",
|
|
203
203
|
"systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "defaultContext", "defaultAsync", "defaultTimeoutMs",
|
|
204
|
-
"defaultToolTimeoutMs", "defaultAcceptance", "acceptanceRole", "runner", "skills", "skillPath",
|
|
204
|
+
"defaultToolTimeoutMs", "defaultAcceptance", "acceptanceRole", "runner", "machine", "skills", "skillPath",
|
|
205
205
|
"extensions", "subagentOnlyExtensions", "mutationTools", "output", "outputMode", "defaultReads", "defaultProgress", "interactive",
|
|
206
206
|
"maxSubagentDepth", "completionGuard", "toolBudget", "permissions",
|
|
207
207
|
]);
|
|
@@ -223,7 +223,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
223
223
|
const allowNestedSubagents = validateBoolean(definition.allowNestedSubagents, "Runtime agent definition allowNestedSubagents");
|
|
224
224
|
const mcpDirectTools = validateStringList(definition.mcpDirectTools, "Runtime agent definition mcpDirectTools");
|
|
225
225
|
const model = validateOptionalString(definition.model, "Runtime agent definition model");
|
|
226
|
-
|
|
226
|
+
if ((definition as Record<string, unknown>).fallbackModels !== undefined) throw new Error("Runtime agent definition fallbackModels was removed; configure one model instead.");
|
|
227
227
|
const inheritProjectContext = validateBoolean(definition.inheritProjectContext, "Runtime agent definition inheritProjectContext");
|
|
228
228
|
const inheritGlobalContext = validateBoolean(definition.inheritGlobalContext, "Runtime agent definition inheritGlobalContext");
|
|
229
229
|
const inheritSkills = validateBoolean(definition.inheritSkills, "Runtime agent definition inheritSkills");
|
|
@@ -237,6 +237,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
237
237
|
const extensions = validateStringList(definition.extensions, "Runtime agent definition extensions");
|
|
238
238
|
const subagentOnlyExtensions = validateStringList(definition.subagentOnlyExtensions, "Runtime agent definition subagentOnlyExtensions");
|
|
239
239
|
const mutationTools = validateStringList(definition.mutationTools, "Runtime agent definition mutationTools");
|
|
240
|
+
const machine = validateOptionalString(definition.machine, "Runtime agent definition machine");
|
|
240
241
|
const output = validateOptionalString(definition.output, "Runtime agent definition output");
|
|
241
242
|
const defaultReads = validateStringList(definition.defaultReads, "Runtime agent definition defaultReads");
|
|
242
243
|
const defaultProgress = validateBoolean(definition.defaultProgress, "Runtime agent definition defaultProgress");
|
|
@@ -254,7 +255,6 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
254
255
|
...(allowNestedSubagents !== undefined ? { allowNestedSubagents } : {}),
|
|
255
256
|
...(mcpDirectTools ? { mcpDirectTools } : {}),
|
|
256
257
|
...(model ? { model } : {}),
|
|
257
|
-
...(fallbackModels ? { fallbackModels } : {}),
|
|
258
258
|
...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
|
|
259
259
|
...(systemPromptMode !== undefined ? { systemPromptMode: systemPromptMode as "append" | "replace" } : {}),
|
|
260
260
|
...(inheritProjectContext !== undefined ? { inheritProjectContext } : {}),
|
|
@@ -272,6 +272,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
272
272
|
...(extensions ? { extensions } : {}),
|
|
273
273
|
...(subagentOnlyExtensions ? { subagentOnlyExtensions } : {}),
|
|
274
274
|
...(mutationTools ? { mutationTools } : {}),
|
|
275
|
+
...(machine ? { machine } : {}),
|
|
275
276
|
...(output ? { output } : {}),
|
|
276
277
|
...(outputMode !== undefined ? { outputMode: outputMode as OutputMode } : {}),
|
|
277
278
|
...(defaultReads ? { defaultReads } : {}),
|
|
@@ -334,7 +335,6 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
|
|
|
334
335
|
...(definition.allowNestedSubagents !== undefined ? { allowNestedSubagents: definition.allowNestedSubagents } : {}),
|
|
335
336
|
...(definition.mcpDirectTools !== undefined ? { mcpDirectTools: [...definition.mcpDirectTools] } : {}),
|
|
336
337
|
...(definition.model !== undefined ? { model: definition.model } : {}),
|
|
337
|
-
...(definition.fallbackModels !== undefined ? { fallbackModels: [...definition.fallbackModels] } : {}),
|
|
338
338
|
...(definition.thinking !== undefined ? { thinking: definition.thinking } : {}),
|
|
339
339
|
systemPromptMode: definition.systemPromptMode ?? defaultSystemPromptMode(name),
|
|
340
340
|
inheritProjectContext: definition.inheritProjectContext ?? defaultInheritProjectContext(name),
|
|
@@ -354,6 +354,7 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
|
|
|
354
354
|
...(definition.extensions !== undefined ? { extensions: [...definition.extensions] } : {}),
|
|
355
355
|
...(definition.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...definition.subagentOnlyExtensions] } : {}),
|
|
356
356
|
...(definition.mutationTools !== undefined ? { mutationTools: [...definition.mutationTools] } : {}),
|
|
357
|
+
...(definition.machine !== undefined ? { machine: definition.machine } : {}),
|
|
357
358
|
...(definition.output !== undefined ? { output: definition.output } : {}),
|
|
358
359
|
...(definition.outputMode !== undefined ? { outputMode: definition.outputMode } : {}),
|
|
359
360
|
...(definition.defaultReads !== undefined ? { defaultReads: [...definition.defaultReads] } : {}),
|
package/src/api/preflight.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { discoverAgentSnapshot, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentDiscoveryAllResult, type AgentScope, type AgentSource } from "../agents/agents.ts";
|
|
5
5
|
import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
|
|
6
6
|
import { normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { inheritsParentModel, resolveEffectiveSubagentModel, resolveModelOrigin, resolveModelSelection, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-resolution.ts";
|
|
8
8
|
import { resolveModelScopesForAgent } from "../runs/shared/model-scope.ts";
|
|
9
9
|
import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/child-tool-plan.ts";
|
|
10
10
|
import { buildEffectiveSystemPrompt } from "../runs/shared/effective-system-prompt.ts";
|
|
@@ -26,6 +26,7 @@ import { processTerminalCandidatePath, processTerminalPath } from "../runs/backg
|
|
|
26
26
|
import { resultFilePath } from "../runs/background/result-files.ts";
|
|
27
27
|
import { nestedResultsPath } from "../runs/shared/nested-events.ts";
|
|
28
28
|
import { normalizeExtensionBindings, type ExtensionBindings } from "../runs/shared/extension-bindings.ts";
|
|
29
|
+
import { resolveRequiredChildExtensions } from "../shared/required-child-extensions.ts";
|
|
29
30
|
|
|
30
31
|
// v3: the contract reports the resolved Intercom bridge state and binds its
|
|
31
32
|
// prompt and tools into launchContractDigest, matching execution (#2127).
|
|
@@ -72,11 +73,13 @@ export interface SubagentLaunchContractInput {
|
|
|
72
73
|
skill?: string | string[] | boolean;
|
|
73
74
|
output?: string | boolean;
|
|
74
75
|
outputMode?: OutputMode;
|
|
75
|
-
outputSchema?: JsonSchemaObject;
|
|
76
|
+
outputSchema?: JsonSchemaObject | false;
|
|
76
77
|
extensionBindings?: ExtensionBindings;
|
|
77
78
|
artifacts?: boolean;
|
|
78
79
|
artifactDir?: ArtifactDirPreference;
|
|
79
80
|
parentSessionFile?: string | null;
|
|
81
|
+
/** Parent session whose host-required child extension snapshot is preflighted. */
|
|
82
|
+
parentSessionId?: string;
|
|
80
83
|
/** Current parent leaf required before an implicit `defaultContext: fork` stays `fork`. */
|
|
81
84
|
parentLeafId?: string | null;
|
|
82
85
|
sessionRoot?: string;
|
|
@@ -144,6 +147,7 @@ export interface SubagentLaunchContractTools {
|
|
|
144
147
|
toolExtensionPaths: string[];
|
|
145
148
|
runtimeExtensions: string[];
|
|
146
149
|
configuredExtensions: string[];
|
|
150
|
+
requiredExtensionIds: string[];
|
|
147
151
|
extensionArgs: string[];
|
|
148
152
|
disableAmbientExtensions: boolean;
|
|
149
153
|
fanoutAuthorized: boolean;
|
|
@@ -175,7 +179,6 @@ export interface SubagentLaunchContract {
|
|
|
175
179
|
agent: SubagentLaunchContractAgent;
|
|
176
180
|
context: "fresh" | "fork";
|
|
177
181
|
model?: string;
|
|
178
|
-
modelCandidates: string[];
|
|
179
182
|
thinking?: string;
|
|
180
183
|
thinkingCeiling?: ThinkingLevel;
|
|
181
184
|
systemPromptMode: AgentConfig["systemPromptMode"];
|
|
@@ -345,6 +348,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
345
348
|
...(input.outputMode !== undefined ? { outputMode: input.outputMode } : {}),
|
|
346
349
|
...(skillInput !== undefined ? { skills: skillInput } : {}),
|
|
347
350
|
...(input.model !== undefined ? { model: input.model } : {}),
|
|
351
|
+
...(input.outputSchema !== undefined ? { outputSchema: input.outputSchema } : {}),
|
|
348
352
|
});
|
|
349
353
|
const requestedSkills = behavior.skills === false ? [] : behavior.skills;
|
|
350
354
|
const resolvedSkills = resolveSkillsWithFallback(
|
|
@@ -360,6 +364,9 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
360
364
|
if (resolvedSkills.missing.length > 0) diagnostics.push({ code: "missing_skill", severity: "error", message: `Missing skills: ${resolvedSkills.missing.join(", ")}` });
|
|
361
365
|
|
|
362
366
|
const externalRunner = agent.runner?.type === "external-cli" || agent.runner?.type === "external-job";
|
|
367
|
+
if (externalRunner && behavior.outputSchema) {
|
|
368
|
+
return { ok: false, code: "unsupported_mode", message: `Agent '${agent.name}' uses runner.type='${agent.runner?.type}' and does not support: structured output.`, diagnostics };
|
|
369
|
+
}
|
|
363
370
|
const availableModels = normalizeAvailableModels(input.availableModels);
|
|
364
371
|
const preferredProvider = agent.modelProvider ?? input.preferredProvider ?? input.parentModel?.provider;
|
|
365
372
|
const modelScopes = resolveModelScopesForAgent(discovered.modelScope, agent.name, input.parentModel);
|
|
@@ -376,19 +383,14 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
376
383
|
input.thinkingCeiling,
|
|
377
384
|
input.inheritedThinkingCeiling,
|
|
378
385
|
);
|
|
379
|
-
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel,
|
|
380
|
-
const modelCandidates = externalRunner
|
|
381
|
-
? []
|
|
382
|
-
: buildModelCandidates(primaryModel, agent.fallbackModels, availableModels, preferredProvider, {
|
|
386
|
+
const model = externalRunner ? undefined : applyThinkingSuffix(resolveModelSelection(primaryModel, availableModels, preferredProvider, {
|
|
383
387
|
scope: modelScopes,
|
|
384
388
|
primaryModelFromParent: modelOrigin === "inherited" || inheritsParentModel(input.model, agent.model, input.parentModel),
|
|
385
389
|
origin: modelOrigin,
|
|
386
|
-
})
|
|
387
|
-
.map((candidate) => applyThinkingSuffix(candidate, effectiveThinkingConfig, input.thinking !== undefined) ?? candidate);
|
|
390
|
+
}).model, effectiveThinkingConfig, input.thinking !== undefined);
|
|
388
391
|
if (!externalRunner) {
|
|
389
392
|
try {
|
|
390
393
|
assertThinkingWithinCeiling({ model, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
|
|
391
|
-
for (const candidate of modelCandidates) assertThinkingWithinCeiling({ model: candidate, configThinking: effectiveThinkingConfig, ceiling: thinkingCeiling, agent: agent.name, runId });
|
|
392
394
|
} catch (error) {
|
|
393
395
|
const message = error instanceof Error ? error.message : String(error);
|
|
394
396
|
diagnostics.push({ code: "thinking_ceiling", severity: "error", message });
|
|
@@ -398,6 +400,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
398
400
|
let toolPlan: PiLaunchToolPlan;
|
|
399
401
|
const permissionRules = resolvePermissionRules(loadConfig().permissions, agent.permissions);
|
|
400
402
|
const fast = input.fast ?? agent.fast;
|
|
403
|
+
const requiredExtensions = externalRunner ? [] : resolveRequiredChildExtensions(input.parentSessionId);
|
|
401
404
|
try {
|
|
402
405
|
toolPlan = resolvePiLaunchToolPlan({
|
|
403
406
|
tools: agent.tools,
|
|
@@ -405,13 +408,13 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
405
408
|
allowNestedSubagents: agent.allowNestedSubagents,
|
|
406
409
|
extensions: agent.extensions,
|
|
407
410
|
subagentOnlyExtensions: agent.subagentOnlyExtensions,
|
|
411
|
+
requiredExtensions,
|
|
408
412
|
mcpDirectTools: agent.mcpDirectTools,
|
|
409
413
|
cwd: effectiveCwd,
|
|
410
414
|
requireReadTool: resolvedSkills.resolved.length > 0,
|
|
411
|
-
structuredOutput: Boolean(
|
|
415
|
+
structuredOutput: Boolean(behavior.outputSchema),
|
|
412
416
|
fast,
|
|
413
417
|
model,
|
|
414
|
-
modelCandidates,
|
|
415
418
|
capabilityCeiling: effectiveCapabilityCeiling,
|
|
416
419
|
agentName: agent.name,
|
|
417
420
|
permissionRules,
|
|
@@ -448,7 +451,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
448
451
|
const binding = resolveLaunchBinding({
|
|
449
452
|
agent,
|
|
450
453
|
task: input.task ?? "",
|
|
451
|
-
|
|
454
|
+
model,
|
|
452
455
|
...(fast !== undefined ? { fast } : {}),
|
|
453
456
|
...(effectiveThinking ? { thinking: effectiveThinking } : {}),
|
|
454
457
|
systemPrompt: buildEffectiveSystemPrompt({ agent, resolvedSkills: resolvedSkills.resolved, cwd: effectiveCwd, ...(outputPath ? { outputPath } : {}) }),
|
|
@@ -456,7 +459,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
456
459
|
toolPlan,
|
|
457
460
|
...(outputPath ? { outputPath } : {}),
|
|
458
461
|
outputMode: behavior.outputMode,
|
|
459
|
-
...(
|
|
462
|
+
...(behavior.outputSchema ? { structuredOutputSchema: behavior.outputSchema } : {}),
|
|
460
463
|
...(extensionBindings ? { extensionBindings } : {}),
|
|
461
464
|
});
|
|
462
465
|
const candidates = candidateList(input.agent, agent, discovery.all);
|
|
@@ -476,7 +479,6 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
476
479
|
},
|
|
477
480
|
context,
|
|
478
481
|
...(model ? { model } : {}),
|
|
479
|
-
modelCandidates,
|
|
480
482
|
...(effectiveThinking ? { thinking: effectiveThinking } : {}),
|
|
481
483
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
482
484
|
systemPromptMode: agent.systemPromptMode,
|
|
@@ -501,7 +503,9 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
501
503
|
toolExtensionPaths: toolPlan.toolExtensionPaths,
|
|
502
504
|
runtimeExtensions: toolPlan.runtimeExtensions,
|
|
503
505
|
configuredExtensions: toolPlan.configuredExtensions,
|
|
504
|
-
|
|
506
|
+
requiredExtensionIds: toolPlan.requiredExtensions.map(({ id }) => id),
|
|
507
|
+
// Required paths are private launch authority; preflight exposes their safe IDs above.
|
|
508
|
+
extensionArgs: toolPlan.extensionArgs.filter((extensionPath) => !requiredExtensions.some(({ path }) => path === extensionPath)),
|
|
505
509
|
disableAmbientExtensions: toolPlan.disableAmbientExtensions,
|
|
506
510
|
fanoutAuthorized: toolPlan.fanoutAuthorized,
|
|
507
511
|
...(toolPlan.capabilityCeiling ? { capabilityCeiling: toolPlan.capabilityCeiling } : {}),
|
package/src/extension/config.ts
CHANGED
|
@@ -6,7 +6,6 @@ import { FLEET_KEYBINDING_ACTIONS, type ArtifactDirPreference, type ExtensionCon
|
|
|
6
6
|
import { validateMissionStoreConfig } from "../missions/store.ts";
|
|
7
7
|
import { validateAuthorityPolicy } from "../policy/authority.ts";
|
|
8
8
|
import { getAgentDir } from "../shared/utils.ts";
|
|
9
|
-
import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultTTL } from "../runs/shared/model-exclusions.ts";
|
|
10
9
|
import { validatePermissionConfig } from "../runs/shared/permissions.ts";
|
|
11
10
|
import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
|
|
12
11
|
import { normalizeWorktreeBranchPrefix } from "../runs/shared/worktree.ts";
|
|
@@ -97,17 +96,6 @@ function validateCapacityConfig(value: unknown): void {
|
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
98
|
|
|
100
|
-
/** Validate the user-controlled TTL policy before it reaches the exclusion store. */
|
|
101
|
-
// TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
|
|
102
|
-
function validateModelExclusionsConfig(value: unknown): void {
|
|
103
|
-
if (value === undefined) return;
|
|
104
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.modelExclusions must be a JSON object");
|
|
105
|
-
const defaultTtlMs = (value as Record<string, unknown>).defaultTtlMs;
|
|
106
|
-
if (defaultTtlMs !== undefined && (typeof defaultTtlMs !== "number" || !Number.isFinite(defaultTtlMs) || defaultTtlMs <= 0 || defaultTtlMs > MAX_MODEL_EXCLUSION_TTL_MS)) {
|
|
107
|
-
throw new Error(`config.modelExclusions.defaultTtlMs must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
99
|
function validateOrcaProgressTabsConfig(value: unknown): void {
|
|
112
100
|
if (value === undefined) return;
|
|
113
101
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.orcaProgressTabs must be a JSON object");
|
|
@@ -154,6 +142,13 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
154
142
|
throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
|
|
155
143
|
}
|
|
156
144
|
validateForkContextConfig(config.forkContext);
|
|
145
|
+
if (config.checkpointBeforeDeadlineMs !== undefined
|
|
146
|
+
&& (typeof config.checkpointBeforeDeadlineMs !== "number"
|
|
147
|
+
|| !Number.isInteger(config.checkpointBeforeDeadlineMs)
|
|
148
|
+
|| config.checkpointBeforeDeadlineMs <= 0
|
|
149
|
+
|| config.checkpointBeforeDeadlineMs > 2_147_483_647)) {
|
|
150
|
+
throw new Error("config.checkpointBeforeDeadlineMs must be a positive integer no larger than 2147483647");
|
|
151
|
+
}
|
|
157
152
|
if (config.foregroundDetachShortcut !== undefined
|
|
158
153
|
&& (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
|
|
159
154
|
throw new Error("config.foregroundDetachShortcut must be a valid keybinding string such as \"ctrl+b\"");
|
|
@@ -177,7 +172,7 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
177
172
|
validateFleetKeybindingsConfig(config.fleetKeybindings);
|
|
178
173
|
validateArtifactConfig(config.artifactConfig);
|
|
179
174
|
validateCapacityConfig(config.capacity);
|
|
180
|
-
|
|
175
|
+
if (config.modelExclusions !== undefined) throw new Error("config.modelExclusions was removed; model failures are no longer persisted or used for automatic switching");
|
|
181
176
|
validateModelResponseAliases(config.modelResponseAliases);
|
|
182
177
|
validateMainWindowRendererConfig(config.mainWindowRenderer);
|
|
183
178
|
validateOrcaProgressTabsConfig(config.orcaProgressTabs);
|
|
@@ -210,28 +205,6 @@ export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConf
|
|
|
210
205
|
return next;
|
|
211
206
|
}
|
|
212
207
|
|
|
213
|
-
/**
|
|
214
|
-
* Resolve the default TTL that the process-wide exclusion store should use.
|
|
215
|
-
*
|
|
216
|
-
* @param config Extension configuration after validation.
|
|
217
|
-
* @returns The configured TTL in milliseconds, or the built-in 24-hour default.
|
|
218
|
-
*/
|
|
219
|
-
// TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
|
|
220
|
-
export function resolveModelExclusionTTL(config: Pick<ExtensionConfig, "modelExclusions">): number {
|
|
221
|
-
return config.modelExclusions?.defaultTtlMs ?? DEFAULT_MODEL_EXCLUSION_TTL_MS;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Apply the configured exclusion policy to the process-wide model store.
|
|
226
|
-
*
|
|
227
|
-
* @param config Extension configuration after validation.
|
|
228
|
-
* @returns Nothing.
|
|
229
|
-
*/
|
|
230
|
-
// TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
|
|
231
|
-
export function applyModelExclusionsConfig(config: Pick<ExtensionConfig, "modelExclusions">): void {
|
|
232
|
-
setDefaultTTL(resolveModelExclusionTTL(config), { shortenExisting: config.modelExclusions?.defaultTtlMs !== undefined });
|
|
233
|
-
}
|
|
234
|
-
|
|
235
208
|
export function resolveAsyncByDefault(config: Pick<ExtensionConfig, "asyncByDefault">): boolean {
|
|
236
209
|
return config.asyncByDefault !== false;
|
|
237
210
|
}
|
|
@@ -242,12 +215,12 @@ export function loadConfig(): ExtensionConfig {
|
|
|
242
215
|
return readConfigForUpdate(configPath);
|
|
243
216
|
} catch (error) {
|
|
244
217
|
if (error instanceof PrunedForkConfigError) throw error;
|
|
245
|
-
// Explicit route identity and
|
|
218
|
+
// Explicit route identity, worktree, and checkpoint policies must not be silently
|
|
246
219
|
// discarded and replaced by the built-in defaults after validation fails.
|
|
247
220
|
try {
|
|
248
221
|
const raw = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
|
|
249
222
|
if (raw && typeof raw === "object" && !Array.isArray(raw)
|
|
250
|
-
&& (Object.hasOwn(raw, "worktreeProvider") || Object.hasOwn(raw, "worktreeBranchPrefix") || Object.hasOwn(raw, "modelResponseAliases"))) throw error;
|
|
223
|
+
&& (Object.hasOwn(raw, "worktreeProvider") || Object.hasOwn(raw, "worktreeBranchPrefix") || Object.hasOwn(raw, "modelResponseAliases") || Object.hasOwn(raw, "modelExclusions") || Object.hasOwn(raw, "checkpointBeforeDeadlineMs"))) throw error;
|
|
251
224
|
} catch (readError) {
|
|
252
225
|
if (readError === error) throw error;
|
|
253
226
|
}
|
|
@@ -190,6 +190,8 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
|
|
|
190
190
|
};
|
|
191
191
|
},
|
|
192
192
|
});
|
|
193
|
+
const hasPendingSupervisorRequest = supervisorChannel.hasPendingRequests;
|
|
194
|
+
childConfig.hasPendingSupervisorRequest = hasPendingSupervisorRequest;
|
|
193
195
|
const executor = createSubagentExecutor({
|
|
194
196
|
pi,
|
|
195
197
|
state,
|
|
@@ -245,6 +247,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, c
|
|
|
245
247
|
asyncChildren.clear();
|
|
246
248
|
foregroundChannels.clear();
|
|
247
249
|
supervisorChannel.dispose();
|
|
250
|
+
if (childConfig.hasPendingSupervisorRequest === hasPendingSupervisorRequest) childConfig.hasPendingSupervisorRequest = undefined;
|
|
248
251
|
state.supervisorOwnerSessionId = null;
|
|
249
252
|
});
|
|
250
253
|
const route = resolveNestedControlRoute(childConfig);
|
|
@@ -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
|
+
}
|