@melihmucuk/pi-crew 1.0.13 → 1.0.15
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 +19 -18
- package/agents/code-reviewer.md +52 -104
- package/agents/oracle.md +26 -52
- package/agents/planner.md +7 -7
- package/agents/quality-reviewer.md +90 -131
- package/agents/scout.md +3 -2
- package/agents/worker.md +8 -2
- package/extension/agent-discovery.ts +791 -0
- package/extension/bootstrap-session.ts +131 -0
- package/extension/index.ts +63 -0
- package/extension/integration/register-renderers.ts +77 -0
- package/extension/integration/register-tools.ts +39 -0
- package/extension/integration/tool-presentation.ts +50 -0
- package/extension/integration/tools/crew-abort.ts +126 -0
- package/extension/integration/tools/crew-done.ts +46 -0
- package/extension/integration/tools/crew-list.ts +92 -0
- package/extension/integration/tools/crew-respond.ts +59 -0
- package/extension/integration/tools/crew-spawn.ts +87 -0
- package/extension/integration/tools/tool-deps.ts +16 -0
- package/extension/integration.ts +13 -0
- package/extension/runtime/crew-runtime.ts +426 -0
- package/extension/runtime/delivery-coordinator.ts +131 -0
- package/extension/runtime/overflow-recovery.ts +211 -0
- package/extension/runtime/subagent-registry.ts +78 -0
- package/extension/runtime/subagent-state.ts +59 -0
- package/extension/status-widget.ts +107 -0
- package/extension/subagent-messages.ts +124 -0
- package/extension/tool-registry.ts +19 -0
- package/package.json +14 -14
- package/prompts/pi-crew-plan.md +46 -37
- package/prompts/pi-crew-review.md +3 -1
- package/skills/pi-crew/SKILL.md +129 -0
- package/dist/agent-discovery.d.ts +0 -29
- package/dist/agent-discovery.js +0 -527
- package/dist/bootstrap-session.d.ts +0 -21
- package/dist/bootstrap-session.js +0 -74
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -46
- package/dist/integration/register-command.d.ts +0 -3
- package/dist/integration/register-command.js +0 -51
- package/dist/integration/register-renderers.d.ts +0 -2
- package/dist/integration/register-renderers.js +0 -59
- package/dist/integration/register-tools.d.ts +0 -3
- package/dist/integration/register-tools.js +0 -25
- package/dist/integration/tool-presentation.d.ts +0 -27
- package/dist/integration/tool-presentation.js +0 -29
- package/dist/integration/tools/crew-abort.d.ts +0 -2
- package/dist/integration/tools/crew-abort.js +0 -79
- package/dist/integration/tools/crew-done.d.ts +0 -2
- package/dist/integration/tools/crew-done.js +0 -28
- package/dist/integration/tools/crew-list.d.ts +0 -2
- package/dist/integration/tools/crew-list.js +0 -74
- package/dist/integration/tools/crew-respond.d.ts +0 -2
- package/dist/integration/tools/crew-respond.js +0 -32
- package/dist/integration/tools/crew-spawn.d.ts +0 -2
- package/dist/integration/tools/crew-spawn.js +0 -48
- package/dist/integration/tools/tool-deps.d.ts +0 -9
- package/dist/integration/tools/tool-deps.js +0 -1
- package/dist/integration.d.ts +0 -3
- package/dist/integration.js +0 -8
- package/dist/runtime/crew-runtime.d.ts +0 -62
- package/dist/runtime/crew-runtime.js +0 -285
- package/dist/runtime/delivery-coordinator.d.ts +0 -26
- package/dist/runtime/delivery-coordinator.js +0 -86
- package/dist/runtime/overflow-recovery.d.ts +0 -3
- package/dist/runtime/overflow-recovery.js +0 -155
- package/dist/runtime/subagent-registry.d.ts +0 -14
- package/dist/runtime/subagent-registry.js +0 -58
- package/dist/runtime/subagent-state.d.ts +0 -35
- package/dist/runtime/subagent-state.js +0 -32
- package/dist/status-widget.d.ts +0 -3
- package/dist/status-widget.js +0 -84
- package/dist/subagent-messages.d.ts +0 -37
- package/dist/subagent-messages.js +0 -68
- package/dist/tool-registry.d.ts +0 -5
- package/dist/tool-registry.js +0 -13
- package/docs/architecture.md +0 -187
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import type { AgentSession, AgentSessionEvent } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const OVERFLOW_RECOVERY_TIMEOUT_MS = 120_000;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Short grace period for the first terminal agent_end after prompt() resolves.
|
|
7
|
+
* If this window expires, we still wait the full recovery timeout.
|
|
8
|
+
*/
|
|
9
|
+
const INITIAL_AGENT_END_WAIT_MS = 5_000;
|
|
10
|
+
|
|
11
|
+
type PhaseWaitResult = "done" | "timeout" | "cancelled";
|
|
12
|
+
|
|
13
|
+
export type OverflowRecoveryResult = "none" | "recovered" | "failed";
|
|
14
|
+
|
|
15
|
+
interface DeferredPhase {
|
|
16
|
+
promise: Promise<void>;
|
|
17
|
+
resolve: () => void;
|
|
18
|
+
isDone: () => boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createDeferredPhase(): DeferredPhase {
|
|
22
|
+
let done = false;
|
|
23
|
+
let resolveFn: (() => void) | undefined;
|
|
24
|
+
|
|
25
|
+
const promise = new Promise<void>((resolve) => {
|
|
26
|
+
resolveFn = () => {
|
|
27
|
+
if (done) return;
|
|
28
|
+
done = true;
|
|
29
|
+
resolve();
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
promise,
|
|
35
|
+
resolve: () => resolveFn?.(),
|
|
36
|
+
isDone: () => done,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class OverflowRecoveryTracker {
|
|
41
|
+
private overflowDetected = false;
|
|
42
|
+
private compactionWillRetry = false;
|
|
43
|
+
|
|
44
|
+
private autoRetryActive = false;
|
|
45
|
+
private readonly initialAgentEnd = createDeferredPhase();
|
|
46
|
+
private compactionEnd: DeferredPhase | undefined;
|
|
47
|
+
private retryAgentEnd: DeferredPhase | undefined;
|
|
48
|
+
private overflowAutoRetryEnd: DeferredPhase | undefined;
|
|
49
|
+
private timers: ReturnType<typeof setTimeout>[] = [];
|
|
50
|
+
|
|
51
|
+
handleEvent(event: AgentSessionEvent): void {
|
|
52
|
+
switch (event.type) {
|
|
53
|
+
case "agent_end":
|
|
54
|
+
this.onAgentEnd();
|
|
55
|
+
break;
|
|
56
|
+
case "compaction_start":
|
|
57
|
+
this.onCompactionStart(event.reason);
|
|
58
|
+
break;
|
|
59
|
+
case "compaction_end":
|
|
60
|
+
this.onCompactionEnd(event.reason, event.willRetry);
|
|
61
|
+
break;
|
|
62
|
+
case "auto_retry_start":
|
|
63
|
+
this.onAutoRetryStart();
|
|
64
|
+
break;
|
|
65
|
+
case "auto_retry_end":
|
|
66
|
+
this.onAutoRetryEnd();
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async awaitCompletion(signal: AbortSignal): Promise<OverflowRecoveryResult> {
|
|
74
|
+
const cancelPromise = new Promise<void>((resolve) => {
|
|
75
|
+
if (signal.aborted) {
|
|
76
|
+
resolve();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
let initialEnd = await this.waitForPhase(
|
|
84
|
+
this.initialAgentEnd.promise,
|
|
85
|
+
INITIAL_AGENT_END_WAIT_MS,
|
|
86
|
+
cancelPromise,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (initialEnd === "timeout") {
|
|
90
|
+
initialEnd = await this.waitForPhase(
|
|
91
|
+
this.initialAgentEnd.promise,
|
|
92
|
+
OVERFLOW_RECOVERY_TIMEOUT_MS,
|
|
93
|
+
cancelPromise,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (initialEnd !== "done") {
|
|
98
|
+
return this.overflowDetected ? "failed" : "none";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!this.overflowDetected) return "none";
|
|
102
|
+
|
|
103
|
+
if (this.compactionEnd) {
|
|
104
|
+
const compactionEnd = await this.waitForPhase(
|
|
105
|
+
this.compactionEnd.promise,
|
|
106
|
+
OVERFLOW_RECOVERY_TIMEOUT_MS,
|
|
107
|
+
cancelPromise,
|
|
108
|
+
);
|
|
109
|
+
if (compactionEnd !== "done") return "failed";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!this.compactionWillRetry) return "failed";
|
|
113
|
+
|
|
114
|
+
if (this.retryAgentEnd) {
|
|
115
|
+
const retryEnd = await this.waitForPhase(
|
|
116
|
+
this.retryAgentEnd.promise,
|
|
117
|
+
OVERFLOW_RECOVERY_TIMEOUT_MS,
|
|
118
|
+
cancelPromise,
|
|
119
|
+
);
|
|
120
|
+
if (retryEnd !== "done") return "failed";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (this.overflowAutoRetryEnd) {
|
|
124
|
+
const autoRetryEnd = await this.waitForPhase(
|
|
125
|
+
this.overflowAutoRetryEnd.promise,
|
|
126
|
+
OVERFLOW_RECOVERY_TIMEOUT_MS,
|
|
127
|
+
cancelPromise,
|
|
128
|
+
);
|
|
129
|
+
if (autoRetryEnd !== "done") return "failed";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return "recovered";
|
|
133
|
+
} finally {
|
|
134
|
+
for (const timer of this.timers) clearTimeout(timer);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async waitForPhase(
|
|
139
|
+
phasePromise: Promise<void>,
|
|
140
|
+
timeoutMs: number,
|
|
141
|
+
cancelPromise: Promise<void>,
|
|
142
|
+
): Promise<PhaseWaitResult> {
|
|
143
|
+
return Promise.race([
|
|
144
|
+
phasePromise.then(() => "done" as const),
|
|
145
|
+
cancelPromise.then(() => "cancelled" as const),
|
|
146
|
+
new Promise<"timeout">((resolve) => {
|
|
147
|
+
this.timers.push(setTimeout(() => resolve("timeout"), timeoutMs));
|
|
148
|
+
}),
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// agent_end can be followed immediately by auto_retry_start in the same
|
|
153
|
+
// _processAgentEvent tick. Resolve on microtask so we can ignore retrying
|
|
154
|
+
// attempts and only accept terminal agent_end events.
|
|
155
|
+
private onAgentEnd(): void {
|
|
156
|
+
queueMicrotask(() => {
|
|
157
|
+
if (this.autoRetryActive) return;
|
|
158
|
+
|
|
159
|
+
if (!this.initialAgentEnd.isDone()) {
|
|
160
|
+
this.initialAgentEnd.resolve();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.retryAgentEnd?.resolve();
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private onCompactionStart(reason: "manual" | "threshold" | "overflow"): void {
|
|
169
|
+
if (reason !== "overflow") return;
|
|
170
|
+
this.overflowDetected = true;
|
|
171
|
+
this.compactionEnd ??= createDeferredPhase();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private onCompactionEnd(reason: "manual" | "threshold" | "overflow", willRetry: boolean): void {
|
|
175
|
+
if (reason !== "overflow") return;
|
|
176
|
+
|
|
177
|
+
this.compactionWillRetry = willRetry;
|
|
178
|
+
if (willRetry) {
|
|
179
|
+
this.retryAgentEnd ??= createDeferredPhase();
|
|
180
|
+
}
|
|
181
|
+
this.compactionEnd?.resolve();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private onAutoRetryStart(): void {
|
|
185
|
+
this.autoRetryActive = true;
|
|
186
|
+
if (this.overflowDetected) {
|
|
187
|
+
this.overflowAutoRetryEnd ??= createDeferredPhase();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private onAutoRetryEnd(): void {
|
|
192
|
+
this.autoRetryActive = false;
|
|
193
|
+
this.overflowAutoRetryEnd?.resolve();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function runPromptWithOverflowRecovery(
|
|
198
|
+
session: AgentSession,
|
|
199
|
+
text: string,
|
|
200
|
+
signal: AbortSignal,
|
|
201
|
+
): Promise<OverflowRecoveryResult> {
|
|
202
|
+
const tracker = new OverflowRecoveryTracker();
|
|
203
|
+
const unsubscribe = session.subscribe((event) => tracker.handleEvent(event));
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
await session.prompt(text);
|
|
207
|
+
return await tracker.awaitCompletion(signal);
|
|
208
|
+
} finally {
|
|
209
|
+
unsubscribe();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AgentConfig } from "../agent-discovery.js";
|
|
2
|
+
import type { ActiveAgentSummary, SubagentState } from "./subagent-state.js";
|
|
3
|
+
import {
|
|
4
|
+
buildActiveAgentSummary,
|
|
5
|
+
generateId,
|
|
6
|
+
isAbortableStatus,
|
|
7
|
+
} from "./subagent-state.js";
|
|
8
|
+
|
|
9
|
+
export class SubagentRegistry {
|
|
10
|
+
private activeAgents = new Map<string, SubagentState>();
|
|
11
|
+
|
|
12
|
+
create(agentConfig: AgentConfig, task: string, ownerSessionId: string): SubagentState {
|
|
13
|
+
const id = generateId(agentConfig.name, new Set(this.activeAgents.keys()));
|
|
14
|
+
const state: SubagentState = {
|
|
15
|
+
id,
|
|
16
|
+
agentConfig,
|
|
17
|
+
task,
|
|
18
|
+
status: "running",
|
|
19
|
+
ownerSessionId,
|
|
20
|
+
session: null,
|
|
21
|
+
turns: 0,
|
|
22
|
+
contextTokens: 0,
|
|
23
|
+
model: undefined,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
this.activeAgents.set(id, state);
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get(id: string): SubagentState | undefined {
|
|
31
|
+
return this.activeAgents.get(id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
hasState(state: SubagentState): boolean {
|
|
35
|
+
return this.activeAgents.get(state.id) === state;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
delete(id: string): void {
|
|
39
|
+
this.activeAgents.delete(id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
countRunningForOwner(ownerSessionId: string, excludeId: string): number {
|
|
43
|
+
let count = 0;
|
|
44
|
+
for (const state of this.activeAgents.values()) {
|
|
45
|
+
if (
|
|
46
|
+
state.id !== excludeId &&
|
|
47
|
+
state.ownerSessionId === ownerSessionId &&
|
|
48
|
+
state.status === "running"
|
|
49
|
+
) {
|
|
50
|
+
count++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return count;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getActiveSummariesForOwner(ownerSessionId: string): ActiveAgentSummary[] {
|
|
57
|
+
return Array.from(this.activeAgents.values())
|
|
58
|
+
.filter(
|
|
59
|
+
(state) => isAbortableStatus(state.status) && state.ownerSessionId === ownerSessionId,
|
|
60
|
+
)
|
|
61
|
+
.map(buildActiveAgentSummary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getOwnedAbortableIds(ownerSessionId: string): string[] {
|
|
65
|
+
return Array.from(this.activeAgents.values())
|
|
66
|
+
.filter(
|
|
67
|
+
(state) =>
|
|
68
|
+
state.ownerSessionId === ownerSessionId && isAbortableStatus(state.status),
|
|
69
|
+
)
|
|
70
|
+
.map((state) => state.id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
getAllAbortable(): SubagentState[] {
|
|
74
|
+
return Array.from(this.activeAgents.values()).filter((state) =>
|
|
75
|
+
isAbortableStatus(state.status),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import type { AgentConfig } from "../agent-discovery.js";
|
|
4
|
+
import type { SubagentStatus } from "../subagent-messages.js";
|
|
5
|
+
|
|
6
|
+
export interface SubagentState {
|
|
7
|
+
id: string;
|
|
8
|
+
agentConfig: AgentConfig;
|
|
9
|
+
task: string;
|
|
10
|
+
status: SubagentStatus;
|
|
11
|
+
ownerSessionId: string;
|
|
12
|
+
session: AgentSession | null;
|
|
13
|
+
turns: number;
|
|
14
|
+
contextTokens: number;
|
|
15
|
+
model: string | undefined;
|
|
16
|
+
error?: string;
|
|
17
|
+
result?: string;
|
|
18
|
+
promptAbortController?: AbortController;
|
|
19
|
+
unsubscribe?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ActiveAgentSummary {
|
|
23
|
+
id: string;
|
|
24
|
+
agentName: string;
|
|
25
|
+
status: SubagentStatus;
|
|
26
|
+
turns: number;
|
|
27
|
+
contextTokens: number;
|
|
28
|
+
model: string | undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function generateId(name: string, existingIds: Set<string>): string {
|
|
32
|
+
for (let i = 0; i < 10; i++) {
|
|
33
|
+
const id = `${name}-${randomBytes(4).toString("hex")}`;
|
|
34
|
+
if (!existingIds.has(id)) return id;
|
|
35
|
+
}
|
|
36
|
+
return `${name}-${randomBytes(8).toString("hex")}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Status may change externally via abort(). Standalone function avoids TS narrowing.
|
|
40
|
+
export function isAborted(state: SubagentState): boolean {
|
|
41
|
+
return state.status === "aborted";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isAbortableStatus(status: SubagentStatus): boolean {
|
|
45
|
+
return status === "running" || status === "waiting";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildActiveAgentSummary(
|
|
49
|
+
state: SubagentState,
|
|
50
|
+
): ActiveAgentSummary {
|
|
51
|
+
return {
|
|
52
|
+
id: state.id,
|
|
53
|
+
agentName: state.agentConfig.name,
|
|
54
|
+
status: state.status,
|
|
55
|
+
turns: state.turns,
|
|
56
|
+
contextTokens: state.contextTokens,
|
|
57
|
+
model: state.model,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
3
|
+
import type { ActiveAgentSummary } from "./runtime/crew-runtime.js";
|
|
4
|
+
import type { CrewRuntime } from "./runtime/crew-runtime.js";
|
|
5
|
+
|
|
6
|
+
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
+
const SPINNER_INTERVAL_MS = 80;
|
|
8
|
+
|
|
9
|
+
function formatTokens(tokens: number): string {
|
|
10
|
+
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
|
|
11
|
+
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
12
|
+
return String(tokens);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function buildLine(agent: ActiveAgentSummary, frame: string): string {
|
|
16
|
+
const model = agent.model ?? "…";
|
|
17
|
+
const icon = agent.status === "waiting" ? "⏳" : frame;
|
|
18
|
+
return `${icon} ${agent.id} (${model}) · turn ${agent.turns} · ${formatTokens(agent.contextTokens)} ctx`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface WidgetState {
|
|
22
|
+
ctx: ExtensionContext;
|
|
23
|
+
text: Text;
|
|
24
|
+
// biome-ignore lint: TUI type from factory param
|
|
25
|
+
tui: any;
|
|
26
|
+
timer: ReturnType<typeof setInterval>;
|
|
27
|
+
frameIndex: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let widget: WidgetState | undefined;
|
|
31
|
+
|
|
32
|
+
function disposeWidget(state: WidgetState): void {
|
|
33
|
+
clearInterval(state.timer);
|
|
34
|
+
if (widget === state) {
|
|
35
|
+
widget = undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clearWidget(): void {
|
|
40
|
+
const current = widget;
|
|
41
|
+
if (!current) return;
|
|
42
|
+
disposeWidget(current);
|
|
43
|
+
current.ctx.ui.setWidget("crew-status", undefined);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function hasRunningAgent(agents: ActiveAgentSummary[]): boolean {
|
|
47
|
+
return agents.some((agent) => agent.status === "running");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function syncWidgetText(state: WidgetState, agents: ActiveAgentSummary[]): void {
|
|
51
|
+
const frame = SPINNER_FRAMES[state.frameIndex % SPINNER_FRAMES.length];
|
|
52
|
+
const lines = agents.map((agent) => buildLine(agent, frame));
|
|
53
|
+
state.text.setText(lines.join("\n"));
|
|
54
|
+
state.tui.requestRender();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function updateWidget(ctx: ExtensionContext, crew: CrewRuntime): void {
|
|
58
|
+
if (!ctx.hasUI) {
|
|
59
|
+
clearWidget();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ownerSessionId = ctx.sessionManager.getSessionId();
|
|
64
|
+
const running = crew.getActiveSummariesForOwner(ownerSessionId);
|
|
65
|
+
if (running.length === 0) {
|
|
66
|
+
clearWidget();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (widget && widget.ctx !== ctx) {
|
|
71
|
+
clearWidget();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (widget) {
|
|
75
|
+
syncWidgetText(widget, running);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
ctx.ui.setWidget("crew-status", (tui, _theme) => {
|
|
80
|
+
const text = new Text("", 1, 0);
|
|
81
|
+
const state: WidgetState = {
|
|
82
|
+
ctx,
|
|
83
|
+
text,
|
|
84
|
+
tui,
|
|
85
|
+
frameIndex: 0,
|
|
86
|
+
timer: setInterval(() => {
|
|
87
|
+
const agents = crew.getActiveSummariesForOwner(ownerSessionId);
|
|
88
|
+
if (agents.length === 0) {
|
|
89
|
+
clearWidget();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!hasRunningAgent(agents)) return;
|
|
93
|
+
state.frameIndex++;
|
|
94
|
+
syncWidgetText(state, agents);
|
|
95
|
+
}, SPINNER_INTERVAL_MS),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
widget = state;
|
|
99
|
+
syncWidgetText(state, running);
|
|
100
|
+
|
|
101
|
+
return Object.assign(text, {
|
|
102
|
+
dispose() {
|
|
103
|
+
disposeWidget(state);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export type SubagentStatus = "running" | "waiting" | "done" | "error" | "aborted";
|
|
4
|
+
|
|
5
|
+
export type SendMessageFn = ExtensionAPI["sendMessage"];
|
|
6
|
+
|
|
7
|
+
export const STATUS_ICON: Record<SubagentStatus, string> = {
|
|
8
|
+
running: "⏳",
|
|
9
|
+
waiting: "⏳",
|
|
10
|
+
done: "✅",
|
|
11
|
+
error: "❌",
|
|
12
|
+
aborted: "⏹️",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const STATUS_LABEL: Record<SubagentStatus, string> = {
|
|
16
|
+
running: "running",
|
|
17
|
+
waiting: "waiting for response",
|
|
18
|
+
done: "done",
|
|
19
|
+
error: "failed",
|
|
20
|
+
aborted: "aborted",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface SteeringPayload {
|
|
24
|
+
id: string;
|
|
25
|
+
agentName: string;
|
|
26
|
+
sessionFile?: string;
|
|
27
|
+
status: SubagentStatus;
|
|
28
|
+
result?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CrewResultMessageDetails {
|
|
33
|
+
agentId: string;
|
|
34
|
+
agentName: string;
|
|
35
|
+
sessionFile?: string;
|
|
36
|
+
status: SubagentStatus;
|
|
37
|
+
body?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getCrewResultTitle(details: {
|
|
41
|
+
agentId: string;
|
|
42
|
+
agentName: string;
|
|
43
|
+
status: SubagentStatus;
|
|
44
|
+
}): string {
|
|
45
|
+
return `Subagent '${details.agentName}' (${details.agentId}) ${STATUS_LABEL[details.status]}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getSteeringBody(payload: SteeringPayload): string | undefined {
|
|
49
|
+
return (payload.status === "error" || payload.status === "aborted")
|
|
50
|
+
? (payload.error ?? payload.result)
|
|
51
|
+
: (payload.result ?? payload.error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function sendSteeringMessage(
|
|
55
|
+
payload: SteeringPayload,
|
|
56
|
+
sendMessage: SendMessageFn,
|
|
57
|
+
opts: { isIdle: boolean; triggerTurn: boolean },
|
|
58
|
+
): void {
|
|
59
|
+
const body = getSteeringBody(payload);
|
|
60
|
+
const title = getCrewResultTitle({
|
|
61
|
+
agentId: payload.id,
|
|
62
|
+
agentName: payload.agentName,
|
|
63
|
+
status: payload.status,
|
|
64
|
+
});
|
|
65
|
+
const content = body
|
|
66
|
+
? `**${STATUS_ICON[payload.status]} ${title}**\n\n${body}`
|
|
67
|
+
: `**${STATUS_ICON[payload.status]} ${title}**`;
|
|
68
|
+
|
|
69
|
+
const message = {
|
|
70
|
+
customType: "crew-result",
|
|
71
|
+
content,
|
|
72
|
+
display: true,
|
|
73
|
+
details: {
|
|
74
|
+
agentId: payload.id,
|
|
75
|
+
agentName: payload.agentName,
|
|
76
|
+
sessionFile: payload.sessionFile,
|
|
77
|
+
status: payload.status,
|
|
78
|
+
body,
|
|
79
|
+
} satisfies CrewResultMessageDetails,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
sendMessage(
|
|
83
|
+
message,
|
|
84
|
+
opts.isIdle
|
|
85
|
+
? { triggerTurn: opts.triggerTurn }
|
|
86
|
+
: { deliverAs: "steer", triggerTurn: opts.triggerTurn },
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function sendRemainingNote(
|
|
91
|
+
remainingCount: number,
|
|
92
|
+
sendMessage: SendMessageFn,
|
|
93
|
+
opts: { isIdle: boolean; triggerTurn: boolean },
|
|
94
|
+
): void {
|
|
95
|
+
if (remainingCount <= 0) return;
|
|
96
|
+
|
|
97
|
+
sendMessage(
|
|
98
|
+
{
|
|
99
|
+
customType: "crew-remaining",
|
|
100
|
+
content: `⏳ ${remainingCount} subagent(s) still running`,
|
|
101
|
+
display: true,
|
|
102
|
+
},
|
|
103
|
+
opts.isIdle
|
|
104
|
+
? { triggerTurn: opts.triggerTurn }
|
|
105
|
+
: { deliverAs: "steer", triggerTurn: opts.triggerTurn },
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function sendCrewListActiveWarning(
|
|
110
|
+
sendMessage: SendMessageFn,
|
|
111
|
+
opts: { isIdle: boolean; triggerTurn: boolean },
|
|
112
|
+
): void {
|
|
113
|
+
sendMessage(
|
|
114
|
+
{
|
|
115
|
+
customType: "crew-list-warning",
|
|
116
|
+
content:
|
|
117
|
+
"⚠ Active subagents detected. Do not poll crew_list for completion — results arrive as steering messages. Continue with unrelated work or end your turn and wait for the steering messages.",
|
|
118
|
+
display: true,
|
|
119
|
+
},
|
|
120
|
+
opts.isIdle
|
|
121
|
+
? { triggerTurn: opts.triggerTurn }
|
|
122
|
+
: { deliverAs: "steer", triggerTurn: opts.triggerTurn },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const SUPPORTED_TOOL_NAMES_LITERAL = [
|
|
2
|
+
"read",
|
|
3
|
+
"bash",
|
|
4
|
+
"edit",
|
|
5
|
+
"write",
|
|
6
|
+
"grep",
|
|
7
|
+
"find",
|
|
8
|
+
"ls",
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export type SupportedToolName = (typeof SUPPORTED_TOOL_NAMES_LITERAL)[number];
|
|
12
|
+
|
|
13
|
+
export const SUPPORTED_TOOL_NAMES = Object.freeze(
|
|
14
|
+
[...SUPPORTED_TOOL_NAMES_LITERAL] as SupportedToolName[],
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
export function isSupportedToolName(name: string): name is SupportedToolName {
|
|
18
|
+
return SUPPORTED_TOOL_NAMES.includes(name as SupportedToolName);
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@melihmucuk/pi-crew",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Non-blocking subagent orchestration for pi coding agent",
|
|
6
6
|
"files": [
|
|
7
|
-
"
|
|
7
|
+
"extension/",
|
|
8
8
|
"agents/",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"skills/",
|
|
10
|
+
"prompts/"
|
|
11
11
|
],
|
|
12
12
|
"author": "Melih Mucuk",
|
|
13
13
|
"license": "MIT",
|
|
@@ -21,7 +21,10 @@
|
|
|
21
21
|
],
|
|
22
22
|
"pi": {
|
|
23
23
|
"extensions": [
|
|
24
|
-
"./
|
|
24
|
+
"./extension/index.ts"
|
|
25
|
+
],
|
|
26
|
+
"skills": [
|
|
27
|
+
"./skills"
|
|
25
28
|
],
|
|
26
29
|
"prompts": [
|
|
27
30
|
"./prompts"
|
|
@@ -29,10 +32,7 @@
|
|
|
29
32
|
"video": "https://monkeys-team.ams3.cdn.digitaloceanspaces.com/pi-crew-demo.mp4"
|
|
30
33
|
},
|
|
31
34
|
"scripts": {
|
|
32
|
-
"
|
|
33
|
-
"build": "npm run clean && tsc",
|
|
34
|
-
"typecheck": "tsc --noEmit",
|
|
35
|
-
"prepare": "npm run build"
|
|
35
|
+
"typecheck": "tsc --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@mariozechner/pi-agent-core": "*",
|
|
@@ -42,12 +42,12 @@
|
|
|
42
42
|
"typebox": "*"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@mariozechner/pi-agent-core": "^0.
|
|
46
|
-
"@mariozechner/pi-ai": "^0.
|
|
47
|
-
"@mariozechner/pi-coding-agent": "^0.
|
|
48
|
-
"@mariozechner/pi-tui": "^0.
|
|
45
|
+
"@mariozechner/pi-agent-core": "^0.73.0",
|
|
46
|
+
"@mariozechner/pi-ai": "^0.73.0",
|
|
47
|
+
"@mariozechner/pi-coding-agent": "^0.73.0",
|
|
48
|
+
"@mariozechner/pi-tui": "^0.73.0",
|
|
49
49
|
"@types/node": "^22.19.17",
|
|
50
|
-
"typebox": "^1.1.
|
|
50
|
+
"typebox": "^1.1.37",
|
|
51
51
|
"typescript": "^5.9.3"
|
|
52
52
|
}
|
|
53
53
|
}
|