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.
- package/dist/agents/AgentManager.d.ts +16 -0
- package/dist/agents/AgentManager.d.ts.map +1 -1
- package/dist/agents/AgentManager.js +68 -43
- package/dist/agents/AgentManager.js.map +1 -1
- package/dist/agents/AgentName.d.ts +15 -0
- package/dist/agents/AgentName.d.ts.map +1 -0
- package/dist/agents/AgentName.js +40 -0
- package/dist/agents/AgentName.js.map +1 -0
- package/dist/config/ServerConfig.d.ts.map +1 -1
- package/dist/config/ServerConfig.js +101 -70
- package/dist/config/ServerConfig.js.map +1 -1
- package/dist/execution/AgentExecutor.d.ts +27 -3
- package/dist/execution/AgentExecutor.d.ts.map +1 -1
- package/dist/execution/AgentExecutor.js +328 -196
- package/dist/execution/AgentExecutor.js.map +1 -1
- package/dist/execution/StreamProcessor.d.ts.map +1 -1
- package/dist/execution/StreamProcessor.js +17 -16
- package/dist/execution/StreamProcessor.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/AgentResources.d.ts.map +1 -1
- package/dist/resources/AgentResources.js +2 -8
- package/dist/resources/AgentResources.js.map +1 -1
- package/dist/server/McpServer.d.ts.map +1 -1
- package/dist/server/McpServer.js +40 -21
- package/dist/server/McpServer.js.map +1 -1
- package/dist/session/SessionManager.d.ts +11 -1
- package/dist/session/SessionManager.d.ts.map +1 -1
- package/dist/session/SessionManager.js +118 -28
- package/dist/session/SessionManager.js.map +1 -1
- package/dist/tools/RunAgentTool.d.ts +32 -9
- package/dist/tools/RunAgentTool.d.ts.map +1 -1
- package/dist/tools/RunAgentTool.js +281 -190
- package/dist/tools/RunAgentTool.js.map +1 -1
- package/dist/types/ExecutionParams.d.ts +0 -1
- package/dist/types/ExecutionParams.d.ts.map +1 -1
- package/dist/types/SessionData.d.ts +6 -1
- package/dist/types/SessionData.d.ts.map +1 -1
- package/dist/utils/ErrorHandler.d.ts +14 -2
- package/dist/utils/ErrorHandler.d.ts.map +1 -1
- package/dist/utils/ErrorHandler.js +40 -9
- package/dist/utils/ErrorHandler.js.map +1 -1
- package/dist/utils/Logger.d.ts.map +1 -1
- package/dist/utils/Logger.js +3 -2
- package/dist/utils/Logger.js.map +1 -1
- 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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
384
|
+
this.logger = logger || new Logger(resolveLogLevelFromEnv());
|
|
144
385
|
}
|
|
145
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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)}`;
|