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/index.ts
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Config:
|
|
13
13
|
* - Loaded from ~/.pi/agent/subagents-lite.json at session_start
|
|
14
|
-
* -
|
|
15
|
-
* -
|
|
14
|
+
* - ConfigStore owns config + session overrides + persistence + side effects
|
|
15
|
+
* - Tool execution and menus read/write through store
|
|
16
16
|
*
|
|
17
17
|
* Commands:
|
|
18
18
|
* - /agents: Management menu (model settings, concurrency, running agents, debug)
|
|
@@ -23,276 +23,13 @@
|
|
|
23
23
|
* - session_shutdown: Abort all, dispose manager
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
27
|
-
import
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
ExtensionCommandContext,
|
|
31
|
-
ExtensionContext,
|
|
32
|
-
} from "@earendil-works/pi-coding-agent";
|
|
33
|
-
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
34
|
-
import { registerAgents, getAvailableTypes, setAgentScanDirs } from "./agent-types.js";
|
|
35
|
-
import { scanAgentFilesInDir, mergeAgents } from "./agent-discovery.js";
|
|
36
|
-
import { AgentManager } from "./agent-manager.js";
|
|
37
|
-
import { AgentWidget, type UICtx } from "./ui/agent-widget.js";
|
|
38
|
-
import { showAgentsMainMenu } from "./menus.js";
|
|
39
|
-
import { loadConfig } from "./config-io.js";
|
|
40
|
-
import { executeAgentTool, executeStopAgentTool, toolCallListener, backgroundAgentIds, scheduleNudge } from "./tool-execution.js";
|
|
41
|
-
import { renderAgentToolCall, renderAgentToolResult, renderSubagentResult } from "./renderer.js";
|
|
42
|
-
import {
|
|
43
|
-
__config,
|
|
44
|
-
sessionOverrides,
|
|
45
|
-
agentActivity,
|
|
46
|
-
piInstance,
|
|
47
|
-
setConfig,
|
|
48
|
-
setManager,
|
|
49
|
-
clearManager,
|
|
50
|
-
setWidget,
|
|
51
|
-
setPiInstance,
|
|
52
|
-
setSessionCtx,
|
|
53
|
-
resetSessionOverrides,
|
|
54
|
-
resetLastToolsExpanded,
|
|
55
|
-
syncWidgetSettings,
|
|
56
|
-
syncCompactFromToolsExpanded,
|
|
57
|
-
getManager,
|
|
58
|
-
getWidget,
|
|
59
|
-
} from "./state.js";
|
|
60
|
-
|
|
61
|
-
// Re-exports for backward compatibility
|
|
62
|
-
export {
|
|
63
|
-
__config,
|
|
64
|
-
sessionOverrides,
|
|
65
|
-
agentActivity,
|
|
66
|
-
piInstance,
|
|
67
|
-
setShowCostEnabled,
|
|
68
|
-
syncWidgetSettings,
|
|
69
|
-
syncCompactFromToolsExpanded,
|
|
70
|
-
} from "./state.js";
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// ============================================================================
|
|
75
|
-
// Config loader — session_start handler logic
|
|
76
|
-
// ============================================================================
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Ensure the manager and widget singletons exist.
|
|
80
|
-
* Idempotent — safe to call on every session_start.
|
|
81
|
-
*/
|
|
82
|
-
function ensureManagerAndWidget(): void {
|
|
83
|
-
const currentManager = getManager();
|
|
84
|
-
const currentWidget = getWidget();
|
|
85
|
-
// Create manager if missing
|
|
86
|
-
if (!currentManager) {
|
|
87
|
-
const newManager = new AgentManager(
|
|
88
|
-
(record) => {
|
|
89
|
-
// Only nudge for background (async) agents — sync agents already returned via tool result
|
|
90
|
-
if (backgroundAgentIds.has(record.id)) {
|
|
91
|
-
scheduleNudge(record.id);
|
|
92
|
-
backgroundAgentIds.delete(record.id);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Mark finished and update widget BEFORE deleting activity —
|
|
96
|
-
// renderFinishedLine reads activity for turn count, tokens, etc.
|
|
97
|
-
getWidget()?.markFinished(record.id);
|
|
98
|
-
getWidget()?.update();
|
|
99
|
-
|
|
100
|
-
// Remove from live activity tracking
|
|
101
|
-
agentActivity.delete(record.id);
|
|
102
|
-
},
|
|
103
|
-
__config.concurrency,
|
|
104
|
-
);
|
|
105
|
-
setManager(newManager);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Create widget if missing (uses existing or newly created manager)
|
|
109
|
-
if (!currentWidget) {
|
|
110
|
-
const newWidget = new AgentWidget(getManager(), agentActivity);
|
|
111
|
-
newWidget.setShowCost(__config.agent.showCost === true);
|
|
112
|
-
setWidget(newWidget);
|
|
113
|
-
syncWidgetSettings();
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Scan agent files from user and project directories, merge with defaults,
|
|
119
|
-
* and register into the type registry.
|
|
120
|
-
*/
|
|
121
|
-
async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
122
|
-
const homeDir = process.env.HOME || "";
|
|
123
|
-
const userAgentDir = path.join(homeDir, ".pi", "agent", "agents");
|
|
124
|
-
const projectAgentDir = path.join(ctx.cwd, ".pi", "agents");
|
|
125
|
-
|
|
126
|
-
// Store scan dirs for on-demand discovery (agents added during the session)
|
|
127
|
-
setAgentScanDirs(userAgentDir, projectAgentDir);
|
|
128
|
-
|
|
129
|
-
const [userAgents, projectAgents] = await Promise.all([
|
|
130
|
-
scanAgentFilesInDir(userAgentDir, "user"),
|
|
131
|
-
scanAgentFilesInDir(projectAgentDir, "project"),
|
|
132
|
-
]);
|
|
133
|
-
|
|
134
|
-
// Merge with defaults
|
|
135
|
-
const merged = mergeAgents(DEFAULT_AGENTS, userAgents, projectAgents);
|
|
136
|
-
|
|
137
|
-
// Register into the type registry
|
|
138
|
-
registerAgents(merged);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
142
|
-
setConfig(loadConfig());
|
|
143
|
-
ensureManagerAndWidget();
|
|
144
|
-
await scanAndRegisterAgents(ctx);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// ============================================================================
|
|
150
|
-
// Agent tool registration helper — dynamic enum for agent types
|
|
151
|
-
// ============================================================================
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Register (or re-register) the Agent tool with current agent types.
|
|
155
|
-
* At init time only defaults exist; call again from session_start after
|
|
156
|
-
* user/project agents are loaded to update the enum.
|
|
157
|
-
*/
|
|
158
|
-
function registerAgentTool(pi: ExtensionAPI): void {
|
|
159
|
-
const types = getAvailableTypes();
|
|
160
|
-
// Use plain string to avoid verbose anyOf in prompt.
|
|
161
|
-
// Available types are listed in description for discoverability.
|
|
162
|
-
const agentParam = types.length > 0
|
|
163
|
-
? Type.Optional(Type.String({ description: types.join(",") }))
|
|
164
|
-
: Type.Optional(Type.String());
|
|
165
|
-
// @ts-expect-error — description removed to save prompt tokens
|
|
166
|
-
pi.registerTool({
|
|
167
|
-
name: "Agent",
|
|
168
|
-
label: "Agent",
|
|
169
|
-
parameters: Type.Object({
|
|
170
|
-
prompt: Type.String(),
|
|
171
|
-
description: Type.String(),
|
|
172
|
-
agent: agentParam,
|
|
173
|
-
run_in_background: Type.Optional(Type.Boolean()),
|
|
174
|
-
worktree_path: Type.Optional(Type.String()),
|
|
175
|
-
}),
|
|
176
|
-
execute: executeAgentTool,
|
|
177
|
-
|
|
178
|
-
renderCall: (args, theme) => renderAgentToolCall(args as Record<string, unknown>, theme),
|
|
179
|
-
|
|
180
|
-
renderResult: (result, options, theme) => renderAgentToolResult(
|
|
181
|
-
result as { content: Array<{ type: string; text?: string }>; details?: Record<string, unknown>; isError?: boolean },
|
|
182
|
-
options as { expanded?: boolean },
|
|
183
|
-
theme,
|
|
184
|
-
),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// ============================================================================
|
|
189
|
-
// Extension factory
|
|
190
|
-
// ============================================================================
|
|
26
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import { setPiInstance } from "./shell.js";
|
|
28
|
+
import { registerTools } from "./registration.js";
|
|
29
|
+
import { setupEventListeners } from "./events.js";
|
|
191
30
|
|
|
192
31
|
export default function (pi: ExtensionAPI) {
|
|
193
|
-
// Store pi for execute callbacks
|
|
194
32
|
setPiInstance(pi);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Tool registration (stealth schemas — at init time)
|
|
198
|
-
// ========================================================================
|
|
199
|
-
|
|
200
|
-
// Agent tool — stealth schema with dynamic agent type enum
|
|
201
|
-
registerAgentTool(pi);
|
|
202
|
-
|
|
203
|
-
// StopAgent tool — stealth schema, stop a running agent by ID
|
|
204
|
-
// @ts-expect-error — description removed to save prompt tokens
|
|
205
|
-
pi.registerTool({
|
|
206
|
-
name: "StopAgent",
|
|
207
|
-
label: "StopAgent",
|
|
208
|
-
parameters: Type.Object({
|
|
209
|
-
agent_id: Type.String(),
|
|
210
|
-
}),
|
|
211
|
-
execute: executeStopAgentTool,
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// Message renderer — subagent-result (background agent completion)
|
|
215
|
-
pi.registerMessageRenderer("subagent-result", (message, options, theme) =>
|
|
216
|
-
renderSubagentResult(
|
|
217
|
-
message as { content?: string; details?: Record<string, unknown> },
|
|
218
|
-
options as { expanded?: boolean },
|
|
219
|
-
theme,
|
|
220
|
-
),
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
// Command registration
|
|
224
|
-
pi.registerCommand("agents", {
|
|
225
|
-
description: "Manage subagents: agent briefing, model settings, concurrency, running agents, agent types",
|
|
226
|
-
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
227
|
-
const modelOptions = ctx.modelRegistry.getAvailable().map((m) => `${m.provider}/${m.id}`);
|
|
228
|
-
await showAgentsMainMenu(ctx, modelOptions);
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Event listeners
|
|
233
|
-
pi.on("tool_call", toolCallListener);
|
|
234
|
-
|
|
235
|
-
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
236
|
-
// Set UI context on first tool execution
|
|
237
|
-
if (!getWidget()) {
|
|
238
|
-
ensureManagerAndWidget();
|
|
239
|
-
}
|
|
240
|
-
getWidget()?.setUICtx(ctx.ui as unknown as UICtx);
|
|
241
|
-
getWidget()?.onTurnStart();
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// session_start — load config, scan agents, register into registry,
|
|
247
|
-
// then re-register Agent tool with dynamic agent type enum
|
|
248
|
-
// Listen for ctrl+o keypress to sync compact mode (push-based, no polling)
|
|
249
|
-
let unregisterTerminalInput: (() => void) | undefined;
|
|
250
|
-
|
|
251
|
-
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => {
|
|
252
|
-
setSessionCtx(ctx);
|
|
253
|
-
resetSessionOverrides();
|
|
254
|
-
agentActivity.clear();
|
|
255
|
-
resetLastToolsExpanded();
|
|
256
|
-
await loadConfigAndRegisterAgents(ctx);
|
|
257
|
-
// Re-register with updated agent type list (now includes user/project agents)
|
|
258
|
-
registerAgentTool(pi);
|
|
259
|
-
// Register ctrl+o listener
|
|
260
|
-
if (ctx.hasUI && !unregisterTerminalInput) {
|
|
261
|
-
unregisterTerminalInput = ctx.ui.onTerminalInput((data: string) => {
|
|
262
|
-
// ctrl+o = 0x0F (15) — toggles tool expansion
|
|
263
|
-
if (data === "\u000f") {
|
|
264
|
-
// Read state after a tick to let the built-in handler process it first
|
|
265
|
-
setTimeout(() => {
|
|
266
|
-
const ui = ctx.ui as unknown as { getToolsExpanded?: () => boolean };
|
|
267
|
-
const expanded = ui.getToolsExpanded?.();
|
|
268
|
-
if (expanded !== undefined) {
|
|
269
|
-
getWidget()?.notifyToolsExpansionChanged(expanded);
|
|
270
|
-
}
|
|
271
|
-
}, 0);
|
|
272
|
-
}
|
|
273
|
-
return undefined; // Don't consume the input
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
// Sync compact mode with initial tool expansion state
|
|
277
|
-
syncCompactFromToolsExpanded(false);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
pi.on("session_shutdown", async (_event: unknown, ctx: ExtensionContext) => {
|
|
281
|
-
// Warn if agents were killed
|
|
282
|
-
const currentManager = getManager();
|
|
283
|
-
if (currentManager) {
|
|
284
|
-
const records = currentManager.listAgents();
|
|
285
|
-
const active = records.filter(r => r.lifecycle.status === "running" || r.lifecycle.status === "queued");
|
|
286
|
-
if (active.length > 0 && ctx.hasUI) {
|
|
287
|
-
ctx.ui.notify(`${active.length} agent(s) killed by reload`, "warning");
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
getWidget()?.dispose();
|
|
291
|
-
setWidget(undefined);
|
|
292
|
-
const mgr = getManager();
|
|
293
|
-
if (mgr) {
|
|
294
|
-
await mgr.dispose();
|
|
295
|
-
clearManager();
|
|
296
|
-
}
|
|
297
|
-
});
|
|
33
|
+
registerTools(pi);
|
|
34
|
+
setupEventListeners(pi);
|
|
298
35
|
}
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* 6. parentModelId (inherit from parent)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { ThinkingLevel } from "../types.js";
|
|
16
|
+
import type { SystemPromptMode } from "../agents/types.js";
|
|
17
|
+
|
|
15
18
|
/** Shape of the subagents-lite.json config file. */
|
|
16
19
|
export interface SubagentsConfig {
|
|
17
20
|
agent: {
|
|
@@ -23,6 +26,36 @@ export interface SubagentsConfig {
|
|
|
23
26
|
widgetMaxLinesCompact?: number;
|
|
24
27
|
widgetCompact?: boolean;
|
|
25
28
|
widgetShortcut?: boolean;
|
|
29
|
+
/** System prompt mode: replace (default), inherit parent, or custom file. */
|
|
30
|
+
systemPromptMode?: SystemPromptMode;
|
|
31
|
+
/** Whether to include AGENTS.md context files in the subagent system prompt. Default: true. */
|
|
32
|
+
includeContextFiles?: boolean;
|
|
33
|
+
/** Default thinking level for spawned agents. Undefined = inherit from agent config. */
|
|
34
|
+
defaultThinking?: ThinkingLevel;
|
|
35
|
+
/** Default max turns for spawned agents. Undefined = unlimited. */
|
|
36
|
+
defaultMaxTurns?: number;
|
|
37
|
+
/** Global default for skills loading when agent doesn't explicitly set skills. true (default) or false. */
|
|
38
|
+
loadSkillsImplicitly?: boolean;
|
|
39
|
+
/** Global default for extensions loading when agent doesn't explicitly set extensions. true (default) or false. */
|
|
40
|
+
loadExtensionsImplicitly?: boolean;
|
|
41
|
+
/** When true, skip built-in default agents (general-purpose, Explore) at registration. */
|
|
42
|
+
disableDefaultAgents?: boolean;
|
|
43
|
+
/** Whether to show toolUses count in widget stats line. Default: true. */
|
|
44
|
+
showTools?: boolean;
|
|
45
|
+
/** Whether to show turn count in widget stats line. Default: true. */
|
|
46
|
+
showTurns?: boolean;
|
|
47
|
+
/** Whether to show input tokens in widget stats line. Default: true. */
|
|
48
|
+
showInput?: boolean;
|
|
49
|
+
/** Whether to show output tokens in widget stats line. Default: true. */
|
|
50
|
+
showOutput?: boolean;
|
|
51
|
+
/** Whether to show context percent and compactions in widget stats line. Default: true. */
|
|
52
|
+
showContext?: boolean;
|
|
53
|
+
/** Whether to show elapsed time in widget stats line. Default: true. */
|
|
54
|
+
showTime?: boolean;
|
|
55
|
+
/** Max description length in widget full mode. Default: 50. */
|
|
56
|
+
widgetDescLengthFull?: number;
|
|
57
|
+
/** Max description length in widget compact mode. Default: 30. */
|
|
58
|
+
widgetDescLengthCompact?: number;
|
|
26
59
|
[agentType: string]: string | null | undefined | boolean | number;
|
|
27
60
|
};
|
|
28
61
|
concurrency: {
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
Text,
|
|
16
16
|
} from "@earendil-works/pi-tui";
|
|
17
17
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import type { Theme } from "
|
|
18
|
+
import type { Theme } from "../ui/agent-widget.js";
|
|
19
19
|
|
|
20
20
|
/* ------------------------------------------------------------------ */
|
|
21
21
|
/* Types */
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* buildSnapshotMarkdown: format agent conversation as markdown for snapshot viewer.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { summarizeToolArgs } from "
|
|
8
|
+
import { summarizeToolArgs } from "../ui/format.js";
|
|
9
9
|
|
|
10
10
|
function isTextBlock(c: unknown): c is { type: "text"; text: string } {
|
|
11
11
|
return typeof c === "object" && c !== null && (c as Record<string, unknown>).type === "text";
|
|
@@ -0,0 +1,180 @@
|
|
|
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 { EnvInfo } from "../types.js";
|
|
9
|
+
import type { AgentConfig, SystemPromptMode } from "../agents/types.js";
|
|
10
|
+
import type { SkillMeta, PreloadedSkill } from "./skill-loader.js";
|
|
11
|
+
import { formatSkillsForPrompt, type Skill } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
/** Extra sections to inject into the system prompt (skills). */
|
|
14
|
+
export interface PromptExtras {
|
|
15
|
+
/** Preloaded skill contents to inject (full content + description). */
|
|
16
|
+
skillBlocks?: PreloadedSkill[];
|
|
17
|
+
/** Skill metadata for whitelist display (name, description, location only). */
|
|
18
|
+
skillMetas?: SkillMeta[];
|
|
19
|
+
/** Parent system prompt (for inherit mode). */
|
|
20
|
+
parentSystemPrompt?: string;
|
|
21
|
+
/** Custom system prompt content (for custom mode). */
|
|
22
|
+
customSystemPrompt?: string;
|
|
23
|
+
/** Project context files (AGENTS.md) for custom mode. */
|
|
24
|
+
contextFiles?: Array<{ path: string; content: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Strip pi scaffolding sections from a parent system prompt.
|
|
29
|
+
*
|
|
30
|
+
* In inherit mode, the parent's prompt already contains:
|
|
31
|
+
* - <project_context>...</project_context> (AGENTS.md)
|
|
32
|
+
* - Skills block (text intro + <available_skills>...</available_skills>)
|
|
33
|
+
* - Current date: YYYY-MM-DD
|
|
34
|
+
* - Current working directory: /path
|
|
35
|
+
*
|
|
36
|
+
* These are re-added by subagents-lite from the subagent's own config,
|
|
37
|
+
* so we strip them to avoid duplication.
|
|
38
|
+
*
|
|
39
|
+
* @param prompt The parent system prompt to clean.
|
|
40
|
+
* @returns The prompt with scaffolding sections removed.
|
|
41
|
+
*/
|
|
42
|
+
function stripScaffolding(prompt: string): string {
|
|
43
|
+
let result = prompt;
|
|
44
|
+
|
|
45
|
+
// 1. Strip <project_context>...</project_context> block
|
|
46
|
+
result = result.replace(/\n?<\s*project_context\s*>[\s\S]*?<\/\s*project_context\s*>\n?/g, "\n");
|
|
47
|
+
|
|
48
|
+
// 2. Strip skills block: optional intro text + <available_skills>...</available_skills>
|
|
49
|
+
result = result.replace(/\n?(?:The following skills provide[\s\S]*?)?<\s*available_skills\s*>[\s\S]*?<\/\s*available_skills\s*>\n?/g, "\n");
|
|
50
|
+
|
|
51
|
+
// 3. Strip Current date: line
|
|
52
|
+
result = result.replace(/\n?Current date:.*\n?/g, "\n");
|
|
53
|
+
|
|
54
|
+
// 4. Strip Current working directory: line
|
|
55
|
+
result = result.replace(/\n?Current working directory:.*\n?/g, "\n");
|
|
56
|
+
|
|
57
|
+
// Clean up: collapse runs of 3+ newlines into 2
|
|
58
|
+
result = result.replace(/\n{3,}/g, "\n\n");
|
|
59
|
+
|
|
60
|
+
return result.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Build the system prompt for an agent from its config.
|
|
65
|
+
*
|
|
66
|
+
* Three modes:
|
|
67
|
+
* - replace (default): generic header + env + agent's systemPrompt
|
|
68
|
+
* - inherit: parent's system prompt (stripped of scaffolding) + env + agent's systemPrompt
|
|
69
|
+
* - custom: content of ~/.pi/agent/subagents-lite-prompt.md + env + agent's systemPrompt
|
|
70
|
+
*
|
|
71
|
+
* Agent's own systemPrompt is always included in <agent_instructions> tags.
|
|
72
|
+
*
|
|
73
|
+
* @param config Agent configuration.
|
|
74
|
+
* @param cwd Current working directory.
|
|
75
|
+
* @param env Environment info.
|
|
76
|
+
* @param extras Optional extra sections to inject (skills, parent/custom prompts).
|
|
77
|
+
* @param mode System prompt mode (replace, inherit, custom).
|
|
78
|
+
*/
|
|
79
|
+
export function buildAgentPrompt(
|
|
80
|
+
config: AgentConfig,
|
|
81
|
+
cwd: string,
|
|
82
|
+
env: EnvInfo,
|
|
83
|
+
extras?: PromptExtras,
|
|
84
|
+
mode: SystemPromptMode = "replace",
|
|
85
|
+
): string {
|
|
86
|
+
const envLines = [
|
|
87
|
+
"# Environment",
|
|
88
|
+
`Working directory: ${cwd}`,
|
|
89
|
+
env.isGitRepo ? "Git repository: yes" : "Not a git repository",
|
|
90
|
+
];
|
|
91
|
+
if (env.isGitRepo && env.branch) {
|
|
92
|
+
envLines.push(`Branch: ${env.branch}`);
|
|
93
|
+
}
|
|
94
|
+
envLines.push(`Platform: ${env.platform}`);
|
|
95
|
+
const envBlock = envLines.join("\n");
|
|
96
|
+
|
|
97
|
+
// Unified skill index — all skills in one <available_skills> block
|
|
98
|
+
const hasSkills = extras?.skillMetas?.length || extras?.skillBlocks?.length;
|
|
99
|
+
let extrasSuffix = "";
|
|
100
|
+
if (hasSkills) {
|
|
101
|
+
const skillLines: string[] = [];
|
|
102
|
+
|
|
103
|
+
// Location-based skills: use Pi's formatSkillsForPrompt for XML escaping and
|
|
104
|
+
// disable-model-invocation filtering, then extract the <skill> elements.
|
|
105
|
+
if (extras?.skillMetas?.length) {
|
|
106
|
+
const piSkills: Skill[] = extras.skillMetas.map((m) => ({
|
|
107
|
+
name: m.name,
|
|
108
|
+
description: m.description,
|
|
109
|
+
filePath: m.location,
|
|
110
|
+
baseDir: "",
|
|
111
|
+
sourceInfo: {} as any,
|
|
112
|
+
disableModelInvocation: m.disableModelInvocation,
|
|
113
|
+
}));
|
|
114
|
+
const formatted = formatSkillsForPrompt(piSkills);
|
|
115
|
+
const skillElements = formatted.match(/<skill>[\s\S]*?<\/skill>/g);
|
|
116
|
+
if (skillElements) skillLines.push(...skillElements);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Preloaded skills: content tags (not in Pi's formatSkillsForPrompt)
|
|
120
|
+
for (const skill of extras?.skillBlocks ?? []) {
|
|
121
|
+
skillLines.push(`<skill><name>${escapeXml(skill.name)}</name><description>${escapeXml(skill.description)}</description><content>${escapeXml(skill.content)}</content></skill>`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const lines = [
|
|
125
|
+
"The following skills provide specialized instructions for specific tasks.",
|
|
126
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
127
|
+
"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.",
|
|
128
|
+
"",
|
|
129
|
+
"<available_skills>",
|
|
130
|
+
...skillLines,
|
|
131
|
+
"</available_skills>",
|
|
132
|
+
];
|
|
133
|
+
extrasSuffix = `\n\n${lines.join("\n")}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Agent's own system prompt wrapped in <agent_instructions> tags
|
|
137
|
+
const agentInstructions = `\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`;
|
|
138
|
+
|
|
139
|
+
// Project context files (AGENTS.md) — placed after agent_instructions, before extras
|
|
140
|
+
let contextSuffix = "";
|
|
141
|
+
if (extras?.contextFiles?.length) {
|
|
142
|
+
const lines = [
|
|
143
|
+
"<project_context>",
|
|
144
|
+
"",
|
|
145
|
+
"Project-specific instructions and guidelines:",
|
|
146
|
+
"",
|
|
147
|
+
];
|
|
148
|
+
for (const file of extras.contextFiles) {
|
|
149
|
+
lines.push(`<project_instructions path="${escapeXml(file.path)}">`);
|
|
150
|
+
lines.push(file.content);
|
|
151
|
+
lines.push(`</project_instructions>`);
|
|
152
|
+
lines.push("");
|
|
153
|
+
}
|
|
154
|
+
lines.push("</project_context>");
|
|
155
|
+
contextSuffix = `\n\n${lines.join("\n")}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Build base prompt: mode-specific header if provided, otherwise default
|
|
159
|
+
const activeAgentTag = `<active_agent name="${config.name}"/>`;
|
|
160
|
+
const rawHeader = mode === "inherit" ? extras?.parentSystemPrompt
|
|
161
|
+
: mode === "custom" ? extras?.customSystemPrompt
|
|
162
|
+
: undefined;
|
|
163
|
+
// Parent/custom headers carry pi's scaffolding (context, skills, date, cwd);
|
|
164
|
+
// strip it — we re-add these from the subagent's own config. (rawHeader is
|
|
165
|
+
// undefined in replace mode, so nothing to strip there.)
|
|
166
|
+
const customHeader = rawHeader ? stripScaffolding(rawHeader) : rawHeader;
|
|
167
|
+
const basePrompt = customHeader
|
|
168
|
+
? `${customHeader}\n\n${envBlock}`
|
|
169
|
+
: `You are a Pi, an expert coding sub-agent.\nYou have been invoked to handle a specific task autonomously.\n\n${envBlock}`;
|
|
170
|
+
|
|
171
|
+
// active_agent goes AFTER shared prefix (header + env + context) for KV cache
|
|
172
|
+
return `${basePrompt}${contextSuffix}\n${activeAgentTag}\n${agentInstructions}${extrasSuffix}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function escapeXml(value: string): string {
|
|
176
|
+
// Only escape < and > — enough for XML-like tags, keeps text readable for LLMs
|
|
177
|
+
return value
|
|
178
|
+
.replace(/</g, "<")
|
|
179
|
+
.replace(/>/g, ">");
|
|
180
|
+
}
|