@melihmucuk/pi-crew 1.0.17 → 1.0.19

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 (35) hide show
  1. package/agents/code-reviewer.md +16 -11
  2. package/agents/quality-reviewer.md +8 -17
  3. package/extension/catalog.ts +543 -0
  4. package/extension/crew.ts +377 -0
  5. package/extension/index.ts +35 -18
  6. package/extension/subagent-session.ts +257 -0
  7. package/extension/tools.ts +323 -0
  8. package/extension/ui.ts +291 -0
  9. package/package.json +6 -6
  10. package/prompts/pi-crew-review.md +25 -16
  11. package/skills/pi-crew/SKILL.md +3 -1
  12. package/extension/agent-catalog.ts +0 -369
  13. package/extension/agent-config-fields.ts +0 -359
  14. package/extension/agent-discovery.ts +0 -123
  15. package/extension/bootstrap-session.ts +0 -131
  16. package/extension/integration/crew-tool-actions.ts +0 -306
  17. package/extension/integration/crew-tool-executor.ts +0 -109
  18. package/extension/integration/register-renderers.ts +0 -77
  19. package/extension/integration/register-tools.ts +0 -47
  20. package/extension/integration/tool-presentation.ts +0 -30
  21. package/extension/integration/tools/crew-abort.ts +0 -56
  22. package/extension/integration/tools/crew-done.ts +0 -27
  23. package/extension/integration/tools/crew-list.ts +0 -36
  24. package/extension/integration/tools/crew-respond.ts +0 -38
  25. package/extension/integration/tools/crew-spawn.ts +0 -46
  26. package/extension/message-delivery-policy.ts +0 -22
  27. package/extension/runtime/crew-runtime.ts +0 -263
  28. package/extension/runtime/overflow-recovery.ts +0 -211
  29. package/extension/runtime/owner-session-coordinator.ts +0 -138
  30. package/extension/runtime/subagent-lifecycle.ts +0 -203
  31. package/extension/runtime/subagent-registry.ts +0 -122
  32. package/extension/runtime/subagent-transitions.ts +0 -100
  33. package/extension/status-widget.ts +0 -107
  34. package/extension/subagent-messages.ts +0 -116
  35. package/extension/tool-registry.ts +0 -19
