pi-subagents 0.34.0 → 0.35.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 +78 -9
- package/README.md +213 -32
- package/index.ts +1 -0
- package/install.mjs +1 -1
- package/package.json +23 -8
- package/prompts/review-loop.md +3 -1
- package/skills/pi-subagents/SKILL.md +87 -25
- package/src/agents/agent-management.ts +82 -15
- package/src/agents/agent-serializer.ts +19 -0
- package/src/agents/agents.ts +91 -49
- package/src/agents/frontmatter.ts +67 -13
- package/src/agents/skills.ts +25 -12
- package/src/api/background-work.ts +197 -0
- package/src/api/delegation.ts +158 -0
- package/src/extension/chain-validation.ts +165 -0
- package/src/extension/doctor.ts +15 -0
- package/src/extension/fanout-child.ts +3 -1
- package/src/extension/index.ts +65 -124
- package/src/extension/rpc.ts +10 -2
- package/src/extension/schemas.ts +18 -14
- package/src/extension/steering-notices.ts +35 -0
- package/src/extension/tool-description.ts +20 -9
- package/src/intercom/intercom-bridge.ts +3 -2
- package/src/intercom/native-supervisor-channel.ts +9 -1
- package/src/intercom/result-intercom.ts +4 -0
- package/src/runs/background/async-execution.ts +293 -44
- package/src/runs/background/async-job-tracker.ts +56 -9
- package/src/runs/background/async-resume.ts +159 -52
- package/src/runs/background/async-status.ts +25 -18
- package/src/runs/background/auto-drain.ts +67 -0
- package/src/runs/background/chain-root-attachment.ts +16 -8
- package/src/runs/background/control-channel.ts +260 -13
- package/src/runs/background/fleet-view.ts +23 -2
- package/src/runs/background/notify.ts +79 -10
- package/src/runs/background/result-watcher.ts +12 -9
- package/src/runs/background/run-id-resolver.ts +14 -2
- package/src/runs/background/run-status.ts +23 -15
- package/src/runs/background/scheduled-runs.ts +3 -0
- package/src/runs/background/stale-run-reconciler.ts +32 -10
- package/src/runs/background/steering.ts +237 -0
- package/src/runs/background/subagent-runner.ts +898 -236
- package/src/runs/background/subagent-wait.ts +484 -0
- package/src/runs/background/top-level-async.ts +2 -1
- package/src/runs/background/wait-config.ts +36 -0
- package/src/runs/background/wait-tool.ts +26 -0
- package/src/runs/foreground/async-steering-action.ts +230 -0
- package/src/runs/foreground/chain-clarify.ts +22 -6
- package/src/runs/foreground/chain-execution.ts +50 -32
- package/src/runs/foreground/execution.ts +308 -94
- package/src/runs/foreground/subagent-executor.ts +592 -268
- package/src/runs/shared/acceptance.ts +355 -97
- package/src/runs/shared/child-protocol.ts +121 -0
- package/src/runs/shared/completion-guard.ts +8 -127
- package/src/runs/shared/dynamic-fanout.ts +6 -4
- package/src/runs/shared/model-fallback.ts +36 -0
- package/src/runs/shared/nested-events.ts +9 -4
- package/src/runs/shared/nested-render.ts +4 -1
- package/src/runs/shared/parallel-utils.ts +7 -0
- package/src/runs/shared/pi-args.ts +34 -7
- package/src/runs/shared/pi-spawn.ts +18 -12
- package/src/runs/shared/session-lease.ts +279 -0
- package/src/runs/shared/single-output.ts +61 -6
- package/src/runs/shared/spawn-budget.ts +128 -0
- package/src/runs/shared/subagent-control.ts +10 -6
- package/src/runs/shared/subagent-prompt-runtime.ts +127 -26
- package/src/runs/shared/task-intent.ts +176 -0
- package/src/runs/shared/tool-availability.ts +65 -0
- package/src/runs/shared/turn-budget.ts +49 -4
- package/src/shared/atomic-json.ts +4 -1
- package/src/shared/fork-context.ts +28 -3
- package/src/shared/model-info.ts +7 -4
- package/src/shared/status-format.ts +7 -1
- package/src/shared/types.ts +203 -25
- package/src/shared/utils.ts +35 -7
- package/src/slash/delegation-adapters.ts +457 -0
- package/src/slash/delegation-request.ts +103 -0
- package/src/slash/prompt-template-bridge.ts +167 -344
- package/src/slash/slash-commands.ts +239 -6
- package/src/slash/subagents-admin.ts +428 -0
- package/src/slash/subagents-editor.ts +86 -0
- package/src/tui/fleet.ts +405 -0
- package/src/tui/render.ts +90 -16
- package/src/watchdog/change-signature.ts +127 -0
- package/src/watchdog/child-status.ts +205 -0
- package/src/watchdog/emission-guard.ts +123 -0
- package/src/watchdog/lsp-diagnostics.ts +532 -0
- package/src/watchdog/model-selection.ts +167 -0
- package/src/watchdog/register-child.ts +117 -0
- package/src/watchdog/register-main.ts +433 -0
- package/src/watchdog/render.ts +54 -0
- package/src/watchdog/review.ts +293 -0
- package/src/watchdog/runtime.ts +712 -0
- package/src/watchdog/settings.ts +528 -0
- package/src/watchdog/tool-actions.ts +155 -0
- package/src/watchdog/turn-delta.ts +161 -0
- package/src/watchdog/types.ts +188 -0
- package/src/watchdog/warning-format.ts +73 -0
- package/src/runs/background/wait.ts +0 -394
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { normalizeModelSegment, resolveModelCandidate } from "../runs/shared/model-fallback.ts";
|
|
3
|
+
import {
|
|
4
|
+
getSupportedThinkingLevels,
|
|
5
|
+
splitKnownThinkingSuffix,
|
|
6
|
+
THINKING_LEVELS,
|
|
7
|
+
toModelInfo,
|
|
8
|
+
type ModelInfo,
|
|
9
|
+
type ThinkingLevel,
|
|
10
|
+
} from "../shared/model-info.ts";
|
|
11
|
+
|
|
12
|
+
export const STRONG_WATCHDOG_THINKING: ThinkingLevel = "high";
|
|
13
|
+
|
|
14
|
+
const STRONG_WATCHDOG_MODELS = {
|
|
15
|
+
opus48: {
|
|
16
|
+
label: "Opus 4.8",
|
|
17
|
+
queries: [
|
|
18
|
+
"anthropic/claude-opus-4-8",
|
|
19
|
+
"anthropic/claude-opus-4.8",
|
|
20
|
+
"anthropic/opus-4-8",
|
|
21
|
+
"anthropic/opus-4.8",
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
gpt55: {
|
|
25
|
+
label: "GPT 5.5",
|
|
26
|
+
queries: [
|
|
27
|
+
"openai-codex/gpt-5.5",
|
|
28
|
+
"openai-codex/gpt-5-5",
|
|
29
|
+
"openai/gpt-5.5",
|
|
30
|
+
"openai/gpt-5-5",
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
type StrongWatchdogFamily = keyof typeof STRONG_WATCHDOG_MODELS;
|
|
36
|
+
|
|
37
|
+
type RegistryModel = ReturnType<ExtensionContext["modelRegistry"]["find"]>;
|
|
38
|
+
|
|
39
|
+
export interface ResolvedWatchdogModelInput {
|
|
40
|
+
model: string;
|
|
41
|
+
thinking?: ThinkingLevel;
|
|
42
|
+
registryModel: NonNullable<RegistryModel>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface WatchdogModelRecommendation {
|
|
46
|
+
model: string;
|
|
47
|
+
thinking: ThinkingLevel;
|
|
48
|
+
label: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
registryModel: NonNullable<RegistryModel>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fullModelId(model: Pick<ModelInfo, "provider" | "id">): string {
|
|
54
|
+
return `${model.provider}/${model.id}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function modelRegistryEntries(ctx: ExtensionContext): ModelInfo[] {
|
|
58
|
+
return ctx.modelRegistry.getAvailable().map(toModelInfo);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function splitProviderModel(value: string): { provider: string; id: string } | undefined {
|
|
62
|
+
const slashIndex = value.indexOf("/");
|
|
63
|
+
if (slashIndex <= 0 || slashIndex === value.length - 1) return undefined;
|
|
64
|
+
return { provider: value.slice(0, slashIndex), id: value.slice(slashIndex + 1) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertSupportedThinking(value: string, source: string): ThinkingLevel {
|
|
68
|
+
if ((THINKING_LEVELS as readonly string[]).includes(value)) return value as ThinkingLevel;
|
|
69
|
+
throw new Error(`Unsupported watchdog thinking '${value}' from ${source}; expected ${THINKING_LEVELS.join(", ")}, false, or inherit.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function parseWatchdogThinkingInput(value: string | false | undefined, source = "watchdog input"): ThinkingLevel | false | undefined {
|
|
73
|
+
if (value === undefined || value === "") return undefined;
|
|
74
|
+
if (value === false) return false;
|
|
75
|
+
if (value === "false") return false;
|
|
76
|
+
return assertSupportedThinking(value, source);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resolveWatchdogModelInput(ctx: ExtensionContext, rawModel: string): ResolvedWatchdogModelInput {
|
|
80
|
+
const trimmed = rawModel.trim();
|
|
81
|
+
if (!trimmed) throw new Error("Watchdog model must be a non-empty provider/model value.");
|
|
82
|
+
const availableModels = modelRegistryEntries(ctx);
|
|
83
|
+
const preferredProvider = typeof ctx.model?.provider === "string" ? ctx.model.provider : undefined;
|
|
84
|
+
const resolved = resolveModelCandidate(trimmed, availableModels, preferredProvider) ?? trimmed;
|
|
85
|
+
const { baseModel, thinkingSuffix } = splitKnownThinkingSuffix(resolved);
|
|
86
|
+
const named = splitProviderModel(baseModel);
|
|
87
|
+
if (!named) throw new Error(`Watchdog model '${rawModel}' did not resolve to provider/model. Use a provider-qualified model such as openai-codex/gpt-5.5:high or anthropic/claude-opus-4-8:high.`);
|
|
88
|
+
const registryModel = ctx.modelRegistry.find(named.provider, named.id);
|
|
89
|
+
if (!registryModel) throw new Error(`Watchdog model '${rawModel}' was not found as '${baseModel}'.`);
|
|
90
|
+
if (!ctx.modelRegistry.hasConfiguredAuth(registryModel)) {
|
|
91
|
+
throw new Error(`Watchdog model '${baseModel}' is not authenticated. Configure credentials for provider '${named.provider}' or choose an authenticated model.`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
model: `${named.provider}/${named.id}`,
|
|
95
|
+
...(thinkingSuffix ? { thinking: assertSupportedThinking(thinkingSuffix.slice(1), "watchdog model suffix") } : {}),
|
|
96
|
+
registryModel,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function familyForModel(model: Pick<ModelInfo, "provider" | "id"> | undefined): StrongWatchdogFamily | undefined {
|
|
101
|
+
if (!model) return undefined;
|
|
102
|
+
const provider = normalizeModelSegment(model.provider);
|
|
103
|
+
const id = normalizeModelSegment(model.id);
|
|
104
|
+
if (provider.includes("openai") && /^gpt-5-5(-\d{8}|-\d{4}-\d{2}-\d{2})?$/.test(id)) return "gpt55";
|
|
105
|
+
if (provider.includes("anthropic") && /^(claude-opus-4-8|opus-4-8)(-\d{8}|-\d{4}-\d{2}-\d{2})?$/.test(id)) return "opus48";
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function currentProviderFamily(ctx: ExtensionContext): "openai" | "anthropic" | undefined {
|
|
110
|
+
const provider = typeof ctx.model?.provider === "string" ? normalizeModelSegment(ctx.model.provider) : "";
|
|
111
|
+
if (provider.includes("openai")) return "openai";
|
|
112
|
+
if (provider.includes("anthropic")) return "anthropic";
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function strongFamilyOrder(ctx: ExtensionContext): StrongWatchdogFamily[] {
|
|
117
|
+
const current = ctx.model ? familyForModel(toModelInfo(ctx.model)) : undefined;
|
|
118
|
+
if (current === "gpt55") return ["opus48"];
|
|
119
|
+
if (current === "opus48") return ["gpt55"];
|
|
120
|
+
const providerFamily = currentProviderFamily(ctx);
|
|
121
|
+
if (providerFamily === "openai") return ["opus48", "gpt55"];
|
|
122
|
+
if (providerFamily === "anthropic") return ["gpt55", "opus48"];
|
|
123
|
+
return ["gpt55", "opus48"];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findFamilyMatch(family: StrongWatchdogFamily, availableModels: ModelInfo[]): string | undefined {
|
|
127
|
+
const matches = availableModels.filter((entry) => familyForModel(entry) === family);
|
|
128
|
+
if (matches.length === 1) return matches[0]!.fullId;
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveStrongCandidate(ctx: ExtensionContext, family: StrongWatchdogFamily): WatchdogModelRecommendation | undefined {
|
|
133
|
+
const availableModels = modelRegistryEntries(ctx);
|
|
134
|
+
const preference = STRONG_WATCHDOG_MODELS[family];
|
|
135
|
+
const queries = [...preference.queries];
|
|
136
|
+
const familyMatch = findFamilyMatch(family, availableModels);
|
|
137
|
+
if (familyMatch) queries.push(familyMatch);
|
|
138
|
+
for (const query of queries) {
|
|
139
|
+
let resolved: ResolvedWatchdogModelInput;
|
|
140
|
+
try {
|
|
141
|
+
resolved = resolveWatchdogModelInput(ctx, query);
|
|
142
|
+
} catch {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const modelInfo = toModelInfo(resolved.registryModel);
|
|
146
|
+
if (familyForModel(modelInfo) !== family) continue;
|
|
147
|
+
if (!getSupportedThinkingLevels(modelInfo).includes(STRONG_WATCHDOG_THINKING)) continue;
|
|
148
|
+
const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "no current session model";
|
|
149
|
+
return {
|
|
150
|
+
model: resolved.model,
|
|
151
|
+
thinking: STRONG_WATCHDOG_THINKING,
|
|
152
|
+
label: preference.label,
|
|
153
|
+
reason: `Use ${preference.label} with thinking high as a strong independent watchdog for ${current}.`,
|
|
154
|
+
registryModel: resolved.registryModel,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function recommendStrongWatchdogModel(ctx: ExtensionContext): WatchdogModelRecommendation {
|
|
161
|
+
for (const family of strongFamilyOrder(ctx)) {
|
|
162
|
+
const recommendation = resolveStrongCandidate(ctx, family);
|
|
163
|
+
if (recommendation) return recommendation;
|
|
164
|
+
}
|
|
165
|
+
const current = ctx.model ? fullModelId(toModelInfo(ctx.model)) : "the current session";
|
|
166
|
+
throw new Error(`No authenticated strong complementary watchdog model was found for ${current}. Configure access to Opus 4.8 or GPT 5.5, then run the recommendation again.`);
|
|
167
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { MainWatchdogRuntime } from "./runtime.ts";
|
|
3
|
+
import { createMainWatchdogReview } from "./review.ts";
|
|
4
|
+
import { DEFAULT_WATCHDOG_CONFIG } from "./settings.ts";
|
|
5
|
+
import { createWatchdogWarningMessage } from "./warning-format.ts";
|
|
6
|
+
import {
|
|
7
|
+
CHILD_WATCHDOG_CONFIG_ENV,
|
|
8
|
+
CHILD_WATCHDOG_STATUS_EVENT,
|
|
9
|
+
decodeChildWatchdogConfig,
|
|
10
|
+
type ChildWatchdogConfig,
|
|
11
|
+
type ChildWatchdogPhase,
|
|
12
|
+
} from "./child-status.ts";
|
|
13
|
+
import type { ResolvedWatchdogConfig, WatchdogWarningDetails } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
function childResolvedConfig(config: ChildWatchdogConfig): ResolvedWatchdogConfig {
|
|
16
|
+
return {
|
|
17
|
+
...DEFAULT_WATCHDOG_CONFIG,
|
|
18
|
+
enabled: true,
|
|
19
|
+
agentEndTimeoutMs: config.agentEndTimeoutMs,
|
|
20
|
+
maxWarnings: config.maxWarnings,
|
|
21
|
+
main: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
...(config.model ? { model: config.model } : {}),
|
|
24
|
+
...(config.thinking !== undefined ? { thinking: config.thinking } : {}),
|
|
25
|
+
},
|
|
26
|
+
autoFollow: {
|
|
27
|
+
blockers: config.autoFollowBlockers,
|
|
28
|
+
maxAttempts: config.autoFollowMaxAttempts,
|
|
29
|
+
stalemateRepeats: config.stalemateRepeats,
|
|
30
|
+
},
|
|
31
|
+
children: {
|
|
32
|
+
...DEFAULT_WATCHDOG_CONFIG.children,
|
|
33
|
+
watchdogTailTimeoutMs: config.watchdogTailTimeoutMs,
|
|
34
|
+
},
|
|
35
|
+
lsp: { ...config.lsp },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function childWarningDetails(details: WatchdogWarningDetails, config: ChildWatchdogConfig): WatchdogWarningDetails {
|
|
40
|
+
return {
|
|
41
|
+
...details,
|
|
42
|
+
source: details.source === "lsp" ? "lsp" : "child",
|
|
43
|
+
...(config.agent ? { agent: config.agent } : {}),
|
|
44
|
+
...(config.runId ? { runId: config.runId } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeStatus(event: unknown): void {
|
|
49
|
+
try {
|
|
50
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
51
|
+
} catch {
|
|
52
|
+
// Child watchdog status is advisory; stdout failures are handled by the child process itself.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registerChildWatchdog(pi: ExtensionAPI, rawConfig = process.env[CHILD_WATCHDOG_CONFIG_ENV]): MainWatchdogRuntime | undefined {
|
|
57
|
+
const childConfig = decodeChildWatchdogConfig(rawConfig);
|
|
58
|
+
if (!childConfig?.enabled) return undefined;
|
|
59
|
+
let currentContext: ExtensionContext | undefined;
|
|
60
|
+
let seq = 0;
|
|
61
|
+
const emitStatus = (phase: ChildWatchdogPhase, followUpPending = false, reason?: string): void => {
|
|
62
|
+
writeStatus({
|
|
63
|
+
type: CHILD_WATCHDOG_STATUS_EVENT,
|
|
64
|
+
...(childConfig.runId ? { runId: childConfig.runId } : {}),
|
|
65
|
+
...(childConfig.agent ? { agent: childConfig.agent } : {}),
|
|
66
|
+
...(childConfig.childIndex !== undefined ? { childIndex: childConfig.childIndex, stepIndex: childConfig.childIndex } : {}),
|
|
67
|
+
seq: ++seq,
|
|
68
|
+
phase,
|
|
69
|
+
ts: Date.now(),
|
|
70
|
+
followUpPending,
|
|
71
|
+
...(reason ? { reason } : {}),
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
const resolved = childResolvedConfig(childConfig);
|
|
75
|
+
const runtime = new MainWatchdogRuntime({
|
|
76
|
+
resolveConfig: () => ({ ok: true, config: resolved, errors: [], sources: [{ scope: "session", exists: true }] }),
|
|
77
|
+
review: createMainWatchdogReview(() => currentContext, { getThinkingLevel: () => pi.getThinkingLevel() }),
|
|
78
|
+
reviewDescription: "child model review",
|
|
79
|
+
reviewChangesOnly: true,
|
|
80
|
+
displayWarning: (details) => {
|
|
81
|
+
const childDetails = childWarningDetails(details, childConfig);
|
|
82
|
+
pi.sendMessage(createWatchdogWarningMessage(childDetails, { display: true, details: childDetails }));
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
const rememberContext = (ctx: ExtensionContext) => {
|
|
86
|
+
currentContext = ctx;
|
|
87
|
+
};
|
|
88
|
+
const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown, ctx: ExtensionContext) => unknown) => void;
|
|
89
|
+
onRuntimeEvent("session_start", (_event, ctx) => {
|
|
90
|
+
rememberContext(ctx);
|
|
91
|
+
runtime.bindSession(ctx);
|
|
92
|
+
emitStatus("idle");
|
|
93
|
+
});
|
|
94
|
+
onRuntimeEvent("before_agent_start", (event, ctx) => {
|
|
95
|
+
rememberContext(ctx);
|
|
96
|
+
runtime.handleBeforeAgentStart(event, ctx);
|
|
97
|
+
});
|
|
98
|
+
onRuntimeEvent("turn_end", (event, ctx) => {
|
|
99
|
+
rememberContext(ctx);
|
|
100
|
+
runtime.handleTurnEnd(event, ctx);
|
|
101
|
+
});
|
|
102
|
+
onRuntimeEvent("agent_end", async (event, ctx) => {
|
|
103
|
+
rememberContext(ctx);
|
|
104
|
+
emitStatus("reviewing");
|
|
105
|
+
await runtime.handleAgentEnd(event, ctx);
|
|
106
|
+
const snapshot = runtime.getSnapshot(ctx.cwd);
|
|
107
|
+
if (snapshot.status === "failed") emitStatus("failed", false, snapshot.lastError);
|
|
108
|
+
else if (snapshot.status === "stale") emitStatus("stale", false, "review stale");
|
|
109
|
+
else emitStatus("idle");
|
|
110
|
+
});
|
|
111
|
+
onRuntimeEvent("session_shutdown", () => {
|
|
112
|
+
currentContext = undefined;
|
|
113
|
+
runtime.dispose();
|
|
114
|
+
emitStatus("idle");
|
|
115
|
+
});
|
|
116
|
+
return runtime;
|
|
117
|
+
}
|