@ryan_nookpi/pi-extension-subagent 0.5.6 → 0.5.7

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/README.md CHANGED
@@ -98,9 +98,9 @@ subagent run <agent> [--main|--isolated] -- <task>
98
98
  subagent continue <runId> [--agent <agent>] [--main|--isolated] -- <task>
99
99
  subagent batch [--main|--isolated] --agent <agent> --task <task> ...
100
100
  subagent chain [--main|--isolated] --agent <agent> --task <task> ...
101
- subagent status <runId>
102
- subagent detail <runId>
103
- subagent abort <runId|runId,runId|all>
101
+ subagent status <runId|groupId>
102
+ subagent detail <runId|groupId>
103
+ subagent abort <runId|groupId|runId,runId|all>
104
104
  subagent remove <runId|runId,runId|all>
105
105
  ```
106
106
 
package/cli.ts CHANGED
@@ -64,7 +64,7 @@ export const SUBAGENT_CLI_HELP_TEXT = [
64
64
  " subagent chain [--main|--isolated] --agent <agent> --task <task> --agent <agent> --task <task> ...",
65
65
  "",
66
66
  " Cleanup:",
67
- " subagent abort <runId|runId,runId|all>",
67
+ " subagent abort <runId|groupId|runId,runId|all>",
68
68
  " subagent remove <runId|runId,runId|all>",
69
69
  "",
70
70
  "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
@@ -88,6 +88,7 @@ export const SUBAGENT_CLI_HELP_TEXT = [
88
88
  " subagent status 22",
89
89
  " subagent detail 22",
90
90
  " subagent abort 22",
91
+ " subagent abort p_1712...",
91
92
  " subagent remove all",
92
93
  "",
93
94
  "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
@@ -508,8 +509,14 @@ export function parseSubagentToolCommand(
508
509
  if (!target)
509
510
  return {
510
511
  type: "error",
511
- message: `❌ ${verb} requires <runId|runId,runId|all>\n\n✓ Examples:\n subagent ${verb} 22\n subagent ${verb} 22,23,24\n subagent ${verb} all`,
512
+ message:
513
+ verb === "abort"
514
+ ? "❌ abort requires <runId|groupId|runId,runId|all>\n\n✓ Examples:\n subagent abort 22\n subagent abort p_1712_abcd\n subagent abort 22,23,24\n subagent abort all"
515
+ : "❌ remove requires <runId|runId,runId|all>\n\n✓ Examples:\n subagent remove 22\n subagent remove 22,23,24\n subagent remove all",
512
516
  };
517
+ if (verb === "abort" && isGroupId(target)) {
518
+ return { type: "params", params: { asyncAction: "abort", groupId: target } };
519
+ }
513
520
  const parsedTarget = parseRunTarget(target, options.knownRunIds);
514
521
  if ("error" in parsedTarget)
515
522
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/run-utils.ts CHANGED
@@ -242,18 +242,18 @@ export function snapshotBatchGroup(
242
242
  let failed = 0;
243
243
  let aborted = 0;
244
244
  const members: FinishedGroupMember[] = batch.runIds.map((runId) => {
245
- const run = store.commandRuns.get(runId);
245
+ const run = batch.completedRuns.get(runId) ?? store.commandRuns.get(runId);
246
246
  if (!run) {
247
247
  if (batch.abortedRunIds.has(runId)) aborted++;
248
248
  else if (batch.failedRunIds.has(runId)) failed++;
249
249
  return {
250
250
  summaryLine: `#${runId} [gone] (run no longer available)`,
251
- output: batch.pendingResults.get(runId)?.trim() || "(no output)",
251
+ output: "(no output)",
252
252
  };
253
253
  }
254
254
  if (run.errorClass === "aborted" || batch.abortedRunIds.has(runId)) aborted++;
255
255
  else if (run.status === "error" || batch.failedRunIds.has(runId)) failed++;
256
- return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run, batch.pendingResults.get(runId)) };
256
+ return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run) };
257
257
  });
