@clanker-code/pi-subagents 0.10.8 → 0.11.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.
@@ -34,7 +34,6 @@ export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
34
34
  builtinToolNames: READ_ONLY_TOOLS,
35
35
  extensions: true,
36
36
  skills: true,
37
- model: "anthropic/claude-haiku-4-5-20251001",
38
37
  systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
39
38
  You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
40
39
  Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
package/src/index.ts CHANGED
@@ -24,8 +24,10 @@ import { getAgentConversation, getCurrentExtensionAgentId, getCurrentExtensionDe
24
24
  import { buildAgentToolDescription, getModelLabelFromConfig } from "./agent-tool-description.js";
25
25
  import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
26
26
  import { formatOutputFileHint, limitText, MAX_RESULT_CHARS, MAX_VERBOSE_CHARS } from "./bounded-output.js";
27
+ import { extractText } from "./context.js";
27
28
  import { registerRpcHandlers } from "./cross-extension-rpc.js";
28
29
  import { loadCustomAgents } from "./custom-agents.js";
30
+ import { registerDashboardModules } from "./dashboard-ui.js";
29
31
  import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
30
32
  import { GroupJoinManager } from "./group-join.js";
31
33
  import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
@@ -37,11 +39,11 @@ import { SubagentScheduler } from "./schedule.js";
37
39
  import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
38
40
  import { applyAndEmitLoaded, DEFAULT_WAIT_TIMEOUT_SECONDS, type SubagentsSettings, saveAndEmitChanged, type ToolDescriptionMode } from "./settings.js";
39
41
  import { getStatusNote } from "./status-note.js";
42
+ import { registerSubagentListClearTools } from "./subagent-list-clear.js";
40
43
  import { type AgentConfig, type AgentInvocation, type AgentRecord, type JoinMode, MAX_RECURSIVE_DEPTH, type NotificationDetails, type SubagentType } from "./types.js";
41
- import { renderAgentCall, renderAgentResult, renderSteerCall, tailPreview } from "./ui/agent-tool-rendering.js";
44
+ import { renderAgentCall, renderAgentResult, renderSteerCall, snipMiddleLines, tailPreview } from "./ui/agent-tool-rendering.js";
42
45
  import {
43
46
  type AgentActivity,
44
- type AgentDetails,
45
47
  AgentWidget,
46
48
  buildInvocationTags,
47
49
  describeActivity,
@@ -60,10 +62,21 @@ import { formatWaitTimeout, pollPendingMessages, raceWait, type WaitOutcome, wai
60
62
  // ---- Shared helpers ----
61
63
 
62
64
  /** Tool execute return value for a text response. */
63
- function textResult(msg: string, details?: AgentDetails) {
65
+ function textResult(msg: string, details?: unknown) {
64
66
  return { content: [{ type: "text" as const, text: msg }], details: details as any };
65
67
  }
66
68
 
69
+ /** Metadata attached to get_subagent_result for compact UI rendering. */
70
+ interface GetResultDetails {
71
+ status: AgentRecord["status"];
72
+ description: string;
73
+ toolUses: number;
74
+ tokens: string | null;
75
+ contextPercent: number | null;
76
+ duration: string;
77
+ outputFile?: string;
78
+ }
79
+
67
80
  /**
68
81
  * Create an AgentActivity state and spawn callbacks for tracking tool usage.
69
82
  * Used by the background spawn path to track tool usage.
@@ -431,7 +444,7 @@ export default function (pi: ExtensionAPI) {
431
444
  if (!sessionId) return; // sessionId not yet available — try again on next event
432
445
  const path = resolveStorePath(ctx.cwd, sessionId);
433
446
  const store = new ScheduleStore(path);
434
- scheduler.start(pi, ctx, manager, store);
447
+ scheduler.start(pi, ctx, manager, store, { depth: nextSubagentDepth, parentAgentId: extensionAgentId });
435
448
  pi.events.emit("subagents:scheduler_ready", { sessionId, jobCount: store.list().length });
436
449
  } catch (err) {
437
450
  // Scheduling is non-essential — log and move on so the rest of the
@@ -443,6 +456,8 @@ export default function (pi: ExtensionAPI) {
443
456
  // Capture ctx from session_start for RPC spawn handler + start the scheduler.
444
457
  pi.on("session_start", async (_event, ctx) => {
445
458
  currentCtx = ctx;
459
+ clearBatchState();
460
+ groupJoin.dispose();
446
461
  manager.clearCompleted();
447
462
  widget.clearSnapshots();
448
463
  retryStash.clear();
@@ -450,8 +465,10 @@ export default function (pi: ExtensionAPI) {
450
465
  });
451
466
 
452
467
  pi.on("session_before_switch", () => {
468
+ clearBatchState();
469
+ groupJoin.dispose();
453
470
  manager.clearCompleted();
454
- widget.clearSnapshots();
471
+ widget.dispose();
455
472
  retryStash.clear();
456
473
  scheduler.stop();
457
474
  });
@@ -461,6 +478,8 @@ export default function (pi: ExtensionAPI) {
461
478
  pi,
462
479
  getCtx: () => currentCtx,
463
480
  manager,
481
+ depth: nextSubagentDepth,
482
+ parentAgentId: extensionAgentId,
464
483
  });
465
484
 
466
485
  // Broadcast readiness so extensions loaded after us can discover us
@@ -472,13 +491,17 @@ export default function (pi: ExtensionAPI) {
472
491
  unsubSpawnRpc();
473
492
  unsubStopRpc();
474
493
  unsubPingRpc();
494
+ unsubWidgetCreated?.();
475
495
  unsubWidgetStarted?.();
476
496
  unsubWidgetCompleted?.();
477
497
  unsubWidgetFailed?.();
478
498
  currentCtx = undefined;
479
499
  delete (globalThis as any)[MANAGER_KEY];
480
500
  scheduler.stop();
501
+ clearBatchState();
502
+ groupJoin.dispose();
481
503
  manager.abortAll();
504
+ widget.dispose();
482
505
  for (const timer of pendingNudges.values()) clearTimeout(timer);
483
506
  pendingNudges.clear();
484
507
  retryStash.clear();
@@ -491,6 +514,7 @@ export default function (pi: ExtensionAPI) {
491
514
  const snapshot = widgetSnapshotFromEvent(payload);
492
515
  if (snapshot) widget.upsertSnapshot(snapshot);
493
516
  };
517
+ const unsubWidgetCreated = pi.events.on("subagents:created", upsertWidgetEventSnapshot);
494
518
  const unsubWidgetStarted = pi.events.on("subagents:started", upsertWidgetEventSnapshot);
495
519
  const unsubWidgetCompleted = pi.events.on("subagents:completed", upsertWidgetEventSnapshot);
496
520
  const unsubWidgetFailed = pi.events.on("subagents:failed", upsertWidgetEventSnapshot);
@@ -567,6 +591,14 @@ export default function (pi: ExtensionAPI) {
567
591
  let batchFinalizeTimer: ReturnType<typeof setTimeout> | undefined;
568
592
  let batchCounter = 0;
569
593
 
594
+ function clearBatchState() {
595
+ if (batchFinalizeTimer) {
596
+ clearTimeout(batchFinalizeTimer);
597
+ batchFinalizeTimer = undefined;
598
+ }
599
+ currentBatchAgents = [];
600
+ }
601
+
570
602
  /** Finalize the current batch: if 2+ smart-mode agents, register as a group. */
571
603
  function finalizeBatch() {
572
604
  batchFinalizeTimer = undefined;
@@ -653,7 +685,7 @@ export default function (pi: ExtensionAPI) {
653
685
 
654
686
  const agentToolDescription = buildAgentToolDescription({
655
687
  mode: getToolDescriptionMode(),
656
- extensionDepth,
688
+ nextSubagentDepth,
657
689
  schedulingEnabled: isSchedulingEnabled(),
658
690
  });
659
691
 
@@ -679,7 +711,7 @@ export default function (pi: ExtensionAPI) {
679
711
  }),
680
712
  subagent_type: Type.Optional(
681
713
  Type.String({
682
- description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available. OMIT when retrying (preserved by the handle) unless you want to override it.`,
714
+ description: `The type of specialized agent to use. Defaults to general-purpose when omitted. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available. OMIT when retrying (preserved by the handle) unless you want to override it.`,
683
715
  }),
684
716
  ),
685
717
  model: Type.Optional(
@@ -782,18 +814,16 @@ export default function (pi: ExtensionAPI) {
782
814
  const { retry: _omit, ...overrides } = params;
783
815
  P = { ...stashed.params, ...overrides } as typeof params;
784
816
  }
785
- emitPromptPreview(P.prompt, P.description, P.subagent_type);
817
+ const requestedSubagentType = (P.subagent_type ?? "general-purpose") as SubagentType;
818
+ emitPromptPreview(P.prompt, P.description, requestedSubagentType);
786
819
 
787
- // Retry supplied the prompt/type from the stash; otherwise both are required.
788
- if (!retryHandle && (!P.prompt || !P.subagent_type)) {
789
- return textResult(
790
- `Missing required argument${!P.prompt && !P.subagent_type ? "s" : ""}: ` +
791
- [!P.prompt && "prompt", !P.subagent_type && "subagent_type"].filter(Boolean).join(", ") +
792
- ".",
793
- );
820
+ // Retry supplied the prompt from the stash; otherwise prompt is required.
821
+ // subagent_type defaults to general-purpose when omitted.
822
+ if (!retryHandle && !P.prompt) {
823
+ return textResult("Missing required argument: prompt.");
794
824
  }
795
825
 
796
- const rawType = P.subagent_type as SubagentType;
826
+ const rawType = requestedSubagentType;
797
827
  const resolved = resolveType(rawType);
798
828
  if (!resolved) {
799
829
  // Unknown agent type — recoverable. List valid types so the orchestrator
@@ -988,6 +1018,7 @@ export default function (pi: ExtensionAPI) {
988
1018
  invocation: agentInvocation,
989
1019
  depth: nextSubagentDepth,
990
1020
  parentAgentId: extensionAgentId,
1021
+ eventBus: pi.events,
991
1022
  outputFileForAgent: (agentId) => createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId()),
992
1023
  onOutputFileCreated: (outputFile, agentId) => writeInitialEntry(outputFile, agentId, P.prompt!, ctx.cwd),
993
1024
  ...bgCallbacks,
@@ -1032,6 +1063,10 @@ export default function (pi: ExtensionAPI) {
1032
1063
  isBackground: true,
1033
1064
  depth: record?.depth ?? nextSubagentDepth,
1034
1065
  parentAgentId: extensionAgentId,
1066
+ status: record?.status ?? "running",
1067
+ startedAt: record?.startedAt,
1068
+ toolUses: record?.toolUses ?? 0,
1069
+ invocation: record?.invocation,
1035
1070
  });
1036
1071
 
1037
1072
  const isQueued = record?.status === "queued";
@@ -1077,6 +1112,57 @@ export default function (pi: ExtensionAPI) {
1077
1112
  }),
1078
1113
  ),
1079
1114
  }),
1115
+ renderResult(result, { expanded }, theme) {
1116
+ const details = result.details as GetResultDetails | undefined;
1117
+ const text = extractText(result.content);
1118
+
1119
+ // Header: status + stats + description
1120
+ let line = "";
1121
+ if (details) {
1122
+ const icon = details.status === "error" || details.status === "stopped" || details.status === "aborted"
1123
+ ? theme.fg("error", "✗")
1124
+ : details.status === "running" || details.status === "queued"
1125
+ ? theme.fg("accent", "◌")
1126
+ : theme.fg("success", "✓");
1127
+
1128
+ const parts: string[] = [];
1129
+ if (details.toolUses > 0) parts.push(`${details.toolUses} tool use${details.toolUses === 1 ? "" : "s"}`);
1130
+ if (details.tokens) parts.push(details.tokens);
1131
+ if (details.contextPercent !== null) parts.push(`ctx ${Math.round(details.contextPercent)}%`);
1132
+ if (details.duration) parts.push(details.duration);
1133
+ const stats = parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
1134
+
1135
+ line = `${icon} ${theme.bold(details.description)} ${theme.fg("dim", details.status)}`;
1136
+ if (stats) line += "\n " + stats;
1137
+ }
1138
+
1139
+ // Body: snip when collapsed, full when expanded
1140
+ // Extract the body portion (after the first blank line) to keep the
1141
+ // tool-output header always visible and only snip the actual result.
1142
+ if (text.trim()) {
1143
+ const firstBlank = text.indexOf("\n\n");
1144
+ const body = firstBlank >= 0 ? text.slice(firstBlank + 2) : text;
1145
+
1146
+ if (expanded) {
1147
+ for (const l of text.split("\n")) {
1148
+ line += "\n" + theme.fg("dim", ` ${l}`);
1149
+ }
1150
+ } else {
1151
+ // Show the tool-output header verbatim, then snip only the body
1152
+ if (firstBlank >= 0) {
1153
+ for (const l of text.slice(0, firstBlank).split("\n")) {
1154
+ line += "\n" + theme.fg("dim", ` ${l}`);
1155
+ }
1156
+ }
1157
+ for (const l of snipMiddleLines(body, 20)) {
1158
+ line += "\n" + theme.fg("dim", ` ${l}`);
1159
+ }
1160
+ }
1161
+ }
1162
+
1163
+ return new Text(line, 0, 0);
1164
+ },
1165
+
1080
1166
  execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
1081
1167
  const record = manager.getRecord(params.agent_id);
1082
1168
  if (!record) {
@@ -1125,6 +1211,16 @@ export default function (pi: ExtensionAPI) {
1125
1211
  if (record.compactionCount) statsParts.push(`Compactions: ${record.compactionCount}`);
1126
1212
  statsParts.push(`Duration: ${duration}`);
1127
1213
 
1214
+ const details: GetResultDetails = {
1215
+ status: record.status,
1216
+ description: record.description,
1217
+ toolUses: record.toolUses,
1218
+ tokens: tokens || null,
1219
+ contextPercent,
1220
+ duration,
1221
+ outputFile: record.outputFile,
1222
+ };
1223
+
1128
1224
  let output =
1129
1225
  `Agent: ${record.id}\n` +
1130
1226
  `Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
@@ -1168,7 +1264,7 @@ export default function (pi: ExtensionAPI) {
1168
1264
  }
1169
1265
  }
1170
1266
 
1171
- return textResult(output);
1267
+ return textResult(output, details);
1172
1268
  },
1173
1269
  }));
1174
1270
 
@@ -1228,6 +1324,11 @@ export default function (pi: ExtensionAPI) {
1228
1324
  },
1229
1325
  }));
