@melihmucuk/pi-crew 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/crew-manager.d.ts +2 -1
- package/dist/crew-manager.js +26 -3
- package/dist/integration/register-command.js +1 -1
- package/dist/integration/tools/crew-spawn.js +3 -3
- package/dist/runtime/overflow-recovery.d.ts +3 -0
- package/dist/runtime/overflow-recovery.js +155 -0
- package/docs/architecture.md +5 -5
- package/package.json +2 -2
- /package/prompts/{pi-crew:review.md → pi-crew-review.md} +0 -0
package/README.md
CHANGED
|
@@ -68,12 +68,12 @@ Closes an interactive subagent session owned by the current session when you no
|
|
|
68
68
|
"close planner-a1b2, the plan looks good"
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
### `/pi-crew
|
|
71
|
+
### `/pi-crew-abort`
|
|
72
72
|
|
|
73
73
|
Aborts a running subagent. Supports tab completion for subagent IDs.
|
|
74
74
|
Unlike the `crew_abort` tool, this command is intentionally unrestricted and works as an emergency escape hatch across sessions.
|
|
75
75
|
|
|
76
|
-
### `/pi-crew
|
|
76
|
+
### `/pi-crew-review`
|
|
77
77
|
|
|
78
78
|
Expands a bundled prompt template that orchestrates parallel code and quality reviews.
|
|
79
79
|
Use it to review recent commits, staged changes, unstaged changes, and untracked files with `code-reviewer` and `quality-reviewer`, then merge both results into one report.
|
package/dist/crew-manager.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { AgentConfig } from "./agent-discovery.js";
|
|
3
3
|
import { type AbortableAgentSummary, type ActiveAgentSummary } from "./runtime/subagent-state.js";
|
|
4
|
-
export type { AbortableAgentSummary, ActiveAgentSummary } from "./runtime/subagent-state.js";
|
|
4
|
+
export type { AbortableAgentSummary, ActiveAgentSummary, } from "./runtime/subagent-state.js";
|
|
5
5
|
export interface AbortOwnedResult {
|
|
6
6
|
abortedIds: string[];
|
|
7
7
|
missingIds: string[];
|
|
@@ -14,6 +14,7 @@ export declare class CrewManager {
|
|
|
14
14
|
private extensionResolvedPath;
|
|
15
15
|
private registry;
|
|
16
16
|
private delivery;
|
|
17
|
+
private overflowAbortControllers;
|
|
17
18
|
onWidgetUpdate: (() => void) | undefined;
|
|
18
19
|
constructor(extensionResolvedPath: string);
|
|
19
20
|
activateSession(sessionId: string, isIdle: () => boolean, pi: ExtensionAPI): void;
|
package/dist/crew-manager.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { bootstrapSession } from "./bootstrap-session.js";
|
|
2
2
|
import { DeliveryCoordinator } from "./runtime/delivery-coordinator.js";
|
|
3
|
+
import { runPromptWithOverflowRecovery } from "./runtime/overflow-recovery.js";
|
|
3
4
|
import { SubagentRegistry } from "./runtime/subagent-registry.js";
|
|
4
5
|
import { isAbortableStatus, isAborted, } from "./runtime/subagent-state.js";
|
|
5
6
|
function getLastAssistantMessage(messages) {
|
|
@@ -46,6 +47,7 @@ export class CrewManager {
|
|
|
46
47
|
extensionResolvedPath;
|
|
47
48
|
registry = new SubagentRegistry();
|
|
48
49
|
delivery = new DeliveryCoordinator();
|
|
50
|
+
overflowAbortControllers = new Map();
|
|
49
51
|
onWidgetUpdate;
|
|
50
52
|
constructor(extensionResolvedPath) {
|
|
51
53
|
this.extensionResolvedPath = extensionResolvedPath;
|
|
@@ -111,11 +113,21 @@ export class CrewManager {
|
|
|
111
113
|
async runPromptCycle(state, prompt, pi) {
|
|
112
114
|
if (isAborted(state))
|
|
113
115
|
return;
|
|
116
|
+
const abortController = new AbortController();
|
|
117
|
+
this.overflowAbortControllers.set(state.id, abortController);
|
|
114
118
|
try {
|
|
115
|
-
await state.session.
|
|
119
|
+
const recovery = await runPromptWithOverflowRecovery(state.session, prompt, abortController.signal);
|
|
116
120
|
if (isAborted(state))
|
|
117
121
|
return;
|
|
118
122
|
const outcome = getPromptOutcome(state);
|
|
123
|
+
// If overflow recovery ran but failed, keep the error from outcome.
|
|
124
|
+
// If it recovered, outcome now reflects the retry turn's result.
|
|
125
|
+
if (recovery === "failed" && outcome.status !== "error") {
|
|
126
|
+
this.settleAgent(state, "error", {
|
|
127
|
+
error: "Context overflow recovery failed",
|
|
128
|
+
}, pi);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
119
131
|
this.settleAgent(state, outcome.status, outcome, pi);
|
|
120
132
|
}
|
|
121
133
|
catch (err) {
|
|
@@ -124,6 +136,9 @@ export class CrewManager {
|
|
|
124
136
|
const error = err instanceof Error ? err.message : String(err);
|
|
125
137
|
this.settleAgent(state, "error", { error }, pi);
|
|
126
138
|
}
|
|
139
|
+
finally {
|
|
140
|
+
this.overflowAbortControllers.delete(state.id);
|
|
141
|
+
}
|
|
127
142
|
}
|
|
128
143
|
async spawnSession(state, cwd, parentSessionFile, ctx, pi) {
|
|
129
144
|
try {
|
|
@@ -159,7 +174,9 @@ export class CrewManager {
|
|
|
159
174
|
return { error: `Subagent "${id}" belongs to a different session` };
|
|
160
175
|
}
|
|
161
176
|
if (state.status !== "waiting") {
|
|
162
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
error: `Subagent "${id}" is not waiting for a response (status: ${state.status})`,
|
|
179
|
+
};
|
|
163
180
|
}
|
|
164
181
|
if (!state.session)
|
|
165
182
|
return { error: `Subagent "${id}" has no active session` };
|
|
@@ -185,6 +202,10 @@ export class CrewManager {
|
|
|
185
202
|
const state = this.registry.get(id);
|
|
186
203
|
if (!state || !isAbortableStatus(state.status))
|
|
187
204
|
return false;
|
|
205
|
+
this.overflowAbortControllers.get(id)?.abort();
|
|
206
|
+
this.overflowAbortControllers.delete(id);
|
|
207
|
+
state.session?.abortCompaction();
|
|
208
|
+
state.session?.abortRetry();
|
|
188
209
|
state.session?.abort().catch(() => { });
|
|
189
210
|
this.settleAgent(state, "aborted", { error: opts.reason }, pi);
|
|
190
211
|
return true;
|
|
@@ -223,7 +244,9 @@ export class CrewManager {
|
|
|
223
244
|
return ids;
|
|
224
245
|
}
|
|
225
246
|
abortForOwner(ownerSessionId, pi) {
|
|
226
|
-
this.abortAllOwned(ownerSessionId, pi, {
|
|
247
|
+
this.abortAllOwned(ownerSessionId, pi, {
|
|
248
|
+
reason: "Aborted on session shutdown",
|
|
249
|
+
});
|
|
227
250
|
this.delivery.clearPendingForOwner(ownerSessionId);
|
|
228
251
|
}
|
|
229
252
|
getAbortableAgents() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function registerCrewCommand(pi, crewManager) {
|
|
2
|
-
pi.registerCommand("pi-crew
|
|
2
|
+
pi.registerCommand("pi-crew-abort", {
|
|
3
3
|
description: "Abort an active subagent",
|
|
4
4
|
getArgumentCompletions(argumentPrefix) {
|
|
5
5
|
const activeAgents = crewManager.getAbortableAgents();
|
|
@@ -5,7 +5,7 @@ export function registerCrewSpawnTool({ pi, crewManager, notifyDiscoveryWarnings
|
|
|
5
5
|
pi.registerTool({
|
|
6
6
|
name: "crew_spawn",
|
|
7
7
|
label: "Spawn Crew",
|
|
8
|
-
description: "Spawn a non-blocking subagent that runs in an isolated session. The subagent works independently while
|
|
8
|
+
description: "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 when done. NEVER PREDICT or FABRICATE results for subagents that have not yet reported back to you. Use crew_list first to see available subagents.",
|
|
9
9
|
parameters: Type.Object({
|
|
10
10
|
subagent: Type.String({ description: "Subagent name from crew_list" }),
|
|
11
11
|
task: Type.String({ description: "Task to delegate to the subagent" }),
|
|
@@ -14,10 +14,10 @@ export function registerCrewSpawnTool({ pi, crewManager, notifyDiscoveryWarnings
|
|
|
14
14
|
promptGuidelines: [
|
|
15
15
|
"Use crew_* tools to delegate parallelizable, independent tasks to specialized subagents. For interactive multi-turn workflows, use crew_respond/crew_done. Avoid spawning for trivial, single-turn tasks.",
|
|
16
16
|
"crew_spawn: Always call crew_list first to see which subagents are available before spawning.",
|
|
17
|
-
"crew_spawn: The spawned subagent runs in a separate context window with no access to
|
|
17
|
+
"crew_spawn: The spawned subagent runs in a separate context window with no access to your session. Include all relevant context (file paths, requirements, prior findings) directly in the task parameter.",
|
|
18
18
|
"crew_spawn: Results are delivered asynchronously as steering messages. Do not block or poll for completion. If there are other independent tasks to handle, continue with those; otherwise wait for the user's next instruction or the subagent result.",
|
|
19
19
|
"crew_spawn: NEVER perform the same work you delegated to a subagent. Once a task is spawned, trust the subagent to do it. Do not run the same searches, reads, or analysis yourself while waiting. You may only gather context BEFORE spawning to prepare the task description. After spawning, move on to other independent work or simply wait for the result.",
|
|
20
|
-
"crew_spawn: When multiple subagents are spawned, each result arrives as a separate steering message. NEVER
|
|
20
|
+
"crew_spawn: When multiple subagents are spawned, each result arrives as a separate steering message. NEVER PREDICT or FABRICATE results for subagents that have not yet reported back to you. Wait for ALL crew-result messages.",
|
|
21
21
|
"crew_spawn: Interactive subagents (marked with 'interactive' in crew_list) stay alive after responding. Use crew_respond to continue the conversation and crew_done to close when finished.",
|
|
22
22
|
],
|
|
23
23
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
export type OverflowRecoveryResult = "none" | "recovered" | "failed";
|
|
3
|
+
export declare function runPromptWithOverflowRecovery(session: AgentSession, text: string, signal: AbortSignal): Promise<OverflowRecoveryResult>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const OVERFLOW_RECOVERY_TIMEOUT_MS = 120_000;
|
|
2
|
+
/**
|
|
3
|
+
* Short grace period for the first terminal agent_end after prompt() resolves.
|
|
4
|
+
* If this window expires, we still wait the full recovery timeout.
|
|
5
|
+
*/
|
|
6
|
+
const INITIAL_AGENT_END_WAIT_MS = 5_000;
|
|
7
|
+
function createDeferredPhase() {
|
|
8
|
+
let done = false;
|
|
9
|
+
let resolveFn;
|
|
10
|
+
const promise = new Promise((resolve) => {
|
|
11
|
+
resolveFn = () => {
|
|
12
|
+
if (done)
|
|
13
|
+
return;
|
|
14
|
+
done = true;
|
|
15
|
+
resolve();
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
promise,
|
|
20
|
+
resolve: () => resolveFn?.(),
|
|
21
|
+
isDone: () => done,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
class OverflowRecoveryTracker {
|
|
25
|
+
overflowDetected = false;
|
|
26
|
+
compactionWillRetry = false;
|
|
27
|
+
autoRetryActive = false;
|
|
28
|
+
initialAgentEnd = createDeferredPhase();
|
|
29
|
+
compactionEnd;
|
|
30
|
+
retryAgentEnd;
|
|
31
|
+
overflowAutoRetryEnd;
|
|
32
|
+
timers = [];
|
|
33
|
+
handleEvent(event) {
|
|
34
|
+
switch (event.type) {
|
|
35
|
+
case "agent_end":
|
|
36
|
+
this.onAgentEnd();
|
|
37
|
+
break;
|
|
38
|
+
case "compaction_start":
|
|
39
|
+
this.onCompactionStart(event.reason);
|
|
40
|
+
break;
|
|
41
|
+
case "compaction_end":
|
|
42
|
+
this.onCompactionEnd(event.reason, event.willRetry);
|
|
43
|
+
break;
|
|
44
|
+
case "auto_retry_start":
|
|
45
|
+
this.onAutoRetryStart();
|
|
46
|
+
break;
|
|
47
|
+
case "auto_retry_end":
|
|
48
|
+
this.onAutoRetryEnd();
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async awaitCompletion(signal) {
|
|
55
|
+
const cancelPromise = new Promise((resolve) => {
|
|
56
|
+
if (signal.aborted) {
|
|
57
|
+
resolve();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
let initialEnd = await this.waitForPhase(this.initialAgentEnd.promise, INITIAL_AGENT_END_WAIT_MS, cancelPromise);
|
|
64
|
+
if (initialEnd === "timeout") {
|
|
65
|
+
initialEnd = await this.waitForPhase(this.initialAgentEnd.promise, OVERFLOW_RECOVERY_TIMEOUT_MS, cancelPromise);
|
|
66
|
+
}
|
|
67
|
+
if (initialEnd !== "done") {
|
|
68
|
+
return this.overflowDetected ? "failed" : "none";
|
|
69
|
+
}
|
|
70
|
+
if (!this.overflowDetected)
|
|
71
|
+
return "none";
|
|
72
|
+
if (this.compactionEnd) {
|
|
73
|
+
const compactionEnd = await this.waitForPhase(this.compactionEnd.promise, OVERFLOW_RECOVERY_TIMEOUT_MS, cancelPromise);
|
|
74
|
+
if (compactionEnd !== "done")
|
|
75
|
+
return "failed";
|
|
76
|
+
}
|
|
77
|
+
if (!this.compactionWillRetry)
|
|
78
|
+
return "failed";
|
|
79
|
+
if (this.retryAgentEnd) {
|
|
80
|
+
const retryEnd = await this.waitForPhase(this.retryAgentEnd.promise, OVERFLOW_RECOVERY_TIMEOUT_MS, cancelPromise);
|
|
81
|
+
if (retryEnd !== "done")
|
|
82
|
+
return "failed";
|
|
83
|
+
}
|
|
84
|
+
if (this.overflowAutoRetryEnd) {
|
|
85
|
+
const autoRetryEnd = await this.waitForPhase(this.overflowAutoRetryEnd.promise, OVERFLOW_RECOVERY_TIMEOUT_MS, cancelPromise);
|
|
86
|
+
if (autoRetryEnd !== "done")
|
|
87
|
+
return "failed";
|
|
88
|
+
}
|
|
89
|
+
return "recovered";
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
for (const timer of this.timers)
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async waitForPhase(phasePromise, timeoutMs, cancelPromise) {
|
|
97
|
+
return Promise.race([
|
|
98
|
+
phasePromise.then(() => "done"),
|
|
99
|
+
cancelPromise.then(() => "cancelled"),
|
|
100
|
+
new Promise((resolve) => {
|
|
101
|
+
this.timers.push(setTimeout(() => resolve("timeout"), timeoutMs));
|
|
102
|
+
}),
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
// agent_end can be followed immediately by auto_retry_start in the same
|
|
106
|
+
// _processAgentEvent tick. Resolve on microtask so we can ignore retrying
|
|
107
|
+
// attempts and only accept terminal agent_end events.
|
|
108
|
+
onAgentEnd() {
|
|
109
|
+
queueMicrotask(() => {
|
|
110
|
+
if (this.autoRetryActive)
|
|
111
|
+
return;
|
|
112
|
+
if (!this.initialAgentEnd.isDone()) {
|
|
113
|
+
this.initialAgentEnd.resolve();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.retryAgentEnd?.resolve();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
onCompactionStart(reason) {
|
|
120
|
+
if (reason !== "overflow")
|
|
121
|
+
return;
|
|
122
|
+
this.overflowDetected = true;
|
|
123
|
+
this.compactionEnd ??= createDeferredPhase();
|
|
124
|
+
}
|
|
125
|
+
onCompactionEnd(reason, willRetry) {
|
|
126
|
+
if (reason !== "overflow")
|
|
127
|
+
return;
|
|
128
|
+
this.compactionWillRetry = willRetry;
|
|
129
|
+
if (willRetry) {
|
|
130
|
+
this.retryAgentEnd ??= createDeferredPhase();
|
|
131
|
+
}
|
|
132
|
+
this.compactionEnd?.resolve();
|
|
133
|
+
}
|
|
134
|
+
onAutoRetryStart() {
|
|
135
|
+
this.autoRetryActive = true;
|
|
136
|
+
if (this.overflowDetected) {
|
|
137
|
+
this.overflowAutoRetryEnd ??= createDeferredPhase();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
onAutoRetryEnd() {
|
|
141
|
+
this.autoRetryActive = false;
|
|
142
|
+
this.overflowAutoRetryEnd?.resolve();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export async function runPromptWithOverflowRecovery(session, text, signal) {
|
|
146
|
+
const tracker = new OverflowRecoveryTracker();
|
|
147
|
+
const unsubscribe = session.subscribe((event) => tracker.handleEvent(event));
|
|
148
|
+
try {
|
|
149
|
+
await session.prompt(text);
|
|
150
|
+
return await tracker.awaitCompletion(signal);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
unsubscribe();
|
|
154
|
+
}
|
|
155
|
+
}
|
package/docs/architecture.md
CHANGED
|
@@ -199,7 +199,7 @@ File:
|
|
|
199
199
|
|
|
200
200
|
- `extension/integration/register-command.ts`
|
|
201
201
|
|
|
202
|
-
The extension also registers the `/pi-crew
|
|
202
|
+
The extension also registers the `/pi-crew-abort` command.
|
|
203
203
|
|
|
204
204
|
This command differs from `crew_abort` in one important way:
|
|
205
205
|
|
|
@@ -616,7 +616,7 @@ That last point is essential. The latest subagent response was already delivered
|
|
|
616
616
|
There are three conceptually different abort sources:
|
|
617
617
|
|
|
618
618
|
1. tool-triggered aborts through `crew_abort`
|
|
619
|
-
2. unrestricted manual aborts through `/pi-crew
|
|
619
|
+
2. unrestricted manual aborts through `/pi-crew-abort`
|
|
620
620
|
3. cleanup aborts when an owner session shuts down
|
|
621
621
|
|
|
622
622
|
Each path should report the real reason.
|
|
@@ -648,7 +648,7 @@ Relevant file:
|
|
|
648
648
|
|
|
649
649
|
- `extension/integration/register-command.ts`
|
|
650
650
|
|
|
651
|
-
`/pi-crew
|
|
651
|
+
`/pi-crew-abort` can target any active abortable subagent, regardless of owner. This is not a bug. It is an explicit operational decision.
|
|
652
652
|
|
|
653
653
|
### 11.4 Session shutdown cleanup
|
|
654
654
|
|
|
@@ -691,7 +691,7 @@ This prevents cross-session interference in normal tool-driven workflows.
|
|
|
691
691
|
|
|
692
692
|
### 12.3 What is intentionally not isolated
|
|
693
693
|
|
|
694
|
-
The emergency command `/pi-crew
|
|
694
|
+
The emergency command `/pi-crew-abort` is intentionally cross-session.
|
|
695
695
|
|
|
696
696
|
This is the only major exception to normal ownership isolation.
|
|
697
697
|
|
|
@@ -752,7 +752,7 @@ Architecturally, these files are not special-cased by the runtime. They are auto
|
|
|
752
752
|
|
|
753
753
|
File:
|
|
754
754
|
|
|
755
|
-
- `prompts/pi-crew
|
|
755
|
+
- `prompts/pi-crew-review.md`
|
|
756
756
|
|
|
757
757
|
This prompt template is a good example of how `pi-crew` is meant to be consumed by higher-level orchestration prompts.
|
|
758
758
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@melihmucuk/pi-crew",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Non-blocking subagent orchestration for pi coding agent",
|
|
6
6
|
"files": [
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"@types/node": "^22.19.3",
|
|
50
50
|
"typescript": "^5.9.3"
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
}
|
|
File without changes
|