pi-subagents 0.65.0 → 0.66.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 +87 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +17 -3
- package/docs/configuration.md +34 -0
- package/docs/extension-api.md +94 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +11 -5
- package/docs/workflows.md +22 -7
- package/package.json +4 -1
- package/runner-server-preload.mjs +13 -0
- package/skills/pi-subagents/SKILL.md +2 -1
- package/skills/pi-subagents/references/execution-controls.md +19 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +2 -0
- package/src/agents/advertised-agent-prompt.ts +63 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +8 -0
- package/src/api/preflight.ts +5 -1
- package/src/api/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/config.ts +4 -2
- package/src/extension/index.ts +71 -4
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +9 -7
- package/src/extension/tool-description.ts +10 -5
- package/src/integrations/pi-web-session-liveness.ts +73 -0
- package/src/intercom/native-supervisor-channel.ts +102 -88
- package/src/intercom/supervisor-ui.ts +3 -2
- package/src/missions/workflow-state.ts +37 -16
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-execution.ts +8 -1
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-retention.ts +9 -0
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +40 -29
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/notify.ts +88 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/retained-nested-route-tracker.ts +96 -0
- package/src/runs/background/run-child-session.ts +62 -33
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +46 -8
- package/src/runs/background/runner-child-launch.ts +86 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/subagent-runner.ts +430 -208
- package/src/runs/background/subagent-wait.ts +3 -0
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/async-steering-action.ts +19 -0
- package/src/runs/foreground/execution.ts +116 -26
- package/src/runs/foreground/foreground-history.ts +3 -1
- package/src/runs/foreground/prompt-audit.ts +9 -5
- package/src/runs/foreground/subagent-executor.ts +531 -227
- package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
- package/src/runs/shared/acceptance.ts +16 -3
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +47 -47
- package/src/runs/shared/child-hooks.ts +151 -2
- package/src/runs/shared/child-launch.ts +18 -13
- package/src/runs/shared/child-session.ts +55 -24
- package/src/runs/shared/child-tool-plan.ts +2 -2
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +1 -0
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +30 -20
- package/src/runs/shared/model-exclusions.ts +2 -1
- package/src/runs/shared/model-fallback.ts +41 -8
- package/src/runs/shared/nested-events.ts +8 -8
- package/src/runs/shared/orca-progress-tabs.ts +6 -0
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +3 -2
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
- package/src/runs/shared/worktree-cleanup-plan.ts +6 -3
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +403 -210
- package/src/shared/model-response-aliases.ts +13 -0
- package/src/shared/types.ts +89 -60
- package/src/shared/utils.ts +10 -2
- package/src/shared/watch-strategy.ts +2 -0
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet-status.ts +1 -1
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +44 -15
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +70 -16
- package/src/workflows/workflow-checklist.ts +15 -18
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
package/src/extension/index.ts
CHANGED
|
@@ -19,7 +19,8 @@ import * as path from "node:path";
|
|
|
19
19
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
20
20
|
import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
22
|
-
import { discoverAgentSnapshot, discoverAgents, type AgentConfig, type AgentScope } from "../agents/agents.ts";
|
|
22
|
+
import { clearAgentDiscoveryCache, discoverAgentSnapshot, discoverAgents, type AgentConfig, type AgentScope } from "../agents/agents.ts";
|
|
23
|
+
import { appendAdvertisedAgentPrompt, buildAdvertisedAgentPrompt } from "../agents/advertised-agent-prompt.ts";
|
|
23
24
|
import { clearRuntimeAgentsForPi, listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
|
|
24
25
|
import { registerRuntimeAgentEventListener } from "../agents/runtime-agent-events.ts";
|
|
25
26
|
import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
|
|
@@ -54,6 +55,8 @@ import {
|
|
|
54
55
|
type SupervisorRequestMessageDetails,
|
|
55
56
|
} from "../intercom/supervisor-ui.ts";
|
|
56
57
|
import { registerHerdrStatusBridge, type HerdrStatusRun } from "../integrations/herdr-status.ts";
|
|
58
|
+
import { hasLiveSubagentWork, registerPiWebSessionLiveness } from "../integrations/pi-web-session-liveness.ts";
|
|
59
|
+
import { createRetainedNestedRouteTracker } from "../runs/background/retained-nested-route-tracker.ts";
|
|
57
60
|
import { listHerdrProjectPaneRoots, restoreHerdrProjectPaneSnapshots } from "../inspectors/herdr/project-panes.ts";
|
|
58
61
|
import { registerSubagentRpcBridge } from "./rpc.ts";
|
|
59
62
|
import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
|
|
@@ -487,11 +490,14 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
487
490
|
}, run);
|
|
488
491
|
};
|
|
489
492
|
|
|
490
|
-
const supervisorChannel = createNativeSupervisorChannel(pi, state
|
|
493
|
+
const supervisorChannel = createNativeSupervisorChannel(pi, state, {
|
|
494
|
+
getCurrentOwnerStates: () => executor.getCurrentSupervisorOwnerStates(),
|
|
495
|
+
});
|
|
491
496
|
const waitSubscriptionManager = createWaitSubscriptionManager(pi, state);
|
|
492
497
|
const mainWatchdog = registerMainWatchdog(pi);
|
|
493
498
|
const resultDeliveryOwnership = createResultDeliveryOwnership(state);
|
|
494
499
|
const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch, ownership: resultDeliveryOwnership });
|
|
500
|
+
let retainedNestedRouteTracker: ReturnType<typeof createRetainedNestedRouteTracker> | undefined;
|
|
495
501
|
const fleetStatus = fleetViewEnabled
|
|
496
502
|
? new SubagentFleetStatus(state, async (itemKey) => {
|
|
497
503
|
const ctx = withLastUiContext((current) => current);
|
|
@@ -510,6 +516,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
510
516
|
let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
|
|
511
517
|
let goalTurnId = 0;
|
|
512
518
|
let parentSessionEnvValue: string | null = null;
|
|
519
|
+
let releaseHostSessionLiveness = () => {};
|
|
513
520
|
const scheduledStoreRoot = config.scheduledRuns?.storeRoot === undefined ? undefined : resolveScheduledStoreRoot(config.scheduledRuns.storeRoot);
|
|
514
521
|
const scheduledRunManager = createScheduledRunManager({
|
|
515
522
|
config,
|
|
@@ -527,6 +534,15 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
527
534
|
resolveCapabilityCeiling: (sessionId) => resolveCurrentSubagentCapabilityCeiling(sessionId),
|
|
528
535
|
});
|
|
529
536
|
let refreshResultDelivery = () => {};
|
|
537
|
+
let advertisedAgents: AgentConfig[] = [];
|
|
538
|
+
let advertisedContext: Pick<ExtensionContext, "cwd" | "model"> | undefined;
|
|
539
|
+
const refreshAdvertisedAgents = () => {
|
|
540
|
+
advertisedAgents = [];
|
|
541
|
+
if (!advertisedContext) return;
|
|
542
|
+
clearAgentDiscoveryCache();
|
|
543
|
+
advertisedAgents = discoverAgents(advertisedContext.cwd, "both", advertisedContext.model?.provider).agents
|
|
544
|
+
.filter((agent) => agent.advertise === true);
|
|
545
|
+
};
|
|
530
546
|
const hasResultDeliveryDemand = () => {
|
|
531
547
|
if ([...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running")) return true;
|
|
532
548
|
if (state.foregroundControls.size > 0) return true;
|
|
@@ -592,7 +608,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
592
608
|
}, ASYNC_RETENTION_DELAY_MS);
|
|
593
609
|
asyncRetentionTimer.unref?.();
|
|
594
610
|
|
|
595
|
-
const
|
|
611
|
+
const executorDeps: Parameters<typeof createSubagentExecutor>[0] = {
|
|
596
612
|
pi,
|
|
597
613
|
state,
|
|
598
614
|
config,
|
|
@@ -605,9 +621,20 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
605
621
|
getSubagentSessionRoot,
|
|
606
622
|
expandTilde,
|
|
607
623
|
discoverAgents: discoverAgentsForRuntime,
|
|
624
|
+
onAgentsChanged: () => {
|
|
625
|
+
try {
|
|
626
|
+
refreshAdvertisedAgents();
|
|
627
|
+
} catch (error) {
|
|
628
|
+
// The mutation already persisted. Withdraw stale guidance, not its result.
|
|
629
|
+
console.error("Failed to refresh advertised agents; catalog withdrawn until refresh:", error);
|
|
630
|
+
}
|
|
631
|
+
},
|
|
608
632
|
activateSupervisorTransport: () => supervisorChannel.activateTransport(),
|
|
633
|
+
findPendingAsks: (target) => supervisorChannel.findPendingAsks(target),
|
|
609
634
|
refreshResultDelivery: () => refreshResultDelivery(),
|
|
610
|
-
|
|
635
|
+
trackRetainedNestedRoute: undefined,
|
|
636
|
+
};
|
|
637
|
+
const executor = createSubagentExecutor(executorDeps);
|
|
611
638
|
executorScheduled = executor.executeScheduled;
|
|
612
639
|
|
|
613
640
|
pi.registerMessageRenderer<SupervisorRequestMessageDetails>(SUPERVISOR_REQUEST_MESSAGE_TYPE, renderSupervisorRequest);
|
|
@@ -772,6 +799,16 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
772
799
|
|
|
773
800
|
pi.registerTool(tool);
|
|
774
801
|
|
|
802
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
803
|
+
const selectedTools = event.systemPromptOptions.selectedTools ?? pi.getActiveTools();
|
|
804
|
+
const sessionId = state.currentSessionId ?? resolveCurrentSessionId(ctx.sessionManager);
|
|
805
|
+
const advertisedPrompt = selectedTools.includes("subagent")
|
|
806
|
+
? buildAdvertisedAgentPrompt(advertisedAgents, resolveCurrentSubagentCapabilityCeiling(sessionId))
|
|
807
|
+
: undefined;
|
|
808
|
+
const systemPrompt = appendAdvertisedAgentPrompt(event.systemPrompt, advertisedPrompt);
|
|
809
|
+
if (systemPrompt !== event.systemPrompt) return { systemPrompt };
|
|
810
|
+
});
|
|
811
|
+
|
|
775
812
|
registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager, waitToolConfig.defaultTimeoutMs);
|
|
776
813
|
|
|
777
814
|
pi.on("agent_end", async (_event, ctx) => {
|
|
@@ -906,6 +943,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
906
943
|
const previousRuntimeSessionId = state.currentSessionId;
|
|
907
944
|
resultDeliveryOwnership.claimPredecessor(previousSessionFile, previousRuntimeSessionId);
|
|
908
945
|
state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
946
|
+
state.supervisorOwnerSessionId = ctx.sessionManager.getSessionId() || null;
|
|
909
947
|
transitionResultDelivery();
|
|
910
948
|
state.parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
911
949
|
state.trustedSessionFileRoot = state.parentSessionFile ? path.join(getAgentDir(), "sessions") : undefined;
|
|
@@ -942,6 +980,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
942
980
|
cleanupSessionArtifacts(ctx);
|
|
943
981
|
logSlowPhase("session-artifact-cleanup", phaseStartedAt);
|
|
944
982
|
state.foregroundControls.clear();
|
|
983
|
+
retainedNestedRouteTracker?.clear();
|
|
984
|
+
retainedNestedRouteTracker = undefined;
|
|
985
|
+
executorDeps.trackRetainedNestedRoute = undefined;
|
|
945
986
|
state.lastForegroundControlId = null;
|
|
946
987
|
phaseStartedAt = Date.now();
|
|
947
988
|
resetJobs(ctx);
|
|
@@ -978,6 +1019,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
978
1019
|
if (runtimeCleaned) return;
|
|
979
1020
|
runtimeCleaned = true;
|
|
980
1021
|
const shuttingDownParentSession = parentSessionEnvValue;
|
|
1022
|
+
releaseHostSessionLiveness();
|
|
1023
|
+
releaseHostSessionLiveness = () => {};
|
|
981
1024
|
// Workflow continuations retain their launch context; abort them before
|
|
982
1025
|
// teardown so a reload cannot launch through a stale context.
|
|
983
1026
|
for (const controller of state.workflowControllers?.values() ?? []) {
|
|
@@ -1001,6 +1044,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1001
1044
|
for (const timer of state.cleanupTimers.values()) clearTimeout(timer);
|
|
1002
1045
|
state.cleanupTimers.clear();
|
|
1003
1046
|
state.asyncJobs.clear();
|
|
1047
|
+
retainedNestedRouteTracker?.clear();
|
|
1048
|
+
retainedNestedRouteTracker = undefined;
|
|
1049
|
+
executorDeps.trackRetainedNestedRoute = undefined;
|
|
1004
1050
|
for (const unsubscribe of eventUnsubscribes) {
|
|
1005
1051
|
try {
|
|
1006
1052
|
unsubscribe();
|
|
@@ -1015,6 +1061,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1015
1061
|
promptTemplateBridge.dispose();
|
|
1016
1062
|
state.widgetsSuspended = false;
|
|
1017
1063
|
state.currentSessionId = null;
|
|
1064
|
+
state.supervisorOwnerSessionId = null;
|
|
1018
1065
|
state.statusProjectionSessionId = null;
|
|
1019
1066
|
state.parentSessionFile = null;
|
|
1020
1067
|
parentSessionEnvValue = null;
|
|
@@ -1094,6 +1141,21 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1094
1141
|
installRuntime(ctx);
|
|
1095
1142
|
const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
|
|
1096
1143
|
resetSessionState(ctx, recovering, event.previousSessionFile);
|
|
1144
|
+
releaseHostSessionLiveness();
|
|
1145
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
1146
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1147
|
+
const liveness = sessionId
|
|
1148
|
+
? registerPiWebSessionLiveness({
|
|
1149
|
+
sessionId,
|
|
1150
|
+
...(sessionFile ? { sessionFile } : {}),
|
|
1151
|
+
isActive: () => hasLiveSubagentWork(state) || completionNotifier.hasPendingDelivery(),
|
|
1152
|
+
})
|
|
1153
|
+
: { registered: false, release: () => {} };
|
|
1154
|
+
releaseHostSessionLiveness = liveness.release;
|
|
1155
|
+
if (liveness.registered) {
|
|
1156
|
+
retainedNestedRouteTracker = createRetainedNestedRouteTracker(state);
|
|
1157
|
+
executorDeps.trackRetainedNestedRoute = retainedNestedRouteTracker.track;
|
|
1158
|
+
}
|
|
1097
1159
|
herdrStatusBridge.sessionStarted({
|
|
1098
1160
|
hasUI: ctx.hasUI === true,
|
|
1099
1161
|
runs: activeHerdrRuns(),
|
|
@@ -1112,4 +1174,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1112
1174
|
}
|
|
1113
1175
|
await herdrStatusBridge.flush();
|
|
1114
1176
|
});
|
|
1177
|
+
|
|
1178
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1179
|
+
advertisedContext = { cwd: ctx.cwd, model: ctx.model };
|
|
1180
|
+
refreshAdvertisedAgents();
|
|
1181
|
+
});
|
|
1115
1182
|
}
|
|
@@ -73,7 +73,6 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
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
76
|
try {
|
|
78
77
|
normalizeWorktreeBaseRef(params.baseRef);
|
|
79
78
|
} catch (error) {
|
package/src/extension/rpc.ts
CHANGED
|
@@ -643,27 +643,10 @@ function stopAsyncRun(
|
|
|
643
643
|
message: `Stop requested for async run ${initialRunId}.`,
|
|
644
644
|
};
|
|
645
645
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
kill: options.kill,
|
|
651
|
-
now: options.now,
|
|
652
|
-
source: "rpc-stop",
|
|
653
|
-
...(child ? { targetIndex: child.index, childId: child.id } : {}),
|
|
654
|
-
});
|
|
655
|
-
} catch (error) {
|
|
656
|
-
throw new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
657
|
-
}
|
|
658
|
-
if (child) emitChildStopping(initialRunId, location.asyncDir, child);
|
|
659
|
-
return {
|
|
660
|
-
runId: initialRunId,
|
|
661
|
-
asyncDir: location.asyncDir,
|
|
662
|
-
previousState: initialStatus.state,
|
|
663
|
-
state: "stopping",
|
|
664
|
-
...(child ? { childId: child.id } : {}),
|
|
665
|
-
message: child ? `Stop requested for child ${child.id} in async run ${initialRunId}.` : `Stop requested for async run ${initialRunId}.`,
|
|
666
|
-
};
|
|
646
|
+
// Workflow controls live in-process; a persisted run directory cannot restore them.
|
|
647
|
+
throw new SubagentRpcError("invalid_state", child
|
|
648
|
+
? `Child '${child.id}' in workflow ${initialRunId} has no live stop callback available.`
|
|
649
|
+
: `Workflow ${initialRunId} has no live run controller available to stop.`);
|
|
667
650
|
}
|
|
668
651
|
|
|
669
652
|
let status;
|
package/src/extension/schemas.ts
CHANGED
|
@@ -77,6 +77,8 @@ const AcceptanceEvidenceKinds = [
|
|
|
77
77
|
"manual-notes",
|
|
78
78
|
];
|
|
79
79
|
|
|
80
|
+
// Provider boolean branches intentionally overapproximate false-only runtime inputs.
|
|
81
|
+
// Restricted function-declaration converters only support string enum members.
|
|
80
82
|
const AcceptanceOverride = Type.Unsafe({
|
|
81
83
|
anyOf: [
|
|
82
84
|
{ type: "string", enum: ["auto", "attested", "checked"] },
|
|
@@ -89,10 +91,10 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
89
91
|
{
|
|
90
92
|
type: "string",
|
|
91
93
|
},
|
|
92
|
-
{ type: "boolean"
|
|
94
|
+
{ type: "boolean" },
|
|
93
95
|
{ type: "object", additionalProperties: true },
|
|
94
96
|
],
|
|
95
|
-
description: `Optional acceptance policy. Prefer an inline JSON object. JSON-encoded object strings are tolerated only during input normalization; invalid strings fail closed. Reviewer/read-only calls, omit acceptance. { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(",")}. acceptance.review.required.`,
|
|
97
|
+
description: `Optional acceptance policy. false disables acceptance; true is invalid. Prefer an inline JSON object. JSON-encoded object strings are tolerated only during input normalization; invalid strings fail closed. Reviewer/read-only calls, omit acceptance. { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(",")}. acceptance.review.required.`,
|
|
96
98
|
});
|
|
97
99
|
|
|
98
100
|
const AgentContractOverride = Type.Object({
|
|
@@ -149,7 +151,7 @@ const WorkflowPreflightOverride = Type.Object({
|
|
|
149
151
|
version: Type.Integer({ minimum: 1, maximum: 1 }),
|
|
150
152
|
coverage: Type.Optional(Type.String({ enum: ["complete", "partial"] })),
|
|
151
153
|
lanes: Type.Array(WorkflowPreflightLane, { maxItems: 64 }),
|
|
152
|
-
}, { additionalProperties: false, description: "Bounded display-only lane hints for workflow launch/status.
|
|
154
|
+
}, { additionalProperties: false, description: "Bounded display-only lane hints for workflow launch/status. Coverage mismatches warn but never change launch authority or execution." });
|
|
153
155
|
|
|
154
156
|
// Parallel task item (within a parallel step)
|
|
155
157
|
export const ParallelTaskSchema = Type.Object({
|
|
@@ -257,7 +259,7 @@ export const ChainItem = Type.Object({
|
|
|
257
259
|
const MissionLaunchOverride = Type.Unsafe({
|
|
258
260
|
anyOf: [
|
|
259
261
|
{ type: "object", additionalProperties: true },
|
|
260
|
-
{ type: "boolean"
|
|
262
|
+
{ type: "boolean" },
|
|
261
263
|
],
|
|
262
264
|
});
|
|
263
265
|
const MissionUpdateOverride = Type.Unsafe({ type: "object", additionalProperties: true });
|
|
@@ -317,7 +319,7 @@ const SubagentParamProperties = {
|
|
|
317
319
|
scope: Type.Optional(Type.String({ enum: ["session", "user", "project"], description: "Scope for action='watchdog.configure'. Defaults to session to avoid persistent settings writes unless user/project is explicit." })),
|
|
318
320
|
target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
|
|
319
321
|
focus: Type.Optional(Type.Boolean({ description: "Focus the new Herdr pane for inspector.open or project.open." })),
|
|
320
|
-
thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean"
|
|
322
|
+
thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean" }], description: "Thinking level for action='watchdog.configure' only (off/minimal/low/medium/high/xhigh/max, inherit, or false for off; true is invalid). Ignored on dispatch; set per-run child thinking with a suffix on the model string, e.g. model: 'provider/id:high'." })),
|
|
321
323
|
at: Type.Optional(Type.String({ description: "One-shot trigger for action='schedule.create': a relative delay such as '+10m' or an ISO timestamp with timezone." })),
|
|
322
324
|
every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })),
|
|
323
325
|
sessionOnly: Type.Optional(Type.Boolean()),
|
|
@@ -326,7 +328,7 @@ const SubagentParamProperties = {
|
|
|
326
328
|
overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })),
|
|
327
329
|
catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })),
|
|
328
330
|
missionId: Type.Optional(Type.String({ description: "Mission id." })),
|
|
329
|
-
mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission. Set exactly one non-empty title or summary; objective and labels are optional. goal may only be true and then requires budget.tokens." })),
|
|
331
|
+
mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission; true is invalid. Set exactly one non-empty title or summary; objective and labels are optional. goal may only be true and then requires budget.tokens." })),
|
|
330
332
|
missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: objective, goal false or {paused:boolean}, budget, summary, labels, decisions, artifacts, or delivery receipts." })),
|
|
331
333
|
missionStatus: Type.Optional(Type.String({ description: "Mission status." })),
|
|
332
334
|
missionScope: Type.Optional(Type.String({ description: "Mission list scope: project (default) or global pointer index." })),
|
|
@@ -387,7 +389,7 @@ const SubagentParamProperties = {
|
|
|
387
389
|
outputSchema: Type.Optional(JsonSchemaObject),
|
|
388
390
|
agentContract: Type.Optional(AgentContractOverride),
|
|
389
391
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
390
|
-
gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance." })),
|
|
392
|
+
gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance; an explicit acceptance of false is treated as omitted." })),
|
|
391
393
|
};
|
|
392
394
|
|
|
393
395
|
const SubagentParamsSchema = Type.Object(SubagentParamProperties);
|
|
@@ -6,15 +6,17 @@ 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 SUBAGENT_FAILURE_RECOVERY_GUIDANCE = "If a subagent workflow, child launch, prompt runtime, extension load, or child tooling setup fails, treat it as a lane infrastructure blocker—not permission to change execution mode. Stop and report the exact failure, run/status, and repo/cwd/worktree/branch/ref state; verify the worktree is clean or capture a partial diff before retrying or asking the owner. Retry or fix the subagent path only through a clear same-protocol retry. Do not silently switch to interactive_shell, pi -ne, Codex/Claude/Cursor CLI, a foreground agent, or another external mode. For backlog lanes and other subagent-governed workflows, external/foreground/CLI fallback requires explicit owner approval. Pi core may print a generic pi -ne extension-load hint; that out-of-repo hint is not protocol-approved fallback. interactive_shell remains valid when the user explicitly requests foreground/CLI work or the task is outside the governed subagent protocol.";
|
|
9
10
|
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.";
|
|
10
11
|
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.";
|
|
11
12
|
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.";
|
|
12
13
|
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.";
|
|
14
|
+
const WORKTREE_BASE_REF_GUIDANCE = "baseRef must be HEAD or a supported named ref such as refs/heads/main; full 40/64-character commit IDs and revision expressions such as HEAD~1 are unsupported. Omitted baseRef defaults to HEAD resolved at worktree allocation. The source checkout must still be clean.";
|
|
13
15
|
const WORKFLOW_SCRIPT_PORTABILITY_GUIDANCE = "workflowScript rejects nested async function, arrow, and method helpers; use top-level await, plain helper functions that return runs.run(...), or explicit Promise chains instead.";
|
|
14
16
|
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.";
|
|
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').
|
|
17
|
+
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'). runs.host supports only command steps; output is bounded and command failure fails the workflow.";
|
|
16
18
|
|
|
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.`;
|
|
19
|
+
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} ${WORKTREE_BASE_REF_GUIDANCE} ${EXTERNAL_CLI_RUNNER_GUIDANCE} ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE} Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
18
20
|
|
|
19
21
|
export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
|
|
20
22
|
|
|
@@ -28,6 +30,7 @@ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
|
|
|
28
30
|
|
|
29
31
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
30
32
|
• ${AGENT_SELECTION_GUIDANCE}
|
|
33
|
+
• ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE}
|
|
31
34
|
• Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
|
|
32
35
|
• 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.
|
|
33
36
|
• ${WORKFLOW_RESUME_KEY_GUIDANCE}
|
|
@@ -47,7 +50,7 @@ EXECUTION:
|
|
|
47
50
|
• ${AGENT_SELECTION_GUIDANCE}
|
|
48
51
|
• 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.
|
|
49
52
|
• 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.
|
|
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.
|
|
53
|
+
• 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. ${WORKTREE_BASE_REF_GUIDANCE} 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.
|
|
51
54
|
• ${WORKFLOW_LANES_GUIDANCE}
|
|
52
55
|
• 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.
|
|
53
56
|
• 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" }
|
|
@@ -58,7 +61,7 @@ EXECUTION:
|
|
|
58
61
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
59
62
|
• 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.
|
|
60
63
|
• 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.
|
|
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
|
|
64
|
+
• 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 uses the same managed-worktree ref policy and resolves at allocation; 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.
|
|
62
65
|
|
|
63
66
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
64
67
|
|
|
@@ -71,7 +74,7 @@ EXECUTE:
|
|
|
71
74
|
• ${AGENT_SELECTION_GUIDANCE}
|
|
72
75
|
• 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.
|
|
73
76
|
• SINGLE {agent:"worker",task:"..."} starts exactly one direct child. Fields apply to that child. Do not combine agent/task with action, workflowScript, or workflowScriptPath.
|
|
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
|
|
77
|
+
• 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. ${WORKTREE_BASE_REF_GUIDANCE} 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.
|
|
75
78
|
• ${WORKFLOW_LANES_GUIDANCE}
|
|
76
79
|
• 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.
|
|
77
80
|
• 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]"}
|
|
@@ -82,6 +85,7 @@ MANAGE / CONTROL:
|
|
|
82
85
|
• A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
83
86
|
|
|
84
87
|
ASYNC / SAFETY:
|
|
88
|
+
• ${SUBAGENT_FAILURE_RECOVERY_GUIDANCE}
|
|
85
89
|
• Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. 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 merely to wait; use bg_wait only for provider, detached, or other background work without a native notification when this turn must receive its result.
|
|
86
90
|
• ${WORKFLOW_RESUME_KEY_GUIDANCE}
|
|
87
91
|
• ${WORKFLOW_OUTPUT_BINDING_GUIDANCE}
|
|
@@ -197,6 +201,7 @@ function loadCustomToolDescription(options?: ToolDescriptionOptions): string | u
|
|
|
197
201
|
function withMandatorySafetyGuidance(description: string): string {
|
|
198
202
|
const customDescription = description
|
|
199
203
|
.split(SUBAGENT_SAFETY_GUIDANCE)
|
|
204
|
+
.flatMap((part) => part.split(SUBAGENT_FAILURE_RECOVERY_GUIDANCE))
|
|
200
205
|
.map((part) => part.trim())
|
|
201
206
|
.filter(Boolean)
|
|
202
207
|
.join("\n\n");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { hasLiveNestedDescendants, projectNestedEvents } from "../runs/shared/nested-events.ts";
|
|
2
|
+
import type { NestedRouteInfo, SubagentState } from "../shared/types.ts";
|
|
3
|
+
|
|
4
|
+
export const PI_WEB_SESSION_LIVENESS_REGISTRY_KEY = "@agegr/pi-web/session-liveness/v1";
|
|
5
|
+
const PI_WEB_SESSION_LIVENESS_PROTOCOL_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
interface PiWebSessionLivenessProvider {
|
|
8
|
+
name: string;
|
|
9
|
+
sessionId: string;
|
|
10
|
+
sessionFile?: string;
|
|
11
|
+
isActive(): boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PiWebSessionLivenessRegistry {
|
|
15
|
+
version: typeof PI_WEB_SESSION_LIVENESS_PROTOCOL_VERSION;
|
|
16
|
+
register(provider: PiWebSessionLivenessProvider): () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SessionLivenessRegistration = Omit<PiWebSessionLivenessProvider, "name">;
|
|
20
|
+
|
|
21
|
+
export interface PiWebSessionLivenessHandle {
|
|
22
|
+
/** True only when the compatible host accepted the provider registration. */
|
|
23
|
+
registered: boolean;
|
|
24
|
+
release: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type LiveWorkState = Pick<SubagentState, "asyncJobs" | "foregroundControls" | "retainedForegroundNestedRoutes">;
|
|
28
|
+
|
|
29
|
+
function resolveRegistry(): PiWebSessionLivenessRegistry | null {
|
|
30
|
+
const value = (globalThis as Record<PropertyKey, unknown>)[Symbol.for(PI_WEB_SESSION_LIVENESS_REGISTRY_KEY)];
|
|
31
|
+
if (!value || typeof value !== "object") return null;
|
|
32
|
+
const registry = value as Partial<PiWebSessionLivenessRegistry>;
|
|
33
|
+
if (registry.version !== PI_WEB_SESSION_LIVENESS_PROTOCOL_VERSION || typeof registry.register !== "function") return null;
|
|
34
|
+
return registry as PiWebSessionLivenessRegistry;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function retainLiveForegroundNestedRoute(state: Pick<SubagentState, "retainedForegroundNestedRoutes">, route: NestedRouteInfo): boolean {
|
|
38
|
+
const nested = projectNestedEvents(route);
|
|
39
|
+
if (!hasLiveNestedDescendants(nested.children)) return false;
|
|
40
|
+
state.retainedForegroundNestedRoutes ??= new Map();
|
|
41
|
+
state.retainedForegroundNestedRoutes.set(route.rootRunId, route);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hasLiveSubagentWork(state: LiveWorkState): boolean {
|
|
46
|
+
for (const job of state.asyncJobs.values()) {
|
|
47
|
+
if (job.status === "queued" || job.status === "running" || hasLiveNestedDescendants(job.nestedChildren)) return true;
|
|
48
|
+
}
|
|
49
|
+
for (const control of state.foregroundControls.values()) {
|
|
50
|
+
if ((control.schedulingOwners ?? 0) > 0
|
|
51
|
+
|| (control.activeChildren?.size ?? 0) > 0
|
|
52
|
+
|| hasLiveNestedDescendants(control.nestedChildren)) return true;
|
|
53
|
+
}
|
|
54
|
+
return (state.retainedForegroundNestedRoutes?.size ?? 0) > 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerPiWebSessionLiveness(registration: SessionLivenessRegistration): PiWebSessionLivenessHandle {
|
|
58
|
+
const registry = resolveRegistry();
|
|
59
|
+
if (!registry) return { registered: false, release: () => {} };
|
|
60
|
+
try {
|
|
61
|
+
const release = registry.register({
|
|
62
|
+
name: "pi-subagents",
|
|
63
|
+
sessionId: registration.sessionId,
|
|
64
|
+
...(registration.sessionFile ? { sessionFile: registration.sessionFile } : {}),
|
|
65
|
+
isActive: registration.isActive,
|
|
66
|
+
});
|
|
67
|
+
if (typeof release === "function") return { registered: true, release };
|
|
68
|
+
console.error("Failed to register pi-web session liveness: host registry returned no release function.");
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error("Failed to register pi-web session liveness:", error);
|
|
71
|
+
}
|
|
72
|
+
return { registered: false, release: () => {} };
|
|
73
|
+
}
|