258
258
  return {
259
259
  groupId: batch.batchId,
package/tool-execute.ts CHANGED
@@ -502,7 +502,13 @@ function recordBatchResult(batch: BatchGroupState, finalized: FinalizedRun): voi
502
502
  batch.completedRunIds.add(runId);
503
503
  if (status === "aborted") batch.abortedRunIds.add(runId);
504
504
  else if (status === "error") batch.failedRunIds.add(runId);
505
- batch.pendingResults.set(runId, finalized.rawOutput);
505
+ batch.completedRuns.set(runId, { ...finalized.runState, abortController: undefined });
506
+ }
507
+
508
+ function getCompletedBatchRuns(batch: BatchGroupState): CommandRunState[] {
509
+ return batch.runIds
510
+ .map((runId) => batch.completedRuns.get(runId))
511
+ .filter((run): run is CommandRunState => Boolean(run));
506
512
  }
507
513
 
508
514
  function buildRunCompletionMessage(finalized: FinalizedRun, options?: { display?: boolean }) {
@@ -1040,6 +1046,45 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1040
1046
  };
1041
1047
  }
1042
1048
 
1049
+ let abortGroupTarget: { groupId: string; kind: "batch" | "chain" } | undefined;
1050
+ if (asyncAction === "abort" && typeof cmdParams.groupId === "string") {
1051
+ evictStaleFinishedGroups(store);
1052
+ const groupId = cmdParams.groupId;
1053
+ const finished = store.finishedGroups.get(groupId);
1054
+ if (finished) {
1055
+ return {
1056
+ content: [{ type: "text", text: `Subagent ${finished.kind} ${groupId} has already finished.` }],
1057
+ details: makeDetails("single"),
1058
+ };
1059
+ }
1060
+
1061
+ const batch = store.batchGroups.get(groupId);
1062
+ const pipeline = store.pipelines.get(groupId);
1063
+ if (!batch && !pipeline) {
1064
+ return {
1065
+ content: [{ type: "text", text: `Unknown subagent group "${groupId}".` }],
1066
+ details: makeDetails("single"),
1067
+ isError: true,
1068
+ };
1069
+ }
1070
+
1071
+ abortGroupTarget = { groupId, kind: batch ? "batch" : "chain" };
1072
+ if (pipeline) pipeline.abortRequested = true;
1073
+ const groupRunIds = (batch?.runIds ?? pipeline?.stepRunIds ?? []).filter((runId) => {
1074
+ const run = store.globalLiveRuns.get(runId)?.runState ?? store.commandRuns.get(runId);
1075
+ return batch ? run?.batchId === groupId : run?.pipelineId === groupId;
1076
+ });
1077
+ if (groupRunIds.length === 0) {
1078
+ return {
1079
+ content: [
1080
+ { type: "text", text: `Subagent ${abortGroupTarget.kind} ${groupId} has no started runs to abort.` },
1081
+ ],
1082
+ details: makeDetails("single"),
1083
+ };
1084
+ }
1085
+ cmdParams.runIds = groupRunIds;
1086
+ }
1087
+
1043
1088
  if ((asyncAction === "status" || asyncAction === "detail") && typeof cmdParams.groupId === "string") {
1044
1089
  evictStaleFinishedGroups(store);
1045
1090
  const groupId = cmdParams.groupId;
@@ -1178,7 +1223,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1178
1223
  const unknown: number[] = [];
1179
1224
 
1180
1225
  for (const runId of targetRunIds) {
1181
- const run = store.commandRuns.get(runId);
1226
+ const run = abortGroupTarget
1227
+ ? (store.globalLiveRuns.get(runId)?.runState ?? store.commandRuns.get(runId))
1228
+ : store.commandRuns.get(runId);
1182
1229
  if (!run) {
1183
1230
  unknown.push(runId);
1184
1231
  continue;
@@ -1200,7 +1247,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1200
1247
  updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
1201
1248
  }
1202
1249
 
1203
- if (targetRunIds.length === 1 && aborting.length === 1 && notRunning.length === 0 && unknown.length === 0) {
1250
+ if (
1251
+ !abortGroupTarget &&
1252
+ targetRunIds.length === 1 &&
1253
+ aborting.length === 1 &&
1254
+ notRunning.length === 0 &&
1255
+ unknown.length === 0
1256
+ ) {
1204
1257
  return {
1205
1258
  content: [{ type: "text", text: `Aborting subagent run #${aborting[0]}...` }],
1206
1259
  details: makeDetails("single"),
@@ -1208,6 +1261,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1208
1261
  }
1209
1262
 
1210
1263
  const lines: string[] = [];
1264
+ if (abortGroupTarget) {
1265
+ lines.push(`Subagent ${abortGroupTarget.kind} ${abortGroupTarget.groupId}:`);
1266
+ }
1211
1267
  if (aborting.length > 0) lines.push(`Aborting: ${aborting.map((id) => `#${id}`).join(", ")}.`);
1212
1268
  if (notRunning.length > 0) lines.push(`Not running: ${notRunning.map((id) => `#${id}`).join(", ")}.`);
1213
1269
  if (unknown.length > 0) lines.push(`Unknown: ${unknown.map((id) => `#${id}`).join(", ")}.`);
@@ -1413,6 +1469,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1413
1469
  store.recentLaunchTimestamps.delete(runId);
1414
1470
  }
1415
1471
 
1472
+ function cleanupGroupRunAfterFinalDelivery(runId: number, groupId: string, kind: "batch" | "chain") {
1473
+ const run = store.globalLiveRuns.get(runId)?.runState ?? store.commandRuns.get(runId);
1474
+ const ownsGroup = kind === "batch" ? run?.batchId === groupId : run?.pipelineId === groupId;
1475
+ if (run && !ownsGroup) return;
1476
+ cleanupRunAfterFinalDelivery(runId);
1477
+ }
1478
+
1416
1479
  function launchRunInBackground(runState: CommandRunState, taskForAgent: string): Promise<FinalizedRun> {
1417
1480
  let claudeCheckpointSent = !!runState.claudeSessionId;
1418
1481
  const abortSignal = runState.abortController?.signal;
@@ -1734,7 +1797,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1734
1797
  abortedRunIds: new Set(),
1735
1798
  originSessionFile,
1736
1799
  createdAt: Date.now(),
1737
- pendingResults: new Map(),
1800
+ completedRuns: new Map(),
1738
1801
  });
1739
1802
  updateCommandRunsWidget(store, ctx as WidgetRenderCtx);
1740
1803
 
@@ -1750,11 +1813,13 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1750
1813
  }
1751
1814
  }),
1752
1815
  );
1753
- const orderedRuns = runStates.map(({ runState }) => runState);
1754
1816
  const batchForSnapshot = store.batchGroups.get(batchId);
1755
1817
  if (batchForSnapshot) {
1756
1818
  for (const finalized of finalizedRuns) recordBatchResult(batchForSnapshot, finalized);
1757
1819
  }
1820
+ const orderedRuns = batchForSnapshot
1821
+ ? getCompletedBatchRuns(batchForSnapshot)
1822
+ : runStates.map(({ runState }) => runState);
1758
1823
  const terminalStatus = resolveGroupTerminalStatus(
1759
1824
  batchForSnapshot?.failedRunIds.size ?? 0,
1760
1825
  batchForSnapshot?.abortedRunIds.size ?? 0,
@@ -1763,7 +1828,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1763
1828
  if (batchForSnapshot) {
1764
1829
  retireFinishedGroup(store, snapshotBatchGroup(store, batchForSnapshot, terminalStatus));
1765
1830
  }
1766
- for (const { runState } of runStates) cleanupRunAfterFinalDelivery(runState.id);
1831
+ for (const { runState } of runStates) cleanupGroupRunAfterFinalDelivery(runState.id, batchId, "batch");
1767
1832
  clearPendingGroupCompletion("batch", batchId);
1768
1833
  store.batchGroups.delete(batchId);
1769
1834
  trimCommandRunHistory(store, {
@@ -1799,9 +1864,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1799
1864
  updateCommandRunsWidget(store);
1800
1865
 
1801
1866
  if (batch.completedRunIds.size === batch.runIds.length) {
1802
- const orderedRuns = batch.runIds
1803
- .map((runId) => store.commandRuns.get(runId))
1804
- .filter((run): run is CommandRunState => Boolean(run));
1867
+ const orderedRuns = getCompletedBatchRuns(batch);
1805
1868
  const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
1806
1869
  const content = formatBatchSummary(batchId, orderedRuns, batchTerminalStatus);
1807
1870
  const message = {
@@ -1819,7 +1882,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1819
1882
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1820
1883
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
1821
1884
  clearPendingGroupCompletion("batch", batchId);
1822
- for (const runId of batch.runIds) cleanupRunAfterFinalDelivery(runId);
1885
+ for (const runId of batch.runIds) cleanupGroupRunAfterFinalDelivery(runId, batchId, "batch");
1823
1886
  store.batchGroups.delete(batchId);
1824
1887
  } else {
1825
1888
  batch.pendingCompletion = makePendingCompletion(message, true);
@@ -1853,9 +1916,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1853
1916
  if (!batch) return;
1854
1917
  recordBatchResult(batch, finalized);
1855
1918
  if (batch.completedRunIds.size === batch.runIds.length) {
1856
- const orderedRuns = batch.runIds
1857
- .map((runId) => store.commandRuns.get(runId))
1858
- .filter((run): run is CommandRunState => Boolean(run));
1919
+ const orderedRuns = getCompletedBatchRuns(batch);
1859
1920
  const batchTerminalStatus = resolveGroupTerminalStatus(batch.failedRunIds.size, batch.abortedRunIds.size);
1860
1921
  const message = {
1861
1922
  customType: "subagent-tool" as const,
@@ -1872,7 +1933,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1872
1933
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1873
1934
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
1874
1935
  clearPendingGroupCompletion("batch", batchId);
1875
- for (const runId of batch.runIds) cleanupRunAfterFinalDelivery(runId);
1936
+ for (const runId of batch.runIds) cleanupGroupRunAfterFinalDelivery(runId, batchId, "batch");
1876
1937
  store.batchGroups.delete(batchId);
1877
1938
  } else {
1878
1939
  batch.pendingCompletion = makePendingCompletion(message, true);
@@ -1931,6 +1992,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1931
1992
  store.pipelines.set(pipelineId, {
1932
1993
  pipelineId,
1933
1994
  currentIndex: 0,
1995
+ abortRequested: false,
1934
1996
  stepRunIds: [],
1935
1997
  stepResults: [],
1936
1998
  originSessionFile,
@@ -1952,6 +2014,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1952
2014
 
1953
2015
  try {
1954
2016
  for (let index = 0; index < steps.length; index++) {
2017
+ if (pipeline.abortRequested) {
2018
+ terminalStatus = "aborted";
2019
+ break;
2020
+ }
1955
2021
  pipeline.currentIndex = index;
1956
2022
  const step = steps[index];
1957
2023
  const pipelineReferenceSection =
@@ -2005,26 +2071,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2005
2071
  }
2006
2072
  finalizedRuns.push(finalized);
2007
2073
 
2008
- if (runState.removed) {
2009
- terminalStatus = "aborted";
2010
- pipeline.stepResults.push({
2011
- runId: runState.id,
2012
- agent: runState.agent,
2013
- task: step.task,
2014
- output: finalized.rawOutput || "Run removed before pipeline completion.",
2015
- status: "aborted",
2016
- });
2017
- break;
2018
- }
2019
-
2020
- const stepStatus = getFinalizedRunStatus(finalized);
2074
+ const stepStatus = runState.removed ? "aborted" : getFinalizedRunStatus(finalized);
2021
2075
  pipeline.stepResults.push({
2022
2076
  runId: runState.id,
2023
2077
  agent: runState.agent,
2024
2078
  task: step.task,
2025
- output: finalized.rawOutput,
2079
+ output: runState.removed
2080
+ ? finalized.rawOutput || "Run removed before pipeline completion."
2081
+ : finalized.rawOutput,
2026
2082
  status: stepStatus,
2027
2083
  });
2084
+ if (runState.removed || pipeline.abortRequested) {
2085
+ terminalStatus = "aborted";
2086
+ break;
2087
+ }
2028
2088
  previousOutput = finalized.rawOutput;
2029
2089
  updateCommandRunsWidget(store);
2030
2090
 
@@ -2050,7 +2110,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2050
2110
  else if (terminalStatus === "completed" && hasAborted) terminalStatus = "aborted";
2051
2111
  const content = formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus);
2052
2112
  retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
2053
- for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
2113
+ for (const runId of pipeline.stepRunIds) cleanupGroupRunAfterFinalDelivery(runId, pipelineId, "chain");
2054
2114
  clearPendingGroupCompletion("chain", pipelineId);
2055
2115
  store.pipelines.delete(pipelineId);
2056
2116
  trimCommandRunHistory(store, {
@@ -2082,6 +2142,10 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2082
2142
  for (let index = 0; index < steps.length; index++) {
2083
2143
  const pipeline = store.pipelines.get(pipelineId);
2084
2144
  if (!pipeline) return;
2145
+ if (pipeline.abortRequested) {
2146
+ terminalStatus = "aborted";
2147
+ break;
2148
+ }
2085
2149
  pipeline.currentIndex = index;
2086
2150
 
2087
2151
  const step = steps[index];
@@ -2134,26 +2198,20 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2134
2198
  } finally {
2135
2199
  clearRunAbortState(runState);
2136
2200
  }
2137
- if (runState.removed) {
2138
- terminalStatus = "aborted";
2139
- pipeline.stepResults.push({
2140
- runId: runState.id,
2141
- agent: runState.agent,
2142
- task: step.task,
2143
- output: finalized.rawOutput || "Run removed before pipeline completion.",
2144
- status: "aborted",
2145
- });
2146
- break;
2147
- }
2148
-
2149
2201
  const stepResult: PipelineStepResult = {
2150
2202
  runId: runState.id,
2151
2203
  agent: runState.agent,
2152
2204
  task: step.task,
2153
- output: finalized.rawOutput,
2154
- status: getFinalizedRunStatus(finalized),
2205
+ output: runState.removed
2206
+ ? finalized.rawOutput || "Run removed before pipeline completion."
2207
+ : finalized.rawOutput,
2208
+ status: runState.removed ? "aborted" : getFinalizedRunStatus(finalized),
2155
2209
  };
2156
2210
  pipeline.stepResults.push(stepResult);
2211
+ if (runState.removed || pipeline.abortRequested) {
2212
+ terminalStatus = "aborted";
2213
+ break;
2214
+ }
2157
2215
  previousOutput = finalized.rawOutput;
2158
2216
  updateCommandRunsWidget(store);
2159
2217
 
@@ -2199,7 +2257,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
2199
2257
  if (isInOriginSession(ctx, pipeline.originSessionFile)) {
2200
2258
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
2201
2259
  clearPendingGroupCompletion("chain", pipelineId);
2202
- for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
2260
+ for (const runId of pipeline.stepRunIds) cleanupGroupRunAfterFinalDelivery(runId, pipelineId, "chain");
2203
2261
  store.pipelines.delete(pipelineId);
2204
2262
  } else {
2205
2263
  pipeline.pendingCompletion = makePendingCompletion(message, true);
package/types.ts CHANGED
@@ -177,7 +177,8 @@ export interface BatchGroupState {
177
177
  abortedRunIds: Set<number>;
178
178
  originSessionFile: string;
179
179
  createdAt: number;
180
- pendingResults: Map<number, string>;
180
+ /** Immutable member states captured as each batch run completes. */
181
+ completedRuns: Map<number, CommandRunState>;
181
182
  pendingCompletion?: PendingCompletion;
182
183
  }
183
184
 
@@ -192,6 +193,8 @@ export interface PipelineStepResult {
192
193
  export interface PipelineState {
193
194
  pipelineId: string;
194
195
  currentIndex: number;
196
+ /** Set by `abort <pipelineId>` to prevent any subsequent step from launching. */
197
+ abortRequested: boolean;
195
198
  stepRunIds: number[];
196
199
  stepResults: PipelineStepResult[];
197
200
  originSessionFile: string;