pi-subagents 0.56.0 → 0.58.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 +92 -0
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +124 -21
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +41 -2
- package/docs/models.md +3 -3
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +14 -3
- package/docs/workflows.md +44 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +36 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +257 -51
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +18 -4
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +28 -3
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +33 -8
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +55 -19
- package/src/extension/schemas.ts +6 -5
- package/src/extension/tool-description.ts +18 -12
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +51 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +127 -19
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +7 -0
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +36 -15
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +239 -56
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +92 -11
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +22 -1
- package/src/runs/foreground/subagent-executor.ts +322 -102
- package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/completion-guard.ts +4 -3
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +348 -55
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +386 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
- package/src/runs/shared/model-exclusions.ts +69 -7
- package/src/runs/shared/model-fallback.ts +39 -5
- package/src/runs/shared/mutation-evidence.ts +7 -2
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +13 -1
- package/src/shared/launch-contract.ts +4 -0
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +10 -2
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +160 -10
- package/src/shared/utils.ts +6 -29
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +55 -21
- package/src/workflows/scripted-workflow.ts +299 -31
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +155 -5
|
@@ -20,14 +20,30 @@ type RecordModelFailureOptions = ModelExclusionTarget & {
|
|
|
20
20
|
|
|
21
21
|
let exclusions: ModelExclusion[] = [];
|
|
22
22
|
let loaded = false;
|
|
23
|
-
|
|
23
|
+
/** Default duration for a new model exclusion when no per-record TTL is supplied. */
|
|
24
|
+
export const DEFAULT_MODEL_EXCLUSION_TTL_MS = 24 * 60 * 60_000;
|
|
25
|
+
/** Keeps a new expiry safely below JavaScript's maximum Date timestamp. */
|
|
26
|
+
export const MAX_MODEL_EXCLUSION_TTL_MS = 8_000_000_000_000_000;
|
|
27
|
+
const MAX_DATE_TIMESTAMP_MS = 8_640_000_000_000_000;
|
|
28
|
+
let defaultTTLMs = DEFAULT_MODEL_EXCLUSION_TTL_MS;
|
|
29
|
+
let loadedTTLCeilingMs: number | undefined;
|
|
24
30
|
let persistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
25
31
|
let persistSeq = 0;
|
|
26
32
|
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Override the default TTL applied to newly recorded model exclusions.
|
|
35
|
+
*
|
|
36
|
+
* @param ms Duration in milliseconds. Must be finite and positive.
|
|
37
|
+
* @returns Nothing.
|
|
38
|
+
*/
|
|
39
|
+
// TEST:test/unit/model-exclusions.test.ts[model exclusions — TTL expiry]
|
|
40
|
+
export function setDefaultTTL(ms: number, options?: { shortenExisting?: boolean }): void {
|
|
41
|
+
if (!Number.isFinite(ms) || ms <= 0 || ms > MAX_MODEL_EXCLUSION_TTL_MS) {
|
|
42
|
+
throw new Error(`Default model exclusion TTL must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}.`);
|
|
43
|
+
}
|
|
30
44
|
defaultTTLMs = ms;
|
|
45
|
+
loadedTTLCeilingMs = options?.shortenExisting ? ms : undefined;
|
|
46
|
+
if (loaded && loadedTTLCeilingMs !== undefined && shortenExclusionsToTTL(exclusions, loadedTTLCeilingMs, Date.now())) schedulePersist();
|
|
31
47
|
}
|
|
32
48
|
|
|
33
49
|
/**
|
|
@@ -78,9 +94,23 @@ function ensureLoaded(): void {
|
|
|
78
94
|
const raw = fs.readFileSync(getExclusionsFilePath(), "utf-8");
|
|
79
95
|
const data = JSON.parse(raw);
|
|
80
96
|
if (data.version === 1) {
|
|
97
|
+
if (!Array.isArray(data.exclusions)) throw new Error("Model exclusion store version 1 must contain an exclusions array.");
|
|
98
|
+
for (let index = 0; index < data.exclusions.length; index++) {
|
|
99
|
+
const entry = data.exclusions[index];
|
|
100
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`Model exclusion store entry ${index} must be an object.`);
|
|
101
|
+
if (entry.reason !== undefined && typeof entry.reason !== "string") throw new Error(`Model exclusion store entry ${index} has an invalid reason.`);
|
|
102
|
+
for (const field of ["recordedAt", "expiresAt"] as const) {
|
|
103
|
+
const timestamp = entry[field];
|
|
104
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp) || timestamp <= 0 || timestamp > MAX_DATE_TIMESTAMP_MS) {
|
|
105
|
+
throw new Error(`Model exclusion store entry ${index} has an invalid ${field}.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
81
109
|
const now = Date.now();
|
|
82
110
|
exclusions = (data.exclusions ?? []).filter((e: ModelExclusion) => e.expiresAt > now);
|
|
111
|
+
const shortened = loadedTTLCeilingMs !== undefined && shortenExclusionsToTTL(exclusions, loadedTTLCeilingMs, now);
|
|
83
112
|
exclusions = deduplicate(exclusions);
|
|
113
|
+
if (shortened) schedulePersist();
|
|
84
114
|
}
|
|
85
115
|
} catch (error) {
|
|
86
116
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
@@ -175,6 +205,18 @@ export function isExcluded(modelId: string, provider: string): boolean {
|
|
|
175
205
|
return exclusions.some((entry) => entryMatches(entry, modelId, provider, Date.now()));
|
|
176
206
|
}
|
|
177
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Return the active exclusion matching a full model id, if any.
|
|
210
|
+
*
|
|
211
|
+
* The caller uses this for hard-fail diagnostics; fallback filtering should
|
|
212
|
+
* continue to use {@link filterFallbackCandidates}.
|
|
213
|
+
*/
|
|
214
|
+
export function findModelExclusion(fullId: string, now = Date.now()): Readonly<ModelExclusion> | undefined {
|
|
215
|
+
ensureLoaded();
|
|
216
|
+
const { provider, modelId } = parseModelKey(fullId);
|
|
217
|
+
return exclusions.find((entry) => entryMatches(entry, modelId, provider, now));
|
|
218
|
+
}
|
|
219
|
+
|
|
178
220
|
/**
|
|
179
221
|
* Number of live (non-expired) exclusions.
|
|
180
222
|
*/
|
|
@@ -205,7 +247,10 @@ export function parseModelKey(fullId: string): { provider?: string; modelId: str
|
|
|
205
247
|
* Filter a list of candidate fullIds, removing excluded models/providers and
|
|
206
248
|
* duplicates while preserving order.
|
|
207
249
|
*/
|
|
208
|
-
export function filterFallbackCandidates(candidates: string[], opts?: {
|
|
250
|
+
export function filterFallbackCandidates(candidates: string[], opts?: {
|
|
251
|
+
now?: number;
|
|
252
|
+
onExcluded?: (candidate: string, exclusion: Readonly<ModelExclusion>) => void;
|
|
253
|
+
}): string[] {
|
|
209
254
|
ensureLoaded();
|
|
210
255
|
const timestamp = opts?.now ?? Date.now();
|
|
211
256
|
const seen = new Set<string>();
|
|
@@ -213,8 +258,11 @@ export function filterFallbackCandidates(candidates: string[], opts?: { now?: nu
|
|
|
213
258
|
for (const raw of candidates) {
|
|
214
259
|
if (!raw || seen.has(raw)) continue;
|
|
215
260
|
const { provider: candidateProvider, modelId: candidateModelId } = parseModelKey(raw);
|
|
216
|
-
const
|
|
217
|
-
if (
|
|
261
|
+
const exclusion = exclusions.find((entry) => entryMatches(entry, candidateModelId, candidateProvider, timestamp));
|
|
262
|
+
if (exclusion) {
|
|
263
|
+
opts?.onExcluded?.(raw, exclusion);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
218
266
|
seen.add(raw);
|
|
219
267
|
filtered.push(raw);
|
|
220
268
|
}
|
|
@@ -241,3 +289,17 @@ function prune(items: ModelExclusion[], now: number): void {
|
|
|
241
289
|
}
|
|
242
290
|
items.length = write;
|
|
243
291
|
}
|
|
292
|
+
|
|
293
|
+
function shortenExclusionsToTTL(items: ModelExclusion[], ttlMs: number, now: number): boolean {
|
|
294
|
+
let changed = false;
|
|
295
|
+
for (const entry of items) {
|
|
296
|
+
const configuredExpiry = entry.recordedAt + ttlMs;
|
|
297
|
+
if (entry.expiresAt > configuredExpiry) {
|
|
298
|
+
entry.expiresAt = configuredExpiry;
|
|
299
|
+
changed = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const previousLength = items.length;
|
|
303
|
+
prune(items, now);
|
|
304
|
+
return changed || items.length !== previousLength;
|
|
305
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { splitKnownThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
|
|
2
2
|
import type { Usage } from "../../shared/types.ts";
|
|
3
|
-
import { filterFallbackCandidates, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
|
|
3
|
+
import { filterFallbackCandidates, findModelExclusion, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
|
|
4
4
|
import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
|
|
5
|
+
import { redactSecretValues } from "./permissions.ts";
|
|
5
6
|
|
|
6
7
|
export type { AvailableModelInfo };
|
|
7
8
|
|
|
@@ -278,6 +279,14 @@ function enforceModelScopes(
|
|
|
278
279
|
for (const violation of violations) (onWarn ?? defaultScopeWarn)(violation);
|
|
279
280
|
}
|
|
280
281
|
|
|
282
|
+
function throwForExplicitModelExclusion(model: string): void {
|
|
283
|
+
const exclusion = findModelExclusion(model);
|
|
284
|
+
if (!exclusion) return;
|
|
285
|
+
const reason = redactSecretValues((exclusion.reason ?? "runtime-failure").replace(/[\u0000-\u001f\u007f]+/g, " ")).slice(0, 240);
|
|
286
|
+
const expiry = Number.isFinite(exclusion.expiresAt) ? `; expires: ${new Date(exclusion.expiresAt).toISOString()}` : "";
|
|
287
|
+
throw new Error(`Requested subagent model '${model}' is excluded and cannot be replaced by a fallback (reason: ${reason}${expiry}).`);
|
|
288
|
+
}
|
|
289
|
+
|
|
281
290
|
/**
|
|
282
291
|
* Resolve the `--model` override passed to a spawned subagent.
|
|
283
292
|
*
|
|
@@ -312,6 +321,9 @@ export function resolveSubagentModelOverride(
|
|
|
312
321
|
} else {
|
|
313
322
|
resolved = resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
314
323
|
}
|
|
324
|
+
if (resolved && explicit !== undefined && options?.source === "explicit") {
|
|
325
|
+
throwForExplicitModelExclusion(resolved);
|
|
326
|
+
}
|
|
315
327
|
if (resolved && options?.scope) {
|
|
316
328
|
const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
|
|
317
329
|
enforceModelScopes(resolved, options.scope, source, options.onWarn);
|
|
@@ -325,14 +337,15 @@ export function resolveEffectiveSubagentModel(
|
|
|
325
337
|
parentModel: ParentModel | undefined,
|
|
326
338
|
availableModels: AvailableModelInfo[] | undefined,
|
|
327
339
|
preferredProvider?: string,
|
|
328
|
-
options?:
|
|
340
|
+
options?: ResolveSubagentModelOverrideOptions,
|
|
329
341
|
): string | undefined {
|
|
342
|
+
const source = options?.source ?? (explicitModel !== undefined ? "explicit" : "inherited");
|
|
330
343
|
const resolved = resolveSubagentModelOverride(
|
|
331
344
|
explicitModel ?? agentModel,
|
|
332
345
|
parentModel,
|
|
333
346
|
availableModels,
|
|
334
347
|
preferredProvider,
|
|
335
|
-
{ ...options, source
|
|
348
|
+
{ ...options, source },
|
|
336
349
|
);
|
|
337
350
|
if (resolved || explicitModel === undefined) return resolved;
|
|
338
351
|
return resolveSubagentModelOverride(
|
|
@@ -340,7 +353,7 @@ export function resolveEffectiveSubagentModel(
|
|
|
340
353
|
parentModel,
|
|
341
354
|
availableModels,
|
|
342
355
|
preferredProvider,
|
|
343
|
-
{ ...options, source: "inherited" },
|
|
356
|
+
{ ...options, source: options?.source ?? "inherited" },
|
|
344
357
|
);
|
|
345
358
|
}
|
|
346
359
|
|
|
@@ -394,7 +407,12 @@ export function buildModelCandidates(
|
|
|
394
407
|
seen.add(normalized);
|
|
395
408
|
candidates.push(normalized);
|
|
396
409
|
}
|
|
397
|
-
return filterFallbackCandidates(candidates
|
|
410
|
+
return filterFallbackCandidates(candidates, {
|
|
411
|
+
onExcluded(candidate, exclusion) {
|
|
412
|
+
const reason = redactSecretValues((exclusion.reason ?? "runtime-failure").replace(/[\u0000-\u001f\u007f]+/g, " ")).slice(0, 240);
|
|
413
|
+
console.warn(`[pi-subagents] Skipping model '${candidate}' due to a cached exclusion (reason: ${reason}; expires: ${new Date(exclusion.expiresAt).toISOString()}).`);
|
|
414
|
+
},
|
|
415
|
+
});
|
|
398
416
|
}
|
|
399
417
|
|
|
400
418
|
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
@@ -419,6 +437,7 @@ const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
|
419
437
|
/overloaded/i,
|
|
420
438
|
/service unavailable/i,
|
|
421
439
|
/temporar(?:ily)? unavailable/i,
|
|
440
|
+
/connection\s+(?:error|reset|closed|aborted)/i,
|
|
422
441
|
/connection refused/i,
|
|
423
442
|
/fetch failed/i,
|
|
424
443
|
/network error/i,
|
|
@@ -451,6 +470,21 @@ export function isRetryableModelFailure(error: string | undefined): boolean {
|
|
|
451
470
|
return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
|
|
452
471
|
}
|
|
453
472
|
|
|
473
|
+
function messageError(message: unknown): string | undefined {
|
|
474
|
+
if (!message || typeof message !== "object") return undefined;
|
|
475
|
+
const value = (message as { errorMessage?: unknown }).errorMessage;
|
|
476
|
+
return typeof value === "string" ? value : undefined;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function isRetryableModelFailureAttempt(input: { error: string | undefined; messages?: readonly unknown[]; toolCount?: number }): boolean {
|
|
480
|
+
if (!isRetryableModelFailure(input.error)) return false;
|
|
481
|
+
if ((input.toolCount ?? 0) > 0) return false;
|
|
482
|
+
if (input.error === "Subagent produced no output (possible model cold-start or empty response).") return true;
|
|
483
|
+
if ((input.toolCount ?? 0) === 0 && (input.messages?.length ?? 0) === 0) return true;
|
|
484
|
+
const error = input.error?.trim();
|
|
485
|
+
return Boolean(error && input.messages?.some((message) => messageError(message)?.trim() === error));
|
|
486
|
+
}
|
|
487
|
+
|
|
454
488
|
export function recordRetryableModelFailure(model: string | undefined, error: string | undefined): void {
|
|
455
489
|
if (!model || !isRetryableModelFailure(error)) return;
|
|
456
490
|
const { provider, modelId } = parseModelKey(model);
|
|
@@ -9,8 +9,13 @@ const MAX_TRACKED_PATHS = 500;
|
|
|
9
9
|
const MAX_HASH_BYTES = 1024 * 1024;
|
|
10
10
|
const MAX_TIMEOUT_FILES = 20;
|
|
11
11
|
|
|
12
|
+
function gitArguments(args: string[]): string[] {
|
|
13
|
+
// Mutation evidence must not block child startup on a stale fsmonitor daemon.
|
|
14
|
+
return ["-c", "core.fsmonitor=false", ...args];
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
function gitOutput(cwd: string, args: string[], maxBuffer = MAX_HASH_BYTES): string {
|
|
13
|
-
return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer });
|
|
18
|
+
return execFileSync("git", gitArguments(args), { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], maxBuffer });
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
function splitNul(output: string): string[] {
|
|
@@ -21,7 +26,7 @@ function hashLargeDiff(cwd: string, relativePath: string): string {
|
|
|
21
26
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-tracked-diff-"));
|
|
22
27
|
const diffPath = path.join(tempDir, "diff.patch");
|
|
23
28
|
try {
|
|
24
|
-
execFileSync("git", ["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath], { cwd, stdio: "ignore" });
|
|
29
|
+
execFileSync("git", gitArguments(["diff", "--no-ext-diff", "--binary", `--output=${diffPath}`, "HEAD", "--", relativePath]), { cwd, stdio: "ignore" });
|
|
25
30
|
const hash = createHash("sha256");
|
|
26
31
|
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
27
32
|
const fd = fs.openSync(diffPath, "r");
|
|
@@ -199,8 +199,10 @@ function sanitizeTokenUsage(value: unknown): NestedRunSummary["totalTokens"] | u
|
|
|
199
199
|
const input = clampNumber(raw.input);
|
|
200
200
|
const output = clampNumber(raw.output);
|
|
201
201
|
const total = clampNumber(raw.total);
|
|
202
|
+
const window = clampNumber(raw.window);
|
|
203
|
+
const windowPeak = clampNumber(raw.windowPeak);
|
|
202
204
|
return input !== undefined && output !== undefined && total !== undefined
|
|
203
|
-
? { input, output, total }
|
|
205
|
+
? { input, output, total, ...(window !== undefined ? { window } : {}), ...(windowPeak !== undefined ? { windowPeak } : {}) }
|
|
204
206
|
: undefined;
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatDuration, formatModelThinking,
|
|
1
|
+
import { formatDuration, formatModelThinking, formatTokenUsage, shortenPath } from "../../shared/formatters.ts";
|
|
2
2
|
import { formatActivityLabel } from "../../shared/status-format.ts";
|
|
3
3
|
import type { ActivityState, NestedRunSummary, NestedStepSummary } from "../../shared/types.ts";
|
|
4
4
|
|
|
@@ -68,7 +68,7 @@ function formatNestedActivity(input: {
|
|
|
68
68
|
if (input.currentPath) facts.push(shortenPath(input.currentPath));
|
|
69
69
|
if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
|
|
70
70
|
if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
|
|
71
|
-
if (input.totalTokens) facts.push(
|
|
71
|
+
if (input.totalTokens) facts.push(formatTokenUsage(input.totalTokens));
|
|
72
72
|
const activity = formatActivityLabel(input.lastActivityAt, input.activityState as ActivityState | undefined);
|
|
73
73
|
return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
|
|
74
74
|
}
|
|
@@ -28,22 +28,29 @@ export interface RunnerSubagentStep {
|
|
|
28
28
|
outputName?: string;
|
|
29
29
|
structured?: boolean;
|
|
30
30
|
cwd?: string;
|
|
31
|
+
/** Original cwd input retained for launch diagnostics. */
|
|
32
|
+
requestedCwd?: string;
|
|
31
33
|
model?: string;
|
|
34
|
+
contextLimit?: number;
|
|
32
35
|
fast?: boolean;
|
|
33
36
|
thinking?: string;
|
|
34
37
|
thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
|
|
35
38
|
modelCandidates?: string[];
|
|
36
39
|
/** The primary model is inherited from the parent session and should not be verified against the child-reported active registry model. */
|
|
37
40
|
skipPrimaryModelVerification?: boolean;
|
|
38
|
-
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>;
|
|
41
|
+
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
|
|
39
42
|
tools?: string[];
|
|
40
43
|
extensions?: string[];
|
|
41
44
|
subagentOnlyExtensions?: string[];
|
|
42
45
|
mcpDirectTools?: string[];
|
|
46
|
+
mcpConfig?: import("./mcp-direct-tool-allowlist.ts").McpConfig;
|
|
47
|
+
runtimeServerNames?: string[];
|
|
48
|
+
mutationTools?: string[];
|
|
43
49
|
completionGuard?: boolean;
|
|
44
50
|
systemPrompt?: string | null;
|
|
45
51
|
systemPromptMode?: "append" | "replace";
|
|
46
52
|
inheritProjectContext: boolean;
|
|
53
|
+
inheritGlobalContext: boolean;
|
|
47
54
|
inheritSkills: boolean;
|
|
48
55
|
skills?: string[];
|
|
49
56
|
outputPath?: string;
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
type NestedPathEntry,
|
|
10
10
|
} from "./nested-path.ts";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
formatUnresolvedMcpDirectToolSelectors,
|
|
13
|
+
resolveMcpDirectToolResolution,
|
|
14
|
+
type McpConfig,
|
|
15
|
+
type McpRuntimeSnapshotHost,
|
|
13
16
|
type ResolvedMcpDirectToolSelection,
|
|
14
17
|
} from "./mcp-direct-tool-allowlist.ts";
|
|
15
18
|
import { resolvePiPackageRoot } from "./pi-spawn.ts";
|
|
@@ -141,6 +144,7 @@ export const SUBAGENT_STEER_CAPABILITY_ENV = "PI_SUBAGENT_STEER_CAPABILITY";
|
|
|
141
144
|
export const SUBAGENT_STEER_ACK_DIR_ENV = "PI_SUBAGENT_STEER_ACK_DIR";
|
|
142
145
|
export const PI_INTERCOM_STABLE_ID_ENV = "PI_INTERCOM_STABLE_ID";
|
|
143
146
|
export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
|
|
147
|
+
export const SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_GLOBAL_CONTEXT";
|
|
144
148
|
|
|
145
149
|
export interface BuildPiArgsInput {
|
|
146
150
|
parentSessionId?: string;
|
|
@@ -153,6 +157,7 @@ export interface BuildPiArgsInput {
|
|
|
153
157
|
thinking?: string | false;
|
|
154
158
|
systemPromptMode?: "append" | "replace";
|
|
155
159
|
inheritProjectContext: boolean;
|
|
160
|
+
inheritGlobalContext: boolean;
|
|
156
161
|
inheritSkills: boolean;
|
|
157
162
|
requireReadTool?: boolean;
|
|
158
163
|
tools?: string[];
|
|
@@ -160,6 +165,8 @@ export interface BuildPiArgsInput {
|
|
|
160
165
|
subagentOnlyExtensions?: string[];
|
|
161
166
|
systemPrompt?: string | null;
|
|
162
167
|
mcpDirectTools?: string[];
|
|
168
|
+
mcpConfig?: McpConfig;
|
|
169
|
+
runtimeServerNames?: string[];
|
|
163
170
|
cwd?: string;
|
|
164
171
|
promptFileStem?: string;
|
|
165
172
|
intercomSessionName?: string;
|
|
@@ -202,6 +209,7 @@ export interface BuildPiArgsInput {
|
|
|
202
209
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
203
210
|
thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
|
|
204
211
|
extensionBindings?: ExtensionBindings;
|
|
212
|
+
runtimeSnapshotHost?: McpRuntimeSnapshotHost;
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
export interface BuildPiArgsResult {
|
|
@@ -278,6 +286,8 @@ export interface ResolvePiLaunchToolPlanInput {
|
|
|
278
286
|
extensions?: string[];
|
|
279
287
|
subagentOnlyExtensions?: string[];
|
|
280
288
|
mcpDirectTools?: string[];
|
|
289
|
+
mcpConfig?: McpConfig;
|
|
290
|
+
runtimeServerNames?: string[];
|
|
281
291
|
cwd?: string;
|
|
282
292
|
requireReadTool?: boolean;
|
|
283
293
|
structuredOutput?:
|
|
@@ -294,6 +304,7 @@ export interface ResolvePiLaunchToolPlanInput {
|
|
|
294
304
|
inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
295
305
|
agentName?: string;
|
|
296
306
|
permissionRules?: PermissionRules;
|
|
307
|
+
runtimeSnapshotHost?: McpRuntimeSnapshotHost;
|
|
297
308
|
}
|
|
298
309
|
|
|
299
310
|
export interface PiLaunchToolPlan {
|
|
@@ -316,6 +327,8 @@ export interface PiLaunchToolPlan {
|
|
|
316
327
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
317
328
|
/** Non-fatal launch warnings; they do not change behavior. */
|
|
318
329
|
warnings: string[];
|
|
330
|
+
mcpConfig?: McpConfig;
|
|
331
|
+
runtimeServerNames?: string[];
|
|
319
332
|
}
|
|
320
333
|
|
|
321
334
|
function extensionIdentifier(value: string): string {
|
|
@@ -337,6 +350,21 @@ function hasPermissionRules(rules: PermissionRules | undefined): boolean {
|
|
|
337
350
|
return rules !== undefined && Object.keys(rules).length > 0;
|
|
338
351
|
}
|
|
339
352
|
|
|
353
|
+
function filterRuntimeMcpConfig(
|
|
354
|
+
config: McpConfig,
|
|
355
|
+
runtimeServerNames: readonly string[],
|
|
356
|
+
effectiveRuntimeServerNames: readonly string[],
|
|
357
|
+
): McpConfig {
|
|
358
|
+
const runtimeNames = new Set(runtimeServerNames);
|
|
359
|
+
const effectiveNames = new Set(effectiveRuntimeServerNames);
|
|
360
|
+
return {
|
|
361
|
+
...config,
|
|
362
|
+
mcpServers: Object.fromEntries(
|
|
363
|
+
Object.entries(config.mcpServers).filter(([name]) => !runtimeNames.has(name) || effectiveNames.has(name)),
|
|
364
|
+
),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
340
368
|
export function projectLaunchResolvedChildExtensions(
|
|
341
369
|
toolPlan: Pick<
|
|
342
370
|
PiLaunchToolPlan,
|
|
@@ -461,15 +489,27 @@ export function resolvePiLaunchToolPlan(
|
|
|
461
489
|
!requestedBuiltinTools.includes(tool) &&
|
|
462
490
|
(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js")),
|
|
463
491
|
);
|
|
464
|
-
const
|
|
465
|
-
? []
|
|
466
|
-
:
|
|
492
|
+
const mcpResolution = capabilityCeiling?.denyExtensions
|
|
493
|
+
? { selections: [], unresolvedSelectors: [] }
|
|
494
|
+
: resolveMcpDirectToolResolution(input.mcpDirectTools, input.cwd, input.runtimeSnapshotHost, input.mcpConfig);
|
|
495
|
+
if (mcpResolution.unresolvedSelectors.length > 0) {
|
|
496
|
+
throw new Error(formatUnresolvedMcpDirectToolSelectors(mcpResolution.unresolvedSelectors));
|
|
497
|
+
}
|
|
498
|
+
const resolvedMcpSelections = mcpResolution.selections;
|
|
467
499
|
const effectiveMcpSelections = resolvedMcpSelections.filter(
|
|
468
500
|
(selection) => !allowedToolSet || allowedToolSet.has(selection.name),
|
|
469
501
|
);
|
|
470
502
|
const effectiveMcpTools = effectiveMcpSelections.map(
|
|
471
503
|
(selection) => selection.name,
|
|
472
504
|
);
|
|
505
|
+
const runtimeServerNames = mcpResolution.runtimeServerNames ?? input.runtimeServerNames ?? [];
|
|
506
|
+
const effectiveRuntimeServerNames = runtimeServerNames.filter((serverName) =>
|
|
507
|
+
effectiveMcpSelections.some((selection) => selection.selector.startsWith(`${serverName}/`)),
|
|
508
|
+
);
|
|
509
|
+
const effectiveMcpConfig = mcpResolution.mcpConfig ?? input.mcpConfig;
|
|
510
|
+
const filteredMcpConfig = effectiveMcpConfig
|
|
511
|
+
? filterRuntimeMcpConfig(effectiveMcpConfig, runtimeServerNames, effectiveRuntimeServerNames)
|
|
512
|
+
: undefined;
|
|
473
513
|
const explicitToolAllowlist =
|
|
474
514
|
input.tools !== undefined ||
|
|
475
515
|
(input.mcpDirectTools?.length ?? 0) > 0 ||
|
|
@@ -605,6 +645,9 @@ export function resolvePiLaunchToolPlan(
|
|
|
605
645
|
extensionArgs,
|
|
606
646
|
disableAmbientExtensions,
|
|
607
647
|
warnings,
|
|
648
|
+
...(effectiveRuntimeServerNames.length > 0 && filteredMcpConfig
|
|
649
|
+
? { mcpConfig: filteredMcpConfig, runtimeServerNames: effectiveRuntimeServerNames }
|
|
650
|
+
: {}),
|
|
608
651
|
...(capabilityAudit ? { capabilityAudit } : {}),
|
|
609
652
|
};
|
|
610
653
|
}
|
|
@@ -644,6 +687,8 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
644
687
|
extensions: input.extensions,
|
|
645
688
|
subagentOnlyExtensions: input.subagentOnlyExtensions,
|
|
646
689
|
mcpDirectTools: input.mcpDirectTools,
|
|
690
|
+
mcpConfig: input.mcpConfig,
|
|
691
|
+
runtimeServerNames: input.runtimeServerNames,
|
|
647
692
|
cwd: input.cwd,
|
|
648
693
|
requireReadTool: input.requireReadTool,
|
|
649
694
|
structuredOutput: input.structuredOutput,
|
|
@@ -656,6 +701,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
656
701
|
),
|
|
657
702
|
agentName: input.childAgentName,
|
|
658
703
|
permissionRules: input.permissionRules,
|
|
704
|
+
runtimeSnapshotHost: input.runtimeSnapshotHost,
|
|
659
705
|
});
|
|
660
706
|
if (toolPlan.explicitToolAllowlist) {
|
|
661
707
|
args.push(
|
|
@@ -669,6 +715,14 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
669
715
|
}
|
|
670
716
|
for (const extPath of toolPlan.extensionArgs)
|
|
671
717
|
args.push("--extension", extPath);
|
|
718
|
+
let tempDir: string | undefined;
|
|
719
|
+
if (toolPlan.mcpConfig && toolPlan.runtimeServerNames?.length) {
|
|
720
|
+
if (!tempDir)
|
|
721
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
722
|
+
const mcpConfigPath = path.join(tempDir, "mcp-config.json");
|
|
723
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(toolPlan.mcpConfig, null, 2), { mode: 0o600 });
|
|
724
|
+
args.push("--mcp-config", mcpConfigPath);
|
|
725
|
+
}
|
|
672
726
|
|
|
673
727
|
if (!input.inheritProjectContext) {
|
|
674
728
|
args.push("--no-context-files");
|
|
@@ -677,9 +731,9 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
677
731
|
args.push("--no-skills");
|
|
678
732
|
}
|
|
679
733
|
|
|
680
|
-
let tempDir: string | undefined;
|
|
681
734
|
if (input.systemPrompt !== undefined && input.systemPrompt !== null) {
|
|
682
|
-
|
|
735
|
+
if (!tempDir)
|
|
736
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
683
737
|
const stem = (input.promptFileStem ?? "prompt").replace(/[^\w.-]/g, "_");
|
|
684
738
|
const promptPath = path.join(tempDir, `${stem}.md`);
|
|
685
739
|
// Inject <active_agent> tag so @gotgenes/pi-permission-system can
|
|
@@ -817,6 +871,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
817
871
|
env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext
|
|
818
872
|
? "1"
|
|
819
873
|
: "0";
|
|
874
|
+
env[SUBAGENT_INHERIT_GLOBAL_CONTEXT_ENV] = input.inheritGlobalContext ? "1" : "0";
|
|
820
875
|
env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
|
|
821
876
|
env[PI_INTERCOM_STABLE_ID_ENV] = input.intercomSessionName || undefined;
|
|
822
877
|
env[PI_INTERCOM_SESSION_ID_ENV] = undefined;
|
|
@@ -2,6 +2,19 @@ export function formatProcessSignalError(signal: string): string {
|
|
|
2
2
|
return `Subagent process terminated by signal ${signal}.`;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
export function formatMidToolExitError(input: {
|
|
6
|
+
toolName: string;
|
|
7
|
+
exitCode: number | null;
|
|
8
|
+
processSignal?: string | null;
|
|
9
|
+
}): string {
|
|
10
|
+
const terminal = [`exit ${input.exitCode ?? "unknown"}`, ...(input.processSignal ? [`signal ${input.processSignal}`] : [])].join(", ");
|
|
11
|
+
return `Subagent process exited during '${input.toolName}' tool execution (${terminal}) before the tool completed. Earlier assistant output is not a terminal result.`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isOrdinaryToolForMidToolExit(toolName: string): boolean {
|
|
15
|
+
return toolName !== "intercom" && toolName !== "contact_supervisor";
|
|
16
|
+
}
|
|
17
|
+
|
|
5
18
|
export function isUnexplainedProcessSignal(input: {
|
|
6
19
|
processSignal?: string | null;
|
|
7
20
|
interrupted?: boolean;
|
|
@@ -2,6 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { getAgentDir } from "../../shared/utils.ts";
|
|
5
|
+
import { isUnexplainedProcessSignal } from "./process-signal.ts";
|
|
6
|
+
|
|
7
|
+
export type RunOutcome = "completed" | "failed" | "timed_out" | "stopped" | "interrupted";
|
|
5
8
|
|
|
6
9
|
export interface RunEntry {
|
|
7
10
|
agent: string;
|
|
@@ -9,6 +12,7 @@ export interface RunEntry {
|
|
|
9
12
|
taskHash?: string;
|
|
10
13
|
ts: number;
|
|
11
14
|
status: "ok" | "error";
|
|
15
|
+
outcome?: RunOutcome;
|
|
12
16
|
duration: number;
|
|
13
17
|
exit?: number;
|
|
14
18
|
}
|
|
@@ -129,14 +133,30 @@ function appendPrivateHistoryLine(historyPath: string, line: string): void {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
export function recordRun(
|
|
136
|
+
export function recordRun(
|
|
137
|
+
agent: string,
|
|
138
|
+
task: string,
|
|
139
|
+
exitCode: number,
|
|
140
|
+
durationMs: number,
|
|
141
|
+
terminal: { interrupted?: boolean; processSignal?: string | null; stopped?: boolean; timedOut?: boolean; turnBudgetExceeded?: boolean } = {},
|
|
142
|
+
): void {
|
|
133
143
|
try {
|
|
144
|
+
const outcome: RunOutcome = terminal.stopped
|
|
145
|
+
? "stopped"
|
|
146
|
+
: terminal.interrupted
|
|
147
|
+
? "interrupted"
|
|
148
|
+
: terminal.timedOut
|
|
149
|
+
? "timed_out"
|
|
150
|
+
: exitCode !== 0 && isUnexplainedProcessSignal(terminal)
|
|
151
|
+
? "stopped"
|
|
152
|
+
: exitCode === 0 ? "completed" : "failed";
|
|
134
153
|
const entry: RunEntry = {
|
|
135
154
|
agent,
|
|
136
155
|
task: REDACTED_TASK,
|
|
137
156
|
taskHash: hashTask(task),
|
|
138
157
|
ts: Math.floor(Date.now() / 1000),
|
|
139
158
|
status: exitCode === 0 ? "ok" : "error",
|
|
159
|
+
outcome,
|
|
140
160
|
duration: durationMs,
|
|
141
161
|
...(exitCode !== 0 ? { exit: exitCode } : {}),
|
|
142
162
|
};
|
|
@@ -96,6 +96,23 @@ function formatOutputPathInstruction(outputPath: string, capabilities?: OutputIn
|
|
|
96
96
|
].join("\n");
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
export function requestedOutputPathFromTask(task: string): string | undefined {
|
|
100
|
+
for (const line of task.split(/\r?\n/).reverse()) {
|
|
101
|
+
const match = line.match(/^\s*(?:Write your findings to(?: exactly this path)?:|The runtime will persist it to exactly this path:)\s*(.+?)\s*$/i);
|
|
102
|
+
if (!match?.[1]) continue;
|
|
103
|
+
const requested = match[1].trim();
|
|
104
|
+
return requested.startsWith("`") && requested.endsWith("`") ? requested.slice(1, -1) : requested;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function outputPathMappingFromTask(task: string, savedPath: string | undefined): { requestedPath: string; savedPath: string } | undefined {
|
|
110
|
+
const requestedPath = requestedOutputPathFromTask(task);
|
|
111
|
+
if (!requestedPath || !savedPath) return undefined;
|
|
112
|
+
if (path.isAbsolute(requestedPath) && path.normalize(requestedPath) === path.normalize(savedPath)) return undefined;
|
|
113
|
+
return { requestedPath, savedPath };
|
|
114
|
+
}
|
|
115
|
+
|
|
99
116
|
export function injectSingleOutputInstruction(task: string, outputPath: string | undefined, capabilities?: OutputInstructionCapabilities): string {
|
|
100
117
|
if (!outputPath) return task;
|
|
101
118
|
return `${task}\n\n---\n**Output:**\n${formatOutputPathInstruction(outputPath, capabilities)}`;
|