pi-subagents 0.67.0 → 0.69.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 +90 -0
- package/README.md +1 -1
- package/docs/agents.md +41 -12
- package/docs/configuration.md +61 -19
- package/docs/extension-api.md +5 -1
- package/docs/missions.md +2 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +18 -8
- package/docs/standalone-background.md +13 -3
- package/docs/tool-reference.md +38 -14
- package/docs/watchdog.md +10 -12
- package/docs/workflows.md +59 -1
- package/index.ts +5 -2
- package/package.json +4 -2
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +25 -11
- package/skills/pi-subagents/SKILL.md +18 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +6 -4
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +16 -12
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +185 -72
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/agents/skills.ts +1 -1
- package/src/api/preflight.ts +20 -16
- package/src/api/required-child-extensions.ts +6 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +3 -0
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +42 -31
- package/src/extension/public-execution.ts +3 -3
- package/src/extension/schemas.ts +23 -6
- package/src/extension/tool-description.ts +8 -7
- package/src/inspectors/ghostty/plugin.ts +13 -1
- package/src/intercom/native-supervisor-channel.ts +22 -18
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +309 -126
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +13 -4
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +20 -10
- package/src/runs/background/binary-bootstrap.ts +5 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/notify.ts +74 -6
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +20 -29
- package/src/runs/background/runner-aliases.ts +4 -33
- package/src/runs/background/runner-child-launch.ts +4 -1
- package/src/runs/background/runner-child-sessions.ts +2 -2
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +11 -5
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/subagent-runner.ts +413 -276
- package/src/runs/background/subagent-wait.ts +128 -23
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +4 -2
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +115 -219
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/subagent-executor.ts +281 -80
- package/src/runs/shared/acceptance.ts +194 -37
- package/src/runs/shared/async-status-projection.ts +123 -33
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +19 -6
- package/src/runs/shared/child-runtime-config.ts +5 -0
- package/src/runs/shared/child-session.ts +94 -50
- package/src/runs/shared/child-tool-plan.ts +28 -16
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +30 -8
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/launch-contract.ts +6 -9
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +81 -29
- package/src/slash/slash-commands.ts +0 -6
- package/src/slash/subagents-admin.ts +13 -9
- package/src/tui/render.ts +20 -10
- package/src/watchdog/child-status.ts +28 -36
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +1 -1
- package/src/watchdog/register-child.ts +10 -3
- package/src/watchdog/register-main.ts +20 -20
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +14 -30
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +23 -12
- package/src/watchdog/settings.ts +3 -6
- package/src/watchdog/types.ts +3 -5
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +68 -7
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
import { splitKnownThinkingSuffix as splitThinkingSuffix, type ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
|
|
2
|
-
import type { Usage } from "../../shared/types.ts";
|
|
3
|
-
import { filterFallbackCandidates, findModelExclusion, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
|
|
4
2
|
import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
|
|
5
|
-
import { redactSecretValues } from "./permissions.ts";
|
|
6
3
|
|
|
7
4
|
export type { AvailableModelInfo };
|
|
8
5
|
|
|
9
|
-
interface
|
|
10
|
-
model
|
|
11
|
-
|
|
12
|
-
exitCode?: number | null;
|
|
13
|
-
error?: string;
|
|
14
|
-
usage?: Usage;
|
|
6
|
+
export interface ModelSelectionEvidence {
|
|
7
|
+
model?: string;
|
|
8
|
+
requestedModel?: string;
|
|
15
9
|
}
|
|
16
10
|
|
|
17
11
|
export { splitThinkingSuffix };
|
|
@@ -290,57 +284,6 @@ function enforceModelScopes(
|
|
|
290
284
|
for (const violation of violations) (onWarn ?? defaultScopeWarn)(violation);
|
|
291
285
|
}
|
|
292
286
|
|
|
293
|
-
const MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH = 240;
|
|
294
|
-
const MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES = 20;
|
|
295
|
-
|
|
296
|
-
function sanitizeModelExclusionDiagnostic(value: string | undefined, fallback: string): string {
|
|
297
|
-
const normalized = typeof value === "string"
|
|
298
|
-
? value.replace(/[\u0000-\u001f\u007f\u2028\u2029]+/g, " ").trim()
|
|
299
|
-
: "";
|
|
300
|
-
return redactSecretValues(normalized || fallback).slice(0, MODEL_EXCLUSION_DIAGNOSTIC_MAX_LENGTH);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function formatModelExclusionExpiry(expiresAt: number): string {
|
|
304
|
-
if (!Number.isFinite(expiresAt)) return "unknown";
|
|
305
|
-
const date = new Date(expiresAt);
|
|
306
|
-
return Number.isNaN(date.getTime()) ? "unknown" : date.toISOString();
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function formatExcludedCandidateEvidence(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>): string {
|
|
310
|
-
const { provider, modelId } = parseModelKey(candidate);
|
|
311
|
-
const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
|
|
312
|
-
const displayModel = sanitizeModelExclusionDiagnostic(modelId, "unknown");
|
|
313
|
-
const displayProvider = sanitizeModelExclusionDiagnostic(provider ?? exclusion.provider, "unspecified");
|
|
314
|
-
const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
|
|
315
|
-
return `${displayCandidate} — model: ${displayModel}; provider: ${displayProvider}; reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}`;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const MODEL_UNAVAILABLE_EXCLUSION_PATTERNS = [
|
|
319
|
-
/model.*not found/i,
|
|
320
|
-
/unknown model/i,
|
|
321
|
-
/model.*unavailable/i,
|
|
322
|
-
/model.*disabled/i,
|
|
323
|
-
];
|
|
324
|
-
|
|
325
|
-
function isCurrentRegistryModel(candidate: string, availableModels: AvailableModelInfo[] | undefined): boolean {
|
|
326
|
-
if (!availableModels || availableModels.length === 0) return false;
|
|
327
|
-
const { baseModel } = splitThinkingSuffix(candidate);
|
|
328
|
-
return availableModels.some((entry) => entry.fullId === baseModel);
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function ignoreStaleModelUnavailableExclusion(candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>, availableModels: AvailableModelInfo[] | undefined): boolean {
|
|
332
|
-
const reason = exclusion.reason ?? "";
|
|
333
|
-
return MODEL_UNAVAILABLE_EXCLUSION_PATTERNS.some((pattern) => pattern.test(reason)) && isCurrentRegistryModel(candidate, availableModels);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function throwForExplicitModelExclusion(model: string): void {
|
|
337
|
-
const exclusion = findModelExclusion(model);
|
|
338
|
-
if (!exclusion) return;
|
|
339
|
-
const reason = redactSecretValues((exclusion.reason ?? "runtime-failure").replace(/[\u0000-\u001f\u007f]+/g, " ")).slice(0, 240);
|
|
340
|
-
const expiry = Number.isFinite(exclusion.expiresAt) ? `; expires: ${new Date(exclusion.expiresAt).toISOString()}` : "";
|
|
341
|
-
throw new Error(`Requested subagent model '${model}' is excluded and cannot be replaced by a fallback (reason: ${reason}${expiry}).`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
287
|
/**
|
|
345
288
|
* Resolve the `--model` override passed to a spawned subagent.
|
|
346
289
|
*
|
|
@@ -377,7 +320,6 @@ export function resolveSubagentModelOverride(
|
|
|
377
320
|
const candidate = resolveSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
378
321
|
if (options?.source === "explicit") {
|
|
379
322
|
resolved = candidate ?? resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
380
|
-
throwForExplicitModelExclusion(resolved);
|
|
381
323
|
resolvedFromRegistry = true;
|
|
382
324
|
} else if (candidate) {
|
|
383
325
|
resolved = candidate;
|
|
@@ -421,19 +363,15 @@ export function resolveEffectiveSubagentModel(
|
|
|
421
363
|
|
|
422
364
|
export type ModelOrigin = ModelSource | "configured";
|
|
423
365
|
|
|
424
|
-
export interface
|
|
425
|
-
/** Fallback models warn by default and throw when strict scope enforcement is enabled. */
|
|
366
|
+
export interface ResolveModelSelectionOptions {
|
|
426
367
|
scope?: ModelScopeCheckRule | ModelScopeCheckRule[];
|
|
427
368
|
onWarn?: (violation: ModelScopeViolation) => void;
|
|
428
369
|
/** The primary model came from the running parent session, not configuration. */
|
|
429
370
|
primaryModelFromParent?: boolean;
|
|
430
|
-
/** How the
|
|
371
|
+
/** How the model was selected. */
|
|
431
372
|
origin?: ModelOrigin;
|
|
432
373
|
}
|
|
433
374
|
|
|
434
|
-
const ZERO_USABLE_MODEL_CANDIDATES_ERROR =
|
|
435
|
-
"No usable subagent models remain after registry, scope, and cached-exclusion filtering.";
|
|
436
|
-
|
|
437
375
|
export function resolveModelOrigin(input: {
|
|
438
376
|
explicitModel?: string | boolean;
|
|
439
377
|
agentModel?: string | boolean;
|
|
@@ -458,176 +396,30 @@ export function inheritsParentModel(
|
|
|
458
396
|
return Boolean(parentModel && (!trimmed || trimmed === INHERIT_MODEL));
|
|
459
397
|
}
|
|
460
398
|
|
|
461
|
-
export function
|
|
462
|
-
|
|
463
|
-
fallbackModels: string[] | undefined,
|
|
399
|
+
export function resolveModelSelection(
|
|
400
|
+
model: string | undefined,
|
|
464
401
|
availableModels: AvailableModelInfo[] | undefined,
|
|
465
402
|
preferredProvider?: string,
|
|
466
|
-
options?:
|
|
467
|
-
):
|
|
468
|
-
if (!
|
|
403
|
+
options?: ResolveModelSelectionOptions,
|
|
404
|
+
): ModelSelectionEvidence {
|
|
405
|
+
if (!model) throwForUnresolvedEnforcedInheritScope(options?.scope, true);
|
|
469
406
|
const origin = options?.origin ?? (options?.primaryModelFromParent ? "inherited" : "configured");
|
|
407
|
+
const requestedModel = origin === "inherited" ? undefined : model;
|
|
470
408
|
const scopes = configuredScopes(options?.scope);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
let excludedCandidateCount = 0;
|
|
474
|
-
const warnCachedExclusion = (candidate: string, exclusion: NonNullable<ReturnType<typeof findModelExclusion>>) => {
|
|
475
|
-
excludedCandidateCount++;
|
|
476
|
-
if (excludedCandidates.length < MODEL_EXCLUSION_DIAGNOSTIC_MAX_ENTRIES) excludedCandidates.push({ candidate, exclusion });
|
|
477
|
-
const displayCandidate = sanitizeModelExclusionDiagnostic(candidate, "unknown");
|
|
478
|
-
const reason = sanitizeModelExclusionDiagnostic(exclusion.reason, "runtime-failure");
|
|
479
|
-
console.warn(`[pi-subagents] Skipping model '${displayCandidate}' due to a cached exclusion (reason: ${reason}; expires: ${formatModelExclusionExpiry(exclusion.expiresAt)}).`);
|
|
480
|
-
};
|
|
481
|
-
if (origin === "explicit" && primaryModel) {
|
|
482
|
-
const normalized = resolveRequiredSubagentModelCandidate(primaryModel.trim(), availableModels, preferredProvider);
|
|
483
|
-
throwForExplicitModelExclusion(normalized);
|
|
409
|
+
if (origin === "explicit" && model) {
|
|
410
|
+
const normalized = resolveRequiredSubagentModelCandidate(model.trim(), availableModels, preferredProvider);
|
|
484
411
|
enforceModelScopes(normalized, scopes, "explicit", options?.onWarn);
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
const seen = new Set<string>();
|
|
488
|
-
const candidates: string[] = [];
|
|
489
|
-
const rawCandidates = [primaryModel, ...(fallbackModels ?? [])];
|
|
490
|
-
let skippedPrimary: string | undefined;
|
|
491
|
-
let skippedFallback: string | undefined;
|
|
492
|
-
for (let index = 0; index < rawCandidates.length; index++) {
|
|
493
|
-
const raw = rawCandidates[index];
|
|
494
|
-
if (!raw) continue;
|
|
495
|
-
const model = raw.trim();
|
|
496
|
-
const normalized = index === 0 && (origin === "inherited" || origin === "explicit" || options?.primaryModelFromParent)
|
|
497
|
-
? model
|
|
498
|
-
: resolveSubagentModelCandidate(model, availableModels, preferredProvider);
|
|
499
|
-
if (!normalized) {
|
|
500
|
-
if (index === 0) skippedPrimary = model;
|
|
501
|
-
else {
|
|
502
|
-
skippedFallback ??= model;
|
|
503
|
-
console.warn(`[pi-subagents] Skipping fallback model '${model}' because it is unavailable in this environment.`);
|
|
504
|
-
}
|
|
505
|
-
continue;
|
|
506
|
-
}
|
|
507
|
-
if (seen.has(normalized)) continue;
|
|
508
|
-
if (index > 0 || scopes.some((scope) => scope.enforce === true && scope.strict === true)) {
|
|
509
|
-
enforceModelScopes(normalized, scopes, "inherited", options?.onWarn);
|
|
510
|
-
}
|
|
511
|
-
seen.add(normalized);
|
|
512
|
-
candidates.push(normalized);
|
|
513
|
-
}
|
|
514
|
-
const resolved = filterFallbackCandidates(candidates, {
|
|
515
|
-
onExcluded: warnCachedExclusion,
|
|
516
|
-
ignoreExclusion: (candidate, exclusion) => ignoreStaleModelUnavailableExclusion(candidate, exclusion, availableModels),
|
|
517
|
-
});
|
|
518
|
-
if (resolved.length === 0) {
|
|
519
|
-
if (skippedPrimary) resolveRequiredSubagentModelCandidate(skippedPrimary, availableModels, preferredProvider);
|
|
520
|
-
if (candidates.length === 0 && skippedFallback) resolveRequiredSubagentModelCandidate(skippedFallback, availableModels, preferredProvider);
|
|
521
|
-
if (candidates.length > 0) {
|
|
522
|
-
const shownExclusions = excludedCandidates;
|
|
523
|
-
const omittedExclusions = excludedCandidateCount - shownExclusions.length;
|
|
524
|
-
const evidence = shownExclusions.length > 0
|
|
525
|
-
? ` (excluded: ${shownExclusions.map(({ candidate, exclusion }) => formatExcludedCandidateEvidence(candidate, exclusion)).join("; ")}${omittedExclusions > 0 ? `; ... and ${omittedExclusions} more` : ""})`
|
|
526
|
-
: "";
|
|
527
|
-
throw new Error(`${ZERO_USABLE_MODEL_CANDIDATES_ERROR}${evidence}`);
|
|
528
|
-
}
|
|
529
|
-
return resolved;
|
|
412
|
+
model = normalized;
|
|
530
413
|
}
|
|
531
|
-
|
|
532
|
-
|
|
414
|
+
const resolved = model && (origin === "inherited" || origin === "explicit" || options?.primaryModelFromParent)
|
|
415
|
+
? model.trim()
|
|
416
|
+
: model ? resolveRequiredSubagentModelCandidate(model.trim(), availableModels, preferredProvider) : undefined;
|
|
417
|
+
if (resolved && scopes.some((scope) => scope.enforce === true && scope.strict === true)) {
|
|
418
|
+
enforceModelScopes(resolved, scopes, "inherited", options?.onWarn);
|
|
533
419
|
}
|
|
534
|
-
return resolved;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const RETRYABLE_MODEL_FAILURE_PATTERNS = [
|
|
538
|
-
/^REQUEST_LIMIT_EXCEEDED$/,
|
|
539
|
-
/rate\s*limit/i,
|
|
540
|
-
/usage\s*limit/i,
|
|
541
|
-
/too many requests/i,
|
|
542
|
-
/\b429\b/,
|
|
543
|
-
/quota/i,
|
|
544
|
-
/billing/i,
|
|
545
|
-
/credit/i,
|
|
546
|
-
// OpenRouter can return only a status-prefixed body, without auth-related prose.
|
|
547
|
-
/^\s*401\s*:/,
|
|
548
|
-
/auth(?:entication)?/i,
|
|
549
|
-
/unauthori[sz]ed/i,
|
|
550
|
-
/forbidden/i,
|
|
551
|
-
/api key/i,
|
|
552
|
-
/token expired/i,
|
|
553
|
-
/invalid key/i,
|
|
554
|
-
/provider.*unavailable/i,
|
|
555
|
-
/model.*unavailable/i,
|
|
556
|
-
/model.*disabled/i,
|
|
557
|
-
/model.*not found/i,
|
|
558
|
-
/unknown model/i,
|
|
559
|
-
/overloaded/i,
|
|
560
|
-
/service unavailable/i,
|
|
561
|
-
/temporar(?:ily)? unavailable/i,
|
|
562
|
-
/connection\s+(?:error|reset|closed|aborted)/i,
|
|
563
|
-
/connection refused/i,
|
|
564
|
-
/fetch failed/i,
|
|
565
|
-
/network error/i,
|
|
566
|
-
/socket hang up/i,
|
|
567
|
-
/stream ended without finish_reason/i,
|
|
568
|
-
/upstream/i,
|
|
569
|
-
/timed? out/i,
|
|
570
|
-
/timeout/i,
|
|
571
|
-
/\b500\b/,
|
|
572
|
-
/\b502\b/,
|
|
573
|
-
/\b503\b/,
|
|
574
|
-
/\b504\b/,
|
|
575
|
-
/internal server error/i,
|
|
576
|
-
/cold.?start/i,
|
|
577
|
-
/empty response/i,
|
|
578
|
-
/no output/i,
|
|
579
|
-
/model.*(?:load|fail|error)/i,
|
|
580
|
-
];
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* Failures reported as `<tool> failed (exit N): ...` or `<tool> failed with
|
|
584
|
-
* exit code N` come from a tool call inside the child's task, not from the
|
|
585
|
-
* provider/model, however network-flavored their details read. Retrying a
|
|
586
|
-
* different model cannot fix them and would rerun the whole task. Tool names
|
|
587
|
-
* include namespaced forms like `mcp.server/write`.
|
|
588
|
-
*/
|
|
589
|
-
const TOOL_FAILURE_PREFIX = /^[\w.:@/-]+ failed (?:(?:\(exit \d+\):)|(?:with exit code \d+))(?:\s|$)/i;
|
|
590
|
-
|
|
591
|
-
export function isRetryableModelFailure(error: string | undefined): boolean {
|
|
592
|
-
if (!error) return false;
|
|
593
|
-
if (TOOL_FAILURE_PREFIX.test(error.trim())) return false;
|
|
594
|
-
return RETRYABLE_MODEL_FAILURE_PATTERNS.some((pattern) => pattern.test(error));
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function messageError(message: unknown): string | undefined {
|
|
598
|
-
if (!message || typeof message !== "object") return undefined;
|
|
599
|
-
const value = (message as { errorMessage?: unknown }).errorMessage;
|
|
600
|
-
return typeof value === "string" ? value : undefined;
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
export function isRetryableModelFailureAttempt(input: { error: string | undefined; messages?: readonly unknown[]; toolCount?: number }): boolean {
|
|
604
|
-
if (!isRetryableModelFailure(input.error)) return false;
|
|
605
|
-
if ((input.toolCount ?? 0) > 0) return false;
|
|
606
|
-
if (input.error === "Subagent produced no output (possible model cold-start or empty response)." || /^Subagent produced no output after terminal assistant stopReason "[^"]+"\.$/.test(input.error ?? "")) return true;
|
|
607
|
-
if ((input.toolCount ?? 0) === 0 && (input.messages?.length ?? 0) === 0) return true;
|
|
608
|
-
const error = input.error?.trim();
|
|
609
|
-
return Boolean(error && input.messages?.some((message) => messageError(message)?.trim() === error));
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Request-shape failures can match broad fallback signals such as "upstream",
|
|
613
|
-
// but do not establish that the model is unhealthy for subsequent requests.
|
|
614
|
-
const REQUEST_SHAPE_FAILURE_PATTERN = /\b(?:bad[ _]request|invalid[ _]argument|invalid_request_error)\b/i;
|
|
615
|
-
|
|
616
|
-
export function recordRetryableModelFailure(model: string | undefined, error: string | undefined): void {
|
|
617
|
-
if (!model || !error || !isRetryableModelFailure(error) || isContextOverflow(error)) return;
|
|
618
|
-
if (REQUEST_SHAPE_FAILURE_PATTERN.test(error)) return;
|
|
619
|
-
const { provider, modelId } = parseModelKey(model);
|
|
620
|
-
recordModelFailure({ modelId, reason: error, ...(provider ? { provider } : {}) });
|
|
420
|
+
return { ...(resolved ? { model: resolved } : {}), ...(requestedModel ? { requestedModel } : {}) };
|
|
621
421
|
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Context-overflow signals. These are deliberately NOT part of
|
|
625
|
-
* {@link RETRYABLE_MODEL_FAILURE_PATTERNS}: an overflow means the input was too
|
|
626
|
-
* large for the model's context window, so retrying the same input on another
|
|
627
|
-
* model (or the same model again) cannot succeed. Callers should treat overflow
|
|
628
|
-
* as a terminal, non-retryable failure and surface a clear "input too large"
|
|
629
|
-
* error instead of burning fallback attempts on a guaranteed failure.
|
|
630
|
-
*/
|
|
422
|
+
/** Context-overflow signals used to surface a clear input-too-large error. */
|
|
631
423
|
const CONTEXT_OVERFLOW_PATTERNS = [
|
|
632
424
|
/context(?: length| window| limit)? (?:exceed|overflow|too long)/i,
|
|
633
425
|
/maximum context length/i,
|
|
@@ -644,13 +436,6 @@ const CONTEXT_OVERFLOW_PATTERNS = [
|
|
|
644
436
|
|
|
645
437
|
export function isContextOverflow(error: string | undefined): boolean {
|
|
646
438
|
if (!error) return false;
|
|
647
|
-
if (
|
|
439
|
+
if (/^[\w.:@/-]+ failed (?:(?:\(exit \d+\):)|(?:with exit code \d+))(?:\s|$)/i.test(error.trim())) return false;
|
|
648
440
|
return CONTEXT_OVERFLOW_PATTERNS.some((pattern) => pattern.test(error));
|
|
649
441
|
}
|
|
650
|
-
|
|
651
|
-
export function formatModelAttemptNote(attempt: ModelAttemptSummary, nextModel?: string): string {
|
|
652
|
-
const failure = attempt.error?.trim() || `exit ${attempt.exitCode ?? 1}`;
|
|
653
|
-
return nextModel
|
|
654
|
-
? `[fallback] ${attempt.model} failed: ${failure}. Retrying with ${nextModel}.`
|
|
655
|
-
: `[fallback] ${attempt.model} failed: ${failure}.`;
|
|
656
|
-
}
|
|
@@ -17,7 +17,7 @@ import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
|
17
17
|
|
|
18
18
|
export interface ModelScopeRule {
|
|
19
19
|
enforce?: boolean;
|
|
20
|
-
/** Reject inherited
|
|
20
|
+
/** Reject inherited models outside the allowlist instead of warning. */
|
|
21
21
|
strict?: boolean;
|
|
22
22
|
/** Glob-style allow patterns (only `*` is special), matched against `provider/id`. */
|
|
23
23
|
allow?: string[];
|
|
@@ -232,10 +232,12 @@ function sanitizeLaunchResolvedExtensions(value: unknown): LaunchResolvedChildEx
|
|
|
232
232
|
disableAmbientExtensions: raw.disableAmbientExtensions,
|
|
233
233
|
runtime: stringList(raw.runtime),
|
|
234
234
|
configured: stringList(raw.configured),
|
|
235
|
+
required: Array.isArray(raw.required) ? raw.required.filter((item): item is string => typeof item === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(item)).slice(0, 32) : [],
|
|
235
236
|
effective: stringList(raw.effective),
|
|
236
237
|
omitted: {
|
|
237
238
|
runtime: omittedCount("runtime"),
|
|
238
239
|
configured: omittedCount("configured"),
|
|
240
|
+
required: omittedCount("required"),
|
|
239
241
|
effective: omittedCount("effective"),
|
|
240
242
|
},
|
|
241
243
|
};
|
|
@@ -1063,8 +1065,15 @@ export function isTopLevelAsyncDir(asyncDir: string): boolean {
|
|
|
1063
1065
|
return containedPath(DIRS.async, resolved) && !containedPath(path.join(TEMP_ROOT_DIR, "nested-subagent-runs"), resolved);
|
|
1064
1066
|
}
|
|
1065
1067
|
|
|
1066
|
-
export function
|
|
1068
|
+
export function nestedRunScope(rootRunId: string) {
|
|
1067
1069
|
assertSafeId("rootRunId", rootRunId);
|
|
1070
|
+
return {
|
|
1071
|
+
asyncDirRoot: path.join(TEMP_ROOT_DIR, "nested-subagent-runs", rootRunId),
|
|
1072
|
+
resultsDir: path.join(DIRS.results, "nested", rootRunId),
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
export function nestedResultsPath(rootRunId: string, id: string): string {
|
|
1068
1077
|
assertSafeId("id", id);
|
|
1069
|
-
return path.join(
|
|
1078
|
+
return path.join(nestedRunScope(rootRunId).resultsDir, `${id}.json`);
|
|
1070
1079
|
}
|
|
@@ -10,6 +10,10 @@ export interface RunnerSubagentStep {
|
|
|
10
10
|
sessionName?: string;
|
|
11
11
|
task: string;
|
|
12
12
|
runner?: ResolvedRunnerConfig;
|
|
13
|
+
/** Herdr saved machine this external-cli step runs on; `cwd` is then the directory on that machine. */
|
|
14
|
+
machine?: import("../../shared/types.ts").HerdrMachineReference;
|
|
15
|
+
remoteReads?: string[] | false;
|
|
16
|
+
machineEnv?: Record<string, string>;
|
|
13
17
|
externalJobFollowUp?: {
|
|
14
18
|
sourceRunId: string;
|
|
15
19
|
sourceStepIndex: number;
|
|
@@ -37,7 +41,7 @@ export interface RunnerSubagentStep {
|
|
|
37
41
|
fast?: boolean;
|
|
38
42
|
thinking?: string;
|
|
39
43
|
thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
|
|
40
|
-
|
|
44
|
+
requestedModel?: string;
|
|
41
45
|
/** The primary model is inherited from the parent session and should not be verified against the child-reported active registry model. */
|
|
42
46
|
skipPrimaryModelVerification?: boolean;
|
|
43
47
|
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string; contextWindow?: number }>;
|
|
@@ -47,6 +51,8 @@ export interface RunnerSubagentStep {
|
|
|
47
51
|
allowNestedSubagents?: boolean;
|
|
48
52
|
extensions?: string[];
|
|
49
53
|
subagentOnlyExtensions?: string[];
|
|
54
|
+
/** Private immutable host policy snapshot serialized to the native runner. */
|
|
55
|
+
requiredExtensions?: import("../../shared/required-child-extensions.ts").RequiredChildExtensionSnapshot;
|
|
50
56
|
mcpDirectTools?: string[];
|
|
51
57
|
mutationTools?: string[];
|
|
52
58
|
completionGuard?: boolean;
|
|
@@ -237,7 +243,6 @@ export interface ParallelTaskResult {
|
|
|
237
243
|
error?: string;
|
|
238
244
|
timedOut?: boolean;
|
|
239
245
|
model?: string;
|
|
240
|
-
attemptedModels?: string[];
|
|
241
246
|
outputTargetPath?: string;
|
|
242
247
|
outputTargetExists?: boolean;
|
|
243
248
|
}
|
|
@@ -63,7 +63,7 @@ export interface PiSpawnDeps {
|
|
|
63
63
|
export function resolveBunPiExecutable(deps: PiSpawnDeps = {}): string | undefined {
|
|
64
64
|
const bunVersion = deps.bunVersion ?? process.versions.bun;
|
|
65
65
|
const entry = deps.argv1 ?? process.argv[1];
|
|
66
|
-
if (!bunVersion || !entry
|
|
66
|
+
if (!bunVersion || !entry || !/^(?:\/\$bunfs\/|B:[\\/]~BUN[\\/])/.test(entry)) return undefined;
|
|
67
67
|
const env = deps.env ?? process.env;
|
|
68
68
|
return env[PI_SUBAGENT_PI_BINARY_ENV]?.trim() || (deps.execPath ?? process.execPath);
|
|
69
69
|
}
|
|
@@ -17,6 +17,7 @@ import { registerChildWatchdog } from "../../watchdog/register-child.ts";
|
|
|
17
17
|
import type { ChildWatchdogConfig } from "../../watchdog/child-status.ts";
|
|
18
18
|
import { requestWatchdogPermission, type WatchdogPermissionRequest, type WatchdogPermissionResult } from "../../watchdog/permission-arbiter.ts";
|
|
19
19
|
import { SUBAGENT_WATCHDOG_WARNING_TYPE } from "../../watchdog/types.ts";
|
|
20
|
+
import { inheritedNestedRouteOf } from "./nested-events.ts";
|
|
20
21
|
import { registerWaitTool } from "../background/wait-tool.ts";
|
|
21
22
|
import { drainOutstandingWork } from "../background/auto-drain.ts";
|
|
22
23
|
import {
|
|
@@ -466,7 +467,8 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI, config?:
|
|
|
466
467
|
watcherRestartTimer: null,
|
|
467
468
|
resultFileCoalescer: { schedule: () => false, clear: () => {} },
|
|
468
469
|
} as unknown as SubagentState;
|
|
469
|
-
|
|
470
|
+
const nestedRootRunId = inheritedNestedRouteOf(config)?.rootRunId;
|
|
471
|
+
if (typeof pi.registerTool === "function") registerWaitTool(pi, waitState, config.waitTool.enabled, undefined, config.waitTool.defaultTimeoutMs, { nestedRootRunId });
|
|
470
472
|
const supervisorMetadata = childSupervisorMetadata(config);
|
|
471
473
|
let nativeSupervisorClientRegistered = false;
|
|
472
474
|
const registerNativeSupervisorClientOnce = (): void => {
|
|
@@ -495,7 +497,7 @@ export default function registerSubagentPromptRuntime(pi: ExtensionAPI, config?:
|
|
|
495
497
|
}
|
|
496
498
|
config.holdFinalDrain?.(true);
|
|
497
499
|
try {
|
|
498
|
-
await drainOutstandingWork({ state: waitState, events: pi.events }, drainObservation);
|
|
500
|
+
await drainOutstandingWork({ state: waitState, events: pi.events, nestedRootRunId, hasPendingSupervisorRequest: config.hasPendingSupervisorRequest }, drainObservation);
|
|
499
501
|
} finally {
|
|
500
502
|
config.holdFinalDrain?.(false);
|
|
501
503
|
}
|
|
@@ -2,6 +2,26 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createOwnedProcessTreeController } from "../background/owned-process-tree.ts";
|
|
3
3
|
import type { ProcessTreeTerminal } from "../../shared/types.ts";
|
|
4
4
|
|
|
5
|
+
function terminateCommandTree(pid: number, controller: ReturnType<typeof createOwnedProcessTreeController>): Promise<ProcessTreeTerminal> {
|
|
6
|
+
if (process.platform !== "win32") return controller.terminate();
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let settled = false;
|
|
9
|
+
const fallback = () => {
|
|
10
|
+
if (settled) return;
|
|
11
|
+
settled = true;
|
|
12
|
+
void controller.terminate().then(resolve);
|
|
13
|
+
};
|
|
14
|
+
const cleanup = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
|
|
15
|
+
cleanup.once("error", fallback);
|
|
16
|
+
cleanup.once("close", (status) => {
|
|
17
|
+
if (settled) return;
|
|
18
|
+
if (status !== 0) return fallback();
|
|
19
|
+
settled = true;
|
|
20
|
+
resolve({ state: "observed", mechanism: "windows-taskkill", pid, verifiedAt: Date.now() });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
export interface SetupCommandOptions {
|
|
6
26
|
cwd?: string;
|
|
7
27
|
env?: NodeJS.ProcessEnv;
|
|
@@ -19,6 +39,7 @@ export interface SetupCommandOptions {
|
|
|
19
39
|
|
|
20
40
|
export interface SetupCommandResult {
|
|
21
41
|
stdout: string;
|
|
42
|
+
stdoutBuffer: Buffer;
|
|
22
43
|
stderr: string;
|
|
23
44
|
status: number | null;
|
|
24
45
|
signal: NodeJS.Signals | null;
|
|
@@ -46,7 +67,7 @@ export async function runSetupCommand(
|
|
|
46
67
|
options: SetupCommandOptions,
|
|
47
68
|
): Promise<SetupCommandResult> {
|
|
48
69
|
const result: SetupCommandResult = {
|
|
49
|
-
stdout: "", stderr: "", status: null, signal: null, outputIncomplete: false,
|
|
70
|
+
stdout: "", stdoutBuffer: Buffer.alloc(0), stderr: "", status: null, signal: null, outputIncomplete: false,
|
|
50
71
|
};
|
|
51
72
|
const maxBuffer = options.maxBuffer ?? 1024 * 1024;
|
|
52
73
|
if (!Number.isSafeInteger(maxBuffer) || maxBuffer <= 0) throw new Error("Invalid setup command maxBuffer");
|
|
@@ -84,7 +105,8 @@ export async function runSetupCommand(
|
|
|
84
105
|
let directSettled = false;
|
|
85
106
|
const releaseUnknownIO = () => {
|
|
86
107
|
if (!directSettled || result.processTree?.state !== "unknown") return;
|
|
87
|
-
|
|
108
|
+
const detail = result.processTree.diagnostic ? `: ${result.processTree.diagnostic}` : "";
|
|
109
|
+
result.error = commandError(`Worktree setup process tree settlement is unverified${detail}`, "PROCESS_TREE_UNVERIFIED");
|
|
88
110
|
result.outputIncomplete = true;
|
|
89
111
|
// Only local I/O is released; unknown descendant ownership remains retained.
|
|
90
112
|
child.stdin.destroy();
|
|
@@ -101,7 +123,7 @@ export async function runSetupCommand(
|
|
|
101
123
|
releaseUnknownIO();
|
|
102
124
|
return;
|
|
103
125
|
}
|
|
104
|
-
termination = tree
|
|
126
|
+
termination = terminateCommandTree(result.pid!, tree).then((proof) => {
|
|
105
127
|
result.processTree = proof;
|
|
106
128
|
releaseUnknownIO();
|
|
107
129
|
return proof;
|
|
@@ -177,7 +199,8 @@ export async function runSetupCommand(
|
|
|
177
199
|
if (termination) result.processTree = await termination;
|
|
178
200
|
const cancelled = cancellation();
|
|
179
201
|
if (cancelled) fail(cancelled);
|
|
180
|
-
result.
|
|
202
|
+
result.stdoutBuffer = Buffer.concat(stdout);
|
|
203
|
+
result.stdout = result.stdoutBuffer.toString("utf8");
|
|
181
204
|
result.stderr = Buffer.concat(stderr).toString("utf8");
|
|
182
205
|
return result;
|
|
183
206
|
} finally {
|
|
@@ -180,7 +180,7 @@ export interface WorktreeSetupProgress {
|
|
|
180
180
|
setup: WorktreeSetup;
|
|
181
181
|
attempts: Array<{ index: number; branch: string; path?: string; validated: boolean; command?: WorktreeSetupProgress["command"]; hookCommand?: WorktreeSetupProgress["command"] }>;
|
|
182
182
|
phase: string;
|
|
183
|
-
command?: { command: string; args: string[]; pid?: number; processGroupId?: number; result?: Omit<SetupCommandResult, "stdout" | "stderr"> };
|
|
183
|
+
command?: { command: string; args: string[]; pid?: number; processGroupId?: number; result?: Omit<SetupCommandResult, "stdout" | "stdoutBuffer" | "stderr"> };
|
|
184
184
|
unknown?: string;
|
|
185
185
|
cleanup?: WorktreeCleanupReport;
|
|
186
186
|
}
|
|
@@ -245,7 +245,7 @@ class SetupTransaction {
|
|
|
245
245
|
...options, signal: this.options.signal, deadlineAt: this.options.deadlineAt,
|
|
246
246
|
onSpawn: (process) => { Object.assign(this.progress.command!, process); this.publish(); },
|
|
247
247
|
});
|
|
248
|
-
const { stdout: _stdout, stderr: _stderr, ...metadata } = result;
|
|
248
|
+
const { stdout: _stdout, stdoutBuffer: _stdoutBuffer, stderr: _stderr, ...metadata } = result;
|
|
249
249
|
this.progress.command.result = metadata;
|
|
250
250
|
if (result.processTree?.state === "unknown") this.unknown(result.error ?? "Command tree settlement unverified");
|
|
251
251
|
this.publish();
|
|
@@ -743,13 +743,35 @@ export function resolveExpectedWorktreeAgentCwd(cwd: string, runId: string, inde
|
|
|
743
743
|
function linkNodeModulesIfPresent(toplevel: string, worktreePath: string): boolean {
|
|
744
744
|
const nodeModulesPath = path.join(toplevel, "node_modules");
|
|
745
745
|
const nodeModulesLinkPath = path.join(worktreePath, "node_modules");
|
|
746
|
-
|
|
746
|
+
const hasDirectoryEntry = (candidate: string): boolean => {
|
|
747
|
+
try { fs.lstatSync(candidate); return true; }
|
|
748
|
+
catch (error) {
|
|
749
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
752
|
+
};
|
|
747
753
|
try {
|
|
748
|
-
|
|
754
|
+
if (hasDirectoryEntry(nodeModulesLinkPath)) return false;
|
|
755
|
+
let sourceRealPath: string;
|
|
756
|
+
try {
|
|
757
|
+
if (!fs.statSync(nodeModulesPath).isDirectory()) throw new Error("source node_modules is not a directory");
|
|
758
|
+
sourceRealPath = fs.realpathSync.native(nodeModulesPath);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
fs.symlinkSync(nodeModulesPath, nodeModulesLinkPath, process.platform === "win32" ? "junction" : "dir");
|
|
764
|
+
if (!fs.lstatSync(nodeModulesLinkPath).isSymbolicLink()
|
|
765
|
+
|| fs.realpathSync.native(nodeModulesLinkPath) !== sourceRealPath) {
|
|
766
|
+
throw new Error("created link does not resolve to the source node_modules");
|
|
767
|
+
}
|
|
749
768
|
return true;
|
|
750
|
-
} catch {
|
|
751
|
-
|
|
752
|
-
|
|
769
|
+
} catch (error) {
|
|
770
|
+
const code = error instanceof Error && "code" in error && typeof error.code === "string" ? `${error.code}: ` : "";
|
|
771
|
+
throw new Error(
|
|
772
|
+
`failed to link node_modules from ${nodeModulesPath} to ${nodeModulesLinkPath}: ${code}${error instanceof Error ? error.message : String(error)}`,
|
|
773
|
+
{ cause: error },
|
|
774
|
+
);
|
|
753
775
|
}
|
|
754
776
|
}
|
|
755
777
|
|
|
@@ -1295,7 +1317,7 @@ async function compensateSetup(tx: SetupTransaction): Promise<WorktreeCleanupRep
|
|
|
1295
1317
|
deadlineAt: tx.options.deadlineAt, acceptedExitCodes,
|
|
1296
1318
|
onSpawn: (process) => { Object.assign(tx.progress.command!, process); tx.publish(); },
|
|
1297
1319
|
});
|
|
1298
|
-
const { stdout: _stdout, stderr: _stderr, ...metadata } = result;
|
|
1320
|
+
const { stdout: _stdout, stdoutBuffer: _stdoutBuffer, stderr: _stderr, ...metadata } = result;
|
|
1299
1321
|
tx.progress.command.result = metadata;
|
|
1300
1322
|
if (result.processTree?.state === "unknown") tx.unknown(result.error ?? "Rollback command settlement unverified");
|
|
1301
1323
|
tx.publish();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { StreamFn } from "@earendil-works/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Anthropic prices a cache write by the TTL it is asked for: 1.25x base input
|
|
5
|
+
* for the 5m window, 2x for the 1h one. Child sessions are short-lived and
|
|
6
|
+
* rarely idle, so a 1h window they never claim is a flat surcharge on every
|
|
7
|
+
* write. Setting this to `short` keeps that surcharge off children while the
|
|
8
|
+
* parent keeps `long`, the same split Claude Code makes between its main
|
|
9
|
+
* conversation and its subagents.
|
|
10
|
+
*
|
|
11
|
+
* Unset means children inherit the parent's retention, which is the behaviour
|
|
12
|
+
* before this setting existed.
|
|
13
|
+
*/
|
|
14
|
+
export function childCacheRetention(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
15
|
+
return env.PI_SUBAGENT_CACHE_RETENTION || undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Launch-environment form for spawned children. Empty when unset, so the child
|
|
20
|
+
* inherits the parent's `PI_CACHE_RETENTION` rather than having it cleared.
|
|
21
|
+
*/
|
|
22
|
+
export function childCacheRetentionEnv(env?: NodeJS.ProcessEnv): { PI_CACHE_RETENTION?: string } {
|
|
23
|
+
const retention = childCacheRetention(env);
|
|
24
|
+
return retention ? { PI_CACHE_RETENTION: retention } : {};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pi resolves retention per request as `options.env?.[name] || process.env[name]`,
|
|
29
|
+
* so a per-call env beats the process-wide one. Wrapping the session's own
|
|
30
|
+
* stream function keeps this scoped to one child, with no shared-state race
|
|
31
|
+
* against a parent turn streaming concurrently.
|
|
32
|
+
*/
|
|
33
|
+
export function pinChildCacheRetention(agent: { streamFunction: StreamFn } | undefined, env?: NodeJS.ProcessEnv): void {
|
|
34
|
+
if (!agent?.streamFunction) return;
|
|
35
|
+
const retention = childCacheRetention(env);
|
|
36
|
+
if (!retention) return;
|
|
37
|
+
const base = agent.streamFunction;
|
|
38
|
+
agent.streamFunction = (model, context, options) =>
|
|
39
|
+
base(model, context, {
|
|
40
|
+
...options,
|
|
41
|
+
env: { ...(options?.env ?? {}), PI_CACHE_RETENTION: retention },
|
|
42
|
+
});
|
|
43
|
+
}
|