@tintinweb/pi-subagents 0.8.0 → 0.9.1
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 +25 -0
- package/README.md +22 -1
- package/dist/default-agents.js +3 -3
- package/dist/enabled-models.d.ts +49 -0
- package/dist/enabled-models.js +145 -0
- package/dist/index.js +224 -84
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +5 -0
- package/package.json +1 -1
- package/src/default-agents.ts +3 -3
- package/src/enabled-models.ts +180 -0
- package/src/index.ts +247 -85
- package/src/settings.ts +27 -0
package/dist/index.js
CHANGED
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { defineTool, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
14
|
+
import { defineTool, getAgentDir, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { Container, Key, matchesKey, SettingsList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
16
16
|
import { Type } from "@sinclair/typebox";
|
|
17
17
|
import { AgentManager } from "./agent-manager.js";
|
|
18
18
|
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
19
19
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveType } from "./agent-types.js";
|
|
20
20
|
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
21
21
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
22
|
+
import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
22
23
|
import { GroupJoinManager } from "./group-join.js";
|
|
23
24
|
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
24
25
|
import { resolveModel } from "./model-resolver.js";
|
|
@@ -459,6 +460,16 @@ export default function (pi) {
|
|
|
459
460
|
let schedulingEnabled = true;
|
|
460
461
|
function isSchedulingEnabled() { return schedulingEnabled; }
|
|
461
462
|
function setSchedulingEnabled(b) { schedulingEnabled = b; }
|
|
463
|
+
// ---- Scope models configuration ----
|
|
464
|
+
// When enabled, subagent model choices are validated against `enabledModels`
|
|
465
|
+
// from pi's settings — both global `<agentDir>/settings.json` and
|
|
466
|
+
// project-local `<cwd>/.pi/settings.json` (project overrides global).
|
|
467
|
+
// Off by default; opt-in via `/agents → Settings`. See docstring on
|
|
468
|
+
// SubagentsSettings.scopeModels for the hard-error vs warn-and-proceed
|
|
469
|
+
// policy and its rationale.
|
|
470
|
+
let scopeModelsEnabled = false;
|
|
471
|
+
function isScopeModelsEnabled() { return scopeModelsEnabled; }
|
|
472
|
+
function setScopeModelsEnabled(enabled) { scopeModelsEnabled = enabled; }
|
|
462
473
|
// ---- Batch tracking for smart join mode ----
|
|
463
474
|
// Collects background agent IDs spawned in the current turn for smart grouping.
|
|
464
475
|
// Uses a debounced timer: each new agent resets the 100ms window so that all
|
|
@@ -506,26 +517,24 @@ export default function (pi) {
|
|
|
506
517
|
widget.setUICtx(ctx.ui);
|
|
507
518
|
widget.onTurnStart();
|
|
508
519
|
});
|
|
520
|
+
/** Format an agent's tool scope: "*" when it has all built-ins, else a comma-separated list. */
|
|
521
|
+
const formatToolsSuffix = (cfg) => {
|
|
522
|
+
const tools = cfg?.builtinToolNames;
|
|
523
|
+
if (!tools || tools.length === 0)
|
|
524
|
+
return "*";
|
|
525
|
+
const isFullSet = tools.length === BUILTIN_TOOL_NAMES.length
|
|
526
|
+
&& BUILTIN_TOOL_NAMES.every((t) => tools.includes(t));
|
|
527
|
+
return isFullSet ? "*" : tools.join(", ");
|
|
528
|
+
};
|
|
509
529
|
/** Build the full type list text dynamically from the unified registry. */
|
|
510
530
|
const buildTypeListText = () => {
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
const defaultDescs = defaultNames.map((name) => {
|
|
531
|
+
const allNames = [...getDefaultAgentNames(), ...getUserAgentNames()];
|
|
532
|
+
return allNames.map((name) => {
|
|
514
533
|
const cfg = getAgentConfig(name);
|
|
515
534
|
const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const cfg = getAgentConfig(name);
|
|
520
|
-
return `- ${name}: ${cfg?.description ?? name}`;
|
|
521
|
-
});
|
|
522
|
-
return [
|
|
523
|
-
"Default agents:",
|
|
524
|
-
...defaultDescs,
|
|
525
|
-
...(customDescs.length > 0 ? ["", "Custom agents:", ...customDescs] : []),
|
|
526
|
-
"",
|
|
527
|
-
`Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.`,
|
|
528
|
-
].join("\n");
|
|
535
|
+
const toolsSuffix = ` (Tools: ${formatToolsSuffix(cfg)})`;
|
|
536
|
+
return `- ${name}: ${cfg?.description ?? name}${modelSuffix}${toolsSuffix}`;
|
|
537
|
+
}).join("\n");
|
|
529
538
|
};
|
|
530
539
|
/** Derive a short model label from a model string. */
|
|
531
540
|
function getModelLabelFromConfig(model) {
|
|
@@ -544,6 +553,7 @@ export default function (pi) {
|
|
|
544
553
|
setGraceTurns,
|
|
545
554
|
setDefaultJoinMode,
|
|
546
555
|
setSchedulingEnabled,
|
|
556
|
+
setScopeModels: setScopeModelsEnabled,
|
|
547
557
|
}, (event, payload) => pi.events.emit(event, payload));
|
|
548
558
|
// ---- Agent tool ----
|
|
549
559
|
// Schedule param + its guideline are gated on `schedulingEnabled` (read once
|
|
@@ -565,27 +575,55 @@ export default function (pi) {
|
|
|
565
575
|
pi.registerTool(defineTool({
|
|
566
576
|
name: "Agent",
|
|
567
577
|
label: "Agent",
|
|
568
|
-
description: `Launch a new agent to handle complex, multi-step tasks autonomously.
|
|
569
|
-
|
|
570
|
-
The Agent tool launches specialized agents that autonomously handle complex tasks. Each agent type has specific capabilities and tools available to it.
|
|
578
|
+
description: `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
571
579
|
|
|
572
|
-
Available agent types:
|
|
580
|
+
Available agent types and the tools they have access to:
|
|
573
581
|
${typeListText}
|
|
574
582
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
583
|
+
Custom agents can be defined in .pi/agents/<name>.md (project) or ${getAgentDir()}/agents/<name>.md (global) — they are picked up automatically. Project-level agents override global ones. Creating a .md file with the same name as a default agent overrides it.
|
|
584
|
+
|
|
585
|
+
When using the Agent tool, specify a subagent_type parameter to select which agent type to use.
|
|
586
|
+
|
|
587
|
+
## When not to use
|
|
588
|
+
|
|
589
|
+
If the target is already known, use a direct tool — \`read\` for a known path, \`grep\`/\`find\` for a specific symbol or string. Reserve this tool for open-ended questions that span the codebase, or tasks that match an available agent type.
|
|
590
|
+
|
|
591
|
+
## Usage notes
|
|
592
|
+
|
|
593
|
+
- Always include a short (3-5 word) description summarizing what the agent will do (shown in UI).
|
|
594
|
+
- When you launch multiple agents for independent work, send them in a single message with multiple tool uses, with run_in_background: true on each, so they run concurrently. If the user specifies that they want agents run "in parallel", you MUST send a single message with multiple tool calls. Foreground calls run sequentially — only one executes at a time.
|
|
595
|
+
- When the agent is done, it returns a single message back to you. The result is not visible to the user — to show the user, send a text message with a concise summary.
|
|
596
|
+
- Trust but verify: an agent's summary describes what it intended to do, not necessarily what it did. When an agent writes or edits code, check the actual changes before reporting work as done.
|
|
597
|
+
- Use run_in_background for work you don't need immediately. You will be notified when it completes — do NOT poll or sleep waiting for it. Continue with other work or respond to the user instead.
|
|
598
|
+
- Foreground vs background: use foreground (default) when you need the agent's results before you can proceed. Use background when you have genuinely independent work to do in parallel.
|
|
599
|
+
- Use resume with an agent ID to continue a previous agent's work. A new (non-resume) Agent call starts a fresh agent with no memory of prior runs, so the prompt must be self-contained.
|
|
584
600
|
- Use steer_subagent to send mid-run messages to a running background agent.
|
|
601
|
+
- Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, etc.), since it is not aware of the user's intent.
|
|
602
|
+
- If an agent's description says it should be used proactively, try to use it without the user having to ask for it first.
|
|
585
603
|
- Use model to specify a different model (as "provider/modelId", or fuzzy e.g. "haiku", "sonnet").
|
|
586
604
|
- Use thinking to control extended thinking level.
|
|
587
605
|
- Use inherit_context if the agent needs the parent conversation history.
|
|
588
|
-
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications).${scheduleGuideline}
|
|
606
|
+
- Use isolation: "worktree" to run the agent in an isolated git worktree (safe parallel file modifications). The worktree is automatically cleaned up if the agent makes no changes; otherwise the path and branch are returned in the result.${scheduleGuideline}
|
|
607
|
+
|
|
608
|
+
## Writing the prompt
|
|
609
|
+
|
|
610
|
+
Provide clear, detailed prompts so the agent can work autonomously. Brief it like a smart colleague who just walked into the room — it hasn't seen this conversation, doesn't know what you've tried, doesn't understand why this task matters.
|
|
611
|
+
- Explain what you're trying to accomplish and why.
|
|
612
|
+
- Describe what you've already learned or ruled out.
|
|
613
|
+
- Give enough context about the surrounding problem that the agent can make judgment calls rather than just following a narrow instruction.
|
|
614
|
+
- If you need a short response, say so ("report in under 200 words").
|
|
615
|
+
- Lookups: hand over the exact command. Investigations: hand over the question — prescribed steps become dead weight when the premise is wrong.
|
|
616
|
+
|
|
617
|
+
Terse command-style prompts produce shallow, generic work.
|
|
618
|
+
|
|
619
|
+
**Never delegate understanding.** Don't write "based on your findings, fix the bug" or "based on the research, implement it." Those phrases push synthesis onto the agent instead of doing it yourself. Write prompts that prove you understood: include file paths, line numbers, what specifically to change.`,
|
|
620
|
+
promptSnippet: "Launch autonomous sub-agents for complex multi-step tasks",
|
|
621
|
+
promptGuidelines: [
|
|
622
|
+
"Use Agent with specialized agents when the task matches an agent type's description. Subagents are valuable for parallelizing independent queries or for protecting the main context window from excessive results, but should not be used excessively when not needed. Importantly, avoid duplicating work that subagents are already doing — if you delegate research to a subagent, do not also perform the same searches yourself.",
|
|
623
|
+
"For broad codebase exploration or research, spawn Agent with an appropriate subagent_type (e.g. Explore). Otherwise use direct tools (read, grep, find) when the target is already known.",
|
|
624
|
+
"When an agent runs in the background, you will be notified on completion — do not poll or sleep waiting for it. Continue with other work instead.",
|
|
625
|
+
"Trust but verify: an agent's summary describes intent, not outcome. When an agent writes or edits code, check the actual changes before reporting work as done.",
|
|
626
|
+
],
|
|
589
627
|
parameters: Type.Object({
|
|
590
628
|
prompt: Type.String({
|
|
591
629
|
description: "The task for the agent to perform.",
|
|
@@ -734,6 +772,29 @@ Guidelines:
|
|
|
734
772
|
model = resolved;
|
|
735
773
|
}
|
|
736
774
|
}
|
|
775
|
+
// Scope validation: the effective resolved model is checked against the
|
|
776
|
+
// user's enabledModels list (read in `enabled-models.ts`).
|
|
777
|
+
//
|
|
778
|
+
// Design: scopeModels guards against *runtime* LLM choices, not user-level config.
|
|
779
|
+
// - Caller-supplied out-of-scope → hard error (the orchestrator made an explicit
|
|
780
|
+
// out-of-scope choice; surface it so it picks differently).
|
|
781
|
+
// - Frontmatter-pinned or parent-inherited out-of-scope → warn but proceed (the
|
|
782
|
+
// user authored/installed this agent or chose the parent's model; trust it).
|
|
783
|
+
// See SubagentsSettings.scopeModels docstring for the full policy.
|
|
784
|
+
if (isScopeModelsEnabled() && model) {
|
|
785
|
+
const allowed = resolveEnabledModels(readEnabledModels(ctx.cwd), ctx.modelRegistry, ctx.cwd);
|
|
786
|
+
if (allowed && !isModelInScope(model, allowed)) {
|
|
787
|
+
if (resolvedConfig.modelFromParams) {
|
|
788
|
+
const list = [...allowed].sort().map(m => ` ${m}`).join("\n");
|
|
789
|
+
return textResult(`Model not in scope: "${resolvedConfig.modelInput}".\n\n` +
|
|
790
|
+
`Allowed models (from enabledModels):\n${list}`);
|
|
791
|
+
}
|
|
792
|
+
// Frontmatter-pinned or parent-inherited: warn + proceed.
|
|
793
|
+
const agentLabel = customConfig?.displayName ?? subagentType;
|
|
794
|
+
const modelLabel = resolvedConfig.modelInput ?? `${model.provider}/${model.id}`;
|
|
795
|
+
ctx.ui.notify(`Agent "${agentLabel}" using out-of-scope model "${modelLabel}"`, "warning");
|
|
796
|
+
}
|
|
797
|
+
}
|
|
737
798
|
const thinking = resolvedConfig.thinking;
|
|
738
799
|
const inheritContext = resolvedConfig.inheritContext;
|
|
739
800
|
const runInBackground = resolvedConfig.runInBackground;
|
|
@@ -984,6 +1045,7 @@ Guidelines:
|
|
|
984
1045
|
name: "get_subagent_result",
|
|
985
1046
|
label: "Get Agent Result",
|
|
986
1047
|
description: "Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background.",
|
|
1048
|
+
promptSnippet: "Check status and retrieve results from a background agent",
|
|
987
1049
|
parameters: Type.Object({
|
|
988
1050
|
agent_id: Type.String({
|
|
989
1051
|
description: "The agent ID to check.",
|
|
@@ -1054,6 +1116,7 @@ Guidelines:
|
|
|
1054
1116
|
label: "Steer Agent",
|
|
1055
1117
|
description: "Send a steering message to a running agent. The message will interrupt the agent after its current tool execution " +
|
|
1056
1118
|
"and be injected into its conversation, allowing you to redirect its work mid-run. Only works on running agents.",
|
|
1119
|
+
promptSnippet: "Send a steering message to redirect a running background agent",
|
|
1057
1120
|
parameters: Type.Object({
|
|
1058
1121
|
agent_id: Type.String({
|
|
1059
1122
|
description: "The agent ID to steer (must be currently running).",
|
|
@@ -1352,7 +1415,7 @@ Guidelines:
|
|
|
1352
1415
|
}
|
|
1353
1416
|
// Build the .md file content
|
|
1354
1417
|
const fmFields = [];
|
|
1355
|
-
fmFields.push(`description: ${cfg.description}`);
|
|
1418
|
+
fmFields.push(`description: ${JSON.stringify(cfg.description)}`);
|
|
1356
1419
|
if (cfg.displayName)
|
|
1357
1420
|
fmFields.push(`display_name: ${cfg.displayName}`);
|
|
1358
1421
|
fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") || "all"}`);
|
|
@@ -1630,35 +1693,70 @@ ${systemPrompt}
|
|
|
1630
1693
|
graceTurns: getGraceTurns(),
|
|
1631
1694
|
defaultJoinMode: getDefaultJoinMode(),
|
|
1632
1695
|
schedulingEnabled: isSchedulingEnabled(),
|
|
1696
|
+
scopeModels: isScopeModelsEnabled(),
|
|
1633
1697
|
};
|
|
1634
1698
|
}
|
|
1699
|
+
const NUMERIC_IDS = new Set(["maxConcurrent", "defaultMaxTurns", "graceTurns"]);
|
|
1635
1700
|
async function showSettings(ctx) {
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1701
|
+
function buildItems() {
|
|
1702
|
+
const mc = manager.getMaxConcurrent();
|
|
1703
|
+
const dmt = getDefaultMaxTurns() ?? 0;
|
|
1704
|
+
const gt = getGraceTurns();
|
|
1705
|
+
return [
|
|
1706
|
+
{
|
|
1707
|
+
id: "maxConcurrent",
|
|
1708
|
+
label: "Max concurrency",
|
|
1709
|
+
description: "Max concurrent background agents (Enter to type)",
|
|
1710
|
+
currentValue: String(mc),
|
|
1711
|
+
values: [String(mc)],
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
id: "defaultMaxTurns",
|
|
1715
|
+
label: "Default max turns",
|
|
1716
|
+
description: "Default max turns before wrap-up (0 = unlimited, Enter to type)",
|
|
1717
|
+
currentValue: String(dmt),
|
|
1718
|
+
values: [String(dmt)],
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
id: "graceTurns",
|
|
1722
|
+
label: "Grace turns",
|
|
1723
|
+
description: "Grace turns after wrap-up steer (Enter to type)",
|
|
1724
|
+
currentValue: String(gt),
|
|
1725
|
+
values: [String(gt)],
|
|
1726
|
+
},
|
|
1727
|
+
{
|
|
1728
|
+
id: "joinMode",
|
|
1729
|
+
label: "Join mode",
|
|
1730
|
+
description: "Default join mode for background agents",
|
|
1731
|
+
currentValue: getDefaultJoinMode(),
|
|
1732
|
+
values: ["smart", "async", "group"],
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
id: "schedulingEnabled",
|
|
1736
|
+
label: "Scheduling",
|
|
1737
|
+
description: "Schedule subagent feature (off removes `schedule` param from Agent tool spec on next pi session)",
|
|
1738
|
+
currentValue: isSchedulingEnabled() ? "on" : "off",
|
|
1739
|
+
values: ["on", "off"],
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
id: "scopeModels",
|
|
1743
|
+
label: "Scope models",
|
|
1744
|
+
description: "Validate subagent models against scoped models (/scoped-models)",
|
|
1745
|
+
currentValue: isScopeModelsEnabled() ? "on" : "off",
|
|
1746
|
+
values: ["on", "off"],
|
|
1747
|
+
},
|
|
1748
|
+
];
|
|
1749
|
+
}
|
|
1750
|
+
function applyValue(id, value) {
|
|
1751
|
+
if (id === "maxConcurrent") {
|
|
1752
|
+
const n = parseInt(value, 10);
|
|
1649
1753
|
if (n >= 1) {
|
|
1650
1754
|
manager.setMaxConcurrent(n);
|
|
1651
1755
|
notifyApplied(ctx, `Max concurrency set to ${n}`);
|
|
1652
1756
|
}
|
|
1653
|
-
else {
|
|
1654
|
-
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1655
|
-
}
|
|
1656
1757
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
const val = await ctx.ui.input("Default max turns before wrap-up (0 = unlimited)", String(getDefaultMaxTurns() ?? 0));
|
|
1660
|
-
if (val) {
|
|
1661
|
-
const n = parseInt(val, 10);
|
|
1758
|
+
else if (id === "defaultMaxTurns") {
|
|
1759
|
+
const n = parseInt(value, 10);
|
|
1662
1760
|
if (n === 0) {
|
|
1663
1761
|
setDefaultMaxTurns(undefined);
|
|
1664
1762
|
notifyApplied(ctx, "Default max turns set to unlimited");
|
|
@@ -1667,43 +1765,20 @@ ${systemPrompt}
|
|
|
1667
1765
|
setDefaultMaxTurns(n);
|
|
1668
1766
|
notifyApplied(ctx, `Default max turns set to ${n}`);
|
|
1669
1767
|
}
|
|
1670
|
-
else {
|
|
1671
|
-
ctx.ui.notify("Must be 0 (unlimited) or a positive integer.", "warning");
|
|
1672
|
-
}
|
|
1673
1768
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
const val = await ctx.ui.input("Grace turns after wrap-up steer", String(getGraceTurns()));
|
|
1677
|
-
if (val) {
|
|
1678
|
-
const n = parseInt(val, 10);
|
|
1769
|
+
else if (id === "graceTurns") {
|
|
1770
|
+
const n = parseInt(value, 10);
|
|
1679
1771
|
if (n >= 1) {
|
|
1680
1772
|
setGraceTurns(n);
|
|
1681
1773
|
notifyApplied(ctx, `Grace turns set to ${n}`);
|
|
1682
1774
|
}
|
|
1683
|
-
else {
|
|
1684
|
-
ctx.ui.notify("Must be a positive integer.", "warning");
|
|
1685
|
-
}
|
|
1686
1775
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
"smart — auto-group 2+ agents in same turn (default)",
|
|
1691
|
-
"async — always notify individually",
|
|
1692
|
-
"group — always group background agents",
|
|
1693
|
-
]);
|
|
1694
|
-
if (val) {
|
|
1695
|
-
const mode = val.split(" ")[0];
|
|
1696
|
-
setDefaultJoinMode(mode);
|
|
1697
|
-
notifyApplied(ctx, `Default join mode set to ${mode}`);
|
|
1776
|
+
else if (id === "joinMode") {
|
|
1777
|
+
setDefaultJoinMode(value);
|
|
1778
|
+
notifyApplied(ctx, `Default join mode set to ${value}`);
|
|
1698
1779
|
}
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
const val = await ctx.ui.select("Schedule subagent feature", [
|
|
1702
|
-
"enabled — Agent tool accepts a `schedule` param; /agents → Scheduled jobs visible",
|
|
1703
|
-
"disabled — `schedule` removed from Agent tool spec (no LLM-context cost); menu hidden",
|
|
1704
|
-
]);
|
|
1705
|
-
if (val) {
|
|
1706
|
-
const enabled = val.startsWith("enabled");
|
|
1780
|
+
else if (id === "schedulingEnabled") {
|
|
1781
|
+
const enabled = value === "on";
|
|
1707
1782
|
if (enabled === isSchedulingEnabled()) {
|
|
1708
1783
|
ctx.ui.notify(`Scheduling already ${enabled ? "enabled" : "disabled"}.`, "info");
|
|
1709
1784
|
}
|
|
@@ -1714,6 +1789,71 @@ ${systemPrompt}
|
|
|
1714
1789
|
notifyApplied(ctx, `Scheduling ${enabled ? "enabled" : "disabled"}. Tool spec change takes effect on next pi session.`);
|
|
1715
1790
|
}
|
|
1716
1791
|
}
|
|
1792
|
+
else if (id === "scopeModels") {
|
|
1793
|
+
const enabled = value === "on";
|
|
1794
|
+
setScopeModelsEnabled(enabled);
|
|
1795
|
+
notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
let list;
|
|
1799
|
+
// Track current selection index directly (SettingsList doesn't expose it).
|
|
1800
|
+
// Updated on arrow keys so Enter knows which field is selected immediately.
|
|
1801
|
+
let currentIndex = 0;
|
|
1802
|
+
const result = await ctx.ui.custom((_tui, _theme, _kb, done) => {
|
|
1803
|
+
const items = buildItems();
|
|
1804
|
+
list = new SettingsList(items, items.length + 2, getSettingsListTheme(), (id, newValue) => {
|
|
1805
|
+
applyValue(id, newValue);
|
|
1806
|
+
}, () => done(undefined));
|
|
1807
|
+
const container = new Container();
|
|
1808
|
+
container.addChild(new Text("⚙ Subagent Settings", 0, 0));
|
|
1809
|
+
container.addChild(new Spacer(1));
|
|
1810
|
+
container.addChild(list);
|
|
1811
|
+
return {
|
|
1812
|
+
render: (w) => container.render(w),
|
|
1813
|
+
invalidate: () => container.invalidate(),
|
|
1814
|
+
handleInput: (data) => {
|
|
1815
|
+
// Track navigation so Enter knows the current field
|
|
1816
|
+
if (matchesKey(data, "up")) {
|
|
1817
|
+
currentIndex = Math.max(0, currentIndex - 1);
|
|
1818
|
+
}
|
|
1819
|
+
else if (matchesKey(data, "down")) {
|
|
1820
|
+
currentIndex = Math.min(items.length - 1, currentIndex + 1);
|
|
1821
|
+
}
|
|
1822
|
+
// Enter on numeric field → close and prompt for typed input
|
|
1823
|
+
if (matchesKey(data, Key.enter) && NUMERIC_IDS.has(items[currentIndex].id)) {
|
|
1824
|
+
done(items[currentIndex].id);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
list.handleInput?.(data);
|
|
1828
|
+
},
|
|
1829
|
+
};
|
|
1830
|
+
});
|
|
1831
|
+
// If a numeric field ID was returned, prompt for typed input
|
|
1832
|
+
if (result && NUMERIC_IDS.has(result)) {
|
|
1833
|
+
const current = result === "maxConcurrent"
|
|
1834
|
+
? String(manager.getMaxConcurrent())
|
|
1835
|
+
: result === "defaultMaxTurns"
|
|
1836
|
+
? String(getDefaultMaxTurns() ?? 0)
|
|
1837
|
+
: String(getGraceTurns());
|
|
1838
|
+
const label = result === "maxConcurrent"
|
|
1839
|
+
? "Max concurrency (1+)"
|
|
1840
|
+
: result === "defaultMaxTurns"
|
|
1841
|
+
? "Default max turns (0 = unlimited)"
|
|
1842
|
+
: "Grace turns (1+)";
|
|
1843
|
+
// Loop until user enters a valid integer or cancels (Esc / null).
|
|
1844
|
+
// Silently trims whitespace; rejects non-numeric input by re-prompting.
|
|
1845
|
+
let input = await ctx.ui.input(label, current);
|
|
1846
|
+
while (input != null) {
|
|
1847
|
+
const trimmed = input.trim();
|
|
1848
|
+
const n = Number(trimmed);
|
|
1849
|
+
if (trimmed !== "" && Number.isInteger(n)) {
|
|
1850
|
+
applyValue(result, String(n));
|
|
1851
|
+
await showSettings(ctx);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
// Invalid — re-prompt with the user's last entry so they can edit it
|
|
1855
|
+
input = await ctx.ui.input(label, trimmed);
|
|
1856
|
+
}
|
|
1717
1857
|
}
|
|
1718
1858
|
}
|
|
1719
1859
|
// Persist the current snapshot, emit `subagents:settings_changed`, and surface
|
package/dist/settings.d.ts
CHANGED
|
@@ -18,6 +18,28 @@ export interface SubagentsSettings {
|
|
|
18
18
|
* (next pi session); runtime menu/runtime-fire short-circuit is immediate.
|
|
19
19
|
*/
|
|
20
20
|
schedulingEnabled?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* When true, the effective model of each subagent spawn is validated
|
|
23
|
+
* against `enabledModels` from pi's settings — both global
|
|
24
|
+
* (`<agentDir>/settings.json`) and project-local (`<cwd>/.pi/settings.json`),
|
|
25
|
+
* with project overriding global (mirrors pi's SettingsManager deep-merge).
|
|
26
|
+
*
|
|
27
|
+
* scopeModels guards against runtime LLM choices, not user-level config.
|
|
28
|
+
* Out-of-scope handling reflects this:
|
|
29
|
+
* - Caller-supplied via `Agent({ model: "..." })` (only when frontmatter
|
|
30
|
+
* has no `model:`, since frontmatter is authoritative): hard error
|
|
31
|
+
* returned to the orchestrator, listing the allowed models. The LLM
|
|
32
|
+
* made an explicit out-of-scope choice and gets explicit feedback.
|
|
33
|
+
* - Frontmatter-pinned: warning toast + the pinned model runs. The
|
|
34
|
+
* agent's author/installer chose this; trust it.
|
|
35
|
+
* - Parent-inherited (neither caller nor frontmatter sets a model):
|
|
36
|
+
* warning toast + parent's model runs. The user chose the parent's
|
|
37
|
+
* model when starting the session; trust it.
|
|
38
|
+
*
|
|
39
|
+
* No-op when pi's `enabledModels` is empty or absent — nothing to validate
|
|
40
|
+
* against. Defaults to false: subagents may use any model.
|
|
41
|
+
*/
|
|
42
|
+
scopeModels?: boolean;
|
|
21
43
|
}
|
|
22
44
|
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
23
45
|
export interface SettingsAppliers {
|
|
@@ -26,6 +48,7 @@ export interface SettingsAppliers {
|
|
|
26
48
|
setGraceTurns: (n: number) => void;
|
|
27
49
|
setDefaultJoinMode: (mode: JoinMode) => void;
|
|
28
50
|
setSchedulingEnabled: (b: boolean) => void;
|
|
51
|
+
setScopeModels: (enabled: boolean) => void;
|
|
29
52
|
}
|
|
30
53
|
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
31
54
|
export type SettingsEmit = (event: string, payload: unknown) => void;
|
package/dist/settings.js
CHANGED
|
@@ -38,6 +38,9 @@ function sanitize(raw) {
|
|
|
38
38
|
if (typeof r.schedulingEnabled === "boolean") {
|
|
39
39
|
out.schedulingEnabled = r.schedulingEnabled;
|
|
40
40
|
}
|
|
41
|
+
if (typeof r.scopeModels === "boolean") {
|
|
42
|
+
out.scopeModels = r.scopeModels;
|
|
43
|
+
}
|
|
41
44
|
return out;
|
|
42
45
|
}
|
|
43
46
|
function globalPath() {
|
|
@@ -95,6 +98,8 @@ export function applySettings(s, appliers) {
|
|
|
95
98
|
appliers.setDefaultJoinMode(s.defaultJoinMode);
|
|
96
99
|
if (typeof s.schedulingEnabled === "boolean")
|
|
97
100
|
appliers.setSchedulingEnabled(s.schedulingEnabled);
|
|
101
|
+
if (typeof s.scopeModels === "boolean")
|
|
102
|
+
appliers.setScopeModels(s.scopeModels);
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
100
105
|
* Format the user-facing toast for a settings mutation. Pure function —
|
package/package.json
CHANGED
package/src/default-agents.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
14
14
|
{
|
|
15
15
|
name: "general-purpose",
|
|
16
16
|
displayName: "Agent",
|
|
17
|
-
description: "General-purpose agent for complex, multi-step tasks",
|
|
17
|
+
description: "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
|
|
18
18
|
// builtinToolNames omitted — means "all available tools" (resolved at lookup time)
|
|
19
19
|
// inheritContext / runInBackground / isolated omitted — strategy fields, callers decide per-call.
|
|
20
20
|
// Setting them to false would lock callsite intent (see resolveAgentInvocationConfig in invocation-config.ts).
|
|
@@ -30,7 +30,7 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
|
|
|
30
30
|
{
|
|
31
31
|
name: "Explore",
|
|
32
32
|
displayName: "Explore",
|
|
33
|
-
description: "Fast
|
|
33
|
+
description: "Fast read-only search agent for locating code. Use it to find files by pattern (eg. \"src/components/**/*.tsx\"), grep for symbols or keywords (eg. \"API endpoints\"), or answer \"where is X defined / which files reference Y.\" Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: \"quick\" for a single targeted lookup, \"medium\" for moderate exploration, or \"very thorough\" to search across multiple locations and naming conventions.",
|
|
34
34
|
builtinToolNames: READ_ONLY_TOOLS,
|
|
35
35
|
extensions: true,
|
|
36
36
|
skills: true,
|
|
@@ -72,7 +72,7 @@ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find,
|
|
|
72
72
|
{
|
|
73
73
|
name: "Plan",
|
|
74
74
|
displayName: "Plan",
|
|
75
|
-
description: "Software architect for implementation
|
|
75
|
+
description: "Software architect agent for designing implementation plans. Use this when you need to plan the implementation strategy for a task. Returns step-by-step plans, identifies critical files, and considers architectural trade-offs.",
|
|
76
76
|
builtinToolNames: READ_ONLY_TOOLS,
|
|
77
77
|
extensions: true,
|
|
78
78
|
skills: true,
|