@melihmucuk/pi-crew 1.0.17 → 1.0.18

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.
Files changed (31) hide show
  1. package/extension/catalog.ts +543 -0
  2. package/extension/crew.ts +383 -0
  3. package/extension/index.ts +5 -6
  4. package/extension/subagent-session.ts +270 -0
  5. package/extension/tools.ts +323 -0
  6. package/extension/ui.ts +309 -0
  7. package/package.json +1 -1
  8. package/extension/agent-catalog.ts +0 -369
  9. package/extension/agent-config-fields.ts +0 -359
  10. package/extension/agent-discovery.ts +0 -123
  11. package/extension/bootstrap-session.ts +0 -131
  12. package/extension/integration/crew-tool-actions.ts +0 -306
  13. package/extension/integration/crew-tool-executor.ts +0 -109
  14. package/extension/integration/register-renderers.ts +0 -77
  15. package/extension/integration/register-tools.ts +0 -47
  16. package/extension/integration/tool-presentation.ts +0 -30
  17. package/extension/integration/tools/crew-abort.ts +0 -56
  18. package/extension/integration/tools/crew-done.ts +0 -27
  19. package/extension/integration/tools/crew-list.ts +0 -36
  20. package/extension/integration/tools/crew-respond.ts +0 -38
  21. package/extension/integration/tools/crew-spawn.ts +0 -46
  22. package/extension/message-delivery-policy.ts +0 -22
  23. package/extension/runtime/crew-runtime.ts +0 -263
  24. package/extension/runtime/owner-session-coordinator.ts +0 -138
  25. package/extension/runtime/subagent-lifecycle.ts +0 -203
  26. package/extension/runtime/subagent-registry.ts +0 -122
  27. package/extension/runtime/subagent-transitions.ts +0 -100
  28. package/extension/status-widget.ts +0 -107
  29. package/extension/subagent-messages.ts +0 -116
  30. package/extension/tool-registry.ts +0 -19
  31. /package/extension/{runtime/overflow-recovery.ts → overflow-recovery.ts} +0 -0
