pi-subagents-lite 1.2.0 → 1.4.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/README.md +184 -225
- package/package.json +1 -1
- package/src/{agent-discovery.ts → agents/agent-discovery.ts} +8 -5
- package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
- package/src/{agent-runner.ts → agents/agent-runner.ts} +115 -173
- package/src/agents/agent-status.ts +50 -0
- package/src/agents/agent-types.ts +339 -0
- package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
- package/src/{output-file.ts → agents/output-file.ts} +68 -1
- package/src/{tool-execution.ts → agents/tool-execution.ts} +61 -223
- package/src/agents/types.ts +54 -0
- package/src/{usage.ts → agents/usage.ts} +7 -0
- package/src/{config-io.ts → config/config-io.ts} +20 -3
- package/src/config/config-store.ts +472 -0
- package/src/config/types.ts +26 -0
- package/src/events.ts +185 -0
- package/src/index.ts +8 -271
- package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
- package/src/{model-selector.ts → models/model-selector.ts} +1 -1
- package/src/{context.ts → prompt/context.ts} +1 -1
- package/src/prompt/prompts.ts +180 -0
- package/src/prompt/skill-loader.ts +195 -0
- package/src/registration.ts +101 -0
- package/src/shell.ts +101 -0
- package/src/spawn/spawn-coordinator.ts +232 -0
- package/src/status-note.ts +10 -0
- package/src/types.ts +47 -71
- package/src/ui/agent-widget.ts +61 -49
- package/src/{format.ts → ui/format.ts} +64 -26
- package/src/ui/menu/helpers.ts +93 -0
- package/src/ui/menu/menu-concurrency.ts +192 -0
- package/src/ui/menu/menu-debug.ts +125 -0
- package/src/ui/menu/menu-model-settings.ts +208 -0
- package/src/ui/menu/menu-running-agents.ts +224 -0
- package/src/ui/menu/menu-spawn-options.ts +87 -0
- package/src/ui/menu/menu-spawn-wizard.ts +418 -0
- package/src/ui/menu/menu-system-prompt.ts +109 -0
- package/src/ui/menu/menu-widget-settings.ts +130 -0
- package/src/ui/menu/menus.ts +101 -0
- package/src/ui/menu/submenus/confirm.ts +47 -0
- package/src/ui/menu/submenus/model-select.ts +70 -0
- package/src/ui/menu/submenus/numeric-input.ts +98 -0
- package/src/ui/menu/wrappers/settings-list.ts +205 -0
- package/src/{renderer.ts → ui/renderer.ts} +7 -6
- package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
- package/src/ui/types.ts +11 -0
- package/src/agent-types.ts +0 -184
- package/src/config-mutator.ts +0 -183
- package/src/menus.ts +0 -1333
- package/src/prompts.ts +0 -94
- package/src/skill-loader.ts +0 -178
- package/src/state.ts +0 -83
- /package/src/{worktree-validator.ts → spawn/worktree-validator.ts} +0 -0
package/src/prompts.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* prompts.ts — System prompt builder for agents.
|
|
3
|
-
*
|
|
4
|
-
* Every agent gets a fresh context — no inherited parent identity.
|
|
5
|
-
* EnvInfo is imported from types.ts — branch is a string (empty when unknown).
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { AgentConfig, EnvInfo } from "./types.js";
|
|
9
|
-
import type { SkillMeta } from "./skill-loader.js";
|
|
10
|
-
|
|
11
|
-
/** Extra sections to inject into the system prompt (skills). */
|
|
12
|
-
export interface PromptExtras {
|
|
13
|
-
/** Preloaded skill contents to inject (full content). */
|
|
14
|
-
skillBlocks?: { name: string; content: string }[];
|
|
15
|
-
/** Skill metadata for whitelist display (name, description, location only). */
|
|
16
|
-
skillMetas?: SkillMeta[];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Build the system prompt for an agent from its config.
|
|
21
|
-
*
|
|
22
|
-
* Always uses fresh-context mode: env header + config.systemPrompt.
|
|
23
|
-
* Prepends an `<active_agent name=""/>` tag so downstream extensions
|
|
24
|
-
* (e.g. permission/policy systems) can resolve per-agent policy.
|
|
25
|
-
*
|
|
26
|
-
* @param extras Optional extra sections to inject (preloaded skills).
|
|
27
|
-
*/
|
|
28
|
-
export function buildAgentPrompt(
|
|
29
|
-
config: AgentConfig,
|
|
30
|
-
cwd: string,
|
|
31
|
-
env: EnvInfo,
|
|
32
|
-
extras?: PromptExtras,
|
|
33
|
-
): string {
|
|
34
|
-
const activeAgentTag = `<active_agent name="${config.name}"/>\n\n`;
|
|
35
|
-
|
|
36
|
-
const envLines = [
|
|
37
|
-
"# Environment",
|
|
38
|
-
`Working directory: ${cwd}`,
|
|
39
|
-
env.isGitRepo ? "Git repository: yes" : "Not a git repository",
|
|
40
|
-
];
|
|
41
|
-
if (env.isGitRepo && env.branch) {
|
|
42
|
-
envLines.push(`Branch: ${env.branch}`);
|
|
43
|
-
}
|
|
44
|
-
envLines.push(`Platform: ${env.platform}`);
|
|
45
|
-
const envBlock = envLines.join("\n");
|
|
46
|
-
|
|
47
|
-
// Build optional extras suffix (skills)
|
|
48
|
-
const extraSections: string[] = [];
|
|
49
|
-
|
|
50
|
-
// Skill metadata whitelist (like Pi's available_skills format)
|
|
51
|
-
if (extras?.skillMetas?.length) {
|
|
52
|
-
const lines = [
|
|
53
|
-
"The following skills provide specialized instructions for specific tasks.",
|
|
54
|
-
"Use the read tool to load a skill's file when the task matches its description.",
|
|
55
|
-
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
56
|
-
"",
|
|
57
|
-
"<available_skills>",
|
|
58
|
-
];
|
|
59
|
-
for (const skill of extras.skillMetas) {
|
|
60
|
-
lines.push(" <skill>");
|
|
61
|
-
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
62
|
-
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
63
|
-
lines.push(` <location>${escapeXml(skill.location)}</location>`);
|
|
64
|
-
lines.push(" </skill>");
|
|
65
|
-
}
|
|
66
|
-
lines.push("</available_skills>");
|
|
67
|
-
extraSections.push(lines.join("\n"));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Preloaded skill contents (full dump into system prompt)
|
|
71
|
-
if (extras?.skillBlocks?.length) {
|
|
72
|
-
for (const skill of extras.skillBlocks) {
|
|
73
|
-
extraSections.push(`\n# Preloaded Skill: ${skill.name}\n${skill.content}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const extrasSuffix = extraSections.length > 0 ? `\n\n${extraSections.join("\n")}` : "";
|
|
78
|
-
|
|
79
|
-
const header = `You are a pi coding agent sub-agent.
|
|
80
|
-
You have been invoked to handle a specific task autonomously.
|
|
81
|
-
|
|
82
|
-
${envBlock}`;
|
|
83
|
-
|
|
84
|
-
return `${activeAgentTag}${header}\n\n${config.systemPrompt}${extrasSuffix}`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function escapeXml(value: string): string {
|
|
88
|
-
// Only escape < and > — enough for XML-like tags, keeps text readable for LLMs
|
|
89
|
-
return value
|
|
90
|
-
.replace(/</g, "<")
|
|
91
|
-
.replace(/>/g, ">");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
package/src/skill-loader.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* skill-loader.ts — Preload named skills.
|
|
3
|
-
*
|
|
4
|
-
* Roots, in precedence order:
|
|
5
|
-
* - <cwd>/.pi/skills (project, Pi's standard)
|
|
6
|
-
* - <cwd>/.agents/skills (project, cross-tool Agent Skills spec — https://agentskills.io)
|
|
7
|
-
* - getAgentDir()/skills (user, default ~/.pi/agent/skills — Pi's standard)
|
|
8
|
-
* - ~/.agents/skills (user, cross-tool Agent Skills spec)
|
|
9
|
-
* - ~/.pi/skills (legacy global, pre-Pi)
|
|
10
|
-
*
|
|
11
|
-
* Layout per root:
|
|
12
|
-
* - <root>/<name>.md (flat file at the top level)
|
|
13
|
-
* - <root>/.../<name>/SKILL.md (directory skill, may be nested — Pi's standard)
|
|
14
|
-
*
|
|
15
|
-
* Recursion skips dotfile entries and node_modules. A directory that itself contains
|
|
16
|
-
* SKILL.md is a skill — we don't descend into it (Pi: skills don't nest).
|
|
17
|
-
*
|
|
18
|
-
* Symlinks are rejected for security (deviation from Pi, which follows them).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import type { Dirent } from "node:fs";
|
|
22
|
-
import { existsSync, readdirSync } from "node:fs";
|
|
23
|
-
import { homedir } from "node:os";
|
|
24
|
-
import { join } from "node:path";
|
|
25
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
26
|
-
import { isSymlink, isUnsafeName, safeReadFile } from "./utils.js";
|
|
27
|
-
|
|
28
|
-
interface PreloadedSkill {
|
|
29
|
-
name: string;
|
|
30
|
-
content: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface SkillMeta {
|
|
34
|
-
name: string;
|
|
35
|
-
description: string;
|
|
36
|
-
location: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Skill search roots in precedence order (project → user → legacy).
|
|
41
|
-
* Shared by preloadSkills and loadSkillMeta.
|
|
42
|
-
*/
|
|
43
|
-
function getSkillRoots(cwd: string): string[] {
|
|
44
|
-
return [
|
|
45
|
-
join(cwd, ".pi", "skills"), // project — Pi standard
|
|
46
|
-
join(cwd, ".agents", "skills"), // project — Agent Skills spec
|
|
47
|
-
join(getAgentDir(), "skills"), // user — Pi standard
|
|
48
|
-
join(homedir(), ".agents", "skills"), // user — Agent Skills spec
|
|
49
|
-
join(homedir(), ".pi", "skills"), // legacy global, pre-Pi
|
|
50
|
-
];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function preloadSkills(skillNames: string[], cwd: string): PreloadedSkill[] {
|
|
54
|
-
return skillNames.map((name) => ({ name, content: loadSkillContent(name, cwd) }));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Load skill metadata only (name, description, location) without full content.
|
|
59
|
-
* Used for the skills whitelist — agent can read full content on-demand.
|
|
60
|
-
*/
|
|
61
|
-
export function loadSkillMeta(skillNames: string[], cwd: string): SkillMeta[] {
|
|
62
|
-
return skillNames.map((name) => {
|
|
63
|
-
const location = findSkillLocation(name, cwd);
|
|
64
|
-
if (!location) {
|
|
65
|
-
return { name, description: `(Skill "${name}" not found)`, location: "" };
|
|
66
|
-
}
|
|
67
|
-
const description = extractDescription(location);
|
|
68
|
-
return { name, description, location };
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function loadSkillContent(name: string, cwd: string): string {
|
|
73
|
-
if (isUnsafeName(name)) {
|
|
74
|
-
return `(Skill "${name}" skipped: name contains path traversal characters)`;
|
|
75
|
-
}
|
|
76
|
-
for (const root of getSkillRoots(cwd)) {
|
|
77
|
-
const content = findInRoot(root, name, "content");
|
|
78
|
-
if (content !== undefined) return content;
|
|
79
|
-
}
|
|
80
|
-
return `(Skill "${name}" not found in .pi/skills/, .agents/skills/, or global skill locations)`;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function findInRoot(root: string, name: string, mode: "content" | "location"): string | undefined {
|
|
84
|
-
if (isSymlink(root)) return undefined;
|
|
85
|
-
const flatPath = join(root, `${name}.md`);
|
|
86
|
-
if (mode === "location") {
|
|
87
|
-
if (existsSync(flatPath)) return flatPath;
|
|
88
|
-
} else {
|
|
89
|
-
const content = safeReadFile(flatPath)?.trim();
|
|
90
|
-
if (content !== undefined) return content;
|
|
91
|
-
}
|
|
92
|
-
return findSkillDirectory(root, name, mode);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* BFS under `root` for a directory named `name` containing `SKILL.md`.
|
|
97
|
-
* Pi-conforming filters. Returns either the file content or the file path.
|
|
98
|
-
*/
|
|
99
|
-
function findSkillDirectory(root: string, name: string, mode: "content" | "location"): string | undefined {
|
|
100
|
-
if (!existsSync(root)) return undefined;
|
|
101
|
-
const queue: string[] = [root];
|
|
102
|
-
|
|
103
|
-
while (queue.length > 0) {
|
|
104
|
-
const current = queue.shift();
|
|
105
|
-
if (current === undefined) continue;
|
|
106
|
-
|
|
107
|
-
let entries: Dirent[];
|
|
108
|
-
try {
|
|
109
|
-
entries = readdirSync(current, { withFileTypes: true });
|
|
110
|
-
} catch {
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Deterministic byte-order traversal — locale-independent.
|
|
115
|
-
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
116
|
-
|
|
117
|
-
for (const entry of entries) {
|
|
118
|
-
if (!entry.isDirectory()) continue;
|
|
119
|
-
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
120
|
-
|
|
121
|
-
// Symlinked dirs already filtered by entry.isDirectory() — Dirent uses lstat semantics.
|
|
122
|
-
const path = join(current, entry.name);
|
|
123
|
-
const skillMd = join(path, "SKILL.md");
|
|
124
|
-
const isSkillDir = existsSync(skillMd);
|
|
125
|
-
|
|
126
|
-
if (isSkillDir) {
|
|
127
|
-
if (entry.name === name) {
|
|
128
|
-
if (mode === "location") return skillMd;
|
|
129
|
-
const content = safeReadFile(skillMd)?.trim();
|
|
130
|
-
if (content !== undefined) return content;
|
|
131
|
-
}
|
|
132
|
-
continue; // Pi rule: skills don't nest — don't descend into a skill dir
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
queue.push(path);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return undefined;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Find skill file location without reading content.
|
|
143
|
-
* Returns the full path to the SKILL.md or .md file, or undefined if not found.
|
|
144
|
-
*/
|
|
145
|
-
function findSkillLocation(name: string, cwd: string): string | undefined {
|
|
146
|
-
if (isUnsafeName(name)) return undefined;
|
|
147
|
-
for (const root of getSkillRoots(cwd)) {
|
|
148
|
-
const location = findInRoot(root, name, "location");
|
|
149
|
-
if (location !== undefined) return location;
|
|
150
|
-
}
|
|
151
|
-
return undefined;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/** Extract description from SKILL.md frontmatter. */
|
|
155
|
-
function extractDescription(filePath: string): string {
|
|
156
|
-
try {
|
|
157
|
-
const content = safeReadFile(filePath);
|
|
158
|
-
if (!content) return "(no description)";
|
|
159
|
-
|
|
160
|
-
// Simple frontmatter extraction
|
|
161
|
-
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
162
|
-
if (!normalized.startsWith("---\n")) return "(no description)";
|
|
163
|
-
const endIndex = normalized.indexOf("\n---\n", 4);
|
|
164
|
-
if (endIndex === -1) return "(no description)";
|
|
165
|
-
|
|
166
|
-
const yamlString = normalized.slice(4, endIndex);
|
|
167
|
-
// Simple extraction of description field
|
|
168
|
-
const descMatch = yamlString.match(/^description:\s*["']?(.+?)["']?\s*$/m);
|
|
169
|
-
if (descMatch && descMatch[1]) {
|
|
170
|
-
// Truncate long descriptions
|
|
171
|
-
const desc = descMatch[1].trim();
|
|
172
|
-
return desc.length > 200 ? desc.slice(0, 197) + "..." : desc;
|
|
173
|
-
}
|
|
174
|
-
return "(no description)";
|
|
175
|
-
} catch {
|
|
176
|
-
return "(error reading description)";
|
|
177
|
-
}
|
|
178
|
-
}
|
package/src/state.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* state.ts — Shared module state. Extracted from index.ts to break circular deps.
|
|
3
|
-
*
|
|
4
|
-
* manager and widget use holders because they're reassigned after import and the
|
|
5
|
-
* PI runtime doesn't propagate ESM live binding reassignments.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ExtensionContext, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import type { SessionModelOverrides, SubagentsConfig } from "./model-precedence.js";
|
|
10
|
-
import { DEFAULT_CONFIG } from "./config-io.js";
|
|
11
|
-
import { AgentManager } from "./agent-manager.js";
|
|
12
|
-
import { AgentWidget, type AgentActivity } from "./ui/agent-widget.js";
|
|
13
|
-
|
|
14
|
-
export let sessionOverrides: SessionModelOverrides = { default: null };
|
|
15
|
-
export let __config: SubagentsConfig = { ...DEFAULT_CONFIG, agent: { ...DEFAULT_CONFIG.agent }, concurrency: { ...DEFAULT_CONFIG.concurrency } };
|
|
16
|
-
export const agentActivity = new Map<string, AgentActivity>();
|
|
17
|
-
export let piInstance: ExtensionAPI;
|
|
18
|
-
/** Stored ExtensionContext from session_start — used by menu spawn flow. */
|
|
19
|
-
export let sessionCtx: ExtensionContext;
|
|
20
|
-
|
|
21
|
-
// Holder objects — PI runtime doesn't propagate ESM live binding reassignments
|
|
22
|
-
const managerHolder: { current?: AgentManager } = {};
|
|
23
|
-
const widgetHolder: { current?: AgentWidget } = {};
|
|
24
|
-
|
|
25
|
-
export function setConfig(config: SubagentsConfig): void { __config = config; }
|
|
26
|
-
export function resetSessionOverrides(): void { sessionOverrides = { default: null }; }
|
|
27
|
-
export function setManager(m: AgentManager): void { managerHolder.current = m; }
|
|
28
|
-
export function clearManager(): void { managerHolder.current = undefined; }
|
|
29
|
-
export function setWidget(w: AgentWidget | undefined): void { widgetHolder.current = w; }
|
|
30
|
-
export function setPiInstance(pi: ExtensionAPI): void { piInstance = pi; }
|
|
31
|
-
export function setSessionCtx(ctx: ExtensionContext): void { sessionCtx = ctx; }
|
|
32
|
-
export function getManager(): AgentManager { return managerHolder.current!; }
|
|
33
|
-
export function getWidget(): AgentWidget | undefined { return widgetHolder.current; }
|
|
34
|
-
|
|
35
|
-
// State mutation helpers
|
|
36
|
-
|
|
37
|
-
/** Update the cost display toggle in config and sync to widget. */
|
|
38
|
-
export function setShowCostEnabled(enabled: boolean): void {
|
|
39
|
-
__config.agent.showCost = enabled;
|
|
40
|
-
getWidget()?.setShowCost(enabled);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Sync widget display settings from config to the widget instance. */
|
|
44
|
-
export function syncWidgetSettings(): void {
|
|
45
|
-
const w = getWidget();
|
|
46
|
-
if (!w) return;
|
|
47
|
-
w.setForceCompact(__config.agent.widgetCompact === true);
|
|
48
|
-
w.setWidgetShortcut(__config.agent.widgetShortcut === true);
|
|
49
|
-
w.setMaxLines(__config.agent.widgetMaxLines ?? 12);
|
|
50
|
-
w.setMaxLinesCompact(
|
|
51
|
-
__config.agent.widgetMaxLinesCompact ?? Math.floor((__config.agent.widgetMaxLines ?? 12) / 2),
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Track previous tool expansion state to detect ctrl+o toggle. */
|
|
56
|
-
let lastToolsExpanded: boolean | undefined;
|
|
57
|
-
|
|
58
|
-
/** Reset lastToolsExpanded (called at session_start). */
|
|
59
|
-
export function resetLastToolsExpanded(): void {
|
|
60
|
-
lastToolsExpanded = undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Sync compact mode with the tool expansion state (ctrl+o toggle).
|
|
64
|
-
* Only syncs when widgetShortcut is enabled in config (opt-in behavior).
|
|
65
|
-
* Only triggers on state change (not every tool_execution_start).
|
|
66
|
-
* When forceCompact (widgetCompact) is ON, ignores ctrl+o state changes.
|
|
67
|
-
*/
|
|
68
|
-
export function syncCompactFromToolsExpanded(expanded: boolean): void {
|
|
69
|
-
if (__config.agent.widgetShortcut !== true) {
|
|
70
|
-
lastToolsExpanded = expanded;
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
// When forceCompact is ON, ignore ctrl+o state changes
|
|
74
|
-
if (__config.agent.widgetCompact === true) {
|
|
75
|
-
lastToolsExpanded = expanded;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
// Tools expanded → widget full, tools collapsed → widget compact
|
|
79
|
-
if (lastToolsExpanded !== undefined && lastToolsExpanded !== expanded) {
|
|
80
|
-
getWidget()?.setCompactMode(!expanded);
|
|
81
|
-
}
|
|
82
|
-
lastToolsExpanded = expanded;
|
|
83
|
-
}
|
|
File without changes
|