@signalridge/pi-subagents 1.8.0 → 1.9.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 +170 -0
- package/README.md +85 -55
- package/package.json +2 -2
- package/src/agent-manager.ts +235 -120
- package/src/agent-runner.ts +90 -72
- package/src/agent-tiers.ts +236 -70
- package/src/cross-extension-rpc.ts +6 -0
- package/src/default-agents.ts +3 -3
- package/src/index.ts +149 -139
- package/src/internal-run.ts +14 -0
- package/src/invocation-config.ts +24 -13
- package/src/model-resolver.ts +12 -0
- package/src/model-scope.ts +5 -2
- package/src/nested-tools.ts +35 -22
- package/src/schedule.ts +3 -0
- package/src/settings.ts +68 -247
- package/src/types.ts +10 -13
- package/src/ui/agent-display.ts +1 -1
- package/src/workflow-tiers.ts +0 -202
package/src/agent-runner.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
SessionManager,
|
|
19
19
|
SettingsManager,
|
|
20
20
|
} from "@earendil-works/pi-coding-agent";
|
|
21
|
-
import type { WorkflowTier } from "@signalridge/pi-subagents-protocol";
|
|
22
21
|
import { type AgentTierResolutionSnapshot, resolveAgentTier } from "./agent-tiers.js";
|
|
23
22
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
|
|
24
23
|
import { createAskGate } from "./ask-tools.js";
|
|
@@ -29,10 +28,12 @@ import { detectEnv } from "./env.js";
|
|
|
29
28
|
import { formatGateVerdict, type GateExec, runGate, workspaceFingerprint } from "./gate.js";
|
|
30
29
|
import {
|
|
31
30
|
INTERNAL_AGENT_CONFIG_OVERRIDE,
|
|
31
|
+
INTERNAL_PARENT_POLICY_SNAPSHOT,
|
|
32
32
|
type InternalAgentConfigOverride,
|
|
33
|
+
type ParentPolicySnapshot,
|
|
33
34
|
} from "./internal-run.js";
|
|
34
35
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
35
|
-
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
36
|
+
import { type ModelRegistry, resolveModel, shortModelLabel } from "./model-resolver.js";
|
|
36
37
|
import { checkModelScope } from "./model-scope.js";
|
|
37
38
|
import { createNestedSubagentTools, getMaxSubagentDepth, type NestedAgentManager } from "./nested-tools.js";
|
|
38
39
|
import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
|
|
@@ -40,8 +41,6 @@ import { shutdownAndDisposeSession } from "./session-lifecycle.js";
|
|
|
40
41
|
import { preloadSkills } from "./skill-loader.js";
|
|
41
42
|
import { createSupervisorTool } from "./supervisor.js";
|
|
42
43
|
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
43
|
-
import type { WorkflowTierResolutionSnapshot } from "./workflow-tiers.js";
|
|
44
|
-
import { resolveWorkflowTier } from "./workflow-tiers.js";
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
46
|
* Tool names registered by THIS extension. Single source of truth so the
|
|
@@ -507,14 +506,26 @@ export interface RunOptions {
|
|
|
507
506
|
isolated?: boolean;
|
|
508
507
|
inheritContext?: boolean;
|
|
509
508
|
thinkingLevel?: ThinkingLevel;
|
|
510
|
-
/** Semantic workflow tier; resolved here rather than by workflow callers. */
|
|
511
|
-
tier?: WorkflowTier;
|
|
512
509
|
/**
|
|
513
|
-
* User-named model tier
|
|
514
|
-
*
|
|
515
|
-
* the same precedence and the same fail-closed errors from one place.
|
|
510
|
+
* User-named model tier. Resolved here so the top-level Agent tool, nested
|
|
511
|
+
* delegation, the scheduler, cross-extension RPC and managed workflow calls
|
|
512
|
+
* all get the same precedence and the same fail-closed errors from one place.
|
|
516
513
|
*/
|
|
517
514
|
agentTier?: string;
|
|
515
|
+
/**
|
|
516
|
+
* Refuse the implicit `defaultModel`-then-parent fallback when no tier
|
|
517
|
+
* applies, and reach the shipped fallback tier instead.
|
|
518
|
+
*
|
|
519
|
+
* Managed workflow calls set this so that every managed spawn carries a named
|
|
520
|
+
* policy with a durable resolution snapshot, rather than an unnamed model
|
|
521
|
+
* nobody chose for it. Note what this does *not* do: the shipped fallback is
|
|
522
|
+
* `medium`, whose profile inherits, so on an unconfigured host the model is
|
|
523
|
+
* still the parent session's. What the flag buys is that the choice has a
|
|
524
|
+
* name, a recorded thinking level, and a scope check — and that a user who
|
|
525
|
+
* sets `agentTiers.defaultTier` to "none" gets a hard refusal here instead of
|
|
526
|
+
* silent inheritance.
|
|
527
|
+
*/
|
|
528
|
+
requireAgentTier?: boolean;
|
|
518
529
|
/** Optional toolset hint forwarded by managed workflow callers. */
|
|
519
530
|
toolset?: string;
|
|
520
531
|
/** Additional tool names denied by the caller, merged with agent frontmatter. */
|
|
@@ -546,10 +557,15 @@ export interface RunOptions {
|
|
|
546
557
|
/** Called on streaming text deltas from the assistant response. */
|
|
547
558
|
onTextDelta?: (delta: string, fullText: string) => void;
|
|
548
559
|
onSessionCreated?: (session: AgentSession) => void;
|
|
549
|
-
/**
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
560
|
+
/**
|
|
561
|
+
* Called after pi-subagents resolves a user-named agent tier.
|
|
562
|
+
*
|
|
563
|
+
* `modelLabel` is the short display name of the model the tier pinned, and is
|
|
564
|
+
* absent when the profile inherits. It rides along rather than living on the
|
|
565
|
+
* snapshot because the snapshot is a durable policy record that a managed
|
|
566
|
+
* tombstone persists and revalidates; a cosmetic label does not belong in it.
|
|
567
|
+
*/
|
|
568
|
+
onAgentTierResolved?: (snapshot: AgentTierResolutionSnapshot, modelLabel?: string) => void;
|
|
553
569
|
/** Called at the end of each agentic turn with the cumulative count. */
|
|
554
570
|
onTurnEnd?: (turnCount: number) => void;
|
|
555
571
|
/**
|
|
@@ -568,6 +584,8 @@ export interface RunOptions {
|
|
|
568
584
|
* JSON/public spawn field; the generation wizard is the only issuer.
|
|
569
585
|
*/
|
|
570
586
|
readonly [INTERNAL_AGENT_CONFIG_OVERRIDE]?: InternalAgentConfigOverride;
|
|
587
|
+
/** Parent model/thinking captured when a managed spawn was allocated. */
|
|
588
|
+
readonly [INTERNAL_PARENT_POLICY_SNAPSHOT]?: ParentPolicySnapshot;
|
|
571
589
|
/**
|
|
572
590
|
* Reopen an existing conversation from this session file instead of starting
|
|
573
591
|
* a new one. Package-internal: the only issuer is the `@handle` mention
|
|
@@ -765,41 +783,49 @@ export async function runAgent(
|
|
|
765
783
|
inheritContext: internalOverride.inheritContext,
|
|
766
784
|
}
|
|
767
785
|
: loadedAgentConfig;
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
thinkingOverride: options.thinkingLevel,
|
|
782
|
-
parentModel: ctx.model,
|
|
783
|
-
parentThinking,
|
|
784
|
-
modelRegistry: ctx.modelRegistry,
|
|
785
|
-
})
|
|
786
|
-
: undefined;
|
|
787
|
-
if (tierResolution?.snapshot) options.onTierResolved?.(tierResolution.snapshot);
|
|
788
|
-
|
|
789
|
-
// Agent tiers resolve in the same place and before the same await, so every
|
|
790
|
-
// spawn path shares one precedence and one set of fail-closed errors. A tier
|
|
791
|
-
// that applies decides model and thinking outright: it is current policy,
|
|
792
|
-
// while an agent's legacy `model:`/`thinking:` frontmatter is the older, weaker
|
|
793
|
-
// statement of the same thing. Throwing here refuses the spawn rather than
|
|
794
|
-
// quietly running a model the caller did not choose.
|
|
786
|
+
const parentPolicySnapshot = options[INTERNAL_PARENT_POLICY_SNAPSHOT];
|
|
787
|
+
const parentModel = parentPolicySnapshot ? parentPolicySnapshot.model : ctx.model;
|
|
788
|
+
const parentThinking = parentPolicySnapshot
|
|
789
|
+
? parentPolicySnapshot.thinking
|
|
790
|
+
: options.parentThinking ?? (() => {
|
|
791
|
+
const level = options.pi.getThinkingLevel?.();
|
|
792
|
+
return level === "minimal" || level === "low" || level === "medium" || level === "high" || level === "xhigh" || level === "max"
|
|
793
|
+
? level
|
|
794
|
+
: undefined;
|
|
795
|
+
})();
|
|
796
|
+
// One resolver, one precedence, one set of fail-closed errors, for every
|
|
797
|
+
// spawn path. It runs before the first await so a managed caller can persist
|
|
798
|
+
// the immutable snapshot before any provider work begins.
|
|
795
799
|
const agentTierResolution = resolveAgentTier({
|
|
796
800
|
requestedTier: options.agentTier,
|
|
801
|
+
// The same flag that forbids the parent fallback below is what lets the
|
|
802
|
+
// resolver reach its shipped fallback, so the two can never disagree about
|
|
803
|
+
// whether this spawn has a tier.
|
|
804
|
+
requireTier: options.requireAgentTier === true,
|
|
797
805
|
agentConfig,
|
|
798
|
-
parentModel
|
|
806
|
+
parentModel,
|
|
799
807
|
parentThinking,
|
|
800
808
|
modelRegistry: ctx.modelRegistry,
|
|
801
809
|
});
|
|
802
|
-
|
|
810
|
+
const agentTierSnapshot = agentTierResolution.snapshot;
|
|
811
|
+
if (options.requireAgentTier === true && !agentTierSnapshot) {
|
|
812
|
+
throw new Error(
|
|
813
|
+
`No agent tier selected for "${type}". A managed workflow call must name a tier, ` +
|
|
814
|
+
"the agent must declare one, or agentTiers must offer a default; " +
|
|
815
|
+
"inheriting the parent session's model is not a policy this call can fall back to.",
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
if (agentTierSnapshot) {
|
|
819
|
+
// A tier owns model resolution, so it owns the label the UI shows for the
|
|
820
|
+
// spawn: no caller can compute one, because none of them resolved the
|
|
821
|
+
// model. A profile that inherits pinned nothing, so it gets no label — the
|
|
822
|
+
// agent is running the parent's model, which the UI shows by omission.
|
|
823
|
+
const tierModelLabel =
|
|
824
|
+
agentTierSnapshot.configuredModel === "inherit" || !agentTierResolution.model
|
|
825
|
+
? undefined
|
|
826
|
+
: shortModelLabel(agentTierResolution.model);
|
|
827
|
+
options.onAgentTierResolved?.(agentTierSnapshot, tierModelLabel);
|
|
828
|
+
}
|
|
803
829
|
|
|
804
830
|
// Resolve working directory: worktree override > parent cwd
|
|
805
831
|
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
@@ -1021,26 +1047,24 @@ export async function runAgent(
|
|
|
1021
1047
|
}
|
|
1022
1048
|
}
|
|
1023
1049
|
|
|
1024
|
-
//
|
|
1025
|
-
//
|
|
1026
|
-
const model =
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
const thinkingLevel =
|
|
1050
|
+
// A resolved tier owns both values. In particular, a pre-resolved parent or
|
|
1051
|
+
// legacy model/thinking option from a caller cannot bypass it.
|
|
1052
|
+
const model = agentTierSnapshot
|
|
1053
|
+
? agentTierResolution.model
|
|
1054
|
+
: options.model ?? resolveDefaultModel(parentModel, ctx.modelRegistry, agentConfig?.model);
|
|
1055
|
+
const thinkingLevel = agentTierSnapshot
|
|
1030
1056
|
? agentTierResolution.thinkingLevel
|
|
1031
|
-
: options.
|
|
1032
|
-
? tierResolution?.thinkingLevel
|
|
1033
|
-
: options.thinkingLevel ?? agentConfig?.thinking;
|
|
1057
|
+
: options.thinkingLevel ?? agentConfig?.thinking;
|
|
1034
1058
|
|
|
1035
|
-
if (
|
|
1036
|
-
const { configuredModel, source } =
|
|
1059
|
+
if (agentTierSnapshot) {
|
|
1060
|
+
const { configuredModel, source } = agentTierSnapshot;
|
|
1037
1061
|
const scopeVerdict = checkModelScope({
|
|
1038
1062
|
model,
|
|
1039
1063
|
cwd: ctx.cwd,
|
|
1040
1064
|
modelRegistry: ctx.modelRegistry,
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1043
|
-
//
|
|
1065
|
+
// scopeModels polices a tier chosen per dispatch, by the host model or by
|
|
1066
|
+
// a workflow script alike; a tier from the agent file or the configured
|
|
1067
|
+
// default is standing config and only warns.
|
|
1044
1068
|
callerSupplied: source === "call",
|
|
1045
1069
|
agentLabel: agentConfig?.displayName ?? type,
|
|
1046
1070
|
modelInput: configuredModel === "inherit" ? undefined : configuredModel,
|
|
@@ -1048,20 +1072,6 @@ export async function runAgent(
|
|
|
1048
1072
|
if (scopeVerdict.kind === "error") throw new Error(scopeVerdict.message);
|
|
1049
1073
|
if (scopeVerdict.kind === "warn" && ctx.hasUI) ctx.ui.notify(scopeVerdict.message, "warning");
|
|
1050
1074
|
}
|
|
1051
|
-
|
|
1052
|
-
if (options.tier) {
|
|
1053
|
-
const configuredModel = tierResolution?.snapshot?.configuredModel;
|
|
1054
|
-
const scopeVerdict = checkModelScope({
|
|
1055
|
-
model,
|
|
1056
|
-
cwd: ctx.cwd,
|
|
1057
|
-
modelRegistry: ctx.modelRegistry,
|
|
1058
|
-
callerSupplied: false,
|
|
1059
|
-
agentLabel: agentConfig?.displayName ?? type,
|
|
1060
|
-
modelInput: configuredModel === "inherit" ? undefined : configuredModel,
|
|
1061
|
-
});
|
|
1062
|
-
if (scopeVerdict.kind === "error") throw new Error(scopeVerdict.message);
|
|
1063
|
-
if (scopeVerdict.kind === "warn" && ctx.hasUI) ctx.ui.notify(scopeVerdict.message, "warning");
|
|
1064
|
-
}
|
|
1065
1075
|
const disallowedSet = (() => {
|
|
1066
1076
|
const names = [...(agentConfig?.disallowedTools ?? []), ...(options.excludeTools ?? [])];
|
|
1067
1077
|
return names.length > 0 ? new Set(names) : undefined;
|
|
@@ -1373,7 +1383,11 @@ export async function runAgent(
|
|
|
1373
1383
|
pendingToolTimeouts.delete(toolCallId);
|
|
1374
1384
|
try {
|
|
1375
1385
|
session.abort();
|
|
1376
|
-
} catch {
|
|
1386
|
+
} catch {
|
|
1387
|
+
// The session may already be torn down by the time this fires; the
|
|
1388
|
+
// timeout's only job is to stop a hung tool, and there is nothing
|
|
1389
|
+
// left to stop.
|
|
1390
|
+
}
|
|
1377
1391
|
}, defaultToolTimeoutMs);
|
|
1378
1392
|
handle.unref?.();
|
|
1379
1393
|
pendingToolTimeouts.set(toolCallId, handle);
|
|
@@ -1528,7 +1542,11 @@ export async function resumeAgent(
|
|
|
1528
1542
|
resumePendingTimeouts.delete(id);
|
|
1529
1543
|
try {
|
|
1530
1544
|
session.abort();
|
|
1531
|
-
} catch {
|
|
1545
|
+
} catch {
|
|
1546
|
+
// The session may already be torn down by the time this fires; the
|
|
1547
|
+
// timeout's only job is to stop a hung tool, and there is nothing
|
|
1548
|
+
// left to stop.
|
|
1549
|
+
}
|
|
1532
1550
|
}, defaultToolTimeoutMs);
|
|
1533
1551
|
handle.unref?.();
|
|
1534
1552
|
resumePendingTimeouts.set(id, handle);
|