pi-subagents 0.53.0 → 0.55.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 +66 -5
- package/README.md +1 -1
- package/agents/reviewer.md +12 -2
- package/docs/agents.md +4 -4
- package/docs/configuration.md +6 -6
- package/docs/extension-api.md +14 -4
- package/docs/models.md +41 -6
- package/docs/observability.md +14 -3
- package/docs/tool-reference.md +21 -6
- package/docs/workflows.md +7 -1
- package/index.ts +10 -1
- package/package.json +1 -1
- package/prompts/council.md +19 -7
- package/prompts/parallel-review.md +5 -1
- package/prompts/review-loop.md +9 -5
- package/skills/council-mode/SKILL.md +50 -26
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
- package/skills/pi-subagents/references/execution-controls.md +13 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
- package/src/agents/agent-management.ts +115 -14
- package/src/agents/agent-serializer.ts +2 -2
- package/src/agents/agents.ts +195 -47
- package/src/api/external-job-provider.ts +10 -1
- package/src/api/preflight.ts +33 -19
- package/src/api/project-panes.ts +2 -0
- package/src/extension/doctor.ts +10 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +24 -4
- package/src/extension/public-execution.ts +12 -9
- package/src/extension/rpc.ts +77 -3
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -6
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/inspectors/herdr/focus.ts +55 -0
- package/src/inspectors/herdr/project-panes.ts +228 -44
- package/src/integrations/herdr-status.ts +26 -4
- package/src/runs/background/async-execution.ts +112 -13
- package/src/runs/background/async-job-tracker.ts +33 -14
- package/src/runs/background/async-resume.ts +20 -2
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/async-status.ts +10 -0
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/control-channel.ts +98 -10
- package/src/runs/background/notify.ts +62 -1
- package/src/runs/background/result-watcher.ts +4 -3
- package/src/runs/background/run-status.ts +15 -1
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +251 -38
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +4 -3
- package/src/runs/foreground/async-stop-action.ts +23 -2
- package/src/runs/foreground/execution.ts +110 -8
- package/src/runs/foreground/subagent-executor.ts +361 -45
- package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
- package/src/runs/shared/acceptance.ts +1 -0
- package/src/runs/shared/child-identity.ts +36 -0
- package/src/runs/shared/completion-guard.ts +50 -1
- package/src/runs/shared/external-job-bridge.ts +53 -37
- package/src/runs/shared/external-job-runner.ts +126 -23
- package/src/runs/shared/model-fallback.ts +46 -15
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/orca-progress-tabs.ts +71 -9
- package/src/runs/shared/parallel-utils.ts +12 -0
- package/src/runs/shared/pi-args.ts +31 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
- package/src/runs/shared/tool-availability.ts +1 -3
- package/src/shared/launch-contract.ts +6 -5
- package/src/shared/thinking-ceiling.ts +52 -0
- package/src/shared/types.ts +72 -5
- package/src/tui/fleet-status.ts +66 -13
- package/src/tui/render.ts +5 -4
- package/src/watchdog/permission-arbiter.ts +59 -51
- package/src/workflows/scripted-workflow.ts +107 -10
|
@@ -11,6 +11,7 @@ import { RUNTIME_EXTENSION_ACK_EVENT, RUNTIME_EXTENSION_ACK_PATH_ENV, isRuntimeA
|
|
|
11
11
|
import { createStructuredOutputToolParameters, STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
12
12
|
import {
|
|
13
13
|
CHILD_TOOL_DIAGNOSTIC_PATH_ENV,
|
|
14
|
+
formatChildToolDiagnostic,
|
|
14
15
|
MCP_DIRECT_CHILD_TOOLS_ENV,
|
|
15
16
|
REQUIRED_CHILD_TOOLS_ENV,
|
|
16
17
|
writeChildToolDiagnostic,
|
|
@@ -21,7 +22,7 @@ import type { JsonSchemaObject, ResolvedToolBudget, SubagentState } from "../../
|
|
|
21
22
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
22
23
|
import { resolveWatchPath } from "../../shared/utils.ts";
|
|
23
24
|
import { registerChildWatchdog } from "../../watchdog/register-child.ts";
|
|
24
|
-
import { CHILD_WATCHDOG_CONFIG_ENV } from "../../watchdog/child-status.ts";
|
|
25
|
+
import { CHILD_WATCHDOG_CONFIG_ENV, decodeChildWatchdogConfig } from "../../watchdog/child-status.ts";
|
|
25
26
|
import { requestWatchdogPermission, type WatchdogPermissionRequest, type WatchdogPermissionResult } from "../../watchdog/permission-arbiter.ts";
|
|
26
27
|
import { SUBAGENT_WATCHDOG_WARNING_TYPE } from "../../watchdog/types.ts";
|
|
27
28
|
import { resolveWaitToolConfig } from "../background/wait-config.ts";
|
|
@@ -296,14 +297,41 @@ export function registerPermissionGate(
|
|
|
296
297
|
const decision = permissionDecision(rules, toolName);
|
|
297
298
|
if (decision === "allow") return undefined;
|
|
298
299
|
if (decision === "deny") return { block: true, reason: `Blocked by pi-subagents permission rule: '${toolName}' is denied.` };
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
300
|
+
const rawWatchdogConfig = process.env[CHILD_WATCHDOG_CONFIG_ENV];
|
|
301
|
+
let timeoutMs = 30_000;
|
|
302
|
+
try {
|
|
303
|
+
timeoutMs = decodeChildWatchdogConfig(rawWatchdogConfig)?.agentEndTimeoutMs ?? timeoutMs;
|
|
304
|
+
} catch {
|
|
305
|
+
// The arbiter reports invalid configuration with the concrete decode error.
|
|
306
|
+
}
|
|
307
|
+
if (ctx.signal?.aborted) return { block: true, reason: "Blocked by pi-subagents permission rule: Watchdog permission decision was cancelled." };
|
|
308
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
309
|
+
let abort: (() => void) | undefined;
|
|
310
|
+
let result: WatchdogPermissionResult;
|
|
311
|
+
try {
|
|
312
|
+
result = await Promise.race([
|
|
313
|
+
requestPermission({
|
|
314
|
+
ctx,
|
|
315
|
+
toolName,
|
|
316
|
+
args: event.input ?? {},
|
|
317
|
+
rawWatchdogConfig,
|
|
318
|
+
auditPath: process.env[PERMISSION_AUDIT_PATH_ENV],
|
|
319
|
+
...(ctx.signal ? { signal: ctx.signal } : {}),
|
|
320
|
+
}),
|
|
321
|
+
new Promise<WatchdogPermissionResult>((resolve) => {
|
|
322
|
+
if (!ctx.signal) return;
|
|
323
|
+
abort = () => resolve({ approved: false, reason: "Watchdog permission decision was cancelled.", source: "watchdog" });
|
|
324
|
+
ctx.signal.addEventListener("abort", abort, { once: true });
|
|
325
|
+
}),
|
|
326
|
+
new Promise<never>((_, reject) => { timeout = setTimeout(() => reject(new Error(`Watchdog permission decision timed out after ${timeoutMs}ms.`)), timeoutMs); }),
|
|
327
|
+
]);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
330
|
+
return { block: true, reason: `Blocked by pi-subagents permission rule: Watchdog permission arbiter failed closed: ${reason}` };
|
|
331
|
+
} finally {
|
|
332
|
+
if (timeout) clearTimeout(timeout);
|
|
333
|
+
if (abort) ctx.signal?.removeEventListener("abort", abort);
|
|
334
|
+
}
|
|
307
335
|
if (result.approved) return undefined;
|
|
308
336
|
return { block: true, reason: `Blocked by pi-subagents permission rule: ${result.reason}` };
|
|
309
337
|
});
|
|
@@ -598,7 +626,8 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
|
598
626
|
registerNativeSupervisorClientOnce();
|
|
599
627
|
});
|
|
600
628
|
onRuntimeEvent("agent_start", () => {
|
|
601
|
-
refreshChildToolDiagnostic(pi);
|
|
629
|
+
const diagnostic = refreshChildToolDiagnostic(pi);
|
|
630
|
+
if (diagnostic) throw new Error(formatChildToolDiagnostic(diagnostic));
|
|
602
631
|
});
|
|
603
632
|
onRuntimeEvent("agent_end", async (_event: unknown, ctx: unknown) => {
|
|
604
633
|
if ((ctx as { hasUI?: boolean } | undefined)?.hasUI === true) return;
|
|
@@ -13,8 +13,6 @@ export interface ChildToolDiagnostic {
|
|
|
13
13
|
missingMcpDirectTools?: string[];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const PI_CORE_CHILD_TOOLS = new Set(["bash", "edit", "find", "grep", "ls", "read", "write"]);
|
|
17
|
-
|
|
18
16
|
export function writeChildToolDiagnostic(
|
|
19
17
|
filePath: string,
|
|
20
18
|
required: string[],
|
|
@@ -22,7 +20,7 @@ export function writeChildToolDiagnostic(
|
|
|
22
20
|
agent?: string,
|
|
23
21
|
mcpDirectTools?: string[],
|
|
24
22
|
): ChildToolDiagnostic | undefined {
|
|
25
|
-
const availableNames = new Set(
|
|
23
|
+
const availableNames = new Set(available);
|
|
26
24
|
const missing = required.filter((name) => !availableNames.has(name));
|
|
27
25
|
if (missing.length === 0) {
|
|
28
26
|
fs.rmSync(filePath, { force: true });
|
|
@@ -17,7 +17,7 @@ function stableJson(value: unknown): string {
|
|
|
17
17
|
return JSON.stringify(value);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function
|
|
20
|
+
export function stableJsonDigest(value: unknown): string {
|
|
21
21
|
return createHash("sha256").update(stableJson(value)).digest("hex");
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -44,6 +44,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
|
|
|
44
44
|
inheritProjectContext: agent.inheritProjectContext,
|
|
45
45
|
inheritSkills: agent.inheritSkills,
|
|
46
46
|
model: agent.model,
|
|
47
|
+
modelProvider: agent.modelProvider,
|
|
47
48
|
fallbackModels: agent.fallbackModels,
|
|
48
49
|
thinking: agent.thinking,
|
|
49
50
|
tools: agent.tools,
|
|
@@ -70,7 +71,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export function agentDefinitionDigest(agent: AgentConfig): string {
|
|
73
|
-
return
|
|
74
|
+
return stableJsonDigest(projectAgentDefinition(agent));
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
export interface LaunchBindingInput {
|
|
@@ -99,12 +100,12 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
|
|
|
99
100
|
return {
|
|
100
101
|
version: LAUNCH_BINDING_PROJECTION_VERSION,
|
|
101
102
|
definitionDigest: input.definitionDigest,
|
|
102
|
-
taskDigest: input.task === undefined ? undefined :
|
|
103
|
+
taskDigest: input.task === undefined ? undefined : stableJsonDigest(input.task),
|
|
103
104
|
// The ordered candidate set already contains each attempted model; keeping only
|
|
104
105
|
// this set makes retries correlate to the same preflight binding.
|
|
105
106
|
modelCandidates: input.modelCandidates,
|
|
106
107
|
thinking: input.thinking,
|
|
107
|
-
systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined :
|
|
108
|
+
systemPromptDigest: input.systemPrompt === undefined || input.systemPrompt === null ? undefined : stableJsonDigest(input.systemPrompt),
|
|
108
109
|
systemPromptMode: input.systemPromptMode,
|
|
109
110
|
inheritProjectContext: input.inheritProjectContext,
|
|
110
111
|
inheritSkills: input.inheritSkills,
|
|
@@ -120,5 +121,5 @@ export function projectLaunchBinding(input: LaunchBindingInput): Record<string,
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
export function launchBindingDigest(input: LaunchBindingInput): string {
|
|
123
|
-
return
|
|
124
|
+
return stableJsonDigest(projectLaunchBinding(input));
|
|
124
125
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { resolveEffectiveThinking, THINKING_LEVELS, type ThinkingLevel } from "./model-info.ts";
|
|
2
|
+
export type { ThinkingLevel } from "./model-info.ts";
|
|
3
|
+
|
|
4
|
+
export const SUBAGENT_THINKING_CEILING_ENV = "PI_SUBAGENT_THINKING_CEILING";
|
|
5
|
+
|
|
6
|
+
const thinkingLevelRanks = new Map<ThinkingLevel, number>(THINKING_LEVELS.map((level, index) => [level, index]));
|
|
7
|
+
|
|
8
|
+
export function parseThinkingLevel(value: unknown, field = "thinking level"): ThinkingLevel {
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (thinkingLevelRanks.has(trimmed as ThinkingLevel)) return trimmed as ThinkingLevel;
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Invalid ${field}; expected one of ${THINKING_LEVELS.join(", ")}.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function compareThinkingLevels(left: ThinkingLevel, right: ThinkingLevel): number {
|
|
17
|
+
const leftRank = thinkingLevelRanks.get(left);
|
|
18
|
+
const rightRank = thinkingLevelRanks.get(right);
|
|
19
|
+
if (leftRank === undefined || rightRank === undefined) throw new Error(`Invalid thinking level comparison; expected one of ${THINKING_LEVELS.join(", ")}.`);
|
|
20
|
+
return leftRank - rightRank;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function intersectThinkingCeilings(...ceilings: Array<ThinkingLevel | undefined>): ThinkingLevel | undefined {
|
|
24
|
+
const active = ceilings.filter((ceiling): ceiling is ThinkingLevel => ceiling !== undefined);
|
|
25
|
+
if (active.length === 0) return undefined;
|
|
26
|
+
return active.reduce((lowest, ceiling) => compareThinkingLevels(ceiling, lowest) < 0 ? ceiling : lowest);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function decodeThinkingCeiling(value: string | undefined): ThinkingLevel | undefined {
|
|
30
|
+
if (value === undefined || value === "") return undefined;
|
|
31
|
+
return parseThinkingLevel(value, "inherited thinking ceiling");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ThinkingCeilingCheck {
|
|
35
|
+
model?: string;
|
|
36
|
+
configThinking?: string | false;
|
|
37
|
+
ceiling?: ThinkingLevel;
|
|
38
|
+
agent?: string;
|
|
39
|
+
runId?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function assertThinkingWithinCeiling(input: ThinkingCeilingCheck): void {
|
|
43
|
+
if (!input.ceiling) return;
|
|
44
|
+
const requested = resolveEffectiveThinking(input.model, input.configThinking);
|
|
45
|
+
if (!requested) return;
|
|
46
|
+
const requestedLevel = parseThinkingLevel(requested, "requested thinking level");
|
|
47
|
+
if (compareThinkingLevels(requestedLevel, input.ceiling) <= 0) return;
|
|
48
|
+
const subject = [input.agent ? `agent '${input.agent}'` : undefined, input.runId ? `run '${input.runId}'` : undefined]
|
|
49
|
+
.filter((value): value is string => Boolean(value))
|
|
50
|
+
.join(" ");
|
|
51
|
+
throw new Error(`Thinking level '${requestedLevel}' exceeds configured maximum '${input.ceiling}'${subject ? ` for ${subject}` : ""}.`);
|
|
52
|
+
}
|
package/src/shared/types.ts
CHANGED
|
@@ -8,9 +8,10 @@ import type { Message } from "@earendil-works/pi-ai";
|
|
|
8
8
|
import type { AgentConfig } from "../agents/agents.ts";
|
|
9
9
|
import type { FSWatcher } from "node:fs";
|
|
10
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import type {
|
|
11
|
+
import type { ModelScopeRule } from "../runs/shared/model-scope.ts";
|
|
12
12
|
import type { ResolvedSubagentCapabilityCeiling, SubagentCapabilityAudit } from "../runs/shared/capability-ceiling.ts";
|
|
13
13
|
import type { AuthorityPolicyConfig } from "../policy/authority.ts";
|
|
14
|
+
import type { ThinkingLevel } from "./model-info.ts";
|
|
14
15
|
import type { GlobalMissionIndexRecord, MissionRecord, MissionStoreConfig } from "../missions/types.ts";
|
|
15
16
|
|
|
16
17
|
// ============================================================================
|
|
@@ -342,7 +343,7 @@ export interface ReviewProjection {
|
|
|
342
343
|
}
|
|
343
344
|
|
|
344
345
|
export interface FileMutationEffect {
|
|
345
|
-
status: "not-requested" | "not-applicable" | "observed" | "missing";
|
|
346
|
+
status: "not-requested" | "not-applicable" | "observed" | "missing" | "blocked";
|
|
346
347
|
expected: boolean;
|
|
347
348
|
attempted: boolean;
|
|
348
349
|
message?: string;
|
|
@@ -532,9 +533,11 @@ export interface SteeringRecoveryDescriptor {
|
|
|
532
533
|
sessionFile?: string;
|
|
533
534
|
cwd: string;
|
|
534
535
|
model?: string;
|
|
536
|
+
modelProvider?: string;
|
|
535
537
|
modelOverrideFromParent?: boolean;
|
|
536
538
|
fallbackModels?: string[];
|
|
537
539
|
thinking?: string;
|
|
540
|
+
thinkingCeiling?: ThinkingLevel;
|
|
538
541
|
tools?: string[];
|
|
539
542
|
extensions?: string[];
|
|
540
543
|
subagentOnlyExtensions?: string[];
|
|
@@ -957,6 +960,13 @@ export interface SingleResult {
|
|
|
957
960
|
modelAttempts?: ModelAttempt[];
|
|
958
961
|
controlEvents?: ControlEvent[];
|
|
959
962
|
error?: string;
|
|
963
|
+
/**
|
|
964
|
+
* True when the dispatch failed because the input exceeded the model's
|
|
965
|
+
* context window. The model fallback loop stops immediately (retrying the
|
|
966
|
+
* same input on another model cannot succeed). Callers should treat this as
|
|
967
|
+
* a signal to reduce input size or re-decompose the task.
|
|
968
|
+
*/
|
|
969
|
+
contextOverflow?: boolean;
|
|
960
970
|
protocolError?: ProtocolOutputLimit;
|
|
961
971
|
sessionFile?: string;
|
|
962
972
|
skills?: string[];
|
|
@@ -1022,6 +1032,7 @@ export interface WaitCompletionChild {
|
|
|
1022
1032
|
outputState?: SubagentOutputState;
|
|
1023
1033
|
error?: string;
|
|
1024
1034
|
model?: string;
|
|
1035
|
+
contextOverflow?: boolean;
|
|
1025
1036
|
artifactPaths?: Partial<ArtifactPaths>;
|
|
1026
1037
|
}
|
|
1027
1038
|
|
|
@@ -1364,6 +1375,12 @@ export interface ExternalJobStatus {
|
|
|
1364
1375
|
provider: string;
|
|
1365
1376
|
providerJobId?: string;
|
|
1366
1377
|
promptDigest: string;
|
|
1378
|
+
operation?: "start" | "follow-up";
|
|
1379
|
+
sourceRunId?: string;
|
|
1380
|
+
sourceStepIndex?: number;
|
|
1381
|
+
parentProviderJobId?: string;
|
|
1382
|
+
requestId?: string;
|
|
1383
|
+
requestDigest?: string;
|
|
1367
1384
|
options: Record<string, unknown>;
|
|
1368
1385
|
handleUrl?: string;
|
|
1369
1386
|
conversationUrl?: string;
|
|
@@ -1446,6 +1463,8 @@ export interface AsyncStatus {
|
|
|
1446
1463
|
/** Set when a durable schedule launched this run, so completions can name their origin. */
|
|
1447
1464
|
scheduleOrigin?: ScheduleOrigin;
|
|
1448
1465
|
steps?: Array<{
|
|
1466
|
+
/** Stable caller-facing child identity for inspect/status/stop. */
|
|
1467
|
+
childId?: string;
|
|
1449
1468
|
agent: string;
|
|
1450
1469
|
runner?: ExternalCliRunnerStatus | ExternalJobRunnerStatus;
|
|
1451
1470
|
externalProcess?: ExternalProcessStatus;
|
|
@@ -1465,6 +1484,8 @@ export interface AsyncStatus {
|
|
|
1465
1484
|
outputName?: string;
|
|
1466
1485
|
structured?: boolean;
|
|
1467
1486
|
status: "pending" | "running" | "complete" | "completed" | "failed" | "paused" | "stopped" | "rejected";
|
|
1487
|
+
stopRequested?: boolean;
|
|
1488
|
+
stopRequestedAt?: number;
|
|
1468
1489
|
children?: NestedRunSummary[];
|
|
1469
1490
|
sessionFile?: string;
|
|
1470
1491
|
transcriptPath?: string;
|
|
@@ -1494,8 +1515,11 @@ export interface AsyncStatus {
|
|
|
1494
1515
|
skills?: string[];
|
|
1495
1516
|
model?: string;
|
|
1496
1517
|
thinking?: string;
|
|
1518
|
+
thinkingCeiling?: ThinkingLevel;
|
|
1497
1519
|
attemptedModels?: string[];
|
|
1498
1520
|
modelAttempts?: ModelAttempt[];
|
|
1521
|
+
/** True when the child input exceeded the model context window. */
|
|
1522
|
+
contextOverflow?: boolean;
|
|
1499
1523
|
totalCost?: CostSummary;
|
|
1500
1524
|
steering?: SteeringStatus;
|
|
1501
1525
|
error?: string;
|
|
@@ -1754,6 +1778,8 @@ export interface SubagentState {
|
|
|
1754
1778
|
};
|
|
1755
1779
|
/** Current-session top-level async capacity projection. */
|
|
1756
1780
|
activeAsyncCapacity?: ActiveAsyncCapacitySnapshot;
|
|
1781
|
+
/** Herdr project panes opened by this Pi session, keyed by project root. */
|
|
1782
|
+
herdrProjectPanes?: Map<string, HerdrProjectPaneSnapshot>;
|
|
1757
1783
|
asyncJobs: Map<string, AsyncJobState>;
|
|
1758
1784
|
/** Current-session active and recent async runs for the native fleet inspector. */
|
|
1759
1785
|
fleetJobs?: Map<string, AsyncJobState>;
|
|
@@ -1780,6 +1806,26 @@ export interface SubagentState {
|
|
|
1780
1806
|
waitSubscriptions?: Map<string, WaitSubscriptionRecord>;
|
|
1781
1807
|
/** Live in-process workflow controllers. Durable status remains on disk after settlement. */
|
|
1782
1808
|
workflowControllers?: Map<string, AbortController>;
|
|
1809
|
+
/** Live in-process workflow child stoppers keyed by parent workflow run id. */
|
|
1810
|
+
workflowChildStops?: Map<string, (childId: string, message?: string) => boolean>;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
export interface HerdrProjectPaneSnapshot {
|
|
1814
|
+
projectRoot: string;
|
|
1815
|
+
bindingPath: string;
|
|
1816
|
+
paneId: string;
|
|
1817
|
+
openedAt: string;
|
|
1818
|
+
lastFocusedAt?: string;
|
|
1819
|
+
state: "open" | "stale";
|
|
1820
|
+
agentStatus: string;
|
|
1821
|
+
ownership: "verified" | "unknown" | "mismatch";
|
|
1822
|
+
safeToClose: boolean;
|
|
1823
|
+
refreshedAt: number;
|
|
1824
|
+
summary?: string;
|
|
1825
|
+
tabId?: string;
|
|
1826
|
+
workspaceId?: string;
|
|
1827
|
+
terminalTitle?: string;
|
|
1828
|
+
staleReason?: string;
|
|
1783
1829
|
}
|
|
1784
1830
|
|
|
1785
1831
|
// ============================================================================
|
|
@@ -1815,9 +1861,28 @@ export const SUBAGENT_FOREGROUND_COMPLETE_EVENT = "subagent:foreground-complete"
|
|
|
1815
1861
|
export const SUBAGENT_CONTROL_EVENT = "subagent:control-event";
|
|
1816
1862
|
export const SUBAGENT_CONTROL_INTERCOM_EVENT = "subagent:control-intercom";
|
|
1817
1863
|
export const SUBAGENT_STEERING_NOTICE_EVENT = "subagent:steering-notice";
|
|
1864
|
+
export const SUBAGENT_CHILD_STATUS_EVENT = "subagent:child-status";
|
|
1818
1865
|
export const SUBAGENT_RESULT_INTERCOM_EVENT = "subagent:result-intercom";
|
|
1819
1866
|
export const SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT = "subagent:result-intercom-delivery";
|
|
1820
1867
|
|
|
1868
|
+
export interface SubagentChildStatusEvent {
|
|
1869
|
+
type: "subagent.child-status";
|
|
1870
|
+
version: 1;
|
|
1871
|
+
runId: string;
|
|
1872
|
+
childId: string;
|
|
1873
|
+
status: "stopping" | "stopped";
|
|
1874
|
+
ts: number;
|
|
1875
|
+
reason?: string;
|
|
1876
|
+
source?: "rpc" | "async";
|
|
1877
|
+
asyncDir?: string;
|
|
1878
|
+
stepIndex?: number;
|
|
1879
|
+
agent?: string;
|
|
1880
|
+
childRunId?: string;
|
|
1881
|
+
workflowKey?: string;
|
|
1882
|
+
phase?: string;
|
|
1883
|
+
label?: string;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1821
1886
|
// ============================================================================
|
|
1822
1887
|
// Execution Options
|
|
1823
1888
|
// ============================================================================
|
|
@@ -1885,12 +1950,13 @@ export interface RunSyncOptions {
|
|
|
1885
1950
|
llmIntentArbiter?: import("../runs/shared/llm-intent-arbiter.ts").TaskMutationArbiter;
|
|
1886
1951
|
/** Override the agent's default thinking level for this run */
|
|
1887
1952
|
thinkingOverride?: AgentConfig["thinking"];
|
|
1953
|
+
thinkingCeiling?: ThinkingLevel;
|
|
1888
1954
|
/** Registry models available for heuristic bare-model resolution */
|
|
1889
1955
|
availableModels?: Array<{ provider: string; id: string; fullId: string }>;
|
|
1890
1956
|
/** Current parent-session provider to prefer for ambiguous bare model ids */
|
|
1891
1957
|
preferredModelProvider?: string;
|
|
1892
1958
|
/** Optional subagent model-scope enforcement for fallback candidates */
|
|
1893
|
-
modelScope?:
|
|
1959
|
+
modelScope?: ModelScopeRule | ModelScopeRule[];
|
|
1894
1960
|
/** Skills to make available (overrides agent default if provided) */
|
|
1895
1961
|
skills?: string[];
|
|
1896
1962
|
structuredOutput?: {
|
|
@@ -1972,7 +2038,7 @@ export type FleetKeybindingAction = typeof FLEET_KEYBINDING_ACTIONS[number];
|
|
|
1972
2038
|
export type FleetKeybindingsConfig = Partial<Record<FleetKeybindingAction, string[]>>;
|
|
1973
2039
|
|
|
1974
2040
|
export interface OrcaProgressTabsConfig {
|
|
1975
|
-
/** Create one Orca
|
|
2041
|
+
/** Create one Orca observer tab per top-level subagent call. Experimental and opt-in. */
|
|
1976
2042
|
enabled?: boolean;
|
|
1977
2043
|
}
|
|
1978
2044
|
|
|
@@ -2054,7 +2120,7 @@ export interface ExtensionConfig {
|
|
|
2054
2120
|
/** Artifact cleanup retention. Set cleanupDays to 0 to disable cleanup. */
|
|
2055
2121
|
artifactConfig?: Pick<ArtifactConfig, "cleanupDays">;
|
|
2056
2122
|
intercomBridge?: IntercomBridgeConfig;
|
|
2057
|
-
/** Control how slow result-index scans are logged. Defaults to \"
|
|
2123
|
+
/** Control how slow result-index scans are logged. Defaults to \"activity\".
|
|
2058
2124
|
* - \"all\": log every slow scan, including scans that find nothing.
|
|
2059
2125
|
* - \"activity\": log only slow scans that found or scheduled work. Silences
|
|
2060
2126
|
* the periodic healthy rescan that inspects zero files while no async runs
|
|
@@ -2168,6 +2234,7 @@ export const SLASH_SUBAGENT_RESPONSE_EVENT = "subagent:slash:response";
|
|
|
2168
2234
|
export const SLASH_SUBAGENT_UPDATE_EVENT = "subagent:slash:update";
|
|
2169
2235
|
export const SLASH_SUBAGENT_CANCEL_EVENT = "subagent:slash:cancel";
|
|
2170
2236
|
export const POLL_INTERVAL_MS = 250;
|
|
2237
|
+
export const WIDGET_ANIMATION_INTERVAL_MS = 1000;
|
|
2171
2238
|
export const MAX_WIDGET_JOBS = 4;
|
|
2172
2239
|
export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
|
|
2173
2240
|
export const SUBAGENT_ACTIONS = ["list", "get", "models", "children.list", "guide", "create", "update", "delete", "eject", "disable", "enable", "reset", "mission.create", "mission.list", "mission.show", "mission.update", "mission.resolve-decision", "mission.attach-run", "mission.close", "worktree.discard", "refine", "refine.show", "refine.rollback", "inspector.open", "inspector.status", "inspector.close", "project.open", "project.status", "project.close", "status", "debug.run", "grant-spawn-budget", "interrupt", "resume", "steer", "stop", "dismiss", "doctor", "watchdog.status", "watchdog.check", "watchdog.configure", "watchdog.recommend-model", "schedule.create", "schedule.list", "schedule.show", "schedule.history", "schedule.pause", "schedule.resume", "schedule.run", "schedule.run-due", "schedule.delete"] as const;
|
package/src/tui/fleet-status.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { type EditorComponent, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { snapshotExternalRuns } from "../api/external-runs.ts";
|
|
4
4
|
import { formatModelThinking } from "../shared/formatters.ts";
|
|
5
|
-
import type { AsyncJobStep, FleetViewPlacement, NestedRunSummary, NestedStepSummary, SubagentState } from "../shared/types.ts";
|
|
5
|
+
import type { AsyncJobState, AsyncJobStep, FleetViewPlacement, HerdrProjectPaneSnapshot, NestedRunSummary, NestedStepSummary, SubagentState } from "../shared/types.ts";
|
|
6
6
|
import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
|
|
7
7
|
|
|
8
8
|
export const FLEET_STATUS_WIDGET_KEY = "subagent-fleet-status";
|
|
@@ -17,6 +17,7 @@ type FleetStatusTui = {
|
|
|
17
17
|
};
|
|
18
18
|
type FleetStatusEntry = {
|
|
19
19
|
key: string;
|
|
20
|
+
surface?: "project-pane";
|
|
20
21
|
parentKey?: string;
|
|
21
22
|
workflowWrapper?: boolean;
|
|
22
23
|
agent: string;
|
|
@@ -26,6 +27,7 @@ type FleetStatusEntry = {
|
|
|
26
27
|
tokens: number;
|
|
27
28
|
state: string;
|
|
28
29
|
external?: true;
|
|
30
|
+
projectPane?: HerdrProjectPaneSnapshot;
|
|
29
31
|
nestedChildren?: NestedRunSummary[];
|
|
30
32
|
workflowRows?: FleetWorkflowRow[];
|
|
31
33
|
};
|
|
@@ -284,7 +286,32 @@ function foregroundDescription(control: { parentWorkflowRunId?: string; workflow
|
|
|
284
286
|
}
|
|
285
287
|
|
|
286
288
|
function activeLeafAgentCount(entries: FleetStatusEntry[]): number {
|
|
287
|
-
return entries.filter((entry) => !entry.workflowWrapper).length;
|
|
289
|
+
return entries.filter((entry) => !entry.workflowWrapper && !entry.surface).length;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function projectPaneNeedsAttention(pane: HerdrProjectPaneSnapshot): boolean {
|
|
293
|
+
return ["attention", "blocked", "paused", "failed", "error"].some((status) => pane.agentStatus.includes(status))
|
|
294
|
+
|| pane.summary?.includes("⚠") === true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function projectName(projectRoot: string): string {
|
|
298
|
+
return projectRoot.split(/[\\/]/).filter(Boolean).at(-1) ?? projectRoot;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function projectPaneEntries(state: SubagentState): FleetStatusEntry[] {
|
|
302
|
+
return [...(state.herdrProjectPanes?.values() ?? [])]
|
|
303
|
+
.filter((pane) => pane.state === "open")
|
|
304
|
+
.sort((left, right) => left.openedAt.localeCompare(right.openedAt) || left.projectRoot.localeCompare(right.projectRoot))
|
|
305
|
+
.map((pane) => ({
|
|
306
|
+
key: `project-pane:${pane.projectRoot}`,
|
|
307
|
+
surface: "project-pane" as const,
|
|
308
|
+
agent: `${projectName(pane.projectRoot)} · ${pane.paneId}`,
|
|
309
|
+
description: pane.summary,
|
|
310
|
+
startedAt: Date.parse(pane.openedAt) || pane.refreshedAt,
|
|
311
|
+
tokens: 0,
|
|
312
|
+
state: pane.agentStatus || "unknown",
|
|
313
|
+
projectPane: pane,
|
|
314
|
+
}));
|
|
288
315
|
}
|
|
289
316
|
|
|
290
317
|
export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntry[] {
|
|
@@ -406,6 +433,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
|
|
|
406
433
|
}
|
|
407
434
|
}
|
|
408
435
|
|
|
436
|
+
entries.push(...projectPaneEntries(state));
|
|
409
437
|
return entries.sort((left, right) => left.startedAt - right.startedAt || left.key.localeCompare(right.key));
|
|
410
438
|
}
|
|
411
439
|
|
|
@@ -483,7 +511,7 @@ export class SubagentFleetStatus {
|
|
|
483
511
|
this.clearWidget();
|
|
484
512
|
return;
|
|
485
513
|
}
|
|
486
|
-
if (this.
|
|
514
|
+
if (!this.hasInlineSurface()) {
|
|
487
515
|
this.active = false;
|
|
488
516
|
this.selectedKey = "main";
|
|
489
517
|
this.lastRenderKey = "";
|
|
@@ -578,26 +606,31 @@ export class SubagentFleetStatus {
|
|
|
578
606
|
}
|
|
579
607
|
|
|
580
608
|
render(width: number, theme: Theme): string[] {
|
|
581
|
-
if (this.
|
|
609
|
+
if (!this.hasInlineSurface()) return [];
|
|
582
610
|
if (!this.active) {
|
|
583
|
-
const
|
|
611
|
+
const workEntries = this.entries.filter((entry) => !entry.surface);
|
|
612
|
+
const projectEntries = this.entries.filter((entry) => entry.surface === "project-pane");
|
|
613
|
+
const tokens = workEntries.reduce((total, entry) => total + entry.tokens, 0);
|
|
584
614
|
const capacity = this.state.activeAsyncCapacity;
|
|
585
|
-
const hasNativeRows =
|
|
615
|
+
const hasNativeRows = workEntries.some((entry) => !entry.external);
|
|
586
616
|
const showNativeSummary = hasNativeRows || Boolean(capacity?.used);
|
|
587
617
|
const asyncRuns = capacity && showNativeSummary ? `Async runs ${capacity.used}/${capacity.limit || "∞"}` : "";
|
|
588
|
-
const activeEntries = activeLeafAgentCount(
|
|
589
|
-
const noun =
|
|
618
|
+
const activeEntries = activeLeafAgentCount(workEntries);
|
|
619
|
+
const noun = workEntries.some((entry) => entry.external) ? "job" : "agent";
|
|
590
620
|
const agents = activeEntries > 0 ? `${activeEntries} active ${noun}${activeEntries === 1 ? "" : "s"}` : "";
|
|
591
|
-
const
|
|
621
|
+
const paneAttention = projectEntries.filter((entry) => entry.projectPane && projectPaneNeedsAttention(entry.projectPane)).length;
|
|
622
|
+
const panes = projectEntries.length > 0 ? `${projectEntries.length} pane${projectEntries.length === 1 ? "" : "s"}${paneAttention ? ` (${paneAttention} ⚠)` : ""}` : "";
|
|
623
|
+
const label = [agents, asyncRuns, panes].filter(Boolean).join(" · ");
|
|
592
624
|
const detail = [showNativeSummary ? formatFleetTokens(tokens) : undefined, "↓/← to inspect"].filter(Boolean).join(" · ");
|
|
593
|
-
return [truncateToWidth(` ${theme.fg("muted", label)} · ${theme.fg("dim", detail)}`, width)];
|
|
625
|
+
return [truncateToWidth(` ${theme.fg("muted", label)}${label && detail ? " · " : ""}${theme.fg("dim", detail)}`, width)];
|
|
594
626
|
}
|
|
595
627
|
const roster = this.rosterKeys();
|
|
596
628
|
const selectedIndex = Math.max(0, roster.indexOf(this.selectedKey));
|
|
597
629
|
const lines = [truncateToWidth(` ${theme.fg("dim", "↑↓/jk select · enter inspect · esc back")}`, width), ""];
|
|
598
630
|
lines.push(truncateToWidth(` ${this.bullet(0, selectedIndex, theme)} main`, width));
|
|
599
631
|
|
|
600
|
-
const
|
|
632
|
+
const workEntries = this.entries.filter((entry) => !entry.surface);
|
|
633
|
+
const tree = fleetTreeRows(workEntries);
|
|
601
634
|
const selectedTreeIndex = Math.max(0, tree.findIndex((row) => (row.kind === "owner" || row.kind === "child") && row.entry.key === this.selectedKey));
|
|
602
635
|
const visibleCount = Math.min(this.maxAgentRows, tree.length);
|
|
603
636
|
const start = selectedTreeIndex < visibleCount ? 0 : selectedTreeIndex - visibleCount + 1;
|
|
@@ -615,15 +648,30 @@ export class SubagentFleetStatus {
|
|
|
615
648
|
}
|
|
616
649
|
}
|
|
617
650
|
if (hiddenBelow > 0) lines.push(rightAlign("", theme.fg("dim", `↓ ${hiddenBelow} more`), width));
|
|
651
|
+
this.renderProjectPaneSection(lines, selectedIndex, width, theme);
|
|
618
652
|
return lines;
|
|
619
653
|
}
|
|
620
654
|
|
|
655
|
+
private renderProjectPaneSection(lines: string[], selectedIndex: number, width: number, theme: Theme): void {
|
|
656
|
+
const entries = this.entries.filter((entry) => entry.surface === "project-pane");
|
|
657
|
+
if (!entries.length) return;
|
|
658
|
+
lines.push("", truncateToWidth(` ${theme.fg("dim", "project panes")}`, width));
|
|
659
|
+
for (const entry of entries) {
|
|
660
|
+
const rosterIndex = this.entries.findIndex((candidate) => candidate.key === entry.key) + 1;
|
|
661
|
+
lines.push(this.renderEntry(rosterIndex, selectedIndex, entry, width, theme));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
|
|
621
666
|
private renderEntry(rosterIndex: number, selectedIndex: number, entry: FleetStatusEntry, width: number, theme: Theme, branch?: string): string {
|
|
622
667
|
const agent = entry.modelThinking ? `${entry.agent} (${entry.modelThinking})` : entry.agent;
|
|
623
668
|
const prefix = branch ? ` ${branch}` : " ";
|
|
624
669
|
const left = `${prefix} ${this.bullet(rosterIndex, selectedIndex, theme)} ${theme.fg("muted", agent)} · ${entry.state}`;
|
|
625
670
|
const elapsed = Date.now() - entry.startedAt;
|
|
626
|
-
const
|
|
671
|
+
const rightText = entry.projectPane
|
|
672
|
+
? `${entry.projectPane.summary ?? "—"} · ${formatFleetElapsed(Date.now() - entry.projectPane.refreshedAt)} ago`
|
|
673
|
+
: entry.external ? formatFleetElapsed(elapsed) : `${formatFleetElapsed(elapsed)} · ${formatFleetTokens(entry.tokens)}`;
|
|
674
|
+
const right = theme.fg("dim", rightText);
|
|
627
675
|
return rightAlign(left, right, width);
|
|
628
676
|
}
|
|
629
677
|
|
|
@@ -692,6 +740,7 @@ export class SubagentFleetStatus {
|
|
|
692
740
|
entries: this.entries.map((entry) => this.active
|
|
693
741
|
? [
|
|
694
742
|
entry.key,
|
|
743
|
+
entry.surface,
|
|
695
744
|
entry.parentKey,
|
|
696
745
|
entry.agent,
|
|
697
746
|
entry.state,
|
|
@@ -719,10 +768,14 @@ export class SubagentFleetStatus {
|
|
|
719
768
|
row.overflow,
|
|
720
769
|
]),
|
|
721
770
|
]
|
|
722
|
-
: [entry.key, entry.state, entry.external, entry.tokens]),
|
|
771
|
+
: [entry.key, entry.state, entry.external, entry.surface, entry.tokens, entry.projectPane?.refreshedAt, entry.projectPane?.summary]),
|
|
723
772
|
});
|
|
724
773
|
}
|
|
725
774
|
|
|
775
|
+
private hasInlineSurface(): boolean {
|
|
776
|
+
return this.entries.length > 0 || Boolean(this.state.activeAsyncCapacity?.used);
|
|
777
|
+
}
|
|
778
|
+
|
|
726
779
|
private getActiveUiContext(): ExtensionContext | undefined {
|
|
727
780
|
const ctx = this.ctx;
|
|
728
781
|
if (!ctx) return undefined;
|
package/src/tui/render.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
type WorkflowNodeStatus,
|
|
19
19
|
type MainWindowRendererConfig,
|
|
20
20
|
MAX_WIDGET_JOBS,
|
|
21
|
-
|
|
21
|
+
WIDGET_ANIMATION_INTERVAL_MS,
|
|
22
22
|
WIDGET_KEY,
|
|
23
23
|
} from "../shared/types.ts";
|
|
24
24
|
import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
|
|
@@ -1580,7 +1580,7 @@ function buildWidgetComponent(jobs: AsyncJobState[], expanded: boolean): (_tui:
|
|
|
1580
1580
|
const container = new Container();
|
|
1581
1581
|
container.render = (renderWidth: number): string[] => {
|
|
1582
1582
|
const width = Math.max(0, renderWidth - 2);
|
|
1583
|
-
const frame = Math.floor(Date.now() /
|
|
1583
|
+
const frame = Math.floor(Date.now() / WIDGET_ANIMATION_INTERVAL_MS);
|
|
1584
1584
|
const buildLines = (): string[] => expanded
|
|
1585
1585
|
? buildWidgetLines(jobs, theme, width, true, frame)
|
|
1586
1586
|
: jobs.length === 1
|
|
@@ -1746,8 +1746,9 @@ function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): stri
|
|
|
1746
1746
|
}
|
|
1747
1747
|
|
|
1748
1748
|
function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue: boolean, isError?: boolean): "running" | "complete" | "failed" | "paused" {
|
|
1749
|
-
if (
|
|
1749
|
+
if (rows.some((row) => row.state === "failed")) return "failed";
|
|
1750
1750
|
if (rows.some((row) => row.state === "detached")) return "paused";
|
|
1751
|
+
if (isError) return "failed";
|
|
1751
1752
|
if ((rows.length > 0 && rows.every((row) => row.state === "complete")) || hasTerminalValue) return "complete";
|
|
1752
1753
|
return "running";
|
|
1753
1754
|
}
|
|
@@ -1948,7 +1949,7 @@ export function renderSubagentResult(
|
|
|
1948
1949
|
const layout = resolveMainWindowRenderLayout(rendererConfig);
|
|
1949
1950
|
const compact = (component: Component): Component => capCompactMainWindowResult(component, layout, theme, !options.expanded);
|
|
1950
1951
|
const d = result.details;
|
|
1951
|
-
if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && !result.isError
|
|
1952
|
+
if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && d.workflow?.value === undefined && (!result.isError || (d.workflow?.trace.length ?? 0) > 0)) {
|
|
1952
1953
|
return compact(renderWorkflowChatProgress(d, result, theme, options.expanded ? resolveMainWindowRenderLayout() : layout, frame));
|
|
1953
1954
|
}
|
|
1954
1955
|
if (!d || !d.results.length) {
|