@melihmucuk/pi-crew 1.0.15 → 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 +32 -102
- package/agents/oracle.md +23 -29
- package/agents/planner.md +35 -116
- package/agents/quality-reviewer.md +39 -124
- package/agents/scout.md +21 -38
- package/agents/worker.md +27 -72
- package/extension/agent-catalog.ts +369 -0
- package/extension/agent-config-fields.ts +359 -0
- package/extension/agent-discovery.ts +49 -717
- package/extension/bootstrap-session.ts +2 -2
- package/extension/index.ts +5 -3
- package/extension/integration/crew-tool-actions.ts +306 -0
- package/extension/integration/crew-tool-executor.ts +109 -0
- package/extension/integration/register-renderers.ts +2 -2
- package/extension/integration/register-tools.ts +11 -3
- package/extension/integration/tool-presentation.ts +3 -23
- 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 +5 -61
- package/extension/integration/tools/crew-respond.ts +8 -29
- package/extension/integration/tools/crew-spawn.ts +16 -57
- package/extension/message-delivery-policy.ts +22 -0
- package/extension/runtime/crew-runtime.ts +60 -223
- package/extension/runtime/overflow-recovery.ts +1 -1
- 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/status-widget.ts +2 -2
- package/extension/subagent-messages.ts +9 -17
- package/package.json +13 -11
- package/prompts/pi-crew-plan.md +34 -137
- package/prompts/pi-crew-review.md +36 -112
- package/skills/pi-crew/REFERENCE.md +82 -0
- package/skills/pi-crew/SKILL.md +33 -104
- 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,5 +1,5 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@
|
|
2
|
-
import { Text } from "@
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { ActiveAgentSummary } from "./runtime/crew-runtime.js";
|
|
4
4
|
import type { CrewRuntime } from "./runtime/crew-runtime.js";
|
|
5
5
|
|
|
@@ -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,22 +32,24 @@
|
|
|
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
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"@
|
|
39
|
+
"@earendil-works/pi-agent-core": "*",
|
|
40
|
+
"@earendil-works/pi-ai": "*",
|
|
41
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
42
|
+
"@earendil-works/pi-tui": "*",
|
|
42
43
|
"typebox": "*"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
|
-
"@
|
|
46
|
-
"@
|
|
47
|
-
"@
|
|
48
|
-
"@
|
|
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",
|
|
50
|
-
"
|
|
51
|
+
"tsx": "^4.22.3",
|
|
52
|
+
"typebox": "^1.1.38",
|
|
51
53
|
"typescript": "^5.9.3"
|
|
52
54
|
}
|
|
53
55
|
}
|