@melihmucuk/pi-crew 1.0.13 → 1.0.14

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 (68) hide show
  1. package/docs/architecture.md +0 -1
  2. package/extension/agent-discovery.ts +791 -0
  3. package/extension/bootstrap-session.ts +131 -0
  4. package/extension/index.ts +65 -0
  5. package/extension/integration/register-command.ts +59 -0
  6. package/extension/integration/register-renderers.ts +77 -0
  7. package/extension/integration/register-tools.ts +39 -0
  8. package/extension/integration/tool-presentation.ts +50 -0
  9. package/extension/integration/tools/crew-abort.ts +121 -0
  10. package/extension/integration/tools/crew-done.ts +42 -0
  11. package/extension/integration/tools/crew-list.ts +91 -0
  12. package/extension/integration/tools/crew-respond.ts +57 -0
  13. package/extension/integration/tools/crew-spawn.ts +88 -0
  14. package/extension/integration/tools/tool-deps.ts +16 -0
  15. package/extension/integration.ts +15 -0
  16. package/extension/runtime/crew-runtime.ts +426 -0
  17. package/extension/runtime/delivery-coordinator.ts +131 -0
  18. package/extension/runtime/overflow-recovery.ts +211 -0
  19. package/extension/runtime/subagent-registry.ts +85 -0
  20. package/extension/runtime/subagent-state.ts +73 -0
  21. package/extension/status-widget.ts +107 -0
  22. package/extension/subagent-messages.ts +124 -0
  23. package/extension/tool-registry.ts +19 -0
  24. package/package.json +8 -11
  25. package/dist/agent-discovery.d.ts +0 -29
  26. package/dist/agent-discovery.js +0 -527
  27. package/dist/bootstrap-session.d.ts +0 -21
  28. package/dist/bootstrap-session.js +0 -74
  29. package/dist/index.d.ts +0 -2
  30. package/dist/index.js +0 -46
  31. package/dist/integration/register-command.d.ts +0 -3
  32. package/dist/integration/register-command.js +0 -51
  33. package/dist/integration/register-renderers.d.ts +0 -2
  34. package/dist/integration/register-renderers.js +0 -59
  35. package/dist/integration/register-tools.d.ts +0 -3
  36. package/dist/integration/register-tools.js +0 -25
  37. package/dist/integration/tool-presentation.d.ts +0 -27
  38. package/dist/integration/tool-presentation.js +0 -29
  39. package/dist/integration/tools/crew-abort.d.ts +0 -2
  40. package/dist/integration/tools/crew-abort.js +0 -79
  41. package/dist/integration/tools/crew-done.d.ts +0 -2
  42. package/dist/integration/tools/crew-done.js +0 -28
  43. package/dist/integration/tools/crew-list.d.ts +0 -2
  44. package/dist/integration/tools/crew-list.js +0 -74
  45. package/dist/integration/tools/crew-respond.d.ts +0 -2
  46. package/dist/integration/tools/crew-respond.js +0 -32
  47. package/dist/integration/tools/crew-spawn.d.ts +0 -2
  48. package/dist/integration/tools/crew-spawn.js +0 -48
  49. package/dist/integration/tools/tool-deps.d.ts +0 -9
  50. package/dist/integration/tools/tool-deps.js +0 -1
  51. package/dist/integration.d.ts +0 -3
  52. package/dist/integration.js +0 -8
  53. package/dist/runtime/crew-runtime.d.ts +0 -62
  54. package/dist/runtime/crew-runtime.js +0 -285
  55. package/dist/runtime/delivery-coordinator.d.ts +0 -26
  56. package/dist/runtime/delivery-coordinator.js +0 -86
  57. package/dist/runtime/overflow-recovery.d.ts +0 -3
  58. package/dist/runtime/overflow-recovery.js +0 -155
  59. package/dist/runtime/subagent-registry.d.ts +0 -14
  60. package/dist/runtime/subagent-registry.js +0 -58
  61. package/dist/runtime/subagent-state.d.ts +0 -35
  62. package/dist/runtime/subagent-state.js +0 -32
  63. package/dist/status-widget.d.ts +0 -3
  64. package/dist/status-widget.js +0 -84
  65. package/dist/subagent-messages.d.ts +0 -37
  66. package/dist/subagent-messages.js +0 -68
  67. package/dist/tool-registry.d.ts +0 -5
  68. package/dist/tool-registry.js +0 -13
