pi-subagents-lite 1.3.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.
Files changed (53) hide show
  1. package/README.md +184 -235
  2. package/package.json +1 -1
  3. package/src/{agent-discovery.ts → agents/agent-discovery.ts} +8 -5
  4. package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
  5. package/src/{agent-runner.ts → agents/agent-runner.ts} +115 -173
  6. package/src/{agent-status.ts → agents/agent-status.ts} +4 -4
  7. package/src/agents/agent-types.ts +339 -0
  8. package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
  9. package/src/{output-file.ts → agents/output-file.ts} +68 -1
  10. package/src/{tool-execution.ts → agents/tool-execution.ts} +60 -222
  11. package/src/agents/types.ts +54 -0
  12. package/src/{usage.ts → agents/usage.ts} +7 -0
  13. package/src/{config-io.ts → config/config-io.ts} +20 -3
  14. package/src/config/config-store.ts +472 -0
  15. package/src/config/types.ts +26 -0
  16. package/src/events.ts +185 -0
  17. package/src/index.ts +8 -281
  18. package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
  19. package/src/{model-selector.ts → models/model-selector.ts} +1 -1
  20. package/src/{context.ts → prompt/context.ts} +1 -1
  21. package/src/prompt/prompts.ts +180 -0
  22. package/src/prompt/skill-loader.ts +195 -0
  23. package/src/registration.ts +101 -0
  24. package/src/shell.ts +101 -0
  25. package/src/spawn/spawn-coordinator.ts +232 -0
  26. package/src/status-note.ts +10 -0
  27. package/src/types.ts +47 -71
  28. package/src/ui/agent-widget.ts +61 -49
  29. package/src/{format.ts → ui/format.ts} +64 -26
  30. package/src/ui/menu/helpers.ts +93 -0
  31. package/src/ui/menu/menu-concurrency.ts +192 -0
  32. package/src/ui/menu/menu-debug.ts +125 -0
  33. package/src/ui/menu/menu-model-settings.ts +208 -0
  34. package/src/ui/menu/menu-running-agents.ts +224 -0
  35. package/src/ui/menu/menu-spawn-options.ts +87 -0
  36. package/src/ui/menu/menu-spawn-wizard.ts +418 -0
  37. package/src/ui/menu/menu-system-prompt.ts +109 -0
  38. package/src/ui/menu/menu-widget-settings.ts +130 -0
  39. package/src/ui/menu/menus.ts +101 -0
  40. package/src/ui/menu/submenus/confirm.ts +47 -0
  41. package/src/ui/menu/submenus/model-select.ts +70 -0
  42. package/src/ui/menu/submenus/numeric-input.ts +98 -0
  43. package/src/ui/menu/wrappers/settings-list.ts +205 -0
  44. package/src/{renderer.ts → ui/renderer.ts} +7 -6
  45. package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
  46. package/src/ui/types.ts +11 -0
  47. package/src/agent-types.ts +0 -184
  48. package/src/config-mutator.ts +0 -183
  49. package/src/menus.ts +0 -1333
  50. package/src/prompts.ts +0 -94
  51. package/src/skill-loader.ts +0 -178
  52. package/src/state.ts +0 -83
  53. /package/src/{worktree-validator.ts → spawn/worktree-validator.ts} +0 -0
@@ -11,8 +11,9 @@
11
11
 
12
12
  import * as fs from "node:fs";
13
13
  import * as path from "node:path";
14
- import type { AgentConfig, ThinkingLevel } from "./types.js";
15
- import { parseThinkingLevel } from "./utils.js";
14
+ import type { AgentConfig } from "./types.js";
15
+ import type { ThinkingLevel } from "../types.js";
16
+ import { parseThinkingLevel } from "../utils.js";
16
17
 
17
18
  /* ------------------------------------------------------------------ */
18
19
  /* Types */
