@melihmucuk/pi-crew 1.0.0
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/LICENSE +21 -0
- package/README.md +199 -0
- package/agents/code-reviewer.md +145 -0
- package/agents/planner.md +142 -0
- package/agents/quality-reviewer.md +164 -0
- package/agents/scout.md +58 -0
- package/agents/worker.md +81 -0
- package/dist/agent-discovery.d.ts +34 -0
- package/dist/agent-discovery.js +527 -0
- package/dist/bootstrap-session.d.ts +11 -0
- package/dist/bootstrap-session.js +63 -0
- package/dist/crew-manager.d.ts +43 -0
- package/dist/crew-manager.js +235 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -0
- package/dist/integration/register-command.d.ts +3 -0
- package/dist/integration/register-command.js +51 -0
- package/dist/integration/register-renderers.d.ts +2 -0
- package/dist/integration/register-renderers.js +50 -0
- package/dist/integration/register-tools.d.ts +3 -0
- package/dist/integration/register-tools.js +25 -0
- package/dist/integration/tool-presentation.d.ts +30 -0
- package/dist/integration/tool-presentation.js +29 -0
- package/dist/integration/tools/crew-abort.d.ts +2 -0
- package/dist/integration/tools/crew-abort.js +79 -0
- package/dist/integration/tools/crew-done.d.ts +2 -0
- package/dist/integration/tools/crew-done.js +28 -0
- package/dist/integration/tools/crew-list.d.ts +2 -0
- package/dist/integration/tools/crew-list.js +72 -0
- package/dist/integration/tools/crew-respond.d.ts +2 -0
- package/dist/integration/tools/crew-respond.js +30 -0
- package/dist/integration/tools/crew-spawn.d.ts +2 -0
- package/dist/integration/tools/crew-spawn.js +42 -0
- package/dist/integration/tools/tool-deps.d.ts +8 -0
- package/dist/integration/tools/tool-deps.js +1 -0
- package/dist/integration.d.ts +3 -0
- package/dist/integration.js +8 -0
- package/dist/runtime/delivery-coordinator.d.ts +17 -0
- package/dist/runtime/delivery-coordinator.js +60 -0
- package/dist/runtime/subagent-registry.d.ts +13 -0
- package/dist/runtime/subagent-registry.js +55 -0
- package/dist/runtime/subagent-state.d.ts +34 -0
- package/dist/runtime/subagent-state.js +34 -0
- package/dist/status-widget.d.ts +3 -0
- package/dist/status-widget.js +84 -0
- package/dist/subagent-messages.d.ts +30 -0
- package/dist/subagent-messages.js +58 -0
- package/dist/tool-registry.d.ts +76 -0
- package/dist/tool-registry.js +17 -0
- package/docs/architecture.md +883 -0
- package/package.json +52 -0
- package/prompts/pi-crew:review.md +168 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type CreateAgentSessionResult, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AgentConfig } from "./agent-discovery.js";
|
|
3
|
+
interface BootstrapOptions {
|
|
4
|
+
agentConfig: AgentConfig;
|
|
5
|
+
cwd: string;
|
|
6
|
+
ctx: ExtensionContext;
|
|
7
|
+
extensionResolvedPath: string;
|
|
8
|
+
parentSessionFile?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function bootstrapSession(opts: BootstrapOptions): Promise<CreateAgentSessionResult>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { createSupportedTools, SUPPORTED_TOOL_NAMES } from "./tool-registry.js";
|
|
3
|
+
function resolveTools(agentConfig, cwd) {
|
|
4
|
+
return createSupportedTools(agentConfig.tools ?? SUPPORTED_TOOL_NAMES, cwd);
|
|
5
|
+
}
|
|
6
|
+
function resolveModel(agentConfig, ctx) {
|
|
7
|
+
const model = ctx.model;
|
|
8
|
+
if (!agentConfig.parsedModel)
|
|
9
|
+
return model;
|
|
10
|
+
const found = ctx.modelRegistry.find(agentConfig.parsedModel.provider, agentConfig.parsedModel.modelId);
|
|
11
|
+
if (found)
|
|
12
|
+
return found;
|
|
13
|
+
console.warn(`[pi-crew] Subagent "${agentConfig.name}": model "${agentConfig.model}" not found in registry, using default`);
|
|
14
|
+
return model;
|
|
15
|
+
}
|
|
16
|
+
function warnUnknownSkills(agentConfig, resourceLoader) {
|
|
17
|
+
if (!agentConfig.skills)
|
|
18
|
+
return;
|
|
19
|
+
const availableSkillNames = new Set(resourceLoader.getSkills().skills.map((skill) => skill.name));
|
|
20
|
+
const unknownSkills = agentConfig.skills.filter((skillName) => !availableSkillNames.has(skillName));
|
|
21
|
+
if (unknownSkills.length === 0)
|
|
22
|
+
return;
|
|
23
|
+
console.warn(`[pi-crew] Subagent "${agentConfig.name}": unknown skills ${unknownSkills.map((skillName) => `"${skillName}"`).join(", ")}, ignoring`);
|
|
24
|
+
}
|
|
25
|
+
export async function bootstrapSession(opts) {
|
|
26
|
+
const { agentConfig, cwd, ctx, extensionResolvedPath, parentSessionFile } = opts;
|
|
27
|
+
const authStorage = ctx.modelRegistry.authStorage;
|
|
28
|
+
const modelRegistry = ctx.modelRegistry;
|
|
29
|
+
const model = resolveModel(agentConfig, ctx);
|
|
30
|
+
const tools = resolveTools(agentConfig, cwd);
|
|
31
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
32
|
+
cwd,
|
|
33
|
+
extensionsOverride: (base) => ({
|
|
34
|
+
...base,
|
|
35
|
+
extensions: base.extensions.filter((ext) => !ext.resolvedPath.startsWith(extensionResolvedPath)),
|
|
36
|
+
}),
|
|
37
|
+
skillsOverride: agentConfig.skills
|
|
38
|
+
? (base) => ({
|
|
39
|
+
skills: base.skills.filter((skill) => agentConfig.skills.includes(skill.name)),
|
|
40
|
+
diagnostics: base.diagnostics,
|
|
41
|
+
})
|
|
42
|
+
: undefined,
|
|
43
|
+
appendSystemPromptOverride: (base) => agentConfig.systemPrompt.trim() ? [...base, agentConfig.systemPrompt] : base,
|
|
44
|
+
});
|
|
45
|
+
await resourceLoader.reload();
|
|
46
|
+
warnUnknownSkills(agentConfig, resourceLoader);
|
|
47
|
+
const settingsManager = SettingsManager.inMemory({
|
|
48
|
+
compaction: { enabled: agentConfig.compaction ?? true },
|
|
49
|
+
});
|
|
50
|
+
const sessionManager = SessionManager.create(cwd);
|
|
51
|
+
sessionManager.newSession({ parentSession: parentSessionFile });
|
|
52
|
+
return createAgentSession({
|
|
53
|
+
cwd,
|
|
54
|
+
model,
|
|
55
|
+
thinkingLevel: agentConfig.thinking,
|
|
56
|
+
tools,
|
|
57
|
+
resourceLoader,
|
|
58
|
+
sessionManager,
|
|
59
|
+
settingsManager,
|
|
60
|
+
authStorage,
|
|
61
|
+
modelRegistry,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AgentConfig } from "./agent-discovery.js";
|
|
3
|
+
import { type AbortableAgentSummary, type ActiveAgentSummary } from "./runtime/subagent-state.js";
|
|
4
|
+
export type { AbortableAgentSummary, ActiveAgentSummary } from "./runtime/subagent-state.js";
|
|
5
|
+
export interface AbortOwnedResult {
|
|
6
|
+
abortedIds: string[];
|
|
7
|
+
missingIds: string[];
|
|
8
|
+
foreignIds: string[];
|
|
9
|
+
}
|
|
10
|
+
interface AbortOptions {
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class CrewManager {
|
|
14
|
+
private extensionResolvedPath;
|
|
15
|
+
private registry;
|
|
16
|
+
private delivery;
|
|
17
|
+
onWidgetUpdate: (() => void) | undefined;
|
|
18
|
+
constructor(extensionResolvedPath: string);
|
|
19
|
+
activateSession(sessionId: string, isIdle: () => boolean, pi: ExtensionAPI): void;
|
|
20
|
+
spawn(agentConfig: AgentConfig, task: string, cwd: string, ownerSessionId: string, ctx: ExtensionContext, pi: ExtensionAPI): string;
|
|
21
|
+
private attachSessionListeners;
|
|
22
|
+
private attachSpawnedSession;
|
|
23
|
+
/**
|
|
24
|
+
* Single owner for post-prompt and terminal state transitions.
|
|
25
|
+
* Publishes the outcome, updates state, and disposes finished subagents.
|
|
26
|
+
*/
|
|
27
|
+
private settleAgent;
|
|
28
|
+
private disposeAgent;
|
|
29
|
+
private runPromptCycle;
|
|
30
|
+
private spawnSession;
|
|
31
|
+
respond(id: string, message: string, pi: ExtensionAPI, callerSessionId: string): {
|
|
32
|
+
error?: string;
|
|
33
|
+
};
|
|
34
|
+
done(id: string, callerSessionId: string): {
|
|
35
|
+
error?: string;
|
|
36
|
+
};
|
|
37
|
+
abort(id: string, pi: ExtensionAPI, opts: AbortOptions): boolean;
|
|
38
|
+
abortOwned(ids: string[], callerSessionId: string, pi: ExtensionAPI, opts: AbortOptions): AbortOwnedResult;
|
|
39
|
+
abortAllOwned(callerSessionId: string, pi: ExtensionAPI, opts: AbortOptions): string[];
|
|
40
|
+
abortForOwner(ownerSessionId: string, pi: ExtensionAPI): void;
|
|
41
|
+
getAbortableAgents(): AbortableAgentSummary[];
|
|
42
|
+
getActiveSummariesForOwner(ownerSessionId: string): ActiveAgentSummary[];
|
|
43
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { bootstrapSession } from "./bootstrap-session.js";
|
|
2
|
+
import { DeliveryCoordinator } from "./runtime/delivery-coordinator.js";
|
|
3
|
+
import { SubagentRegistry } from "./runtime/subagent-registry.js";
|
|
4
|
+
import { isAbortableStatus, isAborted, } from "./runtime/subagent-state.js";
|
|
5
|
+
function getLastAssistantMessage(messages) {
|
|
6
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
7
|
+
const msg = messages[i];
|
|
8
|
+
if (msg.role === "assistant") {
|
|
9
|
+
return msg;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
function getAssistantText(message) {
|
|
15
|
+
if (!message)
|
|
16
|
+
return undefined;
|
|
17
|
+
const texts = [];
|
|
18
|
+
for (const part of message.content) {
|
|
19
|
+
if (part.type === "text") {
|
|
20
|
+
texts.push(part.text);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return texts.length > 0 ? texts.join("\n") : undefined;
|
|
24
|
+
}
|
|
25
|
+
function getPromptOutcome(state) {
|
|
26
|
+
const lastAssistant = getLastAssistantMessage(state.session.messages);
|
|
27
|
+
const text = getAssistantText(lastAssistant);
|
|
28
|
+
if (lastAssistant?.stopReason === "error") {
|
|
29
|
+
return {
|
|
30
|
+
status: "error",
|
|
31
|
+
error: lastAssistant.errorMessage ?? text ?? "(no output)",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (lastAssistant?.stopReason === "aborted") {
|
|
35
|
+
return {
|
|
36
|
+
status: "aborted",
|
|
37
|
+
error: lastAssistant.errorMessage ?? text ?? "(no output)",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
status: state.agentConfig.interactive ? "waiting" : "done",
|
|
42
|
+
result: text ?? "(no output)",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export class CrewManager {
|
|
46
|
+
extensionResolvedPath;
|
|
47
|
+
registry = new SubagentRegistry();
|
|
48
|
+
delivery = new DeliveryCoordinator();
|
|
49
|
+
onWidgetUpdate;
|
|
50
|
+
constructor(extensionResolvedPath) {
|
|
51
|
+
this.extensionResolvedPath = extensionResolvedPath;
|
|
52
|
+
}
|
|
53
|
+
activateSession(sessionId, isIdle, pi) {
|
|
54
|
+
this.delivery.activateSession(sessionId, isIdle, pi, (ownerSessionId, excludeId) => this.registry.countRunningForOwner(ownerSessionId, excludeId));
|
|
55
|
+
}
|
|
56
|
+
spawn(agentConfig, task, cwd, ownerSessionId, ctx, pi) {
|
|
57
|
+
const state = this.registry.create(agentConfig, task, ownerSessionId);
|
|
58
|
+
this.onWidgetUpdate?.();
|
|
59
|
+
void this.spawnSession(state, cwd, ctx.sessionManager.getSessionFile(), ctx, pi);
|
|
60
|
+
return state.id;
|
|
61
|
+
}
|
|
62
|
+
attachSessionListeners(state, session) {
|
|
63
|
+
session.subscribe((event) => {
|
|
64
|
+
if (event.type !== "turn_end")
|
|
65
|
+
return;
|
|
66
|
+
state.turns++;
|
|
67
|
+
const msg = event.message;
|
|
68
|
+
if (msg.role === "assistant") {
|
|
69
|
+
const assistantMsg = msg;
|
|
70
|
+
state.contextTokens = assistantMsg.usage.totalTokens;
|
|
71
|
+
state.model = assistantMsg.model;
|
|
72
|
+
}
|
|
73
|
+
this.onWidgetUpdate?.();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
attachSpawnedSession(state, session) {
|
|
77
|
+
if (!this.registry.hasState(state)) {
|
|
78
|
+
session.dispose();
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
state.session = session;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Single owner for post-prompt and terminal state transitions.
|
|
86
|
+
* Publishes the outcome, updates state, and disposes finished subagents.
|
|
87
|
+
*/
|
|
88
|
+
settleAgent(state, nextStatus, opts, pi) {
|
|
89
|
+
state.status = nextStatus;
|
|
90
|
+
state.result = opts.result;
|
|
91
|
+
state.error = opts.error;
|
|
92
|
+
this.delivery.deliver(state.ownerSessionId, {
|
|
93
|
+
id: state.id,
|
|
94
|
+
agentName: state.agentConfig.name,
|
|
95
|
+
status: state.status,
|
|
96
|
+
result: state.result,
|
|
97
|
+
error: state.error,
|
|
98
|
+
}, pi, (ownerSessionId, excludeId) => this.registry.countRunningForOwner(ownerSessionId, excludeId));
|
|
99
|
+
if (state.status !== "waiting") {
|
|
100
|
+
this.disposeAgent(state);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
this.onWidgetUpdate?.();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
disposeAgent(state) {
|
|
107
|
+
state.session?.dispose();
|
|
108
|
+
this.registry.delete(state.id);
|
|
109
|
+
this.onWidgetUpdate?.();
|
|
110
|
+
}
|
|
111
|
+
async runPromptCycle(state, prompt, pi) {
|
|
112
|
+
if (isAborted(state))
|
|
113
|
+
return;
|
|
114
|
+
try {
|
|
115
|
+
await state.session.prompt(prompt);
|
|
116
|
+
if (isAborted(state))
|
|
117
|
+
return;
|
|
118
|
+
const outcome = getPromptOutcome(state);
|
|
119
|
+
this.settleAgent(state, outcome.status, outcome, pi);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (isAborted(state))
|
|
123
|
+
return;
|
|
124
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
125
|
+
this.settleAgent(state, "error", { error }, pi);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async spawnSession(state, cwd, parentSessionFile, ctx, pi) {
|
|
129
|
+
try {
|
|
130
|
+
if (isAborted(state))
|
|
131
|
+
return;
|
|
132
|
+
const { session } = await bootstrapSession({
|
|
133
|
+
agentConfig: state.agentConfig,
|
|
134
|
+
cwd,
|
|
135
|
+
ctx,
|
|
136
|
+
extensionResolvedPath: this.extensionResolvedPath,
|
|
137
|
+
parentSessionFile,
|
|
138
|
+
});
|
|
139
|
+
if (!this.attachSpawnedSession(state, session))
|
|
140
|
+
return;
|
|
141
|
+
this.attachSessionListeners(state, session);
|
|
142
|
+
await this.runPromptCycle(state, state.task, pi);
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (isAborted(state))
|
|
146
|
+
return;
|
|
147
|
+
// Only bootstrap errors reach here; runPromptCycle handles its own errors
|
|
148
|
+
if (state.status === "running") {
|
|
149
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
150
|
+
this.settleAgent(state, "error", { error }, pi);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
respond(id, message, pi, callerSessionId) {
|
|
155
|
+
const state = this.registry.get(id);
|
|
156
|
+
if (!state)
|
|
157
|
+
return { error: `No subagent with id "${id}"` };
|
|
158
|
+
if (state.ownerSessionId !== callerSessionId) {
|
|
159
|
+
return { error: `Subagent "${id}" belongs to a different session` };
|
|
160
|
+
}
|
|
161
|
+
if (state.status !== "waiting") {
|
|
162
|
+
return { error: `Subagent "${id}" is not waiting for a response (status: ${state.status})` };
|
|
163
|
+
}
|
|
164
|
+
if (!state.session)
|
|
165
|
+
return { error: `Subagent "${id}" has no active session` };
|
|
166
|
+
state.status = "running";
|
|
167
|
+
this.onWidgetUpdate?.();
|
|
168
|
+
void this.runPromptCycle(state, message, pi);
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
done(id, callerSessionId) {
|
|
172
|
+
const state = this.registry.get(id);
|
|
173
|
+
if (!state)
|
|
174
|
+
return { error: `No active subagent with id "${id}"` };
|
|
175
|
+
if (state.ownerSessionId !== callerSessionId) {
|
|
176
|
+
return { error: `Subagent "${id}" belongs to a different session` };
|
|
177
|
+
}
|
|
178
|
+
if (state.status !== "waiting") {
|
|
179
|
+
return { error: `Subagent "${id}" is not in waiting state` };
|
|
180
|
+
}
|
|
181
|
+
this.disposeAgent(state);
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
abort(id, pi, opts) {
|
|
185
|
+
const state = this.registry.get(id);
|
|
186
|
+
if (!state || !isAbortableStatus(state.status))
|
|
187
|
+
return false;
|
|
188
|
+
state.session?.abort().catch(() => { });
|
|
189
|
+
this.settleAgent(state, "aborted", { error: opts.reason }, pi);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
abortOwned(ids, callerSessionId, pi, opts) {
|
|
193
|
+
const uniqueIds = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
|
|
194
|
+
const result = {
|
|
195
|
+
abortedIds: [],
|
|
196
|
+
missingIds: [],
|
|
197
|
+
foreignIds: [],
|
|
198
|
+
};
|
|
199
|
+
for (const id of uniqueIds) {
|
|
200
|
+
const state = this.registry.get(id);
|
|
201
|
+
if (!state || !isAbortableStatus(state.status)) {
|
|
202
|
+
result.missingIds.push(id);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (state.ownerSessionId !== callerSessionId) {
|
|
206
|
+
result.foreignIds.push(id);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (this.abort(id, pi, opts)) {
|
|
210
|
+
result.abortedIds.push(id);
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
result.missingIds.push(id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
abortAllOwned(callerSessionId, pi, opts) {
|
|
219
|
+
const ids = this.registry.getOwnedAbortableIds(callerSessionId);
|
|
220
|
+
for (const id of ids) {
|
|
221
|
+
this.abort(id, pi, opts);
|
|
222
|
+
}
|
|
223
|
+
return ids;
|
|
224
|
+
}
|
|
225
|
+
abortForOwner(ownerSessionId, pi) {
|
|
226
|
+
this.abortAllOwned(ownerSessionId, pi, { reason: "Aborted on session shutdown" });
|
|
227
|
+
this.delivery.clearPendingForOwner(ownerSessionId);
|
|
228
|
+
}
|
|
229
|
+
getAbortableAgents() {
|
|
230
|
+
return this.registry.getAbortableAgents();
|
|
231
|
+
}
|
|
232
|
+
getActiveSummariesForOwner(ownerSessionId) {
|
|
233
|
+
return this.registry.getActiveSummariesForOwner(ownerSessionId);
|
|
234
|
+
}
|
|
235
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { CrewManager } from "./crew-manager.js";
|
|
4
|
+
import { registerCrewIntegration } from "./integration.js";
|
|
5
|
+
import { updateWidget } from "./status-widget.js";
|
|
6
|
+
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export default function (pi) {
|
|
8
|
+
const crewManager = new CrewManager(extensionDir);
|
|
9
|
+
let currentCtx;
|
|
10
|
+
const refreshWidget = () => {
|
|
11
|
+
if (currentCtx)
|
|
12
|
+
updateWidget(currentCtx, crewManager);
|
|
13
|
+
};
|
|
14
|
+
const activateSession = (ctx) => {
|
|
15
|
+
currentCtx = ctx;
|
|
16
|
+
crewManager.activateSession(ctx.sessionManager.getSessionId(), () => ctx.isIdle(), pi);
|
|
17
|
+
refreshWidget();
|
|
18
|
+
};
|
|
19
|
+
crewManager.onWidgetUpdate = refreshWidget;
|
|
20
|
+
pi.on("session_start", (_event, ctx) => activateSession(ctx));
|
|
21
|
+
pi.on("session_switch", (_event, ctx) => activateSession(ctx));
|
|
22
|
+
pi.on("session_fork", (_event, ctx) => activateSession(ctx));
|
|
23
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
24
|
+
crewManager.abortForOwner(ctx.sessionManager.getSessionId(), pi);
|
|
25
|
+
});
|
|
26
|
+
registerCrewIntegration(pi, crewManager);
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function registerCrewCommand(pi, crewManager) {
|
|
2
|
+
pi.registerCommand("pi-crew:abort", {
|
|
3
|
+
description: "Abort an active subagent",
|
|
4
|
+
getArgumentCompletions(argumentPrefix) {
|
|
5
|
+
const activeAgents = crewManager.getAbortableAgents();
|
|
6
|
+
if (activeAgents.length === 0)
|
|
7
|
+
return null;
|
|
8
|
+
return activeAgents
|
|
9
|
+
.filter((agent) => agent.id.startsWith(argumentPrefix))
|
|
10
|
+
.map((agent) => ({
|
|
11
|
+
value: agent.id,
|
|
12
|
+
label: `${agent.id} (${agent.agentName})`,
|
|
13
|
+
}));
|
|
14
|
+
},
|
|
15
|
+
async handler(args, ctx) {
|
|
16
|
+
const trimmed = args.trim();
|
|
17
|
+
if (trimmed) {
|
|
18
|
+
const success = crewManager.abort(trimmed, pi, { reason: "Aborted by user command" });
|
|
19
|
+
if (!success) {
|
|
20
|
+
ctx.ui.notify(`No active subagent with id "${trimmed}"`, "error");
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
ctx.ui.notify(`Subagent ${trimmed} aborted`, "info");
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const activeAgents = crewManager.getAbortableAgents();
|
|
28
|
+
if (activeAgents.length === 0) {
|
|
29
|
+
ctx.ui.notify("No active subagents", "info");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const options = activeAgents.map((agent) => ({
|
|
33
|
+
id: agent.id,
|
|
34
|
+
label: `${agent.id} (${agent.agentName})`,
|
|
35
|
+
}));
|
|
36
|
+
const selected = await ctx.ui.select("Select subagent to abort", options.map((option) => option.label));
|
|
37
|
+
if (!selected)
|
|
38
|
+
return;
|
|
39
|
+
const selectedOption = options.find((option) => option.label === selected);
|
|
40
|
+
if (!selectedOption)
|
|
41
|
+
return;
|
|
42
|
+
const success = crewManager.abort(selectedOption.id, pi, { reason: "Aborted by user command" });
|
|
43
|
+
if (success) {
|
|
44
|
+
ctx.ui.notify(`Subagent ${selectedOption.id} aborted`, "info");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
ctx.ui.notify(`Subagent ${selectedOption.id} already finished`, "error");
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { getMarkdownTheme, } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Box, Markdown, Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import { STATUS_ICON, getCrewResultTitle, } from "../subagent-messages.js";
|
|
4
|
+
function getStatusColor(status) {
|
|
5
|
+
switch (status) {
|
|
6
|
+
case "done":
|
|
7
|
+
return "success";
|
|
8
|
+
case "error":
|
|
9
|
+
case "aborted":
|
|
10
|
+
return "error";
|
|
11
|
+
case "running":
|
|
12
|
+
case "waiting":
|
|
13
|
+
return "warning";
|
|
14
|
+
default:
|
|
15
|
+
return "muted";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function registerCrewMessageRenderers(pi) {
|
|
19
|
+
pi.registerMessageRenderer("crew-result", (message, { expanded }, theme) => {
|
|
20
|
+
const details = message.details;
|
|
21
|
+
const title = details ? getCrewResultTitle(details) : "Subagent update";
|
|
22
|
+
const icon = details
|
|
23
|
+
? theme.fg(getStatusColor(details.status), STATUS_ICON[details.status])
|
|
24
|
+
: theme.fg("muted", "ℹ");
|
|
25
|
+
const header = `${icon} ${theme.fg("toolTitle", theme.bold(title))}`;
|
|
26
|
+
const body = details?.body ?? (!details && message.content ? String(message.content) : undefined);
|
|
27
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
28
|
+
box.addChild(new Text(header, 0, 0));
|
|
29
|
+
if (body) {
|
|
30
|
+
if (expanded) {
|
|
31
|
+
box.addChild(new Text("", 0, 0));
|
|
32
|
+
box.addChild(new Markdown(body, 0, 0, getMarkdownTheme()));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
const lines = body.split("\n");
|
|
36
|
+
const preview = lines.slice(0, 5).join("\n");
|
|
37
|
+
box.addChild(new Text(theme.fg("dim", preview), 0, 0));
|
|
38
|
+
if (lines.length > 5) {
|
|
39
|
+
box.addChild(new Text(theme.fg("muted", "(Ctrl+O to expand)"), 0, 0));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return box;
|
|
44
|
+
});
|
|
45
|
+
pi.registerMessageRenderer("crew-remaining", (message, _options, theme) => {
|
|
46
|
+
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
47
|
+
box.addChild(new Text(theme.fg("warning", String(message.content ?? "")), 0, 0));
|
|
48
|
+
return box;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { registerCrewAbortTool } from "./tools/crew-abort.js";
|
|
2
|
+
import { registerCrewDoneTool } from "./tools/crew-done.js";
|
|
3
|
+
import { registerCrewListTool } from "./tools/crew-list.js";
|
|
4
|
+
import { registerCrewRespondTool } from "./tools/crew-respond.js";
|
|
5
|
+
import { registerCrewSpawnTool } from "./tools/crew-spawn.js";
|
|
6
|
+
export function registerCrewTools(pi, crewManager) {
|
|
7
|
+
const shownDiscoveryWarnings = new Set();
|
|
8
|
+
const notifyDiscoveryWarnings = (ctx, warnings) => {
|
|
9
|
+
if (!ctx.hasUI)
|
|
10
|
+
return;
|
|
11
|
+
for (const warning of warnings) {
|
|
12
|
+
const key = `${warning.filePath}:${warning.message}`;
|
|
13
|
+
if (shownDiscoveryWarnings.has(key))
|
|
14
|
+
continue;
|
|
15
|
+
shownDiscoveryWarnings.add(key);
|
|
16
|
+
ctx.ui.notify(`${warning.message} (${warning.filePath})`, "error");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const deps = { pi, crewManager, notifyDiscoveryWarnings };
|
|
20
|
+
registerCrewListTool(deps);
|
|
21
|
+
registerCrewSpawnTool(deps);
|
|
22
|
+
registerCrewAbortTool(deps);
|
|
23
|
+
registerCrewRespondTool(deps);
|
|
24
|
+
registerCrewDoneTool(deps);
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
3
|
+
export type ToolTheme = Parameters<Exclude<Parameters<ExtensionAPI["registerTool"]>[0]["renderCall"], undefined>>[1];
|
|
4
|
+
export type ToolResult = {
|
|
5
|
+
content: {
|
|
6
|
+
type: string;
|
|
7
|
+
text?: string;
|
|
8
|
+
}[];
|
|
9
|
+
details: unknown;
|
|
10
|
+
};
|
|
11
|
+
export declare function toolError(text: string): {
|
|
12
|
+
content: {
|
|
13
|
+
type: "text";
|
|
14
|
+
text: string;
|
|
15
|
+
}[];
|
|
16
|
+
isError: boolean;
|
|
17
|
+
details: {
|
|
18
|
+
error: boolean;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function toolSuccess(text: string, details?: Record<string, unknown>): {
|
|
22
|
+
content: {
|
|
23
|
+
type: "text";
|
|
24
|
+
text: string;
|
|
25
|
+
}[];
|
|
26
|
+
details: Record<string, unknown>;
|
|
27
|
+
};
|
|
28
|
+
export declare function truncatePreview(text: string, max: number): string;
|
|
29
|
+
export declare function renderCrewCall(theme: ToolTheme, name: string, id: string, preview?: string): Text;
|
|
30
|
+
export declare function renderCrewResult(result: ToolResult, theme: ToolTheme): Text;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
2
|
+
export function toolError(text) {
|
|
3
|
+
return {
|
|
4
|
+
content: [{ type: "text", text }],
|
|
5
|
+
isError: true,
|
|
6
|
+
details: { error: true },
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function toolSuccess(text, details = {}) {
|
|
10
|
+
return {
|
|
11
|
+
content: [{ type: "text", text }],
|
|
12
|
+
details,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function truncatePreview(text, max) {
|
|
16
|
+
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
17
|
+
}
|
|
18
|
+
export function renderCrewCall(theme, name, id, preview) {
|
|
19
|
+
let text = theme.fg("toolTitle", theme.bold(`${name} `)) + theme.fg("accent", id);
|
|
20
|
+
if (preview)
|
|
21
|
+
text += theme.fg("dim", ` "${preview}"`);
|
|
22
|
+
return new Text(text, 0, 0);
|
|
23
|
+
}
|
|
24
|
+
export function renderCrewResult(result, theme) {
|
|
25
|
+
const text = result.content[0];
|
|
26
|
+
const details = result.details;
|
|
27
|
+
const content = text?.type === "text" && text.text ? text.text : "(no output)";
|
|
28
|
+
return new Text(details?.error ? theme.fg("error", content) : theme.fg("success", content), 0, 0);
|
|
29
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import { renderCrewCall, renderCrewResult, toolError, toolSuccess, } from "../tool-presentation.js";
|
|
3
|
+
function formatAbortToolMessage(result) {
|
|
4
|
+
const parts = [];
|
|
5
|
+
if (result.abortedIds.length > 0) {
|
|
6
|
+
parts.push(`Aborted ${result.abortedIds.length} subagent(s): ${result.abortedIds.join(", ")}`);
|
|
7
|
+
}
|
|
8
|
+
if (result.missingIds.length > 0) {
|
|
9
|
+
parts.push(`Not found or already finished: ${result.missingIds.join(", ")}`);
|
|
10
|
+
}
|
|
11
|
+
if (result.foreignIds.length > 0) {
|
|
12
|
+
parts.push(`Belong to a different session: ${result.foreignIds.join(", ")}`);
|
|
13
|
+
}
|
|
14
|
+
return parts.join("\n");
|
|
15
|
+
}
|
|
16
|
+
export function registerCrewAbortTool({ pi, crewManager }) {
|
|
17
|
+
pi.registerTool({
|
|
18
|
+
name: "crew_abort",
|
|
19
|
+
label: "Abort Crew",
|
|
20
|
+
description: "Abort one, many, or all active subagents owned by the current session.",
|
|
21
|
+
parameters: Type.Object({
|
|
22
|
+
subagent_id: Type.Optional(Type.String({ description: "Single subagent ID to abort" })),
|
|
23
|
+
subagent_ids: Type.Optional(Type.Array(Type.String(), {
|
|
24
|
+
minItems: 1,
|
|
25
|
+
description: "Multiple subagent IDs to abort",
|
|
26
|
+
})),
|
|
27
|
+
all: Type.Optional(Type.Boolean({
|
|
28
|
+
description: "Abort all active subagents owned by the current session",
|
|
29
|
+
})),
|
|
30
|
+
}),
|
|
31
|
+
promptSnippet: "Abort one, many, or all active subagents from this session.",
|
|
32
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
33
|
+
const callerSessionId = ctx.sessionManager.getSessionId();
|
|
34
|
+
const modeCount = Number(Boolean(params.subagent_id))
|
|
35
|
+
+ Number(Boolean(params.subagent_ids?.length))
|
|
36
|
+
+ Number(params.all === true);
|
|
37
|
+
if (modeCount !== 1) {
|
|
38
|
+
return toolError("Provide exactly one of: subagent_id, subagent_ids, or all=true.");
|
|
39
|
+
}
|
|
40
|
+
if (params.all) {
|
|
41
|
+
const abortedIds = crewManager.abortAllOwned(callerSessionId, pi, {
|
|
42
|
+
reason: "Aborted by tool request",
|
|
43
|
+
});
|
|
44
|
+
if (abortedIds.length === 0) {
|
|
45
|
+
return toolError("No active subagents in the current session.");
|
|
46
|
+
}
|
|
47
|
+
return toolSuccess(`Aborted ${abortedIds.length} subagent(s): ${abortedIds.join(", ")}`, { ids: abortedIds });
|
|
48
|
+
}
|
|
49
|
+
const ids = params.subagent_id
|
|
50
|
+
? [params.subagent_id]
|
|
51
|
+
: (params.subagent_ids ?? []);
|
|
52
|
+
const result = crewManager.abortOwned(ids, callerSessionId, pi, {
|
|
53
|
+
reason: "Aborted by tool request",
|
|
54
|
+
});
|
|
55
|
+
const message = formatAbortToolMessage(result);
|
|
56
|
+
if (result.abortedIds.length === 0) {
|
|
57
|
+
return toolError(message || "No subagents were aborted.");
|
|
58
|
+
}
|
|
59
|
+
return toolSuccess(message, {
|
|
60
|
+
ids: result.abortedIds,
|
|
61
|
+
missing_ids: result.missingIds,
|
|
62
|
+
foreign_ids: result.foreignIds,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
renderCall(args, theme, _context) {
|
|
66
|
+
if (args.all) {
|
|
67
|
+
return renderCrewCall(theme, "crew_abort", "all");
|
|
68
|
+
}
|
|
69
|
+
if (args.subagent_id) {
|
|
70
|
+
return renderCrewCall(theme, "crew_abort", args.subagent_id);
|
|
71
|
+
}
|
|
72
|
+
const count = Array.isArray(args.subagent_ids) ? args.subagent_ids.length : 0;
|
|
73
|
+
return renderCrewCall(theme, "crew_abort", `${count} ids`);
|
|
74
|
+
},
|
|
75
|
+
renderResult(result, _options, theme, _context) {
|
|
76
|
+
return renderCrewResult(result, theme);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|