sub-agents-mcp 0.14.1 → 0.14.2

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 (47) hide show
  1. package/dist/agents/AgentManager.d.ts +16 -0
  2. package/dist/agents/AgentManager.d.ts.map +1 -1
  3. package/dist/agents/AgentManager.js +68 -43
  4. package/dist/agents/AgentManager.js.map +1 -1
  5. package/dist/agents/AgentName.d.ts +15 -0
  6. package/dist/agents/AgentName.d.ts.map +1 -0
  7. package/dist/agents/AgentName.js +40 -0
  8. package/dist/agents/AgentName.js.map +1 -0
  9. package/dist/config/ServerConfig.d.ts.map +1 -1
  10. package/dist/config/ServerConfig.js +101 -70
  11. package/dist/config/ServerConfig.js.map +1 -1
  12. package/dist/execution/AgentExecutor.d.ts +27 -3
  13. package/dist/execution/AgentExecutor.d.ts.map +1 -1
  14. package/dist/execution/AgentExecutor.js +328 -196
  15. package/dist/execution/AgentExecutor.js.map +1 -1
  16. package/dist/execution/StreamProcessor.d.ts.map +1 -1
  17. package/dist/execution/StreamProcessor.js +17 -16
  18. package/dist/execution/StreamProcessor.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +8 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/resources/AgentResources.d.ts.map +1 -1
  23. package/dist/resources/AgentResources.js +2 -8
  24. package/dist/resources/AgentResources.js.map +1 -1
  25. package/dist/server/McpServer.d.ts.map +1 -1
  26. package/dist/server/McpServer.js +40 -21
  27. package/dist/server/McpServer.js.map +1 -1
  28. package/dist/session/SessionManager.d.ts +11 -1
  29. package/dist/session/SessionManager.d.ts.map +1 -1
  30. package/dist/session/SessionManager.js +118 -28
  31. package/dist/session/SessionManager.js.map +1 -1
  32. package/dist/tools/RunAgentTool.d.ts +32 -9
  33. package/dist/tools/RunAgentTool.d.ts.map +1 -1
  34. package/dist/tools/RunAgentTool.js +281 -190
  35. package/dist/tools/RunAgentTool.js.map +1 -1
  36. package/dist/types/ExecutionParams.d.ts +0 -1
  37. package/dist/types/ExecutionParams.d.ts.map +1 -1
  38. package/dist/types/SessionData.d.ts +6 -1
  39. package/dist/types/SessionData.d.ts.map +1 -1
  40. package/dist/utils/ErrorHandler.d.ts +14 -2
  41. package/dist/utils/ErrorHandler.d.ts.map +1 -1
  42. package/dist/utils/ErrorHandler.js +40 -9
  43. package/dist/utils/ErrorHandler.js.map +1 -1
  44. package/dist/utils/Logger.d.ts.map +1 -1
  45. package/dist/utils/Logger.js +3 -2
  46. package/dist/utils/Logger.js.map +1 -1
  47. package/package.json +10 -14
@@ -3,7 +3,8 @@ import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { StringDecoder } from 'node:string_decoder';
6
- import { Logger } from '../utils/Logger.js';
6
+ import { toErrorMessage } from '../utils/ErrorHandler.js';
7
+ import { isLogLevel, Logger } from '../utils/Logger.js';
7
8
  import { StreamProcessor } from './StreamProcessor.js';
8
9
  export const DEFAULT_EXECUTION_TIMEOUT = 300000; // 5 minutes
9
10
  const MAX_CAPTURED_OUTPUT_BYTES = 16 * 1024 * 1024;
@@ -31,7 +32,7 @@ export const AGENT_TYPES = [
31
32
  'command-code',
32
33
  ];
33
34
  export function isAgentType(value) {
34
- return typeof value === 'string' && AGENT_TYPES.includes(value);
35
+ return typeof value === 'string' && AGENT_TYPES.some((agentType) => agentType === value);
35
36
  }
