@melihmucuk/pi-crew 1.0.13 → 1.0.14
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/docs/architecture.md +0 -1
- package/extension/agent-discovery.ts +791 -0
- package/extension/bootstrap-session.ts +131 -0
- package/extension/index.ts +65 -0
- package/extension/integration/register-command.ts +59 -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 +121 -0
- package/extension/integration/tools/crew-done.ts +42 -0
- package/extension/integration/tools/crew-list.ts +91 -0
- package/extension/integration/tools/crew-respond.ts +57 -0
- package/extension/integration/tools/crew-spawn.ts +88 -0
- package/extension/integration/tools/tool-deps.ts +16 -0
- package/extension/integration.ts +15 -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 +85 -0
- package/extension/runtime/subagent-state.ts +73 -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 +8 -11
- 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
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import { bootstrapSession } from "../bootstrap-session.js";
|
|
2
|
-
import { DeliveryCoordinator } from "./delivery-coordinator.js";
|
|
3
|
-
import { runPromptWithOverflowRecovery } from "./overflow-recovery.js";
|
|
4
|
-
import { SubagentRegistry } from "./subagent-registry.js";
|
|
5
|
-
import { isAbortableStatus, isAborted, } from "./subagent-state.js";
|
|
6
|
-
function toBootstrapContext(ctx) {
|
|
7
|
-
return {
|
|
8
|
-
model: ctx.model,
|
|
9
|
-
modelRegistry: ctx.modelRegistry,
|
|
10
|
-
agentDir: ctx.agentDir,
|
|
11
|
-
parentSessionFile: ctx.parentSessionFile,
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
function getLastAssistantMessage(messages) {
|
|
15
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
16
|
-
const msg = messages[i];
|
|
17
|
-
if (msg.role === "assistant") {
|
|
18
|
-
return msg;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
function getAssistantText(message) {
|
|
24
|
-
if (!message)
|
|
25
|
-
return undefined;
|
|
26
|
-
const texts = [];
|
|
27
|
-
for (const part of message.content) {
|
|
28
|
-
if (part.type === "text") {
|
|
29
|
-
texts.push(part.text);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return texts.length > 0 ? texts.join("\n") : undefined;
|
|
33
|
-
}
|
|
34
|
-
function getPromptOutcome(state) {
|
|
35
|
-
const lastAssistant = getLastAssistantMessage(state.session.messages);
|
|
36
|
-
const text = getAssistantText(lastAssistant);
|
|
37
|
-
if (lastAssistant?.stopReason === "error") {
|
|
38
|
-
return {
|
|
39
|
-
status: "error",
|
|
40
|
-
error: lastAssistant.errorMessage ?? text ?? "(no output)",
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (lastAssistant?.stopReason === "aborted") {
|
|
44
|
-
return {
|
|
45
|
-
status: "aborted",
|
|
46
|
-
error: lastAssistant.errorMessage ?? text ?? "(no output)",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
status: state.agentConfig.interactive ? "waiting" : "done",
|
|
51
|
-
result: text ?? "(no output)",
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Process-level singleton that owns all durable subagent state.
|
|
56
|
-
*
|
|
57
|
-
* This survives extension instance replacement caused by runtime
|
|
58
|
-
* teardown/recreation on /resume, /new, /fork (pi 0.65.0+).
|
|
59
|
-
* Each new extension instance rebinds delivery and widget hooks
|
|
60
|
-
* via activateSession/deactivateSession.
|
|
61
|
-
*/
|
|
62
|
-
class CrewRuntime {
|
|
63
|
-
registry = new SubagentRegistry();
|
|
64
|
-
delivery = new DeliveryCoordinator();
|
|
65
|
-
// Per-session refresh callbacks, keyed by ownerSessionId
|
|
66
|
-
refreshCallbacks = new Map();
|
|
67
|
-
refreshWidgetFor(sessionId) {
|
|
68
|
-
this.refreshCallbacks.get(sessionId)?.();
|
|
69
|
-
}
|
|
70
|
-
activateSession(binding, refreshWidget) {
|
|
71
|
-
if (refreshWidget) {
|
|
72
|
-
this.refreshCallbacks.set(binding.sessionId, refreshWidget);
|
|
73
|
-
}
|
|
74
|
-
this.delivery.activateSession(binding, (ownerSessionId, excludeId) => this.registry.countRunningForOwner(ownerSessionId, excludeId));
|
|
75
|
-
refreshWidget?.();
|
|
76
|
-
}
|
|
77
|
-
deactivateSession(sessionId) {
|
|
78
|
-
this.delivery.deactivateSession(sessionId);
|
|
79
|
-
this.refreshCallbacks.delete(sessionId);
|
|
80
|
-
}
|
|
81
|
-
spawn(agentConfig, task, cwd, ownerSessionId, ctx, extensionResolvedPath) {
|
|
82
|
-
const state = this.registry.create(agentConfig, task, ownerSessionId);
|
|
83
|
-
this.refreshWidgetFor(ownerSessionId);
|
|
84
|
-
void this.spawnSession(state, cwd, ctx, extensionResolvedPath);
|
|
85
|
-
return state.id;
|
|
86
|
-
}
|
|
87
|
-
attachSessionListeners(state, session) {
|
|
88
|
-
state.unsubscribe = session.subscribe((event) => {
|
|
89
|
-
if (event.type !== "turn_end")
|
|
90
|
-
return;
|
|
91
|
-
state.turns++;
|
|
92
|
-
const msg = event.message;
|
|
93
|
-
if (msg.role === "assistant") {
|
|
94
|
-
const assistantMsg = msg;
|
|
95
|
-
state.contextTokens = assistantMsg.usage.totalTokens;
|
|
96
|
-
state.model = assistantMsg.model;
|
|
97
|
-
}
|
|
98
|
-
this.refreshWidgetFor(state.ownerSessionId);
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
attachSpawnedSession(state, session) {
|
|
102
|
-
if (!this.registry.hasState(state)) {
|
|
103
|
-
session.dispose();
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
state.session = session;
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
settleAgent(state, nextStatus, opts) {
|
|
110
|
-
state.status = nextStatus;
|
|
111
|
-
state.result = opts.result;
|
|
112
|
-
state.error = opts.error;
|
|
113
|
-
this.delivery.deliver(state.ownerSessionId, {
|
|
114
|
-
id: state.id,
|
|
115
|
-
agentName: state.agentConfig.name,
|
|
116
|
-
sessionFile: state.session?.sessionFile,
|
|
117
|
-
status: state.status,
|
|
118
|
-
result: state.result,
|
|
119
|
-
error: state.error,
|
|
120
|
-
}, (ownerSessionId, excludeId) => this.registry.countRunningForOwner(ownerSessionId, excludeId));
|
|
121
|
-
if (state.status !== "waiting") {
|
|
122
|
-
this.disposeAgent(state);
|
|
123
|
-
}
|
|
124
|
-
else {
|
|
125
|
-
this.refreshWidgetFor(state.ownerSessionId);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
disposeAgent(state) {
|
|
129
|
-
state.unsubscribe?.();
|
|
130
|
-
state.promptAbortController = undefined;
|
|
131
|
-
state.session?.dispose();
|
|
132
|
-
this.registry.delete(state.id);
|
|
133
|
-
this.refreshWidgetFor(state.ownerSessionId);
|
|
134
|
-
}
|
|
135
|
-
async runPromptCycle(state, prompt) {
|
|
136
|
-
if (isAborted(state))
|
|
137
|
-
return;
|
|
138
|
-
const abortController = new AbortController();
|
|
139
|
-
state.promptAbortController = abortController;
|
|
140
|
-
try {
|
|
141
|
-
const recovery = await runPromptWithOverflowRecovery(state.session, prompt, abortController.signal);
|
|
142
|
-
if (isAborted(state))
|
|
143
|
-
return;
|
|
144
|
-
const outcome = getPromptOutcome(state);
|
|
145
|
-
if (recovery === "failed" && outcome.status !== "error") {
|
|
146
|
-
this.settleAgent(state, "error", {
|
|
147
|
-
error: "Context overflow recovery failed",
|
|
148
|
-
});
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
this.settleAgent(state, outcome.status, outcome);
|
|
152
|
-
}
|
|
153
|
-
catch (err) {
|
|
154
|
-
if (isAborted(state))
|
|
155
|
-
return;
|
|
156
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
157
|
-
this.settleAgent(state, "error", { error });
|
|
158
|
-
}
|
|
159
|
-
finally {
|
|
160
|
-
state.promptAbortController = undefined;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
async spawnSession(state, cwd, ctx, extensionResolvedPath) {
|
|
164
|
-
try {
|
|
165
|
-
if (isAborted(state))
|
|
166
|
-
return;
|
|
167
|
-
const { session, warnings } = await bootstrapSession({
|
|
168
|
-
agentConfig: state.agentConfig,
|
|
169
|
-
cwd,
|
|
170
|
-
ctx: toBootstrapContext(ctx),
|
|
171
|
-
extensionResolvedPath,
|
|
172
|
-
});
|
|
173
|
-
// Emit bootstrap warnings to UI
|
|
174
|
-
for (const warning of warnings) {
|
|
175
|
-
ctx.onWarning?.(warning);
|
|
176
|
-
}
|
|
177
|
-
if (!this.attachSpawnedSession(state, session))
|
|
178
|
-
return;
|
|
179
|
-
this.attachSessionListeners(state, session);
|
|
180
|
-
await this.runPromptCycle(state, state.task);
|
|
181
|
-
}
|
|
182
|
-
catch (err) {
|
|
183
|
-
if (isAborted(state))
|
|
184
|
-
return;
|
|
185
|
-
if (state.status === "running") {
|
|
186
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
187
|
-
this.settleAgent(state, "error", { error });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
respond(id, message, callerSessionId) {
|
|
192
|
-
const state = this.registry.get(id);
|
|
193
|
-
if (!state)
|
|
194
|
-
return { error: `No subagent with id "${id}"` };
|
|
195
|
-
if (state.ownerSessionId !== callerSessionId) {
|
|
196
|
-
return { error: `Subagent "${id}" belongs to a different session` };
|
|
197
|
-
}
|
|
198
|
-
if (state.status !== "waiting") {
|
|
199
|
-
return {
|
|
200
|
-
error: `Subagent "${id}" is not waiting for a response (status: ${state.status})`,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
if (!state.session)
|
|
204
|
-
return { error: `Subagent "${id}" has no active session` };
|
|
205
|
-
state.status = "running";
|
|
206
|
-
this.refreshWidgetFor(state.ownerSessionId);
|
|
207
|
-
void this.runPromptCycle(state, message);
|
|
208
|
-
return {};
|
|
209
|
-
}
|
|
210
|
-
done(id, callerSessionId) {
|
|
211
|
-
const state = this.registry.get(id);
|
|
212
|
-
if (!state)
|
|
213
|
-
return { error: `No active subagent with id "${id}"` };
|
|
214
|
-
if (state.ownerSessionId !== callerSessionId) {
|
|
215
|
-
return { error: `Subagent "${id}" belongs to a different session` };
|
|
216
|
-
}
|
|
217
|
-
if (state.status !== "waiting") {
|
|
218
|
-
return { error: `Subagent "${id}" is not in waiting state` };
|
|
219
|
-
}
|
|
220
|
-
this.disposeAgent(state);
|
|
221
|
-
return {};
|
|
222
|
-
}
|
|
223
|
-
abort(id, opts) {
|
|
224
|
-
const state = this.registry.get(id);
|
|
225
|
-
if (!state || !isAbortableStatus(state.status))
|
|
226
|
-
return false;
|
|
227
|
-
state.promptAbortController?.abort();
|
|
228
|
-
state.promptAbortController = undefined;
|
|
229
|
-
state.session?.abortCompaction();
|
|
230
|
-
state.session?.abortRetry();
|
|
231
|
-
state.session?.abort().catch(() => { });
|
|
232
|
-
this.settleAgent(state, "aborted", { error: opts.reason });
|
|
233
|
-
return true;
|
|
234
|
-
}
|
|
235
|
-
abortOwned(ids, callerSessionId, opts) {
|
|
236
|
-
const uniqueIds = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
|
|
237
|
-
const result = {
|
|
238
|
-
abortedIds: [],
|
|
239
|
-
missingIds: [],
|
|
240
|
-
foreignIds: [],
|
|
241
|
-
};
|
|
242
|
-
for (const id of uniqueIds) {
|
|
243
|
-
const state = this.registry.get(id);
|
|
244
|
-
if (!state || !isAbortableStatus(state.status)) {
|
|
245
|
-
result.missingIds.push(id);
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
if (state.ownerSessionId !== callerSessionId) {
|
|
249
|
-
result.foreignIds.push(id);
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
if (this.abort(id, opts)) {
|
|
253
|
-
result.abortedIds.push(id);
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
result.missingIds.push(id);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return result;
|
|
260
|
-
}
|
|
261
|
-
abortAllOwned(callerSessionId, opts) {
|
|
262
|
-
const ids = this.registry.getOwnedAbortableIds(callerSessionId);
|
|
263
|
-
for (const id of ids) {
|
|
264
|
-
this.abort(id, opts);
|
|
265
|
-
}
|
|
266
|
-
return ids;
|
|
267
|
-
}
|
|
268
|
-
/**
|
|
269
|
-
* Abort all running subagents during shutdown cleanup.
|
|
270
|
-
* Called from SIGINT, session_shutdown(reason="quit"), and beforeExit fallback paths.
|
|
271
|
-
*/
|
|
272
|
-
abortAll() {
|
|
273
|
-
const allAgents = this.registry.getAllRunning();
|
|
274
|
-
for (const state of allAgents) {
|
|
275
|
-
this.abort(state.id, { reason: "Aborted during shutdown" });
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
getAbortableAgents() {
|
|
279
|
-
return this.registry.getAbortableAgents();
|
|
280
|
-
}
|
|
281
|
-
getActiveSummariesForOwner(ownerSessionId) {
|
|
282
|
-
return this.registry.getActiveSummariesForOwner(ownerSessionId);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
export const crewRuntime = new CrewRuntime();
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { type SteeringPayload, type SendMessageFn } from "../subagent-messages.js";
|
|
2
|
-
export interface ActiveRuntimeBinding {
|
|
3
|
-
sessionId: string;
|
|
4
|
-
isIdle: () => boolean;
|
|
5
|
-
sendMessage: SendMessageFn;
|
|
6
|
-
}
|
|
7
|
-
export declare class DeliveryCoordinator {
|
|
8
|
-
private binding;
|
|
9
|
-
private pendingMessages;
|
|
10
|
-
private flushScheduled;
|
|
11
|
-
activateSession(binding: ActiveRuntimeBinding, countRunningForOwner: (ownerSessionId: string, excludeId: string) => number): void;
|
|
12
|
-
deactivateSession(sessionId: string): void;
|
|
13
|
-
deliver(ownerSessionId: string, payload: SteeringPayload, countRunningForOwner: (ownerSessionId: string, excludeId: string) => number): void;
|
|
14
|
-
/**
|
|
15
|
-
* Remove pending messages older than the TTL.
|
|
16
|
-
* Called during activateSession to prevent unbounded memory growth.
|
|
17
|
-
*/
|
|
18
|
-
private cleanStaleMessages;
|
|
19
|
-
private flushPending;
|
|
20
|
-
/**
|
|
21
|
-
* Result messages always go first. If more subagents are still running and the
|
|
22
|
-
* owner is idle, queue the result without triggering, then queue the separate
|
|
23
|
-
* remaining note with triggerTurn so the next turn sees both in order.
|
|
24
|
-
*/
|
|
25
|
-
private send;
|
|
26
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { sendRemainingNote, sendSteeringMessage, } from "../subagent-messages.js";
|
|
2
|
-
export class DeliveryCoordinator {
|
|
3
|
-
binding;
|
|
4
|
-
pendingMessages = [];
|
|
5
|
-
flushScheduled = false;
|
|
6
|
-
activateSession(binding, countRunningForOwner) {
|
|
7
|
-
this.binding = binding;
|
|
8
|
-
// Delay flush to next macrotask. session_start fires before pi-core
|
|
9
|
-
// calls _reconnectToAgent(), so synchronous delivery would emit agent
|
|
10
|
-
// events while the session listener is disconnected, losing JSONL persistence.
|
|
11
|
-
if (this.pendingMessages.some((entry) => entry.ownerSessionId === binding.sessionId)) {
|
|
12
|
-
this.flushScheduled = true;
|
|
13
|
-
setTimeout(() => {
|
|
14
|
-
this.flushScheduled = false;
|
|
15
|
-
this.flushPending(countRunningForOwner);
|
|
16
|
-
}, 0);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
deactivateSession(sessionId) {
|
|
20
|
-
if (this.binding?.sessionId === sessionId) {
|
|
21
|
-
this.binding = undefined;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
deliver(ownerSessionId, payload, countRunningForOwner) {
|
|
25
|
-
if (!this.binding || ownerSessionId !== this.binding.sessionId || this.flushScheduled) {
|
|
26
|
-
this.pendingMessages.push({ ownerSessionId, payload, queuedAt: Date.now() });
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
this.send(ownerSessionId, payload, countRunningForOwner);
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Remove pending messages older than the TTL.
|
|
33
|
-
* Called during activateSession to prevent unbounded memory growth.
|
|
34
|
-
*/
|
|
35
|
-
cleanStaleMessages() {
|
|
36
|
-
const maxAgeMs = 86_400_000; // 24 hours
|
|
37
|
-
const cutoff = Date.now() - maxAgeMs;
|
|
38
|
-
this.pendingMessages = this.pendingMessages.filter((entry) => entry.queuedAt >= cutoff);
|
|
39
|
-
}
|
|
40
|
-
flushPending(countRunningForOwner) {
|
|
41
|
-
if (!this.binding)
|
|
42
|
-
return;
|
|
43
|
-
const targetSessionId = this.binding.sessionId;
|
|
44
|
-
// Clean up stale messages first (older than TTL)
|
|
45
|
-
this.cleanStaleMessages();
|
|
46
|
-
const toDeliver = [];
|
|
47
|
-
const remaining = [];
|
|
48
|
-
for (const entry of this.pendingMessages) {
|
|
49
|
-
if (entry.ownerSessionId === targetSessionId) {
|
|
50
|
-
toDeliver.push(entry);
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
// Keep all other messages - they may be for sessions that will be reactivated later
|
|
54
|
-
remaining.push(entry);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
// Keep messages for other sessions
|
|
58
|
-
this.pendingMessages = remaining;
|
|
59
|
-
// Deliver messages for the active session
|
|
60
|
-
for (const entry of toDeliver) {
|
|
61
|
-
this.send(entry.ownerSessionId, entry.payload, countRunningForOwner);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Result messages always go first. If more subagents are still running and the
|
|
66
|
-
* owner is idle, queue the result without triggering, then queue the separate
|
|
67
|
-
* remaining note with triggerTurn so the next turn sees both in order.
|
|
68
|
-
*/
|
|
69
|
-
send(ownerSessionId, payload, countRunningForOwner) {
|
|
70
|
-
if (!this.binding || this.binding.sessionId !== ownerSessionId) {
|
|
71
|
-
this.pendingMessages.push({ ownerSessionId, payload, queuedAt: Date.now() });
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const remaining = countRunningForOwner(ownerSessionId, payload.id);
|
|
75
|
-
const isIdle = this.binding.isIdle();
|
|
76
|
-
const triggerResultTurn = !(isIdle && remaining > 0);
|
|
77
|
-
sendSteeringMessage(payload, this.binding.sendMessage, {
|
|
78
|
-
isIdle,
|
|
79
|
-
triggerTurn: triggerResultTurn,
|
|
80
|
-
});
|
|
81
|
-
sendRemainingNote(remaining, this.binding.sendMessage, {
|
|
82
|
-
isIdle,
|
|
83
|
-
triggerTurn: isIdle && remaining > 0,
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
@@ -1,3 +0,0 @@
|
|
|
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>;
|
|
@@ -1,155 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { AgentConfig } from "../agent-discovery.js";
|
|
2
|
-
import type { AbortableAgentSummary, ActiveAgentSummary, SubagentState } from "./subagent-state.js";
|
|
3
|
-
export declare class SubagentRegistry {
|
|
4
|
-
private activeAgents;
|
|
5
|
-
create(agentConfig: AgentConfig, task: string, ownerSessionId: string): SubagentState;
|
|
6
|
-
get(id: string): SubagentState | undefined;
|
|
7
|
-
hasState(state: SubagentState): boolean;
|
|
8
|
-
delete(id: string): void;
|
|
9
|
-
countRunningForOwner(ownerSessionId: string, excludeId: string): number;
|
|
10
|
-
getAbortableAgents(): AbortableAgentSummary[];
|
|
11
|
-
getActiveSummariesForOwner(ownerSessionId: string): ActiveAgentSummary[];
|
|
12
|
-
getOwnedAbortableIds(ownerSessionId: string): string[];
|
|
13
|
-
getAllRunning(): SubagentState[];
|
|
14
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { buildAbortableAgentSummary, buildActiveAgentSummary, generateId, isAbortableStatus, } from "./subagent-state.js";
|
|
2
|
-
export class SubagentRegistry {
|
|
3
|
-
activeAgents = new Map();
|
|
4
|
-
create(agentConfig, task, ownerSessionId) {
|
|
5
|
-
const id = generateId(agentConfig.name, new Set(this.activeAgents.keys()));
|
|
6
|
-
const state = {
|
|
7
|
-
id,
|
|
8
|
-
agentConfig,
|
|
9
|
-
task,
|
|
10
|
-
status: "running",
|
|
11
|
-
ownerSessionId,
|
|
12
|
-
session: null,
|
|
13
|
-
turns: 0,
|
|
14
|
-
contextTokens: 0,
|
|
15
|
-
model: undefined,
|
|
16
|
-
};
|
|
17
|
-
this.activeAgents.set(id, state);
|
|
18
|
-
return state;
|
|
19
|
-
}
|
|
20
|
-
get(id) {
|
|
21
|
-
return this.activeAgents.get(id);
|
|
22
|
-
}
|
|
23
|
-
hasState(state) {
|
|
24
|
-
return this.activeAgents.get(state.id) === state;
|
|
25
|
-
}
|
|
26
|
-
delete(id) {
|
|
27
|
-
this.activeAgents.delete(id);
|
|
28
|
-
}
|
|
29
|
-
countRunningForOwner(ownerSessionId, excludeId) {
|
|
30
|
-
let count = 0;
|
|
31
|
-
for (const state of this.activeAgents.values()) {
|
|
32
|
-
if (state.id !== excludeId &&
|
|
33
|
-
state.ownerSessionId === ownerSessionId &&
|
|
34
|
-
state.status === "running") {
|
|
35
|
-
count++;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return count;
|
|
39
|
-
}
|
|
40
|
-
getAbortableAgents() {
|
|
41
|
-
return Array.from(this.activeAgents.values())
|
|
42
|
-
.filter((state) => isAbortableStatus(state.status))
|
|
43
|
-
.map(buildAbortableAgentSummary);
|
|
44
|
-
}
|
|
45
|
-
getActiveSummariesForOwner(ownerSessionId) {
|
|
46
|
-
return Array.from(this.activeAgents.values())
|
|
47
|
-
.filter((state) => isAbortableStatus(state.status) && state.ownerSessionId === ownerSessionId)
|
|
48
|
-
.map(buildActiveAgentSummary);
|
|
49
|
-
}
|
|
50
|
-
getOwnedAbortableIds(ownerSessionId) {
|
|
51
|
-
return Array.from(this.activeAgents.values())
|
|
52
|
-
.filter((state) => state.ownerSessionId === ownerSessionId && isAbortableStatus(state.status))
|
|
53
|
-
.map((state) => state.id);
|
|
54
|
-
}
|
|
55
|
-
getAllRunning() {
|
|
56
|
-
return Array.from(this.activeAgents.values()).filter((state) => isAbortableStatus(state.status));
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { AgentConfig } from "../agent-discovery.js";
|
|
3
|
-
import type { SubagentStatus } from "../subagent-messages.js";
|
|
4
|
-
export interface SubagentState {
|
|
5
|
-
id: string;
|
|
6
|
-
agentConfig: AgentConfig;
|
|
7
|
-
task: string;
|
|
8
|
-
status: SubagentStatus;
|
|
9
|
-
ownerSessionId: string;
|
|
10
|
-
session: AgentSession | null;
|
|
11
|
-
turns: number;
|
|
12
|
-
contextTokens: number;
|
|
13
|
-
model: string | undefined;
|
|
14
|
-
error?: string;
|
|
15
|
-
result?: string;
|
|
16
|
-
promptAbortController?: AbortController;
|
|
17
|
-
unsubscribe?: () => void;
|
|
18
|
-
}
|
|
19
|
-
export interface ActiveAgentSummary {
|
|
20
|
-
id: string;
|
|
21
|
-
agentName: string;
|
|
22
|
-
status: SubagentStatus;
|
|
23
|
-
turns: number;
|
|
24
|
-
contextTokens: number;
|
|
25
|
-
model: string | undefined;
|
|
26
|
-
}
|
|
27
|
-
export interface AbortableAgentSummary {
|
|
28
|
-
id: string;
|
|
29
|
-
agentName: string;
|
|
30
|
-
}
|
|
31
|
-
export declare function generateId(name: string, existingIds: Set<string>): string;
|
|
32
|
-
export declare function isAborted(state: SubagentState): boolean;
|
|
33
|
-
export declare function isAbortableStatus(status: SubagentStatus): boolean;
|
|
34
|
-
export declare function buildActiveAgentSummary(state: SubagentState): ActiveAgentSummary;
|
|
35
|
-
export declare function buildAbortableAgentSummary(state: SubagentState): AbortableAgentSummary;
|