pi-subagents-lite 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents-lite",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Lightweight sub-agents for pi — spawn specialized agents with isolated sessions, tools, and models.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -115,6 +115,8 @@ export interface SpawnOptions {
115
115
  onAssistantUsage?: (usage: LifetimeUsage) => void;
116
116
  /** Called when the session successfully compacts. */
117
117
  onCompaction?: (info: CompactionInfo) => void;
118
+ /** Grace turns: extra turns allowed after hitting maxTurns. */
119
+ graceTurns?: number;
118
120
  }
119
121
 
120
122
  export class AgentManager {
@@ -304,6 +306,7 @@ export class AgentManager {
304
306
  model: options.model,
305
307
  maxTurns: options.maxTurns,
306
308
  thinkingLevel: options.thinkingLevel,
309
+ graceTurns: options.graceTurns,
307
310
  signal: record.abortController!.signal,
308
311
  ...this.createRecordCallbacks(record, options),
309
312
  onTurnEnd: (turnCount) => {
@@ -29,8 +29,8 @@ import { type CompactionInfo, type EnvInfo, SHORT_ID_LENGTH, type SubagentType,
29
29
  /** Names of tools registered by this extension that subagents must NOT inherit. */
30
30
  const EXCLUDED_TOOL_NAMES = ["Agent"];
31
31
 
32
- /** Additional turns allowed after the soft limit steer message. */
33
- const GRACE_TURNS = 5;
32
+ /** Default grace turns when not specified in config. */
33
+ const DEFAULT_GRACE_TURNS = 6;
34
34
 
35
35
  /** Timeout for quick git commands (branch detection, repo check). */
36
36
  const GIT_EXEC_TIMEOUT_MS = 5000;
@@ -76,6 +76,8 @@ interface RunOptions {
76
76
  * pre-compaction context size estimate. Aborted compactions don't fire.
77
77
  */
78
78
  onCompaction?: (info: CompactionInfo) => void;
79
+ /** Grace turns: extra turns allowed after hitting maxTurns. Defaults to 6. */
80
+ graceTurns?: number;
79
81
  }
80
82
 
81
83
  interface RunResult {
@@ -558,6 +560,7 @@ export async function runAgent(
558
560
  const maxTurns = normalizeMaxTurns(options.maxTurns ?? agentConfig?.maxTurns);
559
561
  let softLimitReached = false;
560
562
  let aborted = false;
563
+ const graceTurns = options.graceTurns ?? DEFAULT_GRACE_TURNS;
561
564
 
562
565
  const unsubEvents = subscribeToSessionEvents(session, options);
563
566
 
@@ -569,7 +572,7 @@ export async function runAgent(
569
572
  if (!softLimitReached && turnCount >= maxTurns) {
570
573
  softLimitReached = true;
571
574
  session.steer("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
572
- } else if (softLimitReached && turnCount >= maxTurns + GRACE_TURNS) {
575
+ } else if (softLimitReached && turnCount >= maxTurns + graceTurns) {
573
576
  aborted = true;
574
577
  session.abort();
575
578
  }
@@ -17,7 +17,7 @@ import type { AgentConfig } from "./types.js";
17
17
  * `find` and `ls` were removed — they're thin wrappers over bash commands
18
18
  * that add ~180 tokens/turn with no real benefit.
19
19
  */
20
- export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep"];
20
+ export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find"];
21
21
 
22
22
  /** Unified runtime registry of all agents (defaults + user-defined). */
23
23
  const agents = new Map<string, AgentConfig>();
package/src/config-io.ts CHANGED
@@ -14,7 +14,7 @@ const CONFIG_PATH = path.join(CONFIG_DIR, "subagents-lite.json");
14
14
 
15
15
  /** Default configuration — used when config file doesn't exist or is invalid. */
16
16
  export const DEFAULT_CONFIG: SubagentsConfig = {
17
- agent: { default: null, forceBackground: false },
17
+ agent: { default: null, forceBackground: false, graceTurns: 6 },
18
18
  concurrency: { default: 4 },
19
19
  };
20
20
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { AgentConfig } from "./types.js";
9
9
 
10
- const READ_ONLY_TOOLS = ["read", "bash", "grep"];
10
+ const READ_ONLY_TOOLS = ["read", "bash", "grep", "find"];
11
11
 
12
12
  export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
13
13
  [
package/src/index.ts CHANGED
@@ -136,6 +136,13 @@ async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void>
136
136
  // UI helpers — stats card rendering (shared by renderResult and message renderer)
137
137
  // ============================================================================
138
138
 
139
+ /** Format agent display name with optional model: "Agent (mimo-v2.5-pro)" or "Agent". */
140
+ function agentNameLabel(d: Record<string, unknown>, theme: Theme): string {
141
+ const typeName = getDisplayName((d.type as string) || "");
142
+ const modelName = d.modelName as string | undefined;
143
+ return modelName ? `${theme.bold(typeName)} (${modelName})` : theme.bold(typeName);
144
+ }
145
+
139
146
  /** Build the stats line for an agent result card. Used by both renderers. */
140
147
  function buildStatsLine(d: Record<string, unknown>, theme: Theme): string {
141
148
  const parts = buildStatsParts({
@@ -205,8 +212,9 @@ function registerAgentTool(pi: ExtensionAPI): void {
205
212
  const desc = (d?.description as string) || "";
206
213
 
207
214
  if (d && d.turnCount != null) {
215
+ const namePart = agentNameLabel(d, theme);
208
216
  const statsLine = buildStatsLine(d, theme);
209
- let lines = `${icon} ${statsLine}\n ${theme.fg("text", desc)}`;
217
+ let lines = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
210
218
  if (expanded && text) {
211
219
  lines += "\n" + text.split("\n").map(l => ` ${l}`).join("\n");
212
220
  }
@@ -265,11 +273,9 @@ export default function (pi: ExtensionAPI) {
265
273
  if (d && d.turnCount != null) {
266
274
  const isError = d.status === "error" || d.status === "aborted" || d.status === "stopped";
267
275
  const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
268
- const typeName = getDisplayName((d.type as string) || "");
269
- const modelName = d.modelName as string | undefined;
270
276
  const desc = (d.description as string) || "";
271
277
 
272
- const namePart = modelName ? `${theme.bold(typeName)} (${modelName})` : theme.bold(typeName);
278
+ const namePart = agentNameLabel(d, theme);
273
279
  const statsLine = buildStatsLine(d, theme);
274
280
  let headerLine = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
275
281
  if ((d.outputFile as string)) {
@@ -283,13 +289,10 @@ export default function (pi: ExtensionAPI) {
283
289
  inner.addChild(new Text(resultLines, 0, 0));
284
290
  }
285
291
  } else {
286
- const typeName = getDisplayName((d?.type as string) || "");
287
- const modelName = d?.modelName as string | undefined;
288
292
  const desc = (d?.description as string) || "";
289
293
  let line = `${theme.fg("success", "✓")}`;
290
- if (typeName) {
291
- const namePart = modelName ? `${theme.bold(typeName)} (${modelName})` : theme.bold(typeName);
292
- line += ` ${namePart}`;
294
+ if (d?.type) {
295
+ line += ` ${agentNameLabel(d, theme)}`;
293
296
  }
294
297
  if (desc) line += `\n ${theme.fg("text", desc)}`;
295
298
  if (d?.outputFile) {
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
  });
@@ -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)
@@ -191,11 +191,8 @@ export async function executeAgentTool(
191
191
  const model = findModelInRegistry(modelStr, ctx.modelRegistry, ctx.model);
192
192
  const modelKey = model ? `${model.provider}/${model.id}` : undefined;
193
193
 
194
- // Determine modelName for invocation (only when different from parent)
195
- const parentModelId = ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : "";
196
- const modelName = (modelKey && modelKey !== parentModelId)
197
- ? parseModelKey(modelKey)?.modelId
198
- : undefined;
194
+ // Determine modelName for invocation (always capture for display)
195
+ const modelName = model?.id;
199
196
 
200
197
  // Resolve thinking: explicit param > agent config (frontmatter) > undefined (inherit)
201
198
  const thinkingLevel = parseThinkingLevel(params.thinking as string | undefined)
@@ -207,7 +204,8 @@ export async function executeAgentTool(
207
204
  maxTurns,
208
205
  thinkingLevel,
209
206
  modelKey,
210
- invocation: modelName ? { modelName } : undefined,
207
+ invocation: { modelName },
208
+ graceTurns: __config.agent.graceTurns,
211
209
  };
212
210
 
213
211
  if (runInBackground || __config.agent.forceBackground) {
@@ -239,7 +237,7 @@ async function executeSpawnBackground(
239
237
 
240
238
  const record = manager.getRecord(agentId)!;
241
239
  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}`;
240
+ 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
241
  const label = record.status === "queued" ? "Agent queued" : "Agent running";
244
242
 
245
243
  return successResult(`[${label}] ${suffix}`, details);
@@ -318,12 +316,10 @@ export async function toolCallListener(
318
316
 
319
317
  if (effectiveModel) {
320
318
  input.model = effectiveModel;
321
- // Inject _modelOverride for renderCall when model differs from parent
322
- if (effectiveModel !== parentModelId) {
323
- const parsed = parseModelKey(effectiveModel);
324
- if (parsed) {
325
- input._modelOverride = parsed.modelId;
326
- }
319
+ // Always inject _modelOverride for renderCall
320
+ const parsed = parseModelKey(effectiveModel);
321
+ if (parsed) {
322
+ input._modelOverride = parsed.modelId;
327
323
  }
328
324
  }
329
325