pi-subagents 0.52.1 → 0.54.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 +69 -0
- package/README.md +4 -0
- package/docs/configuration.md +12 -2
- package/docs/extension-api.md +3 -1
- package/docs/models.md +17 -3
- package/docs/workflows.md +2 -0
- package/index.ts +10 -1
- package/package.json +2 -1
- package/prompts/council.md +51 -0
- package/skills/council-mode/SKILL.md +231 -0
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -0
- package/skills/pi-subagents/references/execution-controls.md +13 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +7 -0
- package/src/agents/agent-management.ts +107 -8
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +96 -37
- package/src/agents/builtin-names.ts +9 -0
- package/src/agents/runtime-agent-registry.ts +418 -0
- package/src/api/agents.ts +7 -0
- package/src/api/preflight.ts +8 -3
- package/src/extension/config.ts +3 -0
- package/src/extension/doctor.ts +11 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +20 -4
- package/src/extension/public-execution.ts +1 -1
- package/src/extension/rpc.ts +41 -1
- package/src/extension/schemas.ts +6 -3
- package/src/extension/tool-description.ts +2 -2
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/runs/background/async-execution.ts +12 -6
- package/src/runs/background/async-job-tracker.ts +4 -3
- package/src/runs/background/async-resume.ts +2 -1
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/async-status-snapshot.ts +14 -5
- package/src/runs/background/auto-drain.ts +1 -0
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/result-watcher.ts +9 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +32 -5
- package/src/runs/background/subagent-wait.ts +9 -5
- package/src/runs/background/terminal-run-index.ts +15 -6
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/execution.ts +34 -2
- package/src/runs/foreground/subagent-executor.ts +196 -52
- package/src/runs/foreground/workflow-detach-reconcile.ts +83 -15
- package/src/runs/shared/acceptance.ts +44 -1
- package/src/runs/shared/model-exclusions.ts +242 -0
- package/src/runs/shared/model-fallback.ts +72 -16
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/pi-args.ts +34 -1
- package/src/runs/shared/subagent-control.ts +25 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +36 -9
- package/src/shared/fork-context.ts +17 -1
- package/src/shared/model-info.ts +20 -0
- package/src/shared/settings.ts +2 -2
- package/src/shared/types.ts +47 -2
- package/src/slash/slash-commands.ts +20 -6
- package/src/slash/slash-live-state.ts +3 -3
- package/src/tui/fleet-status.ts +86 -1
- package/src/tui/fleet.ts +55 -2
- package/src/tui/render.ts +73 -3
- package/src/watchdog/permission-arbiter.ts +59 -51
- package/src/workflows/scripted-workflow.ts +100 -12
- package/src/workflows/workflow-receipt.ts +140 -0
|
@@ -86,6 +86,8 @@ export interface SubagentWaitParams {
|
|
|
86
86
|
all?: boolean;
|
|
87
87
|
/** Give up after this many milliseconds. Defaults to 30 minutes. */
|
|
88
88
|
timeoutMs?: number;
|
|
89
|
+
/** False keeps a blocking wait open through idle attention; supervisor/contact requests still stop the wait. */
|
|
90
|
+
stopOnAttention?: boolean;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/** Minimal event-bus surface wait subscribes to (matches pi.events). */
|
|
@@ -110,6 +112,8 @@ export interface SubagentWaitDeps {
|
|
|
110
112
|
stopOnAttention?: boolean;
|
|
111
113
|
/** Internal auto-drain mode surfaces failed terminal subagent runs as errors. */
|
|
112
114
|
failOnFailedRuns?: boolean;
|
|
115
|
+
/** Internal auto-drain mode surfaces actionable attention as an error. */
|
|
116
|
+
failOnAttention?: boolean;
|
|
113
117
|
/** Arm a durable exact-target wait subscription in a long-lived interactive runtime. */
|
|
114
118
|
subscribe?: (input: { targetKind: "async" | "foreground"; runId: string; requestedId: string; timeoutMs: number }) => { token: string; expiresAt: number };
|
|
115
119
|
/** Injectable provider protocol surfaces for deterministic tests. */
|
|
@@ -453,7 +457,7 @@ async function waitForDetachedForegroundRun(
|
|
|
453
457
|
return result(`Remembered foreground run "${run.runId}" disappeared before a terminal child result was recorded. Completion cannot be confirmed; do not launch a replacement without checking the originating child session.`, true);
|
|
454
458
|
}
|
|
455
459
|
const pending = current.children.filter((child) => initialDetachedIndices.has(child.index) && child.status === "detached");
|
|
456
|
-
const attention =
|
|
460
|
+
const attention = foregroundChildrenNeedingAttention(current, initialDetachedIndices);
|
|
457
461
|
if (attention.length > 0) return formatForegroundAttention(current, attention, now() - startedAt);
|
|
458
462
|
if (pending.length === 0) {
|
|
459
463
|
const outcome = summarizeForegroundChildren(current, initialDetachedIndices);
|
|
@@ -555,11 +559,11 @@ export async function waitForSubagents(
|
|
|
555
559
|
const initialProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
556
560
|
const initialProviderNames = new Set(providerActive.map((item) => item.provider));
|
|
557
561
|
const initialCount = initialAsyncIds.size + initialProviderIds.size;
|
|
558
|
-
const stopOnAttention = deps.stopOnAttention !== false;
|
|
562
|
+
const stopOnAttention = params.stopOnAttention ?? deps.stopOnAttention !== false;
|
|
559
563
|
let attention = active.filter((run) => needsAttention(run));
|
|
560
564
|
|
|
561
565
|
const isDone = (): boolean => {
|
|
562
|
-
if (
|
|
566
|
+
if (attention.some((run) => initialAsyncIds.has(run.id) && (stopOnAttention || hasSupervisorTool(run)))) return true;
|
|
563
567
|
const activeAsyncIds = new Set(active.map((run) => run.id));
|
|
564
568
|
const activeProviderIds = new Set(providerActive.map(backgroundWorkIdentity));
|
|
565
569
|
if (waitForAll) {
|
|
@@ -643,7 +647,7 @@ export async function waitForSubagents(
|
|
|
643
647
|
const status = relevantAttention.length > 0 ? "attention required" : "done";
|
|
644
648
|
return result(
|
|
645
649
|
`Waited ${elapsed} for ${scope}; ${status}.${outcome}${resumeGuidance}${attentionNote} Completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
646
|
-
deps.failOnFailedRuns === true && failedAsyncCount > 0,
|
|
650
|
+
(deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
|
|
647
651
|
completions,
|
|
648
652
|
);
|
|
649
653
|
}
|
|
@@ -660,7 +664,7 @@ export async function waitForSubagents(
|
|
|
660
664
|
: `${finishedCount} of ${initialCount} ${subject} finished`;
|
|
661
665
|
return result(
|
|
662
666
|
`Waited ${elapsed}; ${progress}.${outcome}${resumeGuidance}${attentionNote}${remainder} Relevant completion/control events have been observed; inspect status if a notification is not visible yet.`,
|
|
663
|
-
deps.failOnFailedRuns === true && failedAsyncCount > 0,
|
|
667
|
+
(deps.failOnFailedRuns === true && failedAsyncCount > 0) || (deps.failOnAttention === true && relevantAttention.length > 0),
|
|
664
668
|
completions,
|
|
665
669
|
);
|
|
666
670
|
}
|
|
@@ -68,11 +68,16 @@ function removeInvalidMarker(marker: string): void {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
interface MarkerFile {
|
|
72
|
+
dir: string;
|
|
73
|
+
name: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function markerFiles(dir: string): MarkerFile[] {
|
|
72
77
|
try {
|
|
73
78
|
return fs.readdirSync(dir, { withFileTypes: true })
|
|
74
79
|
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
75
|
-
.map((entry) =>
|
|
80
|
+
.map((entry) => ({ dir, name: entry.name }));
|
|
76
81
|
} catch (error) {
|
|
77
82
|
const code = (error as NodeJS.ErrnoException).code;
|
|
78
83
|
if (code === "ENOENT" || code === "ENOTDIR") return [];
|
|
@@ -93,12 +98,16 @@ function sessionDirs(asyncDirRoot: string, sessionId: string | undefined): strin
|
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
function recentMarkerFiles(dirs: string[], limit: number): string[] {
|
|
102
|
+
return dirs.flatMap(markerFiles)
|
|
103
|
+
.sort((left, right) => right.name.localeCompare(left.name))
|
|
104
|
+
.slice(0, limit)
|
|
105
|
+
.map((marker) => path.join(marker.dir, marker.name));
|
|
106
|
+
}
|
|
107
|
+
|
|
96
108
|
export function readRecentTerminalRunIndex(asyncDirRoot: string, options: { sessionId?: string; limit?: number } = {}): string[] {
|
|
97
109
|
const limit = options.limit === undefined ? Number.POSITIVE_INFINITY : Math.max(0, Math.floor(options.limit));
|
|
98
|
-
const candidates = sessionDirs(asyncDirRoot, options.sessionId)
|
|
99
|
-
.flatMap(markerFiles)
|
|
100
|
-
.sort((left, right) => path.basename(right).localeCompare(path.basename(left)))
|
|
101
|
-
.slice(0, limit);
|
|
110
|
+
const candidates = recentMarkerFiles(sessionDirs(asyncDirRoot, options.sessionId), limit);
|
|
102
111
|
const runIds: string[] = [];
|
|
103
112
|
const seen = new Set<string>();
|
|
104
113
|
for (const marker of candidates) {
|
|
@@ -44,6 +44,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
44
44
|
const childRunId = asNonEmptyString(child.runId);
|
|
45
45
|
const error = asNonEmptyString(child.error);
|
|
46
46
|
const model = asNonEmptyString(child.model);
|
|
47
|
+
const contextOverflow = child.contextOverflow === true;
|
|
47
48
|
return [{
|
|
48
49
|
...(agent ? { agent } : {}),
|
|
49
50
|
...(childRunId ? { runId: childRunId } : {}),
|
|
@@ -51,6 +52,7 @@ export function toWaitCompletion(data: Record<string, unknown>, runId: string):
|
|
|
51
52
|
...(outputState ? { outputState } : {}),
|
|
52
53
|
...(error ? { error } : {}),
|
|
53
54
|
...(model ? { model } : {}),
|
|
55
|
+
...(contextOverflow ? { contextOverflow: true } : {}),
|
|
54
56
|
...(artifactPaths ? { artifactPaths } : {}),
|
|
55
57
|
}];
|
|
56
58
|
})
|
|
@@ -3,6 +3,7 @@ import { SubagentWaitParams } from "../../extension/schemas.ts";
|
|
|
3
3
|
import type { Details, SubagentState } from "../../shared/types.ts";
|
|
4
4
|
import { resolveWaitToolConfig, waitForSubagents } from "./subagent-wait.ts";
|
|
5
5
|
import type { WaitSubscriptionManager } from "./wait-subscriptions.ts";
|
|
6
|
+
import { finalizeToolResult } from "../../extension/tool-result.ts";
|
|
6
7
|
|
|
7
8
|
export function registerWaitTool(pi: ExtensionAPI, state: SubagentState, enabled = resolveWaitToolConfig().enabled, subscriptions?: Pick<WaitSubscriptionManager, "arm">): void {
|
|
8
9
|
const tool: ToolDefinition<typeof SubagentWaitParams, Details> = {
|
|
@@ -16,18 +17,19 @@ In an interactive chat, do not call this merely to wait: return control to the u
|
|
|
16
17
|
• { all: true } — wait for every async run and provider item that was active when the call began.
|
|
17
18
|
• { id: "..." } — wait for one async or remembered detached foreground subagent run (id or prefix).
|
|
18
19
|
• { id: "...", nonBlocking: true } — resolve the prefix once, persist an exact-run wake subscription, and return immediately. The originating interactive session wakes on completion, failure, attention, reconciliation failure, or timeout.
|
|
20
|
+
• { stopOnAttention: false } — for blocking waits only, keep waiting through idle or long-thinking attention; supervisor/contact requests still stop the wait.
|
|
19
21
|
• { timeoutMs: 600000 } — stop waiting after N ms; active work keeps running.
|
|
20
22
|
|
|
21
23
|
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 \`subagent_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: subagent_wait is disabled by config.waitTool or PI_SUBAGENT_WAIT_TOOL_ENABLED and returns immediately without blocking."}`,
|
|
22
24
|
parameters: SubagentWaitParams,
|
|
23
|
-
execute(_id, params, signal, onUpdate, ctx) {
|
|
24
|
-
return waitForSubagents(params, signal, {
|
|
25
|
+
async execute(_id, params, signal, onUpdate, ctx) {
|
|
26
|
+
return finalizeToolResult(await waitForSubagents(params, signal, {
|
|
25
27
|
state,
|
|
26
28
|
events: pi.events,
|
|
27
29
|
enabled,
|
|
28
30
|
onUpdate,
|
|
29
31
|
...(subscriptions && ctx?.hasUI ? { subscribe: (input) => subscriptions.arm(input) } : {}),
|
|
30
|
-
});
|
|
32
|
+
}));
|
|
31
33
|
},
|
|
32
34
|
};
|
|
33
35
|
pi.registerTool(tool);
|
|
@@ -73,7 +73,9 @@ import { captureSingleOutputSnapshot, extractChildWrittenOutput, finalizeSingleO
|
|
|
73
73
|
import {
|
|
74
74
|
buildModelCandidates,
|
|
75
75
|
formatModelAttemptNote,
|
|
76
|
+
isContextOverflow,
|
|
76
77
|
isRetryableModelFailure,
|
|
78
|
+
recordRetryableModelFailure,
|
|
77
79
|
} from "../shared/model-fallback.ts";
|
|
78
80
|
import {
|
|
79
81
|
SUBAGENT_STARTUP_RETRY_DELAYS_MS,
|
|
@@ -307,6 +309,7 @@ async function runSingleAttempt(
|
|
|
307
309
|
originalTask?: string;
|
|
308
310
|
taskDelivery?: SubagentTaskDelivery;
|
|
309
311
|
orcaProgressTab?: OrcaProgressTab;
|
|
312
|
+
launchWarnings: { emitted: boolean };
|
|
310
313
|
},
|
|
311
314
|
): Promise<SingleResult> {
|
|
312
315
|
const effectiveThinking = options.thinkingOverride ?? agent.thinking;
|
|
@@ -325,7 +328,7 @@ async function runSingleAttempt(
|
|
|
325
328
|
const permissionAuditPath = permissionRules && options.artifactsDir
|
|
326
329
|
? path.join(options.artifactsDir, "permission-audit", `${options.runId}-${options.index ?? 0}.jsonl`)
|
|
327
330
|
: undefined;
|
|
328
|
-
const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit } = buildPiArgs({
|
|
331
|
+
const { args, env: sharedEnv, tempDir, toolDiagnosticPath, runtimeAcknowledgedExtensionsPath, capabilityAudit, warnings } = buildPiArgs({
|
|
329
332
|
baseArgs: ["--mode", "json", "-p"],
|
|
330
333
|
task,
|
|
331
334
|
taskDelivery: shared.taskDelivery,
|
|
@@ -368,6 +371,10 @@ async function runSingleAttempt(
|
|
|
368
371
|
waitToolEnabled: options.waitToolEnabled,
|
|
369
372
|
capabilityCeiling: options.capabilityCeiling,
|
|
370
373
|
});
|
|
374
|
+
if (!shared.launchWarnings.emitted && warnings.length > 0) {
|
|
375
|
+
for (const warning of warnings) console.warn(`[pi-subagents] ${warning}`);
|
|
376
|
+
shared.launchWarnings.emitted = true;
|
|
377
|
+
}
|
|
371
378
|
|
|
372
379
|
const effectiveSystemPrompt = appendTurnBudgetSystemPrompt(shared.systemPrompt, options.turnBudget);
|
|
373
380
|
const toolPlan = resolvePiLaunchToolPlan({
|
|
@@ -381,6 +388,7 @@ async function runSingleAttempt(
|
|
|
381
388
|
capabilityCeiling: options.capabilityCeiling,
|
|
382
389
|
inheritedCapabilityCeiling: decodeSubagentCapabilityCeiling(process.env[SUBAGENT_CAPABILITY_CEILING_ENV]),
|
|
383
390
|
agentName: agent.name,
|
|
391
|
+
permissionRules,
|
|
384
392
|
});
|
|
385
393
|
const launchResolvedExtensions = projectLaunchResolvedChildExtensions(toolPlan);
|
|
386
394
|
const launchContractDigest = launchBindingDigest({
|
|
@@ -872,6 +880,7 @@ async function runSingleAttempt(
|
|
|
872
880
|
startedAt: startTime,
|
|
873
881
|
lastActivityAt: progress.lastActivityAt,
|
|
874
882
|
currentTool: progress.currentTool,
|
|
883
|
+
thinking: resolvedThinking,
|
|
875
884
|
now,
|
|
876
885
|
});
|
|
877
886
|
if (idleState === "needs_attention") {
|
|
@@ -1049,6 +1058,20 @@ async function runSingleAttempt(
|
|
|
1049
1058
|
}
|
|
1050
1059
|
|
|
1051
1060
|
if (evt.type === "tool_result_end" && evt.message) {
|
|
1061
|
+
const toolResultCompletion = {
|
|
1062
|
+
toolCallId: (evt.message as { toolCallId?: unknown }).toolCallId ?? (evt as { toolCallId?: unknown }).toolCallId,
|
|
1063
|
+
toolName: (evt.message as { toolName?: unknown }).toolName ?? (evt as { toolName?: unknown }).toolName,
|
|
1064
|
+
};
|
|
1065
|
+
clearActiveToolTimeout(toolResultCompletion);
|
|
1066
|
+
const endedTool = removeActiveToolCall(toolResultCompletion);
|
|
1067
|
+
if (endedTool) {
|
|
1068
|
+
progress.recentTools.push({
|
|
1069
|
+
tool: endedTool.tool,
|
|
1070
|
+
args: endedTool.args,
|
|
1071
|
+
endMs: now,
|
|
1072
|
+
});
|
|
1073
|
+
refreshCurrentTool();
|
|
1074
|
+
}
|
|
1052
1075
|
result.messages!.push(evt.message);
|
|
1053
1076
|
const resultText = extractTextFromContent(evt.message.content);
|
|
1054
1077
|
if (options.toolBudget && pendingToolResult && resultText.includes("Tool budget hard limit reached")) {
|
|
@@ -1679,6 +1702,7 @@ async function runSyncCompletionInner(
|
|
|
1679
1702
|
const modelAttempts: ModelAttempt[] = [];
|
|
1680
1703
|
const aggregateUsage = emptyUsage();
|
|
1681
1704
|
const attemptNotes: string[] = [];
|
|
1705
|
+
const launchWarnings = { emitted: false };
|
|
1682
1706
|
let totalToolCount = 0;
|
|
1683
1707
|
let totalDurationMs = 0;
|
|
1684
1708
|
|
|
@@ -1770,6 +1794,7 @@ async function runSyncCompletionInner(
|
|
|
1770
1794
|
originalTask: task,
|
|
1771
1795
|
taskDelivery: taskDeliveryOverride,
|
|
1772
1796
|
orcaProgressTab,
|
|
1797
|
+
launchWarnings,
|
|
1773
1798
|
});
|
|
1774
1799
|
lastResult = result;
|
|
1775
1800
|
if (startupAttemptIndex === 0) {
|
|
@@ -1860,7 +1885,14 @@ async function runSyncCompletionInner(
|
|
|
1860
1885
|
attempt.error = startupError;
|
|
1861
1886
|
break modelAttemptsLoop;
|
|
1862
1887
|
}
|
|
1863
|
-
|
|
1888
|
+
const retryableModelFailure = isRetryableModelFailure(result.error);
|
|
1889
|
+
if (retryableModelFailure) recordRetryableModelFailure(result.model ?? candidate, result.error);
|
|
1890
|
+
if (isContextOverflow(result.error)) {
|
|
1891
|
+
result.contextOverflow = true;
|
|
1892
|
+
attemptNotes.push(`[fallback] ${attempt.model} failed: context overflow — the input exceeds this model's context window. Reduce the task input or use a model with a larger context window.`);
|
|
1893
|
+
break modelAttemptsLoop;
|
|
1894
|
+
}
|
|
1895
|
+
if (!retryableModelFailure || modelIndex === modelsToTry.length - 1) break modelAttemptsLoop;
|
|
1864
1896
|
attemptNotes.push(formatModelAttemptNote(attempt, modelsToTry[modelIndex + 1]));
|
|
1865
1897
|
break;
|
|
1866
1898
|
}
|