@melihmucuk/pi-crew 1.0.16 → 1.0.17
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 +8 -8
- package/agents/code-reviewer.md +2 -2
- package/agents/oracle.md +1 -1
- package/agents/planner.md +5 -1
- package/agents/quality-reviewer.md +2 -2
- package/agents/scout.md +2 -2
- package/agents/worker.md +3 -3
- package/extension/agent-catalog.ts +369 -0
- package/extension/agent-config-fields.ts +359 -0
- package/extension/agent-discovery.ts +49 -717
- package/extension/index.ts +4 -2
- package/extension/integration/crew-tool-actions.ts +306 -0
- package/extension/integration/crew-tool-executor.ts +109 -0
- package/extension/integration/register-tools.ts +10 -2
- package/extension/integration/tool-presentation.ts +0 -20
- package/extension/integration/tools/crew-abort.ts +14 -84
- package/extension/integration/tools/crew-done.ts +7 -26
- package/extension/integration/tools/crew-list.ts +4 -60
- package/extension/integration/tools/crew-respond.ts +8 -29
- package/extension/integration/tools/crew-spawn.ts +15 -56
- package/extension/message-delivery-policy.ts +22 -0
- package/extension/runtime/crew-runtime.ts +60 -223
- package/extension/runtime/{delivery-coordinator.ts → owner-session-coordinator.ts} +44 -37
- package/extension/runtime/subagent-lifecycle.ts +203 -0
- package/extension/runtime/subagent-registry.ts +50 -6
- package/extension/runtime/subagent-transitions.ts +100 -0
- package/extension/subagent-messages.ts +9 -17
- package/package.json +8 -6
- package/prompts/pi-crew-plan.md +14 -13
- package/prompts/pi-crew-review.md +20 -16
- package/skills/pi-crew/REFERENCE.md +32 -20
- package/skills/pi-crew/SKILL.md +13 -10
- package/extension/integration/tools/tool-deps.ts +0 -16
- package/extension/integration.ts +0 -13
- package/extension/runtime/subagent-state.ts +0 -59
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import type { SendMessageFn } from "../message-delivery-policy.js";
|
|
1
2
|
import {
|
|
2
3
|
type SteeringPayload,
|
|
3
4
|
sendRemainingNote,
|
|
4
5
|
sendSteeringMessage,
|
|
5
|
-
type SendMessageFn,
|
|
6
6
|
} from "../subagent-messages.js";
|
|
7
7
|
|
|
8
8
|
export interface ActiveRuntimeBinding {
|
|
@@ -17,25 +17,43 @@ interface PendingMessage {
|
|
|
17
17
|
queuedAt: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface OwnerSessionCoordinatorDeps {
|
|
21
|
+
countRunningForOwner: (ownerSessionId: string, excludeId: string) => number;
|
|
22
|
+
onRefreshOwnerSession: (ownerSessionId: string) => void;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
scheduleFlush?: (callback: () => void) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const PENDING_MESSAGE_TTL_MS = 86_400_000;
|
|
28
|
+
|
|
29
|
+
export class OwnerSessionCoordinator {
|
|
21
30
|
private binding: ActiveRuntimeBinding | undefined;
|
|
22
31
|
private pendingMessages: PendingMessage[] = [];
|
|
23
32
|
private flushScheduled = false;
|
|
33
|
+
private readonly countRunningForOwner: (ownerSessionId: string, excludeId: string) => number;
|
|
34
|
+
private readonly onRefreshOwnerSession: (ownerSessionId: string) => void;
|
|
35
|
+
private readonly now: () => number;
|
|
36
|
+
private readonly scheduleFlush: (callback: () => void) => void;
|
|
37
|
+
|
|
38
|
+
constructor(deps: OwnerSessionCoordinatorDeps) {
|
|
39
|
+
this.countRunningForOwner = deps.countRunningForOwner;
|
|
40
|
+
this.onRefreshOwnerSession = deps.onRefreshOwnerSession;
|
|
41
|
+
this.now = deps.now ?? Date.now;
|
|
42
|
+
this.scheduleFlush = deps.scheduleFlush ?? ((callback) => setTimeout(callback, 0));
|
|
43
|
+
}
|
|
24
44
|
|
|
25
|
-
activateSession(
|
|
26
|
-
binding: ActiveRuntimeBinding,
|
|
27
|
-
countRunningForOwner: (ownerSessionId: string, excludeId: string) => number,
|
|
28
|
-
): void {
|
|
45
|
+
activateSession(binding: ActiveRuntimeBinding): void {
|
|
29
46
|
this.binding = binding;
|
|
47
|
+
|
|
30
48
|
// Delay flush to next macrotask. session_start fires before pi-core
|
|
31
49
|
// calls _reconnectToAgent(), so synchronous delivery would emit agent
|
|
32
50
|
// events while the session listener is disconnected, losing JSONL persistence.
|
|
33
51
|
if (this.pendingMessages.some((entry) => entry.ownerSessionId === binding.sessionId)) {
|
|
34
52
|
this.flushScheduled = true;
|
|
35
|
-
|
|
53
|
+
this.scheduleFlush(() => {
|
|
36
54
|
this.flushScheduled = false;
|
|
37
|
-
this.flushPending(
|
|
38
|
-
}
|
|
55
|
+
this.flushPending();
|
|
56
|
+
});
|
|
39
57
|
}
|
|
40
58
|
}
|
|
41
59
|
|
|
@@ -45,38 +63,34 @@ export class DeliveryCoordinator {
|
|
|
45
63
|
}
|
|
46
64
|
}
|
|
47
65
|
|
|
48
|
-
|
|
49
|
-
ownerSessionId
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
): void {
|
|
66
|
+
refresh(ownerSessionId: string): void {
|
|
67
|
+
this.onRefreshOwnerSession(ownerSessionId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
deliver(ownerSessionId: string, payload: SteeringPayload): void {
|
|
53
71
|
if (!this.binding || ownerSessionId !== this.binding.sessionId || this.flushScheduled) {
|
|
54
|
-
this.
|
|
72
|
+
this.queue(ownerSessionId, payload);
|
|
55
73
|
return;
|
|
56
74
|
}
|
|
57
75
|
|
|
58
|
-
this.send(ownerSessionId, payload
|
|
76
|
+
this.send(ownerSessionId, payload);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private queue(ownerSessionId: string, payload: SteeringPayload): void {
|
|
80
|
+
this.pendingMessages.push({ ownerSessionId, payload, queuedAt: this.now() });
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
/**
|
|
62
|
-
* Remove pending messages older than the TTL.
|
|
63
|
-
* Called during activateSession to prevent unbounded memory growth.
|
|
64
|
-
*/
|
|
65
83
|
private cleanStaleMessages(): void {
|
|
66
|
-
const
|
|
67
|
-
const cutoff = Date.now() - maxAgeMs;
|
|
84
|
+
const cutoff = this.now() - PENDING_MESSAGE_TTL_MS;
|
|
68
85
|
this.pendingMessages = this.pendingMessages.filter(
|
|
69
86
|
(entry) => entry.queuedAt >= cutoff,
|
|
70
87
|
);
|
|
71
88
|
}
|
|
72
89
|
|
|
73
|
-
private flushPending(
|
|
74
|
-
countRunningForOwner: (ownerSessionId: string, excludeId: string) => number,
|
|
75
|
-
): void {
|
|
90
|
+
private flushPending(): void {
|
|
76
91
|
if (!this.binding) return;
|
|
77
92
|
const targetSessionId = this.binding.sessionId;
|
|
78
93
|
|
|
79
|
-
// Clean up stale messages first (older than TTL)
|
|
80
94
|
this.cleanStaleMessages();
|
|
81
95
|
|
|
82
96
|
const toDeliver: PendingMessage[] = [];
|
|
@@ -86,17 +100,14 @@ export class DeliveryCoordinator {
|
|
|
86
100
|
if (entry.ownerSessionId === targetSessionId) {
|
|
87
101
|
toDeliver.push(entry);
|
|
88
102
|
} else {
|
|
89
|
-
// Keep all other messages - they may be for sessions that will be reactivated later
|
|
90
103
|
remaining.push(entry);
|
|
91
104
|
}
|
|
92
105
|
}
|
|
93
106
|
|
|
94
|
-
// Keep messages for other sessions
|
|
95
107
|
this.pendingMessages = remaining;
|
|
96
108
|
|
|
97
|
-
// Deliver messages for the active session
|
|
98
109
|
for (const entry of toDeliver) {
|
|
99
|
-
this.send(entry.ownerSessionId, entry.payload
|
|
110
|
+
this.send(entry.ownerSessionId, entry.payload);
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
113
|
|
|
@@ -105,17 +116,13 @@ export class DeliveryCoordinator {
|
|
|
105
116
|
* owner is idle, queue the result without triggering, then queue the separate
|
|
106
117
|
* remaining note with triggerTurn so the next turn sees both in order.
|
|
107
118
|
*/
|
|
108
|
-
private send(
|
|
109
|
-
ownerSessionId: string,
|
|
110
|
-
payload: SteeringPayload,
|
|
111
|
-
countRunningForOwner: (ownerSessionId: string, excludeId: string) => number,
|
|
112
|
-
): void {
|
|
119
|
+
private send(ownerSessionId: string, payload: SteeringPayload): void {
|
|
113
120
|
if (!this.binding || this.binding.sessionId !== ownerSessionId) {
|
|
114
|
-
this.
|
|
121
|
+
this.queue(ownerSessionId, payload);
|
|
115
122
|
return;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
const remaining = countRunningForOwner(ownerSessionId, payload.id);
|
|
125
|
+
const remaining = this.countRunningForOwner(ownerSessionId, payload.id);
|
|
119
126
|
const isIdle = this.binding.isIdle();
|
|
120
127
|
const triggerResultTurn = !(isIdle && remaining > 0);
|
|
121
128
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { BootstrapContext } from "../bootstrap-session.js";
|
|
5
|
+
import { bootstrapSession } from "../bootstrap-session.js";
|
|
6
|
+
import type { SubagentStatus } from "../subagent-messages.js";
|
|
7
|
+
import { runPromptWithOverflowRecovery } from "./overflow-recovery.js";
|
|
8
|
+
import type { SubagentState } from "./subagent-registry.js";
|
|
9
|
+
import { isAborted } from "./subagent-transitions.js";
|
|
10
|
+
|
|
11
|
+
interface PromptOutcome {
|
|
12
|
+
status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">;
|
|
13
|
+
result?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface StartOptions {
|
|
18
|
+
cwd: string;
|
|
19
|
+
ctx: BootstrapContext;
|
|
20
|
+
extensionResolvedPath: string;
|
|
21
|
+
onWarning?: (message: string) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SubagentLifecycleCallbacks {
|
|
25
|
+
isCurrent: (state: SubagentState) => boolean;
|
|
26
|
+
onProgress: (ownerSessionId: string) => void;
|
|
27
|
+
onSettled: (
|
|
28
|
+
state: SubagentState,
|
|
29
|
+
status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">,
|
|
30
|
+
outcome: { result?: string; error?: string },
|
|
31
|
+
) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getLastAssistantMessage(
|
|
35
|
+
messages: AgentMessage[],
|
|
36
|
+
): AssistantMessage | undefined {
|
|
37
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
38
|
+
const msg = messages[i];
|
|
39
|
+
if (msg.role === "assistant") {
|
|
40
|
+
return msg as AssistantMessage;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getAssistantText(
|
|
47
|
+
message: AssistantMessage | undefined,
|
|
48
|
+
): string | undefined {
|
|
49
|
+
if (!message) return undefined;
|
|
50
|
+
|
|
51
|
+
const texts: string[] = [];
|
|
52
|
+
for (const part of message.content) {
|
|
53
|
+
if (part.type === "text") {
|
|
54
|
+
texts.push(part.text);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return texts.length > 0 ? texts.join("\n") : undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getPromptOutcome(state: SubagentState): PromptOutcome {
|
|
62
|
+
const lastAssistant = getLastAssistantMessage(state.session!.messages);
|
|
63
|
+
const text = getAssistantText(lastAssistant);
|
|
64
|
+
|
|
65
|
+
if (lastAssistant?.stopReason === "error") {
|
|
66
|
+
return {
|
|
67
|
+
status: "error",
|
|
68
|
+
error: lastAssistant.errorMessage ?? text ?? "(no output)",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (lastAssistant?.stopReason === "aborted") {
|
|
73
|
+
return {
|
|
74
|
+
status: "aborted",
|
|
75
|
+
error: lastAssistant.errorMessage ?? text ?? "(no output)",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
status: state.agentConfig.interactive ? "waiting" : "done",
|
|
81
|
+
result: text ?? "(no output)",
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class SubagentLifecycle {
|
|
86
|
+
constructor(private readonly callbacks: SubagentLifecycleCallbacks) {}
|
|
87
|
+
|
|
88
|
+
start(state: SubagentState, opts: StartOptions): void {
|
|
89
|
+
void this.spawnSession(state, opts);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
respond(state: SubagentState, message: string): void {
|
|
93
|
+
void this.runPromptCycle(state, message);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
abortPrompt(state: SubagentState): void {
|
|
97
|
+
state.promptAbortController?.abort();
|
|
98
|
+
state.promptAbortController = undefined;
|
|
99
|
+
state.session?.abortCompaction();
|
|
100
|
+
state.session?.abortRetry();
|
|
101
|
+
state.session?.abort().catch(() => {});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private attachSessionListeners(
|
|
105
|
+
state: SubagentState,
|
|
106
|
+
session: AgentSession,
|
|
107
|
+
): void {
|
|
108
|
+
state.unsubscribe = session.subscribe((event) => {
|
|
109
|
+
if (event.type !== "turn_end") return;
|
|
110
|
+
|
|
111
|
+
state.turns++;
|
|
112
|
+
const msg = event.message;
|
|
113
|
+
if (msg.role === "assistant") {
|
|
114
|
+
const assistantMsg = msg as AssistantMessage;
|
|
115
|
+
state.contextTokens = assistantMsg.usage.totalTokens;
|
|
116
|
+
state.model = assistantMsg.model;
|
|
117
|
+
}
|
|
118
|
+
this.callbacks.onProgress(state.ownerSessionId);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private attachSpawnedSession(
|
|
123
|
+
state: SubagentState,
|
|
124
|
+
session: AgentSession,
|
|
125
|
+
): boolean {
|
|
126
|
+
if (!this.callbacks.isCurrent(state)) {
|
|
127
|
+
session.dispose();
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
state.session = session;
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async runPromptCycle(
|
|
136
|
+
state: SubagentState,
|
|
137
|
+
prompt: string,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
if (isAborted(state)) return;
|
|
140
|
+
|
|
141
|
+
const abortController = new AbortController();
|
|
142
|
+
state.promptAbortController = abortController;
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const recovery = await runPromptWithOverflowRecovery(
|
|
146
|
+
state.session!,
|
|
147
|
+
prompt,
|
|
148
|
+
abortController.signal,
|
|
149
|
+
);
|
|
150
|
+
if (isAborted(state)) return;
|
|
151
|
+
|
|
152
|
+
const outcome = getPromptOutcome(state);
|
|
153
|
+
|
|
154
|
+
if (recovery === "failed" && outcome.status !== "error") {
|
|
155
|
+
this.callbacks.onSettled(state, "error", {
|
|
156
|
+
error: "Context overflow recovery failed",
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.callbacks.onSettled(state, outcome.status, outcome);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (isAborted(state)) return;
|
|
164
|
+
|
|
165
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
166
|
+
this.callbacks.onSettled(state, "error", { error });
|
|
167
|
+
} finally {
|
|
168
|
+
state.promptAbortController = undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async spawnSession(
|
|
173
|
+
state: SubagentState,
|
|
174
|
+
opts: StartOptions,
|
|
175
|
+
): Promise<void> {
|
|
176
|
+
try {
|
|
177
|
+
if (isAborted(state)) return;
|
|
178
|
+
|
|
179
|
+
const { session, warnings } = await bootstrapSession({
|
|
180
|
+
agentConfig: state.agentConfig,
|
|
181
|
+
cwd: opts.cwd,
|
|
182
|
+
ctx: opts.ctx,
|
|
183
|
+
extensionResolvedPath: opts.extensionResolvedPath,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
for (const warning of warnings) {
|
|
187
|
+
opts.onWarning?.(warning);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (!this.attachSpawnedSession(state, session)) return;
|
|
191
|
+
|
|
192
|
+
this.attachSessionListeners(state, session);
|
|
193
|
+
await this.runPromptCycle(state, state.task);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (isAborted(state)) return;
|
|
196
|
+
|
|
197
|
+
if (state.status === "running") {
|
|
198
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
199
|
+
this.callbacks.onSettled(state, "error", { error });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -1,10 +1,54 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
1
3
|
import type { AgentConfig } from "../agent-discovery.js";
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import type { SubagentStatus } from "../subagent-messages.js";
|
|
5
|
+
import { isAbortableStatus } from "./subagent-transitions.js";
|
|
6
|
+
|
|
7
|
+
export interface SubagentState {
|
|
8
|
+
id: string;
|
|
9
|
+
agentConfig: AgentConfig;
|
|
10
|
+
task: string;
|
|
11
|
+
status: SubagentStatus;
|
|
12
|
+
ownerSessionId: string;
|
|
13
|
+
session: AgentSession | null;
|
|
14
|
+
turns: number;
|
|
15
|
+
contextTokens: number;
|
|
16
|
+
model: string | undefined;
|
|
17
|
+
error?: string;
|
|
18
|
+
result?: string;
|
|
19
|
+
promptAbortController?: AbortController;
|
|
20
|
+
unsubscribe?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ActiveAgentSummary {
|
|
24
|
+
id: string;
|
|
25
|
+
agentName: string;
|
|
26
|
+
status: SubagentStatus;
|
|
27
|
+
turns: number;
|
|
28
|
+
contextTokens: number;
|
|
29
|
+
model: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function generateId(name: string, existingIds: Set<string>): string {
|
|
33
|
+
for (let i = 0; i < 10; i++) {
|
|
34
|
+
const id = `${name}-${randomBytes(4).toString("hex")}`;
|
|
35
|
+
if (!existingIds.has(id)) return id;
|
|
36
|
+
}
|
|
37
|
+
return `${name}-${randomBytes(8).toString("hex")}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildActiveAgentSummary(
|
|
41
|
+
state: SubagentState,
|
|
42
|
+
): ActiveAgentSummary {
|
|
43
|
+
return {
|
|
44
|
+
id: state.id,
|
|
45
|
+
agentName: state.agentConfig.name,
|
|
46
|
+
status: state.status,
|
|
47
|
+
turns: state.turns,
|
|
48
|
+
contextTokens: state.contextTokens,
|
|
49
|
+
model: state.model,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
8
52
|
|
|
9
53
|
export class SubagentRegistry {
|
|
10
54
|
private activeAgents = new Map<string, SubagentState>();
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { SubagentStatus } from "../subagent-messages.js";
|
|
2
|
+
import type { SubagentState } from "./subagent-registry.js";
|
|
3
|
+
|
|
4
|
+
export type SettledSubagentStatus = Extract<
|
|
5
|
+
SubagentStatus,
|
|
6
|
+
"done" | "waiting" | "error" | "aborted"
|
|
7
|
+
>;
|
|
8
|
+
|
|
9
|
+
export interface SubagentTransitionOutcome {
|
|
10
|
+
result?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type SubagentTransitionResult =
|
|
15
|
+
| { ok: true; state: SubagentState }
|
|
16
|
+
| { ok: false; error: string };
|
|
17
|
+
|
|
18
|
+
export function isAborted(state: SubagentState): boolean {
|
|
19
|
+
return state.status === "aborted";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isAbortableStatus(status: SubagentStatus): boolean {
|
|
23
|
+
return status === "running" || status === "waiting";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function canAbortSubagent(
|
|
27
|
+
state: SubagentState | undefined,
|
|
28
|
+
): state is SubagentState {
|
|
29
|
+
return Boolean(state && isAbortableStatus(state.status));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateOwnedSubagent(
|
|
33
|
+
state: SubagentState | undefined,
|
|
34
|
+
id: string,
|
|
35
|
+
callerSessionId: string,
|
|
36
|
+
missingMessage: string,
|
|
37
|
+
): SubagentTransitionResult {
|
|
38
|
+
if (!state) return { ok: false, error: missingMessage };
|
|
39
|
+
if (state.ownerSessionId !== callerSessionId) {
|
|
40
|
+
return { ok: false, error: `Subagent "${id}" belongs to a different session` };
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, state };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function startSubagentResponse(
|
|
46
|
+
state: SubagentState | undefined,
|
|
47
|
+
id: string,
|
|
48
|
+
callerSessionId: string,
|
|
49
|
+
): SubagentTransitionResult {
|
|
50
|
+
const owned = validateOwnedSubagent(
|
|
51
|
+
state,
|
|
52
|
+
id,
|
|
53
|
+
callerSessionId,
|
|
54
|
+
`No subagent with id "${id}"`,
|
|
55
|
+
);
|
|
56
|
+
if (!owned.ok) return owned;
|
|
57
|
+
|
|
58
|
+
if (owned.state.status !== "waiting") {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: `Subagent "${id}" is not waiting for a response (status: ${owned.state.status})`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (!owned.state.session) {
|
|
65
|
+
return { ok: false, error: `Subagent "${id}" has no active session` };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
owned.state.status = "running";
|
|
69
|
+
return owned;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function validateSubagentDone(
|
|
73
|
+
state: SubagentState | undefined,
|
|
74
|
+
id: string,
|
|
75
|
+
callerSessionId: string,
|
|
76
|
+
): SubagentTransitionResult {
|
|
77
|
+
const owned = validateOwnedSubagent(
|
|
78
|
+
state,
|
|
79
|
+
id,
|
|
80
|
+
callerSessionId,
|
|
81
|
+
`No active subagent with id "${id}"`,
|
|
82
|
+
);
|
|
83
|
+
if (!owned.ok) return owned;
|
|
84
|
+
|
|
85
|
+
if (owned.state.status !== "waiting") {
|
|
86
|
+
return { ok: false, error: `Subagent "${id}" is not in waiting state` };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return owned;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function settleSubagent(
|
|
93
|
+
state: SubagentState,
|
|
94
|
+
status: SettledSubagentStatus,
|
|
95
|
+
outcome: SubagentTransitionOutcome,
|
|
96
|
+
): void {
|
|
97
|
+
state.status = status;
|
|
98
|
+
state.result = outcome.result;
|
|
99
|
+
state.error = outcome.error;
|
|
100
|
+
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { sendWithDeliveryPolicy, type SendMessageFn } from "./message-delivery-policy.js";
|
|
2
2
|
|
|
3
|
+
export type { SendMessageFn } from "./message-delivery-policy.js";
|
|
3
4
|
export type SubagentStatus = "running" | "waiting" | "done" | "error" | "aborted";
|
|
4
5
|
|
|
5
|
-
export type SendMessageFn = ExtensionAPI["sendMessage"];
|
|
6
|
-
|
|
7
6
|
export const STATUS_ICON: Record<SubagentStatus, string> = {
|
|
8
7
|
running: "⏳",
|
|
9
8
|
waiting: "⏳",
|
|
@@ -79,12 +78,7 @@ export function sendSteeringMessage(
|
|
|
79
78
|
} satisfies CrewResultMessageDetails,
|
|
80
79
|
};
|
|
81
80
|
|
|
82
|
-
sendMessage
|
|
83
|
-
message,
|
|
84
|
-
opts.isIdle
|
|
85
|
-
? { triggerTurn: opts.triggerTurn }
|
|
86
|
-
: { deliverAs: "steer", triggerTurn: opts.triggerTurn },
|
|
87
|
-
);
|
|
81
|
+
sendWithDeliveryPolicy(message, sendMessage, opts);
|
|
88
82
|
}
|
|
89
83
|
|
|
90
84
|
export function sendRemainingNote(
|
|
@@ -94,15 +88,14 @@ export function sendRemainingNote(
|
|
|
94
88
|
): void {
|
|
95
89
|
if (remainingCount <= 0) return;
|
|
96
90
|
|
|
97
|
-
|
|
91
|
+
sendWithDeliveryPolicy(
|
|
98
92
|
{
|
|
99
93
|
customType: "crew-remaining",
|
|
100
94
|
content: `⏳ ${remainingCount} subagent(s) still running`,
|
|
101
95
|
display: true,
|
|
102
96
|
},
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
: { deliverAs: "steer", triggerTurn: opts.triggerTurn },
|
|
97
|
+
sendMessage,
|
|
98
|
+
opts,
|
|
106
99
|
);
|
|
107
100
|
}
|
|
108
101
|
|
|
@@ -110,15 +103,14 @@ export function sendCrewListActiveWarning(
|
|
|
110
103
|
sendMessage: SendMessageFn,
|
|
111
104
|
opts: { isIdle: boolean; triggerTurn: boolean },
|
|
112
105
|
): void {
|
|
113
|
-
|
|
106
|
+
sendWithDeliveryPolicy(
|
|
114
107
|
{
|
|
115
108
|
customType: "crew-list-warning",
|
|
116
109
|
content:
|
|
117
110
|
"⚠ 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
111
|
display: true,
|
|
119
112
|
},
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
: { deliverAs: "steer", triggerTurn: opts.triggerTurn },
|
|
113
|
+
sendMessage,
|
|
114
|
+
opts,
|
|
123
115
|
);
|
|
124
116
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@melihmucuk/pi-crew",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Non-blocking subagent orchestration for pi coding agent",
|
|
6
6
|
"files": [
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"video": "https://monkeys-team.ams3.cdn.digitaloceanspaces.com/pi-crew-demo.mp4"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"typecheck": "tsc --noEmit"
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "node --import tsx --test tests/**/*.test.ts"
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
|
38
39
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -42,11 +43,12 @@
|
|
|
42
43
|
"typebox": "*"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
46
|
-
"@earendil-works/pi-ai": "^0.
|
|
47
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
48
|
-
"@earendil-works/pi-tui": "^0.
|
|
46
|
+
"@earendil-works/pi-agent-core": "^0.75.4",
|
|
47
|
+
"@earendil-works/pi-ai": "^0.75.4",
|
|
48
|
+
"@earendil-works/pi-coding-agent": "^0.75.4",
|
|
49
|
+
"@earendil-works/pi-tui": "^0.75.4",
|
|
49
50
|
"@types/node": "^22.19.17",
|
|
51
|
+
"tsx": "^4.22.3",
|
|
50
52
|
"typebox": "^1.1.38",
|
|
51
53
|
"typescript": "^5.9.3"
|
|
52
54
|
}
|
package/prompts/pi-crew-plan.md
CHANGED
|
@@ -6,31 +6,32 @@ description: Orchestrate scouts and planner to produce an implementation plan.
|
|
|
6
6
|
|
|
7
7
|
Additional instructions: `$ARGUMENTS`
|
|
8
8
|
|
|
9
|
-
You are a planning orchestrator, not a scout, planner, or implementer. Resolve the task and scope, gather only minimal
|
|
9
|
+
You are a planning orchestrator, not a scout, planner, or implementer. Resolve the task and scope, gather only minimal task-specific context, delegate discovery to scouts when available, pass cleaned findings to the planner, and manage the planner lifecycle. Do not perform deep investigation, write the plan yourself, or modify files.
|
|
10
10
|
|
|
11
11
|
## Task and Context
|
|
12
12
|
|
|
13
13
|
Use additional instructions when provided; otherwise use the current conversation task. If the task or scope is decision-critical unclear or conflicting, ask the user before proceeding.
|
|
14
14
|
|
|
15
|
-
Build shared context for subagents:
|
|
15
|
+
Build compact shared context for subagents. Include only information that helps this planning task beyond the selected subagent’s obvious role:
|
|
16
16
|
|
|
17
|
-
- user
|
|
18
|
-
-
|
|
19
|
-
- constraints and
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
- known stack, dependencies, conventions when relevant.
|
|
17
|
+
- user intent and expected outcome;
|
|
18
|
+
- user-provided references, plus a concise summary after reading them when practical;
|
|
19
|
+
- task-specific decisions, constraints, and assumptions not already covered by repo guidance;
|
|
20
|
+
- non-default scope boundaries, when needed;
|
|
21
|
+
- minimal orientation already gathered, only when it clarifies where to look;
|
|
22
|
+
- exact errors/output or verification context, when relevant.
|
|
24
23
|
|
|
25
|
-
Do not copy full reference contents. Subagents
|
|
24
|
+
Do not copy full reference contents. Do not include project root/cwd, generic repo conventions, default scope, edit permissions, output format, or role boilerplate. Subagents run in the same repo cwd and can inspect repo guidance themselves.
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
If the user provides a plan, spec, issue, doc, design, URL, or file as the source of intent, read it when practical and summarize the relevant intent instead of merely passing the path.
|
|
27
|
+
|
|
28
|
+
Gather only enough orientation to assign scout scopes or brief the planner: targeted searches, likely entry points, and small config or structure checks when they materially affect delegation. Do not read full implementation files, trace call chains, or analyze implementations. Do not read README/AGENTS just to repeat generic repo guidance.
|
|
28
29
|
|
|
29
30
|
## Scouts
|
|
30
31
|
|
|
31
32
|
Call `crew_list` and check for `scout`. If unavailable, continue to planner with minimal context and note the missing scout coverage.
|
|
32
33
|
|
|
33
|
-
If available, spawn up to 4 scouts for distinct, non-overlapping focus areas. Keep each task narrow and include
|
|
34
|
+
If available, spawn up to 4 scouts for distinct, non-overlapping focus areas. Keep each task narrow and include only task-specific context, the investigation focus, requested facts, and relevant paths or entry points. Do not restate scout role boilerplate, default read-only behavior, output format, or generic command restrictions.
|
|
34
35
|
|
|
35
36
|
Wait for scout results without polling or fabrication. If a scout fails or returns no useful findings, retry or reformulate once. If it still fails, record the gap and continue.
|
|
36
37
|
|
|
@@ -40,7 +41,7 @@ Before planner handoff, perform only mechanical cleanup: remove duplicates, irre
|
|
|
40
41
|
|
|
41
42
|
Call `crew_list` and check for `planner`. If unavailable, tell the user and stop; do not write the plan yourself.
|
|
42
43
|
|
|
43
|
-
Spawn the planner with shared context, cleaned scout findings, and gaps.
|
|
44
|
+
Spawn the planner with compact shared context, cleaned scout findings, and gaps. Keep the handoff focused on intent, decisions, constraints, facts, paths, relationships, and unresolved questions. Do not restate planner role boilerplate, output format, edit permissions, or obvious next steps.
|
|
44
45
|
|
|
45
46
|
Do not rewrite planner output that is already visible as a steering message.
|
|
46
47
|
|