@@ -0,0 +1,257 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
3
+ import {
4
+ type AgentSession,
5
+ createAgentSession,
6
+ DefaultResourceLoader,
7
+ type ModelRegistry,
8
+ SessionManager,
9
+ SettingsManager,
10
+ } from "@earendil-works/pi-coding-agent";
11
+ import type { AgentConfig } from "./catalog.js";
12
+ import { SUPPORTED_TOOL_NAMES, type SupportedToolName } from "./catalog.js";
13
+ import type { SubagentState } from "./crew.js";
14
+ import type { SubagentStatus } from "./ui.js";
15
+
16
+ export interface BootstrapContext {
17
+ model: Model<Api> | undefined;
18
+ modelRegistry: ModelRegistry;
19
+ agentDir: string;
20
+ parentSessionFile?: string;
21
+ }
22
+
23
+ interface BootstrapOptions {
24
+ agentConfig: AgentConfig;
25
+ cwd: string;
26
+ ctx: BootstrapContext;
27
+ extensionResolvedPath: string;
28
+ }
29
+
30
+ interface BootstrapResult {
31
+ session: AgentSession;
32
+ warnings: string[];
33
+ }
34
+
35
+ interface PromptOutcome {
36
+ status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">;
37
+ result?: string;
38
+ error?: string;
39
+ }
40
+
41
+ interface StartOptions {
42
+ cwd: string;
43
+ ctx: BootstrapContext;
44
+ extensionResolvedPath: string;
45
+ onWarning?: (message: string) => void;
46
+ }
47
+
48
+ export interface SubagentRunnerCallbacks {
49
+ isCurrent: (state: SubagentState) => boolean;
50
+ onProgress: (ownerSessionId: string) => void;
51
+ onSettled: (
52
+ state: SubagentState,
53
+ status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">,
54
+ outcome: { result?: string; error?: string },
55
+ ) => void;
56
+ }
57
+
58
+ export interface SubagentRunner {
59
+ start(state: SubagentState, opts: StartOptions): void;
60
+ respond(state: SubagentState, message: string): void;
61
+ abort(state: SubagentState): void;
62
+ }
63
+
64
+ function resolveTools(agentConfig: AgentConfig): SupportedToolName[] {
65
+ return [...(agentConfig.tools ?? SUPPORTED_TOOL_NAMES)];
66
+ }
67
+
68
+ function resolveModel(agentConfig: AgentConfig, ctx: BootstrapContext): { model: Model<Api> | undefined; warnings: string[] } {
69
+ const warnings: string[] = [];
70
+ const model = ctx.model;
71
+ if (!agentConfig.parsedModel) return { model, warnings };
72
+
73
+ const found = ctx.modelRegistry.find(agentConfig.parsedModel.provider, agentConfig.parsedModel.modelId);
74
+ if (found) return { model: found, warnings };
75
+
76
+ warnings.push(`Model "${agentConfig.model}" not found, using current session model`);
77
+ return { model, warnings };
78
+ }
79
+
80
+ function getSkillWarnings(agentConfig: AgentConfig, resourceLoader: DefaultResourceLoader): string[] {
81
+ const warnings: string[] = [];
82
+ if (!agentConfig.skills) return warnings;
83
+
84
+ const availableSkillNames = new Set(resourceLoader.getSkills().skills.map((skill) => skill.name));
85
+ for (const skillName of agentConfig.skills) {
86
+ if (!availableSkillNames.has(skillName)) {
87
+ warnings.push(`Unknown skill "${skillName}" in subagent config, skipping`);
88
+ }
89
+ }
90
+ return warnings;
91
+ }
92
+
93
+ async function bootstrapSession(opts: BootstrapOptions): Promise<BootstrapResult> {
94
+ const warnings: string[] = [];
95
+ const { agentConfig, cwd, ctx, extensionResolvedPath } = opts;
96
+
97
+ const authStorage = ctx.modelRegistry.authStorage;
98
+ const modelRegistry = ctx.modelRegistry;
99
+ const { model, warnings: modelWarnings } = resolveModel(agentConfig, ctx);
100
+ warnings.push(...modelWarnings);
101
+ const tools = resolveTools(agentConfig);
102
+
103
+ const resourceLoader = new DefaultResourceLoader({
104
+ cwd,
105
+ agentDir: ctx.agentDir,
106
+ extensionsOverride: (base) => ({
107
+ ...base,
108
+ extensions: base.extensions.filter((ext) => !ext.resolvedPath.startsWith(extensionResolvedPath)),
109
+ }),
110
+ skillsOverride: agentConfig.skills
111
+ ? (base) => ({
112
+ skills: base.skills.filter((skill) => agentConfig.skills!.includes(skill.name)),
113
+ diagnostics: base.diagnostics,
114
+ })
115
+ : undefined,
116
+ appendSystemPromptOverride: (base) => agentConfig.systemPrompt.trim() ? [...base, agentConfig.systemPrompt] : base,
117
+ });
118
+ await resourceLoader.reload();
119
+ warnings.push(...getSkillWarnings(agentConfig, resourceLoader));
120
+
121
+ const settingsManager = SettingsManager.inMemory({
122
+ compaction: { enabled: agentConfig.compaction ?? true },
123
+ });
124
+
125
+ const sessionManager = SessionManager.create(cwd);
126
+ sessionManager.newSession({ parentSession: ctx.parentSessionFile });
127
+
128
+ const result = await createAgentSession({
129
+ cwd,
130
+ agentDir: ctx.agentDir,
131
+ model,
132
+ thinkingLevel: agentConfig.thinking,
133
+ tools,
134
+ resourceLoader,
135
+ sessionManager,
136
+ settingsManager,
137
+ authStorage,
138
+ modelRegistry,
139
+ });
140
+
141
+ return { session: result.session, warnings };
142
+ }
143
+
144
+ function getLastAssistantMessage(messages: AgentMessage[]): AssistantMessage | undefined {
145
+ for (let i = messages.length - 1; i >= 0; i--) {
146
+ const msg = messages[i];
147
+ if (msg.role === "assistant") return msg as AssistantMessage;
148
+ }
149
+ return undefined;
150
+ }
151
+
152
+ function getAssistantText(message: AssistantMessage | undefined): string | undefined {
153
+ if (!message) return undefined;
154
+ const texts: string[] = [];
155
+ for (const part of message.content) {
156
+ if (part.type === "text") texts.push(part.text);
157
+ }
158
+ return texts.length > 0 ? texts.join("\n") : undefined;
159
+ }
160
+
161
+ function getPromptOutcome(state: SubagentState): PromptOutcome {
162
+ const lastAssistant = getLastAssistantMessage(state.session!.messages);
163
+ const text = getAssistantText(lastAssistant);
164
+
165
+ if (lastAssistant?.stopReason === "error") {
166
+ return { status: "error", error: lastAssistant.errorMessage ?? text ?? "(no output)" };
167
+ }
168
+ if (lastAssistant?.stopReason === "aborted") {
169
+ return { status: "aborted", error: lastAssistant.errorMessage ?? text ?? "(no output)" };
170
+ }
171
+ return { status: state.agentConfig.interactive ? "waiting" : "done", result: text ?? "(no output)" };
172
+ }
173
+
174
+ function isAborted(state: SubagentState): boolean {
175
+ return state.status === "aborted";
176
+ }
177
+
178
+ export class SubagentSessionRunner implements SubagentRunner {
179
+ constructor(private readonly callbacks: SubagentRunnerCallbacks) {}
180
+
181
+ start(state: SubagentState, opts: StartOptions): void {
182
+ void this.spawnSession(state, opts);
183
+ }
184
+
185
+ respond(state: SubagentState, message: string): void {
186
+ void this.runPromptCycle(state, message);
187
+ }
188
+
189
+ abort(state: SubagentState): void {
190
+ state.session?.abortCompaction();
191
+ state.session?.abort().catch(() => {});
192
+ }
193
+
194
+ private attachSessionListeners(state: SubagentState, session: AgentSession): void {
195
+ state.unsubscribe = session.subscribe((event) => {
196
+ if (event.type !== "turn_end") return;
197
+ state.turns++;
198
+ const msg = event.message;
199
+ if (msg.role === "assistant") {
200
+ const assistantMsg = msg as AssistantMessage;
201
+ state.contextTokens = assistantMsg.usage.totalTokens;
202
+ state.model = assistantMsg.model;
203
+ }
204
+ this.callbacks.onProgress(state.ownerSessionId);
205
+ });
206
+ }
207
+
208
+ private attachSpawnedSession(state: SubagentState, session: AgentSession): boolean {
209
+ if (!this.callbacks.isCurrent(state)) {
210
+ session.dispose();
211
+ return false;
212
+ }
213
+ state.session = session;
214
+ return true;
215
+ }
216
+
217
+ private async runPromptCycle(state: SubagentState, prompt: string): Promise<void> {
218
+ if (isAborted(state)) return;
219
+
220
+ try {
221
+ await state.session!.prompt(prompt);
222
+ if (isAborted(state)) return;
223
+
224
+ const outcome = getPromptOutcome(state);
225
+ this.callbacks.onSettled(state, outcome.status, outcome);
226
+ } catch (err) {
227
+ if (isAborted(state)) return;
228
+ const error = err instanceof Error ? err.message : String(err);
229
+ this.callbacks.onSettled(state, "error", { error });
230
+ }
231
+ }
232
+
233
+ private async spawnSession(state: SubagentState, opts: StartOptions): Promise<void> {
234
+ try {
235
+ if (isAborted(state)) return;
236
+
237
+ const { session, warnings } = await bootstrapSession({
238
+ agentConfig: state.agentConfig,
239
+ cwd: opts.cwd,
240
+ ctx: opts.ctx,
241
+ extensionResolvedPath: opts.extensionResolvedPath,
242
+ });
243
+
244
+ for (const warning of warnings) opts.onWarning?.(warning);
245
+ if (!this.attachSpawnedSession(state, session)) return;
246
+
247
+ this.attachSessionListeners(state, session);
248
+ await this.runPromptCycle(state, state.task);
249
+ } catch (err) {
250
+ if (isAborted(state)) return;
251
+ if (state.status === "running") {
252
+ const error = err instanceof Error ? err.message : String(err);
253
+ this.callbacks.onSettled(state, "error", { error });
254
+ }
255
+ }
256
+ }
257
+ }
@@ -0,0 +1,323 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { Type } from "typebox";
5
+ import {
6
+ discoverAgents,
7
+ type AgentConfig,
8
+ type AgentDiscoveryWarning,
9
+ } from "./catalog.js";
10
+ import type { AbortOwnedResult, ActiveAgentSummary, CrewRuntime } from "./crew.js";
11
+ import { STATUS_ICON, renderCrewCall, renderCrewResult, sendCrewListActiveWarning } from "./ui.js";
12
+
13
+ export type CrewToolResult = AgentToolResult<unknown> & {
14
+ isError?: boolean;
15
+ terminate?: boolean;
16
+ };
17
+
18
+ type RegisteredTool = Parameters<ExtensionAPI["registerTool"]>[0];
19
+ type ToolRenderCall = Exclude<RegisteredTool["renderCall"], undefined>;
20
+
21
+ interface ToolContext {
22
+ cwd: string;
23
+ callerSessionId: string;
24
+ }
25
+
26
+ function getToolContext(ctx: ExtensionContext): ToolContext {
27
+ return {
28
+ cwd: ctx.cwd,
29
+ callerSessionId: ctx.sessionManager.getSessionId(),
30
+ };
31
+ }
32
+
33
+ function toolError(text: string): CrewToolResult {
34
+ return {
35
+ content: [{ type: "text", text }],
36
+ isError: true,
37
+ details: { error: true },
38
+ };
39
+ }
40
+
41
+ function toolSuccess(
42
+ text: string,
43
+ details: Record<string, unknown> = {},
44
+ options: { terminate?: boolean } = {},
45
+ ): CrewToolResult {
46
+ return {
47
+ content: [{ type: "text", text }],
48
+ details,
49
+ ...(options.terminate ? { terminate: true } : {}),
50
+ };
51
+ }
52
+
53
+ function formatAvailableAgents(agents: AgentConfig[]): string[] {
54
+ if (agents.length === 0) {
55
+ return ["No valid subagent definitions found. Add `.md` files to `<cwd>/.pi/agents/` or `~/.pi/agent/agents/`."];
56
+ }
57
+
58
+ return agents.flatMap((agent) => [
59
+ "",
60
+ `name: ${agent.name}`,
61
+ `description: ${agent.description}`,
62
+ `interactive: ${agent.interactive ? "true" : "false"}`,
63
+ ]);
64
+ }
65
+
66
+ function formatWarnings(warnings: AgentDiscoveryWarning[]): string[] {
67
+ if (warnings.length === 0) return [];
68
+ return [
69
+ "",
70
+ "## Ignored subagent definitions",
71
+ ...warnings.map((warning) => `- ${warning.message} (${warning.filePath})`),
72
+ ];
73
+ }
74
+
75
+ function formatActiveAgents(running: ActiveAgentSummary[]): string[] {
76
+ if (running.length === 0) return ["No subagents currently active."];
77
+ return running.flatMap((agent) => {
78
+ const icon = STATUS_ICON[agent.status] ?? "❓";
79
+ return ["", `id: ${agent.id}`, `name: ${agent.agentName}`, `status: ${icon} ${agent.status}`];
80
+ });
81
+ }
82
+
83
+ function formatAbortToolMessage(result: AbortOwnedResult): string {
84
+ const parts: string[] = [];
85
+ if (result.abortedIds.length > 0) parts.push(`Aborted ${result.abortedIds.length} subagent(s): ${result.abortedIds.join(", ")}`);
86
+ if (result.missingIds.length > 0) parts.push(`Not found or already finished: ${result.missingIds.join(", ")}`);
87
+ if (result.foreignIds.length > 0) parts.push(`Belong to a different session: ${result.foreignIds.join(", ")}`);
88
+ return parts.join("\n");
89
+ }
90
+
91
+ function notifyDiscoveryWarnings(
92
+ ctx: ExtensionContext,
93
+ shownDiscoveryWarnings: Set<string>,
94
+ warnings: AgentDiscoveryWarning[],
95
+ ): void {
96
+ if (!ctx.hasUI) return;
97
+ for (const warning of warnings) {
98
+ const key = `${warning.filePath}:${warning.message}`;
99
+ if (shownDiscoveryWarnings.has(key)) continue;
100
+ shownDiscoveryWarnings.add(key);
101
+ ctx.ui.notify(`${warning.message} (${warning.filePath})`, "error");
102
+ }
103
+ }
104
+
105
+ function showActiveListWarning(pi: ExtensionAPI, ctx: ExtensionContext): void {
106
+ Promise.resolve().then(() => {
107
+ sendCrewListActiveWarning(pi.sendMessage.bind(pi), {
108
+ isIdle: ctx.isIdle(),
109
+ triggerTurn: true,
110
+ });
111
+ });
112
+ }
113
+
114
+ function registerActionTool<Params extends object>(
115
+ pi: ExtensionAPI,
116
+ options: Omit<RegisteredTool, "execute" | "renderResult" | "renderCall"> & {
117
+ action: (params: Params, ctx: ExtensionContext) => CrewToolResult;
118
+ renderCall?: (
119
+ args: Partial<Params>,
120
+ theme: Parameters<ToolRenderCall>[1],
121
+ context: Parameters<ToolRenderCall>[2],
122
+ ) => ReturnType<ToolRenderCall>;
123
+ },
124
+ ): void {
125
+ const { action, renderCall, ...tool } = options;
126
+ pi.registerTool({
127
+ ...tool,
128
+ ...(renderCall ? { renderCall: (args, theme, context) => renderCall(args as Partial<Params>, theme, context) } : {}),
129
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
130
+ return action(params as Params, ctx);
131
+ },
132
+ renderResult(result, _options, theme, _context) {
133
+ return renderCrewResult(result, theme);
134
+ },
135
+ });
136
+ }
137
+
138
+ export function registerCrewTools(pi: ExtensionAPI, crew: CrewRuntime, extensionDir: string): void {
139
+ const shownDiscoveryWarnings = new Set<string>();
140
+
141
+ pi.registerTool({
142
+ name: "crew_list",
143
+ label: "List Crew",
144
+ description:
145
+ "List available subagent definitions and currently running subagents with their status. Use only to discover which subagents exist or to get a one-time status snapshot. Do NOT call this repeatedly to check if a subagent has finished — results are delivered automatically as steering messages.",
146
+ parameters: Type.Object({}),
147
+ promptSnippet: "List subagent definitions and active subagents",
148
+ promptGuidelines: [
149
+ "crew_list: List available subagents and active subagents owned by this session.",
150
+ "crew_list: Use before crew_spawn to discover names, descriptions, and interactive status.",
151
+ "crew_list: Use only for discovery or a requested status snapshot; do not poll for completion.",
152
+ ],
153
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
154
+ const toolCtx = getToolContext(ctx);
155
+ const { agents, warnings } = discoverAgents(toolCtx.cwd);
156
+ const running = crew.getActiveSummariesForOwner(toolCtx.callerSessionId);
157
+ const lines = [
158
+ "## Available Subagents",
159
+ ...formatAvailableAgents(agents),
160
+ ...formatWarnings(warnings),
161
+ "",
162
+ "## Active Subagents",
163
+ ...formatActiveAgents(running),
164
+ ];
165
+ notifyDiscoveryWarnings(ctx, shownDiscoveryWarnings, warnings);
166
+ if (running.length > 0) showActiveListWarning(pi, ctx);
167
+ return { content: [{ type: "text", text: lines.join("\n") }], details: {} };
168
+ },
169
+ renderCall(_args, theme, _context) {
170
+ return new Text(theme.fg("toolTitle", theme.bold("crew_list")), 0, 0);
171
+ },
172
+ renderResult(result, _options, _theme, _context) {
173
+ const text = result.content[0];
174
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
175
+ },
176
+ });
177
+
178
+ registerActionTool<{ subagent: string; task: string }>(pi, {
179
+ name: "crew_spawn",
180
+ label: "Spawn Crew",
181
+ description:
182
+ "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.",
183
+ parameters: Type.Object({
184
+ subagent: Type.String({ description: "Subagent name from crew_list" }),
185
+ task: Type.String({ description: "Task to delegate to the subagent" }),
186
+ }),
187
+ promptSnippet: "Spawn a non-blocking subagent. Use crew_list first to see available subagents.",
188
+ promptGuidelines: [
189
+ "crew_spawn: Spawn a discovered subagent for one clearly delegated, self-contained task.",
190
+ "crew_spawn: Include only needed context: constraints, relevant files, acceptance criteria, and expected output.",
191
+ "crew_spawn: After spawning, ownership transfers to the subagent; do not work on that task yourself.",
192
+ "crew_spawn: Results arrive as steering messages; do not poll crew_list or fabricate results.",
193
+ "crew_spawn: Use the bundled pi-crew skill for detailed delegation patterns.",
194
+ ],
195
+ action: (params, ctx) => {
196
+ const toolCtx = getToolContext(ctx);
197
+ const { agents, warnings } = discoverAgents(toolCtx.cwd);
198
+ notifyDiscoveryWarnings(ctx, shownDiscoveryWarnings, warnings);
199
+ const subagent = agents.find((candidate) => candidate.name === params.subagent);
200
+ if (!subagent) {
201
+ const available = agents.map((candidate) => candidate.name).join(", ") || "none";
202
+ return toolError(`Unknown subagent: "${params.subagent}". Available: ${available}`);
203
+ }
204
+
205
+ const id = crew.spawn(
206
+ subagent,
207
+ params.task,
208
+ toolCtx.cwd,
209
+ toolCtx.callerSessionId,
210
+ {
211
+ model: ctx.model,
212
+ modelRegistry: ctx.modelRegistry,
213
+ agentDir: getAgentDir(),
214
+ parentSessionFile: ctx.sessionManager.getSessionFile(),
215
+ onWarning: (msg) => ctx.ui.notify(msg, "warning"),
216
+ },
217
+ extensionDir,
218
+ );
219
+ return toolSuccess(
220
+ `Subagent '${subagent.name}' spawned as ${id}. Result will be delivered as a steering message when done.`,
221
+ { id, agentName: subagent.name, task: params.task },
222
+ );
223
+ },
224
+ renderCall(args, theme, _context) {
225
+ return renderCrewCall(theme, "crew_spawn", args.subagent || "...", args.task);
226
+ },
227
+ });
228
+
229
+ registerActionTool<{ subagent_id?: string; subagent_ids?: string[]; all?: boolean }>(pi, {
230
+ name: "crew_abort",
231
+ label: "Abort Crew",
232
+ description: "Abort one, many, or all active subagents owned by the current session.",
233
+ parameters: Type.Object({
234
+ subagent_id: Type.Optional(Type.String({ description: "Single subagent ID to abort" })),
235
+ subagent_ids: Type.Optional(Type.Array(Type.String(), { minItems: 1, description: "Multiple subagent IDs to abort" })),
236
+ all: Type.Optional(Type.Boolean({ description: "Abort all active subagents owned by the current session" })),
237
+ }),
238
+ promptSnippet: "Abort one, many, or all active subagents from this session.",
239
+ promptGuidelines: [
240
+ "crew_abort: Abort one, many, or all active subagents owned by this session.",
241
+ "crew_abort: Provide exactly one mode: subagent_id, subagent_ids, or all=true.",
242
+ "crew_abort: Use only when delegated work is obsolete, wrong, or explicitly cancelled.",
243
+ ],
244
+ action: (params, ctx) => {
245
+ const { callerSessionId } = getToolContext(ctx);
246
+ const modeCount = Number(Boolean(params.subagent_id)) + Number(Boolean(params.subagent_ids?.length)) + Number(params.all === true);
247
+ if (modeCount !== 1) return toolError("Provide exactly one of: subagent_id, subagent_ids, or all=true.");
248
+
249
+ if (params.all) {
250
+ const abortedIds = crew.abortAllOwned(callerSessionId, { reason: "Aborted by tool request" });
251
+ if (abortedIds.length === 0) return toolError("No active subagents in the current session.");
252
+ return toolSuccess(`Aborted ${abortedIds.length} subagent(s): ${abortedIds.join(", ")}`, { ids: abortedIds }, { terminate: true });
253
+ }
254
+
255
+ const ids = params.subagent_id ? [params.subagent_id] : (params.subagent_ids ?? []);
256
+ const result = crew.abortOwned(ids, callerSessionId, { reason: "Aborted by tool request" });
257
+ const message = formatAbortToolMessage(result);
258
+ if (result.abortedIds.length === 0) return toolError(message || "No subagents were aborted.");
259
+ return toolSuccess(
260
+ message,
261
+ { ids: result.abortedIds, missing_ids: result.missingIds, foreign_ids: result.foreignIds },
262
+ { terminate: true },
263
+ );
264
+ },
265
+ renderCall(args, theme, _context) {
266
+ if (args.all) return renderCrewCall(theme, "crew_abort", "all");
267
+ if (args.subagent_id) return renderCrewCall(theme, "crew_abort", args.subagent_id);
268
+ const count = Array.isArray(args.subagent_ids) ? args.subagent_ids.length : 0;
269
+ return renderCrewCall(theme, "crew_abort", `${count} ids`);
270
+ },
271
+ });
272
+
273
+ registerActionTool<{ subagent_id: string; message: string }>(pi, {
274
+ name: "crew_respond",
275
+ label: "Respond to Crew",
276
+ description: "Send a follow-up message to an interactive subagent that is waiting for a response.",
277
+ parameters: Type.Object({
278
+ subagent_id: Type.String({ description: "ID of the waiting subagent (from crew_list or crew_spawn result)" }),
279
+ message: Type.String({ description: "Message to send to the subagent" }),
280
+ }),
281
+ promptSnippet: "Send a follow-up message to a waiting interactive subagent.",
282
+ promptGuidelines: [
283
+ "crew_respond: Send a complete follow-up message to a waiting interactive subagent.",
284
+ "crew_respond: Use the waiting subagent ID from crew_spawn results or crew_list.",
285
+ "crew_respond: The response arrives as a steering message; do not poll crew_list.",
286
+ ],
287
+ action: (params, ctx) => {
288
+ const { callerSessionId } = getToolContext(ctx);
289
+ const { error } = crew.respond(params.subagent_id, params.message, callerSessionId);
290
+ if (error) return toolError(error);
291
+ return toolSuccess(
292
+ `Message sent to subagent ${params.subagent_id}. Response will be delivered as a steering message.`,
293
+ { id: params.subagent_id, message: params.message },
294
+ );
295
+ },
296
+ renderCall(args, theme, _context) {
297
+ return renderCrewCall(theme, "crew_respond", args.subagent_id || "...", args.message);
298
+ },
299
+ });
300
+
301
+ registerActionTool<{ subagent_id: string }>(pi, {
302
+ name: "crew_done",
303
+ label: "Done with Crew",
304
+ description: "Close an interactive subagent session. Use when you no longer need to interact with the subagent.",
305
+ parameters: Type.Object({
306
+ subagent_id: Type.String({ description: "ID of the subagent to close" }),
307
+ }),
308
+ promptSnippet: "Close an interactive subagent session when done.",
309
+ promptGuidelines: [
310
+ "crew_done: Close a waiting interactive subagent owned by this session.",
311
+ "crew_done: Use only when no further follow-up is needed; otherwise use crew_respond.",
312
+ ],
313
+ action: (params, ctx) => {
314
+ const { callerSessionId } = getToolContext(ctx);
315
+ const { error } = crew.done(params.subagent_id, callerSessionId);
316
+ if (error) return toolError(error);
317
+ return toolSuccess(`Subagent ${params.subagent_id} closed.`, { id: params.subagent_id });
318
+ },
319
+ renderCall(args, theme, _context) {
320
+ return renderCrewCall(theme, "crew_done", args.subagent_id || "...");
321
+ },
322
+ });
323
+ }