@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/index.ts
CHANGED
|
@@ -86,17 +86,22 @@ import {
|
|
|
86
86
|
buildAgentTierListText,
|
|
87
87
|
buildAgentTierParameterDescription,
|
|
88
88
|
buildCompactAgentTierListText,
|
|
89
|
+
type DefaultAgentTierSelection,
|
|
89
90
|
findUnknownAgentTierReferences,
|
|
90
91
|
getAgentTiersConfiguredSettings,
|
|
91
92
|
getAgentTiersSettings,
|
|
93
|
+
getDefaultAgentTierSelection,
|
|
92
94
|
getDefaultAgentTierText,
|
|
95
|
+
getRoutingPolicySnapshot,
|
|
93
96
|
isValidAgentTierKey,
|
|
94
97
|
listAgentTierKeys,
|
|
95
98
|
MAX_AGENT_TIER_KEY_LENGTH,
|
|
96
99
|
offerableTierThinking,
|
|
97
100
|
removeAgentTierProfile,
|
|
101
|
+
selectAgentTier,
|
|
98
102
|
setAgentTiersSettings,
|
|
99
103
|
setDefaultAgentTier,
|
|
104
|
+
shippedFallbackAgentTier,
|
|
100
105
|
upsertAgentTierProfile,
|
|
101
106
|
} from "./agent-tiers.js";
|
|
102
107
|
import {
|
|
@@ -139,7 +144,7 @@ import {
|
|
|
139
144
|
stripAgentPrefix,
|
|
140
145
|
} from "./mention.js";
|
|
141
146
|
import { runMentionClone } from "./mention-clone.js";
|
|
142
|
-
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
147
|
+
import { type ModelRegistry, resolveModel, shortModelLabel } from "./model-resolver.js";
|
|
143
148
|
import {
|
|
144
149
|
checkModelScope,
|
|
145
150
|
isScopeModelsEnabled,
|
|
@@ -222,7 +227,6 @@ import {
|
|
|
222
227
|
getSessionContextPercent,
|
|
223
228
|
type LifetimeUsage,
|
|
224
229
|
} from "./usage.js";
|
|
225
|
-
import { getWorkflowSettings, resolveWorkflowTier, setWorkflowSettings } from "./workflow-tiers.js";
|
|
226
230
|
import { isWorktreeIsolationEnabled, setWorktreeIsolationEnabled } from "./worktree.js";
|
|
227
231
|
|
|
228
232
|
// ---- Shared helpers ----
|
|
@@ -800,6 +804,8 @@ function activateRootRuntime(
|
|
|
800
804
|
durationMs,
|
|
801
805
|
tokens,
|
|
802
806
|
...(record.outputFile ? { outputFile: record.outputFile } : {}),
|
|
807
|
+
...(record.invocation?.agentTier === undefined ? {} : { tier: record.invocation.agentTier }),
|
|
808
|
+
...(record.invocation?.agentTierSnapshot ? { tierSnapshot: record.invocation.agentTierSnapshot } : {}),
|
|
803
809
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
804
810
|
};
|
|
805
811
|
}
|
|
@@ -836,6 +842,8 @@ function activateRootRuntime(
|
|
|
836
842
|
...(terminal?.tokenCount
|
|
837
843
|
? { tokens: { total: terminal.tokenCount } }
|
|
838
844
|
: {}),
|
|
845
|
+
...(tombstone.tier === undefined ? {} : { tier: tombstone.tier }),
|
|
846
|
+
...(tombstone.tierSnapshot ? { tierSnapshot: tombstone.tierSnapshot } : {}),
|
|
839
847
|
owner: snapshotOwner(tombstone.owner),
|
|
840
848
|
};
|
|
841
849
|
}
|
|
@@ -859,6 +867,8 @@ function activateRootRuntime(
|
|
|
859
867
|
compactionCount: terminal.compactionCount,
|
|
860
868
|
...(terminal.outputFile ? { outputFile: terminal.outputFile } : {}),
|
|
861
869
|
...(terminal.tokenCount ? { tokens: terminal.tokenCount } : {}),
|
|
870
|
+
...(tombstone.tier === undefined ? {} : { tier: tombstone.tier }),
|
|
871
|
+
...(tombstone.tierSnapshot ? { tierSnapshot: tombstone.tierSnapshot } : {}),
|
|
862
872
|
owner: snapshotOwner(tombstone.owner),
|
|
863
873
|
});
|
|
864
874
|
}
|
|
@@ -924,6 +934,8 @@ function activateRootRuntime(
|
|
|
924
934
|
...(record.lifetimeUsage.input + record.lifetimeUsage.output > 0
|
|
925
935
|
? { tokens: record.lifetimeUsage.input + record.lifetimeUsage.output }
|
|
926
936
|
: {}),
|
|
937
|
+
...(record.invocation?.agentTier === undefined ? {} : { tier: record.invocation.agentTier }),
|
|
938
|
+
...(record.invocation?.agentTierSnapshot ? { tierSnapshot: record.invocation.agentTierSnapshot } : {}),
|
|
927
939
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
928
940
|
});
|
|
929
941
|
|
|
@@ -968,6 +980,7 @@ function activateRootRuntime(
|
|
|
968
980
|
id: record.id,
|
|
969
981
|
type: record.type,
|
|
970
982
|
description: record.description,
|
|
983
|
+
...(record.invocation?.agentTier === undefined ? {} : { tier: record.invocation.agentTier }),
|
|
971
984
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
972
985
|
});
|
|
973
986
|
},
|
|
@@ -981,6 +994,8 @@ function activateRootRuntime(
|
|
|
981
994
|
reason: info.reason,
|
|
982
995
|
tokensBefore: info.tokensBefore,
|
|
983
996
|
compactionCount: record.compactionCount,
|
|
997
|
+
...(record.invocation?.agentTier === undefined ? {} : { tier: record.invocation.agentTier }),
|
|
998
|
+
...(record.invocation?.agentTierSnapshot ? { tierSnapshot: record.invocation.agentTierSnapshot } : {}),
|
|
984
999
|
...(record.owner ? { owner: snapshotOwner(record.owner) } : {}),
|
|
985
1000
|
});
|
|
986
1001
|
},
|
|
@@ -994,6 +1009,7 @@ function activateRootRuntime(
|
|
|
994
1009
|
type: record.type,
|
|
995
1010
|
description: record.description,
|
|
996
1011
|
isBackground: record.isBackground,
|
|
1012
|
+
...(record.invocation?.agentTier === undefined ? {} : { tier: record.invocation.agentTier }),
|
|
997
1013
|
owner: snapshotOwner(record.owner),
|
|
998
1014
|
});
|
|
999
1015
|
},
|
|
@@ -1224,6 +1240,7 @@ function activateRootRuntime(
|
|
|
1224
1240
|
pi.events.emit("subagents:ready", {
|
|
1225
1241
|
version: PROTOCOL_VERSION,
|
|
1226
1242
|
capabilities: PROTOCOL_CAPABILITIES,
|
|
1243
|
+
routingPolicy: getRoutingPolicySnapshot(),
|
|
1227
1244
|
});
|
|
1228
1245
|
}
|
|
1229
1246
|
// `@` completion for agent handles. Registered at most ONCE for the
|
|
@@ -1422,14 +1439,6 @@ function activateRootRuntime(
|
|
|
1422
1439
|
fleet.ensureTimer();
|
|
1423
1440
|
}
|
|
1424
1441
|
|
|
1425
|
-
const splitManagedModelSpec = (spec: string): { model: string; thinking?: "minimal" | "low" | "medium" | "high" | "xhigh" | "max" } => {
|
|
1426
|
-
const separator = spec.lastIndexOf(":");
|
|
1427
|
-
const suffix = separator > spec.indexOf("/") ? spec.slice(separator + 1) : "";
|
|
1428
|
-
const thinking = ["minimal", "low", "medium", "high", "xhigh", "max"].includes(suffix)
|
|
1429
|
-
? (suffix as "minimal" | "low" | "medium" | "high" | "xhigh" | "max")
|
|
1430
|
-
: undefined;
|
|
1431
|
-
return thinking ? { model: spec.slice(0, separator), thinking } : { model: spec };
|
|
1432
|
-
};
|
|
1433
1442
|
/**
|
|
1434
1443
|
* Add the same activity, transcript, and FleetView wiring used by Agent-tool
|
|
1435
1444
|
* background runs without expanding the managed RPC's policy surface.
|
|
@@ -1446,90 +1455,55 @@ function activateRootRuntime(
|
|
|
1446
1455
|
reloadCustomAgents();
|
|
1447
1456
|
const dispatch = resolveSpawnType(request.type);
|
|
1448
1457
|
if (!dispatch.ok) throw new Error(dispatch.message);
|
|
1449
|
-
const workflowSettings = getWorkflowSettings();
|
|
1450
|
-
if (request.tier === undefined && workflowSettings.blockedDefaultTier) {
|
|
1451
|
-
throw new Error(
|
|
1452
|
-
"workflow defaultTier is blocked by malformed configuration",
|
|
1453
|
-
);
|
|
1454
|
-
}
|
|
1455
|
-
const effectiveTier = request.tier ?? workflowSettings.defaultTier;
|
|
1456
|
-
const normalizedRequest = {
|
|
1457
|
-
...request,
|
|
1458
|
-
type: dispatch.type,
|
|
1459
|
-
...(effectiveTier === undefined ? {} : { tier: effectiveTier }),
|
|
1460
|
-
};
|
|
1461
1458
|
const customConfig = getAgentConfig(dispatch.type);
|
|
1462
|
-
const
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1459
|
+
const agentTiers = getAgentTiersSettings();
|
|
1460
|
+
// Materialize only the *identity* of the tier this call will land on, so a
|
|
1461
|
+
// tombstone and the lifecycle events carry it before runAgent's resolution
|
|
1462
|
+
// callback fires. runAgent stays authoritative for source, model, thinking,
|
|
1463
|
+
// and the snapshot — this is a label, not a decision.
|
|
1464
|
+
//
|
|
1465
|
+
// Asked, not restated: the same `selectAgentTier` the runner uses, with the
|
|
1466
|
+
// same `requireTier` the manager will set, so the label cannot name a tier
|
|
1467
|
+
// other than the one that runs. A fail-closed condition throws here; the
|
|
1468
|
+
// runner owns that refusal and its message, so the label is simply absent
|
|
1469
|
+
// and the spawn fails there with the diagnostic that names the cause.
|
|
1470
|
+
const effectiveTier = (() => {
|
|
1471
|
+
try {
|
|
1472
|
+
return selectAgentTier(
|
|
1473
|
+
{ requestedTier: request.tier, requireTier: true, agentConfig: customConfig },
|
|
1474
|
+
agentTiers,
|
|
1475
|
+
)?.tier;
|
|
1476
|
+
} catch {
|
|
1477
|
+
return undefined;
|
|
1478
|
+
}
|
|
1472
1479
|
})();
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
const resolvedRequestedModel = requestedModel
|
|
1485
|
-
? resolveModel(requestedModel.model, ctxRef.modelRegistry)
|
|
1486
|
-
: undefined;
|
|
1487
|
-
if (typeof resolvedRequestedModel === "string") throw new Error(resolvedRequestedModel);
|
|
1488
|
-
const modelInput = requestedModel?.model ?? resolvedConfig.modelInput;
|
|
1489
|
-
// An exact managed model is still checked by pi-subagents' model-scope policy;
|
|
1490
|
-
// workflows never bypass the host's enabled-model restrictions.
|
|
1491
|
-
if (effectiveTier === undefined || requestedModel !== undefined) {
|
|
1492
|
-
const model = resolvedRequestedModel ?? resolveConfiguredDefaultModel(ctxRef.modelRegistry) ?? ctxRef.model;
|
|
1493
|
-
const scopeVerdict = checkModelScope({
|
|
1494
|
-
model,
|
|
1495
|
-
cwd: ctxRef.cwd,
|
|
1496
|
-
modelRegistry: ctxRef.modelRegistry,
|
|
1497
|
-
callerSupplied: requestedModel !== undefined,
|
|
1498
|
-
agentLabel: customConfig?.displayName ?? dispatch.type,
|
|
1499
|
-
modelInput,
|
|
1500
|
-
});
|
|
1501
|
-
if (scopeVerdict.kind === "error") throw new Error(scopeVerdict.message);
|
|
1502
|
-
if (scopeVerdict.kind === "warn" && ctxRef.hasUI) ctxRef.ui.notify(scopeVerdict.message, "warning");
|
|
1503
|
-
}
|
|
1480
|
+
// `request.tier` already rides the spread; the only field this changes is
|
|
1481
|
+
// the agent type, which `resolveSpawnType` may have redirected.
|
|
1482
|
+
const normalizedRequest = { ...request, type: dispatch.type };
|
|
1483
|
+
// Only the non-tier fields are read here. The manager marks every managed
|
|
1484
|
+
// spawn `requireAgentTier`, so a tier always owns model and thinking; asking
|
|
1485
|
+
// `resolveAgentInvocationConfig` to decide that again would be work whose
|
|
1486
|
+
// answer this path discards.
|
|
1487
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, {});
|
|
1488
|
+
// Model and thinking are resolved only by the runner's tier path. There is
|
|
1489
|
+
// no second selector on this wire, so nothing here can silently win or be
|
|
1490
|
+
// silently dropped; a call with no tier and no default fails closed.
|
|
1504
1491
|
const effectiveMaxTurns = normalizeMaxTurns(
|
|
1505
1492
|
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
1506
1493
|
);
|
|
1507
1494
|
const effectiveIsolation = request.isolation ?? resolvedConfig.isolation;
|
|
1508
|
-
const configuredModel =
|
|
1509
|
-
resolvedConfig.modelInput && resolvedConfig.modelInput !== "inherit"
|
|
1510
|
-
? resolveModel(resolvedConfig.modelInput, ctxRef.modelRegistry)
|
|
1511
|
-
: undefined;
|
|
1512
|
-
const effectiveModel =
|
|
1513
|
-
resolvedRequestedModel ??
|
|
1514
|
-
tierResolution?.model ??
|
|
1515
|
-
(typeof configuredModel === "string" ? undefined : configuredModel) ??
|
|
1516
|
-
(effectiveTier === undefined ? resolveConfiguredDefaultModel(ctxRef.modelRegistry) ?? ctxRef.model : undefined);
|
|
1517
1495
|
const effectiveExcludeTools = [...new Set([...(customConfig?.disallowedTools ?? []), ...(request.excludeTools ?? [])])];
|
|
1518
1496
|
const managedPolicy: ManagedSpawnPolicy = {
|
|
1519
|
-
// The managed request may select an exact model, but resolution and scope
|
|
1520
|
-
// checks above remain owned by pi-subagents.
|
|
1521
|
-
model: effectiveModel,
|
|
1522
1497
|
maxTurns: effectiveMaxTurns,
|
|
1523
1498
|
isolated: resolvedConfig.isolated,
|
|
1524
1499
|
inheritContext: resolvedConfig.inheritContext,
|
|
1525
|
-
thinkingLevel: (tierResolution?.thinkingLevel ?? requestedThinking ?? resolvedConfig.thinking ?? parentThinking ?? undefined) as "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | undefined,
|
|
1526
1500
|
isolation: effectiveIsolation,
|
|
1527
1501
|
toolset: request.toolset,
|
|
1528
1502
|
excludeTools: effectiveExcludeTools,
|
|
1529
1503
|
policyFingerprint: JSON.stringify(customConfig ?? null),
|
|
1530
1504
|
rootSessionId: ctxRef.sessionManager.getSessionId(),
|
|
1531
1505
|
invocation: {
|
|
1532
|
-
|
|
1506
|
+
...(effectiveTier === undefined ? {} : { agentTier: effectiveTier }),
|
|
1533
1507
|
maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
|
|
1534
1508
|
isolated: resolvedConfig.isolated,
|
|
1535
1509
|
inheritContext: resolvedConfig.inheritContext,
|
|
@@ -1901,7 +1875,6 @@ function activateRootRuntime(
|
|
|
1901
1875
|
manager.setMaxSubagentSpawnsPerBranch(n),
|
|
1902
1876
|
setFallbackSubagent,
|
|
1903
1877
|
setWorktreeIsolation: setWorktreeIsolationEnabled,
|
|
1904
|
-
setWorkflow: setWorkflowSettings,
|
|
1905
1878
|
setAgentTiers: setAgentTiersSettings,
|
|
1906
1879
|
});
|
|
1907
1880
|
pi.events.emit("subagents:settings_loaded", { settings: startupSettings });
|
|
@@ -2551,18 +2524,18 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2551
2524
|
// Get agent config (if any)
|
|
2552
2525
|
const customConfig = getAgentConfig(subagentType);
|
|
2553
2526
|
|
|
2554
|
-
const resolvedConfig = resolveAgentInvocationConfig(
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
);
|
|
2527
|
+
const resolvedConfig = resolveAgentInvocationConfig(customConfig, {
|
|
2528
|
+
...params,
|
|
2529
|
+
agentTiers: getAgentTiersSettings(),
|
|
2530
|
+
});
|
|
2558
2531
|
|
|
2559
|
-
//
|
|
2560
|
-
//
|
|
2561
|
-
//
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
resolveConfiguredDefaultModel(ctx.modelRegistry) ?? ctx.model;
|
|
2565
|
-
if (resolvedConfig.modelInput) {
|
|
2532
|
+
// A selected Agent tier owns final model/thinking resolution. Keep both
|
|
2533
|
+
// fields unset here so runAgent is the only resolver and ordinary Agent
|
|
2534
|
+
// calls cannot accidentally bypass the profile with a parent/default pin.
|
|
2535
|
+
let model = resolvedConfig.agentTierSelected
|
|
2536
|
+
? undefined
|
|
2537
|
+
: resolveConfiguredDefaultModel(ctx.modelRegistry) ?? ctx.model;
|
|
2538
|
+
if (!resolvedConfig.agentTierSelected && resolvedConfig.modelInput) {
|
|
2566
2539
|
const resolved = resolveModel(
|
|
2567
2540
|
resolvedConfig.modelInput,
|
|
2568
2541
|
ctx.modelRegistry,
|
|
@@ -2575,21 +2548,22 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2575
2548
|
}
|
|
2576
2549
|
}
|
|
2577
2550
|
|
|
2578
|
-
//
|
|
2579
|
-
//
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2551
|
+
// Tiered model scope is checked after the single final resolution in
|
|
2552
|
+
// runAgent. Only the legacy no-tier path is checked here.
|
|
2553
|
+
if (!resolvedConfig.agentTierSelected) {
|
|
2554
|
+
const scopeVerdict = checkModelScope({
|
|
2555
|
+
model,
|
|
2556
|
+
cwd: ctx.cwd,
|
|
2557
|
+
modelRegistry: ctx.modelRegistry,
|
|
2558
|
+
callerSupplied: resolvedConfig.modelFromParams,
|
|
2559
|
+
agentLabel: customConfig?.displayName ?? subagentType,
|
|
2560
|
+
modelInput: resolvedConfig.modelInput,
|
|
2561
|
+
});
|
|
2562
|
+
if (scopeVerdict.kind === "error")
|
|
2563
|
+
return textResult(scopeVerdict.message);
|
|
2564
|
+
if (scopeVerdict.kind === "warn")
|
|
2565
|
+
ctx.ui.notify(scopeVerdict.message, "warning");
|
|
2566
|
+
}
|
|
2593
2567
|
|
|
2594
2568
|
const thinking = resolvedConfig.thinking;
|
|
2595
2569
|
const inheritContext = resolvedConfig.inheritContext;
|
|
@@ -2616,19 +2590,20 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2616
2590
|
writeInitialEntry(rec.outputFile, agentId, params.prompt, ctx.cwd);
|
|
2617
2591
|
};
|
|
2618
2592
|
|
|
2593
|
+
// Untiered spawns only. A tier resolves its model inside runAgent, and
|
|
2594
|
+
// its resolution callback supplies the label from there — computing one
|
|
2595
|
+
// here would name a model this path never resolved.
|
|
2619
2596
|
const parentModelId = ctx.model?.id;
|
|
2620
|
-
const effectiveModelId = model?.id;
|
|
2621
2597
|
const modelName =
|
|
2622
|
-
|
|
2623
|
-
? (model?.name ?? effectiveModelId)
|
|
2624
|
-
.replace(/^Claude\s+/i, "")
|
|
2625
|
-
.toLowerCase()
|
|
2626
|
-
: undefined;
|
|
2598
|
+
model && model.id !== parentModelId ? shortModelLabel(model) : undefined;
|
|
2627
2599
|
const effectiveMaxTurns = normalizeMaxTurns(
|
|
2628
2600
|
resolvedConfig.maxTurns ?? getDefaultMaxTurns(),
|
|
2629
2601
|
);
|
|
2630
2602
|
const agentInvocation: AgentInvocation = {
|
|
2631
2603
|
modelName,
|
|
2604
|
+
...((resolvedConfig.requestedAgentTier ?? customConfig?.agentTier) === undefined
|
|
2605
|
+
? {}
|
|
2606
|
+
: { agentTier: resolvedConfig.requestedAgentTier ?? customConfig?.agentTier }),
|
|
2632
2607
|
thinking,
|
|
2633
2608
|
// Explicit value only — the default fallback would just add noise.
|
|
2634
2609
|
// Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
|
|
@@ -2688,10 +2663,16 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2688
2663
|
// at fire time, and the original is what a user edits.
|
|
2689
2664
|
subagent_type: requestedType,
|
|
2690
2665
|
prompt: params.prompt as string,
|
|
2666
|
+
// Only an explicitly requested tier is frozen into the job. An
|
|
2667
|
+
// agent's frontmatter tier is deliberately not copied: it is read
|
|
2668
|
+
// again at fire time, so editing the agent file still takes
|
|
2669
|
+
// effect, and it keeps its "frontmatter" source rather than being
|
|
2670
|
+
// replayed as a caller choice.
|
|
2671
|
+
tier: resolvedConfig.requestedAgentTier,
|
|
2691
2672
|
// Store the resolved policy input (agent config first, tool params second)
|
|
2692
2673
|
// so scheduled fires use the same model fallback as an immediate spawn.
|
|
2693
|
-
model: resolvedConfig.modelInput,
|
|
2694
|
-
thinking: thinking,
|
|
2674
|
+
model: resolvedConfig.agentTierSelected ? undefined : resolvedConfig.modelInput,
|
|
2675
|
+
thinking: resolvedConfig.agentTierSelected ? undefined : thinking,
|
|
2695
2676
|
max_turns: effectiveMaxTurns,
|
|
2696
2677
|
isolated: isolated,
|
|
2697
2678
|
isolation: isolation,
|
|
@@ -2844,11 +2825,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2844
2825
|
// call as failed instead of returning a successful-looking text result.
|
|
2845
2826
|
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
2846
2827
|
description: params.description,
|
|
2847
|
-
model,
|
|
2828
|
+
model: resolvedConfig.agentTierSelected ? undefined : model,
|
|
2848
2829
|
maxTurns: effectiveMaxTurns,
|
|
2849
2830
|
isolated,
|
|
2850
2831
|
inheritContext,
|
|
2851
|
-
thinkingLevel: thinking,
|
|
2832
|
+
thinkingLevel: resolvedConfig.agentTierSelected ? undefined : thinking,
|
|
2852
2833
|
agentTier: resolvedConfig.requestedAgentTier,
|
|
2853
2834
|
isBackground: true,
|
|
2854
2835
|
isolation,
|
|
@@ -2994,11 +2975,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
2994
2975
|
params.prompt,
|
|
2995
2976
|
{
|
|
2996
2977
|
description: params.description,
|
|
2997
|
-
model,
|
|
2978
|
+
model: resolvedConfig.agentTierSelected ? undefined : model,
|
|
2998
2979
|
maxTurns: effectiveMaxTurns,
|
|
2999
2980
|
isolated,
|
|
3000
2981
|
inheritContext,
|
|
3001
|
-
thinkingLevel: thinking,
|
|
2982
|
+
thinkingLevel: resolvedConfig.agentTierSelected ? undefined : thinking,
|
|
3002
2983
|
agentTier: resolvedConfig.requestedAgentTier,
|
|
3003
2984
|
isolation,
|
|
3004
2985
|
invocation: agentInvocation,
|
|
@@ -4396,19 +4377,13 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4396
4377
|
worktreeIsolation: isWorktreeIsolationEnabled(),
|
|
4397
4378
|
maxSubagentDepth: getMaxSubagentDepth(),
|
|
4398
4379
|
maxSubagentSpawnsPerBranch: manager.getMaxSubagentSpawnsPerBranch(),
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
(getWorkflowSettings().blockedTiers?.length ?? 0) > 0
|
|
4403
|
-
? { workflow: getWorkflowSettings() }
|
|
4404
|
-
: {}),
|
|
4405
|
-
// Same shape as the workflow block above: written back only when the user
|
|
4406
|
-
// actually configured tiers, so the snapshot never materializes an empty
|
|
4407
|
-
// catalogue — or the shipped `fast` fallback — into the project settings
|
|
4408
|
-
// file.
|
|
4380
|
+
// Written back only when the user actually configured tiers, so the
|
|
4381
|
+
// snapshot never materializes an empty catalogue — or the shipped
|
|
4382
|
+
// profiles and default — into the project settings file.
|
|
4409
4383
|
...(Object.keys(getAgentTiersConfiguredSettings().profiles ?? {}).length >
|
|
4410
4384
|
0 ||
|
|
4411
4385
|
getAgentTiersConfiguredSettings().defaultTier !== undefined ||
|
|
4386
|
+
getAgentTiersConfiguredSettings().noDefaultTier === true ||
|
|
4412
4387
|
getAgentTiersConfiguredSettings().blockedDefaultTier === true ||
|
|
4413
4388
|
(getAgentTiersConfiguredSettings().blockedProfiles?.length ?? 0) > 0
|
|
4414
4389
|
? { agentTiers: getAgentTiersConfiguredSettings() }
|
|
@@ -4448,8 +4423,19 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4448
4423
|
* the numeric fields hand off to a text prompt.
|
|
4449
4424
|
*/
|
|
4450
4425
|
const PICKER_IDS = new Set(["defaultModel"]);
|
|
4451
|
-
/**
|
|
4452
|
-
|
|
4426
|
+
/**
|
|
4427
|
+
* Row values for the two "no default tier" states, which behave differently:
|
|
4428
|
+
* leaving the field unset still lets a managed workflow call reach the shipped
|
|
4429
|
+
* fallback, while "none" withdraws that too. Both contain a space, which a
|
|
4430
|
+
* tier key may not, so a real tier can never shadow either one.
|
|
4431
|
+
*
|
|
4432
|
+
* Neither names the fallback tier. These strings are the sentinels the setter
|
|
4433
|
+
* below matches on, so they have to stay literal and stable — and *which*
|
|
4434
|
+
* tier `unset` reaches depends on the catalogue, which can remove it. The
|
|
4435
|
+
* row description says what it actually resolves to right now.
|
|
4436
|
+
*/
|
|
4437
|
+
const UNSET_DEFAULT_TIER = "unset (workflows use the shipped fallback)";
|
|
4438
|
+
const NO_DEFAULT_TIER = "none (workflows fail closed)";
|
|
4453
4439
|
/** Menu entry that starts a new tier instead of editing an existing one. */
|
|
4454
4440
|
const NEW_TIER_ENTRY = "+ New tier...";
|
|
4455
4441
|
|
|
@@ -4790,12 +4776,25 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4790
4776
|
{
|
|
4791
4777
|
id: "defaultTier",
|
|
4792
4778
|
label: "Default tier",
|
|
4793
|
-
description:
|
|
4794
|
-
tierKeys.length
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4779
|
+
description: (() => {
|
|
4780
|
+
if (tierKeys.length === 0) return "No tiers defined yet — create one in /agents → Model tiers.";
|
|
4781
|
+
const fallback = shippedFallbackAgentTier();
|
|
4782
|
+
// Named from the resolver, not from a constant: on a catalogue that
|
|
4783
|
+
// removed the shipped profile, "unset" reaches nothing and behaves
|
|
4784
|
+
// exactly like "none", and the menu has to say so rather than
|
|
4785
|
+
// promise a tier that is gone.
|
|
4786
|
+
const unset =
|
|
4787
|
+
fallback === undefined
|
|
4788
|
+
? '"unset" reaches no fallback here (the shipped profile has been removed), so managed workflow calls fail closed either way'
|
|
4789
|
+
: `"unset" leaves managed workflow calls on the shipped "${fallback}"`;
|
|
4790
|
+
return `Tier applied when neither the caller nor the agent names one. ${unset}; "none" withdraws that fallback so they fail closed. Edit the tiers themselves in /agents → Model tiers.`;
|
|
4791
|
+
})(),
|
|
4792
|
+
currentValue: (() => {
|
|
4793
|
+
const selection = getDefaultAgentTierSelection();
|
|
4794
|
+
if (selection.kind === "tier") return selection.tier;
|
|
4795
|
+
return selection.kind === "none" ? NO_DEFAULT_TIER : UNSET_DEFAULT_TIER;
|
|
4796
|
+
})(),
|
|
4797
|
+
values: [UNSET_DEFAULT_TIER, NO_DEFAULT_TIER, ...tierKeys],
|
|
4799
4798
|
},
|
|
4800
4799
|
{
|
|
4801
4800
|
id: "joinMode",
|
|
@@ -4959,15 +4958,26 @@ Do not wrap the response in a markdown code fence. Return only the file contents
|
|
|
4959
4958
|
);
|
|
4960
4959
|
}
|
|
4961
4960
|
} else if (id === "defaultTier") {
|
|
4962
|
-
const
|
|
4961
|
+
const selection: DefaultAgentTierSelection =
|
|
4962
|
+
value === UNSET_DEFAULT_TIER
|
|
4963
|
+
? { kind: "unset" }
|
|
4964
|
+
: value === NO_DEFAULT_TIER
|
|
4965
|
+
? { kind: "none" }
|
|
4966
|
+
: { kind: "tier", tier: value };
|
|
4963
4967
|
setAgentTiersSettings(
|
|
4964
|
-
setDefaultAgentTier(getAgentTiersSettings(),
|
|
4968
|
+
setDefaultAgentTier(getAgentTiersSettings(), selection),
|
|
4965
4969
|
);
|
|
4966
4970
|
notifyApplied(
|
|
4967
4971
|
ctx,
|
|
4968
|
-
|
|
4969
|
-
?
|
|
4970
|
-
:
|
|
4972
|
+
selection.kind === "tier"
|
|
4973
|
+
? `Default tier set to ${selection.tier}. The tool description updates on the next pi session.`
|
|
4974
|
+
: selection.kind === "none"
|
|
4975
|
+
? "Default tier set to none. Spawns that name no tier use the agent's own tier or the default model, and managed workflow calls now fail closed rather than falling back to the shipped tier."
|
|
4976
|
+
: `Default tier unset. Spawns that name no tier use the agent's own tier or the default model, and managed workflow calls ${
|
|
4977
|
+
shippedFallbackAgentTier() === undefined
|
|
4978
|
+
? "fail closed, because this catalogue no longer defines the shipped fallback profile"
|
|
4979
|
+
: `fall back to the shipped "${shippedFallbackAgentTier()}"`
|
|
4980
|
+
}.`,
|
|
4971
4981
|
);
|
|
4972
4982
|
} else if (id === "joinMode") {
|
|
4973
4983
|
setDefaultJoinMode(value as JoinMode);
|
package/src/internal-run.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* the symbol-keyed override.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
8
|
+
import type { ThinkingLevel } from "./types.js";
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* Immutable policy used while asking a model to draft an agent definition.
|
|
9
12
|
* The parent owns all file writes, so the child receives no tools, extensions,
|
|
@@ -25,6 +28,17 @@ export interface InternalAgentConfigOverride {
|
|
|
25
28
|
/** Symbol-keyed so ordinary JSON/RPC/public spawn options cannot carry it. */
|
|
26
29
|
export const INTERNAL_AGENT_CONFIG_OVERRIDE = Symbol("pi-subagents.internal-agent-config-override");
|
|
27
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Parent model/thinking chosen when a managed spawn is allocated. The symbol key
|
|
33
|
+
* keeps this capability out of JSON, the public spawn contract, and wire RPC.
|
|
34
|
+
*/
|
|
35
|
+
export interface ParentPolicySnapshot {
|
|
36
|
+
readonly model?: Model<Api>;
|
|
37
|
+
readonly thinking?: ThinkingLevel;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const INTERNAL_PARENT_POLICY_SNAPSHOT = Symbol("pi-subagents.internal-parent-policy-snapshot");
|
|
41
|
+
|
|
28
42
|
/** The only no-tool policy currently issued by the package. */
|
|
29
43
|
export const AGENT_DEFINITION_GENERATION_OVERRIDE: InternalAgentConfigOverride = Object.freeze({
|
|
30
44
|
builtinToolNames: Object.freeze([] as string[]),
|
package/src/invocation-config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { agentTierApplies } from "./agent-tiers.js";
|
|
2
|
+
import type { AgentTiersSettings } from "./settings.js";
|
|
2
3
|
import type {
|
|
3
4
|
AgentConfig,
|
|
4
5
|
IsolationMode,
|
|
@@ -13,11 +14,10 @@ interface AgentInvocationParams {
|
|
|
13
14
|
* are reachable only from programmatic callers and the legacy RPC.
|
|
14
15
|
*/
|
|
15
16
|
tier?: string;
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
workflowTier?: WorkflowTier;
|
|
17
|
+
/** Effective Agent-tier settings, used only to avoid pre-resolving legacy fields. */
|
|
18
|
+
agentTiers?: AgentTiersSettings;
|
|
19
|
+
/** Set by callers that cannot inherit the parent model; see `ResolveAgentTierInput`. */
|
|
20
|
+
requireTier?: boolean;
|
|
21
21
|
model?: string;
|
|
22
22
|
thinking?: string;
|
|
23
23
|
max_turns?: number;
|
|
@@ -35,7 +35,8 @@ export function resolveAgentInvocationConfig(
|
|
|
35
35
|
modelFromParams: boolean;
|
|
36
36
|
/** Passed to `resolveAgentTier` as the requested tier; precedence lives there. */
|
|
37
37
|
requestedAgentTier?: string;
|
|
38
|
-
|
|
38
|
+
/** True when a tier owns final model and thinking resolution. */
|
|
39
|
+
agentTierSelected: boolean;
|
|
39
40
|
thinking?: ThinkingLevel;
|
|
40
41
|
maxTurns?: number;
|
|
41
42
|
inheritContext: boolean;
|
|
@@ -43,14 +44,24 @@ export function resolveAgentInvocationConfig(
|
|
|
43
44
|
isolated: boolean;
|
|
44
45
|
isolation?: IsolationMode;
|
|
45
46
|
} {
|
|
47
|
+
// Asked, not restated: `agentTierApplies` runs the resolver's own selection.
|
|
48
|
+
// Whenever a tier applies it owns model and thinking outright, so pre-resolving
|
|
49
|
+
// a legacy field here would hand the runner a value the tier is about to
|
|
50
|
+
// discard, and a scope check against a model that never runs.
|
|
51
|
+
const agentTierSelected = agentTierApplies({
|
|
52
|
+
requestedTier: params.tier,
|
|
53
|
+
requireTier: params.requireTier,
|
|
54
|
+
agentConfig,
|
|
55
|
+
settings: params.agentTiers,
|
|
56
|
+
});
|
|
46
57
|
return {
|
|
47
|
-
modelInput: agentConfig?.model ?? params.model,
|
|
58
|
+
modelInput: agentTierSelected ? undefined : agentConfig?.model ?? params.model,
|
|
48
59
|
requestedAgentTier: params.tier,
|
|
49
|
-
|
|
50
|
-
modelFromParams: agentConfig?.model == null && params.model != null,
|
|
51
|
-
thinking:
|
|
52
|
-
|
|
53
|
-
| undefined,
|
|
60
|
+
agentTierSelected,
|
|
61
|
+
modelFromParams: !agentTierSelected && agentConfig?.model == null && params.model != null,
|
|
62
|
+
thinking: agentTierSelected
|
|
63
|
+
? undefined
|
|
64
|
+
: (agentConfig?.thinking ?? params.thinking) as ThinkingLevel | undefined,
|
|
54
65
|
maxTurns: agentConfig?.maxTurns ?? params.max_turns,
|
|
55
66
|
inheritContext:
|
|
56
67
|
agentConfig?.inheritContext ?? params.inherit_context ?? false,
|
package/src/model-resolver.ts
CHANGED
|
@@ -98,3 +98,15 @@ export function resolveModel<TModel extends ModelEntry>(
|
|
|
98
98
|
.join("\n");
|
|
99
99
|
return `Model not found: "${input}".\n\nAvailable models:\n${modelList}`;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The short model label the agent UI shows, e.g. `haiku 4.5`.
|
|
104
|
+
*
|
|
105
|
+
* Lives here rather than at either call site because two paths now produce it:
|
|
106
|
+
* the Agent tool, from the model it pre-resolves for an untiered spawn, and the
|
|
107
|
+
* runner, from the model a tier's profile pinned. The label must read the same
|
|
108
|
+
* either way, so it is derived once.
|
|
109
|
+
*/
|
|
110
|
+
export function shortModelLabel(model: Pick<ModelEntry, "id" | "name">): string {
|
|
111
|
+
return (model.name || model.id).replace(/^Claude\s+/iu, "").toLowerCase();
|
|
112
|
+
}
|
package/src/model-scope.ts
CHANGED
|
@@ -55,14 +55,17 @@ export function checkModelScope(args: {
|
|
|
55
55
|
const allowed = resolveEnabledModels(readEnabledModels(cwd), modelRegistry, cwd);
|
|
56
56
|
if (!allowed || isModelInScope(model, allowed)) return { kind: "ok" };
|
|
57
57
|
|
|
58
|
+
// A tier whose profile says `inherit` names no model of its own, so fall back
|
|
59
|
+
// to what it actually resolved to. Without this the refusal reads
|
|
60
|
+
// `Model not in scope: "undefined"`, which tells the caller nothing it can act on.
|
|
61
|
+
const modelLabel = modelInput ?? `${model.provider}/${model.id}`;
|
|
58
62
|
if (callerSupplied) {
|
|
59
63
|
const list = [...allowed].sort().map(m => ` ${m}`).join("\n");
|
|
60
64
|
return {
|
|
61
65
|
kind: "error",
|
|
62
|
-
message: `Model not in scope: "${
|
|
66
|
+
message: `Model not in scope: "${modelLabel}".\n\nAllowed models (from enabledModels):\n${list}`,
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
|
-
const modelLabel = modelInput ?? `${model.provider}/${model.id}`;
|
|
66
69
|
return {
|
|
67
70
|
kind: "warn",
|
|
68
71
|
message: `Agent "${agentLabel}" using out-of-scope model "${modelLabel}"`,
|