pi-subagents 0.65.1 → 0.67.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 +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- 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 +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- 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 +20 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +27 -19
- package/src/tui/render.ts +172 -33
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- 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/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -12,7 +12,7 @@ export const DEFAULT_ABANDONED_SLOT_RELEASE_AFTER_MS = 20 * 60 * 1000;
|
|
|
12
12
|
export const MIN_ABANDONED_SLOT_RELEASE_AFTER_MS = 5 * 60 * 1000;
|
|
13
13
|
export const MAX_ABANDONED_SLOT_RELEASE_AFTER_MS = 24 * 60 * 60 * 1000;
|
|
14
14
|
|
|
15
|
-
export interface
|
|
15
|
+
export interface ActiveAsyncCapacityOwner {
|
|
16
16
|
version: 1;
|
|
17
17
|
reservationToken: string;
|
|
18
18
|
ownerSessionId: string;
|
|
@@ -29,7 +29,7 @@ export interface ActiveAsyncCapacityOwnerV1 {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export interface ActiveAsyncCapacityHandle {
|
|
32
|
-
readonly owner:
|
|
32
|
+
readonly owner: ActiveAsyncCapacityOwner;
|
|
33
33
|
markStarted(runnerProcessInstanceId: string): void;
|
|
34
34
|
markWorkflowStarted(): void;
|
|
35
35
|
rollback(): boolean;
|
|
@@ -44,7 +44,7 @@ interface CapacityOptions {
|
|
|
44
44
|
abandonedSlotReleaseAfterMs?: number | false;
|
|
45
45
|
pidLiveness?: (pid: number) => PidLiveness;
|
|
46
46
|
afterSlotRename?: (releasedDir: string) => void;
|
|
47
|
-
writeOwner?: (filePath: string, owner:
|
|
47
|
+
writeOwner?: (filePath: string, owner: ActiveAsyncCapacityOwner) => void;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export interface ActiveAsyncCapacityReleaseEvidence {
|
|
@@ -61,7 +61,7 @@ export type ActiveAsyncCapacityReleaseVerdict =
|
|
|
61
61
|
| { state: "not-owned"; reason: string };
|
|
62
62
|
|
|
63
63
|
export interface ActiveAsyncCapacityInspection {
|
|
64
|
-
owner?:
|
|
64
|
+
owner?: ActiveAsyncCapacityOwner;
|
|
65
65
|
relation: "current" | "source" | "none";
|
|
66
66
|
slotDir?: string;
|
|
67
67
|
release: ActiveAsyncCapacityReleaseVerdict;
|
|
@@ -100,9 +100,9 @@ function slotDir(poolDir: string, slot: number): string {
|
|
|
100
100
|
return path.join(poolDir, `slot-${slot}`);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function parseOwner(value: unknown):
|
|
103
|
+
function parseOwner(value: unknown): ActiveAsyncCapacityOwner | undefined {
|
|
104
104
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
105
|
-
const owner = value as Partial<
|
|
105
|
+
const owner = value as Partial<ActiveAsyncCapacityOwner>;
|
|
106
106
|
if (owner.version !== 1
|
|
107
107
|
|| typeof owner.reservationToken !== "string" || !owner.reservationToken
|
|
108
108
|
|| typeof owner.ownerSessionId !== "string" || !owner.ownerSessionId
|
|
@@ -116,10 +116,10 @@ function parseOwner(value: unknown): ActiveAsyncCapacityOwnerV1 | undefined {
|
|
|
116
116
|
if (owner.sourceRunId !== undefined && typeof owner.sourceRunId !== "string") return undefined;
|
|
117
117
|
if (owner.runnerProcessInstanceId !== undefined && (typeof owner.runnerProcessInstanceId !== "string" || !owner.runnerProcessInstanceId)) return undefined;
|
|
118
118
|
if (owner.runnerStartedAt !== undefined && (typeof owner.runnerStartedAt !== "number" || !Number.isFinite(owner.runnerStartedAt))) return undefined;
|
|
119
|
-
return owner as
|
|
119
|
+
return owner as ActiveAsyncCapacityOwner;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function readOwner(dir: string):
|
|
122
|
+
function readOwner(dir: string): ActiveAsyncCapacityOwner | undefined {
|
|
123
123
|
try {
|
|
124
124
|
return parseOwner(JSON.parse(fs.readFileSync(path.join(dir, "owner.json"), "utf-8")));
|
|
125
125
|
} catch {
|
|
@@ -142,7 +142,7 @@ function snapshotFor(sessionId: string, limit: number | undefined, rootDir: stri
|
|
|
142
142
|
return { used: occupiedSlots(sessionDir(sessionId, rootDir)).length, limit: limit ?? 0 };
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
function matchingOwner(dir: string, expected:
|
|
145
|
+
function matchingOwner(dir: string, expected: ActiveAsyncCapacityOwner): ActiveAsyncCapacityOwner | undefined {
|
|
146
146
|
const owner = readOwner(dir);
|
|
147
147
|
return owner?.reservationToken === expected.reservationToken
|
|
148
148
|
&& owner.runId === expected.runId
|
|
@@ -176,7 +176,7 @@ function withSlotClaim<T>(dir: string, operation: () => T): { acquired: true; va
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
function removeOwnedSlot(dir: string, expected:
|
|
179
|
+
function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwner, options: CapacityOptions, requireUnstarted = false, release?: ActiveAsyncCapacityReleaseVerdict): boolean {
|
|
180
180
|
if (requireUnstarted && (expected.runnerProcessInstanceId || expected.runnerStartedAt)) return false;
|
|
181
181
|
const claimed = withSlotClaim(dir, () => {
|
|
182
182
|
const current = matchingOwner(dir, expected);
|
|
@@ -193,7 +193,7 @@ function removeOwnedSlot(dir: string, expected: ActiveAsyncCapacityOwnerV1, opti
|
|
|
193
193
|
return claimed.acquired && claimed.value;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
function appendAbandonedReleaseEvent(asyncDir: string, owner:
|
|
196
|
+
function appendAbandonedReleaseEvent(asyncDir: string, owner: ActiveAsyncCapacityOwner, evidence: ActiveAsyncCapacityReleaseEvidence, now: number): void {
|
|
197
197
|
try {
|
|
198
198
|
const eventsPath = path.join(asyncDir, "events.jsonl");
|
|
199
199
|
fs.mkdirSync(path.dirname(eventsPath), { recursive: true });
|
|
@@ -213,7 +213,7 @@ function terminalState(state: AsyncStatus["state"]): boolean {
|
|
|
213
213
|
return state !== "queued" && state !== "running" && state !== "paused";
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
function runnerReleaseVerdict(owner:
|
|
216
|
+
function runnerReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncStatus | null, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
|
|
217
217
|
if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
|
|
218
218
|
if (!owner.runnerProcessInstanceId) return { state: "retained", reason: "runner process identity has not been recorded" };
|
|
219
219
|
if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
|
|
@@ -262,7 +262,7 @@ function abandonedRunnerReleaseVerdict(status: AsyncStatus, proofState: string,
|
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
function workflowReleaseVerdict(owner:
|
|
265
|
+
function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwner, status: AsyncStatus | null, liveWorkflowRunIds: ReadonlySet<string>): ActiveAsyncCapacityReleaseVerdict {
|
|
266
266
|
if (!status) return { state: "retained", reason: "status file is missing or unreadable" };
|
|
267
267
|
if (status.sessionId !== owner.ownerSessionId) return { state: "retained", reason: `status session ${status.sessionId ?? "unknown"} does not match owner session ${owner.ownerSessionId}` };
|
|
268
268
|
if (status.runId !== owner.runId) return { state: "retained", reason: `status run ${status.runId} does not match owner run ${owner.runId}` };
|
|
@@ -280,6 +280,10 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: Async
|
|
|
280
280
|
if (!childStatus) return { state: "retained", reason: `async workflow child ${label} status is missing or unreadable` };
|
|
281
281
|
if (!terminalState(childStatus.state)) return { state: "retained", reason: `async workflow child ${label} is still ${childStatus.state}` };
|
|
282
282
|
if (!childStatus.processTerminal?.runnerProcessInstanceId) return { state: "retained", reason: `async workflow child ${label} has no runner process identity` };
|
|
283
|
+
if (childStatus.processTerminal.state === "not-started"
|
|
284
|
+
&& childStatus.processTerminal.runId === step.runId
|
|
285
|
+
&& typeof childStatus.error === "string"
|
|
286
|
+
&& childStatus.error) continue;
|
|
283
287
|
const proof = readProcessTerminal(childDir, {
|
|
284
288
|
runId: step.runId,
|
|
285
289
|
runnerProcessInstanceId: childStatus.processTerminal.runnerProcessInstanceId,
|
|
@@ -289,7 +293,7 @@ function workflowReleaseVerdict(owner: ActiveAsyncCapacityOwnerV1, status: Async
|
|
|
289
293
|
return { state: "releasable", reason: "workflow is terminal, controller is gone, and async children have observed proof" };
|
|
290
294
|
}
|
|
291
295
|
|
|
292
|
-
function ownerReleaseVerdict(owner:
|
|
296
|
+
function ownerReleaseVerdict(owner: ActiveAsyncCapacityOwner, liveWorkflowRunIds: ReadonlySet<string>, options: CapacityOptions): ActiveAsyncCapacityReleaseVerdict {
|
|
293
297
|
const status = readStatus(owner.asyncDir);
|
|
294
298
|
return owner.kind === "runner"
|
|
295
299
|
? runnerReleaseVerdict(owner, status, options)
|
|
@@ -364,7 +368,7 @@ export function getActiveAsyncCapacitySnapshot(
|
|
|
364
368
|
return reconcileActiveAsyncCapacity(sessionId, limit, options);
|
|
365
369
|
}
|
|
366
370
|
|
|
367
|
-
function createSlot(poolDir: string, owner:
|
|
371
|
+
function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwner): boolean {
|
|
368
372
|
const destination = slotDir(poolDir, owner.slot);
|
|
369
373
|
fs.mkdirSync(poolDir, { recursive: true, mode: 0o700 });
|
|
370
374
|
try {
|
|
@@ -379,7 +383,7 @@ function createSlot(poolDir: string, owner: ActiveAsyncCapacityOwnerV1): boolean
|
|
|
379
383
|
return true;
|
|
380
384
|
}
|
|
381
385
|
|
|
382
|
-
function handleFor(owner:
|
|
386
|
+
function handleFor(owner: ActiveAsyncCapacityOwner, limit: number, options: CapacityOptions, rollbackOwner?: ActiveAsyncCapacityOwner): ActiveAsyncCapacityHandle {
|
|
383
387
|
const rootDir = options.rootDir ?? ACTIVE_ASYNC_CAPACITY_DIR;
|
|
384
388
|
const writeOwner = options.writeOwner ?? writePrivateAtomicJson;
|
|
385
389
|
const dir = slotDir(sessionDir(owner.ownerSessionId, rootDir), owner.slot);
|
|
@@ -462,7 +466,7 @@ export function acquireActiveAsyncCapacity(
|
|
|
462
466
|
const poolDir = sessionDir(input.sessionId, rootDir);
|
|
463
467
|
const token = options.token?.() ?? randomUUID();
|
|
464
468
|
for (let slot = 0; slot < input.limit; slot++) {
|
|
465
|
-
const owner:
|
|
469
|
+
const owner: ActiveAsyncCapacityOwner = {
|
|
466
470
|
version: 1,
|
|
467
471
|
reservationToken: token,
|
|
468
472
|
ownerSessionId: input.sessionId,
|
|
@@ -495,7 +499,7 @@ export function transferActiveAsyncCapacity(
|
|
|
495
499
|
if (!current || !status || status.runId !== input.sourceRunId || status.state === "queued" || status.state === "running") {
|
|
496
500
|
throw new Error(`Active async capacity source '${input.sourceRunId}' is not transferable.`);
|
|
497
501
|
}
|
|
498
|
-
const next:
|
|
502
|
+
const next: ActiveAsyncCapacityOwner = {
|
|
499
503
|
...current,
|
|
500
504
|
runId: input.runId,
|
|
501
505
|
sourceRunId: input.sourceRunId,
|
|
@@ -10,23 +10,22 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig, type UnknownAgentDiagnosticContext } from "../../agents/agents.ts";
|
|
13
|
-
import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
|
|
14
13
|
import { createAtomicJsonWriter, writePrivateAtomicJson } from "../../shared/atomic-json.ts";
|
|
14
|
+
import { buildEffectiveSystemPrompt } from "../shared/effective-system-prompt.ts";
|
|
15
15
|
import { currentCompletionOwnerId } from "../../shared/completion-owner.ts";
|
|
16
16
|
import { planChildLaunch, resolveStepBehavior, suppressProgressForReadOnlyTask, type ResolvedStepBehavior } from "../shared/child-launch-plan.ts";
|
|
17
|
-
import { applyThinkingSuffix, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
|
|
18
|
-
import {
|
|
17
|
+
import { applyThinkingSuffix, getHostBuiltinToolNames, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan } from "../shared/child-tool-plan.ts";
|
|
18
|
+
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
19
19
|
import { applyWatchdogLaunchRules, sendRuleViolationWarning } from "../../watchdog/rules.ts";
|
|
20
20
|
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveExistingReadInstructionPaths, resolveExistingReadPaths, writeInitialProgressFile, type ChainStep, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
21
21
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
22
22
|
import type { ContextMode } from "../shared/context-mode.ts";
|
|
23
|
-
import { PI_CODING_AGENT_PACKAGE, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
23
|
+
import { PI_CODING_AGENT_PACKAGE, resolveBunPiExecutable, resolveInstalledPiPackageRoot, resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
24
24
|
import { JITI_ALIAS_ENV, resolveHostPeerAliases } from "./runner-aliases.ts";
|
|
25
25
|
import { preflightLaunchCwd } from "../shared/launch-cwd.ts";
|
|
26
26
|
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
27
27
|
import { backgroundProcessOptions } from "../shared/background-process-options.ts";
|
|
28
|
-
import {
|
|
29
|
-
import { buildAgentMemoryInjection } from "../../agents/agent-memory.ts";
|
|
28
|
+
import { normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
|
|
30
29
|
import { PI_CODING_AGENT_PACKAGE_ROOT_ENV, PROMPT_REDACTED, resolveChildCwd } from "../../shared/utils.ts";
|
|
31
30
|
import { buildModelCandidates, resolveEffectiveSubagentModel, resolveModelOrigin, resolveSubagentModelOverride, type AvailableModelInfo, type ModelOrigin, type ParentModel } from "../shared/model-fallback.ts";
|
|
32
31
|
import { resolveToolTimeoutMs, toolTimeoutFromEnv } from "../shared/tool-timeout.ts";
|
|
@@ -80,7 +79,7 @@ import type { ActiveAsyncCapacityHandle } from "./active-async-capacity.ts";
|
|
|
80
79
|
import { statusStepDescription } from "./chain-append.ts";
|
|
81
80
|
import { SUBAGENT_PROCESS_TERMINAL_EVENT } from "../../shared/types.ts";
|
|
82
81
|
import { assertAgentAllowedByCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, type ResolvedSubagentCapabilityCeiling } from "../shared/capability-ceiling.ts";
|
|
83
|
-
import {
|
|
82
|
+
import { resolveLaunchBinding } from "../../shared/launch-contract.ts";
|
|
84
83
|
import { resolvePermissionRules, type PermissionConfig } from "../shared/permissions.ts";
|
|
85
84
|
import { normalizeExtensionBindings, omitExtensionBindingsEnv, type ExtensionBindings } from "../shared/extension-bindings.ts";
|
|
86
85
|
import { assertWorkflowLaneKey, normalizeWorkflowLaneMetadata } from "../shared/lane-metadata.ts";
|
|
@@ -542,15 +541,20 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
542
541
|
const cwdError = preflightLaunchCwd(requestedCwd, cwd);
|
|
543
542
|
if (cwdError) return { error: cwdError };
|
|
544
543
|
|
|
545
|
-
|
|
544
|
+
// The compiled host exposes its SDK only through Pi's extension loader.
|
|
545
|
+
const binaryHost = resolveBunPiExecutable();
|
|
546
|
+
const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
|
|
547
|
+
const bootstrap = path.join(path.dirname(runner), "binary-bootstrap.ts");
|
|
548
|
+
if (binaryHost && !fs.existsSync(bootstrap)) return { error: `Background runner bootstrap not found: ${bootstrap}` };
|
|
549
|
+
if (!binaryHost && !jitiCliPath) {
|
|
546
550
|
return { error: "upstream jiti for TypeScript execution could not be found; ensure package dependencies are installed" };
|
|
547
551
|
}
|
|
548
|
-
if (!piPackageRoot) {
|
|
549
|
-
return { error: `Background children require
|
|
552
|
+
if (!binaryHost && !piPackageRoot) {
|
|
553
|
+
return { error: `Background children require a supported standalone Pi host or the installed npm package (${PI_CODING_AGENT_PACKAGE}); neither is available.` };
|
|
550
554
|
}
|
|
551
|
-
const hostPeerAliases = resolveHostPeerAliases(piPackageRoot);
|
|
555
|
+
const hostPeerAliases = !binaryHost && piPackageRoot ? resolveHostPeerAliases(piPackageRoot) : { aliases: {}, missing: [] };
|
|
552
556
|
if (hostPeerAliases.missing.length > 0) {
|
|
553
|
-
return { error: `Background children require
|
|
557
|
+
return { error: `Background children require the host npm package (${PI_CODING_AGENT_PACKAGE}) with its dependencies; ${piPackageRoot} does not provide ${hostPeerAliases.missing.join(", ")}.` };
|
|
554
558
|
}
|
|
555
559
|
|
|
556
560
|
fs.mkdirSync(TEMP_ROOT_DIR, { recursive: true });
|
|
@@ -560,8 +564,7 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
560
564
|
const launchBarrierToken = hasRevivalLease ? undefined : runnerProcessInstanceId;
|
|
561
565
|
const launchConfig = { ...cfg, runnerProcessInstanceId, ...(launchBarrierToken ? { launchBarrierToken } : {}) };
|
|
562
566
|
writePrivateAtomicJson(cfgPath, launchConfig);
|
|
563
|
-
const
|
|
564
|
-
const nodeCommand = resolveNodeExecutable();
|
|
567
|
+
const command = binaryHost ?? resolveNodeExecutable();
|
|
565
568
|
const launchForStartup = launchConfig as typeof launchConfig & { asyncDir?: unknown; id?: unknown; sessionId?: unknown; completionOwnerId?: unknown; revivalLease?: unknown };
|
|
566
569
|
const launchAsyncDir = typeof launchForStartup.asyncDir === "string" ? launchForStartup.asyncDir : undefined;
|
|
567
570
|
const launchRunId = typeof launchForStartup.id === "string" ? launchForStartup.id : suffix;
|
|
@@ -587,17 +590,23 @@ function spawnRunner(cfg: object, suffix: string, cwd: string, initialStatus: Om
|
|
|
587
590
|
stdoutFd = fs.openSync(logPaths.stdoutPath, "a");
|
|
588
591
|
stderrFd = fs.openSync(logPaths.stderrPath, "a");
|
|
589
592
|
}
|
|
590
|
-
const preload = hostPeerAliases.
|
|
591
|
-
? ["--import", new URL("../../../runner-
|
|
593
|
+
const preload = Object.keys(hostPeerAliases.aliases).length > 0
|
|
594
|
+
? ["--import", new URL("../../../runner-peer-preload.mjs", import.meta.url).href]
|
|
592
595
|
: [];
|
|
593
|
-
const
|
|
596
|
+
const args = binaryHost
|
|
597
|
+
? ["--no-extensions", "--no-skills", "--no-prompt-templates", "--no-session", "--mode", "rpc", "--extension", bootstrap]
|
|
598
|
+
: [...preload, jitiCliPath!, runner, cfgPath];
|
|
599
|
+
const proc = spawn(command, args, {
|
|
594
600
|
cwd,
|
|
595
601
|
...backgroundProcessOptions(),
|
|
596
602
|
stdio: ["ignore", stdoutFd ?? "ignore", stderrFd ?? "ignore"],
|
|
597
603
|
env: {
|
|
598
604
|
...omitExtensionBindingsEnv(process.env),
|
|
599
|
-
[PI_CODING_AGENT_PACKAGE_ROOT_ENV]: piPackageRoot,
|
|
600
|
-
|
|
605
|
+
[PI_CODING_AGENT_PACKAGE_ROOT_ENV]: binaryHost ? undefined : piPackageRoot,
|
|
606
|
+
// npm must override inherited bundled layouts (#2071); binaries retain release assets.
|
|
607
|
+
PI_PACKAGE_DIR: binaryHost ? process.env.PI_PACKAGE_DIR : piPackageRoot,
|
|
608
|
+
[JITI_ALIAS_ENV]: binaryHost ? undefined : JSON.stringify(hostPeerAliases.aliases),
|
|
609
|
+
PI_SUBAGENT_RUNNER_CONFIG: binaryHost ? cfgPath : undefined,
|
|
601
610
|
},
|
|
602
611
|
});
|
|
603
612
|
closeFd(stdoutFd);
|
|
@@ -891,22 +900,13 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
891
900
|
);
|
|
892
901
|
if (missingSkills.includes("pi-subagents")) throw new UnavailableSubagentSkillError(UNAVAILABLE_SUBAGENT_SKILL_ERROR);
|
|
893
902
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
const injection = buildSkillInjection(resolvedSkills);
|
|
897
|
-
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
898
|
-
}
|
|
899
|
-
const memoryInjection = buildAgentMemoryInjection(a, stepCwd);
|
|
900
|
-
if (memoryInjection) {
|
|
901
|
-
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
|
|
902
|
-
}
|
|
903
|
-
systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: stepCwd, agentName: a.name });
|
|
903
|
+
// A namespaced parallel output is injected by the runner, not the prompt.
|
|
904
|
+
const systemPrompt = buildEffectiveSystemPrompt({ agent: a, resolvedSkills, cwd: stepCwd, ...(!namespaceOutputPath && outputPath ? { outputPath } : {}) });
|
|
904
905
|
|
|
905
906
|
const readInstructions = buildChainInstructions({ ...behavior, output: false, progress: false }, instructionCwd, false, undefined, readExistenceCwd);
|
|
906
907
|
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
907
908
|
if (behavior.progress) progressInstructionCreated = true;
|
|
908
909
|
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, progressDir, isFirstProgressAgent);
|
|
909
|
-
if (!namespaceOutputPath) systemPrompt = injectOutputPathSystemPrompt(systemPrompt, outputPath, a);
|
|
910
910
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
|
|
911
911
|
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
912
912
|
let taskTemplate = s.task ?? "{previous}";
|
|
@@ -963,6 +963,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
963
963
|
const launchRuleError = applyWatchdogLaunchRules({ cwd: stepCwd, agent: a.name, model: modelCandidates[0] ?? model, warn: (violation) => sendRuleViolationWarning(ctx.pi, violation) });
|
|
964
964
|
if (launchRuleError) throw new AsyncStartValidationError(launchRuleError);
|
|
965
965
|
const fast = s.fast ?? params.fast ?? a.fast;
|
|
966
|
+
const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
|
|
966
967
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
967
968
|
tools: a.tools,
|
|
968
969
|
excludeTools: a.excludeTools,
|
|
@@ -981,6 +982,7 @@ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildPa
|
|
|
981
982
|
agentName: a.name,
|
|
982
983
|
permissionRules,
|
|
983
984
|
runtimeSnapshotHost: ctx.pi,
|
|
985
|
+
hostAvailableBuiltins,
|
|
984
986
|
});
|
|
985
987
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
986
988
|
if (externalRunner && permissionRules) {
|
|
@@ -1377,6 +1379,7 @@ export function executeAsyncChain(
|
|
|
1377
1379
|
deadlineAt,
|
|
1378
1380
|
globalConcurrencyLimit: params.globalConcurrencyLimit,
|
|
1379
1381
|
runFanoutBudget,
|
|
1382
|
+
hostAvailableBuiltins: getHostBuiltinToolNames(ctx.pi),
|
|
1380
1383
|
workflowGraph,
|
|
1381
1384
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
1382
1385
|
...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
|
|
@@ -1636,16 +1639,6 @@ export function executeAsyncSingle(
|
|
|
1636
1639
|
agentConfig.filePath ? path.dirname(agentConfig.filePath) : runnerCwd,
|
|
1637
1640
|
);
|
|
1638
1641
|
if (missingSkills.includes("pi-subagents")) return formatAsyncStartError("single", UNAVAILABLE_SUBAGENT_SKILL_ERROR);
|
|
1639
|
-
let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
|
|
1640
|
-
if (resolvedSkills.length > 0) {
|
|
1641
|
-
const injection = buildSkillInjection(resolvedSkills);
|
|
1642
|
-
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${injection}` : injection;
|
|
1643
|
-
}
|
|
1644
|
-
const memoryInjection = buildAgentMemoryInjection(agentConfig, runnerCwd);
|
|
1645
|
-
if (memoryInjection) {
|
|
1646
|
-
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${memoryInjection}` : memoryInjection;
|
|
1647
|
-
}
|
|
1648
|
-
systemPrompt = appendAgentRefinementOverlay(systemPrompt, { cwd: runnerCwd, agentName: agentConfig.name });
|
|
1649
1642
|
|
|
1650
1643
|
const inheritedNestedRoute = inheritedNestedRouteOf(ctx.childRuntime);
|
|
1651
1644
|
const nestedAddress = inheritedNestedRoute ? inheritedNestedParentAddressOf(ctx.childRuntime) : undefined;
|
|
@@ -1668,7 +1661,7 @@ export function executeAsyncSingle(
|
|
|
1668
1661
|
|
|
1669
1662
|
const effectiveOutput = normalizeSingleOutputOverride(params.output, agentConfig.output);
|
|
1670
1663
|
const outputPath = resolveSingleOutputPath(effectiveOutput, ctx.cwd, instructionCwd, params.outputBaseDir ?? (artifactsDir ? path.join(artifactsDir, "outputs", id) : undefined));
|
|
1671
|
-
systemPrompt =
|
|
1664
|
+
const systemPrompt = buildEffectiveSystemPrompt({ agent: agentConfig, resolvedSkills, cwd: runnerCwd, ...(outputPath ? { outputPath } : {}) });
|
|
1672
1665
|
const outputMode = params.outputMode ?? agentConfig.outputMode ?? "inline";
|
|
1673
1666
|
const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
|
|
1674
1667
|
if (validationError) return formatAsyncStartError("single", validationError);
|
|
@@ -1759,6 +1752,7 @@ export function executeAsyncSingle(
|
|
|
1759
1752
|
return formatAsyncStartError("single", error instanceof Error ? error.message : String(error));
|
|
1760
1753
|
}
|
|
1761
1754
|
}
|
|
1755
|
+
const hostAvailableBuiltins = getHostBuiltinToolNames(ctx.pi);
|
|
1762
1756
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
1763
1757
|
tools: agentConfig.tools,
|
|
1764
1758
|
excludeTools: agentConfig.excludeTools,
|
|
@@ -1777,6 +1771,7 @@ export function executeAsyncSingle(
|
|
|
1777
1771
|
agentName: agentConfig.name,
|
|
1778
1772
|
permissionRules: resolvePermissionRules(ctx.permissions, agentConfig.permissions),
|
|
1779
1773
|
runtimeSnapshotHost: ctx.pi,
|
|
1774
|
+
hostAvailableBuiltins,
|
|
1780
1775
|
});
|
|
1781
1776
|
const launchResolvedExtensions = externalRunner ? undefined : projectLaunchResolvedChildExtensions(toolPlan);
|
|
1782
1777
|
if (!externalRunner) {
|
|
@@ -1793,24 +1788,17 @@ export function executeAsyncSingle(
|
|
|
1793
1788
|
});
|
|
1794
1789
|
if (contractError) return formatAsyncStartError("single", contractError);
|
|
1795
1790
|
}
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1791
|
+
const fast = params.fast ?? agentConfig.fast;
|
|
1792
|
+
const launchThinking = resolveEffectiveThinking(model, effectiveThinking);
|
|
1793
|
+
const { definitionDigest, launchContractDigest } = resolveLaunchBinding({
|
|
1794
|
+
agent: agentConfig,
|
|
1798
1795
|
task,
|
|
1799
|
-
...(model ? { model } : {}),
|
|
1800
1796
|
modelCandidates,
|
|
1801
|
-
...(
|
|
1802
|
-
...(
|
|
1803
|
-
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
1797
|
+
...(fast !== undefined ? { fast } : {}),
|
|
1798
|
+
...(launchThinking ? { thinking: launchThinking } : {}),
|
|
1804
1799
|
systemPrompt,
|
|
1805
|
-
systemPromptMode: agentConfig.systemPromptMode,
|
|
1806
|
-
inheritProjectContext: agentConfig.inheritProjectContext,
|
|
1807
|
-
inheritGlobalContext: agentConfig.inheritGlobalContext,
|
|
1808
|
-
inheritSkills: agentConfig.inheritSkills,
|
|
1809
1800
|
skills: resolvedSkills.map((skill) => skill.name),
|
|
1810
|
-
|
|
1811
|
-
...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
1812
|
-
extensions: toolPlan.extensionArgs,
|
|
1813
|
-
mcpDirectTools: toolPlan.effectiveMcpTools,
|
|
1801
|
+
toolPlan,
|
|
1814
1802
|
...(outputPath ? { outputPath } : {}),
|
|
1815
1803
|
outputMode,
|
|
1816
1804
|
...(params.structuredOutputSchema ? { structuredOutputSchema: params.structuredOutputSchema } : {}),
|
|
@@ -1939,7 +1927,7 @@ export function executeAsyncSingle(
|
|
|
1939
1927
|
waitToolEnabled: params.waitToolEnabled,
|
|
1940
1928
|
waitToolDefaultTimeoutMs: params.waitToolDefaultTimeoutMs,
|
|
1941
1929
|
...(params.agentContract ? { agentContract: params.agentContract } : {}),
|
|
1942
|
-
definitionDigest
|
|
1930
|
+
definitionDigest,
|
|
1943
1931
|
launchBindingTask: task,
|
|
1944
1932
|
launchContractDigest,
|
|
1945
1933
|
...(extensionBindings ? { extensionBindings } : {}),
|
|
@@ -1988,6 +1976,7 @@ export function executeAsyncSingle(
|
|
|
1988
1976
|
launchContractDigest,
|
|
1989
1977
|
launchResolvedExtensions,
|
|
1990
1978
|
runFanoutBudget,
|
|
1979
|
+
hostAvailableBuiltins,
|
|
1991
1980
|
...(params.parentWorkflowRunId ? { parentWorkflowRunId: params.parentWorkflowRunId } : {}),
|
|
1992
1981
|
...(params.workflowKey ? { workflowKey: params.workflowKey } : {}),
|
|
1993
1982
|
...(lane ? { lane } : {}),
|
|
@@ -26,6 +26,7 @@ import { EXTERNAL_JOB_BRIDGE_REQUEST_DIR, serviceExternalJobBridgeRequests } fro
|
|
|
26
26
|
import { shouldUseNativeFsWatch } from "../../shared/watch-strategy.ts";
|
|
27
27
|
import { parseWorkflowChildSummary } from "../../workflows/workflow-child-summary.ts";
|
|
28
28
|
import { validHostStepNodes } from "../shared/host-step-status.ts";
|
|
29
|
+
import { readProcessTerminal } from "./process-terminal.ts";
|
|
29
30
|
import { withCachedUiContext } from "../../shared/extension-context.ts";
|
|
30
31
|
|
|
31
32
|
interface AsyncJobTrackerOptions {
|
|
@@ -78,6 +79,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
78
79
|
let widgetRerenderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
79
80
|
const runningJobIds = new Set<string>();
|
|
80
81
|
const externalJobBridgeRuns = new Set<string>();
|
|
82
|
+
// Early native failure is visible before its publisher finishes. Retain only
|
|
83
|
+
// that scoped observation, using the existing liveness sweep to renew delivery.
|
|
84
|
+
const terminalPublications = new Map<string, { instanceId: string; pending: true } | { pending: false }>();
|
|
81
85
|
const externalJobBridgeEligibility = (steps: AsyncJobState["steps"]): "required" | "not-required" | "unknown" => {
|
|
82
86
|
if (!Array.isArray(steps)) return "unknown";
|
|
83
87
|
for (const step of steps) {
|
|
@@ -380,6 +384,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
380
384
|
refreshTimers.delete(asyncId);
|
|
381
385
|
runningJobIds.delete(asyncId);
|
|
382
386
|
externalJobBridgeRuns.delete(asyncId);
|
|
387
|
+
terminalPublications.delete(asyncId);
|
|
383
388
|
};
|
|
384
389
|
|
|
385
390
|
const refreshJob = (job: AsyncJobState): boolean => {
|
|
@@ -440,6 +445,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
440
445
|
if (status) {
|
|
441
446
|
const previousStatus = job.status;
|
|
442
447
|
job.status = status.state;
|
|
448
|
+
if (!terminalStatus(job.status)) terminalPublications.delete(job.asyncId);
|
|
443
449
|
if (job.status === "running") runningJobIds.add(job.asyncId);
|
|
444
450
|
else runningJobIds.delete(job.asyncId);
|
|
445
451
|
if (job.status !== "complete" && job.status !== "failed" && job.status !== "paused" && job.status !== "stopped") cancelCleanup(job.asyncId);
|
|
@@ -497,9 +503,30 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
497
503
|
job.wrapUpRequested = status.wrapUpRequested ?? job.wrapUpRequested;
|
|
498
504
|
job.sessionFile = status.sessionFile ?? job.sessionFile;
|
|
499
505
|
if (terminalStatus(job.status)) {
|
|
500
|
-
|
|
506
|
+
let publication = terminalPublications.get(job.asyncId);
|
|
507
|
+
if (!publication && status.mode !== "workflow" && status.processTerminal?.state === "pending"
|
|
508
|
+
&& status.runId === job.asyncId && status.sessionId === state.currentSessionId
|
|
509
|
+
&& status.completionOwnerId && status.completionOwnerId === state.completionOwnerId) {
|
|
510
|
+
publication = { instanceId: status.processTerminal.runnerProcessInstanceId, pending: true };
|
|
511
|
+
terminalPublications.set(job.asyncId, publication);
|
|
512
|
+
}
|
|
513
|
+
const wasPending = publication?.pending;
|
|
514
|
+
if (publication?.pending) {
|
|
515
|
+
// Reconciliation resolves pending as well as public payloads. The
|
|
516
|
+
// watcher already discovers exact tracked run IDs without promotion.
|
|
517
|
+
const published = reconciliation.resultPath && fs.existsSync(reconciliation.resultPath);
|
|
518
|
+
const closed = !published && readProcessTerminal(job.asyncDir, {
|
|
519
|
+
runId: job.asyncId, runnerProcessInstanceId: publication.instanceId,
|
|
520
|
+
})?.state === "observed";
|
|
521
|
+
if (published || closed) {
|
|
522
|
+
publication = { pending: false };
|
|
523
|
+
terminalPublications.set(job.asyncId, publication);
|
|
524
|
+
} else cancelCleanup(job.asyncId);
|
|
525
|
+
}
|
|
526
|
+
// Scan on close too: publication may have raced the payload check.
|
|
527
|
+
if (!terminalStatus(previousStatus) || (wasPending && !publication?.pending)) options.onJobTerminal?.();
|
|
501
528
|
rememberFleetJob(state, job);
|
|
502
|
-
if (!nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
529
|
+
if (!publication?.pending && !nestedRefreshFailed && !hasLiveNestedDescendants(job.nestedChildren) && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
|
|
503
530
|
scheduleCleanup(job.asyncId);
|
|
504
531
|
}
|
|
505
532
|
}
|
|
@@ -526,7 +553,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
526
553
|
}
|
|
527
554
|
runningJobIds.delete(job.asyncId);
|
|
528
555
|
rememberFleetJob(state, job);
|
|
529
|
-
if (!hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
|
|
556
|
+
if (!terminalPublications.get(job.asyncId)?.pending && !hasLiveNestedDescendants(job.nestedChildren) && !state.cleanupTimers.has(job.asyncId)) scheduleCleanup(job.asyncId);
|
|
530
557
|
}
|
|
531
558
|
return widgetRenderKey(job, widgetExpanded) !== widgetStateBefore;
|
|
532
559
|
};
|
|
@@ -668,6 +695,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
668
695
|
const sessionRoot = state.liveAsyncSessionRoots?.get(info.id);
|
|
669
696
|
state.liveAsyncSessionRoots?.delete(info.id);
|
|
670
697
|
externalJobBridgeRuns.delete(info.id);
|
|
698
|
+
terminalPublications.delete(info.id);
|
|
671
699
|
state.asyncJobs.set(info.id, {
|
|
672
700
|
asyncId: info.id,
|
|
673
701
|
asyncDir,
|
|
@@ -721,6 +749,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
721
749
|
job.asyncDir = result.asyncDir;
|
|
722
750
|
watchJob(job);
|
|
723
751
|
}
|
|
752
|
+
// Delivery can precede the first terminal status refresh. Remember its
|
|
753
|
+
// settlement even when no pending publication has been observed yet.
|
|
754
|
+
terminalPublications.set(asyncId, { pending: false });
|
|
724
755
|
try {
|
|
725
756
|
updateAsyncJobNestedProjection(job);
|
|
726
757
|
} catch (error) {
|
|
@@ -745,6 +776,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
|
|
|
745
776
|
widgetRerenderTimer = undefined;
|
|
746
777
|
runningJobIds.clear();
|
|
747
778
|
externalJobBridgeRuns.clear();
|
|
779
|
+
terminalPublications.clear();
|
|
748
780
|
};
|
|
749
781
|
|
|
750
782
|
const resetJobs = (ctx?: ExtensionContext) => {
|
|
@@ -15,6 +15,7 @@ import { parallelHandoffPath, resolveRetainedWorktreeCwd } from "../shared/paral
|
|
|
15
15
|
import { normalizeWorktreeBaseRef } from "../shared/worktree.ts";
|
|
16
16
|
import { intersectThinkingCeilings, parseThinkingLevel, type ThinkingLevel } from "../../shared/thinking-ceiling.ts";
|
|
17
17
|
import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
|
|
18
|
+
import { validateIntercomBridgeConfig } from "../../intercom/intercom-bridge.ts";
|
|
18
19
|
import { validateModelResponseAliases } from "../../shared/model-response-aliases.ts";
|
|
19
20
|
|
|
20
21
|
export interface AsyncResumeParams {
|
|
@@ -318,7 +319,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
318
319
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': expected an object.`);
|
|
319
320
|
const parsed = value as Record<string, unknown>;
|
|
320
321
|
const allowedFields = new Set([
|
|
321
|
-
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
322
|
+
"modelResponseAliases", "version", "launchContractDigest", "sourceRunId", "agentContract", "agent", "sessionFile", "cwd", "model", "modelProvider", "modelOverrideFromParent", "modelOrigin", "fallbackModels", "fast", "thinking", "thinkingCeiling", "tools", "allowNestedSubagents", "extensions",
|
|
322
323
|
"subagentOnlyExtensions", "mcpDirectTools", "excludeTools", "mutationTools", "systemPrompt", "systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "skills",
|
|
323
324
|
"skillPath", "agentFilePath", "completionGuard", "memory", "outputPath", "outputMode", "structuredOutputSchema", "acceptance", "sessionDir", "artifactConfig",
|
|
324
325
|
"artifactsDir", "maxOutput", "controlConfig", "context", "intercomBridge", "absoluteDeadlineAt", "initialTurnBudget", "initialToolBudget", "maxSubagentDepth", "share", "capabilityCeiling",
|
|
@@ -352,6 +353,7 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
352
353
|
if (parsed.outputMode !== "inline" && parsed.outputMode !== "file-only") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': outputMode is invalid.`);
|
|
353
354
|
if (parsed.context !== undefined && parsed.context !== "fresh" && parsed.context !== "fork") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': context is invalid.`);
|
|
354
355
|
if (parsed.modelOverrideFromParent !== undefined && typeof parsed.modelOverrideFromParent !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOverrideFromParent must be a boolean.`);
|
|
356
|
+
if (parsed.fast !== undefined && typeof parsed.fast !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': fast must be a boolean.`);
|
|
355
357
|
if (parsed.modelOrigin !== undefined && parsed.modelOrigin !== "explicit" && parsed.modelOrigin !== "inherited" && parsed.modelOrigin !== "configured") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': modelOrigin must be 'explicit', 'inherited', or 'configured'.`);
|
|
356
358
|
if (parsed.modelOrigin === undefined && parsed.model !== undefined) parsed.modelOrigin = parsed.modelOverrideFromParent ? "inherited" : "configured";
|
|
357
359
|
for (const field of ["inheritProjectContext", "inheritSkills", "share"] as const) {
|
|
@@ -411,14 +413,8 @@ export function readAsyncRecoveryDescriptor(asyncDir: string | undefined): Steer
|
|
|
411
413
|
if (!Number.isInteger(artifact.cleanupDays) || (artifact.cleanupDays as number) < 0) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': artifactConfig.cleanupDays must be a non-negative integer.`);
|
|
412
414
|
}
|
|
413
415
|
if (parsed.intercomBridge !== undefined) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
for (const field of Object.keys(bridge)) {
|
|
417
|
-
if (field !== "mode" && field !== "instructionFile" && field !== "resultDelivery") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.${field} is not supported.`);
|
|
418
|
-
}
|
|
419
|
-
if (bridge.mode !== undefined && bridge.mode !== "off" && bridge.mode !== "fork-only" && bridge.mode !== "always") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.mode is invalid.`);
|
|
420
|
-
if (bridge.instructionFile !== undefined && typeof bridge.instructionFile !== "string") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.instructionFile must be a string.`);
|
|
421
|
-
if (bridge.resultDelivery !== undefined && typeof bridge.resultDelivery !== "boolean") throw new Error(`Invalid async recovery descriptor '${descriptorPath}': intercomBridge.resultDelivery must be a boolean.`);
|
|
416
|
+
const bridge = validateIntercomBridgeConfig({ value: parsed.intercomBridge, label: "intercomBridge" });
|
|
417
|
+
if (!bridge.ok) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': ${bridge.error}`);
|
|
422
418
|
}
|
|
423
419
|
if (parsed.controlConfig !== undefined) {
|
|
424
420
|
if (!parsed.controlConfig || typeof parsed.controlConfig !== "object" || Array.isArray(parsed.controlConfig)) throw new Error(`Invalid async recovery descriptor '${descriptorPath}': controlConfig must be an object.`);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { AsyncJobState, SubagentState } from "../../shared/types.ts";
|
|
2
2
|
import {
|
|
3
|
-
projectAsyncStatusSnapshot,
|
|
3
|
+
projectAsyncStatusSnapshot as buildAsyncStatusSnapshot,
|
|
4
4
|
type AsyncStatusSnapshotOptions,
|
|
5
|
-
type
|
|
5
|
+
type AsyncStatusSnapshot,
|
|
6
6
|
} from "../shared/async-status-projection.ts";
|
|
7
7
|
|
|
8
8
|
export {
|
|
@@ -10,22 +10,20 @@ export {
|
|
|
10
10
|
ASYNC_STATUS_SNAPSHOT_VERSION,
|
|
11
11
|
} from "../shared/async-status-projection.ts";
|
|
12
12
|
export type {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
AsyncStatusSnapshotActivity,
|
|
14
|
+
AsyncStatusSnapshotCaps,
|
|
15
|
+
AsyncStatusSnapshotHostStep,
|
|
16
16
|
AsyncStatusSnapshotKind,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
AsyncStatusSnapshotNode,
|
|
18
|
+
AsyncStatusSnapshotOmitted,
|
|
19
19
|
AsyncStatusSnapshotOptions,
|
|
20
20
|
AsyncStatusSnapshotState,
|
|
21
|
-
|
|
21
|
+
AsyncStatusSnapshot,
|
|
22
22
|
} from "../shared/async-status-projection.ts";
|
|
23
23
|
|
|
24
24
|
export const ASYNC_STATUS_SNAPSHOT_WIDGET_PREFIX = "PI_SUBAGENT_ASYNC_JSON:";
|
|
25
25
|
|
|
26
|
-
export
|
|
27
|
-
return projectAsyncStatusSnapshot(jobs, options);
|
|
28
|
-
}
|
|
26
|
+
export { buildAsyncStatusSnapshot };
|
|
29
27
|
|
|
30
28
|
export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined, sessionId: string | null | undefined): AsyncJobState[] {
|
|
31
29
|
if (!state || !sessionId || state.currentSessionId !== sessionId) return [];
|
|
@@ -39,7 +37,7 @@ export function asyncStatusSnapshotJobsForState(state: SubagentState | undefined
|
|
|
39
37
|
return [...jobs.values()];
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}):
|
|
40
|
+
export function buildAsyncStatusSnapshotForState(state: SubagentState | undefined, sessionId: string | null | undefined, options: AsyncStatusSnapshotOptions = {}): AsyncStatusSnapshot {
|
|
43
41
|
return buildAsyncStatusSnapshot(asyncStatusSnapshotJobsForState(state, sessionId), options);
|
|
44
42
|
}
|
|
45
43
|
|