pi-subagents 0.60.0 → 0.62.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 +55 -0
- package/docs/agents.md +7 -3
- package/docs/configuration.md +9 -5
- package/docs/extension-api.md +14 -7
- package/docs/models.md +1 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +13 -4
- package/docs/workflows.md +14 -13
- package/install.mjs +1 -1
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +6 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +41 -11
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +4 -4
- package/skills/pi-subagents/references/review-and-validation.md +1 -1
- package/src/agents/agent-management.ts +121 -63
- package/src/agents/agent-serializer.ts +3 -0
- package/src/agents/agents.ts +543 -223
- package/src/agents/runtime-agent-registry.ts +5 -1
- package/src/api/background-work.ts +7 -2
- package/src/api/external-runs.ts +67 -4
- package/src/api/preflight.ts +17 -8
- package/src/api/shared-types.ts +1 -0
- package/src/extension/index.ts +7 -4
- package/src/extension/public-execution.ts +48 -4
- package/src/extension/rpc.ts +62 -4
- package/src/extension/schemas.ts +17 -9
- package/src/extension/tool-description.ts +15 -19
- package/src/runs/background/active-async-capacity.ts +26 -7
- package/src/runs/background/async-execution.ts +98 -51
- package/src/runs/background/async-job-tracker.ts +62 -3
- package/src/runs/background/async-resume.ts +6 -3
- package/src/runs/background/async-status.ts +59 -9
- package/src/runs/background/auto-drain.ts +1 -1
- package/src/runs/background/fleet-view.ts +1 -1
- package/src/runs/background/process-terminal.ts +16 -0
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/run-status.ts +2 -2
- package/src/runs/background/scheduled-runs.ts +63 -6
- package/src/runs/background/steering.ts +4 -1
- package/src/runs/background/subagent-runner.ts +15 -9
- package/src/runs/background/subagent-wait.ts +20 -21
- package/src/runs/background/wait-completions.ts +1 -1
- package/src/runs/background/wait-tool.ts +18 -18
- package/src/runs/foreground/execution.ts +75 -9
- package/src/runs/foreground/subagent-executor.ts +181 -60
- package/src/runs/shared/acceptance.ts +113 -27
- package/src/runs/shared/capability-ceiling.ts +1 -0
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/host-step-status.ts +1 -0
- package/src/runs/shared/model-fallback.ts +61 -17
- package/src/runs/shared/parallel-utils.ts +2 -6
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +24 -12
- package/src/runs/shared/pi-spawn.ts +69 -35
- package/src/runs/shared/structured-output.ts +33 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -3
- package/src/runs/shared/task-intent.ts +17 -6
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/workflow-graph.ts +3 -2
- package/src/shared/atomic-json.ts +3 -1
- package/src/shared/fork-context.ts +0 -12
- package/src/shared/fork-session-cwd.ts +27 -0
- package/src/shared/launch-contract.ts +3 -0
- package/src/shared/types.ts +43 -4
- package/src/shared/workflow-child-permit.ts +91 -0
- package/src/slash/prompt-template-bridge.ts +37 -1
- package/src/slash/slash-commands.ts +18 -26
- package/src/slash/subagents-admin.ts +2 -0
- package/src/tui/render.ts +95 -52
- package/src/workflows/scripted-workflow.ts +153 -4
- package/src/workflows/workflow-child-summary.ts +1 -1
- package/src/workflows/workflow-receipt.ts +41 -4
- package/src/workflows/workflow-resources.ts +150 -0
|
@@ -77,9 +77,10 @@ import {
|
|
|
77
77
|
} from "../shared/parallel-utils.ts";
|
|
78
78
|
import { applyThinkingSuffix, buildPiArgs, cleanupTempDir, deriveForkPromptCacheKey, projectLaunchResolvedChildExtensions, resolvePiLaunchToolPlan, type SubagentTaskDelivery } from "../shared/pi-args.ts";
|
|
79
79
|
import { deriveChildSessionName } from "../../shared/child-session-name.ts";
|
|
80
|
+
import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
|
|
80
81
|
import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledged-extensions.ts";
|
|
81
82
|
import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
|
|
82
|
-
import { createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
|
|
83
|
+
import { clearStructuredOutputCaptures, createStructuredOutputRuntime, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
|
|
83
84
|
import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
|
|
84
85
|
import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
85
86
|
import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
|
|
@@ -130,7 +131,7 @@ import { assertThinkingWithinCeiling, decodeThinkingCeiling, SUBAGENT_THINKING_C
|
|
|
130
131
|
import { launchBindingDigest } from "../../shared/launch-contract.ts";
|
|
131
132
|
import { writeInitialProgressFile } from "../../shared/settings.ts";
|
|
132
133
|
import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
|
|
133
|
-
import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
|
|
134
|
+
import { acceptanceFailureMessage, aggregateAcceptanceReport, buildSkippedAcceptanceLedger, evaluateAcceptance, formatAcceptancePrompt, resolveAcceptanceReportMode, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
|
|
134
135
|
import { attachContractProjections, isAgentContractV1 } from "../shared/agent-contract.ts";
|
|
135
136
|
import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
|
|
136
137
|
import { normalizeExtensionBindings } from "../shared/extension-bindings.ts";
|
|
@@ -1397,7 +1398,7 @@ async function runSingleStepInner(
|
|
|
1397
1398
|
}
|
|
1398
1399
|
|
|
1399
1400
|
const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
|
|
1400
|
-
? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
|
|
1401
|
+
? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"), { acceptanceReport: resolveAcceptanceReportMode(step.acceptanceInput) })
|
|
1401
1402
|
: undefined);
|
|
1402
1403
|
const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
|
|
1403
1404
|
let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
|
|
@@ -1407,6 +1408,7 @@ async function runSingleStepInner(
|
|
|
1407
1408
|
if (!step.runner) {
|
|
1408
1409
|
resolvedTaskToolPlan = resolvePiLaunchToolPlan(omitUndefinedProperties({
|
|
1409
1410
|
tools: step.tools,
|
|
1411
|
+
excludeTools: step.excludeTools,
|
|
1410
1412
|
allowNestedSubagents: step.allowNestedSubagents,
|
|
1411
1413
|
extensions: step.extensions,
|
|
1412
1414
|
subagentOnlyExtensions: step.subagentOnlyExtensions,
|
|
@@ -1627,6 +1629,9 @@ async function runSingleStepInner(
|
|
|
1627
1629
|
const effectiveCwd = step.cwd ?? ctx.cwd;
|
|
1628
1630
|
const cwdError = preflightLaunchCwd(step.requestedCwd ?? effectiveCwd, effectiveCwd);
|
|
1629
1631
|
if (cwdError) return { agent: step.agent, output: cwdError, error: cwdError, exitCode: 1, context: step.context };
|
|
1632
|
+
if (step.context === "fork" && step.sessionFile && fs.existsSync(step.sessionFile)) {
|
|
1633
|
+
alignForkedSessionCwd(step.sessionFile, effectiveCwd);
|
|
1634
|
+
}
|
|
1630
1635
|
|
|
1631
1636
|
const candidates = step.modelCandidates !== undefined
|
|
1632
1637
|
? step.modelCandidates.length > 0 ? step.modelCandidates : [undefined]
|
|
@@ -1681,10 +1686,9 @@ async function runSingleStepInner(
|
|
|
1681
1686
|
}));
|
|
1682
1687
|
const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
|
|
1683
1688
|
if (effectiveStructuredOutput) {
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
// Missing/stale structured-output files are handled after the child exits.
|
|
1689
|
+
const cleanupError = clearStructuredOutputCaptures(effectiveStructuredOutput);
|
|
1690
|
+
if (cleanupError) {
|
|
1691
|
+
return omitUndefinedProperties({ agent: step.agent, output: cleanupError, error: cleanupError, exitCode: 1, context: step.context });
|
|
1688
1692
|
}
|
|
1689
1693
|
}
|
|
1690
1694
|
const watchdogConfig = resolveWatchdogConfig(step.cwd ?? ctx.cwd);
|
|
@@ -1712,6 +1716,7 @@ async function runSingleStepInner(
|
|
|
1712
1716
|
inheritSkills: step.inheritSkills,
|
|
1713
1717
|
requireReadTool: Boolean(step.skills?.length),
|
|
1714
1718
|
tools: step.tools,
|
|
1719
|
+
excludeTools: step.excludeTools,
|
|
1715
1720
|
allowNestedSubagents: step.allowNestedSubagents,
|
|
1716
1721
|
extensions: step.extensions,
|
|
1717
1722
|
subagentOnlyExtensions: step.subagentOnlyExtensions,
|
|
@@ -1761,6 +1766,7 @@ async function runSingleStepInner(
|
|
|
1761
1766
|
if (step.definitionDigest) {
|
|
1762
1767
|
const toolPlan = resolvedTaskToolPlan ?? resolvePiLaunchToolPlan(omitUndefinedProperties({
|
|
1763
1768
|
tools: step.tools,
|
|
1769
|
+
excludeTools: step.excludeTools,
|
|
1764
1770
|
allowNestedSubagents: step.allowNestedSubagents,
|
|
1765
1771
|
extensions: step.extensions,
|
|
1766
1772
|
subagentOnlyExtensions: step.subagentOnlyExtensions,
|
|
@@ -1793,6 +1799,7 @@ async function runSingleStepInner(
|
|
|
1793
1799
|
inheritSkills: step.inheritSkills,
|
|
1794
1800
|
skills: step.skills,
|
|
1795
1801
|
tools: toolPlan.effectiveToolAllowlist,
|
|
1802
|
+
...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
1796
1803
|
extensions: toolPlan.extensionArgs,
|
|
1797
1804
|
mcpDirectTools: toolPlan.effectiveMcpTools,
|
|
1798
1805
|
...(step.outputPath ? { outputPath: step.outputPath } : {}),
|
|
@@ -2149,7 +2156,7 @@ async function runSingleStepInner(
|
|
|
2149
2156
|
report: structuredAcceptanceReport as import("../../shared/types.ts").AcceptanceReport | undefined,
|
|
2150
2157
|
reportError: structuredAcceptanceReportError,
|
|
2151
2158
|
fileOutput: childWrittenOutput !== undefined && step.outputPath
|
|
2152
|
-
? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only" }
|
|
2159
|
+
? { content: childWrittenOutput, path: step.outputPath, authoritative: step.outputMode === "file-only", durable: resolvedOutput.savedPath !== undefined }
|
|
2153
2160
|
: undefined,
|
|
2154
2161
|
cwd: step.cwd ?? ctx.cwd,
|
|
2155
2162
|
signal: combinedAbortSignal([ctx.timeoutSignal, ctx.stopSignal]),
|
|
@@ -5347,7 +5354,6 @@ async function runSubagent(
|
|
|
5347
5354
|
statusPayload.error = `Step failed: ${failedStep.agent}`;
|
|
5348
5355
|
}
|
|
5349
5356
|
}
|
|
5350
|
-
writeStatusPayload();
|
|
5351
5357
|
try {
|
|
5352
5358
|
runPersistence.write(resultPath, {
|
|
5353
5359
|
lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
2
|
+
* `bg_wait` tool: block the current turn until outstanding async runs
|
|
3
3
|
* or a named remembered detached foreground run finishes.
|
|
4
4
|
*
|
|
5
5
|
* Background subagent runs are detached. In an interactive session the parent
|
|
@@ -8,33 +8,33 @@
|
|
|
8
8
|
* cannot work at all non-interactively (`pi -p ...`), where the run is a single
|
|
9
9
|
* turn: once the turn ends there is nothing left to receive the notification.
|
|
10
10
|
*
|
|
11
|
-
* `
|
|
11
|
+
* `bg_wait` closes that gap. It keeps the turn alive until a tracked async
|
|
12
12
|
* run for this session reaches a terminal state (complete / failed / paused),
|
|
13
13
|
* the caller-supplied timeout elapses, or the turn is aborted. Because it awaits
|
|
14
14
|
* inside the turn, the completion the model was told to wait for is actually
|
|
15
15
|
* observed before the tool returns.
|
|
16
16
|
*
|
|
17
|
-
* By default `
|
|
17
|
+
* By default `bg_wait` returns as soon as ONE run finishes, so a fleet
|
|
18
18
|
* manager can use it in a rolling-replacement loop: launch N workers, wait for
|
|
19
|
-
* the next one to finish, spawn its replacement, then call `
|
|
19
|
+
* the next one to finish, spawn its replacement, then call `bg_wait`
|
|
20
20
|
* again — keeping N in flight instead of draining to zero between batches.
|
|
21
21
|
* Pass `all: true` to block until every tracked async run is terminal, or `id`
|
|
22
22
|
* to block on one specific async or remembered detached foreground run.
|
|
23
23
|
*
|
|
24
|
-
* `
|
|
24
|
+
* `bg_wait` also returns when a run needs attention — not just on
|
|
25
25
|
* completion. A child that goes idle or blocks for a decision surfaces
|
|
26
26
|
* `needs_attention` (the same signal Pi shows as a control notice and,
|
|
27
|
-
* interactively, wakes the parent with). Since `
|
|
27
|
+
* interactively, wakes the parent with). Since `bg_wait` is used exactly
|
|
28
28
|
* where there is no next turn to receive that notice, it must break on it too,
|
|
29
29
|
* or a stuck child would stall the loop until the timeout. Attention runs are
|
|
30
30
|
* reported so the caller can inspect / nudge / resume / interrupt them.
|
|
31
31
|
*
|
|
32
|
-
* Wake mechanism: when given Pi's event bus (`deps.events`), `
|
|
32
|
+
* Wake mechanism: when given Pi's event bus (`deps.events`), `bg_wait`
|
|
33
33
|
* subscribes to the subagent completion/control channels and wakes the instant
|
|
34
34
|
* any fires, rather than waiting out a fixed poll interval. A poll still runs
|
|
35
35
|
* on the interval as a reconciliation fallback (crashed runners, missed
|
|
36
36
|
* events), and the poll is the source of truth for what actually changed — the
|
|
37
|
-
* event only ends the sleep early. With no bus, `
|
|
37
|
+
* event only ends the sleep early. With no bus, `bg_wait` degrades to pure
|
|
38
38
|
* polling.
|
|
39
39
|
*/
|
|
40
40
|
|
|
@@ -82,9 +82,8 @@ export interface SubagentWaitParams {
|
|
|
82
82
|
nonBlocking?: boolean;
|
|
83
83
|
/**
|
|
84
84
|
* When true, block until EVERY active run in this session (or matching `id`)
|
|
85
|
-
* is terminal. Default false: return
|
|
86
|
-
*
|
|
87
|
-
* targets a single run.
|
|
85
|
+
* is terminal. Default false: return when the first tracked run or provider
|
|
86
|
+
* item finishes or needs attention. Ignored when `id` targets a single run.
|
|
88
87
|
*/
|
|
89
88
|
all?: boolean;
|
|
90
89
|
/** Give up after this many milliseconds. Defaults to waitTool.defaultTimeoutMs, then 30 minutes. */
|
|
@@ -241,7 +240,7 @@ function foregroundChildrenNeedingAttention(run: ForegroundResumeRun, indices: S
|
|
|
241
240
|
function formatForegroundAttention(run: ForegroundResumeRun, children: ReturnType<typeof foregroundChildrenNeedingAttention>, elapsedMs: number): AgentToolResult<Details> {
|
|
242
241
|
const childList = children.map((child) => `${child.agent}${child.index !== undefined ? `#${child.index}` : ""}`).join(", ");
|
|
243
242
|
return result(
|
|
244
|
-
`Waited ${formatDuration(elapsedMs)} for remembered detached foreground run "${run.runId}"; attention required. ${children.length} child run(s) need attention: ${childList}. Reply to any pending supervisor request, then call
|
|
243
|
+
`Waited ${formatDuration(elapsedMs)} for remembered detached foreground run "${run.runId}"; attention required. ${children.length} child run(s) need attention: ${childList}. Reply to any pending supervisor request, then call bg_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
|
|
245
244
|
);
|
|
246
245
|
}
|
|
247
246
|
|
|
@@ -262,7 +261,7 @@ function backgroundWorkIdentity(item: RegisteredBackgroundWorkItem): string {
|
|
|
262
261
|
|
|
263
262
|
function backgroundWorkForSession(deps: SubagentWaitDeps, nowMs: number): BackgroundWorkSnapshot {
|
|
264
263
|
const sessionId = deps.state.currentSessionId;
|
|
265
|
-
if (!sessionId) throw new Error("
|
|
264
|
+
if (!sessionId) throw new Error("bg_wait requires an active session identity to scope background work safely.");
|
|
266
265
|
return deps.backgroundWork?.snapshot(sessionId, nowMs) ?? snapshotBackgroundWork(sessionId, nowMs);
|
|
267
266
|
}
|
|
268
267
|
|
|
@@ -376,7 +375,7 @@ function windowElapsedResult(
|
|
|
376
375
|
};
|
|
377
376
|
}
|
|
378
377
|
|
|
379
|
-
/** Build the live status shown while async work keeps
|
|
378
|
+
/** Build the live status shown while async work keeps bg_wait blocked. */
|
|
380
379
|
function asyncWaitUpdate(runs: AsyncRunSummary[], providerCount: number, elapsedMs: number): AgentToolResult<Details> {
|
|
381
380
|
const activity = runs.flatMap((run) => {
|
|
382
381
|
const activeSteps = run.steps.filter((step) => step.status === "pending" || step.status === "running");
|
|
@@ -524,7 +523,7 @@ async function waitForDetachedForegroundRun(
|
|
|
524
523
|
}
|
|
525
524
|
if (now() - startedAt >= timeoutMs) {
|
|
526
525
|
return windowElapsedResult(
|
|
527
|
-
`Wait window elapsed after ${formatDuration(timeoutMs)} with remembered foreground run "${run.runId}" still detached. Reply to any pending supervisor request, then call
|
|
526
|
+
`Wait window elapsed after ${formatDuration(timeoutMs)} with remembered foreground run "${run.runId}" still detached. Reply to any pending supervisor request, then call bg_wait({ id: "${run.runId}" }) again or inspect status; do not resume or launch a replacement while it remains detached.`,
|
|
528
527
|
[run.runId],
|
|
529
528
|
);
|
|
530
529
|
}
|
|
@@ -543,10 +542,10 @@ export async function waitForSubagents(
|
|
|
543
542
|
deps: SubagentWaitDeps,
|
|
544
543
|
): Promise<AgentToolResult<Details>> {
|
|
545
544
|
if (deps.enabled === false) {
|
|
546
|
-
return result("
|
|
545
|
+
return result("bg_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED; returning immediately without blocking background work. Active work keeps going, and you can inspect subagents with subagent({ action: \"status\" }) or rely on completion notifications.");
|
|
547
546
|
}
|
|
548
547
|
if (!deps.state.currentSessionId) {
|
|
549
|
-
return result("
|
|
548
|
+
return result("bg_wait requires an active session identity to scope background work safely.", true);
|
|
550
549
|
}
|
|
551
550
|
|
|
552
551
|
const now = deps.now ?? Date.now;
|
|
@@ -587,7 +586,7 @@ export async function waitForSubagents(
|
|
|
587
586
|
const selected = matches[0];
|
|
588
587
|
if (selected && params.nonBlocking) {
|
|
589
588
|
if (!deps.subscribe) {
|
|
590
|
-
return result("Non-blocking wait subscriptions require a long-lived interactive subagent runtime; this runtime can only use blocking
|
|
589
|
+
return result("Non-blocking wait subscriptions require a long-lived interactive subagent runtime; this runtime can only use blocking bg_wait calls.", true);
|
|
591
590
|
}
|
|
592
591
|
try {
|
|
593
592
|
const registration = deps.subscribe({ targetKind: selected.kind, runId: selected.id, requestedId: params.id, timeoutMs });
|
|
@@ -641,7 +640,7 @@ export async function waitForSubagents(
|
|
|
641
640
|
}
|
|
642
641
|
if (now() - startedAt >= timeoutMs) {
|
|
643
642
|
return windowElapsedResult(
|
|
644
|
-
`Wait window elapsed after ${formatDuration(timeoutMs)} with ${activeInitialRuns.length} async run(s) and ${activeInitialProviderItems.length} provider item(s) still active: ${stillActive}. The work keeps going; call
|
|
643
|
+
`Wait window elapsed after ${formatDuration(timeoutMs)} with ${activeInitialRuns.length} async run(s) and ${activeInitialProviderItems.length} provider item(s) still active: ${stillActive}. The work keeps going; call bg_wait again or inspect subagent status.`,
|
|
645
644
|
activeInitialRuns.map((run) => run.id),
|
|
646
645
|
activeInitialProviderItems,
|
|
647
646
|
);
|
|
@@ -653,7 +652,7 @@ export async function waitForSubagents(
|
|
|
653
652
|
providerSnapshot = params.id ? providerSnapshot : backgroundWorkForSession(deps, now());
|
|
654
653
|
for (const provider of initialProviderNames) {
|
|
655
654
|
if (!providerSnapshot.providers.includes(provider)) {
|
|
656
|
-
return result(`Background-work provider '${provider}' disappeared while
|
|
655
|
+
return result(`Background-work provider '${provider}' disappeared while bg_wait was tracking its active work; completion cannot be confirmed.`, true);
|
|
657
656
|
}
|
|
658
657
|
}
|
|
659
658
|
providerActive = providerSnapshot.items;
|
|
@@ -711,7 +710,7 @@ export async function waitForSubagents(
|
|
|
711
710
|
const finishedCount = finishedAsyncCount + providerFinishedCount;
|
|
712
711
|
const subject = initialProviderIds.size === 0 ? "run(s)" : "item(s)";
|
|
713
712
|
const remainder = stillRunning > 0
|
|
714
|
-
? ` ${stillRunning} ${subject} still in flight — call
|
|
713
|
+
? ` ${stillRunning} ${subject} still in flight — call bg_wait again to catch the next one.`
|
|
715
714
|
: relevantAttention.length > 0
|
|
716
715
|
? " No other work is waitable until attention is handled."
|
|
717
716
|
: initialProviderIds.size === 0 ? " No runs remain in flight." : " No work remains in flight.";
|
|
@@ -99,7 +99,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
|
-
* Record a consumed terminal payload for later surfacing by
|
|
102
|
+
* Record a consumed terminal payload for later surfacing by bg_wait, pruning
|
|
103
103
|
* stale entries with the same TTL that dedupes completion notifications. The result
|
|
104
104
|
* file is deleted after delivery, so this record is the only in-process source once
|
|
105
105
|
* the watcher has consumed it.
|
|
@@ -12,32 +12,32 @@ export function registerWaitTool(
|
|
|
12
12
|
subscriptions?: Pick<WaitSubscriptionManager, "arm">,
|
|
13
13
|
defaultTimeoutMs?: number,
|
|
14
14
|
): void {
|
|
15
|
-
const
|
|
16
|
-
name: "subagent_wait",
|
|
17
|
-
label: "Subagent Wait",
|
|
18
|
-
description: `Block until background work owned by this session changes, then return.
|
|
15
|
+
const description = `Wait for background, provider, or detached work that has no native completion notification, then return.
|
|
19
16
|
|
|
20
|
-
|
|
17
|
+
Ordinary async subagent runs already notify this session natively when they complete or need attention. In an interactive chat, return control instead of calling this merely to wait. Use this tool for provider jobs, remembered detached foreground runs, or other background work without a native notification path. Headless runs auto-drain current-session subagent work at agent_end; use this tool only when the current turn must receive non-notifying background work results.
|
|
21
18
|
|
|
22
19
|
• { } — return when the first initially active async run or registered provider item finishes, or when a subagent needs attention.
|
|
23
20
|
• { all: true } — wait for every async run and provider item that was active when the call began.
|
|
24
21
|
• { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
|
|
25
|
-
• { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately.
|
|
22
|
+
• { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. Use this for detached work without native completion delivery; the originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
|
|
26
23
|
• { stopOnAttention: false } — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
|
|
27
24
|
• { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running. Omitted values use waitTool.defaultTimeoutMs, then 30 minutes. Window expiry returns a non-error window_elapsed result with active work identities.
|
|
28
25
|
|
|
29
|
-
Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`
|
|
26
|
+
Non-blocking subscriptions are visible in subagent status and differ from disabling waitTool: waitTool.enabled=false returns immediately without registering any future wake. Provider jobs are session-scoped and identified exactly, so replacing one job with another cannot hide a completion. Provider extensions must be explicitly loaded in this process. In a child agent, keep \`bg_wait\` in the child tool allowlist and load each provider through the agent's extensions or subagentOnlyExtensions; this tool never loads providers or grants tools itself.${enabled ? "" : "\n\nConfigured behavior: bg_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`;
|
|
27
|
+
const execute: ToolDefinition<typeof SubagentWaitParams, Details>["execute"] = async (_id, params, signal, onUpdate, ctx) => finalizeToolResult(await waitForSubagents(params, signal, {
|
|
28
|
+
state,
|
|
29
|
+
events: pi.events,
|
|
30
|
+
enabled,
|
|
31
|
+
...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
|
|
32
|
+
onUpdate,
|
|
33
|
+
...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
|
|
34
|
+
}));
|
|
35
|
+
const primaryTool: ToolDefinition<typeof SubagentWaitParams, Details> = {
|
|
36
|
+
name: "bg_wait",
|
|
37
|
+
label: "Background Wait",
|
|
38
|
+
description,
|
|
30
39
|
parameters: SubagentWaitParams,
|
|
31
|
-
|
|
32
|
-
return finalizeToolResult(await waitForSubagents(params, signal, {
|
|
33
|
-
state,
|
|
34
|
-
events: pi.events,
|
|
35
|
-
enabled,
|
|
36
|
-
...(defaultTimeoutMs !== undefined ? { defaultTimeoutMs } : {}),
|
|
37
|
-
onUpdate,
|
|
38
|
-
...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
|
|
39
|
-
}));
|
|
40
|
-
},
|
|
40
|
+
execute,
|
|
41
41
|
};
|
|
42
|
-
pi.registerTool(
|
|
42
|
+
pi.registerTool(primaryTool);
|
|
43
43
|
}
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import { existsSync
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import type { Message } from "@earendil-works/pi-ai";
|
|
9
9
|
import { discoverAgents, formatUnknownAgentError, unknownAgentDiagnosticContext, type AgentConfig } from "../../agents/agents.ts";
|
|
10
10
|
import { appendAgentRefinementOverlay } from "../../agents/agent-refinements.ts";
|
|
11
|
-
import { alignForkedSessionCwd } from "../../shared/fork-
|
|
11
|
+
import { alignForkedSessionCwd } from "../../shared/fork-session-cwd.ts";
|
|
12
12
|
import {
|
|
13
13
|
ensureArtifactsDir,
|
|
14
14
|
formatOutputArtifactContent,
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
formatEmptyTerminalAssistantResponseError,
|
|
50
50
|
extractToolArgsPreview,
|
|
51
51
|
extractTextFromContent,
|
|
52
|
+
MAX_STREAMED_RECENT_TOOLS,
|
|
52
53
|
boundStreamedRecentTools,
|
|
53
54
|
boundStreamedRecentOutput,
|
|
54
55
|
boundStreamedToolCalls,
|
|
@@ -72,7 +73,7 @@ import { readRuntimeAcknowledgedExtensions } from "../shared/runtime-acknowledge
|
|
|
72
73
|
import { assertAgentAllowedByCapabilityCeiling, decodeSubagentCapabilityCeiling, intersectSubagentCapabilityCeilings, resolveCurrentSubagentCapabilityCeiling, SUBAGENT_CAPABILITY_CEILING_ENV } from "../shared/capability-ceiling.ts";
|
|
73
74
|
import { resolveEffectiveThinking } from "../../shared/model-info.ts";
|
|
74
75
|
import { assertThinkingWithinCeiling, decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
|
|
75
|
-
import { MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
|
|
76
|
+
import { clearStructuredOutputCaptures, MISSING_STRUCTURED_OUTPUT_CALL_ERROR, readStructuredOutput, readStructuredOutputAcceptanceReport } from "../shared/structured-output.ts";
|
|
76
77
|
import { formatMidToolExitError, formatProcessSignalError, isOrdinaryToolForMidToolExit, isUnexplainedProcessSignal } from "../shared/process-signal.ts";
|
|
77
78
|
import { readChildToolDiagnosticError } from "../shared/tool-availability.ts";
|
|
78
79
|
import { buildTimeoutRecoverySummary, collectTrackedMutationEvidence, snapshotTrackedMutations } from "../shared/mutation-evidence.ts";
|
|
@@ -297,6 +298,55 @@ function snapshotStreamResult(result: SingleResult, progress: AgentProgress): Si
|
|
|
297
298
|
return snapshot;
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
interface StructuredDelegationProgressState {
|
|
302
|
+
currentTool?: string;
|
|
303
|
+
currentToolArgs?: string;
|
|
304
|
+
recentOutput: string[];
|
|
305
|
+
recentTools: Array<{ tool: string; args: string }>;
|
|
306
|
+
activityState?: AgentProgress["activityState"];
|
|
307
|
+
model?: string;
|
|
308
|
+
toolCount: number;
|
|
309
|
+
tokens: number;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function captureStructuredDelegationProgressState(progress: AgentProgress, result: SingleResult): StructuredDelegationProgressState {
|
|
313
|
+
return {
|
|
314
|
+
currentTool: progress.currentTool,
|
|
315
|
+
currentToolArgs: progress.currentToolArgs,
|
|
316
|
+
recentOutput: [...progress.recentOutput],
|
|
317
|
+
recentTools: progress.recentTools.slice(-MAX_STREAMED_RECENT_TOOLS).map(({ tool, args }) => ({ tool, args })),
|
|
318
|
+
activityState: progress.activityState,
|
|
319
|
+
model: progress.model ?? result.model,
|
|
320
|
+
toolCount: progress.toolCount,
|
|
321
|
+
tokens: progress.tokens,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function structuredDelegationProgressChanged(
|
|
326
|
+
previous: StructuredDelegationProgressState,
|
|
327
|
+
progress: AgentProgress,
|
|
328
|
+
result: SingleResult,
|
|
329
|
+
): boolean {
|
|
330
|
+
if (previous.currentTool !== progress.currentTool
|
|
331
|
+
|| previous.currentToolArgs !== progress.currentToolArgs
|
|
332
|
+
|| previous.activityState !== progress.activityState
|
|
333
|
+
|| previous.model !== (progress.model ?? result.model)
|
|
334
|
+
|| previous.toolCount !== progress.toolCount
|
|
335
|
+
|| previous.tokens !== progress.tokens
|
|
336
|
+
|| previous.recentOutput.length !== progress.recentOutput.length) return true;
|
|
337
|
+
for (let index = 0; index < progress.recentOutput.length; index++) {
|
|
338
|
+
if (previous.recentOutput[index] !== progress.recentOutput[index]) return true;
|
|
339
|
+
}
|
|
340
|
+
const recentToolsStart = Math.max(0, progress.recentTools.length - MAX_STREAMED_RECENT_TOOLS);
|
|
341
|
+
if (previous.recentTools.length !== progress.recentTools.length - recentToolsStart) return true;
|
|
342
|
+
for (let index = recentToolsStart; index < progress.recentTools.length; index++) {
|
|
343
|
+
const previousTool = previous.recentTools[index - recentToolsStart];
|
|
344
|
+
const currentTool = progress.recentTools[index];
|
|
345
|
+
if (previousTool?.tool !== currentTool?.tool || previousTool?.args !== currentTool?.args) return true;
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
300
350
|
const AFTER_COMPACTION_SETTLEMENT = Symbol("afterCompactionSettlement");
|
|
301
351
|
type AbortRecoverySingleResult = SingleResult & { [AFTER_COMPACTION_SETTLEMENT]?: true };
|
|
302
352
|
|
|
@@ -361,6 +411,7 @@ async function runSingleAttempt(
|
|
|
361
411
|
inheritSkills: agent.inheritSkills,
|
|
362
412
|
requireReadTool: Boolean(shared.resolvedSkillNames?.length),
|
|
363
413
|
tools: agent.tools,
|
|
414
|
+
excludeTools: agent.excludeTools,
|
|
364
415
|
allowNestedSubagents: agent.allowNestedSubagents,
|
|
365
416
|
extensions: agent.extensions,
|
|
366
417
|
subagentOnlyExtensions: agent.subagentOnlyExtensions,
|
|
@@ -414,6 +465,7 @@ async function runSingleAttempt(
|
|
|
414
465
|
cwd: options.cwd ?? runtimeCwd,
|
|
415
466
|
requireReadTool: Boolean(shared.resolvedSkillNames?.length),
|
|
416
467
|
structuredOutput: Boolean(options.structuredOutput),
|
|
468
|
+
excludeTools: agent.excludeTools,
|
|
417
469
|
fast: options.fast ?? agent.fast,
|
|
418
470
|
model: modelArg,
|
|
419
471
|
modelCandidates: shared.modelCandidates,
|
|
@@ -470,6 +522,7 @@ async function runSingleAttempt(
|
|
|
470
522
|
inheritSkills: agent.inheritSkills,
|
|
471
523
|
skills: shared.resolvedSkillNames ?? [],
|
|
472
524
|
tools: toolPlan.effectiveToolAllowlist,
|
|
525
|
+
...(toolPlan.excludeTools.length > 0 ? { excludeTools: toolPlan.excludeTools } : {}),
|
|
473
526
|
extensions: toolPlan.extensionArgs,
|
|
474
527
|
mcpDirectTools: toolPlan.effectiveMcpTools,
|
|
475
528
|
...(options.outputPath ? { outputPath: options.outputPath } : {}),
|
|
@@ -501,10 +554,14 @@ async function runSingleAttempt(
|
|
|
501
554
|
}, options.context);
|
|
502
555
|
const startTime = Date.now();
|
|
503
556
|
if (options.structuredOutput) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
557
|
+
const cleanupError = clearStructuredOutputCaptures(options.structuredOutput);
|
|
558
|
+
if (cleanupError) {
|
|
559
|
+
cleanupTempDir(tempDir);
|
|
560
|
+
result.exitCode = 1;
|
|
561
|
+
result.error = cleanupError;
|
|
562
|
+
result.finalOutput = cleanupError;
|
|
563
|
+
result.progressSummary = { toolCount: 0, tokens: 0, durationMs: Date.now() - startTime };
|
|
564
|
+
return result;
|
|
508
565
|
}
|
|
509
566
|
}
|
|
510
567
|
const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
|
|
@@ -936,6 +993,7 @@ async function runSingleAttempt(
|
|
|
936
993
|
};
|
|
937
994
|
|
|
938
995
|
|
|
996
|
+
let lastStructuredDelegationProgressState: StructuredDelegationProgressState | undefined;
|
|
939
997
|
const emitUpdateSnapshot = (text: string) => {
|
|
940
998
|
if (!options.onUpdate || processClosed) return;
|
|
941
999
|
const progressSnapshot = snapshotProgress(progress);
|
|
@@ -955,6 +1013,10 @@ async function runSingleAttempt(
|
|
|
955
1013
|
const fireUpdate = () => {
|
|
956
1014
|
if (!options.onUpdate || processClosed) return;
|
|
957
1015
|
progress.durationMs = Date.now() - startTime;
|
|
1016
|
+
if (options.suppressUnchangedDelegationUpdates) {
|
|
1017
|
+
if (lastStructuredDelegationProgressState && !structuredDelegationProgressChanged(lastStructuredDelegationProgressState, progress, result)) return;
|
|
1018
|
+
lastStructuredDelegationProgressState = captureStructuredDelegationProgressState(progress, result);
|
|
1019
|
+
}
|
|
958
1020
|
const output = result.timedOut && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages ?? []);
|
|
959
1021
|
emitUpdateSnapshot(output || "(running...)");
|
|
960
1022
|
};
|
|
@@ -1816,7 +1878,11 @@ async function runSyncCompletionInner(
|
|
|
1816
1878
|
agent.fallbackModels,
|
|
1817
1879
|
options.availableModels,
|
|
1818
1880
|
agent.modelProvider ?? options.preferredModelProvider,
|
|
1819
|
-
{
|
|
1881
|
+
{
|
|
1882
|
+
scope: options.modelScope,
|
|
1883
|
+
primaryModelFromParent: options.modelOverrideFromParent,
|
|
1884
|
+
origin: options.modelOrigin ?? (options.modelOverrideFromParent ? "inherited" : "configured"),
|
|
1885
|
+
},
|
|
1820
1886
|
);
|
|
1821
1887
|
if (options.workflowChildPermitLaunch && candidates.length > 1) {
|
|
1822
1888
|
const error = "Workflow child permit does not support model fallback.";
|
|
@@ -2159,7 +2225,7 @@ async function runSyncCompletionInner(
|
|
|
2159
2225
|
report: (result as SingleResult & { structuredAcceptanceReport?: import("../../shared/types.ts").AcceptanceReport; structuredAcceptanceReportError?: string }).structuredAcceptanceReport,
|
|
2160
2226
|
reportError: (result as SingleResult & { structuredAcceptanceReport?: import("../../shared/types.ts").AcceptanceReport; structuredAcceptanceReportError?: string }).structuredAcceptanceReportError,
|
|
2161
2227
|
fileOutput: childWrittenOutput !== undefined && options.outputPath
|
|
2162
|
-
? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only" }
|
|
2228
|
+
? { content: childWrittenOutput, path: options.outputPath, authoritative: options.outputMode === "file-only", durable: result.savedOutputPath !== undefined }
|
|
2163
2229
|
: undefined,
|
|
2164
2230
|
cwd: options.cwd ?? runtimeCwd,
|
|
2165
2231
|
reportOptional: isAgentContractV1(options.agentContract),
|