@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,131 @@
1
+ import {
2
+ type AgentSession,
3
+ createAgentSession,
4
+ DefaultResourceLoader,
5
+ type ModelRegistry,
6
+ SessionManager,
7
+ SettingsManager,
8
+ } from "@mariozechner/pi-coding-agent";
9
+ import type { Api, Model } from "@mariozechner/pi-ai";
10
+ import type { AgentConfig } from "./agent-discovery.js";
11
+ import { SUPPORTED_TOOL_NAMES, type SupportedToolName } from "./tool-registry.js";
12
+
13
+ function resolveTools(agentConfig: AgentConfig): SupportedToolName[] {
14
+ return [...(agentConfig.tools ?? SUPPORTED_TOOL_NAMES)];
15
+ }
16
+
17
+ function resolveModel(agentConfig: AgentConfig, ctx: BootstrapContext): { model: Model<Api> | undefined; warnings: string[] } {
18
+ const warnings: string[] = [];
19
+ const model = ctx.model;
20
+ if (!agentConfig.parsedModel) return { model, warnings };
21
+
22
+ const found = ctx.modelRegistry.find(
23
+ agentConfig.parsedModel.provider,
24
+ agentConfig.parsedModel.modelId,
25
+ );
26
+ if (found) return { model: found, warnings };
27
+
28
+ warnings.push(
29
+ `Model "${agentConfig.model}" not found, using current session model`,
30
+ );
31
+ return { model, warnings };
32
+ }
33
+
34
+ function getSkillWarnings(
35
+ agentConfig: AgentConfig,
36
+ resourceLoader: DefaultResourceLoader,
37
+ ): string[] {
38
+ const warnings: string[] = [];
39
+ if (!agentConfig.skills) return warnings;
40
+
41
+ const availableSkillNames = new Set(
42
+ resourceLoader.getSkills().skills.map((skill) => skill.name),
43
+ );
44
+ for (const skillName of agentConfig.skills) {
45
+ if (!availableSkillNames.has(skillName)) {
46
+ warnings.push(
47
+ `Unknown skill "${skillName}" in subagent config, skipping`,
48
+ );
49
+ }
50
+ }
51
+ return warnings;
52
+ }
53
+
54
+ export interface BootstrapContext {
55
+ model: Model<Api> | undefined;
56
+ modelRegistry: ModelRegistry;
57
+ agentDir: string;
58
+ parentSessionFile?: string;
59
+ }
60
+
61
+ interface BootstrapOptions {
62
+ agentConfig: AgentConfig;
63
+ cwd: string;
64
+ ctx: BootstrapContext;
65
+ extensionResolvedPath: string;
66
+ }
67
+
68
+ export interface BootstrapResult {
69
+ session: AgentSession;
70
+ warnings: string[];
71
+ }
72
+
73
+ export async function bootstrapSession(
74
+ opts: BootstrapOptions,
75
+ ): Promise<BootstrapResult> {
76
+ const warnings: string[] = [];
77
+ const { agentConfig, cwd, ctx, extensionResolvedPath } = opts;
78
+
79
+ const authStorage = ctx.modelRegistry.authStorage;
80
+ const modelRegistry = ctx.modelRegistry;
81
+ const { model, warnings: modelWarnings } = resolveModel(agentConfig, ctx);
82
+ warnings.push(...modelWarnings);
83
+ const tools = resolveTools(agentConfig);
84
+
85
+ const resourceLoader = new DefaultResourceLoader({
86
+ cwd,
87
+ agentDir: ctx.agentDir,
88
+ extensionsOverride: (base) => ({
89
+ ...base,
90
+ extensions: base.extensions.filter(
91
+ (ext) => !ext.resolvedPath.startsWith(extensionResolvedPath),
92
+ ),
93
+ }),
94
+ skillsOverride: agentConfig.skills
95
+ ? (base) => ({
96
+ skills: base.skills.filter((skill) =>
97
+ agentConfig.skills!.includes(skill.name),
98
+ ),
99
+ diagnostics: base.diagnostics,
100
+ })
101
+ : undefined,
102
+ appendSystemPromptOverride: (base) =>
103
+ agentConfig.systemPrompt.trim()
104
+ ? [...base, agentConfig.systemPrompt]
105
+ : base,
106
+ });
107
+ await resourceLoader.reload();
108
+ warnings.push(...getSkillWarnings(agentConfig, resourceLoader));
109
+
110
+ const settingsManager = SettingsManager.inMemory({
111
+ compaction: { enabled: agentConfig.compaction ?? true },
112
+ });
113
+
114
+ const sessionManager = SessionManager.create(cwd);
115
+ sessionManager.newSession({ parentSession: ctx.parentSessionFile });
116
+
117
+ const result = await createAgentSession({
118
+ cwd,
119
+ agentDir: ctx.agentDir,
120
+ model,
121
+ thinkingLevel: agentConfig.thinking,
122
+ tools,
123
+ resourceLoader,
124
+ sessionManager,
125
+ settingsManager,
126
+ authStorage,
127
+ modelRegistry,
128
+ });
129
+
130
+ return { session: result.session, warnings };
131
+ }
@@ -0,0 +1,65 @@
1
+ import { dirname } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
4
+ import {
5
+ type AbortOwnedResult,
6
+ type AbortableAgentSummary,
7
+ type ActiveAgentSummary,
8
+ crewRuntime,
9
+ } from "./runtime/crew-runtime.js";
10
+ import { registerCrewIntegration } from "./integration.js";
11
+ import { updateWidget } from "./status-widget.js";
12
+
13
+ const extensionDir = dirname(fileURLToPath(import.meta.url));
14
+
15
+ // Process-level cleanup for subagents on exit
16
+ let processHooksSetup = false;
17
+
18
+ function setupProcessHooks() {
19
+ if (processHooksSetup) return;
20
+ processHooksSetup = true;
21
+
22
+ process.once('SIGINT', () => {
23
+ crewRuntime.abortAll();
24
+ process.exit(130);
25
+ });
26
+ process.on('beforeExit', () => crewRuntime.abortAll());
27
+ }
28
+
29
+ export default function (pi: ExtensionAPI) {
30
+ let currentCtx: ExtensionContext | undefined;
31
+
32
+ setupProcessHooks();
33
+
34
+ const refreshWidget = () => {
35
+ if (currentCtx) updateWidget(currentCtx, crewRuntime);
36
+ };
37
+
38
+ const activateSession = (ctx: ExtensionContext) => {
39
+ currentCtx = ctx;
40
+ crewRuntime.activateSession(
41
+ {
42
+ sessionId: ctx.sessionManager.getSessionId(),
43
+ isIdle: () => ctx.isIdle(),
44
+ sendMessage: pi.sendMessage.bind(pi),
45
+ },
46
+ refreshWidget,
47
+ );
48
+ refreshWidget();
49
+ };
50
+
51
+ pi.on("session_start", (_event, ctx) => {
52
+ activateSession(ctx);
53
+ });
54
+
55
+ pi.on("session_shutdown", (event, ctx) => {
56
+ const sessionId = ctx.sessionManager.getSessionId();
57
+ crewRuntime.deactivateSession(sessionId);
58
+
59
+ if (event.reason === "quit") {
60
+ crewRuntime.abortAll();
61
+ }
62
+ });
63
+
64
+ registerCrewIntegration(pi, crewRuntime, extensionDir);
65
+ }
@@ -0,0 +1,59 @@
1
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import type { CrewRuntime } from "../runtime/crew-runtime.js";
3
+
4
+ export function registerCrewCommand(pi: ExtensionAPI, crew: CrewRuntime): void {
5
+ pi.registerCommand("pi-crew-abort", {
6
+ description: "Abort an active subagent",
7
+
8
+ getArgumentCompletions(argumentPrefix) {
9
+ const activeAgents = crew.getAbortableAgents();
10
+ if (activeAgents.length === 0) return null;
11
+ return activeAgents
12
+ .filter((agent) => agent.id.startsWith(argumentPrefix))
13
+ .map((agent) => ({
14
+ value: agent.id,
15
+ label: `${agent.id} (${agent.agentName})`,
16
+ }));
17
+ },
18
+
19
+ async handler(args, ctx) {
20
+ const trimmed = args.trim();
21
+
22
+ if (trimmed) {
23
+ const success = crew.abort(trimmed, { reason: "Aborted by user command" });
24
+ if (!success) {
25
+ ctx.ui.notify(`No active subagent with id "${trimmed}"`, "error");
26
+ } else {
27
+ ctx.ui.notify(`Subagent ${trimmed} aborted`, "info");
28
+ }
29
+ return;
30
+ }
31
+
32
+ const activeAgents = crew.getAbortableAgents();
33
+ if (activeAgents.length === 0) {
34
+ ctx.ui.notify("No active subagents", "info");
35
+ return;
36
+ }
37
+
38
+ const options = activeAgents.map((agent) => ({
39
+ id: agent.id,
40
+ label: `${agent.id} (${agent.agentName})`,
41
+ }));
42
+ const selected = await ctx.ui.select(
43
+ "Select subagent to abort",
44
+ options.map((option) => option.label),
45
+ );
46
+ if (!selected) return;
47
+
48
+ const selectedOption = options.find((option) => option.label === selected);
49
+ if (!selectedOption) return;
50
+
51
+ const success = crew.abort(selectedOption.id, { reason: "Aborted by user command" });
52
+ if (success) {
53
+ ctx.ui.notify(`Subagent ${selectedOption.id} aborted`, "info");
54
+ } else {
55
+ ctx.ui.notify(`Subagent ${selectedOption.id} already finished`, "error");
56
+ }
57
+ },
58
+ });
59
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ type ExtensionAPI,
3
+ getMarkdownTheme,
4
+ } from "@mariozechner/pi-coding-agent";
5
+ import { Box, Markdown, Text } from "@mariozechner/pi-tui";
6
+ import {
7
+ type CrewResultMessageDetails,
8
+ STATUS_ICON,
9
+ getCrewResultTitle,
10
+ } from "../subagent-messages.js";
11
+
12
+ type MessageRenderer = Parameters<ExtensionAPI["registerMessageRenderer"]>[1];
13
+ type MessageRendererTheme = Parameters<MessageRenderer>[2];
14
+
15
+ function getStatusColor(status: CrewResultMessageDetails["status"]): "success" | "error" | "warning" | "muted" {
16
+ switch (status) {
17
+ case "done":
18
+ return "success";
19
+ case "error":
20
+ case "aborted":
21
+ return "error";
22
+ case "running":
23
+ case "waiting":
24
+ return "warning";
25
+ default:
26
+ return "muted";
27
+ }
28
+ }
29
+
30
+ function renderWarningMessage(content: unknown, theme: MessageRendererTheme): Box {
31
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
32
+ box.addChild(new Text(theme.fg("warning", String(content ?? "")), 0, 0));
33
+ return box;
34
+ }
35
+
36
+ export function registerCrewMessageRenderers(pi: ExtensionAPI): void {
37
+ pi.registerMessageRenderer("crew-result", (message, { expanded }, theme) => {
38
+ const details = message.details as CrewResultMessageDetails | undefined;
39
+ const title = details ? getCrewResultTitle(details) : "Subagent update";
40
+ const icon = details
41
+ ? theme.fg(getStatusColor(details.status), STATUS_ICON[details.status])
42
+ : theme.fg("muted", "ℹ");
43
+ const header = `${icon} ${theme.fg("toolTitle", theme.bold(title))}`;
44
+ const body = details?.body ?? (!details && message.content ? String(message.content) : undefined);
45
+
46
+ const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
47
+ box.addChild(new Text(header, 0, 0));
48
+
49
+ if (details?.sessionFile) {
50
+ box.addChild(new Text(theme.fg("muted", `📁 ${details.sessionFile}`), 0, 0));
51
+ }
52
+
53
+ if (body) {
54
+ if (expanded) {
55
+ box.addChild(new Text("", 0, 0));
56
+ box.addChild(new Markdown(body, 0, 0, getMarkdownTheme()));
57
+ } else {
58
+ const lines = body.split("\n");
59
+ const preview = lines.slice(0, 5).join("\n");
60
+ box.addChild(new Text(theme.fg("dim", preview), 0, 0));
61
+ if (lines.length > 5) {
62
+ box.addChild(new Text(theme.fg("muted", "(Ctrl+O to expand)"), 0, 0));
63
+ }
64
+ }
65
+ }
66
+
67
+ return box;
68
+ });
69
+
70
+ pi.registerMessageRenderer("crew-remaining", (message, _options, theme) => {
71
+ return renderWarningMessage(message.content, theme);
72
+ });
73
+
74
+ pi.registerMessageRenderer("crew-list-warning", (message, _options, theme) => {
75
+ return renderWarningMessage(message.content, theme);
76
+ });
77
+ }
@@ -0,0 +1,39 @@
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
+ import { registerCrewAbortTool } from "./tools/crew-abort.js";
8
+ import { registerCrewDoneTool } from "./tools/crew-done.js";
9
+ import { registerCrewListTool } from "./tools/crew-list.js";
10
+ import { registerCrewRespondTool } from "./tools/crew-respond.js";
11
+ import { registerCrewSpawnTool } from "./tools/crew-spawn.js";
12
+
13
+ export function registerCrewTools(
14
+ pi: ExtensionAPI,
15
+ crew: CrewRuntime,
16
+ extensionDir: string,
17
+ ): void {
18
+ const shownDiscoveryWarnings = new Set<string>();
19
+
20
+ const notifyDiscoveryWarnings = (
21
+ ctx: ExtensionContext,
22
+ warnings: AgentDiscoveryWarning[],
23
+ ) => {
24
+ if (!ctx.hasUI) return;
25
+ for (const warning of warnings) {
26
+ const key = `${warning.filePath}:${warning.message}`;
27
+ if (shownDiscoveryWarnings.has(key)) continue;
28
+ shownDiscoveryWarnings.add(key);
29
+ ctx.ui.notify(`${warning.message} (${warning.filePath})`, "error");
30
+ }
31
+ };
32
+
33
+ const deps = { pi, crew, extensionDir, notifyDiscoveryWarnings };
34
+ registerCrewListTool(deps);
35
+ registerCrewSpawnTool(deps);
36
+ registerCrewAbortTool(deps);
37
+ registerCrewRespondTool(deps);
38
+ registerCrewDoneTool(deps);
39
+ }
@@ -0,0 +1,50 @@
1
+ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
2
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import { Box, Text } from "@mariozechner/pi-tui";
4
+
5
+ export type ToolTheme = Parameters<Exclude<Parameters<ExtensionAPI["registerTool"]>[0]["renderCall"], undefined>>[1];
6
+ export type ToolResult = AgentToolResult<unknown>;
7
+
8
+ export function toolError(text: string) {
9
+ return {
10
+ content: [{ type: "text" as const, text }],
11
+ isError: true,
12
+ details: { error: true },
13
+ };
14
+ }
15
+
16
+ export function toolSuccess(
17
+ text: string,
18
+ details: Record<string, unknown> = {},
19
+ options: { terminate?: boolean } = {},
20
+ ) {
21
+ return {
22
+ content: [{ type: "text" as const, text }],
23
+ details,
24
+ ...(options.terminate ? { terminate: true } : {}),
25
+ };
26
+ }
27
+
28
+ export function renderCrewCall(
29
+ theme: ToolTheme,
30
+ name: string,
31
+ id: string,
32
+ preview?: string,
33
+ ): Box {
34
+ const box = new Box(1, 1);
35
+ box.addChild(new Text(theme.fg("toolTitle", theme.bold(`${name} `)) + theme.fg("accent", id), 0, 0));
36
+ if (preview) {
37
+ box.addChild(new Text(theme.fg("dim", preview), 0, 0));
38
+ }
39
+ return box;
40
+ }
41
+
42
+ export function renderCrewResult(
43
+ result: ToolResult,
44
+ theme: ToolTheme,
45
+ ): Text {
46
+ const text = result.content[0];
47
+ const details = result.details as { error?: boolean } | undefined;
48
+ const content = text?.type === "text" && text.text ? text.text : "(no output)";
49
+ return new Text(details?.error ? theme.fg("error", content) : theme.fg("success", content), 0, 0);
50
+ }
@@ -0,0 +1,121 @@
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
+ function formatAbortToolMessage(result: {
11
+ abortedIds: string[];
12
+ missingIds: string[];
13
+ foreignIds: string[];
14
+ }): string {
15
+ const parts: string[] = [];
16
+
17
+ if (result.abortedIds.length > 0) {
18
+ parts.push(`Aborted ${result.abortedIds.length} subagent(s): ${result.abortedIds.join(", ")}`);
19
+ }
20
+ if (result.missingIds.length > 0) {
21
+ parts.push(`Not found or already finished: ${result.missingIds.join(", ")}`);
22
+ }
23
+ if (result.foreignIds.length > 0) {
24
+ parts.push(`Belong to a different session: ${result.foreignIds.join(", ")}`);
25
+ }
26
+
27
+ return parts.join("\n");
28
+ }
29
+
30
+ export function registerCrewAbortTool({ pi, crew }: CrewToolDeps): void {
31
+ pi.registerTool({
32
+ name: "crew_abort",
33
+ label: "Abort Crew",
34
+ description:
35
+ "Abort one, many, or all active subagents owned by the current session.",
36
+ parameters: Type.Object({
37
+ subagent_id: Type.Optional(
38
+ Type.String({ description: "Single subagent ID to abort" }),
39
+ ),
40
+ subagent_ids: Type.Optional(
41
+ Type.Array(Type.String(), {
42
+ minItems: 1,
43
+ description: "Multiple subagent IDs to abort",
44
+ }),
45
+ ),
46
+ all: Type.Optional(
47
+ Type.Boolean({
48
+ description: "Abort all active subagents owned by the current session",
49
+ }),
50
+ ),
51
+ }),
52
+ promptSnippet: "Abort one, many, or all active subagents from this session.",
53
+
54
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
55
+ const callerSessionId = ctx.sessionManager.getSessionId();
56
+ const modeCount = Number(Boolean(params.subagent_id))
57
+ + Number(Boolean(params.subagent_ids?.length))
58
+ + Number(params.all === true);
59
+
60
+ if (modeCount !== 1) {
61
+ return toolError(
62
+ "Provide exactly one of: subagent_id, subagent_ids, or all=true.",
63
+ );
64
+ }
65
+
66
+ if (params.all) {
67
+ const abortedIds = crew.abortAllOwned(callerSessionId, {
68
+ reason: "Aborted by tool request",
69
+ });
70
+ if (abortedIds.length === 0) {
71
+ return toolError("No active subagents in the current session.");
72
+ }
73
+
74
+ return toolSuccess(
75
+ `Aborted ${abortedIds.length} subagent(s): ${abortedIds.join(", ")}`,
76
+ { ids: abortedIds },
77
+ { terminate: true },
78
+ );
79
+ }
80
+
81
+ const ids = params.subagent_id
82
+ ? [params.subagent_id]
83
+ : (params.subagent_ids ?? []);
84
+ const result = crew.abortOwned(ids, callerSessionId, {
85
+ reason: "Aborted by tool request",
86
+ });
87
+ const message = formatAbortToolMessage(result);
88
+
89
+ if (result.abortedIds.length === 0) {
90
+ return toolError(message || "No subagents were aborted.");
91
+ }
92
+
93
+ return toolSuccess(
94
+ message,
95
+ {
96
+ ids: result.abortedIds,
97
+ missing_ids: result.missingIds,
98
+ foreign_ids: result.foreignIds,
99
+ },
100
+ { terminate: true },
101
+ );
102
+ },
103
+
104
+ renderCall(args, theme, _context) {
105
+ if (args.all) {
106
+ return renderCrewCall(theme, "crew_abort", "all");
107
+ }
108
+
109
+ if (args.subagent_id) {
110
+ return renderCrewCall(theme, "crew_abort", args.subagent_id);
111
+ }
112
+
113
+ const count = Array.isArray(args.subagent_ids) ? args.subagent_ids.length : 0;
114
+ return renderCrewCall(theme, "crew_abort", `${count} ids`);
115
+ },
116
+
117
+ renderResult(result, _options, theme, _context) {
118
+ return renderCrewResult(result, theme);
119
+ },
120
+ });
121
+ }
@@ -0,0 +1,42 @@
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 registerCrewDoneTool({ pi, crew }: CrewToolDeps): void {
11
+ pi.registerTool({
12
+ name: "crew_done",
13
+ label: "Done with Crew",
14
+ description:
15
+ "Close an interactive subagent session. Use when you no longer need to interact with the subagent.",
16
+ parameters: Type.Object({
17
+ subagent_id: Type.String({ description: "ID of the subagent to close" }),
18
+ }),
19
+ promptSnippet: "Close an interactive subagent session when done.",
20
+
21
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
22
+ const callerSessionId = ctx.sessionManager.getSessionId();
23
+ const { error } = crew.done(params.subagent_id, callerSessionId);
24
+ if (error) return toolError(error);
25
+
26
+ return toolSuccess(
27
+ `Subagent ${params.subagent_id} closed.`,
28
+ {
29
+ id: params.subagent_id,
30
+ },
31
+ );
32
+ },
33
+
34
+ renderCall(args, theme, _context) {
35
+ return renderCrewCall(theme, "crew_done", args.subagent_id || "...");
36
+ },
37
+
38
+ renderResult(result, _options, theme, _context) {
39
+ return renderCrewResult(result, theme);
40
+ },
41
+ });
42
+ }
@@ -0,0 +1,91 @@
1
+ import { Text } from "@mariozechner/pi-tui";
2
+ import { Type } from "typebox";
3
+ import { discoverAgents } from "../../agent-discovery.js";
4
+ import { STATUS_ICON, sendCrewListActiveWarning } from "../../subagent-messages.js";
5
+ import type { CrewToolDeps } from "./tool-deps.js";
6
+
7
+ export function registerCrewListTool({
8
+ pi,
9
+ crew,
10
+ notifyDiscoveryWarnings,
11
+ }: CrewToolDeps): void {
12
+ pi.registerTool({
13
+ name: "crew_list",
14
+ label: "List Crew",
15
+ description:
16
+ "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.",
17
+ parameters: Type.Object({}),
18
+ promptSnippet: "List subagent definitions and active subagents",
19
+ promptGuidelines: [
20
+ "Use crew_list first to see available subagents before spawning.",
21
+ "crew_list: Call this only to discover available subagents before spawning, or when the user explicitly asks for a status report. Do not call it to check if a subagent finished — results arrive as steering messages automatically.",
22
+ ],
23
+
24
+ async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
25
+ const { agents, warnings } = discoverAgents(ctx.cwd);
26
+ notifyDiscoveryWarnings(ctx, warnings);
27
+ const callerSessionId = ctx.sessionManager.getSessionId();
28
+ const running = crew.getActiveSummariesForOwner(callerSessionId);
29
+
30
+ const lines: string[] = [];
31
+
32
+ lines.push("## Available Subagents");
33
+ if (agents.length === 0) {
34
+ lines.push(
35
+ "No valid subagent definitions found. Add `.md` files to `<cwd>/.pi/agents/` or `~/.pi/agent/agents/`.",
36
+ );
37
+ } else {
38
+ for (const agent of agents) {
39
+ lines.push("");
40
+ lines.push(`name: ${agent.name}`);
41
+ lines.push(`description: ${agent.description}`);
42
+ lines.push(`interactive: ${agent.interactive ? "true" : "false"}`);
43
+ }
44
+ }
45
+
46
+ if (warnings.length > 0) {
47
+ lines.push("");
48
+ lines.push("## Ignored subagent definitions");
49
+ for (const warning of warnings) {
50
+ lines.push(`- ${warning.message} (${warning.filePath})`);
51
+ }
52
+ }
53
+
54
+ lines.push("");
55
+ lines.push("## Active Subagents");
56
+ if (running.length === 0) {
57
+ lines.push("No subagents currently active.");
58
+ } else {
59
+ for (const agent of running) {
60
+ const icon = STATUS_ICON[agent.status] ?? "❓";
61
+ lines.push("");
62
+ lines.push(`id: ${agent.id}`);
63
+ lines.push(`name: ${agent.agentName}`);
64
+ lines.push(`status: ${icon} ${agent.status}`);
65
+ }
66
+ }
67
+
68
+ const text = lines.join("\n");
69
+
70
+ if (running.length > 0) {
71
+ Promise.resolve().then(() => {
72
+ sendCrewListActiveWarning(pi.sendMessage.bind(pi), {
73
+ isIdle: ctx.isIdle(),
74
+ triggerTurn: true,
75
+ });
76
+ });
77
+ }
78
+
79
+ return { content: [{ type: "text", text }], details: {} };
80
+ },
81
+
82
+ renderCall(_args, theme, _context) {
83
+ return new Text(theme.fg("toolTitle", theme.bold("crew_list")), 0, 0);
84
+ },
85
+
86
+ renderResult(result, _options, _theme, _context) {
87
+ const text = result.content[0];
88
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
89
+ },
90
+ });
91
+ }