pi-subagents-lite 0.2.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.
@@ -0,0 +1,435 @@
1
+ /**
2
+ * agent-runner.ts — Core execution engine: creates sessions, runs agents, collects results.
3
+ *
4
+ * Forked from upstream pi-subagents. Key modifications:
5
+ * - Removed buildParentContext import and inheritContext code path
6
+ * - Removed buildMemoryBlock/buildReadOnlyMemoryBlock imports and memory code paths
7
+ * - Replaced import { detectEnv } from env.ts with inline git detection via pi.exec()
8
+ * - Handles `isolated` parameter internally (sets extensions=false, skills=false)
9
+ * - RunOptions: keeps pi: ExtensionAPI, isolated?: boolean. Removes inheritContext, isolation
10
+ * - PromptExtras: removed memoryBlock — keeps skillBlocks[] only
11
+ * - EXCLUDED_TOOL_NAMES prevents sub-subagent spawning
12
+ */
13
+
14
+ import type { Model } from "@earendil-works/pi-ai";
15
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
16
+ import {
17
+ type AgentSession,
18
+ type AgentSessionEvent,
19
+ createAgentSession,
20
+ DefaultResourceLoader,
21
+ type ExtensionAPI,
22
+ getAgentDir,
23
+ SessionManager,
24
+ SettingsManager,
25
+ } from "@earendil-works/pi-coding-agent";
26
+ import { getAgentConfig, getConfig, getToolNamesForType } from "./agent-types.js";
27
+ import { extractText } from "./context.js";
28
+ import { DEFAULT_AGENTS } from "./default-agents.js";
29
+ import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
30
+ import { preloadSkills } from "./skill-loader.js";
31
+ import type { EnvInfo, SubagentType, ThinkingLevel } from "./types.js";
32
+
33
+ /** Names of tools registered by this extension that subagents must NOT inherit. */
34
+ export const EXCLUDED_TOOL_NAMES = ["Agent"];
35
+
36
+ /** Additional turns allowed after the soft limit steer message. */
37
+ const GRACE_TURNS = 5;
38
+
39
+ /** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
40
+ function normalizeMaxTurns(n: number | undefined): number | undefined {
41
+ if (n == null || n === 0) return undefined;
42
+ return Math.max(1, n);
43
+ }
44
+
45
+ /**
46
+ * Try to find the right model for an agent type.
47
+ * Priority: explicit option > config.model > parent model.
48
+ */
49
+ function resolveDefaultModel(
50
+ parentModel: Model<any> | undefined,
51
+ registry: { find(provider: string, modelId: string): Model<any> | undefined },
52
+ configModel?: string,
53
+ ): Model<any> | undefined {
54
+ if (configModel) {
55
+ const slashIdx = configModel.indexOf("/");
56
+ if (slashIdx !== -1) {
57
+ const provider = configModel.slice(0, slashIdx);
58
+ const modelId = configModel.slice(slashIdx + 1);
59
+ const found = registry.find(provider, modelId);
60
+ if (found) return found;
61
+ }
62
+ }
63
+
64
+ return parentModel;
65
+ }
66
+
67
+ /** Info about a tool event in the subagent. */
68
+ export interface ToolActivity {
69
+ type: "start" | "end";
70
+ toolName: string;
71
+ }
72
+
73
+ interface RunOptions {
74
+ /** ExtensionAPI instance — used for pi.exec() for git detection. */
75
+ pi: ExtensionAPI;
76
+ /** Manager-assigned id; suffixes session name to disambiguate parallel spawns (e.g. `Explore#a1b2c3d4`). */
77
+ agentId?: string;
78
+ model?: Model<any>;
79
+ maxTurns?: number;
80
+ signal?: AbortSignal;
81
+ /** When true, agent gets only built-in tools (no extensions, no skills). */
82
+ isolated?: boolean;
83
+ thinkingLevel?: ThinkingLevel;
84
+ /** Override working directory. */
85
+ cwd?: string;
86
+ /** Called on tool start/end with activity info. */
87
+ onToolActivity?: (activity: ToolActivity) => void;
88
+ /** Called on streaming text deltas from the assistant response. */
89
+ onTextDelta?: (delta: string, fullText: string) => void;
90
+ onSessionCreated?: (session: AgentSession) => void;
91
+ /** Called at the end of each agentic turn with the cumulative count. */
92
+ onTurnEnd?: (turnCount: number) => void;
93
+ /**
94
+ * Called once per assistant message_end with that message's usage delta.
95
+ * Lets callers maintain a lifetime accumulator that survives compaction
96
+ * (which replaces session.state.messages and resets stats-derived sums).
97
+ */
98
+ onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
99
+ /**
100
+ * Called when the session successfully compacts. `tokensBefore` is upstream's
101
+ * pre-compaction context size estimate. Aborted compactions don't fire.
102
+ */
103
+ onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
104
+ }
105
+
106
+ interface RunResult {
107
+ responseText: string;
108
+ session: AgentSession;
109
+ /** True if the agent was hard-aborted (max_turns + grace exceeded). */
110
+ aborted: boolean;
111
+ /** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */
112
+ steered: boolean;
113
+ }
114
+
115
+ /**
116
+ * Subscribe to a session and collect the last assistant message text.
117
+ * Returns an object with a `getText()` getter and an `unsubscribe` function.
118
+ */
119
+ function collectResponseText(
120
+ session: AgentSession,
121
+ onTextDelta?: (delta: string, fullText: string) => void,
122
+ ) {
123
+ let text = "";
124
+ const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
125
+ if (event.type === "message_start") {
126
+ text = "";
127
+ }
128
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
129
+ text += event.assistantMessageEvent.delta;
130
+ onTextDelta?.(event.assistantMessageEvent.delta, text);
131
+ }
132
+ });
133
+ return { getText: () => text, unsubscribe };
134
+ }
135
+
136
+ /** Get the last assistant text from the completed session history. */
137
+ function getLastAssistantText(session: AgentSession): string {
138
+ for (let i = session.messages.length - 1; i >= 0; i--) {
139
+ const msg = session.messages[i];
140
+ if (msg.role !== "assistant") continue;
141
+ const text = extractText(msg.content).trim();
142
+ if (text) return text;
143
+ }
144
+ return "";
145
+ }
146
+
147
+ /**
148
+ * Wire an AbortSignal to abort a session.
149
+ * Returns a cleanup function to remove the listener.
150
+ */
151
+ function forwardAbortSignal(session: AgentSession, signal?: AbortSignal): () => void {
152
+ if (!signal) return () => {};
153
+ const onAbort = () => session.abort();
154
+ signal.addEventListener("abort", onAbort, { once: true });
155
+ return () => signal.removeEventListener("abort", onAbort);
156
+ }
157
+
158
+ /**
159
+ * Subscribe to shared session events (tool activity, usage, compaction)
160
+ * used by both runAgent and resumeAgent. Returns an unsubscribe function.
161
+ */
162
+ function subscribeToSessionEvents(
163
+ session: AgentSession,
164
+ options: Pick<RunOptions, "onToolActivity" | "onAssistantUsage" | "onCompaction">,
165
+ ): () => void {
166
+ if (!options.onToolActivity && !options.onAssistantUsage && !options.onCompaction) {
167
+ return () => {};
168
+ }
169
+ return session.subscribe((event: AgentSessionEvent) => {
170
+ if (event.type === "tool_execution_start") {
171
+ options.onToolActivity?.({ type: "start", toolName: event.toolName });
172
+ }
173
+ if (event.type === "tool_execution_end") {
174
+ options.onToolActivity?.({ type: "end", toolName: event.toolName });
175
+ }
176
+ if (event.type === "message_end" && event.message.role === "assistant") {
177
+ const u = (event.message as any).usage;
178
+ if (u) {
179
+ options.onAssistantUsage?.({
180
+ input: u.input ?? 0,
181
+ output: u.output ?? 0,
182
+ cacheWrite: u.cacheWrite ?? 0,
183
+ });
184
+ }
185
+ }
186
+ if (event.type === "compaction_end" && !event.aborted && event.result) {
187
+ options.onCompaction?.({ reason: event.reason, tokensBefore: event.result.tokensBefore });
188
+ }
189
+ });
190
+ }
191
+
192
+ /**
193
+ * Filter active tools: remove extension tools to prevent nesting,
194
+ * apply extension allowlist if specified, and apply disallowedTools denylist.
195
+ * Returns null when no filtering is needed (isolated mode with no denylist).
196
+ */
197
+ function filterActiveTools(
198
+ activeTools: string[],
199
+ builtinToolNames: string[],
200
+ extensions: true | string[] | false,
201
+ disallowedTools?: string[],
202
+ ): string[] | null {
203
+ const disallowedSet = disallowedTools ? new Set(disallowedTools) : undefined;
204
+
205
+ if (extensions === false) {
206
+ // Isolated mode — only apply denylist to built-in tools
207
+ if (!disallowedSet) return null;
208
+ const filtered = activeTools.filter(t => !disallowedSet.has(t));
209
+ return filtered.length !== activeTools.length ? filtered : null;
210
+ }
211
+
212
+ const builtinToolNameSet = new Set(builtinToolNames);
213
+ return activeTools.filter((t) => {
214
+ if (EXCLUDED_TOOL_NAMES.includes(t)) return false;
215
+ if (disallowedSet?.has(t)) return false;
216
+ if (builtinToolNameSet.has(t)) return true;
217
+ if (Array.isArray(extensions)) {
218
+ return extensions.some(ext => t.startsWith(ext) || t.includes(ext));
219
+ }
220
+ return true;
221
+ });
222
+ }
223
+
224
+ /** Run a git command via pi.exec, returning stdout on success or null on failure. */
225
+ async function execGit(pi: ExtensionAPI, args: string[], cwd: string): Promise<string | null> {
226
+ try {
227
+ const result = await pi.exec("git", args, { cwd, timeout: 5000 });
228
+ return result.code === 0 ? result.stdout.trim() : null;
229
+ } catch {
230
+ return null;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Detect environment info using pi.exec() for git detection.
236
+ * Inline replacement for upstream's detectEnv from env.ts.
237
+ */
238
+ async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo> {
239
+ const gitRoot = await execGit(pi, ["rev-parse", "--is-inside-work-tree"], cwd);
240
+ const isGitRepo = gitRoot === "true";
241
+ const branch = isGitRepo ? (await execGit(pi, ["branch", "--show-current"], cwd)) : null;
242
+
243
+ return {
244
+ isGitRepo,
245
+ branch,
246
+ platform: process.platform,
247
+ };
248
+ }
249
+
250
+ export async function runAgent(
251
+ ctx: ExtensionContext,
252
+ type: SubagentType,
253
+ prompt: string,
254
+ options: RunOptions,
255
+ ): Promise<RunResult> {
256
+ const config = getConfig(type);
257
+ const agentConfig = getAgentConfig(type);
258
+
259
+ // Resolve working directory
260
+ const effectiveCwd = options.cwd ?? ctx.cwd;
261
+
262
+ const env = await detectEnv(options.pi, effectiveCwd);
263
+
264
+ // Resolve extensions/skills: isolated overrides to false
265
+ const extensions = options.isolated ? false : config.extensions;
266
+ const skills = options.isolated ? false : config.skills;
267
+
268
+ // Build prompt extras (no memoryBlock — skills only).
269
+ // When skills is string[], preload their content into the prompt.
270
+ const extras: PromptExtras = Array.isArray(skills)
271
+ ? { skillBlocks: preloadSkills(skills, effectiveCwd) }
272
+ : {};
273
+
274
+ const toolNames = getToolNamesForType(type);
275
+
276
+ // Build system prompt from agent config
277
+ let systemPrompt: string;
278
+ if (agentConfig) {
279
+ systemPrompt = buildAgentPrompt(agentConfig, effectiveCwd, env, extras);
280
+ } else {
281
+ // Unknown type fallback: spread the canonical general-purpose config
282
+ const fallback = DEFAULT_AGENTS.get("general-purpose");
283
+ if (!fallback) throw new Error(`No fallback config available for unknown type "${type}"`);
284
+ systemPrompt = buildAgentPrompt({ ...fallback, name: type }, effectiveCwd, env, extras);
285
+ }
286
+
287
+ // When skills is string[], they're already preloaded into the prompt.
288
+ // Pass noSkills: true to prevent the skill loader from loading them again.
289
+ const skipSkillLoader = skills === false || Array.isArray(skills);
290
+
291
+ const agentDir = getAgentDir();
292
+
293
+ // Load extensions/skills: true or string[] → load; false → don't.
294
+ const loader = new DefaultResourceLoader({
295
+ cwd: effectiveCwd,
296
+ agentDir,
297
+ noExtensions: extensions === false,
298
+ noSkills: skipSkillLoader,
299
+ noPromptTemplates: true,
300
+ noThemes: true,
301
+ noContextFiles: true,
302
+ systemPromptOverride: () => systemPrompt,
303
+ appendSystemPromptOverride: () => [],
304
+ });
305
+ await loader.reload();
306
+
307
+ // Resolve model: explicit option > config.model > parent model
308
+ const model = options.model ?? resolveDefaultModel(
309
+ ctx.model, ctx.modelRegistry, agentConfig?.model,
310
+ );
311
+
312
+ // Resolve thinking level: explicit option > agent config > undefined (inherit)
313
+ const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
314
+
315
+ const sessionOpts: Parameters<typeof createAgentSession>[0] = {
316
+ cwd: effectiveCwd,
317
+ agentDir,
318
+ sessionManager: SessionManager.inMemory(effectiveCwd),
319
+ settingsManager: SettingsManager.create(effectiveCwd, agentDir),
320
+ modelRegistry: ctx.modelRegistry,
321
+ model,
322
+ tools: toolNames,
323
+ resourceLoader: loader,
324
+ };
325
+ if (thinkingLevel) {
326
+ sessionOpts.thinkingLevel = thinkingLevel;
327
+ }
328
+
329
+ const { session } = await createAgentSession(sessionOpts);
330
+
331
+ const baseSessionName = agentConfig?.name ?? type;
332
+ session.setSessionName(
333
+ options.agentId ? `${baseSessionName}#${options.agentId.slice(0, 8)}` : baseSessionName,
334
+ );
335
+
336
+ // Filter active tools: remove our own tools to prevent nesting,
337
+ // apply extension allowlist if specified, and apply disallowedTools denylist
338
+ const filteredTools = filterActiveTools(
339
+ session.getActiveToolNames(),
340
+ toolNames,
341
+ extensions,
342
+ agentConfig?.disallowedTools,
343
+ );
344
+ if (filteredTools) {
345
+ session.setActiveToolsByName(filteredTools);
346
+ }
347
+
348
+ // Bind extensions so that session_start fires and extensions can initialize
349
+ await session.bindExtensions({
350
+ onError: (err) => {
351
+ options.onToolActivity?.({
352
+ type: "end",
353
+ toolName: `extension-error:${err.extensionPath}`,
354
+ });
355
+ },
356
+ });
357
+
358
+ options.onSessionCreated?.(session);
359
+
360
+ // Track turns for graceful max_turns enforcement
361
+ let turnCount = 0;
362
+ const maxTurns = normalizeMaxTurns(options.maxTurns ?? agentConfig?.maxTurns);
363
+ let softLimitReached = false;
364
+ let aborted = false;
365
+
366
+ const unsubEvents = subscribeToSessionEvents(session, options);
367
+
368
+ const unsubTurns = session.subscribe((event: AgentSessionEvent) => {
369
+ if (event.type === "turn_end") {
370
+ turnCount++;
371
+ options.onTurnEnd?.(turnCount);
372
+ if (maxTurns == null) return;
373
+ if (!softLimitReached && turnCount >= maxTurns) {
374
+ softLimitReached = true;
375
+ session.steer("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
376
+ } else if (softLimitReached && turnCount >= maxTurns + GRACE_TURNS) {
377
+ aborted = true;
378
+ session.abort();
379
+ }
380
+ }
381
+ });
382
+
383
+ const collector = collectResponseText(session, options.onTextDelta);
384
+ const cleanupAbort = forwardAbortSignal(session, options.signal);
385
+
386
+ try {
387
+ await session.prompt(prompt);
388
+ } finally {
389
+ unsubTurns();
390
+ unsubEvents();
391
+ collector.unsubscribe();
392
+ cleanupAbort();
393
+ }
394
+
395
+ const responseText = collector.getText().trim() || getLastAssistantText(session);
396
+ return { responseText, session, aborted, steered: softLimitReached };
397
+ }
398
+
399
+ /**
400
+ * Send a new prompt to an existing session (resume).
401
+ */
402
+ export async function resumeAgent(
403
+ session: AgentSession,
404
+ prompt: string,
405
+ options: Pick<RunOptions, "onToolActivity" | "onAssistantUsage" | "onCompaction"> & { signal?: AbortSignal } = {},
406
+ ): Promise<string> {
407
+ const collector = collectResponseText(session);
408
+ const cleanupAbort = forwardAbortSignal(session, options.signal);
409
+ const unsubEvents = subscribeToSessionEvents(session, options);
410
+
411
+ try {
412
+ await session.prompt(prompt);
413
+ } finally {
414
+ collector.unsubscribe();
415
+ unsubEvents();
416
+ cleanupAbort();
417
+ }
418
+
419
+ return collector.getText().trim() || getLastAssistantText(session);
420
+ }
421
+
422
+ /**
423
+ * Send a steering message to a running subagent.
424
+ * The message will interrupt the agent after its current tool execution.
425
+ */
426
+ export async function steerAgent(
427
+ session: AgentSession,
428
+ message: string,
429
+ ): Promise<void> {
430
+ await session.steer(message);
431
+ }
432
+
433
+
434
+
435
+
@@ -0,0 +1,140 @@
1
+ /**
2
+ * agent-types.ts — Unified agent type registry.
3
+ *
4
+ * Merges embedded default agents with user-defined agents from .pi/agents/*.md.
5
+ * User agents override defaults with the same name. Disabled agents are kept but excluded from spawning.
6
+ *
7
+ * Trimmed from upstream: removed getMemoryToolNames(), getReadOnlyMemoryToolNames(),
8
+ * MEMORY_TOOL_NAMES, READONLY_MEMORY_TOOL_NAMES (memory feature cut).
9
+ */
10
+
11
+ import { DEFAULT_AGENTS } from "./default-agents.js";
12
+ import type { AgentConfig } from "./types.js";
13
+
14
+ /** All known built-in tool names. */
15
+ export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
16
+
17
+ /** Unified runtime registry of all agents (defaults + user-defined). */
18
+ const agents = new Map<string, AgentConfig>();
19
+
20
+ /**
21
+ * Register agents into the unified registry.
22
+ * Starts with DEFAULT_AGENTS, then overlays user agents (overrides defaults with same name).
23
+ * Disabled agents (enabled === false) are kept in the registry but excluded from spawning.
24
+ */
25
+ export function registerAgents(userAgents: Map<string, AgentConfig>): void {
26
+ agents.clear();
27
+
28
+ // Start with defaults
29
+ for (const [name, config] of DEFAULT_AGENTS) {
30
+ agents.set(name, config);
31
+ }
32
+
33
+ // Overlay user agents (overrides defaults with same name)
34
+ for (const [name, config] of userAgents) {
35
+ agents.set(name, config);
36
+ }
37
+ }
38
+
39
+ /** Case-insensitive key resolution, also matches displayName. */
40
+ function resolveKey(name: string): string | undefined {
41
+ if (!name) return undefined;
42
+ if (agents.has(name)) return name;
43
+ const lower = name.toLowerCase();
44
+ for (const [key, config] of agents.entries()) {
45
+ if (key.toLowerCase() === lower) return key;
46
+ if ((config.displayName ?? '').toLowerCase() === lower) return key;
47
+ }
48
+ return undefined;
49
+ }
50
+
51
+ /** Resolve a type name case-insensitively. Returns the canonical key or undefined. */
52
+ export function resolveType(name: string): string | undefined {
53
+ return resolveKey(name);
54
+ }
55
+
56
+ /** Get the agent config for a type (case-insensitive). */
57
+ export function getAgentConfig(name: string): AgentConfig | undefined {
58
+ const key = resolveKey(name);
59
+ return key ? agents.get(key) : undefined;
60
+ }
61
+
62
+ /** Get all enabled type names (for spawning and tool descriptions). */
63
+ export function getAvailableTypes(): string[] {
64
+ return [...agents.entries()]
65
+ .filter(([_, config]) => config.enabled !== false)
66
+ .map(([name]) => name);
67
+ }
68
+
69
+ /** Get all type names including disabled (for UI listing). */
70
+ export function getAllTypes(): string[] {
71
+ return [...agents.keys()];
72
+ }
73
+
74
+ /** Get names of default agents currently in the registry. */
75
+ export function getDefaultAgentNames(): string[] {
76
+ return [...agents.entries()]
77
+ .filter(([_, config]) => config.isDefault === true)
78
+ .map(([name]) => name);
79
+ }
80
+
81
+ /** Get names of user-defined agents (non-defaults) currently in the registry. */
82
+ export function getUserAgentNames(): string[] {
83
+ return [...agents.entries()]
84
+ .filter(([_, config]) => config.isDefault !== true)
85
+ .map(([name]) => name);
86
+ }
87
+
88
+ /** Check if a type is valid and enabled (case-insensitive). */
89
+ export function isValidType(type: string): boolean {
90
+ const config = getAgentConfig(type);
91
+ return config !== undefined && config.enabled !== false;
92
+ }
93
+
94
+ /** Get built-in tool names for a type (case-insensitive). */
95
+ export function getToolNamesForType(type: string): string[] {
96
+ const config = getAgentConfig(type);
97
+ return config?.builtinToolNames?.length
98
+ ? config.builtinToolNames
99
+ : [...BUILTIN_TOOL_NAMES];
100
+ }
101
+
102
+ /** Convert an AgentConfig to the SubagentTypeConfig shape used by getConfig. */
103
+ function toSubagentTypeConfig(config: AgentConfig): {
104
+ displayName: string;
105
+ description: string;
106
+ builtinToolNames: string[];
107
+ extensions: true | string[] | false;
108
+ skills: true | string[] | false;
109
+ } {
110
+ return {
111
+ displayName: config.displayName ?? config.name,
112
+ description: config.description,
113
+ builtinToolNames: config.builtinToolNames ?? BUILTIN_TOOL_NAMES,
114
+ extensions: config.extensions,
115
+ skills: config.skills,
116
+ };
117
+ }
118
+
119
+ /** Get config for a type (case-insensitive). Falls back to general-purpose. */
120
+ export function getConfig(type: string): ReturnType<typeof toSubagentTypeConfig> {
121
+ const key = resolveKey(type);
122
+ const config = key ? agents.get(key) : undefined;
123
+
124
+ const activeConfig = (config?.enabled !== false)
125
+ ? config
126
+ : agents.get("general-purpose");
127
+
128
+ if (activeConfig && activeConfig.enabled !== false) {
129
+ return toSubagentTypeConfig(activeConfig);
130
+ }
131
+
132
+ // Absolute fallback — general-purpose was disabled or missing
133
+ return {
134
+ displayName: "Agent",
135
+ description: "General-purpose agent for complex, multi-step tasks",
136
+ builtinToolNames: BUILTIN_TOOL_NAMES,
137
+ extensions: true,
138
+ skills: true,
139
+ };
140
+ }
package/src/context.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * context.ts — Extract parent conversation context for subagent inheritance.
3
+ *
4
+ * Keep extractText only. buildParentContext removed (inherit_context is cut).
5
+ */
6
+
7
+ /** Extract text from a message content block array. */
8
+ export function extractText(content: unknown[]): string {
9
+ return content
10
+ .filter((c: any) => c.type === "text")
11
+ .map((c: any) => c.text ?? "")
12
+ .join("\n");
13
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * default-agents.ts — Embedded default agent configurations.
3
+ *
4
+ * These are always available but can be overridden by user .md files with the same name.
5
+ * Kept: general-purpose + Explore. Plan removed (user can create via .md file).
6
+ */
7
+
8
+ import type { AgentConfig } from "./types.js";
9
+
10
+ const READ_ONLY_TOOLS = ["read", "bash", "grep", "find", "ls"];
11
+
12
+ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
13
+ [
14
+ "general-purpose",
15
+ {
16
+ name: "general-purpose",
17
+ displayName: "Agent",
18
+ description: "General-purpose agent for complex, multi-step tasks",
19
+ // builtinToolNames omitted — means "all available tools" (resolved at lookup time)
20
+ extensions: true,
21
+ skills: true,
22
+ systemPrompt: "",
23
+ isDefault: true,
24
+ },
25
+ ],
26
+ [
27
+ "Explore",
28
+ {
29
+ name: "Explore",
30
+ displayName: "Explore",
31
+ description: "Fast codebase exploration agent (read-only)",
32
+ builtinToolNames: READ_ONLY_TOOLS,
33
+ extensions: true,
34
+ skills: true,
35
+ model: "anthropic/claude-haiku-4-5-20251001",
36
+ systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
37
+ You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
38
+ Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
39
+
40
+ You are STRICTLY PROHIBITED from:
41
+ - Creating new files
42
+ - Modifying existing files
43
+ - Deleting files
44
+ - Moving or copying files
45
+ - Creating temporary files anywhere, including /tmp
46
+ - Using redirect operators (>, >>, |) or heredocs to write to files
47
+ - Running ANY commands that change system state
48
+
49
+ Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find, cat, head, tail.
50
+
51
+ # Tool Usage
52
+ - Use the find tool for file pattern matching (NOT the bash find command)
53
+ - Use the grep tool for content search (NOT bash grep/rg command)
54
+ - Use the read tool for reading files (NOT bash cat/head/tail)
55
+ - Use Bash ONLY for read-only operations
56
+ - Make independent tool calls in parallel for efficiency
57
+ - Adapt search approach based on thoroughness level specified
58
+
59
+ # Output
60
+ - Use absolute file paths in all references
61
+ - Report findings as regular messages
62
+ - Do not use emojis
63
+ - Be thorough and precise`,
64
+ isDefault: true,
65
+ },
66
+ ],
67
+ ]);