@@ -0,0 +1,383 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import type { Api, Model } from "@earendil-works/pi-ai";
3
+ import type { AgentSession, ModelRegistry } from "@earendil-works/pi-coding-agent";
4
+ import type { AgentConfig } from "./catalog.js";
5
+ import type { BootstrapContext, SubagentRunner, SubagentRunnerCallbacks } from "./subagent-session.js";
6
+ import { SubagentSessionRunner } from "./subagent-session.js";
7
+ import {
8
+ type SendMessageFn,
9
+ type SteeringPayload,
10
+ type SubagentStatus,
11
+ sendRemainingNote,
12
+ sendSteeringMessage,
13
+ } from "./ui.js";
14
+
15
+ export interface ActiveRuntimeBinding {
16
+ sessionId: string;
17
+ isIdle: () => boolean;
18
+ sendMessage: SendMessageFn;
19
+ }
20
+
21
+ interface PendingMessage {
22
+ ownerSessionId: string;
23
+ payload: SteeringPayload;
24
+ queuedAt: number;
25
+ }
26
+
27
+ export interface SubagentState {
28
+ id: string;
29
+ agentConfig: AgentConfig;
30
+ task: string;
31
+ status: SubagentStatus;
32
+ ownerSessionId: string;
33
+ session: AgentSession | null;
34
+ turns: number;
35
+ contextTokens: number;
36
+ model: string | undefined;
37
+ error?: string;
38
+ result?: string;
39
+ promptAbortController?: AbortController;
40
+ unsubscribe?: () => void;
41
+ }
42
+
43
+ export interface ActiveAgentSummary {
44
+ id: string;
45
+ agentName: string;
46
+ status: SubagentStatus;
47
+ turns: number;
48
+ contextTokens: number;
49
+ model: string | undefined;
50
+ }
51
+
52
+ export interface AbortOwnedResult {
53
+ abortedIds: string[];
54
+ missingIds: string[];
55
+ foreignIds: string[];
56
+ }
57
+
58
+ interface AbortOptions {
59
+ reason: string;
60
+ }
61
+
62
+ export interface SpawnContext {
63
+ model: Model<Api> | undefined;
64
+ modelRegistry: ModelRegistry;
65
+ agentDir: string;
66
+ parentSessionFile?: string;
67
+ onWarning?: (message: string) => void;
68
+ }
69
+
70
+ type SettledSubagentStatus = Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">;
71
+
72
+ const PENDING_MESSAGE_TTL_MS = 86_400_000;
73
+
74
+ function toBootstrapContext(ctx: SpawnContext): BootstrapContext {
75
+ return {
76
+ model: ctx.model,
77
+ modelRegistry: ctx.modelRegistry,
78
+ agentDir: ctx.agentDir,
79
+ parentSessionFile: ctx.parentSessionFile,
80
+ };
81
+ }
82
+
83
+ function generateId(name: string, existingIds: Set<string>): string {
84
+ for (let i = 0; i < 10; i++) {
85
+ const id = `${name}-${randomBytes(4).toString("hex")}`;
86
+ if (!existingIds.has(id)) return id;
87
+ }
88
+ return `${name}-${randomBytes(8).toString("hex")}`;
89
+ }
90
+
91
+ function isAbortableStatus(status: SubagentStatus): boolean {
92
+ return status === "running" || status === "waiting";
93
+ }
94
+
95
+ function buildActiveAgentSummary(state: SubagentState): ActiveAgentSummary {
96
+ return {
97
+ id: state.id,
98
+ agentName: state.agentConfig.name,
99
+ status: state.status,
100
+ turns: state.turns,
101
+ contextTokens: state.contextTokens,
102
+ model: state.model,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Process-global coordinator for subagent state, ownership, delivery, and cleanup.
108
+ */
109
+ export class CrewRuntime {
110
+ private readonly agents = new Map<string, SubagentState>();
111
+ private readonly runner: SubagentRunner;
112
+ private readonly refreshCallbacks = new Map<string, () => void>();
113
+ private activeBinding: ActiveRuntimeBinding | undefined;
114
+ private pendingMessages: PendingMessage[] = [];
115
+ private flushScheduled = false;
116
+ private readonly now: () => number;
117
+ private readonly scheduleFlush: (callback: () => void) => void;
118
+
119
+ constructor(opts: {
120
+ now?: () => number;
121
+ scheduleFlush?: (callback: () => void) => void;
122
+ createRunner?: (callbacks: SubagentRunnerCallbacks) => SubagentRunner;
123
+ } = {}) {
124
+ this.now = opts.now ?? Date.now;
125
+ this.scheduleFlush = opts.scheduleFlush ?? ((callback) => setTimeout(callback, 0));
126
+ const callbacks: SubagentRunnerCallbacks = {
127
+ isCurrent: (state) => this.agents.get(state.id) === state,
128
+ onProgress: (ownerSessionId) => this.refreshWidgetFor(ownerSessionId),
129
+ onSettled: (state, status, outcome) => this.settleAgent(state, status, outcome),
130
+ };
131
+ this.runner = opts.createRunner?.(callbacks) ?? new SubagentSessionRunner(callbacks);
132
+ }
133
+
134
+ activateSession(binding: ActiveRuntimeBinding, refreshWidget?: () => void): void {
135
+ if (refreshWidget) this.refreshCallbacks.set(binding.sessionId, refreshWidget);
136
+ this.activeBinding = binding;
137
+ this.schedulePendingFlushFor(binding.sessionId);
138
+ refreshWidget?.();
139
+ }
140
+
141
+ deactivateSession(sessionId: string): void {
142
+ if (this.activeBinding?.sessionId === sessionId) this.activeBinding = undefined;
143
+ this.refreshCallbacks.delete(sessionId);
144
+ }
145
+
146
+ spawn(
147
+ agentConfig: AgentConfig,
148
+ task: string,
149
+ cwd: string,
150
+ ownerSessionId: string,
151
+ ctx: SpawnContext,
152
+ extensionResolvedPath: string,
153
+ ): string {
154
+ const state = this.createAgent(agentConfig, task, ownerSessionId);
155
+ this.refreshWidgetFor(ownerSessionId);
156
+ this.runner.start(state, {
157
+ cwd,
158
+ ctx: toBootstrapContext(ctx),
159
+ extensionResolvedPath,
160
+ onWarning: ctx.onWarning,
161
+ });
162
+ return state.id;
163
+ }
164
+
165
+ respond(id: string, message: string, callerSessionId: string): { error?: string } {
166
+ const transition = this.startSubagentResponse(id, callerSessionId);
167
+ if (!transition.ok) return { error: transition.error };
168
+
169
+ this.refreshWidgetFor(transition.state.ownerSessionId);
170
+ this.runner.respond(transition.state, message);
171
+ return {};
172
+ }
173
+
174
+ done(id: string, callerSessionId: string): { error?: string } {
175
+ const transition = this.validateSubagentDone(id, callerSessionId);
176
+ if (!transition.ok) return { error: transition.error };
177
+
178
+ this.disposeAgent(transition.state);
179
+ return {};
180
+ }
181
+
182
+ abort(id: string, opts: AbortOptions): boolean {
183
+ const state = this.agents.get(id);
184
+ if (!state || !isAbortableStatus(state.status)) return false;
185
+
186
+ this.runner.abort(state);
187
+ this.settleAgent(state, "aborted", { error: opts.reason });
188
+ return true;
189
+ }
190
+
191
+ abortOwned(ids: string[], callerSessionId: string, opts: AbortOptions): AbortOwnedResult {
192
+ const uniqueIds = Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
193
+ const result: AbortOwnedResult = { abortedIds: [], missingIds: [], foreignIds: [] };
194
+
195
+ for (const id of uniqueIds) {
196
+ const state = this.agents.get(id);
197
+ if (!state || !isAbortableStatus(state.status)) {
198
+ result.missingIds.push(id);
199
+ continue;
200
+ }
201
+ if (state.ownerSessionId !== callerSessionId) {
202
+ result.foreignIds.push(id);
203
+ continue;
204
+ }
205
+ if (this.abort(id, opts)) result.abortedIds.push(id);
206
+ else result.missingIds.push(id);
207
+ }
208
+
209
+ return result;
210
+ }
211
+
212
+ abortAllOwned(callerSessionId: string, opts: AbortOptions): string[] {
213
+ const ids = Array.from(this.agents.values())
214
+ .filter((state) => state.ownerSessionId === callerSessionId && isAbortableStatus(state.status))
215
+ .map((state) => state.id);
216
+ for (const id of ids) this.abort(id, opts);
217
+ return ids;
218
+ }
219
+
220
+ abortAll(): void {
221
+ const allAgents = Array.from(this.agents.values()).filter((state) => isAbortableStatus(state.status));
222
+ for (const state of allAgents) this.abort(state.id, { reason: "Aborted during shutdown" });
223
+ }
224
+
225
+ getActiveSummariesForOwner(ownerSessionId: string): ActiveAgentSummary[] {
226
+ return Array.from(this.agents.values())
227
+ .filter((state) => isAbortableStatus(state.status) && state.ownerSessionId === ownerSessionId)
228
+ .map(buildActiveAgentSummary);
229
+ }
230
+
231
+ private createAgent(agentConfig: AgentConfig, task: string, ownerSessionId: string): SubagentState {
232
+ const id = generateId(agentConfig.name, new Set(this.agents.keys()));
233
+ const state: SubagentState = {
234
+ id,
235
+ agentConfig,
236
+ task,
237
+ status: "running",
238
+ ownerSessionId,
239
+ session: null,
240
+ turns: 0,
241
+ contextTokens: 0,
242
+ model: undefined,
243
+ };
244
+ this.agents.set(id, state);
245
+ return state;
246
+ }
247
+
248
+ private refreshWidgetFor(sessionId: string): void {
249
+ this.refreshCallbacks.get(sessionId)?.();
250
+ }
251
+
252
+ private settleAgent(state: SubagentState, nextStatus: SettledSubagentStatus, opts: { result?: string; error?: string }): void {
253
+ if (this.agents.get(state.id) !== state) return;
254
+
255
+ state.status = nextStatus;
256
+ state.result = opts.result;
257
+ state.error = opts.error;
258
+
259
+ this.deliver(state.ownerSessionId, {
260
+ id: state.id,
261
+ agentName: state.agentConfig.name,
262
+ sessionFile: state.session?.sessionFile,
263
+ status: state.status,
264
+ result: state.result,
265
+ error: state.error,
266
+ });
267
+
268
+ if (state.status !== "waiting") this.disposeAgent(state);
269
+ else this.refreshWidgetFor(state.ownerSessionId);
270
+ }
271
+
272
+ private disposeAgent(state: SubagentState): void {
273
+ state.unsubscribe?.();
274
+ state.promptAbortController = undefined;
275
+ state.session?.dispose();
276
+ this.agents.delete(state.id);
277
+ this.refreshWidgetFor(state.ownerSessionId);
278
+ }
279
+
280
+ private validateOwnedSubagent(
281
+ id: string,
282
+ callerSessionId: string,
283
+ missingMessage: string,
284
+ ): { ok: true; state: SubagentState } | { ok: false; error: string } {
285
+ const state = this.agents.get(id);
286
+ if (!state) return { ok: false, error: missingMessage };
287
+ if (state.ownerSessionId !== callerSessionId) {
288
+ return { ok: false, error: `Subagent "${id}" belongs to a different session` };
289
+ }
290
+ return { ok: true, state };
291
+ }
292
+
293
+ private startSubagentResponse(id: string, callerSessionId: string): { ok: true; state: SubagentState } | { ok: false; error: string } {
294
+ const owned = this.validateOwnedSubagent(id, callerSessionId, `No subagent with id "${id}"`);
295
+ if (!owned.ok) return owned;
296
+ if (owned.state.status !== "waiting") {
297
+ return { ok: false, error: `Subagent "${id}" is not waiting for a response (status: ${owned.state.status})` };
298
+ }
299
+ if (!owned.state.session) return { ok: false, error: `Subagent "${id}" has no active session` };
300
+
301
+ owned.state.status = "running";
302
+ return owned;
303
+ }
304
+
305
+ private validateSubagentDone(id: string, callerSessionId: string): { ok: true; state: SubagentState } | { ok: false; error: string } {
306
+ const owned = this.validateOwnedSubagent(id, callerSessionId, `No active subagent with id "${id}"`);
307
+ if (!owned.ok) return owned;
308
+ if (owned.state.status !== "waiting") return { ok: false, error: `Subagent "${id}" is not in waiting state` };
309
+ return owned;
310
+ }
311
+
312
+ private countRunningForOwner(ownerSessionId: string, excludeId: string): number {
313
+ let count = 0;
314
+ for (const state of this.agents.values()) {
315
+ if (state.id !== excludeId && state.ownerSessionId === ownerSessionId && state.status === "running") count++;
316
+ }
317
+ return count;
318
+ }
319
+
320
+ private schedulePendingFlushFor(sessionId: string): void {
321
+ if (!this.pendingMessages.some((entry) => entry.ownerSessionId === sessionId)) return;
322
+
323
+ // Delay flush to next macrotask. session_start fires before pi-core reconnects the
324
+ // agent event listener; synchronous delivery can lose JSONL persistence.
325
+ this.flushScheduled = true;
326
+ this.scheduleFlush(() => {
327
+ this.flushScheduled = false;
328
+ this.flushPending();
329
+ });
330
+ }
331
+
332
+ private deliver(ownerSessionId: string, payload: SteeringPayload): void {
333
+ if (!this.activeBinding || ownerSessionId !== this.activeBinding.sessionId || this.flushScheduled) {
334
+ this.queue(ownerSessionId, payload);
335
+ return;
336
+ }
337
+ this.send(ownerSessionId, payload);
338
+ }
339
+
340
+ private queue(ownerSessionId: string, payload: SteeringPayload): void {
341
+ this.pendingMessages.push({ ownerSessionId, payload, queuedAt: this.now() });
342
+ }
343
+
344
+ private cleanStaleMessages(): void {
345
+ const cutoff = this.now() - PENDING_MESSAGE_TTL_MS;
346
+ this.pendingMessages = this.pendingMessages.filter((entry) => entry.queuedAt >= cutoff);
347
+ }
348
+
349
+ private flushPending(): void {
350
+ if (!this.activeBinding) return;
351
+ const targetSessionId = this.activeBinding.sessionId;
352
+ this.cleanStaleMessages();
353
+
354
+ const toDeliver: PendingMessage[] = [];
355
+ const remaining: PendingMessage[] = [];
356
+ for (const entry of this.pendingMessages) {
357
+ if (entry.ownerSessionId === targetSessionId) toDeliver.push(entry);
358
+ else remaining.push(entry);
359
+ }
360
+ this.pendingMessages = remaining;
361
+
362
+ for (const entry of toDeliver) this.send(entry.ownerSessionId, entry.payload);
363
+ }
364
+
365
+ private send(ownerSessionId: string, payload: SteeringPayload): void {
366
+ if (!this.activeBinding || this.activeBinding.sessionId !== ownerSessionId) {
367
+ this.queue(ownerSessionId, payload);
368
+ return;
369
+ }
370
+
371
+ const remaining = this.countRunningForOwner(ownerSessionId, payload.id);
372
+ const isIdle = this.activeBinding.isIdle();
373
+ const triggerResultTurn = !(isIdle && remaining > 0);
374
+
375
+ sendSteeringMessage(payload, this.activeBinding.sendMessage, { isIdle, triggerTurn: triggerResultTurn });
376
+ sendRemainingNote(remaining, this.activeBinding.sendMessage, { isIdle, triggerTurn: isIdle && remaining > 0 });
377
+ }
378
+ }
379
+
380
+ const crewRuntimeKey = Symbol.for("pi-crew.runtime");
381
+ const globalWithCrewRuntime = globalThis as typeof globalThis & Record<symbol, CrewRuntime | undefined>;
382
+
383
+ export const crewRuntime = globalWithCrewRuntime[crewRuntimeKey] ??= new CrewRuntime();
@@ -1,10 +1,9 @@
1
1
  import { dirname } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
- import { crewRuntime } from "./runtime/crew-runtime.js";
5
- import { registerCrewMessageRenderers } from "./integration/register-renderers.js";
6
- import { registerCrewTools } from "./integration/register-tools.js";
7
- import { updateWidget } from "./status-widget.js";
4
+ import { crewRuntime } from "./crew.js";
5
+ import { registerCrewTools } from "./tools.js";
6
+ import { registerCrewMessageRenderers, updateWidget } from "./ui.js";
8
7
 
9
8
  const extensionDir = dirname(fileURLToPath(import.meta.url));
10
9
 
@@ -19,11 +18,11 @@ function setupProcessHooks() {
19
18
  if (globalWithProcessHooks[processHooksSetupKey]) return;
20
19
  globalWithProcessHooks[processHooksSetupKey] = true;
21
20
 
22
- process.once('SIGINT', () => {
21
+ process.once("SIGINT", () => {
23
22
  crewRuntime.abortAll();
24
23
  process.exit(130);
25
24
  });
26
- process.on('beforeExit', () => crewRuntime.abortAll());
25
+ process.on("beforeExit", () => crewRuntime.abortAll());
27
26
  }
28
27
 
29
28
  export default function (pi: ExtensionAPI) {
@@ -0,0 +1,270 @@
1
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
3
+ import {
4
+ type AgentSession,
5
+ createAgentSession,
6
+ DefaultResourceLoader,
7
+ type ModelRegistry,
8
+ SessionManager,
9
+ SettingsManager,
10
+ } from "@earendil-works/pi-coding-agent";
11
+ import type { AgentConfig } from "./catalog.js";
12
+ import { SUPPORTED_TOOL_NAMES, type SupportedToolName } from "./catalog.js";
13
+ import type { SubagentState } from "./crew.js";
14
+ import type { SubagentStatus } from "./ui.js";
15
+ import { runPromptWithOverflowRecovery } from "./overflow-recovery.js";
16
+
17
+ export interface BootstrapContext {
18
+ model: Model<Api> | undefined;
19
+ modelRegistry: ModelRegistry;
20
+ agentDir: string;
21
+ parentSessionFile?: string;
22
+ }
23
+
24
+ interface BootstrapOptions {
25
+ agentConfig: AgentConfig;
26
+ cwd: string;
27
+ ctx: BootstrapContext;
28
+ extensionResolvedPath: string;
29
+ }
30
+
31
+ interface BootstrapResult {
32
+ session: AgentSession;
33
+ warnings: string[];
34
+ }
35
+
36
+ interface PromptOutcome {
37
+ status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">;
38
+ result?: string;
39
+ error?: string;
40
+ }
41
+
42
+ interface StartOptions {
43
+ cwd: string;
44
+ ctx: BootstrapContext;
45
+ extensionResolvedPath: string;
46
+ onWarning?: (message: string) => void;
47
+ }
48
+
49
+ export interface SubagentRunnerCallbacks {
50
+ isCurrent: (state: SubagentState) => boolean;
51
+ onProgress: (ownerSessionId: string) => void;
52
+ onSettled: (
53
+ state: SubagentState,
54
+ status: Extract<SubagentStatus, "done" | "waiting" | "error" | "aborted">,
55
+ outcome: { result?: string; error?: string },
56
+ ) => void;
57
+ }
58
+
59
+ export interface SubagentRunner {
60
+ start(state: SubagentState, opts: StartOptions): void;
61
+ respond(state: SubagentState, message: string): void;
62
+ abort(state: SubagentState): void;
63
+ }
64
+
65
+ function resolveTools(agentConfig: AgentConfig): SupportedToolName[] {
66
+ return [...(agentConfig.tools ?? SUPPORTED_TOOL_NAMES)];
67
+ }
68
+
69
+ function resolveModel(agentConfig: AgentConfig, ctx: BootstrapContext): { model: Model<Api> | undefined; warnings: string[] } {
70
+ const warnings: string[] = [];
71
+ const model = ctx.model;
72
+ if (!agentConfig.parsedModel) return { model, warnings };
73
+
74
+ const found = ctx.modelRegistry.find(agentConfig.parsedModel.provider, agentConfig.parsedModel.modelId);
75
+ if (found) return { model: found, warnings };
76
+
77
+ warnings.push(`Model "${agentConfig.model}" not found, using current session model`);
78
+ return { model, warnings };
79
+ }
80
+
81
+ function getSkillWarnings(agentConfig: AgentConfig, resourceLoader: DefaultResourceLoader): string[] {
82
+ const warnings: string[] = [];
83
+ if (!agentConfig.skills) return warnings;
84
+
85
+ const availableSkillNames = new Set(resourceLoader.getSkills().skills.map((skill) => skill.name));
86
+ for (const skillName of agentConfig.skills) {
87
+ if (!availableSkillNames.has(skillName)) {
88
+ warnings.push(`Unknown skill "${skillName}" in subagent config, skipping`);
89
+ }
90
+ }
91
+ return warnings;
92
+ }
93
+
94
+ async function bootstrapSession(opts: BootstrapOptions): Promise<BootstrapResult> {
95
+ const warnings: string[] = [];
96
+ const { agentConfig, cwd, ctx, extensionResolvedPath } = opts;
97
+
98
+ const authStorage = ctx.modelRegistry.authStorage;
99
+ const modelRegistry = ctx.modelRegistry;
100
+ const { model, warnings: modelWarnings } = resolveModel(agentConfig, ctx);
101
+ warnings.push(...modelWarnings);
102
+ const tools = resolveTools(agentConfig);
103
+
104
+ const resourceLoader = new DefaultResourceLoader({
105
+ cwd,
106
+ agentDir: ctx.agentDir,
107
+ extensionsOverride: (base) => ({
108
+ ...base,
109
+ extensions: base.extensions.filter((ext) => !ext.resolvedPath.startsWith(extensionResolvedPath)),
110
+ }),
111
+ skillsOverride: agentConfig.skills
112
+ ? (base) => ({
113
+ skills: base.skills.filter((skill) => agentConfig.skills!.includes(skill.name)),
114
+ diagnostics: base.diagnostics,
115
+ })
116
+ : undefined,
117
+ appendSystemPromptOverride: (base) => agentConfig.systemPrompt.trim() ? [...base, agentConfig.systemPrompt] : base,
118
+ });
119
+ await resourceLoader.reload();
120
+ warnings.push(...getSkillWarnings(agentConfig, resourceLoader));
121
+
122
+ const settingsManager = SettingsManager.inMemory({
123
+ compaction: { enabled: agentConfig.compaction ?? true },
124
+ });
125
+
126
+ const sessionManager = SessionManager.create(cwd);
127
+ sessionManager.newSession({ parentSession: ctx.parentSessionFile });
128
+
129
+ const result = await createAgentSession({
130
+ cwd,
131
+ agentDir: ctx.agentDir,
132
+ model,
133
+ thinkingLevel: agentConfig.thinking,
134
+ tools,
135
+ resourceLoader,
136
+ sessionManager,
137
+ settingsManager,
138
+ authStorage,
139
+ modelRegistry,
140
+ });
141
+
142
+ return { session: result.session, warnings };
143
+ }
144
+
145
+ function getLastAssistantMessage(messages: AgentMessage[]): AssistantMessage | undefined {
146
+ for (let i = messages.length - 1; i >= 0; i--) {
147
+ const msg = messages[i];
148
+ if (msg.role === "assistant") return msg as AssistantMessage;
149
+ }
150
+ return undefined;
151
+ }
152
+
153
+ function getAssistantText(message: AssistantMessage | undefined): string | undefined {
154
+ if (!message) return undefined;
155
+ const texts: string[] = [];
156
+ for (const part of message.content) {
157
+ if (part.type === "text") texts.push(part.text);
158
+ }
159
+ return texts.length > 0 ? texts.join("\n") : undefined;
160
+ }
161
+
162
+ function getPromptOutcome(state: SubagentState): PromptOutcome {
163
+ const lastAssistant = getLastAssistantMessage(state.session!.messages);
164
+ const text = getAssistantText(lastAssistant);
165
+
166
+ if (lastAssistant?.stopReason === "error") {
167
+ return { status: "error", error: lastAssistant.errorMessage ?? text ?? "(no output)" };
168
+ }
169
+ if (lastAssistant?.stopReason === "aborted") {
170
+ return { status: "aborted", error: lastAssistant.errorMessage ?? text ?? "(no output)" };
171
+ }
172
+ return { status: state.agentConfig.interactive ? "waiting" : "done", result: text ?? "(no output)" };
173
+ }
174
+
175
+ function isAborted(state: SubagentState): boolean {
176
+ return state.status === "aborted";
177
+ }
178
+
179
+ export class SubagentSessionRunner implements SubagentRunner {
180
+ constructor(private readonly callbacks: SubagentRunnerCallbacks) {}
181
+
182
+ start(state: SubagentState, opts: StartOptions): void {
183
+ void this.spawnSession(state, opts);
184
+ }
185
+
186
+ respond(state: SubagentState, message: string): void {
187
+ void this.runPromptCycle(state, message);
188
+ }
189
+
190
+ abort(state: SubagentState): void {
191
+ state.promptAbortController?.abort();
192
+ state.promptAbortController = undefined;
193
+ state.session?.abortCompaction();
194
+ state.session?.abortRetry();
195
+ state.session?.abort().catch(() => {});
196
+ }
197
+
198
+ private attachSessionListeners(state: SubagentState, session: AgentSession): void {
199
+ state.unsubscribe = session.subscribe((event) => {
200
+ if (event.type !== "turn_end") return;
201
+ state.turns++;
202
+ const msg = event.message;
203
+ if (msg.role === "assistant") {
204
+ const assistantMsg = msg as AssistantMessage;
205
+ state.contextTokens = assistantMsg.usage.totalTokens;
206
+ state.model = assistantMsg.model;
207
+ }
208
+ this.callbacks.onProgress(state.ownerSessionId);
209
+ });
210
+ }
211
+
212
+ private attachSpawnedSession(state: SubagentState, session: AgentSession): boolean {
213
+ if (!this.callbacks.isCurrent(state)) {
214
+ session.dispose();
215
+ return false;
216
+ }
217
+ state.session = session;
218
+ return true;
219
+ }
220
+
221
+ private async runPromptCycle(state: SubagentState, prompt: string): Promise<void> {
222
+ if (isAborted(state)) return;
223
+
224
+ const abortController = new AbortController();
225
+ state.promptAbortController = abortController;
226
+
227
+ try {
228
+ const recovery = await runPromptWithOverflowRecovery(state.session!, prompt, abortController.signal);
229
+ if (isAborted(state)) return;
230
+
231
+ const outcome = getPromptOutcome(state);
232
+ if (recovery === "failed" && outcome.status !== "error") {
233
+ this.callbacks.onSettled(state, "error", { error: "Context overflow recovery failed" });
234
+ return;
235
+ }
236
+ this.callbacks.onSettled(state, outcome.status, outcome);
237
+ } catch (err) {
238
+ if (isAborted(state)) return;
239
+ const error = err instanceof Error ? err.message : String(err);
240
+ this.callbacks.onSettled(state, "error", { error });
241
+ } finally {
242
+ state.promptAbortController = undefined;
243
+ }
244
+ }
245
+
246
+ private async spawnSession(state: SubagentState, opts: StartOptions): Promise<void> {
247
+ try {
248
+ if (isAborted(state)) return;
249
+
250
+ const { session, warnings } = await bootstrapSession({
251
+ agentConfig: state.agentConfig,
252
+ cwd: opts.cwd,
253
+ ctx: opts.ctx,
254
+ extensionResolvedPath: opts.extensionResolvedPath,
255
+ });
256
+
257
+ for (const warning of warnings) opts.onWarning?.(warning);
258
+ if (!this.attachSpawnedSession(state, session)) return;
259
+
260
+ this.attachSessionListeners(state, session);
261
+ await this.runPromptCycle(state, state.task);
262
+ } catch (err) {
263
+ if (isAborted(state)) return;
264
+ if (state.status === "running") {
265
+ const error = err instanceof Error ? err.message : String(err);
266
+ this.callbacks.onSettled(state, "error", { error });
267
+ }
268
+ }
269
+ }
270
+ }