@tintinweb/pi-subagents 0.9.1 → 0.10.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 +36 -0
- package/README.md +54 -17
- package/dist/agent-runner.d.ts +49 -0
- package/dist/agent-runner.js +225 -35
- package/dist/agent-types.d.ts +12 -1
- package/dist/agent-types.js +26 -7
- package/dist/custom-agents.js +21 -1
- package/dist/index.js +46 -24
- package/dist/prompts.d.ts +6 -3
- package/dist/prompts.js +12 -4
- package/dist/settings.d.ts +8 -0
- package/dist/settings.js +5 -0
- package/dist/status-note.d.ts +13 -0
- package/dist/status-note.js +24 -0
- package/dist/types.d.ts +4 -0
- package/dist/ui/agent-widget.d.ts +4 -4
- package/dist/ui/agent-widget.js +6 -6
- package/dist/ui/conversation-viewer.d.ts +9 -1
- package/dist/ui/conversation-viewer.js +35 -2
- package/dist/worktree.d.ts +2 -0
- package/dist/worktree.js +28 -16
- package/package.json +2 -1
- package/src/agent-runner.ts +238 -34
- package/src/agent-types.ts +29 -7
- package/src/custom-agents.ts +23 -1
- package/src/index.ts +46 -26
- package/src/prompts.ts +12 -4
- package/src/settings.ts +12 -0
- package/src/status-note.ts +25 -0
- package/src/types.ts +4 -1
- package/src/ui/agent-widget.ts +6 -6
- package/src/ui/conversation-viewer.ts +32 -1
- package/src/worktree.ts +30 -17
- package/vitest.config.ts +18 -0
package/dist/agent-types.js
CHANGED
|
@@ -4,11 +4,27 @@
|
|
|
4
4
|
* Merges embedded default agents with user-defined agents from .pi/agents/*.md.
|
|
5
5
|
* User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
|
|
6
6
|
*/
|
|
7
|
+
import { createCodingTools, createReadOnlyTools } from "@earendil-works/pi-coding-agent";
|
|
7
8
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
8
|
-
/**
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* All known built-in tool names, derived from pi's own tool factories rather
|
|
11
|
+
* than hardcoded so the set tracks pi-mono if it adds/renames a built-in.
|
|
12
|
+
* `createCodingTools` → read/bash/edit/write; `createReadOnlyTools` →
|
|
13
|
+
* read/grep/find/ls; their de-duplicated union is the 7 built-ins
|
|
14
|
+
* (read, bash, edit, write, grep, find, ls). The `cwd` only binds tool
|
|
15
|
+
* operations we never invoke here — we read each tool's `.name` and discard it.
|
|
16
|
+
*/
|
|
17
|
+
export const BUILTIN_TOOL_NAMES = [
|
|
18
|
+
...new Set([...createCodingTools("."), ...createReadOnlyTools(".")].map((t) => t.name)),
|
|
19
|
+
];
|
|
10
20
|
/** Unified runtime registry of all agents (defaults + user-defined). */
|
|
11
21
|
const agents = new Map();
|
|
22
|
+
/** When true, DEFAULT_AGENTS are skipped during registration. */
|
|
23
|
+
let disableDefaults = false;
|
|
24
|
+
/** Check whether default agents are disabled. */
|
|
25
|
+
export function isDefaultsDisabled() { return disableDefaults; }
|
|
26
|
+
/** Set whether default agents are disabled. */
|
|
27
|
+
export function setDefaultsDisabled(b) { disableDefaults = b; }
|
|
12
28
|
/**
|
|
13
29
|
* Register agents into the unified registry.
|
|
14
30
|
* Starts with DEFAULT_AGENTS, then overlays user agents (overrides defaults with same name).
|
|
@@ -16,9 +32,11 @@ const agents = new Map();
|
|
|
16
32
|
*/
|
|
17
33
|
export function registerAgents(userAgents) {
|
|
18
34
|
agents.clear();
|
|
19
|
-
// Start with defaults
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
// Start with defaults (unless disabled via settings)
|
|
36
|
+
if (!disableDefaults) {
|
|
37
|
+
for (const [name, config] of DEFAULT_AGENTS) {
|
|
38
|
+
agents.set(name, config);
|
|
39
|
+
}
|
|
22
40
|
}
|
|
23
41
|
// Overlay user agents (overrides defaults with same name)
|
|
24
42
|
for (const [name, config] of userAgents) {
|
|
@@ -95,8 +113,9 @@ export function getToolNamesForType(type) {
|
|
|
95
113
|
const key = resolveKey(type);
|
|
96
114
|
const raw = key ? agents.get(key) : undefined;
|
|
97
115
|
const config = raw?.enabled !== false ? raw : undefined;
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
// `undefined` (definition omitted the field) → all built-ins; an explicit `[]`
|
|
117
|
+
// (`tools: none` or a `tools:` with only `ext:` entries) → zero built-ins.
|
|
118
|
+
return config?.builtinToolNames ?? [...BUILTIN_TOOL_NAMES];
|
|
100
119
|
}
|
|
101
120
|
/** Get config for a type (case-insensitive, returns a SubagentTypeConfig-compatible object). Falls back to general-purpose. */
|
|
102
121
|
export function getConfig(type) {
|
package/dist/custom-agents.js
CHANGED
|
@@ -43,11 +43,13 @@ function loadFromDir(dir, agents, source) {
|
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
45
|
const { frontmatter: fm, body } = parseFrontmatter(content);
|
|
46
|
+
const { builtinToolNames, extSelectors } = parseToolsField(fm.tools);
|
|
46
47
|
agents.set(name, {
|
|
47
48
|
name,
|
|
48
49
|
displayName: str(fm.display_name),
|
|
49
50
|
description: str(fm.description) ?? name,
|
|
50
|
-
builtinToolNames
|
|
51
|
+
builtinToolNames,
|
|
52
|
+
extSelectors,
|
|
51
53
|
disallowedTools: csvListOptional(fm.disallowed_tools),
|
|
52
54
|
extensions: inheritField(fm.extensions ?? fm.inherit_extensions),
|
|
53
55
|
skills: inheritField(fm.skills ?? fm.inherit_skills),
|
|
@@ -97,6 +99,24 @@ function csvList(val, defaults) {
|
|
|
97
99
|
return defaults;
|
|
98
100
|
return parseCsvField(val) ?? [];
|
|
99
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Partition the `tools:` CSV into the built-in tool allowlist and raw `ext:` selectors.
|
|
104
|
+
* `*` (and the case-insensitive alias `all`, for `tools: all`) expands to all
|
|
105
|
+
* built-ins; plain entries are built-in names; `ext:` entries are extension-tool
|
|
106
|
+
* selectors parsed later by the runner. omitted → all built-ins, no selectors.
|
|
107
|
+
* `tools:` present with only `ext:` entries → zero built-ins (use `*`).
|
|
108
|
+
*/
|
|
109
|
+
function parseToolsField(val) {
|
|
110
|
+
const entries = csvList(val, BUILTIN_TOOL_NAMES);
|
|
111
|
+
const isWildcard = (e) => e === "*" || e.toLowerCase() === "all";
|
|
112
|
+
const hasWildcard = entries.some(isWildcard);
|
|
113
|
+
const plain = entries.filter(e => !isWildcard(e) && !e.startsWith("ext:"));
|
|
114
|
+
const extEntries = entries.filter(e => e.startsWith("ext:"));
|
|
115
|
+
return {
|
|
116
|
+
builtinToolNames: hasWildcard ? [...new Set([...BUILTIN_TOOL_NAMES, ...plain])] : plain,
|
|
117
|
+
extSelectors: extEntries.length > 0 ? extEntries : undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
100
120
|
/**
|
|
101
121
|
* Parse an optional comma-separated list field.
|
|
102
122
|
* omitted → undefined; "none"/empty → undefined; csv → listed items.
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,8 @@ import { defineTool, getAgentDir, getSettingsListTheme } from "@earendil-works/p
|
|
|
15
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
|
-
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
19
|
-
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes,
|
|
18
|
+
import { getAgentConversation, getDefaultMaxTurns, getGraceTurns, normalizeMaxTurns, SUBAGENT_TOOL_NAMES, setDefaultMaxTurns, setGraceTurns, steerAgent } from "./agent-runner.js";
|
|
19
|
+
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
|
|
20
20
|
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
21
21
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
22
22
|
import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
@@ -27,6 +27,7 @@ import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./o
|
|
|
27
27
|
import { SubagentScheduler } from "./schedule.js";
|
|
28
28
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
29
29
|
import { applyAndEmitLoaded, saveAndEmitChanged } from "./settings.js";
|
|
30
|
+
import { getStatusNote } from "./status-note.js";
|
|
30
31
|
import { AgentWidget, buildInvocationTags, describeActivity, formatDuration, formatMs, formatTokens, formatTurns, getDisplayName, getPromptModeLabel, SPINNER, } from "./ui/agent-widget.js";
|
|
31
32
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
32
33
|
import { addUsage, getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
@@ -98,15 +99,6 @@ function getStatusLabel(status, error) {
|
|
|
98
99
|
default: return "Done";
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
|
-
/** Parenthetical status note for completed agent result text. */
|
|
102
|
-
function getStatusNote(status) {
|
|
103
|
-
switch (status) {
|
|
104
|
-
case "aborted": return " (aborted — max turns exceeded, output may be incomplete)";
|
|
105
|
-
case "steered": return " (wrapped up — reached turn limit)";
|
|
106
|
-
case "stopped": return " (stopped by user)";
|
|
107
|
-
default: return "";
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
102
|
/** Escape XML special characters to prevent injection in structured notifications. */
|
|
111
103
|
function escapeXml(s) {
|
|
112
104
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -130,7 +122,7 @@ function formatTaskNotification(record, resultMaxLen) {
|
|
|
130
122
|
record.toolCallId ? `<tool-use-id>${escapeXml(record.toolCallId)}</tool-use-id>` : null,
|
|
131
123
|
record.outputFile ? `<output-file>${escapeXml(record.outputFile)}</output-file>` : null,
|
|
132
124
|
`<status>${escapeXml(status)}</status>`,
|
|
133
|
-
`<summary>Agent "${escapeXml(record.description)}" ${record.status}</summary>`,
|
|
125
|
+
`<summary>Agent "${escapeXml(record.description)}" ${record.status}${getStatusNote(record.status)}</summary>`,
|
|
134
126
|
`<result>${escapeXml(resultPreview)}</result>`,
|
|
135
127
|
`<usage><total_tokens>${totalTokens}</total_tokens><tool_uses>${record.toolUses}</tool_uses>${ctxXml}${compactXml}<duration_ms>${durationMs}</duration_ms></usage>`,
|
|
136
128
|
`</task-notification>`,
|
|
@@ -470,6 +462,17 @@ export default function (pi) {
|
|
|
470
462
|
let scopeModelsEnabled = false;
|
|
471
463
|
function isScopeModelsEnabled() { return scopeModelsEnabled; }
|
|
472
464
|
function setScopeModelsEnabled(enabled) { scopeModelsEnabled = enabled; }
|
|
465
|
+
// ---- Disable default agents configuration ----
|
|
466
|
+
// When enabled, the three hardcoded default agents (general-purpose, Explore,
|
|
467
|
+
// Plan) are not registered. User-defined agents from .pi/agents/*.md are
|
|
468
|
+
// completely unaffected — only DEFAULT_AGENTS are suppressed.
|
|
469
|
+
// Defaults to false; opt-in via `/agents → Settings` or subagents.json.
|
|
470
|
+
// State lives in agent-types.ts (isDefaultsDisabled) because registerAgents
|
|
471
|
+
// needs it; this wrapper just re-registers after flipping it.
|
|
472
|
+
function setDisableDefaultAgents(b) {
|
|
473
|
+
setDefaultsDisabled(b);
|
|
474
|
+
reloadCustomAgents(); // re-register with new setting
|
|
475
|
+
}
|
|
473
476
|
// ---- Batch tracking for smart join mode ----
|
|
474
477
|
// Collects background agent IDs spawned in the current turn for smart grouping.
|
|
475
478
|
// Uses a debounced timer: each new agent resets the 100ms window so that all
|
|
@@ -526,10 +529,10 @@ export default function (pi) {
|
|
|
526
529
|
&& BUILTIN_TOOL_NAMES.every((t) => tools.includes(t));
|
|
527
530
|
return isFullSet ? "*" : tools.join(", ");
|
|
528
531
|
};
|
|
529
|
-
/** Build the full type list text dynamically from
|
|
532
|
+
/** Build the full type list text dynamically from available agents only. */
|
|
530
533
|
const buildTypeListText = () => {
|
|
531
|
-
const
|
|
532
|
-
return
|
|
534
|
+
const available = getAvailableTypes();
|
|
535
|
+
return available.map((name) => {
|
|
533
536
|
const cfg = getAgentConfig(name);
|
|
534
537
|
const modelSuffix = cfg?.model ? ` (${getModelLabelFromConfig(cfg.model)})` : "";
|
|
535
538
|
const toolsSuffix = ` (Tools: ${formatToolsSuffix(cfg)})`;
|
|
@@ -543,7 +546,6 @@ export default function (pi) {
|
|
|
543
546
|
// Strip trailing date suffix (e.g. "claude-haiku-4-5-20251001" → "claude-haiku-4-5")
|
|
544
547
|
return name.replace(/-\d{8}$/, "");
|
|
545
548
|
}
|
|
546
|
-
const typeListText = buildTypeListText();
|
|
547
549
|
// Apply persisted settings on startup and emit `subagents:settings_loaded`.
|
|
548
550
|
// Global + project merged; missing → defaults; corrupt file emits a warning
|
|
549
551
|
// to stderr and falls back to defaults.
|
|
@@ -554,6 +556,7 @@ export default function (pi) {
|
|
|
554
556
|
setDefaultJoinMode,
|
|
555
557
|
setSchedulingEnabled,
|
|
556
558
|
setScopeModels: setScopeModelsEnabled,
|
|
559
|
+
setDisableDefaultAgents: setDisableDefaultAgents,
|
|
557
560
|
}, (event, payload) => pi.events.emit(event, payload));
|
|
558
561
|
// ---- Agent tool ----
|
|
559
562
|
// Schedule param + its guideline are gated on `schedulingEnabled` (read once
|
|
@@ -573,12 +576,12 @@ export default function (pi) {
|
|
|
573
576
|
? `\n- Use \`schedule\` only when the user explicitly asked for scheduled / recurring / delayed execution (e.g. "every Monday", "in an hour"). Don't auto-schedule from vague intent like "monitor X" — run once now or ask.`
|
|
574
577
|
: "";
|
|
575
578
|
pi.registerTool(defineTool({
|
|
576
|
-
name:
|
|
579
|
+
name: SUBAGENT_TOOL_NAMES.AGENT,
|
|
577
580
|
label: "Agent",
|
|
578
581
|
description: `Launch a new agent to handle complex, multi-step tasks autonomously. Each agent type has specific capabilities and tools available to it.
|
|
579
582
|
|
|
580
583
|
Available agent types and the tools they have access to:
|
|
581
|
-
${
|
|
584
|
+
${buildTypeListText()}
|
|
582
585
|
|
|
583
586
|
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
587
|
|
|
@@ -673,7 +676,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
673
676
|
const text = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
674
677
|
return new Text(text, 0, 0);
|
|
675
678
|
}
|
|
676
|
-
// Helper: build "haiku · thinking: high ·
|
|
679
|
+
// Helper: build "haiku · thinking: high · ↻5≤30 · 3 tool uses · 33.8k tokens" stats string
|
|
677
680
|
const stats = (d) => {
|
|
678
681
|
const parts = [];
|
|
679
682
|
if (d.modelName)
|
|
@@ -1026,8 +1029,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1026
1029
|
// Get final token count
|
|
1027
1030
|
const tokenText = formatLifetimeTokens(fgState);
|
|
1028
1031
|
const details = buildDetails(detailBase, record, fgState, { tokens: tokenText });
|
|
1032
|
+
// "general-purpose" may itself be unregistered (defaults disabled, no
|
|
1033
|
+
// user override) — getConfig then uses the hardcoded fallback config.
|
|
1029
1034
|
const fallbackNote = fellBack
|
|
1030
|
-
? `Note: Unknown agent type "${rawType}" — using general-purpose.\n\n`
|
|
1035
|
+
? `Note: Unknown agent type "${rawType}" — using ${resolveType("general-purpose") ? "general-purpose" : "the fallback agent config"}.\n\n`
|
|
1031
1036
|
: "";
|
|
1032
1037
|
if (record.status === "error") {
|
|
1033
1038
|
return textResult(`${fallbackNote}Agent failed: ${record.error}`, details);
|
|
@@ -1042,7 +1047,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1042
1047
|
}));
|
|
1043
1048
|
// ---- get_subagent_result tool ----
|
|
1044
1049
|
pi.registerTool(defineTool({
|
|
1045
|
-
name:
|
|
1050
|
+
name: SUBAGENT_TOOL_NAMES.GET_RESULT,
|
|
1046
1051
|
label: "Get Agent Result",
|
|
1047
1052
|
description: "Check status and retrieve results from a background agent. Use the agent ID returned by Agent with run_in_background.",
|
|
1048
1053
|
promptSnippet: "Check status and retrieve results from a background agent",
|
|
@@ -1084,7 +1089,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1084
1089
|
statsParts.push(`Compactions: ${record.compactionCount}`);
|
|
1085
1090
|
statsParts.push(`Duration: ${duration}`);
|
|
1086
1091
|
let output = `Agent: ${record.id}\n` +
|
|
1087
|
-
`Type: ${displayName} | Status: ${record.status} | ${statsParts.join(" | ")}\n` +
|
|
1092
|
+
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
1088
1093
|
`Description: ${record.description}\n\n`;
|
|
1089
1094
|
if (record.status === "running") {
|
|
1090
1095
|
output += "Agent is still running. Use wait: true or check back later.";
|
|
@@ -1112,7 +1117,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1112
1117
|
}));
|
|
1113
1118
|
// ---- steer_subagent tool ----
|
|
1114
1119
|
pi.registerTool(defineTool({
|
|
1115
|
-
name:
|
|
1120
|
+
name: SUBAGENT_TOOL_NAMES.STEER,
|
|
1116
1121
|
label: "Steer Agent",
|
|
1117
1122
|
description: "Send a steering message to a running agent. The message will interrupt the agent after its current tool execution " +
|
|
1118
1123
|
"and be injected into its conversation, allowing you to redirect its work mid-run. Only works on running agents.",
|
|
@@ -1322,7 +1327,11 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1322
1327
|
const session = record.session;
|
|
1323
1328
|
const activity = agentActivity.get(record.id);
|
|
1324
1329
|
await ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
1325
|
-
return new ConversationViewer(tui, session, record, activity, theme, done)
|
|
1330
|
+
return new ConversationViewer(tui, session, record, activity, theme, done, () => {
|
|
1331
|
+
if (manager.abort(record.id)) {
|
|
1332
|
+
ctx.ui.notify(`Stopped "${record.description}".`, "info");
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1326
1335
|
}, {
|
|
1327
1336
|
overlay: true,
|
|
1328
1337
|
overlayOptions: { anchor: "center", width: "90%", maxHeight: `${VIEWPORT_HEIGHT_PCT}%` },
|
|
@@ -1694,6 +1703,7 @@ ${systemPrompt}
|
|
|
1694
1703
|
defaultJoinMode: getDefaultJoinMode(),
|
|
1695
1704
|
schedulingEnabled: isSchedulingEnabled(),
|
|
1696
1705
|
scopeModels: isScopeModelsEnabled(),
|
|
1706
|
+
disableDefaultAgents: isDefaultsDisabled(),
|
|
1697
1707
|
};
|
|
1698
1708
|
}
|
|
1699
1709
|
const NUMERIC_IDS = new Set(["maxConcurrent", "defaultMaxTurns", "graceTurns"]);
|
|
@@ -1745,6 +1755,13 @@ ${systemPrompt}
|
|
|
1745
1755
|
currentValue: isScopeModelsEnabled() ? "on" : "off",
|
|
1746
1756
|
values: ["on", "off"],
|
|
1747
1757
|
},
|
|
1758
|
+
{
|
|
1759
|
+
id: "disableDefaultAgents",
|
|
1760
|
+
label: "Disable defaults",
|
|
1761
|
+
description: "Hide built-in agents (general-purpose, Explore, Plan) — custom agents are unaffected",
|
|
1762
|
+
currentValue: isDefaultsDisabled() ? "on" : "off",
|
|
1763
|
+
values: ["on", "off"],
|
|
1764
|
+
},
|
|
1748
1765
|
];
|
|
1749
1766
|
}
|
|
1750
1767
|
function applyValue(id, value) {
|
|
@@ -1794,6 +1811,11 @@ ${systemPrompt}
|
|
|
1794
1811
|
setScopeModelsEnabled(enabled);
|
|
1795
1812
|
notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`);
|
|
1796
1813
|
}
|
|
1814
|
+
else if (id === "disableDefaultAgents") {
|
|
1815
|
+
const enabled = value === "on";
|
|
1816
|
+
setDisableDefaultAgents(enabled);
|
|
1817
|
+
notifyApplied(ctx, `Default agents ${enabled ? "disabled" : "enabled"}. Tool spec change takes effect on next pi session.`);
|
|
1818
|
+
}
|
|
1797
1819
|
}
|
|
1798
1820
|
let list;
|
|
1799
1821
|
// Track current selection index directly (SettingsList doesn't expose it).
|
package/dist/prompts.d.ts
CHANGED
|
@@ -16,12 +16,15 @@ export interface PromptExtras {
|
|
|
16
16
|
* Build the system prompt for an agent from its config.
|
|
17
17
|
*
|
|
18
18
|
* - "replace" mode: env header + config.systemPrompt (full control, no parent identity)
|
|
19
|
-
* - "append" mode:
|
|
19
|
+
* - "append" mode: parent system prompt + sub-agent context + env header + config.systemPrompt
|
|
20
20
|
* - "append" with empty systemPrompt: pure parent clone
|
|
21
21
|
*
|
|
22
|
-
* Both modes
|
|
22
|
+
* Both modes include an `<active_agent name="${config.name}"/>` tag so downstream
|
|
23
23
|
* extensions (e.g. permission/policy systems) can resolve per-agent policy
|
|
24
|
-
* inside the child session by parsing the system prompt.
|
|
24
|
+
* inside the child session by parsing the system prompt. In replace mode the tag
|
|
25
|
+
* is prepended; in append mode it follows the shared inherited content so the
|
|
26
|
+
* parent prompt forms an identical, cacheable byte prefix with the parent
|
|
27
|
+
* session (the LLM's KV cache can then reuse those tokens across every spawn).
|
|
25
28
|
*
|
|
26
29
|
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
|
27
30
|
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
package/dist/prompts.js
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* Build the system prompt for an agent from its config.
|
|
6
6
|
*
|
|
7
7
|
* - "replace" mode: env header + config.systemPrompt (full control, no parent identity)
|
|
8
|
-
* - "append" mode:
|
|
8
|
+
* - "append" mode: parent system prompt + sub-agent context + env header + config.systemPrompt
|
|
9
9
|
* - "append" with empty systemPrompt: pure parent clone
|
|
10
10
|
*
|
|
11
|
-
* Both modes
|
|
11
|
+
* Both modes include an `<active_agent name="${config.name}"/>` tag so downstream
|
|
12
12
|
* extensions (e.g. permission/policy systems) can resolve per-agent policy
|
|
13
|
-
* inside the child session by parsing the system prompt.
|
|
13
|
+
* inside the child session by parsing the system prompt. In replace mode the tag
|
|
14
|
+
* is prepended; in append mode it follows the shared inherited content so the
|
|
15
|
+
* parent prompt forms an identical, cacheable byte prefix with the parent
|
|
16
|
+
* session (the LLM's KV cache can then reuse those tokens across every spawn).
|
|
14
17
|
*
|
|
15
18
|
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
|
16
19
|
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
|
@@ -49,7 +52,12 @@ You are operating as a sub-agent invoked to handle a specific task.
|
|
|
49
52
|
const customSection = config.systemPrompt?.trim()
|
|
50
53
|
? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
|
|
51
54
|
: "";
|
|
52
|
-
|
|
55
|
+
// Place shared/stable content first so the LLM's KV cache can reuse the
|
|
56
|
+
// inherited prefix across all subagent invocations. The parent prompt is
|
|
57
|
+
// placed verbatim (no wrapper tag) so it forms an identical byte prefix
|
|
58
|
+
// with the parent session, maximising KV cache hits. The <active_agent>
|
|
59
|
+
// tag and env block vary per call and are placed after the cached prefix.
|
|
60
|
+
return identity + "\n\n" + bridge + "\n\n" + activeAgentTag + envBlock + customSection + extrasSuffix;
|
|
53
61
|
}
|
|
54
62
|
// "replace" mode — env header + the config's full system prompt
|
|
55
63
|
const replaceHeader = `You are a pi coding agent sub-agent.
|
package/dist/settings.d.ts
CHANGED
|
@@ -40,6 +40,13 @@ export interface SubagentsSettings {
|
|
|
40
40
|
* against. Defaults to false: subagents may use any model.
|
|
41
41
|
*/
|
|
42
42
|
scopeModels?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* When true, the three built-in default agents (general-purpose, Explore, Plan)
|
|
45
|
+
* are not registered at startup. User-defined agents from .pi/agents/*.md are
|
|
46
|
+
* completely unaffected — only the hardcoded DEFAULT_AGENTS are suppressed.
|
|
47
|
+
* Defaults to false.
|
|
48
|
+
*/
|
|
49
|
+
disableDefaultAgents?: boolean;
|
|
43
50
|
}
|
|
44
51
|
/** Setter hooks used by applySettings to wire persisted values into in-memory state. */
|
|
45
52
|
export interface SettingsAppliers {
|
|
@@ -49,6 +56,7 @@ export interface SettingsAppliers {
|
|
|
49
56
|
setDefaultJoinMode: (mode: JoinMode) => void;
|
|
50
57
|
setSchedulingEnabled: (b: boolean) => void;
|
|
51
58
|
setScopeModels: (enabled: boolean) => void;
|
|
59
|
+
setDisableDefaultAgents: (b: boolean) => void;
|
|
52
60
|
}
|
|
53
61
|
/** Emit callback — a subset of `pi.events.emit` to keep helpers testable. */
|
|
54
62
|
export type SettingsEmit = (event: string, payload: unknown) => void;
|
package/dist/settings.js
CHANGED
|
@@ -41,6 +41,9 @@ function sanitize(raw) {
|
|
|
41
41
|
if (typeof r.scopeModels === "boolean") {
|
|
42
42
|
out.scopeModels = r.scopeModels;
|
|
43
43
|
}
|
|
44
|
+
if (typeof r.disableDefaultAgents === "boolean") {
|
|
45
|
+
out.disableDefaultAgents = r.disableDefaultAgents;
|
|
46
|
+
}
|
|
44
47
|
return out;
|
|
45
48
|
}
|
|
46
49
|
function globalPath() {
|
|
@@ -100,6 +103,8 @@ export function applySettings(s, appliers) {
|
|
|
100
103
|
appliers.setSchedulingEnabled(s.schedulingEnabled);
|
|
101
104
|
if (typeof s.scopeModels === "boolean")
|
|
102
105
|
appliers.setScopeModels(s.scopeModels);
|
|
106
|
+
if (typeof s.disableDefaultAgents === "boolean")
|
|
107
|
+
appliers.setDisableDefaultAgents(s.disableDefaultAgents);
|
|
103
108
|
}
|
|
104
109
|
/**
|
|
105
110
|
* Format the user-facing toast for a settings mutation. Pure function —
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-note.ts — Parenthetical status note appended to agent result text.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Explicit parenthetical note for a non-normal terminal outcome, so the parent
|
|
6
|
+
* agent can't mistake partial output for a completed result. Empty string for a
|
|
7
|
+
* clean completion (and any unknown/non-terminal status).
|
|
8
|
+
*
|
|
9
|
+
* `stopped` (a human aborted it) is deliberately distinct from `aborted` (the
|
|
10
|
+
* turn limit was hit) — the parent should treat human intervention differently
|
|
11
|
+
* from a budget cutoff.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getStatusNote(status: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* status-note.ts — Parenthetical status note appended to agent result text.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Explicit parenthetical note for a non-normal terminal outcome, so the parent
|
|
6
|
+
* agent can't mistake partial output for a completed result. Empty string for a
|
|
7
|
+
* clean completion (and any unknown/non-terminal status).
|
|
8
|
+
*
|
|
9
|
+
* `stopped` (a human aborted it) is deliberately distinct from `aborted` (the
|
|
10
|
+
* turn limit was hit) — the parent should treat human intervention differently
|
|
11
|
+
* from a budget cutoff.
|
|
12
|
+
*/
|
|
13
|
+
export function getStatusNote(status) {
|
|
14
|
+
switch (status) {
|
|
15
|
+
case "stopped":
|
|
16
|
+
return " (STOPPED BY THE USER before completion — output is partial; the task was NOT finished)";
|
|
17
|
+
case "aborted":
|
|
18
|
+
return " (aborted — hit the turn limit before completion; output may be incomplete)";
|
|
19
|
+
case "steered":
|
|
20
|
+
return " (wrapped up at the turn limit — output may be partial)";
|
|
21
|
+
default:
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface AgentConfig {
|
|
|
19
19
|
displayName?: string;
|
|
20
20
|
description: string;
|
|
21
21
|
builtinToolNames?: string[];
|
|
22
|
+
/** Raw `ext:` selector entries from the `tools:` CSV, e.g. ["ext:foo", "ext:bar/x"].
|
|
23
|
+
* Presence of any entry flips extension tools to an explicit allowlist. */
|
|
24
|
+
extSelectors?: string[];
|
|
22
25
|
/** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */
|
|
23
26
|
disallowedTools?: string[];
|
|
24
27
|
/** true = inherit all, string[] = only listed, false = none */
|
|
@@ -71,6 +74,7 @@ export interface AgentRecord {
|
|
|
71
74
|
worktree?: {
|
|
72
75
|
path: string;
|
|
73
76
|
branch: string;
|
|
77
|
+
baseSha: string;
|
|
74
78
|
};
|
|
75
79
|
/** Worktree cleanup result after agent completion. */
|
|
76
80
|
worktreeResult?: {
|
|
@@ -66,15 +66,15 @@ export declare function formatTokens(count: number): string;
|
|
|
66
66
|
/**
|
|
67
67
|
* Token count with optional context-fill % and compaction-count annotations.
|
|
68
68
|
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
69
|
-
* Compaction count rendered as
|
|
69
|
+
* Compaction count rendered as `⇊N` in dim.
|
|
70
70
|
*
|
|
71
71
|
* "12.3k token" — no annotations
|
|
72
72
|
* "12.3k token (45%)" — percent only
|
|
73
|
-
* "12.3k token (
|
|
74
|
-
* "12.3k token (45% ·
|
|
73
|
+
* "12.3k token (⇊2)" — compactions only (e.g. right after compact)
|
|
74
|
+
* "12.3k token (45% · ⇊2)" — both
|
|
75
75
|
*/
|
|
76
76
|
export declare function formatSessionTokens(tokens: number, percent: number | null, theme: Theme, compactions?: number): string;
|
|
77
|
-
/** Format turn count with optional max limit: "
|
|
77
|
+
/** Format turn count with optional max limit: "↻5≤30" or "↻5". */
|
|
78
78
|
export declare function formatTurns(turnCount: number, maxTurns?: number | null): string;
|
|
79
79
|
/** Format milliseconds as human-readable duration. */
|
|
80
80
|
export declare function formatMs(ms: number): string;
|
package/dist/ui/agent-widget.js
CHANGED
|
@@ -36,12 +36,12 @@ export function formatTokens(count) {
|
|
|
36
36
|
/**
|
|
37
37
|
* Token count with optional context-fill % and compaction-count annotations.
|
|
38
38
|
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
39
|
-
* Compaction count rendered as
|
|
39
|
+
* Compaction count rendered as `⇊N` in dim.
|
|
40
40
|
*
|
|
41
41
|
* "12.3k token" — no annotations
|
|
42
42
|
* "12.3k token (45%)" — percent only
|
|
43
|
-
* "12.3k token (
|
|
44
|
-
* "12.3k token (45% ·
|
|
43
|
+
* "12.3k token (⇊2)" — compactions only (e.g. right after compact)
|
|
44
|
+
* "12.3k token (45% · ⇊2)" — both
|
|
45
45
|
*/
|
|
46
46
|
export function formatSessionTokens(tokens, percent, theme, compactions = 0) {
|
|
47
47
|
const tokenStr = formatTokens(tokens);
|
|
@@ -51,15 +51,15 @@ export function formatSessionTokens(tokens, percent, theme, compactions = 0) {
|
|
|
51
51
|
annot.push(theme.fg(color, `${Math.round(percent)}%`));
|
|
52
52
|
}
|
|
53
53
|
if (compactions > 0) {
|
|
54
|
-
annot.push(theme.fg("dim",
|
|
54
|
+
annot.push(theme.fg("dim", `⇊${compactions}`));
|
|
55
55
|
}
|
|
56
56
|
if (annot.length === 0)
|
|
57
57
|
return tokenStr;
|
|
58
58
|
return `${tokenStr} (${annot.join(" · ")})`;
|
|
59
59
|
}
|
|
60
|
-
/** Format turn count with optional max limit: "
|
|
60
|
+
/** Format turn count with optional max limit: "↻5≤30" or "↻5". */
|
|
61
61
|
export function formatTurns(turnCount, maxTurns) {
|
|
62
|
-
return maxTurns != null ?
|
|
62
|
+
return maxTurns != null ? `↻${turnCount}≤${maxTurns}` : `↻${turnCount}`;
|
|
63
63
|
}
|
|
64
64
|
/** Format milliseconds as human-readable duration. */
|
|
65
65
|
export function formatMs(ms) {
|
|
@@ -18,14 +18,22 @@ export declare class ConversationViewer implements Component {
|
|
|
18
18
|
private activity;
|
|
19
19
|
private theme;
|
|
20
20
|
private done;
|
|
21
|
+
/** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
|
|
22
|
+
private onStop?;
|
|
21
23
|
private scrollOffset;
|
|
22
24
|
private autoScroll;
|
|
23
25
|
private unsubscribe;
|
|
24
26
|
private lastInnerW;
|
|
25
27
|
private closed;
|
|
26
|
-
|
|
28
|
+
/** Two-press confirm guard for the stop key, so a stray key can't kill the agent. */
|
|
29
|
+
private stopArmed;
|
|
30
|
+
constructor(tui: TUI, session: AgentSession, record: AgentRecord, activity: AgentActivity | undefined, theme: Theme, done: (result: undefined) => void,
|
|
31
|
+
/** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
|
|
32
|
+
onStop?: (() => void) | undefined);
|
|
27
33
|
handleInput(data: string): void;
|
|
28
34
|
render(width: number): string[];
|
|
35
|
+
/** Stoppable only when a stop handler exists and the agent is still active. */
|
|
36
|
+
private isStoppable;
|
|
29
37
|
invalidate(): void;
|
|
30
38
|
dispose(): void;
|
|
31
39
|
private viewportHeight;
|
|
@@ -20,18 +20,24 @@ export class ConversationViewer {
|
|
|
20
20
|
activity;
|
|
21
21
|
theme;
|
|
22
22
|
done;
|
|
23
|
+
onStop;
|
|
23
24
|
scrollOffset = 0;
|
|
24
25
|
autoScroll = true;
|
|
25
26
|
unsubscribe;
|
|
26
27
|
lastInnerW = 0;
|
|
27
28
|
closed = false;
|
|
28
|
-
|
|
29
|
+
/** Two-press confirm guard for the stop key, so a stray key can't kill the agent. */
|
|
30
|
+
stopArmed = false;
|
|
31
|
+
constructor(tui, session, record, activity, theme, done,
|
|
32
|
+
/** Abort the agent shown here. Omitted → no stop affordance (e.g. read-only history). */
|
|
33
|
+
onStop) {
|
|
29
34
|
this.tui = tui;
|
|
30
35
|
this.session = session;
|
|
31
36
|
this.record = record;
|
|
32
37
|
this.activity = activity;
|
|
33
38
|
this.theme = theme;
|
|
34
39
|
this.done = done;
|
|
40
|
+
this.onStop = onStop;
|
|
35
41
|
this.unsubscribe = session.subscribe(() => {
|
|
36
42
|
if (this.closed)
|
|
37
43
|
return;
|
|
@@ -44,6 +50,23 @@ export class ConversationViewer {
|
|
|
44
50
|
this.done(undefined);
|
|
45
51
|
return;
|
|
46
52
|
}
|
|
53
|
+
// Stop/abort the agent (only while it can still be stopped). Two-press:
|
|
54
|
+
// first "x" arms, second confirms — any other key disarms.
|
|
55
|
+
if (matchesKey(data, "x")) {
|
|
56
|
+
if (this.isStoppable()) {
|
|
57
|
+
if (this.stopArmed) {
|
|
58
|
+
this.stopArmed = false;
|
|
59
|
+
this.onStop?.();
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
this.stopArmed = true;
|
|
63
|
+
}
|
|
64
|
+
this.tui.requestRender();
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (this.stopArmed)
|
|
69
|
+
this.stopArmed = false;
|
|
47
70
|
const totalLines = this.buildContentLines(this.lastInnerW).length;
|
|
48
71
|
const viewportHeight = this.viewportHeight();
|
|
49
72
|
const maxScroll = Math.max(0, totalLines - viewportHeight);
|
|
@@ -132,12 +155,22 @@ export class ConversationViewer {
|
|
|
132
155
|
? "100%"
|
|
133
156
|
: `${Math.round(((visibleStart + viewportHeight) / contentLines.length) * 100)}%`;
|
|
134
157
|
const footerLeft = th.fg("dim", `${contentLines.length} lines · ${scrollPct}`);
|
|
135
|
-
const
|
|
158
|
+
const scrollHint = th.fg("dim", "↑↓ scroll · PgUp/PgDn or Shift+↑↓ · Esc close");
|
|
159
|
+
// Stop hint goes first in the right group so it survives right-edge
|
|
160
|
+
// truncation on narrow terminals (the scroll hint is the expendable part).
|
|
161
|
+
const footerRight = this.isStoppable()
|
|
162
|
+
? (this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop")) +
|
|
163
|
+
th.fg("dim", " · ") + scrollHint
|
|
164
|
+
: scrollHint;
|
|
136
165
|
const footerGap = Math.max(1, innerW - visibleWidth(footerLeft) - visibleWidth(footerRight));
|
|
137
166
|
lines.push(row(footerLeft + " ".repeat(footerGap) + footerRight));
|
|
138
167
|
lines.push(hrBot);
|
|
139
168
|
return lines;
|
|
140
169
|
}
|
|
170
|
+
/** Stoppable only when a stop handler exists and the agent is still active. */
|
|
171
|
+
isStoppable() {
|
|
172
|
+
return !!this.onStop && (this.record.status === "running" || this.record.status === "queued");
|
|
173
|
+
}
|
|
141
174
|
invalidate() { }
|
|
142
175
|
dispose() {
|
|
143
176
|
this.closed = true;
|
package/dist/worktree.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface WorktreeInfo {
|
|
|
10
10
|
path: string;
|
|
11
11
|
/** Branch name created for this worktree (if changes exist). */
|
|
12
12
|
branch: string;
|
|
13
|
+
/** Commit SHA that the worktree was created from. */
|
|
14
|
+
baseSha: string;
|
|
13
15
|
}
|
|
14
16
|
export interface WorktreeCleanupResult {
|
|
15
17
|
/** Whether changes were found in the worktree. */
|