@@ -32,6 +33,7 @@ export interface AgentConfigFromMd {
32
33
  model?: string;
33
34
  thinking?: ThinkingLevel;
34
35
  max_turns?: number;
36
+ max_tokens?: number;
35
37
  hidden?: boolean;
36
38
  systemPrompt: string;
37
39
  source: "user" | "project";
@@ -273,6 +275,7 @@ export function parseAgentFile(
273
275
  model: parseString(frontmatter, "model"),
274
276
  thinking: parseThinkingLevel(parseString(frontmatter, "thinking")),
275
277
  max_turns: parseNumber(frontmatter, "max_turns"),
278
+ max_tokens: parseNumber(frontmatter, "max_tokens"),
276
279
  hidden: parseBoolean(frontmatter, "hidden"),
277
280
  systemPrompt: body,
278
281
  source: source,
@@ -397,8 +400,9 @@ function fromMd(md: AgentConfigFromMd): Partial<AgentConfig> {
397
400
  skills: md.skills,
398
401
  preloadSkills: md.preload_skills,
399
402
  model: md.model,
400
- thinking: md.thinking,
403
+ thinkingLevel: md.thinking,
401
404
  maxTurns: md.max_turns,
405
+ maxTokens: md.max_tokens,
402
406
  hidden: md.hidden,
403
407
  systemPrompt: md.systemPrompt,
404
408
  source: md.source === "project" ? "project" : "global",
@@ -414,7 +418,6 @@ function fromMd(md: AgentConfigFromMd): Partial<AgentConfig> {
414
418
  const BASE_DEFAULTS: AgentConfig = {
415
419
  name: "unknown",
416
420
  description: "",
417
- extensions: true,
418
- skills: true,
421
+ // extensions and skills intentionally omitted — resolved by global default
419
422
  systemPrompt: "",
420
423
  };
@@ -5,21 +5,21 @@
5
5
  */
6
6
 
7
7
  import { randomUUID } from "node:crypto";
8
- import type { Model } from "@earendil-works/pi-ai";
9
8
  import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
10
- import { runAgent, type ToolActivity } from "./agent-runner.js";
11
- import { createOutputFilePath, streamToOutputFile, writeInitialEntry } from "./output-file.js";
9
+ import { runAgent } from "./agent-runner.js";
10
+ import { AgentOutputLog } from "./output-file.js";
12
11
  import {
13
- type AgentInvocation,
14
12
  type AgentRecord,
15
13
  type AgentStatus,
16
14
  type CompactionInfo,
15
+ type RunCallbacks,
17
16
  SHORT_ID_LENGTH,
18
- type SubagentType,
19
- type ThinkingLevel,
20
- } from "./types.js";
17
+ type SpawnConfig,
18
+ type ToolActivity,
19
+ } from "../types.js";
20
+ import type { SubagentType } from "./types.js";
21
21
  import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
22
- import { errorMessage } from "./utils.js";
22
+ import { errorMessage } from "../utils.js";
23
23
 
24
24
  /** How often to check for expired agent records (milliseconds). */
25
25
  const CLEANUP_INTERVAL_MS = 60_000;
@@ -35,27 +35,6 @@ const AGENT_ID_PREFIX_LENGTH = 17;
35
35
  /** Default per-model concurrency limit when not specified in config. */
36
36
  const DEFAULT_CONCURRENCY_LIMIT = 4;
37
37
 
38
- /**
39
- * Create a cleanup function for the output file stream.
40
- * Captures final stats from the record at cleanup time so the DONE line
41
- * reflects actual turn count, tool uses, and total tokens.
42
- */
43
- function createOutputCleanup(
44
- session: AgentSession,
45
- path: string,
46
- record: AgentRecord,
47
- ): () => void {
48
- const outputStats = { turnCount: 0, toolUseCount: 0, totalTokens: 0, cost: 0 };
49
- const cleanup = streamToOutputFile(session, path, outputStats);
50
- return () => {
51
- outputStats.turnCount = record.stats.turnCount ?? 0;
52
- outputStats.toolUseCount = record.stats.toolUses;
53
- outputStats.totalTokens = getLifetimeTotal(record.stats.lifetimeUsage);
54
- outputStats.cost = record.stats.lifetimeUsage.cost;
55
- cleanup();
56
- };
57
- }
58
-
59
38
  /** Whether the agent status is terminal (no longer running or queued). */
60
39
  function isTerminalStatus(status: AgentStatus): boolean {
61
40
  return status !== "running" && status !== "queued";
@@ -88,40 +67,10 @@ interface SpawnArgs {
88
67
  options: SpawnOptions;
89
68
  }
90
69
 
91
- export interface SpawnOptions {
92
- description: string;
93
- model?: Model<any>;
94
- maxTurns?: number;
95
- thinkingLevel?: ThinkingLevel;
70
+ export interface SpawnOptions extends SpawnConfig, RunCallbacks {
96
71
  isBackground?: boolean;
97
- /** Resolved worktree path — forwarded as cwd to runAgent. */
98
- worktreePath?: string;
99
- /** Short display label for the worktree (set on record display after spawn). */
100
- worktreeLabel?: string;
101
- /**
102
- * Model key for concurrency pool lookup (e.g. "llamacpp/4b_small").
103
- * When set, the agent is counted against that model's concurrency limit.
104
- * When unset, the agent bypasses per-model concurrency limits.
105
- */
106
- modelKey?: string;
107
- /** Resolved invocation snapshot captured for UI display. */
108
- invocation?: AgentInvocation;
109
72
  /** Parent abort signal — when aborted, the subagent is also stopped. */
110
73
  signal?: AbortSignal;
111
- /** Called on tool start/end with activity info (for streaming progress to UI). */
112
- onToolActivity?: (activity: ToolActivity) => void;
113
- /** Called on streaming text deltas from the assistant response. */
114
- onTextDelta?: (delta: string, fullText: string) => void;
115
- /** Called when the agent session is created (for accessing session stats). */
116
- onSessionCreated?: (session: AgentSession) => void;
117
- /** Called at the end of each agentic turn with the cumulative count. */
118
- onTurnEnd?: (turnCount: number) => void;
119
- /** Called once per assistant message_end with that message's usage delta. */
120
- onAssistantUsage?: (usage: LifetimeUsage) => void;
121
- /** Called when the session successfully compacts. */
122
- onCompaction?: (info: CompactionInfo) => void;
123
- /** Grace turns: extra turns allowed after hitting maxTurns. */
124
- graceTurns?: number;
125
74
  }
126
75
 
127
76
  export class AgentManager {
@@ -273,6 +222,7 @@ export class AgentManager {
273
222
  stats: {
274
223
  lifetimeUsage: { input: 0, output: 0, cacheWrite: 0, cost: 0 },
275
224
  toolUses: 0,
225
+ turnCount: 1,
276
226
  compactionCount: 0,
277
227
  maxTurns: options.maxTurns,
278
228
  },
@@ -307,9 +257,9 @@ export class AgentManager {
307
257
  record.lifecycle.status = "running";
308
258
  record.lifecycle.startedAt = Date.now();
309
259
 
310
- // Create output file for this agent
311
- record.display.outputFile = createOutputFilePath(id);
312
- writeInitialEntry(record.display.outputFile, prompt);
260
+ // Create output log for this agent (creates file + writes [USER] entry)
261
+ record.execution.outputLog = new AgentOutputLog(id, prompt);
262
+ record.display.outputFile = record.execution.outputLog.path;
313
263
 
314
264
  this.onStart?.(record);
315
265
 
@@ -323,6 +273,7 @@ export class AgentManager {
323
273
  agentId: id,
324
274
  model: options.model,
325
275
  maxTurns: options.maxTurns,
276
+ maxTokens: options.maxTokens,
326
277
  thinkingLevel: options.thinkingLevel,
327
278
  cwd: options.worktreePath,
328
279
  graceTurns: options.graceTurns,
@@ -345,19 +296,17 @@ export class AgentManager {
345
296
  }
346
297
  record.execution.pendingSteers = undefined;
347
298
  }
348
- // Stream session events to the output file
349
- if (record.display.outputFile) {
350
- record.execution.outputCleanup = createOutputCleanup(
351
- session, record.display.outputFile, record,
352
- );
299
+ // Attach output log stream to session
300
+ if (record.execution.outputLog) {
301
+ record.execution.outputLog.attach(session);
353
302
  }
354
303
  options.onSessionCreated?.(session);
355
304
  },
356
305
  })
357
- .then(({ responseText, session, aborted, steered }) => {
306
+ .then(({ responseText, session, aborted, turnLimited }) => {
358
307
  // Don't overwrite status if externally stopped via abort()
359
308
  if (record.lifecycle.status !== "stopped") {
360
- record.lifecycle.status = aborted ? "aborted" : steered ? "steered" : "completed";
309
+ record.lifecycle.status = aborted ? "aborted" : turnLimited ? "turn_limited" : "completed";
361
310
  }
362
311
  record.result = responseText;
363
312
  record.execution.session = session;
@@ -375,10 +324,17 @@ export class AgentManager {
375
324
  return "";
376
325
  })
377
326
  .finally(() => {
378
- // Final flush of streaming output file
379
- if (record.execution.outputCleanup) {
380
- try { record.execution.outputCleanup(); } catch { /* ignore */ }
381
- record.execution.outputCleanup = undefined;
327
+ // Finalize output log with final stats
328
+ if (record.execution.outputLog) {
329
+ try {
330
+ record.execution.outputLog.finalize({
331
+ turnCount: record.stats.turnCount ?? 0,
332
+ toolUseCount: record.stats.toolUses,
333
+ totalTokens: getLifetimeTotal(record.stats.lifetimeUsage),
334
+ cost: record.stats.lifetimeUsage.cost,
335
+ });
336
+ } catch { /* ignore */ }
337
+ record.execution.outputLog = undefined;
382
338
  }
383
339
 
384
340
  // Decrement per-model concurrency count
@@ -397,6 +353,10 @@ export class AgentManager {
397
353
  try { this.onComplete?.(record); } catch { /* ignore */ }
398
354
  }
399
355
 
356
+ setOnComplete(cb: OnAgentComplete): void {
357
+ this.onComplete = cb;
358
+ }
359
+
400
360
  /** Get the session-level cumulative agent cost. Survives agent eviction. */
401
361
  getTotalAgentCost(): number {
402
362
  return this.totalAgentCost;
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Core execution engine: creates sessions, runs agents, collects results.
3
3
  *
4
- * EXCLUDED_TOOL_NAMES prevents sub-subagent spawning.
4
+ * Tool visibility policy is owned by agent-types.ts (resolveVisibleTools).
5
5
  */
6
6
 
7
+ import fs from "node:fs";
7
8
  import path from "node:path";
8
- import type { Model } from "@earendil-works/pi-ai";
9
9
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
  import {
11
11
  type AgentSession,
@@ -14,23 +14,25 @@ import {
14
14
  DefaultResourceLoader,
15
15
  type ExtensionAPI,
16
16
  getAgentDir,
17
+ loadProjectContextFiles,
17
18
  SessionManager,
18
19
  SettingsManager,
19
20
  } from "@earendil-works/pi-coding-agent";
20
- import { getAgentConfig, getConfig, getToolNamesForType, BUILTIN_TOOL_NAMES } from "./agent-types.js";
21
- import { extractText } from "./context.js";
21
+ import { getAgentConfig, getConfig, getToolNamesForType, resolveVisibleTools } from "./agent-types.js";
22
+ import { extractText } from "../prompt/context.js";
22
23
  import type { LifetimeUsage } from "./usage.js";
23
- import { findModelInRegistry } from "./utils.js";
24
+ import { findModelInRegistry } from "../utils.js";
24
25
  import { DEFAULT_AGENTS } from "./default-agents.js";
25
- import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
26
- import { preloadSkills, loadSkillMeta, type SkillMeta } from "./skill-loader.js";
27
- import { type CompactionInfo, type EnvInfo, SHORT_ID_LENGTH, type SubagentType, type ThinkingLevel } from "./types.js";
26
+ import { buildAgentPrompt, type PromptExtras } from "../prompt/prompts.js";
27
+ import { preloadSkills, loadSkillMeta, type SkillMeta } from "../prompt/skill-loader.js";
28
+ import { type EnvInfo, type RunCallbacks, type RunTunables, SHORT_ID_LENGTH } from "../types.js";
29
+ import type { SubagentType, SystemPromptMode } from "./types.js";
30
+ import { getStore } from "../shell.js";
31
+ import { DEFAULT_GRACE_TURNS } from "../config/config-io.js";
28
32
 
29
- /** Names of tools registered by this extension that subagents must NOT inherit. */
30
- const EXCLUDED_TOOL_NAMES = ["Agent"];
33
+ /** Path to custom prompt file. Exported for use in menus.ts. */
34
+ export const CUSTOM_PROMPT_PATH = path.join(process.env.HOME || "", ".pi", "agent", "subagents-lite-prompt.md");
31
35
 
32
- /** Default grace turns when not specified in config. */
33
- const DEFAULT_GRACE_TURNS = 6;
34
36
 
35
37
  /** Timeout for quick git commands (branch detection, repo check). */
36
38
  const GIT_EXEC_TIMEOUT_MS = 5000;
@@ -42,42 +44,15 @@ function normalizeMaxTurns(n: number | undefined): number | undefined {
42
44
  }
43
45
 
44
46
  /** Info about a tool event in the subagent. */
45
- export interface ToolActivity {
46
- type: "start" | "end";
47
- toolName: string;
48
- }
49
-
50
- interface RunOptions {
47
+ interface RunOptions extends RunTunables, RunCallbacks {
51
48
  /** ExtensionAPI instance — used for pi.exec() for git detection. */
52
49
  pi: ExtensionAPI;
53
50
  /** Manager-assigned id; suffixes session name to disambiguate parallel spawns (e.g. `Explore#a1b2c3d4`). */
54
51
  agentId?: string;
55
- model?: Model<any>;
56
- maxTurns?: number;
57
- signal?: AbortSignal;
58
- thinkingLevel?: ThinkingLevel;
59
- /** Override working directory. */
52
+ /** Override working directory (resolved worktree path). */
60
53
  cwd?: string;
61
- /** Called on tool start/end with activity info. */
62
- onToolActivity?: (activity: ToolActivity) => void;
63
- /** Called on streaming text deltas from the assistant response. */
64
- onTextDelta?: (delta: string, fullText: string) => void;
65
- onSessionCreated?: (session: AgentSession) => void;
66
- /** Called at the end of each agentic turn with the cumulative count. */
67
- onTurnEnd?: (turnCount: number) => void;
68
- /**
69
- * Called once per assistant message_end with that message's usage delta.
70
- * Lets callers maintain a lifetime accumulator that survives compaction
71
- * (which replaces session.state.messages and resets stats-derived sums).
72
- */
73
- onAssistantUsage?: (usage: LifetimeUsage) => void;
74
- /**
75
- * Called when the session successfully compacts. `tokensBefore` is upstream's
76
- * pre-compaction context size estimate. Aborted compactions don't fire.
77
- */
78
- onCompaction?: (info: CompactionInfo) => void;
79
- /** Grace turns: extra turns allowed after hitting maxTurns. Defaults to 6. */
80
- graceTurns?: number;
54
+ /** Parent abort signal when aborted, the subagent is also stopped. */
55
+ signal?: AbortSignal;
81
56
  }
82
57
 
83
58
  interface RunResult {
@@ -85,8 +60,8 @@ interface RunResult {
85
60
  session: AgentSession;
86
61
  /** True if the agent was hard-aborted (max_turns + grace exceeded). */
87
62
  aborted: boolean;
88
- /** True if the agent was steered to wrap up (hit soft turn limit) but finished in time. */
89
- steered: boolean;
63
+ /** True if the agent hit the soft turn limit and wrapped up within grace turns. */
64
+ turnLimited: boolean;
90
65
  }
91
66
 
92
67
  /**
@@ -229,120 +204,6 @@ function extractExtensionName(extPath: string): string {
229
204
  return path.basename(path.dirname(extPath));
230
205
  }
231
206
 
232
- /**
233
- * Resolve tool entries (with ext/* syntax) into concrete tool names.
234
- * Returns a set of resolved tool names.
235
- */
236
- function resolveToolEntries(
237
- entries: string[],
238
- extToolMap: Map<string, string[]> | undefined,
239
- notify?: (msg: string) => void,
240
- ): Set<string> {
241
- const resolved = new Set<string>();
242
-
243
- for (const entry of entries) {
244
- const slashIdx = entry.indexOf("/");
245
- if (slashIdx !== -1) {
246
- // ext/* or ext/tool syntax
247
- const extName = entry.slice(0, slashIdx);
248
- const toolPart = entry.slice(slashIdx + 1);
249
- if (toolPart === "*") {
250
- const extTools = extToolMap?.get(extName);
251
- if (extTools && extTools.length > 0) {
252
- for (const t of extTools) resolved.add(t);
253
- } else {
254
- notify?.(`extension "${extName}" is not loaded, "${entry}" will have no effect`);
255
- }
256
- } else {
257
- // ext/tool syntax: e.g. "tavily/web_search"
258
- resolved.add(toolPart);
259
- }
260
- } else {
261
- // Bare tool name
262
- resolved.add(entry);
263
- }
264
- }
265
-
266
- return resolved;
267
- }
268
-
269
- /**
270
- * Filter active tools: apply tools allowlist/denylist and EXCLUDED_TOOL_NAMES.
271
- *
272
- * The `tools` config controls which tool schemas the LLM sees (built-in + extension).
273
- * The `extensions` config controls which extensions are loaded (hooks + commands).
274
- * `extensions` does NOT affect tool visibility — that's `tools`'s job.
275
- *
276
- * Supports ext/* syntax for both whitelist and blacklist modes.
277
- *
278
- * `tools` and `excludeTools` are mutually exclusive. If both set, `tools` wins.
279
- *
280
- * Returns null when no filtering is needed, otherwise the filtered tool list.
281
- */
282
- function filterActiveTools(
283
- activeTools: string[],
284
- extToolMap: Map<string, string[]> | undefined,
285
- tools: true | string[] | false | undefined,
286
- excludeTools: string[] | undefined,
287
- notify?: (msg: string) => void,
288
- ): string[] | null {
289
- // Blacklist mode: excludeTools set and tools not set as whitelist
290
- if (excludeTools && !Array.isArray(tools)) {
291
- const excludeSet = resolveToolEntries(excludeTools, extToolMap, notify);
292
- const filtered = activeTools.filter(t =>
293
- !EXCLUDED_TOOL_NAMES.includes(t) && !excludeSet.has(t)
294
- );
295
- return filtered.length !== activeTools.length ? filtered : null;
296
- }
297
-
298
- if (Array.isArray(tools)) {
299
- // Whitelist mode: resolve entries with ext/* expansion
300
- const allBuiltinSet = new Set(BUILTIN_TOOL_NAMES);
301
- const allowedTools = resolveToolEntries(tools, extToolMap, notify);
302
-
303
- // Warn about unknown entries
304
- for (const entry of tools) {
305
- const slashIdx = entry.indexOf("/");
306
- if (slashIdx === -1 && !allBuiltinSet.has(entry)) {
307
- // Bare name, not a known built-in — check if it's an extension tool
308
- const toolExts = extToolMap ? [...extToolMap.entries()].filter(([, tools]) => tools.includes(entry)) : [];
309
- if (toolExts.length === 0) {
310
- notify?.(`tool "${entry}" not found in any loaded extension`);
311
- }
312
- }
313
- }
314
-
315
- const visibleSet = new Set<string>();
316
- for (const t of activeTools) {
317
- if (EXCLUDED_TOOL_NAMES.includes(t)) continue;
318
- if (allowedTools.has(t)) {
319
- visibleSet.add(t);
320
- }
321
- }
322
-
323
- // Warn if a loaded extension has none of its tools in `tools`
324
- if (extToolMap) {
325
- for (const [extName, extTools] of extToolMap) {
326
- const hasAny = extTools.some(t => allowedTools.has(t));
327
- if (!hasAny) {
328
- notify?.(`extension "${extName}" is loaded but none of its tools are in tools: [${tools.join(", ")}]`);
329
- }
330
- }
331
- }
332
-
333
- return [...visibleSet];
334
- }
335
-
336
- if (tools === false) {
337
- return [];
338
- }
339
-
340
- // tools: true or undefined — all tools visible (except excluded)
341
- const hasExcluded = activeTools.some(t => EXCLUDED_TOOL_NAMES.includes(t));
342
- if (!hasExcluded) return null;
343
- return activeTools.filter(t => !EXCLUDED_TOOL_NAMES.includes(t));
344
- }
345
-
346
207
  /** Run a git command via pi.exec, returning stdout on success or null on failure. */
347
208
  async function execGit(pi: ExtensionAPI, args: string[], cwd: string): Promise<string | null> {
348
209
  try {
@@ -371,8 +232,62 @@ async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo> {
371
232
 
372
233
  // ── runAgent phases ────────────────────────────────────────────────
373
234
 
235
+ /**
236
+ * Resolve system prompt mode, fetch the appropriate source prompt, and
237
+ * load project context files. Returns everything buildPrompt needs.
238
+ */
239
+ function resolveSystemPromptSources(
240
+ ctx: ExtensionContext,
241
+ cwd: string,
242
+ notify: (msg: string) => void,
243
+ ): { mode: SystemPromptMode; extras: Pick<PromptExtras, "parentSystemPrompt" | "customSystemPrompt" | "contextFiles"> } {
244
+ const store = getStore();
245
+ const mode = store.agent.systemPromptMode;
246
+ const extras: Pick<PromptExtras, "parentSystemPrompt" | "customSystemPrompt" | "contextFiles"> = {};
247
+
248
+ // Fetch parent system prompt for inherit mode
249
+ if (mode === "inherit") {
250
+ try {
251
+ extras.parentSystemPrompt = ctx.getSystemPrompt();
252
+ } catch (err) {
253
+ notify(`Failed to get parent system prompt: ${err}. Falling back to replace mode.`);
254
+ }
255
+ }
256
+
257
+ // Read custom prompt file for custom mode
258
+ if (mode === "custom") {
259
+ try {
260
+ const content = fs.readFileSync(CUSTOM_PROMPT_PATH, "utf-8").trim();
261
+ if (content) {
262
+ extras.customSystemPrompt = content;
263
+ } else {
264
+ notify(`Custom prompt file is empty: ${CUSTOM_PROMPT_PATH}. Falling back to replace mode.`);
265
+ }
266
+ } catch (err: any) {
267
+ if (err.code === "ENOENT") {
268
+ notify(`Custom prompt file not found: ${CUSTOM_PROMPT_PATH}. Falling back to replace mode.`);
269
+ } else {
270
+ notify(`Failed to read custom prompt file: ${err.message}. Falling back to replace mode.`);
271
+ }
272
+ }
273
+ }
274
+
275
+ // Load AGENTS.md context files when the setting is enabled
276
+ if (store.agent.includeContextFiles) {
277
+ try {
278
+ extras.contextFiles = loadProjectContextFiles({ cwd, agentDir: getAgentDir() });
279
+ } catch {
280
+ // Non-fatal: context files are supplementary
281
+ }
282
+ }
283
+
284
+ return { mode, extras };
285
+ }
286
+
374
287
  /**
375
288
  * Phase 1: Resolve system prompt from agent config, skills, and env info.
289
+ *
290
+ * @param resolverExtras Partial extras from resolveSystemPromptSources (mode-specific prompts + context files).
376
291
  */
377
292
  function buildPrompt(
378
293
  type: SubagentType,
@@ -380,8 +295,10 @@ function buildPrompt(
380
295
  config: ReturnType<typeof getConfig>,
381
296
  cwd: string,
382
297
  env: EnvInfo,
298
+ systemPromptMode: SystemPromptMode = "replace",
299
+ resolverExtras: Pick<PromptExtras, "parentSystemPrompt" | "customSystemPrompt" | "contextFiles"> = {},
383
300
  ): string {
384
- const extras: PromptExtras = {};
301
+ const extras: PromptExtras = { ...resolverExtras };
385
302
  if (Array.isArray(agentConfig?.preloadSkills)) {
386
303
  extras.skillBlocks = preloadSkills(agentConfig.preloadSkills, cwd);
387
304
  }
@@ -389,11 +306,11 @@ function buildPrompt(
389
306
  extras.skillMetas = loadSkillMeta(config.skills, cwd);
390
307
  }
391
308
  if (agentConfig) {
392
- return buildAgentPrompt(agentConfig, cwd, env, extras);
309
+ return buildAgentPrompt(agentConfig, cwd, env, extras, systemPromptMode);
393
310
  }
394
311
  const fallback = DEFAULT_AGENTS.get("general-purpose");
395
312
  if (!fallback) throw new Error(`No fallback config available for unknown type "${type}"`);
396
- return buildAgentPrompt({ ...fallback, name: type }, cwd, env, extras);
313
+ return buildAgentPrompt({ ...fallback, name: type }, cwd, env, extras, systemPromptMode);
397
314
  }
398
315
 
399
316
  /** Build extension name → tool names map from loaded extensions. */
@@ -482,7 +399,7 @@ async function initSession(
482
399
  const model = options.model ?? findModelInRegistry(
483
400
  agentConfig?.model, ctx.modelRegistry, ctx.model,
484
401
  );
485
- const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
402
+ const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinkingLevel;
486
403
  const agentDir = getAgentDir();
487
404
  const sessionOpts: Parameters<typeof createAgentSession>[0] = {
488
405
  cwd, agentDir,
@@ -492,7 +409,22 @@ async function initSession(
492
409
  tools: getToolNamesForType(type), resourceLoader: loader,
493
410
  };
494
411
  if (thinkingLevel) sessionOpts.thinkingLevel = thinkingLevel;
495
- return createAgentSession(sessionOpts);
412
+ const result = await createAgentSession(sessionOpts);
413
+
414
+ // Inject max_tokens into provider request payloads.
415
+ // Spawn-time value wins over agent config (frontmatter).
416
+ const maxTokens = options.maxTokens ?? agentConfig?.maxTokens;
417
+ if (maxTokens != null && maxTokens > 0 && model) {
418
+ const field = (model.compat as any)?.maxTokensField ?? "max_tokens";
419
+ const origOnPayload = result.session.agent.onPayload;
420
+ result.session.agent.onPayload = async (payload, m) => {
421
+ const applied = origOnPayload ? (await origOnPayload(payload, m)) ?? payload : payload;
422
+ const obj = typeof applied === "object" && applied && !Array.isArray(applied) ? applied : {};
423
+ return { ...obj, [field]: maxTokens };
424
+ };
425
+ }
426
+
427
+ return result;
496
428
  }
497
429
 
498
430
  /**
@@ -518,10 +450,13 @@ async function createAndConfigureSession(
518
450
  type: "end", toolName: `extension-error:${err.extensionPath}`,
519
451
  }),
520
452
  });
521
- const filteredTools = filterActiveTools(
522
- session.getActiveToolNames(), buildExtToolMap(extResult.extensions),
523
- agentConfig?.tools, agentConfig?.excludeTools, notify,
524
- );
453
+ const filteredTools = resolveVisibleTools({
454
+ activeTools: session.getActiveToolNames(),
455
+ tools: agentConfig?.tools,
456
+ excludeTools: agentConfig?.excludeTools,
457
+ extToolMap: buildExtToolMap(extResult.extensions),
458
+ notify,
459
+ });
525
460
  if (filteredTools) session.setActiveToolsByName(filteredTools);
526
461
  options.onSessionCreated?.(session);
527
462
  return session;
@@ -555,7 +490,7 @@ function wireTurnTracking(
555
490
  }
556
491
  });
557
492
 
558
- return { unsubscribe, getAborted: () => aborted, getSteered: () => softLimitReached };
493
+ return { unsubscribe, getAborted: () => aborted, getTurnLimited: () => softLimitReached };
559
494
  }
560
495
 
561
496
  /**
@@ -589,7 +524,8 @@ export async function runAgent(
589
524
  prompt: string,
590
525
  options: RunOptions,
591
526
  ): Promise<RunResult> {
592
- const config = getConfig(type);
527
+ const store = getStore();
528
+ const config = getConfig(type, store.agent.loadSkillsImplicitly, store.agent.loadExtensionsImplicitly);
593
529
  const agentConfig = getAgentConfig(type);
594
530
 
595
531
  // Warn on mutual exclusion violations
@@ -607,17 +543,23 @@ export async function runAgent(
607
543
  const effectiveCwd = options.cwd ?? ctx.cwd;
608
544
  const env = await detectEnv(options.pi, effectiveCwd);
609
545
 
610
- const systemPrompt = buildPrompt(type, agentConfig, config, effectiveCwd, env);
546
+ // Resolve system prompt mode + source prompts + context files
547
+ const { mode, extras: promptExtras } = resolveSystemPromptSources(ctx, effectiveCwd, notify);
548
+
549
+ const systemPrompt = buildPrompt(
550
+ type, agentConfig, config, effectiveCwd, env,
551
+ mode, promptExtras,
552
+ );
611
553
  const { loader, reloadAndMap } = createResourceLoader(config, agentConfig, effectiveCwd, systemPrompt);
612
554
  const { extResult } = await reloadAndMap();
613
555
  const session = await createAndConfigureSession(
614
556
  ctx, options, agentConfig, type, effectiveCwd, loader, extResult, notify,
615
557
  );
616
- const { unsubscribe: unsubTurns, getAborted, getSteered } = wireTurnTracking(session, {
558
+ const { unsubscribe: unsubTurns, getAborted, getTurnLimited } = wireTurnTracking(session, {
617
559
  ...options,
618
560
  maxTurns: options.maxTurns ?? agentConfig?.maxTurns,
619
561
  });
620
562
 
621
563
  const responseText = await runTurnLoop(session, prompt, options, unsubTurns);
622
- return { responseText, session, aborted: getAborted(), steered: getSteered() };
564
+ return { responseText, session, aborted: getAborted(), turnLimited: getTurnLimited() };
623
565
  }
@@ -7,9 +7,9 @@
7
7
  */
8
8
 
9
9
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
- import type { AgentRecord } from "./types.js";
11
- import { SHORT_ID_LENGTH } from "./types.js";
12
- import { getManager } from "./state.js";
10
+ import type { AgentRecord } from "../types.js";
11
+ import { SHORT_ID_LENGTH } from "../types.js";
12
+ import { getManager } from "../shell.js";
13
13
 
14
14
  /**
15
15
  * Format a single agent record as "type·short_id·status".
@@ -32,7 +32,7 @@ export async function executeAgentStatusTool(
32
32
  _onUpdate: ((update: any) => void) | undefined,
33
33
  _ctx: ExtensionContext,
34
34
  ): Promise<any> {
35
- const manager = getManager();
35
+ const manager = getManager()!;
36
36
  const agents = manager.listAgents();
37
37
 
38
38
  const nudge = "Don't poll — you'll receive notifications when agents complete.";