@@ -0,0 +1,57 @@
1
+ import { Type } from "typebox";
2
+ import {
3
+ renderCrewCall,
4
+ renderCrewResult,
5
+ toolError,
6
+ toolSuccess,
7
+ } from "../tool-presentation.js";
8
+ import type { CrewToolDeps } from "./tool-deps.js";
9
+
10
+ export function registerCrewRespondTool({ pi, crew }: CrewToolDeps): void {
11
+ pi.registerTool({
12
+ name: "crew_respond",
13
+ label: "Respond to Crew",
14
+ description:
15
+ "Send a follow-up message to an interactive subagent that is waiting for a response.",
16
+ parameters: Type.Object({
17
+ subagent_id: Type.String({
18
+ description:
19
+ "ID of the waiting subagent (from crew_list or crew_spawn result)",
20
+ }),
21
+ message: Type.String({ description: "Message to send to the subagent" }),
22
+ }),
23
+ promptSnippet:
24
+ "Send a follow-up message to a waiting interactive subagent.",
25
+ promptGuidelines: [
26
+ "crew_respond: Response is delivered asynchronously as a steering message. Do not poll crew_list. Continue with unrelated work or end your turn and wait for the steering message.",
27
+ ],
28
+
29
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
30
+ const callerSessionId = ctx.sessionManager.getSessionId();
31
+ const { error } = crew.respond(
32
+ params.subagent_id,
33
+ params.message,
34
+ callerSessionId,
35
+ );
36
+ if (error) return toolError(error);
37
+
38
+ return toolSuccess(
39
+ `Message sent to subagent ${params.subagent_id}. Response will be delivered as a steering message.`,
40
+ { id: params.subagent_id, message: params.message },
41
+ );
42
+ },
43
+
44
+ renderCall(args, theme, _context) {
45
+ return renderCrewCall(
46
+ theme,
47
+ "crew_respond",
48
+ args.subagent_id || "...",
49
+ args.message,
50
+ );
51
+ },
52
+
53
+ renderResult(result, _options, theme, _context) {
54
+ return renderCrewResult(result, theme);
55
+ },
56
+ });
57
+ }
@@ -0,0 +1,88 @@
1
+ import { getAgentDir } from "@mariozechner/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { discoverAgents } from "../../agent-discovery.js";
4
+ import {
5
+ renderCrewCall,
6
+ renderCrewResult,
7
+ toolError,
8
+ toolSuccess,
9
+ } from "../tool-presentation.js";
10
+ import type { CrewToolDeps } from "./tool-deps.js";
11
+
12
+ export function registerCrewSpawnTool({
13
+ pi,
14
+ crew,
15
+ extensionDir,
16
+ notifyDiscoveryWarnings,
17
+ }: CrewToolDeps): void {
18
+ pi.registerTool({
19
+ name: "crew_spawn",
20
+ label: "Spawn Crew",
21
+ description:
22
+ "Spawn a non-blocking subagent that runs in an isolated session. The subagent works independently while your session stays interactive. Results are delivered back to your session as steering messages.",
23
+ parameters: Type.Object({
24
+ subagent: Type.String({ description: "Subagent name from crew_list" }),
25
+ task: Type.String({ description: "Task to delegate to the subagent" }),
26
+ }),
27
+ promptSnippet:
28
+ "Spawn a non-blocking subagent. Use crew_list first to see available subagents.",
29
+ promptGuidelines: [
30
+ "crew_spawn: The subagent runs in isolation with no access to your session. Include file paths, requirements, and known locations directly in the task parameter.",
31
+ "crew_spawn: DELEGATE means OWNERSHIP TRANSFER. Once you spawn a subagent for a task, that task is exclusively theirs. If you also work on it, you waste the subagent's effort and create conflicting results. After spawning, work on an UNRELATED task or end your turn.",
32
+ "crew_spawn: To avoid duplication, gather only enough context to write a useful task (key files, entry points). Do not pre-investigate the full problem.",
33
+ "crew_spawn: Results arrive asynchronously as steering messages. Do not predict or fabricate results. Wait for all crew-result messages before acting on them.",
34
+ "crew_spawn: Never use crew_list as a completion polling loop. Results arrive as steering messages. Continue with unrelated work or end your turn and wait for the steering messages.",
35
+ "crew_spawn: Interactive subagents stay alive after responding. Use crew_respond to continue or crew_done to close when finished.",
36
+ ],
37
+
38
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
39
+ const { agents, warnings } = discoverAgents(ctx.cwd);
40
+ notifyDiscoveryWarnings(ctx, warnings);
41
+ const subagent = agents.find(
42
+ (candidate) => candidate.name === params.subagent,
43
+ );
44
+
45
+ if (!subagent) {
46
+ const available =
47
+ agents.map((candidate) => candidate.name).join(", ") || "none";
48
+ return toolError(
49
+ `Unknown subagent: "${params.subagent}". Available: ${available}`,
50
+ );
51
+ }
52
+
53
+ const ownerSessionId = ctx.sessionManager.getSessionId();
54
+ const id = crew.spawn(
55
+ subagent,
56
+ params.task,
57
+ ctx.cwd,
58
+ ownerSessionId,
59
+ {
60
+ model: ctx.model,
61
+ modelRegistry: ctx.modelRegistry,
62
+ agentDir: getAgentDir(),
63
+ parentSessionFile: ctx.sessionManager.getSessionFile(),
64
+ onWarning: (msg) => ctx.ui.notify(msg, "warning"),
65
+ },
66
+ extensionDir,
67
+ );
68
+
69
+ return toolSuccess(
70
+ `Subagent '${subagent.name}' spawned as ${id}. Result will be delivered as a steering message when done.`,
71
+ { id, agentName: subagent.name, task: params.task },
72
+ );
73
+ },
74
+
75
+ renderCall(args, theme, _context) {
76
+ return renderCrewCall(
77
+ theme,
78
+ "crew_spawn",
79
+ args.subagent || "...",
80
+ args.task,
81
+ );
82
+ },
83
+
84
+ renderResult(result, _options, theme, _context) {
85
+ return renderCrewResult(result, theme);
86
+ },
87
+ });
88
+ }
@@ -0,0 +1,16 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@mariozechner/pi-coding-agent";
5
+ import type { AgentDiscoveryWarning } from "../../agent-discovery.js";
6
+ import type { CrewRuntime } from "../../runtime/crew-runtime.js";
7
+
8
+ export interface CrewToolDeps {
9
+ pi: ExtensionAPI;
10
+ crew: CrewRuntime;
11
+ extensionDir: string;
12
+ notifyDiscoveryWarnings: (
13
+ ctx: ExtensionContext,
14
+ warnings: AgentDiscoveryWarning[],
15
+ ) => void;
16
+ }
@@ -0,0 +1,15 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { CrewRuntime } from "./runtime/crew-runtime.js";
3
+ import { registerCrewCommand } from "./integration/register-command.js";
4
+ import { registerCrewMessageRenderers } from "./integration/register-renderers.js";
5
+ import { registerCrewTools } from "./integration/register-tools.js";
6
+
7
+ export function registerCrewIntegration(
8
+ pi: ExtensionAPI,
9
+ crew: CrewRuntime,
10
+ extensionDir: string,
11
+ ): void {
12
+ registerCrewTools(pi, crew, extensionDir);
13
+ registerCrewCommand(pi, crew);
14
+ registerCrewMessageRenderers(pi);
15
+ }
@@ -0,0 +1,426 @@
1
+ import type { AgentMessage } from "@mariozechner/pi-agent-core";
2
+ import type { Api, AssistantMessage, Model } from "@mariozechner/pi-ai";
3
+ import type { AgentSession, ModelRegistry } from "@mariozechner/pi-coding-agent";
4
+ import type { AgentConfig } from "../agent-discovery.js";
5
+ import type { BootstrapContext } from "../bootstrap-session.js";
6
+ import { bootstrapSession } from "../bootstrap-session.js";
7
+ import type { SubagentStatus } from "../subagent-messages.js";
8
+ import { type ActiveRuntimeBinding, DeliveryCoordinator } from "./delivery-coordinator.js";
9
+ import { runPromptWithOverflowRecovery } from "./overflow-recovery.js";
10
+ import { SubagentRegistry } from "./subagent-registry.js";
11
+ import {
12
+ type AbortableAgentSummary,
13
+ type ActiveAgentSummary,
14
+ type SubagentState,
15
+ isAbortableStatus,
16
+ isAborted,
17
+ } from "./subagent-state.js";
18
+
19
+ export type {
20
+ AbortableAgentSummary,
21
+ ActiveAgentSummary,
22
+ } from "./subagent-state.js";
23
+
24
+ export interface AbortOwnedResult {
25
+ abortedIds: string[];
26
+ missingIds: string[];
27
+ foreignIds: string[];
28
+ }
29
+
30
+ interface AbortOptions {
31
+ reason: string;
32
+ }
33
+
34
+ export interface SpawnContext {
35
+ model: Model<Api> | undefined;
36
+ modelRegistry: ModelRegistry;
37
+ agentDir: string;
38
+ parentSessionFile?: string;
39
+ onWarning?: (message: string) => void;
40
+ }
41
+
42
+ function toBootstrapContext(ctx: SpawnContext): BootstrapContext {
43
+ return {
44
+ model: ctx.model,
45
+ modelRegistry: ctx.modelRegistry,
46
+ agentDir: ctx.agentDir,
47
+ parentSessionFile: ctx.parentSessionFile,
48
+ };
49
+ }
50
+
51
+ interface PromptOutcome {
52
+ status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">;
53
+ result?: string;
54
+ error?: string;
55
+ }
56
+
57
+ function getLastAssistantMessage(
58
+ messages: AgentMessage[],
59
+ ): AssistantMessage | undefined {
60
+ for (let i = messages.length - 1; i >= 0; i--) {
61
+ const msg = messages[i];
62
+ if (msg.role === "assistant") {
63
+ return msg as AssistantMessage;
64
+ }
65
+ }
66
+ return undefined;
67
+ }
68
+
69
+ function getAssistantText(
70
+ message: AssistantMessage | undefined,
71
+ ): string | undefined {
72
+ if (!message) return undefined;
73
+
74
+ const texts: string[] = [];
75
+ for (const part of message.content) {
76
+ if (part.type === "text") {
77
+ texts.push(part.text);
78
+ }
79
+ }
80
+
81
+ return texts.length > 0 ? texts.join("\n") : undefined;
82
+ }
83
+
84
+ function getPromptOutcome(state: SubagentState): PromptOutcome {
85
+ const lastAssistant = getLastAssistantMessage(state.session!.messages);
86
+ const text = getAssistantText(lastAssistant);
87
+
88
+ if (lastAssistant?.stopReason === "error") {
89
+ return {
90
+ status: "error",
91
+ error: lastAssistant.errorMessage ?? text ?? "(no output)",
92
+ };
93
+ }
94
+
95
+ if (lastAssistant?.stopReason === "aborted") {
96
+ return {
97
+ status: "aborted",
98
+ error: lastAssistant.errorMessage ?? text ?? "(no output)",
99
+ };
100
+ }
101
+
102
+ return {
103
+ status: state.agentConfig.interactive ? "waiting" : "done",
104
+ result: text ?? "(no output)",
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Process-level singleton that owns all durable subagent state.
110
+ *
111
+ * This survives extension instance replacement caused by runtime
112
+ * teardown/recreation on /resume, /new, /fork (pi 0.65.0+).
113
+ * Each new extension instance rebinds delivery and widget hooks
114
+ * via activateSession/deactivateSession.
115
+ */
116
+ class CrewRuntime {
117
+ private readonly registry = new SubagentRegistry();
118
+ private readonly delivery = new DeliveryCoordinator();
119
+
120
+ // Per-session refresh callbacks, keyed by ownerSessionId
121
+ private readonly refreshCallbacks = new Map<string, () => void>();
122
+
123
+ private refreshWidgetFor(sessionId: string): void {
124
+ this.refreshCallbacks.get(sessionId)?.();
125
+ }
126
+
127
+ activateSession(
128
+ binding: ActiveRuntimeBinding,
129
+ refreshWidget?: () => void,
130
+ ): void {
131
+ if (refreshWidget) {
132
+ this.refreshCallbacks.set(binding.sessionId, refreshWidget);
133
+ }
134
+ this.delivery.activateSession(
135
+ binding,
136
+ (ownerSessionId, excludeId) =>
137
+ this.registry.countRunningForOwner(ownerSessionId, excludeId),
138
+ );
139
+ refreshWidget?.();
140
+ }
141
+
142
+ deactivateSession(sessionId: string): void {
143
+ this.delivery.deactivateSession(sessionId);
144
+ this.refreshCallbacks.delete(sessionId);
145
+ }
146
+
147
+ spawn(
148
+ agentConfig: AgentConfig,
149
+ task: string,
150
+ cwd: string,
151
+ ownerSessionId: string,
152
+ ctx: SpawnContext,
153
+ extensionResolvedPath: string,
154
+ ): string {
155
+ const state = this.registry.create(agentConfig, task, ownerSessionId);
156
+ this.refreshWidgetFor(ownerSessionId);
157
+ void this.spawnSession(
158
+ state,
159
+ cwd,
160
+ ctx,
161
+ extensionResolvedPath,
162
+ );
163
+ return state.id;
164
+ }
165
+
166
+ private attachSessionListeners(
167
+ state: SubagentState,
168
+ session: AgentSession,
169
+ ): void {
170
+ state.unsubscribe = session.subscribe((event) => {
171
+ if (event.type !== "turn_end") return;
172
+
173
+ state.turns++;
174
+ const msg = event.message;
175
+ if (msg.role === "assistant") {
176
+ const assistantMsg = msg as AssistantMessage;
177
+ state.contextTokens = assistantMsg.usage.totalTokens;
178
+ state.model = assistantMsg.model;
179
+ }
180
+ this.refreshWidgetFor(state.ownerSessionId);
181
+ });
182
+ }
183
+
184
+ private attachSpawnedSession(
185
+ state: SubagentState,
186
+ session: AgentSession,
187
+ ): boolean {
188
+ if (!this.registry.hasState(state)) {
189
+ session.dispose();
190
+ return false;
191
+ }
192
+
193
+ state.session = session;
194
+ return true;
195
+ }
196
+
197
+ private settleAgent(
198
+ state: SubagentState,
199
+ nextStatus: SubagentStatus,
200
+ opts: { result?: string; error?: string },
201
+ ): void {
202
+ state.status = nextStatus;
203
+ state.result = opts.result;
204
+ state.error = opts.error;
205
+
206
+ this.delivery.deliver(
207
+ state.ownerSessionId,
208
+ {
209
+ id: state.id,
210
+ agentName: state.agentConfig.name,
211
+ sessionFile: state.session?.sessionFile,
212
+ status: state.status,
213
+ result: state.result,
214
+ error: state.error,
215
+ },
216
+ (ownerSessionId, excludeId) =>
217
+ this.registry.countRunningForOwner(ownerSessionId, excludeId),
218
+ );
219
+
220
+ if (state.status !== "waiting") {
221
+ this.disposeAgent(state);
222
+ } else {
223
+ this.refreshWidgetFor(state.ownerSessionId);
224
+ }
225
+ }
226
+
227
+ private disposeAgent(state: SubagentState): void {
228
+ state.unsubscribe?.();
229
+ state.promptAbortController = undefined;
230
+ state.session?.dispose();
231
+ this.registry.delete(state.id);
232
+ this.refreshWidgetFor(state.ownerSessionId);
233
+ }
234
+
235
+ private async runPromptCycle(
236
+ state: SubagentState,
237
+ prompt: string,
238
+ ): Promise<void> {
239
+ if (isAborted(state)) return;
240
+
241
+ const abortController = new AbortController();
242
+ state.promptAbortController = abortController;
243
+
244
+ try {
245
+ const recovery = await runPromptWithOverflowRecovery(
246
+ state.session!,
247
+ prompt,
248
+ abortController.signal,
249
+ );
250
+ if (isAborted(state)) return;
251
+
252
+ const outcome = getPromptOutcome(state);
253
+
254
+ if (recovery === "failed" && outcome.status !== "error") {
255
+ this.settleAgent(state, "error", {
256
+ error: "Context overflow recovery failed",
257
+ });
258
+ return;
259
+ }
260
+
261
+ this.settleAgent(state, outcome.status, outcome);
262
+ } catch (err) {
263
+ if (isAborted(state)) return;
264
+
265
+ const error = err instanceof Error ? err.message : String(err);
266
+ this.settleAgent(state, "error", { error });
267
+ } finally {
268
+ state.promptAbortController = undefined;
269
+ }
270
+ }
271
+
272
+ private async spawnSession(
273
+ state: SubagentState,
274
+ cwd: string,
275
+ ctx: SpawnContext,
276
+ extensionResolvedPath: string,
277
+ ): Promise<void> {
278
+ try {
279
+ if (isAborted(state)) return;
280
+
281
+ const { session, warnings } = await bootstrapSession({
282
+ agentConfig: state.agentConfig,
283
+ cwd,
284
+ ctx: toBootstrapContext(ctx),
285
+ extensionResolvedPath,
286
+ });
287
+
288
+ // Emit bootstrap warnings to UI
289
+ for (const warning of warnings) {
290
+ ctx.onWarning?.(warning);
291
+ }
292
+
293
+ if (!this.attachSpawnedSession(state, session)) return;
294
+
295
+ this.attachSessionListeners(state, session);
296
+ await this.runPromptCycle(state, state.task);
297
+ } catch (err) {
298
+ if (isAborted(state)) return;
299
+
300
+ if (state.status === "running") {
301
+ const error = err instanceof Error ? err.message : String(err);
302
+ this.settleAgent(state, "error", { error });
303
+ }
304
+ }
305
+ }
306
+
307
+ respond(
308
+ id: string,
309
+ message: string,
310
+ callerSessionId: string,
311
+ ): { error?: string } {
312
+ const state = this.registry.get(id);
313
+ if (!state) return { error: `No subagent with id "${id}"` };
314
+ if (state.ownerSessionId !== callerSessionId) {
315
+ return { error: `Subagent "${id}" belongs to a different session` };
316
+ }
317
+ if (state.status !== "waiting") {
318
+ return {
319
+ error: `Subagent "${id}" is not waiting for a response (status: ${state.status})`,
320
+ };
321
+ }
322
+ if (!state.session)
323
+ return { error: `Subagent "${id}" has no active session` };
324
+
325
+ state.status = "running";
326
+ this.refreshWidgetFor(state.ownerSessionId);
327
+ void this.runPromptCycle(state, message);
328
+ return {};
329
+ }
330
+
331
+ done(id: string, callerSessionId: string): { error?: string } {
332
+ const state = this.registry.get(id);
333
+ if (!state) return { error: `No active subagent with id "${id}"` };
334
+ if (state.ownerSessionId !== callerSessionId) {
335
+ return { error: `Subagent "${id}" belongs to a different session` };
336
+ }
337
+ if (state.status !== "waiting") {
338
+ return { error: `Subagent "${id}" is not in waiting state` };
339
+ }
340
+
341
+ this.disposeAgent(state);
342
+ return {};
343
+ }
344
+
345
+ abort(id: string, opts: AbortOptions): boolean {
346
+ const state = this.registry.get(id);
347
+ if (!state || !isAbortableStatus(state.status)) return false;
348
+
349
+ state.promptAbortController?.abort();
350
+ state.promptAbortController = undefined;
351
+ state.session?.abortCompaction();
352
+ state.session?.abortRetry();
353
+ state.session?.abort().catch(() => {});
354
+ this.settleAgent(state, "aborted", { error: opts.reason });
355
+ return true;
356
+ }
357
+
358
+ abortOwned(
359
+ ids: string[],
360
+ callerSessionId: string,
361
+ opts: AbortOptions,
362
+ ): AbortOwnedResult {
363
+ const uniqueIds = Array.from(
364
+ new Set(ids.map((id) => id.trim()).filter(Boolean)),
365
+ );
366
+ const result: AbortOwnedResult = {
367
+ abortedIds: [],
368
+ missingIds: [],
369
+ foreignIds: [],
370
+ };
371
+
372
+ for (const id of uniqueIds) {
373
+ const state = this.registry.get(id);
374
+ if (!state || !isAbortableStatus(state.status)) {
375
+ result.missingIds.push(id);
376
+ continue;
377
+ }
378
+ if (state.ownerSessionId !== callerSessionId) {
379
+ result.foreignIds.push(id);
380
+ continue;
381
+ }
382
+ if (this.abort(id, opts)) {
383
+ result.abortedIds.push(id);
384
+ } else {
385
+ result.missingIds.push(id);
386
+ }
387
+ }
388
+
389
+ return result;
390
+ }
391
+
392
+ abortAllOwned(
393
+ callerSessionId: string,
394
+ opts: AbortOptions,
395
+ ): string[] {
396
+ const ids = this.registry.getOwnedAbortableIds(callerSessionId);
397
+
398
+ for (const id of ids) {
399
+ this.abort(id, opts);
400
+ }
401
+
402
+ return ids;
403
+ }
404
+
405
+ /**
406
+ * Abort all running subagents during shutdown cleanup.
407
+ * Called from SIGINT, session_shutdown(reason="quit"), and beforeExit fallback paths.
408
+ */
409
+ abortAll(): void {
410
+ const allAgents = this.registry.getAllRunning();
411
+ for (const state of allAgents) {
412
+ this.abort(state.id, { reason: "Aborted during shutdown" });
413
+ }
414
+ }
415
+
416
+ getAbortableAgents(): AbortableAgentSummary[] {
417
+ return this.registry.getAbortableAgents();
418
+ }
419
+
420
+ getActiveSummariesForOwner(ownerSessionId: string): ActiveAgentSummary[] {
421
+ return this.registry.getActiveSummariesForOwner(ownerSessionId);
422
+ }
423
+ }
424
+
425
+ export const crewRuntime = new CrewRuntime();
426
+ export type { CrewRuntime };