36
37
  export const AGENT_EFFORT_SUPPORTED_TYPES = [
37
38
  'codex',
@@ -44,11 +45,11 @@ export const AGENT_EFFORT_SUPPORTED_TYPES = [
44
45
  'command-code',
45
46
  ];
46
47
  export function supportsAgentEffort(agentType) {
47
- return AGENT_EFFORT_SUPPORTED_TYPES.includes(agentType);
48
+ return AGENT_EFFORT_SUPPORTED_TYPES.some((supported) => supported === agentType);
48
49
  }
49
50
  export const AGENT_PERMISSIONS = ['read-only', 'safe-edit', 'yolo'];
50
51
  export function isAgentPermission(value) {
51
- return typeof value === 'string' && AGENT_PERMISSIONS.includes(value);
52
+ return typeof value === 'string' && AGENT_PERMISSIONS.some((permission) => permission === value);
52
53
  }
53
54
  export const DEFAULT_AGENT_PERMISSION = 'safe-edit';
54
55
  const PERMISSION_FLAGS = {
@@ -135,14 +136,258 @@ export function createExecutionConfig(agentType, overrides) {
135
136
  agentType,
136
137
  };
137
138
  }
139
+ /** Reads LOG_LEVEL from the environment, falling back to `info` when unset or invalid. */
140
+ function resolveLogLevelFromEnv() {
141
+ const value = process.env['LOG_LEVEL'];
142
+ return isLogLevel(value) ? value : 'info';
143
+ }
144
+ function errorCode(error) {
145
+ if (typeof error !== 'object' || error === null || !('code' in error)) {
146
+ return undefined;
147
+ }
148
+ return typeof error.code === 'string' ? error.code : undefined;
149
+ }
150
+ function signalNumber(signal) {
151
+ if (signal === 'SIGTERM') {
152
+ return 15;
153
+ }
154
+ if (signal === 'SIGKILL') {
155
+ return 9;
156
+ }
157
+ return 1;
158
+ }
159
+ /**
160
+ * Owns the lifecycle of one spawned agent process: output capture with a byte
161
+ * cap, incremental stream parsing, timeout-driven termination, and settlement
162
+ * into a single {@link SpawnOutcome}.
163
+ */
164
+ class SpawnSession {
165
+ childProcess;
166
+ config;
167
+ logger;
168
+ cleanup;
169
+ streamProcessor;
170
+ stdoutParts = [];
171
+ stderrParts = [];
172
+ stdoutDecoder = new StringDecoder('utf8');
173
+ stderrDecoder = new StringDecoder('utf8');
174
+ stdoutLineParts = [];
175
+ stdoutTruncated = false;
176
+ stderrTruncated = false;
177
+ capturedBytes = 0;
178
+ timedOut = false;
179
+ cancelled = false;
180
+ outputExceeded = false;
181
+ processError;
182
+ settled = false;
183
+ forceKillTimer;
184
+ executionTimeout;
185
+ constructor(childProcess, config, logger, cleanup) {
186
+ this.childProcess = childProcess;
187
+ this.config = config;
188
+ this.logger = logger;
189
+ this.cleanup = cleanup;
190
+ this.streamProcessor = new StreamProcessor(config.agentType);
191
+ }
192
+ run(cancelSignal) {
193
+ return new Promise((resolve) => {
194
+ if (cancelSignal) {
195
+ // Reuses the same graceful SIGTERM -> SIGKILL path as a timeout, so a
196
+ // cancelled request cannot leave the CLI running.
197
+ if (cancelSignal.aborted) {
198
+ queueMicrotask(() => this.cancel());
199
+ }
200
+ else {
201
+ cancelSignal.addEventListener('abort', () => this.cancel(), { once: true });
202
+ }
203
+ }
204
+ const settle = (code, signal) => {
205
+ if (this.settled) {
206
+ return;
207
+ }
208
+ this.settled = true;
209
+ this.finish(code, signal).then(resolve, (error) => {
210
+ resolve({
211
+ stdout: '',
212
+ stderr: toErrorMessage(error),
213
+ exitCode: 1,
214
+ hasResult: false,
215
+ });
216
+ });
217
+ };
218
+ this.executionTimeout = setTimeout(() => {
219
+ this.timedOut = true;
220
+ this.logger.warn('Execution timeout reached', { timeout: this.config.executionTimeout });
221
+ this.requestTermination();
222
+ }, this.config.executionTimeout);
223
+ this.childProcess.stdout?.on('data', (data) => {
224
+ this.consumeStdout(data);
225
+ });
226
+ this.childProcess.stderr?.on('data', (data) => {
227
+ this.stderrParts.push(this.captureChunk(data, this.stderrDecoder, () => {
228
+ this.stderrTruncated = true;
229
+ }));
230
+ });
231
+ this.childProcess.on('close', (code, signal) => {
232
+ settle(code, signal);
233
+ });
234
+ this.childProcess.on('error', (error) => {
235
+ this.processError = error;
236
+ settle(null);
237
+ });
238
+ });
239
+ }
240
+ /**
241
+ * Stops the agent process, whether the client cancelled the request or the
242
+ * server is shutting down. Reuses the graceful SIGTERM -> SIGKILL escalation.
243
+ */
244
+ cancel() {
245
+ if (this.settled || this.cancelled) {
246
+ return;
247
+ }
248
+ this.cancelled = true;
249
+ this.logger.info('Execution cancelled before the agent finished');
250
+ this.requestTermination();
251
+ }
252
+ clearTimers() {
253
+ if (this.executionTimeout) {
254
+ clearTimeout(this.executionTimeout);
255
+ }
256
+ if (this.forceKillTimer) {
257
+ clearTimeout(this.forceKillTimer);
258
+ }
259
+ }
260
+ requestTermination() {
261
+ this.childProcess.kill('SIGTERM');
262
+ if (this.forceKillTimer) {
263
+ return;
264
+ }
265
+ this.forceKillTimer = setTimeout(() => {
266
+ this.childProcess.kill('SIGKILL');
267
+ }, TERMINATION_GRACE_MS);
268
+ }
269
+ /**
270
+ * Copies at most the remaining byte budget out of `data`, flagging truncation
271
+ * and terminating the process once the cap is reached.
272
+ */
273
+ captureChunk(data, decoder, markTruncated) {
274
+ const remaining = this.config.maxOutputBytes - this.capturedBytes;
275
+ if (remaining <= 0) {
276
+ this.outputExceeded = true;
277
+ markTruncated();
278
+ this.requestTermination();
279
+ return '';
280
+ }
281
+ const captured = data.length <= remaining ? data : data.subarray(0, remaining);
282
+ this.capturedBytes += captured.length;
283
+ if (captured.length < data.length) {
284
+ this.outputExceeded = true;
285
+ markTruncated();
286
+ this.requestTermination();
287
+ }
288
+ return decoder.write(captured);
289
+ }
290
+ consumeStdout(data) {
291
+ const chunk = this.captureChunk(data, this.stdoutDecoder, () => {
292
+ this.stdoutTruncated = true;
293
+ });
294
+ this.stdoutParts.push(chunk);
295
+ let chunkOffset = 0;
296
+ while (chunkOffset < chunk.length) {
297
+ const newlineIndex = chunk.indexOf('\n', chunkOffset);
298
+ if (newlineIndex < 0) {
299
+ this.stdoutLineParts.push(chunk.slice(chunkOffset));
300
+ break;
301
+ }
302
+ this.stdoutLineParts.push(chunk.slice(chunkOffset, newlineIndex));
303
+ const line = this.stdoutLineParts.join('');
304
+ this.stdoutLineParts = [];
305
+ chunkOffset = newlineIndex + 1;
306
+ if (this.streamProcessor.processLine(line)) {
307
+ this.requestTermination();
308
+ break;
309
+ }
310
+ }
311
+ }
312
+ /** Flushes both decoders and parses any line left without a trailing newline. */
313
+ flushStreams() {
314
+ if (!this.stdoutTruncated) {
315
+ const tail = this.stdoutDecoder.end();
316
+ this.stdoutParts.push(tail);
317
+ this.stdoutLineParts.push(tail);
318
+ }
319
+ if (!this.stderrTruncated) {
320
+ this.stderrParts.push(this.stderrDecoder.end());
321
+ }
322
+ const trailingLine = this.stdoutLineParts.join('');
323
+ if (trailingLine.trim()) {
324
+ this.streamProcessor.processLine(trailingLine);
325
+ }
326
+ this.stdoutLineParts = [];
327
+ }
328
+ resolveExitCode(code, signal) {
329
+ if (this.outputExceeded || this.processError) {
330
+ return errorCode(this.processError) === 'ENOENT' ? 127 : 1;
331
+ }
332
+ if (this.timedOut) {
333
+ return 124;
334
+ }
335
+ if (this.cancelled) {
336
+ return 130;
337
+ }
338
+ return code ?? (signal ? 128 + signalNumber(signal) : 1);
339
+ }
340
+ collectErrors(stderr) {
341
+ const errors = [];
342
+ if (stderr) {
343
+ errors.push(stderr);
344
+ }
345
+ if (this.timedOut) {
346
+ errors.push(`Execution timeout: ${this.config.executionTimeout}ms`);
347
+ }
348
+ if (this.cancelled) {
349
+ errors.push('Execution was cancelled by the client before the agent finished.');
350
+ }
351
+ if (this.outputExceeded) {
352
+ errors.push(`Sub-agent output exceeded ${this.config.maxOutputBytes} bytes`);
353
+ }
354
+ if (this.processError && !stderr) {
355
+ errors.push(this.processError.message);
356
+ }
357
+ return errors;
358
+ }
359
+ async finish(code, signal) {
360
+ this.clearTimers();
361
+ this.flushStreams();
362
+ const stdout = this.stdoutParts.join('');
363
+ const stderr = this.stderrParts.join('');
364
+ let result = this.streamProcessor.getResult();
365
+ if (result === null) {
366
+ this.streamProcessor.processCompleteOutput(stdout);
367
+ result = this.streamProcessor.getResult();
368
+ }
369
+ await this.cleanup();
370
+ return {
371
+ stdout: result ? JSON.stringify(result) : stdout,
372
+ stderr: this.collectErrors(stderr).join('\n'),
373
+ exitCode: this.resolveExitCode(code, signal),
374
+ hasResult: result !== null,
375
+ resultJson: result !== null ? result : undefined,
376
+ };
377
+ }
378
+ }
138
379
  export class AgentExecutor {
139
380
  config;
140
381
  logger;
141
382
  constructor(config, logger) {
142
383
  this.config = config;
143
- this.logger = logger || new Logger(process.env['LOG_LEVEL'] || 'info');
384
+ this.logger = logger || new Logger(resolveLogLevelFromEnv());
144
385
  }
145
- async executeAgent(params) {
386
+ /**
387
+ * Guards against untrusted callers that bypass the declared parameter type
388
+ * (for example MCP requests deserialized as `unknown`).
389
+ */
390
+ assertExecutableParams(params) {
146
391
  if (!params?.agent || !params.prompt) {
147
392
  const error = 'Invalid execution parameters: agent and prompt are required';
148
393
  this.logger.error('Agent execution failed during validation', undefined, { error, params });
@@ -153,6 +398,17 @@ export class AgentExecutor {
153
398
  this.logger.error('Agent execution failed during validation', undefined, { error, params });
154
399
  throw new Error(error);
155
400
  }
401
+ }
402
+ /** Sessions still running, so shutdown does not orphan agent processes. */
403
+ activeSessions = new Set();
404
+ /** Terminates every agent process this executor still has running. */
405
+ terminateAll() {
406
+ for (const session of this.activeSessions) {
407
+ session.cancel();
408
+ }
409
+ }
410
+ async executeAgent(params, signal) {
411
+ this.assertExecutableParams(params);
156
412
  const startTime = Date.now();
157
413
  const requestId = this.generateRequestId();
158
414
  this.logger.info('Starting agent execution', {
@@ -160,12 +416,9 @@ export class AgentExecutor {
160
416
  agent: params.agent,
161
417
  promptLength: params.prompt.length,
162
418
  cwd: params.cwd,
163
- extraArgs: params.extra_args?.length || 0,
164
419
  });
165
420
  try {
166
- // Add minimal delay to ensure execution time is measurable
167
- await new Promise((resolve) => setTimeout(resolve, 1));
168
- const result = await this.executeWithSpawn(params);
421
+ const result = await this.executeWithSpawn(params, signal);
169
422
  const executionTime = Date.now() - startTime;
170
423
  this.logger.info('Agent execution completed', {
171
424
  requestId,
@@ -180,6 +433,7 @@ export class AgentExecutor {
180
433
  executionTime,
181
434
  ...(result.hasResult !== undefined && { hasResult: result.hasResult }),
182
435
  ...(result.resultJson !== undefined && { resultJson: result.resultJson }),
436
+ ...(result.failureReason !== undefined && { failureReason: result.failureReason }),
183
437
  };
184
438
  }
185
439
  catch (error) {
@@ -188,10 +442,6 @@ export class AgentExecutor {
188
442
  requestId,
189
443
  executionTime,
190
444
  });
191
- if (error instanceof Error &&
192
- (error.message.includes('enhance') || error.message.includes('Enhancement'))) {
193
- throw error;
194
- }
195
445
  return {
196
446
  stdout: '',
197
447
  stderr: error instanceof Error ? error.message : 'Unknown execution error',
@@ -229,8 +479,9 @@ export class AgentExecutor {
229
479
  }
230
480
  buildSettingsPathEnv() {
231
481
  const env = {};
232
- if (!this.config.agentsSettingsPath)
482
+ if (!this.config.agentsSettingsPath) {
233
483
  return env;
484
+ }
234
485
  switch (this.config.agentType) {
235
486
  case 'cursor':
236
487
  env['CURSOR_CONFIG_DIR'] = this.config.agentsSettingsPath;
@@ -331,16 +582,25 @@ export class AgentExecutor {
331
582
  if (!apiKey?.trim()) {
332
583
  throw new Error(GLM_MISSING_API_KEY_ERROR);
333
584
  }
334
- return this.buildRedirectedClaudeArgs(params, envOverrides, GLM_BASE_URL, apiKey, 'ANTHROPIC_AUTH_TOKEN');
585
+ return this.buildRedirectedClaudeArgs(params, envOverrides, {
586
+ baseUrl: GLM_BASE_URL,
587
+ apiKey,
588
+ credentialEnv: 'ANTHROPIC_AUTH_TOKEN',
589
+ });
335
590
  }
336
591
  buildKimiArgs(params, envOverrides) {
337
592
  const apiKey = this.config.kimiApiKey;
338
593
  if (!apiKey?.trim()) {
339
594
  throw new Error(KIMI_MISSING_API_KEY_ERROR);
340
595
  }
341
- return this.buildRedirectedClaudeArgs(params, envOverrides, KIMI_BASE_URL, apiKey, 'ANTHROPIC_API_KEY');
596
+ return this.buildRedirectedClaudeArgs(params, envOverrides, {
597
+ baseUrl: KIMI_BASE_URL,
598
+ apiKey,
599
+ credentialEnv: 'ANTHROPIC_API_KEY',
600
+ });
342
601
  }
343
- buildRedirectedClaudeArgs(params, envOverrides, baseUrl, apiKey, credentialEnv) {
602
+ buildRedirectedClaudeArgs(params, envOverrides, redirect) {
603
+ const { baseUrl, apiKey, credentialEnv } = redirect;
344
604
  const flags = this.invocationFlags();
345
605
  const cwd = params.cwd || process.cwd();
346
606
  const systemPrompt = `cwd: ${cwd}\n\n${params.agent}`;
@@ -459,10 +719,10 @@ export class AgentExecutor {
459
719
  await fs.promises.copyFile(authSource, authDestination);
460
720
  }
461
721
  catch (error) {
462
- const code = this.errorCode(error);
722
+ const code = errorCode(error);
463
723
  if (code !== 'ENOENT') {
464
724
  this.logger.warn('Could not copy OpenCode authentication into isolated data home', {
465
- error: error instanceof Error ? error.message : String(error),
725
+ error: toErrorMessage(error),
466
726
  });
467
727
  }
468
728
  }
@@ -482,189 +742,61 @@ export class AgentExecutor {
482
742
  throw error;
483
743
  }
484
744
  }
485
- errorCode(error) {
486
- if (typeof error !== 'object' || error === null || !('code' in error)) {
487
- return undefined;
488
- }
489
- return typeof error.code === 'string' ? error.code : undefined;
490
- }
491
- async executeWithSpawn(params) {
745
+ async executeWithSpawn(params, signal) {
492
746
  const { command, args, envOverrides } = this.buildCommandArgs(params);
493
747
  const preparedEnvironment = await this.prepareSpawnEnvironment(envOverrides);
494
- return new Promise((resolve) => {
495
- this.logger.debug('Executing with spawn', {
496
- command,
748
+ this.logger.debug('Executing with spawn', {
749
+ command,
750
+ cwd: params.cwd || process.cwd(),
751
+ });
752
+ let childProcess;
753
+ try {
754
+ childProcess = spawn(command, args, {
497
755
  cwd: params.cwd || process.cwd(),
756
+ stdio: ['ignore', 'pipe', 'pipe'],
757
+ shell: false,
758
+ env: preparedEnvironment.env,
498
759
  });
499
- let childProcess;
500
- try {
501
- childProcess = spawn(command, args, {
502
- cwd: params.cwd || process.cwd(),
503
- stdio: ['ignore', 'pipe', 'pipe'],
504
- shell: false,
505
- env: preparedEnvironment.env,
506
- });
507
- }
508
- catch (error) {
509
- void preparedEnvironment.cleanup().finally(() => {
510
- resolve({
511
- stdout: '',
512
- stderr: error instanceof Error ? error.message : String(error),
513
- exitCode: this.errorCode(error) === 'ENOENT' ? 127 : 1,
514
- hasResult: false,
515
- });
516
- });
517
- return;
760
+ }
761
+ catch (error) {
762
+ await this.cleanupQuietly(preparedEnvironment.cleanup);
763
+ if (errorCode(error) === 'E2BIG') {
764
+ const promptBytes = Buffer.byteLength(params.prompt, 'utf8');
765
+ return {
766
+ stdout: '',
767
+ stderr: `The prompt is too large to pass to the "${command}" CLI: ` +
768
+ `${promptBytes} bytes exceeds this operating system's argument limit.`,
769
+ exitCode: 1,
770
+ hasResult: false,
771
+ failureReason: 'argv_too_long',
772
+ };
518
773
  }
519
- const streamProcessor = new StreamProcessor(this.config.agentType);
520
- const stdoutParts = [];
521
- const stderrParts = [];
522
- let stdoutLineParts = [];
523
- const stdoutDecoder = new StringDecoder('utf8');
524
- const stderrDecoder = new StringDecoder('utf8');
525
- let stdoutTruncated = false;
526
- let stderrTruncated = false;
527
- let capturedBytes = 0;
528
- let timedOut = false;
529
- let outputExceeded = false;
530
- let processError;
531
- let settled = false;
532
- let forceKillTimer;
533
- const executionTimeout = setTimeout(() => {
534
- timedOut = true;
535
- this.logger.warn('Execution timeout reached', {
536
- timeout: this.config.executionTimeout,
537
- });
538
- requestTermination();
539
- }, this.config.executionTimeout);
540
- const clearTimers = () => {
541
- clearTimeout(executionTimeout);
542
- if (forceKillTimer)
543
- clearTimeout(forceKillTimer);
544
- };
545
- const finish = async (code, signal) => {
546
- if (settled)
547
- return;
548
- settled = true;
549
- clearTimers();
550
- if (!stdoutTruncated) {
551
- const tail = stdoutDecoder.end();
552
- stdoutParts.push(tail);
553
- stdoutLineParts.push(tail);
554
- }
555
- if (!stderrTruncated) {
556
- stderrParts.push(stderrDecoder.end());
557
- }
558
- const trailingLine = stdoutLineParts.join('');
559
- if (trailingLine.trim()) {
560
- streamProcessor.processLine(trailingLine);
561
- }
562
- stdoutLineParts = [];
563
- const stdout = stdoutParts.join('');
564
- const stderr = stderrParts.join('');
565
- let result = streamProcessor.getResult();
566
- if (result === null) {
567
- streamProcessor.processCompleteOutput(stdout);
568
- result = streamProcessor.getResult();
569
- }
570
- let exitCode = code ?? (signal ? 128 + this.signalNumber(signal) : 1);
571
- if (timedOut)
572
- exitCode = 124;
573
- if (outputExceeded || processError)
574
- exitCode = this.errorCode(processError) === 'ENOENT' ? 127 : 1;
575
- const errors = [];
576
- if (stderr)
577
- errors.push(stderr);
578
- if (timedOut)
579
- errors.push(`Execution timeout: ${this.config.executionTimeout}ms`);
580
- if (outputExceeded) {
581
- errors.push(`Sub-agent output exceeded ${this.config.maxOutputBytes} bytes`);
582
- }
583
- if (processError && !stderr)
584
- errors.push(processError.message);
585
- try {
586
- await preparedEnvironment.cleanup();
587
- }
588
- catch (error) {
589
- this.logger.warn('Failed to clean up per-run environment', {
590
- error: error instanceof Error ? error.message : String(error),
591
- });
592
- }
593
- resolve({
594
- stdout: result ? JSON.stringify(result) : stdout,
595
- stderr: errors.join('\n'),
596
- exitCode,
597
- hasResult: result !== null,
598
- resultJson: result !== null ? result : undefined,
599
- });
600
- };
601
- const requestTermination = () => {
602
- childProcess.kill('SIGTERM');
603
- if (forceKillTimer)
604
- return;
605
- forceKillTimer = setTimeout(() => {
606
- childProcess.kill('SIGKILL');
607
- }, TERMINATION_GRACE_MS);
608
- };
609
- const captureChunk = (data, decoder, markTruncated) => {
610
- const remaining = this.config.maxOutputBytes - capturedBytes;
611
- if (remaining <= 0) {
612
- outputExceeded = true;
613
- markTruncated();
614
- requestTermination();
615
- return '';
616
- }
617
- const captured = data.length <= remaining ? data : data.subarray(0, remaining);
618
- capturedBytes += captured.length;
619
- if (captured.length < data.length) {
620
- outputExceeded = true;
621
- markTruncated();
622
- requestTermination();
623
- }
624
- return decoder.write(captured);
774
+ return {
775
+ stdout: '',
776
+ stderr: toErrorMessage(error),
777
+ exitCode: errorCode(error) === 'ENOENT' ? 127 : 1,
778
+ hasResult: false,
625
779
  };
626
- childProcess.stdout?.on('data', (data) => {
627
- const chunk = captureChunk(data, stdoutDecoder, () => {
628
- stdoutTruncated = true;
629
- });
630
- stdoutParts.push(chunk);
631
- let chunkOffset = 0;
632
- while (chunkOffset < chunk.length) {
633
- const newlineIndex = chunk.indexOf('\n', chunkOffset);
634
- if (newlineIndex < 0) {
635
- stdoutLineParts.push(chunk.slice(chunkOffset));
636
- break;
637
- }
638
- stdoutLineParts.push(chunk.slice(chunkOffset, newlineIndex));
639
- const line = stdoutLineParts.join('');
640
- stdoutLineParts = [];
641
- chunkOffset = newlineIndex + 1;
642
- if (streamProcessor.processLine(line)) {
643
- requestTermination();
644
- break;
645
- }
646
- }
647
- });
648
- childProcess.stderr?.on('data', (data) => {
649
- stderrParts.push(captureChunk(data, stderrDecoder, () => {
650
- stderrTruncated = true;
651
- }));
652
- });
653
- childProcess.on('close', (code, signal) => {
654
- void finish(code, signal);
655
- });
656
- childProcess.on('error', (error) => {
657
- processError = error;
658
- void finish(null);
659
- });
660
- });
780
+ }
781
+ const session = new SpawnSession(childProcess, this.config, this.logger, () => this.cleanupQuietly(preparedEnvironment.cleanup));
782
+ this.activeSessions.add(session);
783
+ try {
784
+ return await session.run(signal);
785
+ }
786
+ finally {
787
+ this.activeSessions.delete(session);
788
+ }
661
789
  }
662
- signalNumber(signal) {
663
- if (signal === 'SIGTERM')
664
- return 15;
665
- if (signal === 'SIGKILL')
666
- return 9;
667
- return 1;
790
+ /** Runs a cleanup callback, logging rather than propagating its failures. */
791
+ async cleanupQuietly(cleanup) {
792
+ try {
793
+ await cleanup();
794
+ }
795
+ catch (error) {
796
+ this.logger.warn('Failed to clean up per-run environment', {
797
+ error: toErrorMessage(error),
798
+ });
799
+ }
668
800
  }
669
801
  generateRequestId() {
670
802
  return `req_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;