shariq-pi-extensions 0.1.0
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/LICENSE +21 -0
- package/README.md +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SubagentManager — owns the registry of running/finished subagents.
|
|
3
|
+
*
|
|
4
|
+
* Each subagent is a scoped `SubagentSession` from a `SubagentBackend` plus a
|
|
5
|
+
* pump fiber that folds its normalized event stream into a mutable
|
|
6
|
+
* `SubagentSnapshot`. Closing a subagent's scope kills the underlying
|
|
7
|
+
* session/process and stops the pump.
|
|
8
|
+
*
|
|
9
|
+
* The manager also exposes a synchronous `SubagentReadModel` so the
|
|
10
|
+
* imperative TUI components (which render synchronously) can read snapshots
|
|
11
|
+
* and issue fire-and-forget commands without touching the Effect runtime.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
15
|
+
import {
|
|
16
|
+
Context,
|
|
17
|
+
Effect,
|
|
18
|
+
Exit,
|
|
19
|
+
Fiber,
|
|
20
|
+
Layer,
|
|
21
|
+
Result,
|
|
22
|
+
Scope,
|
|
23
|
+
Stream,
|
|
24
|
+
} from "effect";
|
|
25
|
+
import type { SubagentBackend, SubagentSession } from "./backend.ts";
|
|
26
|
+
import { BackendRegistry } from "./backend.ts";
|
|
27
|
+
import type {
|
|
28
|
+
BackendName,
|
|
29
|
+
LiveToolState,
|
|
30
|
+
PeerMessage,
|
|
31
|
+
RunOutcome,
|
|
32
|
+
SpawnTask,
|
|
33
|
+
SubagentEvent,
|
|
34
|
+
SubagentMeta,
|
|
35
|
+
SubagentSnapshot,
|
|
36
|
+
SubagentStatus,
|
|
37
|
+
TranscriptItem,
|
|
38
|
+
} from "./domain.ts";
|
|
39
|
+
import {
|
|
40
|
+
BackendUnavailableError,
|
|
41
|
+
ConcurrencyLimitError,
|
|
42
|
+
latestText,
|
|
43
|
+
SendError,
|
|
44
|
+
SpawnError,
|
|
45
|
+
} from "./domain.ts";
|
|
46
|
+
|
|
47
|
+
export const MAX_RUNNING = 4;
|
|
48
|
+
export const MAX_TRACKED = 64;
|
|
49
|
+
const STOP_TIMEOUT_MS = 5_000;
|
|
50
|
+
const ERROR_TEXT_MAX_LENGTH = 4_096;
|
|
51
|
+
|
|
52
|
+
function bounded(text: string) {
|
|
53
|
+
return text.slice(0, ERROR_TEXT_MAX_LENGTH);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// --- Internal state -----------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/** Mutable snapshot; exposed to readers via the readonly SubagentSnapshot type. */
|
|
59
|
+
interface MutableSnapshot {
|
|
60
|
+
id: string;
|
|
61
|
+
backend: BackendName;
|
|
62
|
+
title: string;
|
|
63
|
+
prompt: string;
|
|
64
|
+
cwd: string;
|
|
65
|
+
status: SubagentStatus;
|
|
66
|
+
createdAt: number;
|
|
67
|
+
settledAt?: number;
|
|
68
|
+
errorText?: string;
|
|
69
|
+
meta: SubagentMeta;
|
|
70
|
+
usage: { tokens?: number; contextWindow?: number };
|
|
71
|
+
transcript: TranscriptItem[];
|
|
72
|
+
liveAssistant?: { text: string; thinking: string };
|
|
73
|
+
liveTools: LiveToolState[];
|
|
74
|
+
queued: SubagentSnapshot["queued"];
|
|
75
|
+
finalText: string;
|
|
76
|
+
turns: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface Entry {
|
|
80
|
+
snapshot: MutableSnapshot;
|
|
81
|
+
session: SubagentSession;
|
|
82
|
+
scope: Scope.Closeable;
|
|
83
|
+
pump?: Fiber.Fiber<void>;
|
|
84
|
+
liveToolMap: Map<string, LiveToolState>;
|
|
85
|
+
maxConcurrent: number;
|
|
86
|
+
concurrencyGroup: string;
|
|
87
|
+
/** Idle restart dispatched but RunStarted not folded yet; counts as running
|
|
88
|
+
* so concurrent restarts cannot race past the cap. */
|
|
89
|
+
restarting?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// --- Read model ----------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/** Synchronous bridge for the TUI. Snapshots are live objects; do not mutate. */
|
|
95
|
+
export interface SubagentReadModel {
|
|
96
|
+
list(): ReadonlyArray<SubagentSnapshot>;
|
|
97
|
+
get(id: string): SubagentSnapshot | undefined;
|
|
98
|
+
size(): number;
|
|
99
|
+
/** Any-change notification (footer status, dashboard). */
|
|
100
|
+
subscribe(listener: () => void): () => void;
|
|
101
|
+
/** Per-subagent notification (takeover view). */
|
|
102
|
+
subscribeTo(id: string, listener: () => void): () => void;
|
|
103
|
+
/** Fire-and-forget: steer/continue a subagent (takeover input). */
|
|
104
|
+
requestSend(id: string, text: string): void;
|
|
105
|
+
/** Fire-and-forget: abort a running subagent (dashboard `x`, takeover). */
|
|
106
|
+
requestAbort(id: string): void;
|
|
107
|
+
/** Flat peer-message audit log for the coordination UI. */
|
|
108
|
+
peerMessages(): ReadonlyArray<PeerMessage>;
|
|
109
|
+
recordPeerMessage(message: PeerMessage): void;
|
|
110
|
+
clearWorktree(id: string): void;
|
|
111
|
+
retain(id: string, retained: boolean): void;
|
|
112
|
+
/**
|
|
113
|
+
* Register the settle hook. `consumed` is true when an active
|
|
114
|
+
* wait_agent/close_agent is collecting the result (so it must not also be
|
|
115
|
+
* delivered as a follow-up message).
|
|
116
|
+
*/
|
|
117
|
+
setOnSettled(
|
|
118
|
+
hook: ((snap: SubagentSnapshot, consumed: boolean) => void) | undefined,
|
|
119
|
+
): void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- Service --------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
export interface CancelResult {
|
|
125
|
+
readonly id: string;
|
|
126
|
+
readonly title: string;
|
|
127
|
+
readonly status: SubagentStatus;
|
|
128
|
+
readonly cancelled: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface SubagentManagerShape {
|
|
132
|
+
spawn(
|
|
133
|
+
backend: BackendName,
|
|
134
|
+
task: SpawnTask,
|
|
135
|
+
): Effect.Effect<
|
|
136
|
+
SubagentSnapshot,
|
|
137
|
+
SpawnError | ConcurrencyLimitError | BackendUnavailableError
|
|
138
|
+
>;
|
|
139
|
+
/** Atomically reserve capacity for a complete fan-out before starting it. */
|
|
140
|
+
spawnBatch(
|
|
141
|
+
backend: BackendName,
|
|
142
|
+
tasks: ReadonlyArray<SpawnTask>,
|
|
143
|
+
): Effect.Effect<
|
|
144
|
+
ReadonlyArray<SubagentSnapshot>,
|
|
145
|
+
SpawnError | ConcurrencyLimitError | BackendUnavailableError
|
|
146
|
+
>;
|
|
147
|
+
/**
|
|
148
|
+
* Wait until all listed subagents are settled. Unknown ids are treated as
|
|
149
|
+
* settled (the tool layer validates ids first). While waiting, settles for
|
|
150
|
+
* these ids are marked "consumed". Interruption (tool abort) releases the
|
|
151
|
+
* interest and leaves the subagents running.
|
|
152
|
+
*/
|
|
153
|
+
waitFor(
|
|
154
|
+
ids: ReadonlyArray<string>,
|
|
155
|
+
onPending?: (pending: string[]) => void,
|
|
156
|
+
): Effect.Effect<void>;
|
|
157
|
+
/** Cancel running subagents; resolves when they have settled. */
|
|
158
|
+
cancel(
|
|
159
|
+
ids: ReadonlyArray<string>,
|
|
160
|
+
): Effect.Effect<ReadonlyArray<CancelResult>>;
|
|
161
|
+
send(id: string, text: string): Effect.Effect<void, SendError>;
|
|
162
|
+
get(id: string): Effect.Effect<SubagentSnapshot | undefined>;
|
|
163
|
+
readonly list: Effect.Effect<ReadonlyArray<SubagentSnapshot>>;
|
|
164
|
+
readonly disposeAll: Effect.Effect<void>;
|
|
165
|
+
readonly view: SubagentReadModel;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export class SubagentManager extends Context.Service<
|
|
169
|
+
SubagentManager,
|
|
170
|
+
SubagentManagerShape
|
|
171
|
+
>()("subagents/SubagentManager") {}
|
|
172
|
+
|
|
173
|
+
// --- Implementation --------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
const makeManager = Effect.gen(function* () {
|
|
176
|
+
const registry = yield* BackendRegistry;
|
|
177
|
+
// Detached forker for sync contexts (read-model commands, pruning) that
|
|
178
|
+
// preserves the manager's services instead of using the global runtime.
|
|
179
|
+
const runDetached = Effect.runForkWith(yield* Effect.context());
|
|
180
|
+
|
|
181
|
+
const entries = new Map<string, Entry>();
|
|
182
|
+
const waitInterest = new Map<string, number>();
|
|
183
|
+
const rollingBack = new Set<string>();
|
|
184
|
+
const retained = new Set<string>();
|
|
185
|
+
const listeners = new Set<() => void>();
|
|
186
|
+
/** One-shot nextChange waiters, swapped out before invocation so waiters
|
|
187
|
+
* re-registering during notification are not visited in the same sweep. */
|
|
188
|
+
let changeWaiters: Array<() => void> = [];
|
|
189
|
+
const idListeners = new Map<string, Set<() => void>>();
|
|
190
|
+
const cleanups = new Set<Fiber.Fiber<unknown>>();
|
|
191
|
+
const peerMessages: PeerMessage[] = [];
|
|
192
|
+
let counter = 0;
|
|
193
|
+
const reservedByGroup = new Map<string, number>();
|
|
194
|
+
const reservedIn = (group: string) => reservedByGroup.get(group) ?? 0;
|
|
195
|
+
const reserve = (group: string, count: number) =>
|
|
196
|
+
reservedByGroup.set(group, reservedIn(group) + count);
|
|
197
|
+
const release = (group: string, count: number) => {
|
|
198
|
+
const next = reservedIn(group) - count;
|
|
199
|
+
if (next > 0) reservedByGroup.set(group, next);
|
|
200
|
+
else reservedByGroup.delete(group);
|
|
201
|
+
};
|
|
202
|
+
let disposed = false;
|
|
203
|
+
let onSettled:
|
|
204
|
+
((snap: SubagentSnapshot, consumed: boolean) => void) | undefined;
|
|
205
|
+
|
|
206
|
+
const notify = (id?: string) => {
|
|
207
|
+
const waiters = changeWaiters;
|
|
208
|
+
changeWaiters = [];
|
|
209
|
+
for (const waiter of waiters) waiter();
|
|
210
|
+
for (const listener of [...listeners]) {
|
|
211
|
+
try {
|
|
212
|
+
listener();
|
|
213
|
+
} catch {
|
|
214
|
+
// A failed status/render listener must not corrupt lifecycle state.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (id) {
|
|
218
|
+
for (const listener of idListeners.get(id) ?? []) {
|
|
219
|
+
try {
|
|
220
|
+
listener();
|
|
221
|
+
} catch {
|
|
222
|
+
// Same.
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** Resolves on the next state change. Interruption unregisters the waiter. */
|
|
229
|
+
const nextChange = Effect.callback<void>((resume) => {
|
|
230
|
+
const waiter = () => resume(Effect.void);
|
|
231
|
+
changeWaiters.push(waiter);
|
|
232
|
+
return Effect.sync(() => {
|
|
233
|
+
const index = changeWaiters.indexOf(waiter);
|
|
234
|
+
if (index >= 0) changeWaiters.splice(index, 1);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const runningCount = (group: string) =>
|
|
239
|
+
[...entries.values()].filter(
|
|
240
|
+
(entry) =>
|
|
241
|
+
entry.concurrencyGroup === group &&
|
|
242
|
+
(entry.snapshot.status === "running" || entry.restarting === true),
|
|
243
|
+
).length;
|
|
244
|
+
|
|
245
|
+
const addInterest = (ids: ReadonlyArray<string>) => {
|
|
246
|
+
for (const id of ids) waitInterest.set(id, (waitInterest.get(id) ?? 0) + 1);
|
|
247
|
+
};
|
|
248
|
+
const releaseInterest = (ids: ReadonlyArray<string>) => {
|
|
249
|
+
for (const id of ids) {
|
|
250
|
+
const count = (waitInterest.get(id) ?? 1) - 1;
|
|
251
|
+
if (count <= 0) waitInterest.delete(id);
|
|
252
|
+
else waitInterest.set(id, count);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const closeEntryScope = (entry: Entry) =>
|
|
257
|
+
Scope.close(entry.scope, Exit.void).pipe(Effect.ignore);
|
|
258
|
+
|
|
259
|
+
const pruneSettled = () => {
|
|
260
|
+
if (entries.size <= MAX_TRACKED) return;
|
|
261
|
+
const candidates = [...entries.values()]
|
|
262
|
+
.filter(
|
|
263
|
+
(e) =>
|
|
264
|
+
e.snapshot.status !== "running" &&
|
|
265
|
+
!waitInterest.has(e.snapshot.id) &&
|
|
266
|
+
!retained.has(e.snapshot.id),
|
|
267
|
+
)
|
|
268
|
+
.sort(
|
|
269
|
+
(a, b) =>
|
|
270
|
+
(a.snapshot.settledAt ?? a.snapshot.createdAt) -
|
|
271
|
+
(b.snapshot.settledAt ?? b.snapshot.createdAt),
|
|
272
|
+
);
|
|
273
|
+
for (const entry of candidates) {
|
|
274
|
+
if (entries.size <= MAX_TRACKED) break;
|
|
275
|
+
entries.delete(entry.snapshot.id);
|
|
276
|
+
const fiber = runDetached(closeEntryScope(entry));
|
|
277
|
+
cleanups.add(fiber);
|
|
278
|
+
fiber.addObserver(() => cleanups.delete(fiber));
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const settle = (entry: Entry, outcome: RunOutcome, persistOnly = false) => {
|
|
283
|
+
const s = entry.snapshot;
|
|
284
|
+
entry.restarting = false;
|
|
285
|
+
if (s.status !== "running") return;
|
|
286
|
+
s.settledAt = Date.now();
|
|
287
|
+
switch (outcome._tag) {
|
|
288
|
+
case "Completed":
|
|
289
|
+
s.status = "done";
|
|
290
|
+
s.errorText = undefined;
|
|
291
|
+
s.finalText = outcome.finalText;
|
|
292
|
+
break;
|
|
293
|
+
case "Failed":
|
|
294
|
+
s.status = "error";
|
|
295
|
+
s.errorText = bounded(outcome.errorText);
|
|
296
|
+
// Never let a failed run report the previous run's successful output.
|
|
297
|
+
s.finalText = outcome.partialText ?? "";
|
|
298
|
+
break;
|
|
299
|
+
case "Interrupted":
|
|
300
|
+
s.status = "error";
|
|
301
|
+
s.errorText = "Run was aborted";
|
|
302
|
+
s.finalText = outcome.partialText ?? "";
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
s.liveAssistant = undefined;
|
|
306
|
+
entry.liveToolMap.clear();
|
|
307
|
+
s.liveTools = [];
|
|
308
|
+
s.queued = [];
|
|
309
|
+
const consumed = (waitInterest.get(s.id) ?? 0) > 0;
|
|
310
|
+
notify(s.id);
|
|
311
|
+
try {
|
|
312
|
+
// During teardown, don't queue results into a shutting-down session.
|
|
313
|
+
if (!disposed && !rollingBack.has(s.id)) onSettled?.(s, consumed || persistOnly);
|
|
314
|
+
} catch {
|
|
315
|
+
// The parent session may be unavailable; settlement stays final.
|
|
316
|
+
}
|
|
317
|
+
pruneSettled();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const foldEvent = (entry: Entry, event: SubagentEvent) => {
|
|
321
|
+
const s = entry.snapshot;
|
|
322
|
+
switch (event._tag) {
|
|
323
|
+
case "RunStarted":
|
|
324
|
+
entry.restarting = false;
|
|
325
|
+
s.status = "running";
|
|
326
|
+
s.settledAt = undefined;
|
|
327
|
+
s.errorText = undefined;
|
|
328
|
+
break;
|
|
329
|
+
case "RunSettled":
|
|
330
|
+
settle(entry, event.outcome);
|
|
331
|
+
return; // settle() already notified
|
|
332
|
+
case "UserMessage":
|
|
333
|
+
s.transcript.push({ kind: "user", text: event.text });
|
|
334
|
+
break;
|
|
335
|
+
case "AssistantDelta": {
|
|
336
|
+
const live = s.liveAssistant ?? { text: "", thinking: "" };
|
|
337
|
+
s.liveAssistant =
|
|
338
|
+
event.kind === "text"
|
|
339
|
+
? { ...live, text: live.text + event.delta }
|
|
340
|
+
: { ...live, thinking: live.thinking + event.delta };
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "AssistantMessage":
|
|
344
|
+
s.transcript.push({ kind: "assistant", parts: event.parts });
|
|
345
|
+
s.liveAssistant = undefined;
|
|
346
|
+
s.turns++;
|
|
347
|
+
break;
|
|
348
|
+
case "ToolStart":
|
|
349
|
+
entry.liveToolMap.set(event.toolId, {
|
|
350
|
+
toolId: event.toolId,
|
|
351
|
+
name: event.name,
|
|
352
|
+
argsPreview: event.argsPreview,
|
|
353
|
+
});
|
|
354
|
+
s.liveTools = [...entry.liveToolMap.values()];
|
|
355
|
+
break;
|
|
356
|
+
case "ToolUpdate": {
|
|
357
|
+
const current = entry.liveToolMap.get(event.toolId);
|
|
358
|
+
if (current) {
|
|
359
|
+
entry.liveToolMap.set(event.toolId, {
|
|
360
|
+
...current,
|
|
361
|
+
outputPreview: event.outputPreview ?? current.outputPreview,
|
|
362
|
+
});
|
|
363
|
+
s.liveTools = [...entry.liveToolMap.values()];
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "ToolEnd":
|
|
368
|
+
entry.liveToolMap.delete(event.toolId);
|
|
369
|
+
s.liveTools = [...entry.liveToolMap.values()];
|
|
370
|
+
s.transcript.push({
|
|
371
|
+
kind: "toolResult",
|
|
372
|
+
toolId: event.toolId,
|
|
373
|
+
name: event.name,
|
|
374
|
+
isError: event.isError,
|
|
375
|
+
outputPreview: event.outputPreview,
|
|
376
|
+
});
|
|
377
|
+
break;
|
|
378
|
+
case "QueueChanged":
|
|
379
|
+
s.queued = event.queued;
|
|
380
|
+
break;
|
|
381
|
+
case "UsageChanged":
|
|
382
|
+
s.usage = {
|
|
383
|
+
tokens: event.tokens ?? s.usage.tokens,
|
|
384
|
+
contextWindow: event.contextWindow ?? s.usage.contextWindow,
|
|
385
|
+
};
|
|
386
|
+
break;
|
|
387
|
+
case "MetaChanged":
|
|
388
|
+
s.meta = { ...s.meta, ...event.meta };
|
|
389
|
+
break;
|
|
390
|
+
case "BackendError":
|
|
391
|
+
s.errorText = bounded(event.message);
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
notify(s.id);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const spawnInternal = (
|
|
398
|
+
backendName: BackendName,
|
|
399
|
+
task: SpawnTask,
|
|
400
|
+
preReserved = false,
|
|
401
|
+
) =>
|
|
402
|
+
Effect.gen(function* () {
|
|
403
|
+
// Reserve synchronously (before the first yield inside doSpawn) so
|
|
404
|
+
// parallel tool calls cannot race past the global cap.
|
|
405
|
+
if (!preReserved) {
|
|
406
|
+
yield* Effect.suspend(
|
|
407
|
+
(): Effect.Effect<void, SpawnError | ConcurrencyLimitError> => {
|
|
408
|
+
if (disposed) {
|
|
409
|
+
return new SpawnError({
|
|
410
|
+
message: "Subagent manager is shutting down.",
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const group = task.concurrencyGroup ?? "default";
|
|
414
|
+
if (runningCount(group) + reservedIn(group) >= task.maxConcurrent) {
|
|
415
|
+
return new ConcurrencyLimitError({
|
|
416
|
+
message: `Max ${task.maxConcurrent} Pi subagents can run concurrently. Let one finish or close one before spawning another. Use /subagents config to change maxConcurrent.`,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
reserve(group, 1);
|
|
420
|
+
return Effect.void;
|
|
421
|
+
},
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const doSpawn = Effect.gen(function* () {
|
|
426
|
+
const backend: SubagentBackend | undefined = registry.get(backendName);
|
|
427
|
+
if (!backend) {
|
|
428
|
+
return yield* new BackendUnavailableError({
|
|
429
|
+
message: `Unknown backend "${backendName}".`,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const available = yield* backend.available;
|
|
433
|
+
if (!available) {
|
|
434
|
+
return yield* new BackendUnavailableError({
|
|
435
|
+
message: `Backend "${backendName}" is not available on this machine (binary/SDK/credentials missing).`,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const scope = yield* Scope.make();
|
|
440
|
+
const session = yield* Scope.provide(backend.spawn(task), scope).pipe(
|
|
441
|
+
Effect.onError(() => Scope.close(scope, Exit.void)),
|
|
442
|
+
);
|
|
443
|
+
if (disposed) {
|
|
444
|
+
yield* Scope.close(scope, Exit.void);
|
|
445
|
+
return yield* new SpawnError({
|
|
446
|
+
message: "Subagent manager shut down while spawning.",
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const id =
|
|
451
|
+
task.preferredId && !entries.has(task.preferredId)
|
|
452
|
+
? task.preferredId
|
|
453
|
+
: `sa-${++counter}-${randomUUID().slice(0, 6)}`;
|
|
454
|
+
const meta = yield* session.meta;
|
|
455
|
+
const entry: Entry = {
|
|
456
|
+
snapshot: {
|
|
457
|
+
id,
|
|
458
|
+
backend: backendName,
|
|
459
|
+
title: task.title,
|
|
460
|
+
prompt: task.prompt,
|
|
461
|
+
cwd: task.cwd,
|
|
462
|
+
status: "running",
|
|
463
|
+
createdAt: Date.now(),
|
|
464
|
+
meta,
|
|
465
|
+
usage: { contextWindow: meta.contextWindow },
|
|
466
|
+
transcript: [],
|
|
467
|
+
liveTools: [],
|
|
468
|
+
queued: [],
|
|
469
|
+
finalText: "",
|
|
470
|
+
turns: 0,
|
|
471
|
+
},
|
|
472
|
+
session,
|
|
473
|
+
scope,
|
|
474
|
+
liveToolMap: new Map(),
|
|
475
|
+
maxConcurrent: task.maxConcurrent,
|
|
476
|
+
concurrencyGroup: task.concurrencyGroup ?? "default",
|
|
477
|
+
};
|
|
478
|
+
entries.set(id, entry);
|
|
479
|
+
|
|
480
|
+
// Pump: fold the event stream into the snapshot. Tied to the entry
|
|
481
|
+
// scope, so closing the scope stops it. If the stream ends while the
|
|
482
|
+
// subagent still looks running, the backend died out from under us.
|
|
483
|
+
const pump = Stream.runForEach(session.events, (event) =>
|
|
484
|
+
Effect.sync(() => foldEvent(entry, event)),
|
|
485
|
+
).pipe(
|
|
486
|
+
Effect.ensuring(
|
|
487
|
+
Effect.sync(() => {
|
|
488
|
+
if (entry.snapshot.status === "running") {
|
|
489
|
+
settle(entry, {
|
|
490
|
+
_tag: "Failed",
|
|
491
|
+
errorText: "Backend event stream ended unexpectedly",
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}),
|
|
495
|
+
),
|
|
496
|
+
);
|
|
497
|
+
entry.pump = yield* Scope.provide(Effect.forkScoped(pump), scope);
|
|
498
|
+
|
|
499
|
+
notify(id);
|
|
500
|
+
return entry.snapshot as SubagentSnapshot;
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
return yield* (preReserved
|
|
504
|
+
? doSpawn
|
|
505
|
+
: doSpawn.pipe(
|
|
506
|
+
Effect.ensuring(
|
|
507
|
+
Effect.sync(() => {
|
|
508
|
+
release(task.concurrencyGroup ?? "default", 1);
|
|
509
|
+
notify();
|
|
510
|
+
}),
|
|
511
|
+
),
|
|
512
|
+
));
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const spawn = (backendName: BackendName, task: SpawnTask) =>
|
|
516
|
+
spawnInternal(backendName, task);
|
|
517
|
+
|
|
518
|
+
const spawnBatch = (backendName: BackendName, tasks: ReadonlyArray<SpawnTask>) =>
|
|
519
|
+
Effect.gen(function* () {
|
|
520
|
+
if (tasks.length === 0) return [];
|
|
521
|
+
const limit = Math.min(...tasks.map((task) => task.maxConcurrent));
|
|
522
|
+
const groups = new Set(tasks.map((task) => task.concurrencyGroup ?? "default"));
|
|
523
|
+
if (groups.size !== 1) {
|
|
524
|
+
return yield* new SpawnError({ message: "A batch must use one concurrency group." });
|
|
525
|
+
}
|
|
526
|
+
const group = [...groups][0]!;
|
|
527
|
+
yield* Effect.suspend(
|
|
528
|
+
(): Effect.Effect<void, SpawnError | ConcurrencyLimitError> => {
|
|
529
|
+
if (disposed) {
|
|
530
|
+
return new SpawnError({ message: "Subagent manager is shutting down." });
|
|
531
|
+
}
|
|
532
|
+
if (runningCount(group) + reservedIn(group) + tasks.length > limit) {
|
|
533
|
+
return new ConcurrencyLimitError({
|
|
534
|
+
message: `Cannot start ${tasks.length} tasks atomically: ${runningCount(group)} running and ${reservedIn(group)} reserved with maxConcurrent ${limit}.`,
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
reserve(group, tasks.length);
|
|
538
|
+
return Effect.void;
|
|
539
|
+
},
|
|
540
|
+
);
|
|
541
|
+
const started: SubagentSnapshot[] = [];
|
|
542
|
+
return yield* Effect.forEach(
|
|
543
|
+
tasks,
|
|
544
|
+
(task) =>
|
|
545
|
+
spawnInternal(backendName, task, true).pipe(
|
|
546
|
+
Effect.tap((snapshot) => Effect.sync(() => started.push(snapshot))),
|
|
547
|
+
),
|
|
548
|
+
{ concurrency: "unbounded" },
|
|
549
|
+
).pipe(
|
|
550
|
+
Effect.onError(() =>
|
|
551
|
+
Effect.gen(function* () {
|
|
552
|
+
for (const snapshot of started) rollingBack.add(snapshot.id);
|
|
553
|
+
yield* Effect.forEach(
|
|
554
|
+
started,
|
|
555
|
+
(snapshot) => {
|
|
556
|
+
const entry = entries.get(snapshot.id);
|
|
557
|
+
if (!entry) return Effect.void;
|
|
558
|
+
entries.delete(snapshot.id);
|
|
559
|
+
return entry.session.interrupt.pipe(
|
|
560
|
+
Effect.timeout(STOP_TIMEOUT_MS),
|
|
561
|
+
Effect.ignore,
|
|
562
|
+
Effect.andThen(closeEntryScope(entry)),
|
|
563
|
+
Effect.timeout(STOP_TIMEOUT_MS),
|
|
564
|
+
Effect.ignore,
|
|
565
|
+
);
|
|
566
|
+
},
|
|
567
|
+
{ concurrency: "unbounded" },
|
|
568
|
+
);
|
|
569
|
+
for (const snapshot of started) rollingBack.delete(snapshot.id);
|
|
570
|
+
yield* Effect.sync(() => notify());
|
|
571
|
+
}),
|
|
572
|
+
),
|
|
573
|
+
Effect.ensuring(
|
|
574
|
+
Effect.sync(() => {
|
|
575
|
+
release(group, tasks.length);
|
|
576
|
+
notify();
|
|
577
|
+
}),
|
|
578
|
+
),
|
|
579
|
+
);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const waitFor = (
|
|
583
|
+
ids: ReadonlyArray<string>,
|
|
584
|
+
onPending?: (pending: string[]) => void,
|
|
585
|
+
) =>
|
|
586
|
+
Effect.suspend(() => {
|
|
587
|
+
const unique = [...new Set(ids)];
|
|
588
|
+
addInterest(unique);
|
|
589
|
+
const loop = Effect.gen(function* () {
|
|
590
|
+
while (true) {
|
|
591
|
+
const pending = unique.filter((id) => {
|
|
592
|
+
const entry = entries.get(id);
|
|
593
|
+
return entry?.snapshot.status === "running" || entry?.restarting === true;
|
|
594
|
+
});
|
|
595
|
+
if (pending.length === 0) return;
|
|
596
|
+
onPending?.(pending);
|
|
597
|
+
yield* nextChange;
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
return loop.pipe(
|
|
601
|
+
Effect.ensuring(
|
|
602
|
+
Effect.sync(() => {
|
|
603
|
+
releaseInterest(unique);
|
|
604
|
+
pruneSettled();
|
|
605
|
+
}),
|
|
606
|
+
),
|
|
607
|
+
);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
/** Interrupt one running entry, force-closing its scope after 5s. */
|
|
611
|
+
const abortEntry = (entry: Entry) =>
|
|
612
|
+
Effect.gen(function* () {
|
|
613
|
+
if (entry.snapshot.status !== "running") return;
|
|
614
|
+
const graceful = yield* entry.session.interrupt.pipe(
|
|
615
|
+
Effect.timeout(STOP_TIMEOUT_MS),
|
|
616
|
+
Effect.result,
|
|
617
|
+
);
|
|
618
|
+
if (Result.isFailure(graceful)) {
|
|
619
|
+
// Settle before closing the scope so the pump's stream-ended
|
|
620
|
+
// fallback ("Backend event stream ended unexpectedly") cannot win
|
|
621
|
+
// the race and report the wrong terminal reason.
|
|
622
|
+
yield* Effect.sync(() => {
|
|
623
|
+
settle(entry, { _tag: "Interrupted" });
|
|
624
|
+
entry.snapshot.errorText =
|
|
625
|
+
"Abort deadline exceeded; session was force-disposed";
|
|
626
|
+
notify(entry.snapshot.id);
|
|
627
|
+
});
|
|
628
|
+
// Bound the close like disposeAll does: a stuck backend finalizer
|
|
629
|
+
// must not hang cancel after the run is already settled.
|
|
630
|
+
yield* closeEntryScope(entry).pipe(
|
|
631
|
+
Effect.timeout(STOP_TIMEOUT_MS),
|
|
632
|
+
Effect.ignore,
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const cancel = (ids: ReadonlyArray<string>) =>
|
|
638
|
+
Effect.suspend(() => {
|
|
639
|
+
const unique = [...new Set(ids)];
|
|
640
|
+
const running = unique
|
|
641
|
+
.map((id) => entries.get(id))
|
|
642
|
+
.filter(
|
|
643
|
+
(entry): entry is Entry => entry?.snapshot.status === "running",
|
|
644
|
+
);
|
|
645
|
+
const runningIds = running.map((entry) => entry.snapshot.id);
|
|
646
|
+
// Mark consumed before interrupting so cancellation does not also
|
|
647
|
+
// enqueue duplicate automatic result messages into the parent.
|
|
648
|
+
addInterest(runningIds);
|
|
649
|
+
const work = Effect.gen(function* () {
|
|
650
|
+
yield* Effect.forEach(running, abortEntry, {
|
|
651
|
+
concurrency: "unbounded",
|
|
652
|
+
});
|
|
653
|
+
while (running.some((entry) => entry.snapshot.status === "running")) {
|
|
654
|
+
yield* nextChange;
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
return work.pipe(
|
|
658
|
+
Effect.ensuring(
|
|
659
|
+
Effect.sync(() => {
|
|
660
|
+
releaseInterest(runningIds);
|
|
661
|
+
pruneSettled();
|
|
662
|
+
}),
|
|
663
|
+
),
|
|
664
|
+
Effect.map((): ReadonlyArray<CancelResult> =>
|
|
665
|
+
unique.map((id) => {
|
|
666
|
+
const snapshot = entries.get(id)?.snapshot;
|
|
667
|
+
return {
|
|
668
|
+
id,
|
|
669
|
+
title: snapshot?.title ?? "?",
|
|
670
|
+
status: snapshot?.status ?? "error",
|
|
671
|
+
cancelled: runningIds.includes(id),
|
|
672
|
+
};
|
|
673
|
+
}),
|
|
674
|
+
),
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const send = (id: string, text: string) =>
|
|
679
|
+
Effect.suspend((): Effect.Effect<void, SendError> => {
|
|
680
|
+
const entry = entries.get(id);
|
|
681
|
+
if (!entry || disposed) {
|
|
682
|
+
return new SendError({
|
|
683
|
+
message: `Subagent "${id}" is no longer tracked.`,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
// Restarting a settled subagent occupies a running slot again, so it
|
|
687
|
+
// must respect the same cap as spawn. Steering an already-running one
|
|
688
|
+
// does not consume additional capacity.
|
|
689
|
+
if (entry.snapshot.status !== "running") {
|
|
690
|
+
if (runningCount(entry.concurrencyGroup) + reservedIn(entry.concurrencyGroup) >= entry.maxConcurrent) {
|
|
691
|
+
return new SendError({
|
|
692
|
+
message: `Max ${entry.maxConcurrent} Pi subagents can run concurrently; restarting "${id}" would exceed that.`,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
// Occupy the slot synchronously: the RunStarted that flips status
|
|
696
|
+
// arrives via the async pump, and two concurrent restarts must not
|
|
697
|
+
// both pass the check in that window. Cleared by RunStarted/settle,
|
|
698
|
+
// or here when the backend rejects the send.
|
|
699
|
+
entry.restarting = true;
|
|
700
|
+
return entry.session.send(text).pipe(
|
|
701
|
+
Effect.onError(() =>
|
|
702
|
+
Effect.sync(() => {
|
|
703
|
+
entry.restarting = false;
|
|
704
|
+
}),
|
|
705
|
+
),
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
return entry.session.send(text);
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
const disposeAll = Effect.gen(function* () {
|
|
712
|
+
const all = [...entries.values()];
|
|
713
|
+
for (const entry of all) {
|
|
714
|
+
if (entry.snapshot.status === "running") {
|
|
715
|
+
settle(entry, { _tag: "Interrupted", partialText: latestText(entry.snapshot) }, true);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
disposed = true;
|
|
719
|
+
entries.clear();
|
|
720
|
+
yield* Effect.forEach(
|
|
721
|
+
all,
|
|
722
|
+
(entry) =>
|
|
723
|
+
closeEntryScope(entry).pipe(
|
|
724
|
+
Effect.timeout(STOP_TIMEOUT_MS),
|
|
725
|
+
Effect.ignore,
|
|
726
|
+
),
|
|
727
|
+
{ concurrency: "unbounded" },
|
|
728
|
+
);
|
|
729
|
+
// Pruning cleanups are detached; bound them like everything else so a
|
|
730
|
+
// stuck backend finalizer cannot block runtime shutdown indefinitely.
|
|
731
|
+
yield* Effect.forEach(
|
|
732
|
+
[...cleanups],
|
|
733
|
+
(fiber) =>
|
|
734
|
+
Fiber.await(fiber).pipe(Effect.timeout(STOP_TIMEOUT_MS), Effect.ignore),
|
|
735
|
+
{ concurrency: "unbounded" },
|
|
736
|
+
).pipe(Effect.ignore);
|
|
737
|
+
yield* Effect.sync(() => notify());
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
const view: SubagentReadModel = {
|
|
741
|
+
list: () => [...entries.values()].map((entry) => entry.snapshot),
|
|
742
|
+
get: (id) => entries.get(id)?.snapshot,
|
|
743
|
+
size: () => entries.size,
|
|
744
|
+
subscribe: (listener) => {
|
|
745
|
+
listeners.add(listener);
|
|
746
|
+
return () => listeners.delete(listener);
|
|
747
|
+
},
|
|
748
|
+
subscribeTo: (id, listener) => {
|
|
749
|
+
let set = idListeners.get(id);
|
|
750
|
+
if (!set) {
|
|
751
|
+
set = new Set();
|
|
752
|
+
idListeners.set(id, set);
|
|
753
|
+
}
|
|
754
|
+
set.add(listener);
|
|
755
|
+
return () => {
|
|
756
|
+
set.delete(listener);
|
|
757
|
+
if (set.size === 0) idListeners.delete(id);
|
|
758
|
+
};
|
|
759
|
+
},
|
|
760
|
+
requestSend: (id, text) => {
|
|
761
|
+
runDetached(send(id, text).pipe(Effect.ignore));
|
|
762
|
+
},
|
|
763
|
+
requestAbort: (id) => {
|
|
764
|
+
const entry = entries.get(id);
|
|
765
|
+
if (!entry) return;
|
|
766
|
+
// UI-initiated aborts are not "consumed": the failed result still
|
|
767
|
+
// flows back to the parent as a follow-up message, matching v1.
|
|
768
|
+
runDetached(abortEntry(entry).pipe(Effect.ignore));
|
|
769
|
+
},
|
|
770
|
+
peerMessages: () => peerMessages,
|
|
771
|
+
recordPeerMessage: (message) => {
|
|
772
|
+
peerMessages.push(message);
|
|
773
|
+
if (peerMessages.length > 256) peerMessages.splice(0, peerMessages.length - 256);
|
|
774
|
+
notify(message.targetId);
|
|
775
|
+
},
|
|
776
|
+
clearWorktree: (id) => {
|
|
777
|
+
const entry = entries.get(id);
|
|
778
|
+
if (!entry) return;
|
|
779
|
+
entry.snapshot.meta = { ...entry.snapshot.meta, worktree: undefined };
|
|
780
|
+
notify(id);
|
|
781
|
+
},
|
|
782
|
+
retain: (id, keep) => {
|
|
783
|
+
if (keep) retained.add(id);
|
|
784
|
+
else retained.delete(id);
|
|
785
|
+
pruneSettled();
|
|
786
|
+
},
|
|
787
|
+
setOnSettled: (hook) => {
|
|
788
|
+
onSettled = hook;
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// Safety net: disposing the ManagedRuntime tears everything down even if
|
|
793
|
+
// the extension forgot to call disposeAll explicitly.
|
|
794
|
+
yield* Effect.addFinalizer(() => disposeAll);
|
|
795
|
+
|
|
796
|
+
return SubagentManager.of({
|
|
797
|
+
spawn,
|
|
798
|
+
spawnBatch,
|
|
799
|
+
waitFor,
|
|
800
|
+
cancel,
|
|
801
|
+
send,
|
|
802
|
+
get: (id) => Effect.sync(() => entries.get(id)?.snapshot),
|
|
803
|
+
list: Effect.sync(() => [...entries.values()].map((e) => e.snapshot)),
|
|
804
|
+
disposeAll,
|
|
805
|
+
view,
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
export const SubagentManagerLive: Layer.Layer<
|
|
810
|
+
SubagentManager,
|
|
811
|
+
never,
|
|
812
|
+
BackendRegistry
|
|
813
|
+
> = Layer.effect(SubagentManager, makeManager);
|