1230
1326
 
1327
+ // ---- list_subagents / clear_subagents tools ----
1328
+
1329
+ registerSubagentListClearTools(pi, manager);
1330
+ registerDashboardModules(pi, manager);
1331
+
1231
1332
  // ---- list_models tool ----
1232
1333
 
1233
1334
  pi.registerTool(defineTool({
@@ -1728,6 +1829,9 @@ Write the file using the write tool. Only write the file, nothing else.`;
1728
1829
  const record = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
1729
1830
  description: `Generate ${name} agent`,
1730
1831
  maxTurns: 5,
1832
+ eventBus: pi.events,
1833
+ depth: nextSubagentDepth,
1834
+ parentAgentId: extensionAgentId,
1731
1835
  });
1732
1836
 
1733
1837
  if (record.status === "error") {
package/src/peek.ts CHANGED
@@ -112,6 +112,11 @@ function parseOutputFileLines(path: string): string[] {
112
112
  return [];
113
113
  }
114
114
  const out: string[] = [];
115
+ const pushRenderedLines = (text: string) => {
116
+ for (const renderedLine of text.trimEnd().split("\n")) {
117
+ if (renderedLine.trim()) out.push(renderedLine);
118
+ }
119
+ };
115
120
  for (const line of raw.split("\n")) {
116
121
  const trimmed = line.trim();
117
122
  if (!trimmed) continue;
@@ -124,12 +129,12 @@ function parseOutputFileLines(path: string): string[] {
124
129
  const content = entry?.message?.content;
125
130
  if (!Array.isArray(content)) {
126
131
  // Some entries may carry a plain string content.
127
- if (typeof content === "string" && content.trim()) out.push(content.trim());
132
+ if (typeof content === "string" && content.trim()) pushRenderedLines(content);
128
133
  continue;
129
134
  }
130
135
  for (const block of content) {
131
136
  if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) {
132
- out.push(block.text.trimEnd());
137
+ pushRenderedLines(block.text);
133
138
  }
134
139
  }
135
140
  }
package/src/schedule.ts CHANGED
@@ -45,6 +45,13 @@ export interface NewJobInput {
45
45
  isolation?: IsolationMode;
46
46
  }
47
47
 
48
+ interface SchedulerSpawnDefaults {
49
+ /** Recursive depth for scheduled subagents fired from this session. */
50
+ depth?: number;
51
+ /** Parent subagent id for scheduled subagents fired from this session. */
52
+ parentAgentId?: string;
53
+ }
54
+
48
55
  export class SubagentScheduler {
49
56
  private jobs = new Map<string, Cron>();
50
57
  private intervals = new Map<string, NodeJS.Timeout>();
@@ -52,13 +59,21 @@ export class SubagentScheduler {
52
59
  private pi: ExtensionAPI | undefined;
53
60
  private ctx: ExtensionContext | undefined;
54
61
  private manager: AgentManager | undefined;
62
+ private spawnDefaults: SchedulerSpawnDefaults = {};
55
63
 
56
64
  /** Start the scheduler: bind to a session's store and arm enabled jobs. */
57
- start(pi: ExtensionAPI, ctx: ExtensionContext, manager: AgentManager, store: ScheduleStore): void {
65
+ start(
66
+ pi: ExtensionAPI,
67
+ ctx: ExtensionContext,
68
+ manager: AgentManager,
69
+ store: ScheduleStore,
70
+ spawnDefaults: SchedulerSpawnDefaults = {},
71
+ ): void {
58
72
  this.pi = pi;
59
73
  this.ctx = ctx;
60
74
  this.manager = manager;
61
75
  this.store = store;
76
+ this.spawnDefaults = spawnDefaults;
62
77
 
63
78
  for (const job of store.list()) {
64
79
  if (job.enabled) this.scheduleJob(job);
@@ -75,6 +90,7 @@ export class SubagentScheduler {
75
90
  this.pi = undefined;
76
91
  this.ctx = undefined;
77
92
  this.manager = undefined;
93
+ this.spawnDefaults = {};
78
94
  }
79
95
 
80
96
  /** True if start() has bound a store and the scheduler is active. */
@@ -247,6 +263,9 @@ export class SubagentScheduler {
247
263
  isolated: job.isolated,
248
264
  thinkingLevel: job.thinking,
249
265
  isolation: job.isolation,
266
+ eventBus: pi.events,
267
+ depth: this.spawnDefaults.depth,
268
+ parentAgentId: this.spawnDefaults.parentAgentId,
250
269
  });
251
270
  } catch (err) {
252
271
  const error = err instanceof Error ? err.message : String(err);