@bermudi/pi-delegate 0.1.7 → 0.1.9
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/README.md +14 -1
- package/agents.ts +54 -3
- package/config.ts +84 -9
- package/constants.ts +17 -0
- package/delegate.ts +12 -1
- package/dispatch.ts +4 -0
- package/extension.ts +11 -1
- package/host.ts +243 -78
- package/manual.ts +9 -6
- package/package.json +1 -1
- package/schema.ts +4 -4
- package/settings.ts +235 -13
- package/task-resolution.ts +109 -52
- package/types.ts +7 -1
package/host.ts
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* must not run the parent's interactive extensions (custom UI, slash commands,
|
|
17
17
|
* hooks that call `pi.appendEntry()`/`pi.sendMessage()`). A narrow,
|
|
18
18
|
* provider-scoped allowlist is injected as `additionalExtensionPaths` for
|
|
19
|
-
*
|
|
19
|
+
* provider-specific integrations (best-effort for shipped defaults). Those
|
|
20
|
+
* extension-bearing dependencies are
|
|
20
21
|
* deliberately built per session: `AgentSession._buildRuntime` hands the
|
|
21
22
|
* loader's `extensionsResult.runtime` to a new `ExtensionRunner`, whose
|
|
22
23
|
* `bindCore()` overwrites mutable methods on that runtime (`sendMessage`,
|
|
@@ -37,7 +38,15 @@
|
|
|
37
38
|
import { execFileSync } from "node:child_process";
|
|
38
39
|
import { homedir } from "node:os";
|
|
39
40
|
import { existsSync, realpathSync } from "node:fs";
|
|
40
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
basename,
|
|
43
|
+
dirname,
|
|
44
|
+
isAbsolute,
|
|
45
|
+
join,
|
|
46
|
+
relative,
|
|
47
|
+
resolve,
|
|
48
|
+
sep,
|
|
49
|
+
} from "node:path";
|
|
41
50
|
import {
|
|
42
51
|
DefaultPackageManager,
|
|
43
52
|
DefaultResourceLoader,
|
|
@@ -49,7 +58,7 @@ import {
|
|
|
49
58
|
} from "@earendil-works/pi-coding-agent";
|
|
50
59
|
import {
|
|
51
60
|
getSubagentProviderExtensionMap,
|
|
52
|
-
|
|
61
|
+
getSubagentProviderExtensionSourcesForProvider,
|
|
53
62
|
} from "./config.ts";
|
|
54
63
|
|
|
55
64
|
export interface HostDeps {
|
|
@@ -94,6 +103,14 @@ const hostDepsInflight = new Map<string, Promise<HostDeps>>();
|
|
|
94
103
|
/** Prevent a pre-invalidation build from repopulating or clearing newer state. */
|
|
95
104
|
let hostDepsCacheGeneration = 0;
|
|
96
105
|
|
|
106
|
+
// When the shipped best-effort default (npm:@bermudi/pi-codex) is absent — the
|
|
107
|
+
// normal state for most users — Pi's DefaultPackageManager.getInstalledPath
|
|
108
|
+
// would synchronously spawn `npm root -g` to check the legacy global fallback.
|
|
109
|
+
// Doing that once per task in a fan-out blocks the event loop N times and
|
|
110
|
+
// serializes the fan-out. Cache the *absence* per dispatch so only the first
|
|
111
|
+
// task in a fan-out pays the cost; the rest skip the lookup entirely.
|
|
112
|
+
const missingBestEffortNpmCache = new Set<string>();
|
|
113
|
+
|
|
97
114
|
function canonicalPath(candidate: string): string {
|
|
98
115
|
try {
|
|
99
116
|
return realpathSync(candidate);
|
|
@@ -508,17 +525,84 @@ async function assertConfiguredNpmVersion(
|
|
|
508
525
|
}
|
|
509
526
|
}
|
|
510
527
|
|
|
528
|
+
/** Result of resolving a provider's allowlisted extension sources. */
|
|
529
|
+
interface ProviderExtensionResolution {
|
|
530
|
+
/** User-scope package roots to inject as subagent extension paths. */
|
|
531
|
+
paths: string[];
|
|
532
|
+
/** Roots originating from shipped best-effort defaults; these may degrade
|
|
533
|
+
* silently — see the drop-site comments for why silence is the design. */
|
|
534
|
+
bestEffortPaths: Set<string>;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* UI notifier for the provider-extension-loaded notice, primed from
|
|
539
|
+
* extension.ts `execute` (the only place the real, ui-bearing ctx exists —
|
|
540
|
+
* host-dep construction itself has no UI context). Consumed defensively:
|
|
541
|
+
* a throw means the ctx went stale (headless run, torn-down TUI) and simply
|
|
542
|
+
* un-primes the notifier so the next live execute re-primes it.
|
|
543
|
+
*/
|
|
544
|
+
let providerExtensionNotifier: ((message: string) => void) | undefined;
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Prime the UI notifier used for the best-effort extension-loaded notice.
|
|
548
|
+
* Idempotent and cheap; called at the top of every delegate execute. Pass
|
|
549
|
+
* `undefined` to un-prime (tests) — an un-primed notifier makes the notice a
|
|
550
|
+
* no-op, which is also the default state in headless/test runs.
|
|
551
|
+
*/
|
|
552
|
+
export function registerProviderExtensionNotifier(
|
|
553
|
+
notify: ((message: string) => void) | undefined,
|
|
554
|
+
): void {
|
|
555
|
+
providerExtensionNotifier = notify;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** provider+root pairs already noticed this process. */
|
|
559
|
+
const noticedProviderExtensionRoots = new Set<string>();
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Announce that a shipped best-effort provider integration actually loaded
|
|
563
|
+
* for delegated subagents. This is the deliberate inverse of the silent
|
|
564
|
+
* drop: absence of an optional integration is normal and never mentioned,
|
|
565
|
+
* but a default that IS active changes subagent behavior (remote compaction
|
|
566
|
+
* on codex models) invisibly — so it gets one info notice per process per
|
|
567
|
+
* provider+root, not one per dispatch. User-configured sources never get
|
|
568
|
+
* here: the user installed them knowingly and they fail closed.
|
|
569
|
+
*/
|
|
570
|
+
function noticeProviderExtensionLoaded(
|
|
571
|
+
provider: string | undefined,
|
|
572
|
+
root: string,
|
|
573
|
+
): void {
|
|
574
|
+
const providerName = provider?.trim() || "provider";
|
|
575
|
+
const key = `${providerName.toLowerCase()}\0${root}`;
|
|
576
|
+
if (noticedProviderExtensionRoots.has(key)) return;
|
|
577
|
+
const notify = providerExtensionNotifier;
|
|
578
|
+
if (!notify) return;
|
|
579
|
+
const label = basename(root) || root;
|
|
580
|
+
try {
|
|
581
|
+
notify(`⚡ ${label} integration active for ${providerName} subagents`);
|
|
582
|
+
noticedProviderExtensionRoots.add(key);
|
|
583
|
+
} catch {
|
|
584
|
+
// Cosmetic notice on a stale ctx — fail open (status.ts precedent for
|
|
585
|
+
// cached-ctx notify): drop the notifier, keep the key un-noticed so a
|
|
586
|
+
// live ctx can still surface it later. Delegation is unaffected.
|
|
587
|
+
providerExtensionNotifier = undefined;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
511
591
|
async function getProviderExtensionPaths(
|
|
512
592
|
provider: string | undefined,
|
|
513
593
|
cwd: string,
|
|
514
594
|
agentDir: string,
|
|
515
595
|
packageLookupSettingsManager: SettingsManager,
|
|
516
|
-
): Promise<
|
|
517
|
-
// Provider-key normalization (trim + lowercase) lives in `config.ts` —
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
|
|
596
|
+
): Promise<ProviderExtensionResolution> {
|
|
597
|
+
// Provider-key normalization (trim + lowercase) lives in `config.ts` — the
|
|
598
|
+
// sources getter is the single owner of that logic, so this module never
|
|
599
|
+
// re-implements it. Provenance (required vs best-effort) is decided there
|
|
600
|
+
// too, by config presence: user-listed sources fail closed; shipped
|
|
601
|
+
// defaults degrade silently — the extension-free path is Pi's normal
|
|
602
|
+
// operation, not a warning condition.
|
|
603
|
+
const requested = getSubagentProviderExtensionSourcesForProvider(provider);
|
|
604
|
+
if (!requested.length)
|
|
605
|
+
return { paths: [], bestEffortPaths: new Set<string>() };
|
|
522
606
|
|
|
523
607
|
const packageManager = new DefaultPackageManager({
|
|
524
608
|
cwd,
|
|
@@ -567,12 +651,34 @@ async function getProviderExtensionPaths(
|
|
|
567
651
|
|
|
568
652
|
const installedPaths = new Map<string, string>();
|
|
569
653
|
const missing: string[] = [];
|
|
570
|
-
for (const source of requested) {
|
|
654
|
+
for (const { source, required } of requested) {
|
|
655
|
+
// For best-effort npm defaults that are absent — the normal state for the
|
|
656
|
+
// shipped optional integration — avoid Pi's legacy global npm fallback
|
|
657
|
+
// (`npm root -g`) on every task in a fan-out. The first task in a dispatch
|
|
658
|
+
// still pays the cost to discover the absence, but subsequent tasks with
|
|
659
|
+
// the same agentDir+source skip the synchronous spawn entirely. The cache
|
|
660
|
+
// is per-dispatch and cleared on invalidateHostDepsCache.
|
|
661
|
+
if (!required && source.trim().toLowerCase().startsWith("npm:")) {
|
|
662
|
+
const missingKey = `${agentDir}\0${source}`;
|
|
663
|
+
if (missingBestEffortNpmCache.has(missingKey)) {
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
const userPath = packageManager.getInstalledPath(source, "user");
|
|
667
|
+
if (!userPath) {
|
|
668
|
+
missingBestEffortNpmCache.add(missingKey);
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
installedPaths.set(source, userPath);
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
571
674
|
// Deliberately resolve only the user scope. Project-local packages are
|
|
572
675
|
// untrusted input and must never become executable subagent extensions.
|
|
573
676
|
const userPath = packageManager.getInstalledPath(source, "user");
|
|
574
677
|
if (!userPath) {
|
|
575
|
-
missing.push(source);
|
|
678
|
+
if (required) missing.push(source);
|
|
679
|
+
// A best-effort default that is not installed is skipped silently: for
|
|
680
|
+
// most users the package was never installed at all, and its absence is
|
|
681
|
+
// the normal, correct state — not something to warn about.
|
|
576
682
|
continue;
|
|
577
683
|
}
|
|
578
684
|
installedPaths.set(source, userPath);
|
|
@@ -587,23 +693,29 @@ async function getProviderExtensionPaths(
|
|
|
587
693
|
}
|
|
588
694
|
|
|
589
695
|
const paths = new Set<string>();
|
|
590
|
-
|
|
696
|
+
const bestEffortPaths = new Set<string>();
|
|
697
|
+
for (const { source, required } of requested) {
|
|
591
698
|
const userPath = installedPaths.get(source);
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
699
|
+
// Best-effort defaults that were not installed never reached the map.
|
|
700
|
+
if (!userPath) continue;
|
|
701
|
+
try {
|
|
702
|
+
validateInstalledPath(source, userPath);
|
|
703
|
+
if (hasNpmVersionSpecifier(source)) {
|
|
704
|
+
await assertConfiguredNpmVersion(packageManager, source, userPath);
|
|
705
|
+
}
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (required) throw error;
|
|
708
|
+
// A best-effort default that cannot be verified is skipped, not loaded
|
|
709
|
+
// and not fatal. Silent by design: an installed-but-broken package also
|
|
710
|
+
// fails in the parent's own extension inventory, where Pi surfaces it;
|
|
711
|
+
// this path only mirrors a signal the user has already seen.
|
|
712
|
+
continue;
|
|
602
713
|
}
|
|
603
714
|
paths.add(userPath);
|
|
715
|
+
if (!required) bestEffortPaths.add(userPath);
|
|
604
716
|
}
|
|
605
717
|
|
|
606
|
-
return [...paths];
|
|
718
|
+
return { paths: [...paths], bestEffortPaths };
|
|
607
719
|
}
|
|
608
720
|
|
|
609
721
|
/**
|
|
@@ -646,14 +758,16 @@ export async function getHostDeps(options: HostDepsOptions): Promise<HostDeps> {
|
|
|
646
758
|
.map(([provider, entries]) => [provider, [...entries]] as const),
|
|
647
759
|
);
|
|
648
760
|
const providerConfigs = options.providerConfigs ?? [];
|
|
649
|
-
const requestedExtensions =
|
|
761
|
+
const requestedExtensions = getSubagentProviderExtensionSourcesForProvider(
|
|
650
762
|
options.modelProvider,
|
|
651
763
|
);
|
|
652
764
|
|
|
653
|
-
// Resolve provider extensions before deciding whether to use the cache.
|
|
654
|
-
//
|
|
655
|
-
//
|
|
765
|
+
// Resolve provider extensions before deciding whether to use the cache.
|
|
766
|
+
// User-configured sources fail closed when missing; shipped defaults are
|
|
767
|
+
// best-effort and silently drop instead. Both package lookup and child
|
|
768
|
+
// resource loading stay isolated from executable project settings.
|
|
656
769
|
let additionalExtensionPaths: string[] = [];
|
|
770
|
+
let bestEffortExtensionRoots = new Set<string>();
|
|
657
771
|
if (requestedExtensions.length > 0) {
|
|
658
772
|
// Package lookup is a user-scope trust boundary. Pi's legacy npm fallback
|
|
659
773
|
// may execute the configured npmCommand to discover the global npm root,
|
|
@@ -663,12 +777,14 @@ export async function getHostDeps(options: HostDepsOptions): Promise<HostDeps> {
|
|
|
663
777
|
agentDir,
|
|
664
778
|
{ projectTrusted: false },
|
|
665
779
|
);
|
|
666
|
-
|
|
780
|
+
const resolution = await getProviderExtensionPaths(
|
|
667
781
|
options.modelProvider,
|
|
668
782
|
options.cwd,
|
|
669
783
|
agentDir,
|
|
670
784
|
packageLookupSettingsManager,
|
|
671
785
|
);
|
|
786
|
+
additionalExtensionPaths = resolution.paths;
|
|
787
|
+
bestEffortExtensionRoots = resolution.bestEffortPaths;
|
|
672
788
|
}
|
|
673
789
|
|
|
674
790
|
// Provider configs may contain functions (custom stream/OAuth handlers), so a
|
|
@@ -731,65 +847,111 @@ export async function getHostDeps(options: HostDepsOptions): Promise<HostDeps> {
|
|
|
731
847
|
if (testRetryBaseMs !== undefined) {
|
|
732
848
|
installFastRetry(resolvedSettingsManager, testRetryBaseMs);
|
|
733
849
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
850
|
+
// Best-effort default sources that fail to load are dropped and the loader
|
|
851
|
+
// is rebuilt without them, so the subagent runs extension-free on Pi's
|
|
852
|
+
// native compaction — silently, per the drop-site rationale above.
|
|
853
|
+
// User-configured sources still fail closed below.
|
|
854
|
+
//
|
|
855
|
+
// Loop invariant: pi's ResourceLoader.reload() never *throws* for an
|
|
856
|
+
// extension's own failure — its loader wraps module import AND factory
|
|
857
|
+
// invocation in try/catch and returns them as `extensionsResult.errors`
|
|
858
|
+
// (verified in pi 0.80.x, core/extensions/loader.ts). A reload() throw is
|
|
859
|
+
// therefore environmental (settings reload, package resolution) and not
|
|
860
|
+
// attributable to any supplied root; letting it propagate is correct even
|
|
861
|
+
// when best-effort roots are present.
|
|
862
|
+
let extensionPaths = additionalExtensionPaths;
|
|
863
|
+
let resourceLoader: DefaultResourceLoader;
|
|
864
|
+
for (;;) {
|
|
865
|
+
resourceLoader = new DefaultResourceLoader({
|
|
866
|
+
cwd: options.cwd,
|
|
867
|
+
agentDir,
|
|
868
|
+
settingsManager: resolvedSettingsManager,
|
|
869
|
+
// Subagents are headless workers — they must not load the parent's
|
|
870
|
+
// interactive extension inventory. The only paths supplied here are
|
|
871
|
+
// the explicitly allowlisted, user-scoped provider extensions.
|
|
872
|
+
noExtensions: true,
|
|
873
|
+
// Global AGENTS.md files describe the parent harness, not the
|
|
874
|
+
// delegated task. Keep cwd/ancestor project context discovery, but
|
|
875
|
+
// remove Pi's global file and the legacy ~/.agents equivalent. This
|
|
876
|
+
// override also handles symlinked global files because it compares
|
|
877
|
+
// discovered paths.
|
|
878
|
+
agentsFilesOverride: ({ agentsFiles }) => ({
|
|
879
|
+
agentsFiles: agentsFiles.filter(
|
|
880
|
+
({ path: contextPath }) =>
|
|
881
|
+
!isExcludedGlobalContextFile(contextPath, agentDir),
|
|
882
|
+
),
|
|
883
|
+
}),
|
|
884
|
+
...(extensionPaths.length
|
|
885
|
+
? { additionalExtensionPaths: extensionPaths }
|
|
886
|
+
: {}),
|
|
887
|
+
// When a named agent supplies a custom prompt, it becomes the loader's
|
|
888
|
+
// customPrompt — overriding the default system prompt AgentSession
|
|
889
|
+
// would otherwise build. `systemPrompt` (the source) wins over file
|
|
890
|
+
// discovery.
|
|
891
|
+
...(options.systemPrompt !== undefined
|
|
892
|
+
? { systemPrompt: options.systemPrompt }
|
|
893
|
+
: {}),
|
|
894
|
+
});
|
|
895
|
+
await resourceLoader.reload();
|
|
896
|
+
|
|
897
|
+
const extensionsResult = resourceLoader.getExtensions();
|
|
898
|
+
const extensionErrors = extensionsResult.errors;
|
|
899
|
+
const loadedExtensionPaths = extensionsResult.extensions.map(
|
|
900
|
+
(extension) => extension.resolvedPath || extension.path,
|
|
901
|
+
);
|
|
902
|
+
// A package can resolve successfully while exposing only skills/prompts,
|
|
903
|
+
// or a malformed manifest can expose no loadable extension at all.
|
|
904
|
+
const missingExtensionRoots = extensionPaths.filter(
|
|
905
|
+
(root) =>
|
|
906
|
+
!loadedExtensionPaths.some((extensionPath) =>
|
|
907
|
+
isPathWithinDirectory(root, extensionPath),
|
|
908
|
+
),
|
|
909
|
+
);
|
|
778
910
|
const failedRoots = new Set(
|
|
779
911
|
missingExtensionRoots.concat(
|
|
780
|
-
|
|
912
|
+
extensionPaths.filter((root) =>
|
|
781
913
|
extensionErrors.some((error) =>
|
|
782
914
|
isPathWithinDirectory(root, error.path),
|
|
783
915
|
),
|
|
784
916
|
),
|
|
785
917
|
),
|
|
786
918
|
);
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
919
|
+
// An error inside a best-effort root is attributable to that root; any
|
|
920
|
+
// other error (including one no supplied root claims) stays fatal.
|
|
921
|
+
const fatalErrors = extensionErrors.filter(
|
|
922
|
+
(error) =>
|
|
923
|
+
![...bestEffortExtensionRoots].some((root) =>
|
|
924
|
+
isPathWithinDirectory(root, error.path),
|
|
925
|
+
),
|
|
926
|
+
);
|
|
927
|
+
const fatalRoots = [...failedRoots].filter(
|
|
928
|
+
(root) => !bestEffortExtensionRoots.has(root),
|
|
929
|
+
);
|
|
930
|
+
if (fatalErrors.length > 0 || fatalRoots.length > 0) {
|
|
931
|
+
const failureCount = Math.max(fatalRoots.length, fatalErrors.length);
|
|
932
|
+
const providerName =
|
|
933
|
+
options.modelProvider?.trim() || "the selected provider";
|
|
934
|
+
throw new Error(
|
|
935
|
+
`Failed to load ${failureCount} allowlisted provider extension(s) for ${providerName}; delegation stopped instead of running without the required integration.`,
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
const droppableRoots = [...failedRoots].filter((root) =>
|
|
939
|
+
bestEffortExtensionRoots.has(root),
|
|
792
940
|
);
|
|
941
|
+
if (droppableRoots.length === 0) break;
|
|
942
|
+
extensionPaths = extensionPaths.filter(
|
|
943
|
+
(root) => !droppableRoots.includes(root),
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Positive visibility for the invisible-by-design path: surviving
|
|
948
|
+
// best-effort roots each produced at least one loaded extension, and
|
|
949
|
+
// that changes subagent behavior without any user action — the one
|
|
950
|
+
// state worth a notice. Once per process per provider+root.
|
|
951
|
+
for (const root of extensionPaths) {
|
|
952
|
+
if (bestEffortExtensionRoots.has(root)) {
|
|
953
|
+
noticeProviderExtensionLoaded(options.modelProvider, root);
|
|
954
|
+
}
|
|
793
955
|
}
|
|
794
956
|
|
|
795
957
|
return {
|
|
@@ -840,6 +1002,7 @@ export function invalidateHostDepsCache(): void {
|
|
|
840
1002
|
hostDepsCacheGeneration++;
|
|
841
1003
|
hostDepsCache.clear();
|
|
842
1004
|
hostDepsInflight.clear();
|
|
1005
|
+
missingBestEffortNpmCache.clear();
|
|
843
1006
|
}
|
|
844
1007
|
|
|
845
1008
|
/** Test-only alias retained for existing test setup. */
|
|
@@ -860,6 +1023,7 @@ export function _setModelRuntimeFactoryForTesting(
|
|
|
860
1023
|
testModelRuntimeFactory = factory;
|
|
861
1024
|
hostDepsCache.clear();
|
|
862
1025
|
hostDepsInflight.clear();
|
|
1026
|
+
missingBestEffortNpmCache.clear();
|
|
863
1027
|
}
|
|
864
1028
|
|
|
865
1029
|
/**
|
|
@@ -883,6 +1047,7 @@ export function _setHostRetryBaseMsForTesting(
|
|
|
883
1047
|
// ensures newly-built ones come back unpatched.
|
|
884
1048
|
hostDepsCache.clear();
|
|
885
1049
|
hostDepsInflight.clear();
|
|
1050
|
+
missingBestEffortNpmCache.clear();
|
|
886
1051
|
return;
|
|
887
1052
|
}
|
|
888
1053
|
for (const deps of hostDepsCache.values()) {
|
package/manual.ts
CHANGED
|
@@ -39,7 +39,7 @@ function schemaTable(properties: Record<string, TSchema>): string {
|
|
|
39
39
|
export function getSubagentManualMarkdown(
|
|
40
40
|
agents: Map<string, AgentConfig>,
|
|
41
41
|
): string {
|
|
42
|
-
const entries = [...agents];
|
|
42
|
+
const entries = [...agents].filter(([, a]) => !a.builtin);
|
|
43
43
|
const agentList = entries.length
|
|
44
44
|
? entries
|
|
45
45
|
.map(([n, a]) => {
|
|
@@ -95,11 +95,14 @@ export function getSubagentManualMarkdown(
|
|
|
95
95
|
"- Git failures degrade to an empty diff.",
|
|
96
96
|
"- A path missing from `touched:` does **not** mean the file was unchanged. Delegate does not isolate file access or roll back writes.",
|
|
97
97
|
"",
|
|
98
|
-
"## Built-in
|
|
98
|
+
"## Built-in Agents",
|
|
99
99
|
"",
|
|
100
|
-
"- **default**: mirrors the live parent model, thinking level, delegatable native tools, and base
|
|
100
|
+
"- **default**: mirrors the live parent model, thinking level, delegatable native tools, and base prompt. It uses the shared workspace.",
|
|
101
|
+
"- **scout**: investigates without modifying files. Tools: `read`, `grep`, `find`, `ls`. Shared workspace.",
|
|
102
|
+
"- **coder**: implements and verifies changes. Tools: `read`, `write`, `edit`, `bash`. Shared workspace.",
|
|
103
|
+
'- **reviewer**: reviews the current snapshot and reports findings. Tools: `read`, `bash`. Defaults to a disposable scratch workspace; set `workspace: "shared"` for a persistent reviewer with `sessionId`.',
|
|
101
104
|
"",
|
|
102
|
-
"Parent extension/MCP tools are not copied. Parent-global `AGENTS.md` instructions are also excluded. Project-local context
|
|
105
|
+
"Fresh built-ins inherit the parent's exact model object and thinking level unless task fields or settings override them. Parent extension/MCP tools are not copied. Parent-global `AGENTS.md` instructions are also excluded. Project-local context and skills are rebuilt for the task's `cwd`; per-task fields remain explicit overrides.",
|
|
103
106
|
"",
|
|
104
107
|
"## Available Custom Agents",
|
|
105
108
|
"",
|
|
@@ -199,7 +202,7 @@ export function getSubagentManualMarkdown(
|
|
|
199
202
|
"- Dispatch validation is batch-wide and runs before spawning: one invalid task rejects the call without starting its siblings.",
|
|
200
203
|
"- `*` means read/write/edit/bash, not every tool. `grep`, `find`, and `ls` are valid explicit tools and are the `ro` preset.",
|
|
201
204
|
'- `tasks` is an array. The tool recovers common stringified calls for compatibility, but canonical calls use `{ tasks: [{ prompt: "..." }] }`.',
|
|
202
|
-
'- Use `agent: "default"` for the parent\'s live model/thinking/native tools/base prompt.
|
|
205
|
+
'- Use `agent: "default"` for the parent\'s live model/thinking/native tools/base prompt. Built-ins are `default`, `scout`, `coder`, and `reviewer`; omitting `agent` creates an ad-hoc task.',
|
|
203
206
|
"- An ad-hoc task with no `tools` uses `*`; a named custom task uses its profile; a profile with no tools uses `*`.",
|
|
204
207
|
"- Subagents inherit all skills discovered in their `cwd` (via AgentSession's resource loader). Per-task skill filtering is not supported — curate the cwd's skill set instead.",
|
|
205
208
|
`- Sync \`delegate\` runs at most ${getMaxConcurrent()} tasks at once (the rest queue, not fail). Use \`async: true\` to move work to the background.`,
|
|
@@ -211,7 +214,7 @@ export function getSubagentManualMarkdown(
|
|
|
211
214
|
"",
|
|
212
215
|
"## Config",
|
|
213
216
|
"",
|
|
214
|
-
"Tunables live in `~/.pi/agent/delegate.json`: `maxConcurrent` (sync ceiling), `maxAsyncTickets` (background ticket cap), `stallTimeoutMs` (inactivity watchdog; default 900000, 0 disables), per-model/per-provider concurrency limits, and
|
|
217
|
+
"Tunables live in `~/.pi/agent/delegate.json`: `maxConcurrent` (sync ceiling), `maxAsyncTickets` (background ticket cap), `stallTimeoutMs` (inactivity watchdog; default 900000, 0 disables), per-model/per-provider concurrency limits, and legacy custom-agent model overrides. Built-in model/thinking/tools overrides live in `settings.json`; `agentOverridesByParentModel` uses an exact `provider/model-id` key and project settings take precedence.",
|
|
215
218
|
"The inactivity watchdog requests cooperative `AgentSession.abort()` cancellation and waits for the subagent to become idle; it is not a hard wall-clock execution deadline.",
|
|
216
219
|
"",
|
|
217
220
|
`Output bounding: subagent outputs longer than ${OUTPUT_SPILL_THRESHOLD_CHARS} characters are spilled to a temp file, and only the last ${OUTPUT_SPILL_TAIL_CHARS} characters stay in the LLM-facing result. Adjust with \`output.spillThresholdChars\` and \`output.spillTailChars\`. Spill files are written to the system temp directory with owner-only permissions; the full output is always available in the expanded TUI view and the spilled file.`,
|
package/package.json
CHANGED
package/schema.ts
CHANGED
|
@@ -40,7 +40,7 @@ export const delegateTaskSchema = Type.Object({
|
|
|
40
40
|
agent: Type.Optional(
|
|
41
41
|
Type.String({
|
|
42
42
|
description:
|
|
43
|
-
"
|
|
43
|
+
"Built-ins: default, scout, coder, reviewer. Reviewer defaults to one-shot scratch. Omit for ad-hoc.",
|
|
44
44
|
}),
|
|
45
45
|
),
|
|
46
46
|
cwd: Type.Optional(
|
|
@@ -106,8 +106,7 @@ export const delegateTaskSchema = Type.Object({
|
|
|
106
106
|
workspace: Type.Optional(
|
|
107
107
|
StringEnum(["shared", "scratch"], {
|
|
108
108
|
description:
|
|
109
|
-
"
|
|
110
|
-
default: "shared",
|
|
109
|
+
"shared source; scratch disposable copy, one-shot; not security isolation. Reviewer=scratch; others=shared.",
|
|
111
110
|
}),
|
|
112
111
|
),
|
|
113
112
|
});
|
|
@@ -327,9 +326,10 @@ export function validateDelegateOperation(
|
|
|
327
326
|
}
|
|
328
327
|
if (
|
|
329
328
|
task.workspace === "scratch" &&
|
|
329
|
+
!task.agent &&
|
|
330
330
|
(task.sessionId || task.resumeFrom || sessionAction !== undefined)
|
|
331
331
|
) {
|
|
332
|
-
return `task ${index + 1}: workspace 'scratch' is one-shot and cannot be combined with sessionId, resumeFrom, or sessionAction.`;
|
|
332
|
+
return `task ${index + 1}: workspace 'scratch' is one-shot and cannot be combined with sessionId, resumeFrom, or sessionAction. Set workspace: "shared" to use a persistent agent.`;
|
|
333
333
|
}
|
|
334
334
|
if (sessionAction === "close") {
|
|
335
335
|
if (!task.sessionId) {
|