pi-subagents 0.64.0 → 0.65.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 +33 -0
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +18 -16
- package/docs/configuration.md +7 -15
- package/docs/extension-api.md +15 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +7 -6
- package/docs/watchdog.md +1 -1
- package/docs/workflows.md +5 -3
- package/package.json +3 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/src/agents/agent-management.ts +47 -15
- package/src/api/capability-ceiling.ts +0 -1
- package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/src/api/preflight.ts +2 -3
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +27 -5
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -8
- package/src/intercom/native-supervisor-channel.ts +138 -60
- package/src/intercom/supervisor-ui.ts +243 -0
- package/src/runs/background/async-execution.ts +43 -24
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +11 -2
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/run-child-session.ts +613 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +125 -0
- package/src/runs/background/runner-child-sessions.ts +31 -0
- package/src/runs/background/scheduled-runs.ts +18 -4
- package/src/runs/background/subagent-runner.ts +184 -901
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +189 -377
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +100 -57
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- package/src/runs/shared/capability-ceiling.ts +1 -2
- package/src/runs/shared/child-hooks.ts +25 -0
- package/src/runs/shared/child-identity.ts +13 -2
- package/src/runs/shared/child-launch.ts +314 -0
- package/src/runs/shared/child-lifecycle.ts +25 -0
- package/src/runs/shared/child-runtime-config.ts +126 -0
- package/src/runs/shared/child-session.ts +342 -0
- package/src/runs/shared/child-tool-plan.ts +530 -0
- package/src/runs/shared/claude-code-adapter.ts +5 -1
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/external-cli-preflight.ts +16 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +82 -14
- package/src/runs/shared/model-fallback.ts +47 -4
- package/src/runs/shared/nested-events.ts +29 -45
- package/src/runs/shared/nested-path.ts +0 -14
- package/src/runs/shared/orca-progress-tabs.ts +12 -7
- package/src/runs/shared/parallel-utils.ts +0 -2
- package/src/runs/shared/permissions.ts +0 -13
- package/src/runs/shared/process-signal.ts +4 -1
- package/src/runs/shared/run-fanout-budget.ts +0 -13
- package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
- package/src/runs/shared/structured-output.ts +17 -4
- package/src/runs/shared/subagent-control.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
- package/src/runs/shared/worktree.ts +117 -30
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +55 -28
- package/src/shared/utils.ts +3 -4
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +233 -31
- package/src/watchdog/child-status.ts +0 -1
- package/src/watchdog/register-child.ts +12 -12
- package/src/workflows/scripted-workflow.ts +48 -1
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1059
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
|
@@ -32,6 +32,7 @@ import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/m
|
|
|
32
32
|
import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
|
|
33
33
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
34
34
|
import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, resolveExternalCliRunnerStatus, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
|
|
35
|
+
import { resolveExternalCliBinaryAvailability, type ExternalCliBinaryAvailability } from "../runs/shared/external-cli-preflight.ts";
|
|
35
36
|
import type { AcceptanceInput, AgentCapabilitiesSnapshot, AgentCapabilityRow, Details, ExtensionConfig, ToolBudgetConfig } from "../shared/types.ts";
|
|
36
37
|
import { getProjectConfigDir } from "../shared/utils.ts";
|
|
37
38
|
import { previewDisplayText } from "../shared/display-text.ts";
|
|
@@ -704,17 +705,34 @@ function externalJobProviderSuffix(provider: string, names: Set<string> | undefi
|
|
|
704
705
|
return names.has(provider) ? "✓" : "missing";
|
|
705
706
|
}
|
|
706
707
|
|
|
707
|
-
|
|
708
|
+
type ExternalCliAvailabilityByCommand = ReadonlyMap<string, ExternalCliBinaryAvailability>;
|
|
709
|
+
|
|
710
|
+
function externalCliAvailabilityForAgents(agents: readonly AgentConfig[]): ExternalCliAvailabilityByCommand {
|
|
711
|
+
const availability = new Map<string, ExternalCliBinaryAvailability>();
|
|
712
|
+
for (const agent of agents) {
|
|
713
|
+
const runner = agent.runner;
|
|
714
|
+
if (runner?.type === "external-cli" && !availability.has(runner.command)) {
|
|
715
|
+
availability.set(runner.command, resolveExternalCliBinaryAvailability(runner.command, process.env));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return availability;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function runnerListBadge(agent: AgentConfig, providerNames: Set<string> | undefined, externalCliAvailability?: ExternalCliAvailabilityByCommand): string | undefined {
|
|
708
722
|
if (agent.runner?.type === "external-job") return `external-job:${agent.runner.provider} ${externalJobProviderSuffix(agent.runner.provider, providerNames)}`;
|
|
709
|
-
if (agent.runner?.type === "external-cli")
|
|
723
|
+
if (agent.runner?.type === "external-cli") {
|
|
724
|
+
const availability = externalCliAvailability?.get(agent.runner.command);
|
|
725
|
+
if (!availability) return "external-cli";
|
|
726
|
+
return `external-cli:${agent.runner.command} ${availability.available ? "✓" : "missing"}`;
|
|
727
|
+
}
|
|
710
728
|
return undefined;
|
|
711
729
|
}
|
|
712
730
|
|
|
713
|
-
function agentListMetadata(agent: AgentConfig, providerNames: Set<string> | undefined): string {
|
|
731
|
+
function agentListMetadata(agent: AgentConfig, providerNames: Set<string> | undefined, externalCliAvailability?: ExternalCliAvailabilityByCommand): string {
|
|
714
732
|
const source = agent.source === "package" ? packageSourceLabel(agent) : agent.source;
|
|
715
733
|
return [
|
|
716
734
|
source,
|
|
717
|
-
runnerListBadge(agent, providerNames),
|
|
735
|
+
runnerListBadge(agent, providerNames, externalCliAvailability),
|
|
718
736
|
agent.defaultContext ? `context: ${agent.defaultContext}` : undefined,
|
|
719
737
|
agent.aliases?.length ? `aliases: ${agent.aliases.join(", ")}` : undefined,
|
|
720
738
|
].filter((part): part is string => Boolean(part)).join(", ");
|
|
@@ -724,7 +742,7 @@ function formatAgentListLine(agent: AgentConfig, providerNames: Set<string> | un
|
|
|
724
742
|
return `- ${agent.name} (${agentListMetadata(agent, providerNames)}): ${agent.description}`;
|
|
725
743
|
}
|
|
726
744
|
|
|
727
|
-
function formatAgentCapabilitiesLine(agent: AgentConfig, providerNames: Set<string> | undefined): string {
|
|
745
|
+
function formatAgentCapabilitiesLine(agent: AgentConfig, providerNames: Set<string> | undefined, externalCliAvailability?: ExternalCliAvailabilityByCommand): string {
|
|
728
746
|
const declaredTools = [
|
|
729
747
|
...(agent.tools ?? []),
|
|
730
748
|
...(agent.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`),
|
|
@@ -742,7 +760,7 @@ function formatAgentCapabilitiesLine(agent: AgentConfig, providerNames: Set<stri
|
|
|
742
760
|
if (agent.modelProvider && !agent.model.includes("/")) model = `${agent.modelProvider}/${agent.model}`;
|
|
743
761
|
}
|
|
744
762
|
const thinking = agent.thinking === false ? "off" : agent.thinking ?? "default";
|
|
745
|
-
return `- ${agent.name} (${agentListMetadata(agent, providerNames)}): Description: ${previewDisplayText(agent.description, 240)}; Tools: ${tools}; Model: ${model}; Thinking: ${thinking}`;
|
|
763
|
+
return `- ${agent.name} (${agentListMetadata(agent, providerNames, externalCliAvailability)}): Description: ${previewDisplayText(agent.description, 240)}; Tools: ${tools}; Model: ${model}; Thinking: ${thinking}`;
|
|
746
764
|
}
|
|
747
765
|
|
|
748
766
|
const EXTERNAL_JOB_CAPABILITIES = { stop: false, steer: false, resume: false, structuredOutput: false, toolEvents: false } as const;
|
|
@@ -752,10 +770,19 @@ function listOrEmpty<T>(values: T[] | undefined): T[] {
|
|
|
752
770
|
return values ?? [];
|
|
753
771
|
}
|
|
754
772
|
|
|
755
|
-
function agentCapabilityRunner(agent: AgentConfig, providerNames: Set<string> | undefined): AgentCapabilityRow["runner"] {
|
|
773
|
+
function agentCapabilityRunner(agent: AgentConfig, providerNames: Set<string> | undefined, externalCliAvailability: ExternalCliAvailabilityByCommand): AgentCapabilityRow["runner"] {
|
|
756
774
|
const runner = agent.runner;
|
|
757
775
|
if (!runner || runner.type === "pi") return PI_AGENT_RUNNER;
|
|
758
|
-
if (runner.type === "external-cli")
|
|
776
|
+
if (runner.type === "external-cli") {
|
|
777
|
+
const availability = externalCliAvailability.get(runner.command)!;
|
|
778
|
+
return {
|
|
779
|
+
type: "external-cli",
|
|
780
|
+
adapter: runner.adapter,
|
|
781
|
+
command: runner.command,
|
|
782
|
+
...availability,
|
|
783
|
+
capabilities: resolveExternalCliRunnerStatus(runner).capabilities,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
759
786
|
return { type: "external-job", provider: runner.provider, available: providerNames?.has(runner.provider), capabilities: EXTERNAL_JOB_CAPABILITIES };
|
|
760
787
|
}
|
|
761
788
|
|
|
@@ -769,7 +796,7 @@ function agentCapabilityTools(agent: AgentConfig): AgentCapabilityRow["tools"] {
|
|
|
769
796
|
};
|
|
770
797
|
}
|
|
771
798
|
|
|
772
|
-
function agentCapabilityRow(agent: AgentConfig, options: { executable: boolean; providerNames?: Set<string>; restrictionSources?: string[] }): AgentCapabilityRow {
|
|
799
|
+
function agentCapabilityRow(agent: AgentConfig, options: { executable: boolean; providerNames?: Set<string>; externalCliAvailability: ExternalCliAvailabilityByCommand; restrictionSources?: string[] }): AgentCapabilityRow {
|
|
773
800
|
return {
|
|
774
801
|
name: agent.name,
|
|
775
802
|
description: previewDisplayText(agent.description, 1000),
|
|
@@ -777,7 +804,7 @@ function agentCapabilityRow(agent: AgentConfig, options: { executable: boolean;
|
|
|
777
804
|
executable: options.executable,
|
|
778
805
|
restrictionSources: options.executable ? undefined : options.restrictionSources ?? [],
|
|
779
806
|
aliases: agent.aliases ? [...agent.aliases] : undefined,
|
|
780
|
-
runner: agentCapabilityRunner(agent, options.providerNames),
|
|
807
|
+
runner: agentCapabilityRunner(agent, options.providerNames, options.externalCliAvailability),
|
|
781
808
|
tools: agentCapabilityTools(agent),
|
|
782
809
|
model: presentDetails({ value: agent.model, fallbackModels: agent.fallbackModels, thinking: agent.thinking }),
|
|
783
810
|
execution: presentDetails({ defaultAsync: agent.defaultAsync, timeoutMs: agent.defaultTimeoutMs }),
|
|
@@ -786,11 +813,11 @@ function agentCapabilityRow(agent: AgentConfig, options: { executable: boolean;
|
|
|
786
813
|
};
|
|
787
814
|
}
|
|
788
815
|
|
|
789
|
-
function agentCapabilitiesSnapshot(input: { agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; restrictedSources?: string[] }): AgentCapabilitiesSnapshot {
|
|
816
|
+
function agentCapabilitiesSnapshot(input: { agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; externalCliAvailability: ExternalCliAvailabilityByCommand; restrictedSources?: string[] }): AgentCapabilitiesSnapshot {
|
|
790
817
|
return {
|
|
791
818
|
agents: [
|
|
792
|
-
...input.agents.map((agent) => agentCapabilityRow(agent, { executable: true, providerNames: input.providerNames })),
|
|
793
|
-
...input.restrictedAgents.map((agent) => agentCapabilityRow(agent, { executable: false, providerNames: input.providerNames, restrictionSources: input.restrictedSources })),
|
|
819
|
+
...input.agents.map((agent) => agentCapabilityRow(agent, { executable: true, providerNames: input.providerNames, externalCliAvailability: input.externalCliAvailability })),
|
|
820
|
+
...input.restrictedAgents.map((agent) => agentCapabilityRow(agent, { executable: false, providerNames: input.providerNames, externalCliAvailability: input.externalCliAvailability, restrictionSources: input.restrictedSources })),
|
|
794
821
|
],
|
|
795
822
|
restrictedCount: input.restrictedAgents.length,
|
|
796
823
|
...(input.restrictedSources?.length ? { capabilityCeilingSources: [...input.restrictedSources] } : {}),
|
|
@@ -824,13 +851,14 @@ function appendAgentDiagnosticLines(lines: string[], diagnostics: AgentDiscovery
|
|
|
824
851
|
);
|
|
825
852
|
}
|
|
826
853
|
|
|
827
|
-
function agentCapabilityDetails(input: { capabilityMode: boolean; agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; restrictedSources?: string[] }): Partial<Details> | undefined {
|
|
854
|
+
function agentCapabilityDetails(input: { capabilityMode: boolean; agents: AgentConfig[]; restrictedAgents: AgentConfig[]; providerNames?: Set<string>; externalCliAvailability: ExternalCliAvailabilityByCommand; restrictedSources?: string[] }): Partial<Details> | undefined {
|
|
828
855
|
if (!input.capabilityMode) return undefined;
|
|
829
856
|
return {
|
|
830
857
|
agentCapabilities: jsonDetails(agentCapabilitiesSnapshot({
|
|
831
858
|
agents: input.agents,
|
|
832
859
|
restrictedAgents: input.restrictedAgents,
|
|
833
860
|
providerNames: input.providerNames,
|
|
861
|
+
externalCliAvailability: input.externalCliAvailability,
|
|
834
862
|
restrictedSources: input.restrictedSources,
|
|
835
863
|
})),
|
|
836
864
|
};
|
|
@@ -936,7 +964,10 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
936
964
|
const providerStatus = registeredExternalJobProviderStatus();
|
|
937
965
|
const providerNameSet = providerNames(providerStatus);
|
|
938
966
|
const capabilityMode = params.capabilities === true;
|
|
939
|
-
const
|
|
967
|
+
const externalCliAvailability = capabilityMode ? externalCliAvailabilityForAgents([...agents, ...restrictedAgents]) : undefined;
|
|
968
|
+
const formatLine = capabilityMode
|
|
969
|
+
? (agent: AgentConfig, names: Set<string> | undefined) => formatAgentCapabilitiesLine(agent, names, externalCliAvailability)
|
|
970
|
+
: formatAgentListLine;
|
|
940
971
|
const lines = [
|
|
941
972
|
capabilityMode ? "Executable agents (capabilities):" : "Executable agents:",
|
|
942
973
|
...formatAgentListSections(agents, providerNameSet, formatLine),
|
|
@@ -950,6 +981,7 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
|
|
|
950
981
|
agents,
|
|
951
982
|
restrictedAgents,
|
|
952
983
|
providerNames: providerNameSet,
|
|
984
|
+
externalCliAvailability: externalCliAvailability ?? new Map(),
|
|
953
985
|
restrictedSources,
|
|
954
986
|
}));
|
|
955
987
|
}
|
package/src/api/preflight.ts
CHANGED
|
@@ -7,11 +7,11 @@ import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } f
|
|
|
7
7
|
import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
|
|
8
8
|
import { buildModelCandidates, inheritsParentModel, resolveEffectiveSubagentModel, resolveModelOrigin, type AvailableModelInfo, type ParentModel } from "../runs/shared/model-fallback.ts";
|
|
9
9
|
import { resolveModelScopesForAgent } from "../runs/shared/model-scope.ts";
|
|
10
|
-
import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/
|
|
10
|
+
import { applyThinkingSuffix, resolvePiLaunchToolPlan, type PiLaunchToolPlan } from "../runs/shared/child-tool-plan.ts";
|
|
11
11
|
import { injectOutputPathSystemPrompt, normalizeSingleOutputOverride, resolveSingleOutputPath } from "../runs/shared/single-output.ts";
|
|
12
12
|
import { getArtifactPaths, getArtifactsDir } from "../shared/artifacts.ts";
|
|
13
13
|
import { resolveEffectiveThinking } from "../shared/model-info.ts";
|
|
14
|
-
import { assertThinkingWithinCeiling,
|
|
14
|
+
import { assertThinkingWithinCeiling, intersectThinkingCeilings, type ThinkingLevel } from "../shared/thinking-ceiling.ts";
|
|
15
15
|
import { SUBAGENT_LIFECYCLE_ARTIFACT_VERSION, type ArtifactDirPreference, type ArtifactPaths, type JsonSchemaObject, type OutputMode } from "../shared/types.ts";
|
|
16
16
|
import { capabilityCeilingAgentRestrictionMessage, intersectSubagentCapabilityCeilings, type ResolvedSubagentCapabilityCeiling, type SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
|
|
17
17
|
import { resolvePermissionRules } from "../runs/shared/permissions.ts";
|
|
@@ -330,7 +330,6 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
330
330
|
discovered.maxThinking,
|
|
331
331
|
input.thinkingCeiling,
|
|
332
332
|
input.inheritedThinkingCeiling,
|
|
333
|
-
decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
|
|
334
333
|
);
|
|
335
334
|
const model = externalRunner ? undefined : applyThinkingSuffix(primaryModel, effectiveThinkingConfig, input.thinking !== undefined);
|
|
336
335
|
const modelCandidates = externalRunner
|
package/src/extension/doctor.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { discoverAgentsAll, type AgentSource } from "../agents/agents.ts";
|
|
|
4
4
|
import { isAsyncAvailable } from "../runs/background/async-execution.ts";
|
|
5
5
|
import { formatSpawnBudgetSummary, getSpawnBudgetSnapshot } from "../runs/shared/spawn-budget.ts";
|
|
6
6
|
import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "../intercom/intercom-bridge.ts";
|
|
9
9
|
import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
|
|
10
10
|
import {
|
|
@@ -178,14 +178,6 @@ function formatSpawnBudgetSection(input: DoctorReportInput): string[] {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
function formatRunFanoutSection(input: DoctorReportInput): string[] {
|
|
181
|
-
try {
|
|
182
|
-
const inherited = decodeRunFanoutBudgetDescriptor(process.env[RUN_FANOUT_BUDGET_ENV]);
|
|
183
|
-
if (inherited) {
|
|
184
|
-
return [`- usage: ${formatRunFanoutBudget(getRunFanoutBudgetSnapshot(inherited)).replace(/^Run fan-out: /, "")}`, `- root run: ${inherited.rootRunId}`, "- reset boundary: cumulative claims are never released; a new top-level run creates a new budget"];
|
|
185
|
-
}
|
|
186
|
-
} catch (error) {
|
|
187
|
-
return [`- inherited budget: invalid — ${errorText(error)}`];
|
|
188
|
-
}
|
|
189
181
|
const configured = resolveMaxSubagentSpawnsPerRun(input.config.maxSubagentSpawnsPerRun);
|
|
190
182
|
const source = normalizeMaxSubagentSpawnsPerRun(process.env.PI_SUBAGENT_MAX_SPAWNS_PER_RUN) !== undefined
|
|
191
183
|
? "environment"
|
|
@@ -214,7 +206,7 @@ function formatPermissionSystemSection(): string[] {
|
|
|
214
206
|
if (trimmed) {
|
|
215
207
|
lines.push(`- parent session: set (${trimmed})`);
|
|
216
208
|
} else {
|
|
217
|
-
lines.push("- parent session: not set — ask forwarding from
|
|
209
|
+
lines.push("- parent session: not set — ask forwarding from background children will not reach a parent UI");
|
|
218
210
|
}
|
|
219
211
|
const isChild = process.env["PI_SUBAGENT_CHILD"] === "1";
|
|
220
212
|
lines.push(`- subagent process: ${isChild ? "yes (PI_SUBAGENT_CHILD=1)" : "no"}`);
|
|
@@ -6,8 +6,8 @@ import { discoverAgents } from "../agents/agents.ts";
|
|
|
6
6
|
import { getArtifactsDir } from "../shared/artifacts.ts";
|
|
7
7
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
8
8
|
import { resolveWaitToolConfig } from "../runs/background/wait-config.ts";
|
|
9
|
-
import {
|
|
10
|
-
import { readNestedControlRequests,
|
|
9
|
+
import type { ChildRuntimeConfig } from "../runs/shared/child-runtime-config.ts";
|
|
10
|
+
import { readNestedControlRequests, resolveInheritedNestedRoute, type NestedRoute, writeNestedControlResult } from "../runs/shared/nested-events.ts";
|
|
11
11
|
import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
|
|
12
12
|
import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
|
|
13
13
|
import { createSubagentParamsSchema } from "./schemas.ts";
|
|
@@ -51,12 +51,8 @@ function createChildSafeState(): SubagentState {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function resolveNestedControlRoute(): NestedRoute | undefined {
|
|
55
|
-
|
|
56
|
-
return resolveNestedRouteFromEnv();
|
|
57
|
-
} catch {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
54
|
+
function resolveNestedControlRoute(config: ChildRuntimeConfig): NestedRoute | undefined {
|
|
55
|
+
return config.nestedRoute ? resolveInheritedNestedRoute(config.nestedRoute) : undefined;
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
function nestedControlRouteKey(route: NestedRoute): string {
|
|
@@ -145,8 +141,9 @@ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState,
|
|
|
145
141
|
return () => clearInterval(timer);
|
|
146
142
|
}
|
|
147
143
|
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
/** Register the child-side `subagent` tool for fanout-authorized children. */
|
|
145
|
+
export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI, childConfig: ChildRuntimeConfig): void {
|
|
146
|
+
if (!childConfig.fanoutChild) return;
|
|
150
147
|
|
|
151
148
|
const globalStore = globalThis as Record<string, unknown>;
|
|
152
149
|
const registeredKey = "__piSubagentFanoutChildRegisteredApis";
|
|
@@ -172,6 +169,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
172
169
|
expandTilde,
|
|
173
170
|
discoverAgents,
|
|
174
171
|
allowMutatingManagementActions: false,
|
|
172
|
+
childRuntime: childConfig,
|
|
175
173
|
});
|
|
176
174
|
|
|
177
175
|
const params = createSubagentParamsSchema();
|
|
@@ -190,7 +188,7 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
190
188
|
};
|
|
191
189
|
|
|
192
190
|
pi.registerTool(tool);
|
|
193
|
-
const route = resolveNestedControlRoute();
|
|
191
|
+
const route = resolveNestedControlRoute(childConfig);
|
|
194
192
|
if (!route) return;
|
|
195
193
|
const listenerCleanupKey = "__piSubagentFanoutChildNestedControlInboxCleanups";
|
|
196
194
|
const listenerCleanups = globalStore[listenerCleanupKey] instanceof Map
|
package/src/extension/index.ts
CHANGED
|
@@ -46,6 +46,13 @@ import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template
|
|
|
46
46
|
import { registerMainWatchdog } from "../watchdog/register-main.ts";
|
|
47
47
|
import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
|
|
48
48
|
import { createNativeSupervisorChannel } from "../intercom/native-supervisor-channel.ts";
|
|
49
|
+
import {
|
|
50
|
+
renderSupervisorReply,
|
|
51
|
+
renderSupervisorRequest,
|
|
52
|
+
SUPERVISOR_REPLY_ENTRY_TYPE,
|
|
53
|
+
SUPERVISOR_REQUEST_MESSAGE_TYPE,
|
|
54
|
+
type SupervisorRequestMessageDetails,
|
|
55
|
+
} from "../intercom/supervisor-ui.ts";
|
|
49
56
|
import { registerHerdrStatusBridge, type HerdrStatusRun } from "../integrations/herdr-status.ts";
|
|
50
57
|
import { listHerdrProjectPaneRoots, restoreHerdrProjectPaneSnapshots } from "../inspectors/herdr/project-panes.ts";
|
|
51
58
|
import { registerSubagentRpcBridge } from "./rpc.ts";
|
|
@@ -56,7 +63,8 @@ import { createWaitSubscriptionManager } from "../runs/background/wait-subscript
|
|
|
56
63
|
import { drainOutstandingWork } from "../runs/background/auto-drain.ts";
|
|
57
64
|
import registerSubagentNotify, { parseSubagentNotifyContent, type SubagentNotifyDetails } from "../runs/background/notify.ts";
|
|
58
65
|
import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_MESSAGE_TYPE, type SubagentSteeringMessageDetails } from "./steering-notices.ts";
|
|
59
|
-
import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/
|
|
66
|
+
import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/child-runtime-config.ts";
|
|
67
|
+
import { disposeChildSessions } from "../runs/shared/child-session.ts";
|
|
60
68
|
import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
61
69
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
62
70
|
import { applyModelExclusionsConfig, loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
|
|
@@ -546,6 +554,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
546
554
|
const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose: disposeAsyncJobTracker } = createAsyncJobTracker(pi, state, DIRS.async, {
|
|
547
555
|
widgetEnabled: asyncWidgetEnabled,
|
|
548
556
|
onJobTerminal: () => refreshResultDelivery(),
|
|
557
|
+
supervisorRequestState: supervisorChannel.getSupervisorRequestState,
|
|
549
558
|
});
|
|
550
559
|
const resultWatcher = createResultWatcher(
|
|
551
560
|
pi,
|
|
@@ -601,6 +610,14 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
601
610
|
});
|
|
602
611
|
executorScheduled = executor.executeScheduled;
|
|
603
612
|
|
|
613
|
+
pi.registerMessageRenderer<SupervisorRequestMessageDetails>(SUPERVISOR_REQUEST_MESSAGE_TYPE, renderSupervisorRequest);
|
|
614
|
+
const registerEntryRenderer = (pi as unknown as {
|
|
615
|
+
registerEntryRenderer?: (customType: string, renderer: (entry: { data?: unknown }, options: { expanded: boolean }, theme: ExtensionContext["ui"]["theme"]) => Component | undefined) => void;
|
|
616
|
+
}).registerEntryRenderer;
|
|
617
|
+
if (typeof registerEntryRenderer === "function") {
|
|
618
|
+
registerEntryRenderer.call(pi, SUPERVISOR_REPLY_ENTRY_TYPE, renderSupervisorReply);
|
|
619
|
+
}
|
|
620
|
+
|
|
604
621
|
pi.registerMessageRenderer<SlashMessageDetails>(SLASH_RESULT_TYPE, (message, options, theme) => {
|
|
605
622
|
const details = resolveSlashMessageDetails(message.details);
|
|
606
623
|
if (!details) return undefined;
|
|
@@ -906,10 +923,10 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
906
923
|
const projectPaneOwnerRoot = path.resolve(ctx.cwd);
|
|
907
924
|
restoreHerdrProjectPaneSnapshots(state, [...new Set([...(state.herdrProjectPanes?.keys() ?? []), ...listHerdrProjectPaneRoots(projectPaneOwnerRoot), projectPaneOwnerRoot])]);
|
|
908
925
|
// Set PI_SUBAGENT_PARENT_SESSION for permission-system forwarding.
|
|
909
|
-
// Only set in the root session (the interactive UI session), not in
|
|
910
|
-
// child
|
|
911
|
-
//
|
|
912
|
-
//
|
|
926
|
+
// Only set in the root session (the interactive UI session), not in a
|
|
927
|
+
// child host: the runner process inherits the parent's value through
|
|
928
|
+
// its environment at spawn time and must not overwrite it with a child
|
|
929
|
+
// session's identity.
|
|
913
930
|
if (!process.env[SUBAGENT_CHILD_ENV]) {
|
|
914
931
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
915
932
|
if (sessionId) {
|
|
@@ -1088,6 +1105,11 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1088
1105
|
|
|
1089
1106
|
pi.on("session_shutdown", async () => {
|
|
1090
1107
|
runtimeEntry.cleanup();
|
|
1108
|
+
try {
|
|
1109
|
+
await disposeChildSessions();
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
console.error("Failed to dispose in-process child sessions:", error);
|
|
1112
|
+
}
|
|
1091
1113
|
await herdrStatusBridge.flush();
|
|
1092
1114
|
});
|
|
1093
1115
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeWorktreeBaseRef } from "../runs/shared/worktree.ts";
|
|
2
|
+
|
|
1
3
|
export interface PublicSubagentExecutionParams {
|
|
2
4
|
action?: unknown;
|
|
3
5
|
capabilities?: unknown;
|
|
@@ -28,6 +30,7 @@ export interface PublicSubagentExecutionParams {
|
|
|
28
30
|
preflight?: unknown;
|
|
29
31
|
isolation?: unknown;
|
|
30
32
|
worktree?: unknown;
|
|
33
|
+
baseRef?: unknown;
|
|
31
34
|
lane?: unknown;
|
|
32
35
|
async?: unknown;
|
|
33
36
|
output?: unknown;
|
|
@@ -69,6 +72,14 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
69
72
|
return { ok: false, error: "Public execution does not accept workflow resource provenance or permit fields.", mode: params.action === undefined ? "workflow" : "management" };
|
|
70
73
|
}
|
|
71
74
|
}
|
|
75
|
+
if (params.baseRef !== undefined) {
|
|
76
|
+
if (typeof params.baseRef !== "string") return { ok: false, error: "baseRef must be a valid Git ref.", mode: params.action === undefined ? "workflow" : "management" };
|
|
77
|
+
try {
|
|
78
|
+
normalizeWorktreeBaseRef(params.baseRef);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error), mode: params.action === undefined ? "workflow" : "management" };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
72
83
|
if (params.workflowScript !== undefined && params.workflowScriptPath !== undefined) {
|
|
73
84
|
return { ok: false, error: "workflowScript and workflowScriptPath are mutually exclusive.", mode: "workflow" };
|
|
74
85
|
}
|
|
@@ -122,6 +133,9 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
122
133
|
return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
|
|
123
134
|
}
|
|
124
135
|
const normalizedAction = typeof action === "string" ? action.trim() : undefined;
|
|
136
|
+
if (params.baseRef !== undefined && normalizedAction !== undefined && normalizedAction !== "resume" && normalizedAction !== "schedule.create") {
|
|
137
|
+
return { ok: false, error: "baseRef is only supported for child execution, resume, and schedule.create.", mode: "management" };
|
|
138
|
+
}
|
|
125
139
|
if (normalizedAction !== undefined && hasNamedWorkflow) {
|
|
126
140
|
return { ok: false, error: "Named workflow resource execution must omit action.", mode: "management" };
|
|
127
141
|
}
|
package/src/extension/rpc.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { readStatus } from "../shared/utils.ts";
|
|
|
24
24
|
import { SubagentParams } from "./schemas.ts";
|
|
25
25
|
import { normalizePublicSubagentExecution } from "./public-execution.ts";
|
|
26
26
|
import { ASYNC_STATUS_SNAPSHOT_KIND, ASYNC_STATUS_SNAPSHOT_VERSION, buildAsyncStatusSnapshotForState } from "../runs/background/async-status-snapshot.ts";
|
|
27
|
-
import { isStoppableAsyncStatusStep, resolveAsyncStatusChild, type ResolvedAsyncStatusChild } from "../runs/shared/child-identity.ts";
|
|
27
|
+
import { isStoppableAsyncStatusStep, resolveAsyncStatusChild, stopStoppableAsyncStatusChildren, type ResolvedAsyncStatusChild } from "../runs/shared/child-identity.ts";
|
|
28
28
|
|
|
29
29
|
export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
|
|
30
30
|
export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
|
|
@@ -616,8 +616,8 @@ function stopAsyncRun(
|
|
|
616
616
|
}
|
|
617
617
|
}
|
|
618
618
|
if (initialStatus.mode === "workflow" && initialStatus.state === "running") {
|
|
619
|
+
const stopChild = options.state?.workflowChildStops?.get(initialRunId);
|
|
619
620
|
if (child) {
|
|
620
|
-
const stopChild = options.state?.workflowChildStops?.get(initialRunId);
|
|
621
621
|
if (stopChild) {
|
|
622
622
|
if (!stopChild(child.id, `Workflow child '${child.id}' stopped by RPC.`)) throw new SubagentRpcError("invalid_state", `Child '${childId}' in workflow ${initialRunId} is not available to stop.`);
|
|
623
623
|
emitChildStopping(initialRunId, location.asyncDir, child);
|
|
@@ -633,6 +633,7 @@ function stopAsyncRun(
|
|
|
633
633
|
}
|
|
634
634
|
const workflowController = options.state?.workflowControllers?.get(initialRunId);
|
|
635
635
|
if (workflowController && !child) {
|
|
636
|
+
stopStoppableAsyncStatusChildren(initialStatus, stopChild, "Workflow stopped by RPC.");
|
|
636
637
|
workflowController.abort(new Error("Workflow stopped by RPC."));
|
|
637
638
|
return {
|
|
638
639
|
runId: initialRunId,
|
package/src/extension/schemas.ts
CHANGED
|
@@ -343,7 +343,7 @@ const SubagentParamProperties = {
|
|
|
343
343
|
})),
|
|
344
344
|
workflow: Type.Optional(Type.String({ minLength: 1, description: "Extension-owned workflow resource; resolves its script and authority internally." })),
|
|
345
345
|
args: Type.Optional(Type.Unsafe({ type: "object", maxProperties: 16, additionalProperties: true, description: "Bounded plain-JSON args for workflow; resource validation applies." })),
|
|
346
|
-
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Inline JavaScript statement body with unknown resource provenance. Normally async unless asyncByDefault:false; set async:true
|
|
346
|
+
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Inline JavaScript statement body with unknown resource provenance. Normally async unless asyncByDefault:false; set async:true for async workflows and async:false only when the parent must block. Use explicit return, top-level await, plain helper functions, or explicit Promise chains. Nested async function, arrow, and method helpers are rejected. Globals: runs, emit, console, and mission state when enabled. No filesystem, shell, Pi tools, or host globals except through runs.host." })),
|
|
347
347
|
workflowScriptPath: Type.Optional(Type.String({ minLength: 1, description: "Path to a JavaScript workflow file with unknown resource provenance. Mutually exclusive with workflowScript and workflow. Relative paths resolve against the request cwd. The host reads the file before the filesystem-free workflow sandbox starts." })),
|
|
348
348
|
globalConcurrencyLimit: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
349
349
|
maxSubagentSpawnsPerRun: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
@@ -351,6 +351,7 @@ const SubagentParamProperties = {
|
|
|
351
351
|
chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise. Explicit live-card requires same-repository async:false; async workflows should omit chatProgress or use auto/off." })),
|
|
352
352
|
isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
|
|
353
353
|
worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
|
|
354
|
+
baseRef: Type.Optional(Type.String()),
|
|
354
355
|
lane: Type.Optional(WorkflowLaneMetadata),
|
|
355
356
|
context: Type.Optional(Type.String({
|
|
356
357
|
enum: ["fresh", "fork", "profile"],
|
|
@@ -6,6 +6,7 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
|
6
6
|
const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
|
|
7
7
|
const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
8
8
|
const EXTERNAL_CLI_RUNNER_GUIDANCE = "External CLI agents (codex-exec, codex-exec-writer, claude-code, claude-code-writer, cursor-agent, cursor-agent-writer) use their own runner contract and do not support native Pi child options such as model override, structured output, acceptance/agent contract, tool budget, fast mode, fork context, skills, or native Pi tools unless the runner explicitly implements them.";
|
|
9
|
+
const AGENT_SELECTION_GUIDANCE = "Before execution, call { action: \"list\", capabilities: true } and run only executable, non-disabled agents; for external-cli rows, also require runner.available === true. This is a passive PATH/PATHEXT/X_OK lookup, not authentication, version, or launch proof; launch preflight remains authoritative.";
|
|
9
10
|
const WORKFLOW_RESUME_KEY_GUIDANCE = "Each workflow key identifies one result lane: use a new stable workflow key for every distinct retained resume pass; same-key calls are reused only when launch parameters are identical, and incompatible parameters are rejected.";
|
|
10
11
|
const WORKFLOW_OUTPUT_BINDING_GUIDANCE = "For durable workflow child files, set output on runs.run/runs.all; task filename prose is not an output declaration, and return the child's outputReference, outputPathMapping, or artifactPaths instead of inventing a literal path.";
|
|
11
12
|
const WORKFLOW_LANES_GUIDANCE = "For bounded parallel sequential chains, use runs.lanes([{key,stages:[{key,agent,task},{key,resume:'previous',task},...]}]); first stages run together, later stages sequence per lane, and the bounded board reports lane-local failures. Only an explicit structuredOutput.verdict === 'blocked' blocks a successful stage; reviewer prose is not parsed.";
|
|
@@ -13,12 +14,12 @@ const WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE = "workflowScript rejects nested asyn
|
|
|
13
14
|
const WORKFLOW_RESOURCE_GUIDANCE = "For permission/policy-extension interoperability, use an extension-owned named resource such as {workflow:'review',args:{task:'...'}} or {workflow:'run-ci',args:{command:'npm test'}}. The host resolves the script and authority internally so policy can distinguish it from raw workflowScript/workflowScriptPath; args are bounded plain data, and do not combine workflow with agent, task, workflowScript, or workflowScriptPath.";
|
|
14
15
|
const WORKFLOW_HOST_GUIDANCE = "For permission-sensitive host calls, use an extension-owned resource such as {workflow:'run-ci',args:{command:'npm test'}}; raw workflowScript/workflowScriptPath have unknown resource provenance and cannot use runs.host. In a resource that grants it, await runs.host(key,{kind:'command',command,timeoutMs,output?,role?,provider?}). runs.host has no per-step cwd: commands and relative output paths use the workflow cwd; set cwd on the outer subagent request instead (for example, {cwd:'/path/to/worktree',workflowScript:'...'}), or put a trusted directory change in the command (for example, 'cd /path/to/worktree && npm test'). v1 supports only command steps; output is bounded and command failure fails the workflow.";
|
|
15
16
|
|
|
16
|
-
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
17
|
+
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child, workflowScript for inline orchestration, workflowScriptPath to load a script from the request cwd, or a named workflow resource for permission/policy-aware execution. ${WORKFLOW_RESOURCE_GUIDANCE} ${AGENT_SELECTION_GUIDANCE} The script inputs are mutually exclusive. Use action:'validate' with either script input to check it without launching children. For multi-step or parallel work, make exactly one top-level subagent call with async:true; launch children only inside that workflow and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and read its ordered array result with indexes, destructuring, or .map(...), not by key property. ${WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE} ${WORKFLOW_LANES_GUIDANCE} ${WORKFLOW_HOST_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
17
18
|
|
|
18
19
|
export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
|
|
19
20
|
|
|
20
21
|
export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
|
|
21
|
-
|
|
22
|
+
`Use subagent only when delegation is needed. ${AGENT_SELECTION_GUIDANCE}`,
|
|
22
23
|
'Omit action for execution; use { agent, task? } for one child. For multi-step or parallel work, make exactly one top-level { workflowScript, async: true } call and launch children only inside it. Use action only for management/control.',
|
|
23
24
|
"workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions, or explicit Promise chains.",
|
|
24
25
|
"Inside workflowScript, use runs.run/runs.all and await their results. runs.all returns an ordered array, not a key map; stored runs.run promises must later be observed with direct await, Promise.race, or Promise.all.",
|
|
@@ -26,7 +27,7 @@ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
|
|
|
26
27
|
];
|
|
27
28
|
|
|
28
29
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
29
|
-
•
|
|
30
|
+
• ${AGENT_SELECTION_GUIDANCE}
|
|
30
31
|
• Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
|
|
31
32
|
• Async/background runs are the normal default unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Use async:false only when the parent must block until completion. Async mode still shows progress. Final reviews and gate checks stay async; needing a result is not a blocking reason. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Ordinary async subagents notify this session natively, so return control and do not call bg_wait merely to get a completion wake. Do not sleep or poll status just to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
|
|
32
33
|
• ${WORKFLOW_RESUME_KEY_GUIDANCE}
|
|
@@ -43,10 +44,10 @@ ${WORKFLOW_RESOURCE_GUIDANCE}
|
|
|
43
44
|
|
|
44
45
|
EXECUTION:
|
|
45
46
|
• ${EXTERNAL_CLI_RUNNER_GUIDANCE}
|
|
46
|
-
•
|
|
47
|
+
• ${AGENT_SELECTION_GUIDANCE}
|
|
47
48
|
• When passing an explicit model to a child (on the call or a runs.run/runs.all item), first call { action: "models" } and copy an exact provider/id; bare ids resolve only when unique in the registry, and agent names (e.g. gpt-pro, advisor) are not model ids. Set per-run thinking with a suffix on the model string (e.g. provider/id:high; off/minimal/low/medium/high/xhigh/max); the suffix wins over the agent's thinking default. The thinking field only applies to action='watchdog.configure' and is ignored on dispatch.
|
|
48
49
|
• SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one direct child. Fields such as model, context, cwd, worktree, output, budgets, acceptance, and async apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
|
|
49
|
-
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
50
|
+
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. Set baseRef to a safe Git ref (default HEAD) to choose the managed worktree starting commit; the source checkout must still be clean. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.steer, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
50
51
|
• ${WORKFLOW_LANES_GUIDANCE}
|
|
51
52
|
• FILE SCRIPT: { workflowScriptPath:"workflows/review.js" }. Relative paths resolve against the request cwd. The host reads the file before the filesystem-free workflow sandbox starts. Do not combine this field with workflowScript.
|
|
52
53
|
• Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
|
|
@@ -57,7 +58,7 @@ EXECUTION:
|
|
|
57
58
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
58
59
|
• validate checks workflowScript or workflowScriptPath syntax and statically decidable structure without launching children. list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, worktree.cleanup (plan-only), lane.status, lane.recordMerge, lane.recordSupersession, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
59
60
|
• status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
|
|
60
|
-
• Create durable project schedules with { action:"schedule.create", id?, name?, sessionOnly?:true, at:"+10m" | ISO, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. With sessionOnly:true, the schedule records the creating session file and only that session can restore or execute it; omitted/false preserves project-wide behavior. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
|
|
61
|
+
• Create durable project schedules with { action:"schedule.create", id?, name?, sessionOnly?:true, at:"+10m" | ISO, baseRef?, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" }, or use workflowScriptPath instead. An optional baseRef selects the safe Git ref used by managed worktrees (default HEAD); the source checkout must still be clean. With sessionOnly:true, the schedule records the creating session file and only that session can restore or execute it; omitted/false preserves project-wide behavior. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
|
|
61
62
|
|
|
62
63
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
63
64
|
|
|
@@ -67,10 +68,10 @@ ${WORKFLOW_RESOURCE_GUIDANCE}
|
|
|
67
68
|
|
|
68
69
|
EXECUTE:
|
|
69
70
|
• ${EXTERNAL_CLI_RUNNER_GUIDANCE}
|
|
70
|
-
•
|
|
71
|
+
• ${AGENT_SELECTION_GUIDANCE}
|
|
71
72
|
• Passing an explicit model? Call {action:"models"} first and copy an exact provider/id; bare ids resolve only when unique in the registry; agent names (e.g. gpt-pro, advisor) are not model ids. Per-run thinking is a suffix on the model string (provider/id:high; off/minimal/low/medium/high/xhigh/max), and the suffix wins over the agent's thinking default; the thinking field only applies to action='watchdog.configure' and is ignored on dispatch.
|
|
72
73
|
• SINGLE {agent:"worker",task:"..."} starts exactly one direct child. Fields apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
|
|
73
|
-
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
|
|
74
|
+
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. Do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation; set baseRef to a safe Git ref (default HEAD) to choose the managed worktree starting commit. The source checkout must still be clean. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
|
|
74
75
|
• ${WORKFLOW_LANES_GUIDANCE}
|
|
75
76
|
• FILE SCRIPT {workflowScriptPath:"workflows/review.js"} loads the script on the host relative to the request cwd before sandbox execution. Do not combine it with workflowScript.
|
|
76
77
|
• Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
|