pi-subagents-lite 0.4.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * - Config mutations update cache + atomic write to disk
16
16
  *
17
17
  * Commands:
18
- * - /agents: Management menu with 5 sub-menus
18
+ * - /agents: Management menu (model settings, concurrency, running agents, debug)
19
19
  *
20
20
  * Events:
21
21
  * - tool_call: Inject model into Agent tool calls
@@ -33,10 +33,10 @@ import type {
33
33
  } from "@earendil-works/pi-coding-agent";
34
34
  import type { SessionModelOverrides, SubagentsConfig } from "./model-precedence.js";
35
35
  import { DEFAULT_AGENTS } from "./default-agents.js";
36
- import { registerAgents, getAvailableTypes } from "./agent-types.js";
36
+ import { registerAgents, getAvailableTypes, setAgentScanDirs } from "./agent-types.js";
37
37
  import { scanAgentFilesInDir, mergeAgents } from "./agent-discovery.js";
38
38
  import { AgentManager } from "./agent-manager.js";
39
- import { AgentWidget, buildStatsParts, formatMs, getDisplayName, type AgentActivity, type UICtx } from "./ui/agent-widget.js";
39
+ import { AgentWidget, buildStatsParts, formatMs, getDisplayName, type AgentActivity, type Theme, type UICtx } from "./ui/agent-widget.js";
40
40
  import { showAgentsMainMenu } from "./menus.js";
41
41
  import { loadConfig, DEFAULT_CONFIG } from "./config-io.js";
42
42
  import { executeAgentTool, toolCallListener, backgroundAgentIds, scheduleNudge } from "./tool-execution.js";
@@ -111,6 +111,9 @@ async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
111
111
  const userAgentDir = path.join(homeDir, ".pi", "agent", "agents");
112
112
  const projectAgentDir = path.join(ctx.cwd, ".pi", "agents");
113
113
 
114
+ // Store scan dirs for on-demand discovery (agents added during the session)
115
+ setAgentScanDirs(userAgentDir, projectAgentDir);
116
+
114
117
  const [userAgents, projectAgents] = await Promise.all([
115
118
  scanAgentFilesInDir(userAgentDir, "user"),
116
119
  scanAgentFilesInDir(projectAgentDir, "project"),
@@ -134,7 +137,7 @@ async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void>
134
137
  // ============================================================================
135
138
 
136
139
  /** Build the stats line for an agent result card. Used by both renderers. */
137
- function buildStatsLine(d: Record<string, unknown>, theme: any): string {
140
+ function buildStatsLine(d: Record<string, unknown>, theme: Theme): string {
138
141
  const parts = buildStatsParts({
139
142
  toolUses: (d.toolUses as number) ?? 0,
140
143
  turnCount: d.turnCount as number | undefined,
package/src/menus.ts CHANGED
@@ -301,6 +301,26 @@ export async function showModelSettingsMenu(
301
301
  );
302
302
  });
303
303
 
304
+ // Grace turns setting
305
+ const graceTurns = __config.agent.graceTurns ?? 6;
306
+ items.push(`Grace turns · ${graceTurns}`);
307
+ actions.push(async () => {
308
+ const input = await ctx.ui.input("Grace turns (≥ 0)", String(graceTurns));
309
+ if (input === undefined) return;
310
+ const parsed = parseInt(input.trim(), 10);
311
+ if (isNaN(parsed)) {
312
+ ctx.ui.notify("Invalid value — must be a number", "error");
313
+ return;
314
+ }
315
+ if (parsed < 0) {
316
+ ctx.ui.notify("Invalid value — must be ≥ 0", "error");
317
+ return;
318
+ }
319
+ __config.agent.graceTurns = parsed;
320
+ saveConfigAtomic(__config);
321
+ ctx.ui.notify(`Grace turns set to ${parsed}`, "info");
322
+ });
323
+
304
324
  items.push("");
305
325
  actions.push(async () => {});
306
326
  items.push("─── per-type overrides ───");
@@ -375,13 +395,20 @@ export async function showModelSettingsMenu(
375
395
  items.push("Clear all overrides");
376
396
  actions.push(async () => {
377
397
  const hasOverrides = Object.entries(__config.agent).some(
378
- ([k, v]) => k !== "default" && k !== "forceBackground" && v != null,
398
+ ([k, v]) => k !== "default" && k !== "forceBackground" && k !== "graceTurns" && v != null,
379
399
  );
380
400
  if (!hasOverrides && __config.agent.default === null) {
381
401
  ctx.ui.notify("No overrides to clear", "info");
382
402
  return;
383
403
  }
384
- __config.agent = { default: __config.agent.default, forceBackground: __config.agent.forceBackground };
404
+ const preserved: Record<string, unknown> = {
405
+ default: __config.agent.default,
406
+ forceBackground: __config.agent.forceBackground,
407
+ };
408
+ if (__config.agent.graceTurns != null) {
409
+ preserved.graceTurns = __config.agent.graceTurns;
410
+ }
411
+ __config.agent = preserved as typeof __config.agent;
385
412
  saveConfigAtomic(__config);
386
413
  ctx.ui.notify("All model overrides cleared", "info");
387
414
  });
@@ -453,8 +480,8 @@ async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void>
453
480
  lines.push(config.description);
454
481
  lines.push("");
455
482
 
456
- if (config.builtinToolNames) {
457
- lines.push(`**Tools:** ${config.builtinToolNames.join(", ")}`);
483
+ if (config.registeredTools) {
484
+ lines.push(`**Tools:** ${config.registeredTools.join(", ")}`);
458
485
  }
459
486
  if (config.model) {
460
487
  lines.push(`**Default model:** ${config.model}`);
@@ -533,7 +560,7 @@ export async function showConcurrencySettingsMenu(
533
560
  items.push(`Default concurrency limit · ${__config.concurrency.default}`);
534
561
  actions.push(async () => {
535
562
  await promptConcurrencyInput(
536
- ctx, "Default concurrency limit", __config.concurrency.default,
563
+ ctx, "Default limit", __config.concurrency.default,
537
564
  (value) => { __config.concurrency.default = value; },
538
565
  );
539
566
  });
@@ -839,13 +866,13 @@ async function showAgentTypes(ctx: ExtensionCommandContext): Promise<void> {
839
866
  for (const name of types) {
840
867
  const cfg = getAgentConfig(name);
841
868
  if (!cfg) continue;
842
- const disabled = cfg.enabled === false ? " [DISABLED]" : "";
869
+ const hidden = cfg.hidden === true ? " [HIDDEN]" : "";
843
870
  const model = cfg.model ? ` Model: ${cfg.model}` : "";
844
- const tools = cfg.builtinToolNames
845
- ? ` Tools: ${cfg.builtinToolNames.join(", ")}`
871
+ const tools = cfg.registeredTools
872
+ ? ` Tools: ${cfg.registeredTools.join(", ")}`
846
873
  : " Tools: all built-in tools";
847
874
  const source = cfg.source ? ` Source: ${cfg.source}` : "";
848
- lines.push(` ${name}${disabled}`);
875
+ lines.push(` ${name}${hidden}`);
849
876
  lines.push(` ${cfg.description}`);
850
877
  if (model) lines.push(model);
851
878
  lines.push(tools);
@@ -17,7 +17,8 @@ export interface SubagentsConfig {
17
17
  agent: {
18
18
  default: string | null;
19
19
  forceBackground: boolean;
20
- [agentType: string]: string | null | undefined | boolean;
20
+ graceTurns?: number;
21
+ [agentType: string]: string | null | undefined | boolean | number;
21
22
  };
22
23
  concurrency: {
23
24
  default: number;
@@ -60,10 +61,11 @@ export function resolveModel(options: ResolveModelOptions): string {
60
61
  const { subagentType, agentConfig, config, parentModelId, sessionOverrides } = options;
61
62
 
62
63
  // Precedence chain: session > config > frontmatter > parent
64
+ // Cast agent values: index signature includes number (graceTurns), but models are always strings
63
65
  const candidates: Array<string | boolean | null | undefined> = [
64
66
  sessionOverrides?.[subagentType],
65
67
  sessionOverrides?.["default"],
66
- config.agent[subagentType],
68
+ config.agent[subagentType] as string | null | undefined,
67
69
  config.agent["default"],
68
70
  agentConfig?.model,
69
71
  parentModelId, // final fallback (always a valid string)
@@ -1,9 +1,6 @@
1
1
  /**
2
2
  * model-selector.ts — TUI model selection dialog.
3
3
  *
4
- * Ported from subagent-lazy/model-selector.ts verbatim.
5
- * Used by the /agents menu for model selection.
6
- *
7
4
  * Reuses the same building blocks as pi's ModelSelectorComponent but without
8
5
  * the SettingsManager dependency — no side effects, just callbacks.
9
6
  */
@@ -18,10 +15,7 @@ import {
18
15
  Text,
19
16
  } from "@earendil-works/pi-tui";
20
17
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
21
-
22
- // Theme type from ctx.ui.custom() callback — avoid deep import that may not resolve
23
- // in jiti extension loader. The constructor receives the theme instance directly.
24
- type Theme = any;
18
+ import type { Theme } from "./ui/agent-widget.js";
25
19
 
26
20
  /* ------------------------------------------------------------------ */
27
21
  /* Types */
@@ -1,12 +1,9 @@
1
1
  /**
2
2
  * output-file.ts — Human-readable output logging for agent transcripts.
3
3
  *
4
- * Forked from upstream pi-subagents. Key modifications:
5
- * - Rewrote from JSONL to human-readable format
6
- * - Path changed to /tmp/pi-agent-outputs/<agentId>.log (no CID-encoded nesting)
7
- * - Directory created with 0o700 permissions
8
- * - Append-only, human-readable, supports `tail -f`
9
- * - Lines: [USER], [TOOL], [ASSISTANT], [DONE] with ISO timestamps
4
+ * Path: /tmp/pi-agent-outputs/<agentId>.log
5
+ * Append-only, human-readable, supports `tail -f`.
6
+ * Lines: [USER], [TOOL], [ASSISTANT], [DONE] with ISO timestamps.
10
7
  */
11
8
 
12
9
  import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
package/src/prompts.ts CHANGED
@@ -7,9 +7,8 @@
7
7
 
8
8
  import type { AgentConfig, EnvInfo } from "./types.js";
9
9
  import type { SkillMeta } from "./skill-loader.js";
10
- export type { SkillMeta };
11
10
 
12
- /** Extra sections to inject into the system prompt (skills only — no memoryBlock). */
11
+ /** Extra sections to inject into the system prompt (skills). */
13
12
  export interface PromptExtras {
14
13
  /** Preloaded skill contents to inject (full content). */
15
14
  skillBlocks?: { name: string; content: string }[];
@@ -45,7 +44,7 @@ export function buildAgentPrompt(
45
44
  envLines.push(`Platform: ${env.platform}`);
46
45
  const envBlock = envLines.join("\n");
47
46
 
48
- // Build optional extras suffix (skills only — no memoryBlock)
47
+ // Build optional extras suffix (skills)
49
48
  const extraSections: string[] = [];
50
49
 
51
50
  // Skill metadata whitelist (like Pi's available_skills format)
@@ -17,10 +17,7 @@ import {
17
17
  } from "@earendil-works/pi-tui";
18
18
  import { DynamicBorder } from "@earendil-works/pi-coding-agent";
19
19
  import { type LifetimeUsage, formatTokens } from "./usage.js";
20
- import { formatMs } from "./ui/agent-widget.js";
21
-
22
- // Theme type from ctx.ui.custom() callback
23
- type Theme = any;
20
+ import { formatMs, type Theme } from "./ui/agent-widget.js";
24
21
 
25
22
  /* ------------------------------------------------------------------ */
26
23
  /* Types */
@@ -16,8 +16,6 @@
16
16
  * SKILL.md is a skill — we don't descend into it (Pi: skills don't nest).
17
17
  *
18
18
  * Symlinks are rejected for security (deviation from Pi, which follows them).
19
- *
20
- * Changed from upstream: imports from ./utils.js instead of ./memory.js.
21
19
  */
22
20
 
23
21
  import type { Dirent } from "node:fs";
@@ -10,7 +10,7 @@ import type { ExtensionContext, ToolCallEvent } from "@earendil-works/pi-coding-
10
10
  import type { AgentRecord } from "./types.js";
11
11
  import type { SpawnOptions as AgentManagerSpawnOptions } from "./agent-manager.js";
12
12
  import type { AgentActivity } from "./ui/agent-widget.js";
13
- import { resolveType, getAgentConfig } from "./agent-types.js";
13
+ import { resolveType, getAgentConfig, discoverNewAgents } from "./agent-types.js";
14
14
  import { resolveModel } from "./model-precedence.js";
15
15
  import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
16
16
 
@@ -173,7 +173,12 @@ export async function executeAgentTool(
173
173
  ctx: ExtensionContext,
174
174
  ): Promise<any> {
175
175
  const type = (params.agent as string) || "general-purpose";
176
- const resolvedType = resolveType(type);
176
+ let resolvedType = resolveType(type);
177
+ if (!resolvedType) {
178
+ // Not found in registry — try scanning filesystem for agents added during the session
179
+ await discoverNewAgents();
180
+ resolvedType = resolveType(type);
181
+ }
177
182
  if (!resolvedType) {
178
183
  return errorResult(`Unknown agent type: ${type}`);
179
184
  }
@@ -181,7 +186,6 @@ export async function executeAgentTool(
181
186
  const prompt = params.prompt as string;
182
187
  const description = params.description as string;
183
188
  const runInBackground = params.run_in_background as boolean | undefined;
184
- const isolated = params.isolated as boolean | undefined;
185
189
  const maxTurns = params.max_turns as number | undefined ?? getAgentConfig(resolvedType)?.maxTurns;
186
190
  const modelStr = params.model as string | undefined;
187
191
  const model = findModelInRegistry(modelStr, ctx.modelRegistry, ctx.model);
@@ -201,10 +205,10 @@ export async function executeAgentTool(
201
205
  description,
202
206
  model,
203
207
  maxTurns,
204
- isolated,
205
208
  thinkingLevel,
206
209
  modelKey,
207
210
  invocation: modelName ? { modelName } : undefined,
211
+ graceTurns: __config.agent.graceTurns,
208
212
  };
209
213
 
210
214
  if (runInBackground || __config.agent.forceBackground) {
@@ -234,12 +238,9 @@ async function executeSpawnBackground(
234
238
  widget?.ensureTimer();
235
239
  widget?.update();
236
240
 
237
- const record = manager.getRecord(agentId);
238
- if (!record) {
239
- return errorResult("Failed to create agent");
240
- }
241
+ const record = manager.getRecord(agentId)!;
241
242
  const details: Record<string, unknown> = { type: resolvedType, description: spawnOptions.description };
242
- const suffix = `A notification will arrive when done - User asks you not to poll or duplicate the delegated work.\n\nAgent ID: ${agentId}`;
243
+ const suffix = `A notification will arrive when done - User asks you not to poll, check status or duplicate the delegated work.\n\nAgent ID: ${agentId}`;
243
244
  const label = record.status === "queued" ? "Agent queued" : "Agent running";
244
245
 
245
246
  return successResult(`[${label}] ${suffix}`, details);
@@ -327,11 +328,6 @@ export async function toolCallListener(
327
328
  }
328
329
  }
329
330
 
330
- // Inject isolated from agent config if not explicitly passed
331
- if (input.isolated === undefined && agentConfig?.isolated !== undefined) {
332
- input.isolated = agentConfig.isolated;
333
- }
334
-
335
331
  // Inject thinking from agent config if not explicitly passed
336
332
  if (input.thinking === undefined && agentConfig?.thinking !== undefined) {
337
333
  input.thinking = agentConfig.thinking;
package/src/types.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  /**
2
- * types.ts — Type definitions for the subagent system.
3
- *
4
- * Trimmed from upstream: removed ScheduledSubagent, ScheduleStoreData,
5
- * IsolationMode, MemoryScope, JoinMode.
6
- * From AgentConfig: removed memory, isolation, inheritContext, runInBackground.
7
- * From AgentRecord: removed groupId, joinMode, worktree, worktreeResult.
8
- * From AgentInvocation: removed inheritContext, isolation.
2
+ * Type definitions for the subagent system.
9
3
  */
10
4
 
11
5
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
@@ -22,11 +16,21 @@ export interface AgentConfig {
22
16
  name: string;
23
17
  displayName?: string;
24
18
  description: string;
25
- builtinToolNames?: string[];
26
- /** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */
27
- disallowedTools?: string[];
28
- /** true = inherit all, string[] = only listed, false = none */
19
+ /** Tools to register with the session (controls availability, not LLM visibility). */
20
+ registeredTools?: string[];
21
+ /**
22
+ * Controls which tool schemas the LLM sees. Can reference built-in tools
23
+ * and extension tools. true = all, string[] = listed, false = none.
24
+ * Supports ext/* syntax to include all tools from an extension.
25
+ * Mutually exclusive with excludeTools.
26
+ */
27
+ tools?: true | string[] | false;
28
+ /** Tool blacklist — all tools except these are visible. Mutually exclusive with tools (when tools is string[]). */
29
+ excludeTools?: string[];
30
+ /** true = inherit all, string[] = only listed, false = none. Mutually exclusive with excludeExtensions. */
29
31
  extensions: true | string[] | false;
32
+ /** Extension blacklist — all extensions except these load. Mutually exclusive with extensions (when extensions is string[]). */
33
+ excludeExtensions?: string[];
30
34
  /** Whitelist of allowed skills (metadata only in system prompt). true = all, string[] = listed, false = none */
31
35
  skills: true | string[] | false;
32
36
  /** Skills to preload with full content into system prompt. string[] = listed, false/undefined = none */
@@ -35,14 +39,13 @@ export interface AgentConfig {
35
39
  thinking?: ThinkingLevel;
36
40
  maxTurns?: number;
37
41
  systemPrompt: string;
38
- /** Default for spawn: no extension tools. undefined = caller decides. */
39
- isolated?: boolean;
42
+
40
43
  /** true = this is an embedded default agent (informational) */
41
44
  isDefault?: boolean;
42
- /** false = agent is hidden from the registry */
43
- enabled?: boolean;
45
+ /** true = agent is hidden from the schema enum but can still be called by name. */
46
+ hidden?: boolean;
44
47
  /** Where this agent was loaded from */
45
- source?: "default" | "project" | "global";
48
+ source?: "project" | "global";
46
49
  }
47
50
 
48
51
  export interface AgentRecord {
@@ -87,7 +90,6 @@ export interface AgentInvocation {
87
90
  modelName?: string;
88
91
  thinking?: ThinkingLevel;
89
92
  maxTurns?: number;
90
- isolated?: boolean;
91
93
  runInBackground?: boolean;
92
94
  }
93
95
 
@@ -109,8 +111,4 @@ export interface CompactionInfo {
109
111
  tokensBefore: number;
110
112
  }
111
113
 
112
- /** Parsed "provider/model-id" key. */
113
- export interface ModelKey {
114
- provider: string;
115
- modelId: string;
116
- }
114
+
@@ -1,9 +1,5 @@
1
1
  /**
2
2
  * agent-widget.ts — Persistent widget showing running/completed agents above the editor.
3
- *
4
- * Ported from upstream pi-subagents.
5
- * Import paths use relative imports within our extension.
6
- * addUsage/getLifetimeTotal/getSessionContextPercent imported from ../usage.js.
7
3
  */
8
4
 
9
5
  import { truncateToWidth } from "@earendil-works/pi-tui";
@@ -62,9 +58,10 @@ const TOOL_DISPLAY: Record<string, string> = {
62
58
 
63
59
  // ---- Types ----
64
60
 
65
- type Theme = {
61
+ export type Theme = {
66
62
  fg(color: string, text: string): string;
67
63
  bold(text: string): string;
64
+ italic?: (text: string) => string;
68
65
  };
69
66
 
70
67
  export type UICtx = {
package/src/utils.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  /**
2
- * utils.ts — Security helpers: safe file access, name validation + general utilities.
2
+ * utils.ts — Security helpers and general utilities.
3
3
  *
4
- * Security helpers extracted from upstream memory.ts — pure implementations copied verbatim.
5
- * General utilities (parseModelKey, findModelInRegistry) moved here so both agent-runner
6
- * and tool-execution can use them without circular dependencies.
4
+ * Security helpers (isUnsafeName, isSymlink, safeReadFile) protect against
5
+ * path traversal and symlink attacks in agent/skill name resolution.
7
6
  */
8
7
 
9
8
  import { lstatSync, readFileSync } from "node:fs";
@@ -35,7 +34,7 @@ export function isSymlink(filePath: string): boolean {
35
34
  */
36
35
  export function safeReadFile(filePath: string): string | undefined {
37
36
  try {
38
- if (lstatSync(filePath).isSymbolicLink()) return undefined;
37
+ if (isSymlink(filePath)) return undefined;
39
38
  return readFileSync(filePath, "utf-8");
40
39
  } catch {
41
40
  return undefined;