sub-agents-mcp 0.14.0 → 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/README.md +67 -544
- 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 -197
- 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 = {
|
|
@@ -110,7 +111,6 @@ const PERMISSION_FLAGS = {
|
|
|
110
111
|
const OPENCODE_PERMISSION_MAPPING = {
|
|
111
112
|
'read-only': {
|
|
112
113
|
edit: 'deny',
|
|
113
|
-
bash: 'deny',
|
|
114
114
|
task: 'deny',
|
|
115
115
|
external_directory: 'deny',
|
|
116
116
|
question: 'deny',
|
|
@@ -136,14 +136,258 @@ export function createExecutionConfig(agentType, overrides) {
|
|
|
136
136
|
agentType,
|
|
137
137
|
};
|
|
138
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
|
+
}
|
|
139
379
|
export class AgentExecutor {
|
|
140
380
|
config;
|
|
141
381
|
logger;
|
|
142
382
|
constructor(config, logger) {
|
|
143
383
|
this.config = config;
|
|
144
|
-
this.logger = logger || new Logger(
|
|
384
|
+
this.logger = logger || new Logger(resolveLogLevelFromEnv());
|
|
145
385
|
}
|
|
146
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Guards against untrusted callers that bypass the declared parameter type
|
|
388
|
+
* (for example MCP requests deserialized as `unknown`).
|
|
389
|
+
*/
|
|
390
|
+
assertExecutableParams(params) {
|
|
147
391
|
if (!params?.agent || !params.prompt) {
|
|
148
392
|
const error = 'Invalid execution parameters: agent and prompt are required';
|
|
149
393
|
this.logger.error('Agent execution failed during validation', undefined, { error, params });
|
|
@@ -154,6 +398,17 @@ export class AgentExecutor {
|
|
|
154
398
|
this.logger.error('Agent execution failed during validation', undefined, { error, params });
|
|
155
399
|
throw new Error(error);
|
|
156
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);
|
|
157
412
|
const startTime = Date.now();
|
|
158
413
|
const requestId = this.generateRequestId();
|
|
159
414
|
this.logger.info('Starting agent execution', {
|
|
@@ -161,12 +416,9 @@ export class AgentExecutor {
|
|
|
161
416
|
agent: params.agent,
|
|
162
417
|
promptLength: params.prompt.length,
|
|
163
418
|
cwd: params.cwd,
|
|
164
|
-
extraArgs: params.extra_args?.length || 0,
|
|
165
419
|
});
|
|
166
420
|
try {
|
|
167
|
-
|
|
168
|
-
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
169
|
-
const result = await this.executeWithSpawn(params);
|
|
421
|
+
const result = await this.executeWithSpawn(params, signal);
|
|
170
422
|
const executionTime = Date.now() - startTime;
|
|
171
423
|
this.logger.info('Agent execution completed', {
|
|
172
424
|
requestId,
|
|
@@ -181,6 +433,7 @@ export class AgentExecutor {
|
|
|
181
433
|
executionTime,
|
|
182
434
|
...(result.hasResult !== undefined && { hasResult: result.hasResult }),
|
|
183
435
|
...(result.resultJson !== undefined && { resultJson: result.resultJson }),
|
|
436
|
+
...(result.failureReason !== undefined && { failureReason: result.failureReason }),
|
|
184
437
|
};
|
|
185
438
|
}
|
|
186
439
|
catch (error) {
|
|
@@ -189,10 +442,6 @@ export class AgentExecutor {
|
|
|
189
442
|
requestId,
|
|
190
443
|
executionTime,
|
|
191
444
|
});
|
|
192
|
-
if (error instanceof Error &&
|
|
193
|
-
(error.message.includes('enhance') || error.message.includes('Enhancement'))) {
|
|
194
|
-
throw error;
|
|
195
|
-
}
|
|
196
445
|
return {
|
|
197
446
|
stdout: '',
|
|
198
447
|
stderr: error instanceof Error ? error.message : 'Unknown execution error',
|
|
@@ -230,8 +479,9 @@ export class AgentExecutor {
|
|
|
230
479
|
}
|
|
231
480
|
buildSettingsPathEnv() {
|
|
232
481
|
const env = {};
|
|
233
|
-
if (!this.config.agentsSettingsPath)
|
|
482
|
+
if (!this.config.agentsSettingsPath) {
|
|
234
483
|
return env;
|
|
484
|
+
}
|
|
235
485
|
switch (this.config.agentType) {
|
|
236
486
|
case 'cursor':
|
|
237
487
|
env['CURSOR_CONFIG_DIR'] = this.config.agentsSettingsPath;
|
|
@@ -332,16 +582,25 @@ export class AgentExecutor {
|
|
|
332
582
|
if (!apiKey?.trim()) {
|
|
333
583
|
throw new Error(GLM_MISSING_API_KEY_ERROR);
|
|
334
584
|
}
|
|
335
|
-
return this.buildRedirectedClaudeArgs(params, envOverrides,
|
|
585
|
+
return this.buildRedirectedClaudeArgs(params, envOverrides, {
|
|
586
|
+
baseUrl: GLM_BASE_URL,
|
|
587
|
+
apiKey,
|
|
588
|
+
credentialEnv: 'ANTHROPIC_AUTH_TOKEN',
|
|
589
|
+
});
|
|
336
590
|
}
|
|
337
591
|
buildKimiArgs(params, envOverrides) {
|
|
338
592
|
const apiKey = this.config.kimiApiKey;
|
|
339
593
|
if (!apiKey?.trim()) {
|
|
340
594
|
throw new Error(KIMI_MISSING_API_KEY_ERROR);
|
|
341
595
|
}
|
|
342
|
-
return this.buildRedirectedClaudeArgs(params, envOverrides,
|
|
596
|
+
return this.buildRedirectedClaudeArgs(params, envOverrides, {
|
|
597
|
+
baseUrl: KIMI_BASE_URL,
|
|
598
|
+
apiKey,
|
|
599
|
+
credentialEnv: 'ANTHROPIC_API_KEY',
|
|
600
|
+
});
|
|
343
601
|
}
|
|
344
|
-
buildRedirectedClaudeArgs(params, envOverrides,
|
|
602
|
+
buildRedirectedClaudeArgs(params, envOverrides, redirect) {
|
|
603
|
+
const { baseUrl, apiKey, credentialEnv } = redirect;
|
|
345
604
|
const flags = this.invocationFlags();
|
|
346
605
|
const cwd = params.cwd || process.cwd();
|
|
347
606
|
const systemPrompt = `cwd: ${cwd}\n\n${params.agent}`;
|
|
@@ -460,10 +719,10 @@ export class AgentExecutor {
|
|
|
460
719
|
await fs.promises.copyFile(authSource, authDestination);
|
|
461
720
|
}
|
|
462
721
|
catch (error) {
|
|
463
|
-
const code =
|
|
722
|
+
const code = errorCode(error);
|
|
464
723
|
if (code !== 'ENOENT') {
|
|
465
724
|
this.logger.warn('Could not copy OpenCode authentication into isolated data home', {
|
|
466
|
-
error:
|
|
725
|
+
error: toErrorMessage(error),
|
|
467
726
|
});
|
|
468
727
|
}
|
|
469
728
|
}
|
|
@@ -483,189 +742,61 @@ export class AgentExecutor {
|
|
|
483
742
|
throw error;
|
|
484
743
|
}
|
|
485
744
|
}
|
|
486
|
-
|
|
487
|
-
if (typeof error !== 'object' || error === null || !('code' in error)) {
|
|
488
|
-
return undefined;
|
|
489
|
-
}
|
|
490
|
-
return typeof error.code === 'string' ? error.code : undefined;
|
|
491
|
-
}
|
|
492
|
-
async executeWithSpawn(params) {
|
|
745
|
+
async executeWithSpawn(params, signal) {
|
|
493
746
|
const { command, args, envOverrides } = this.buildCommandArgs(params);
|
|
494
747
|
const preparedEnvironment = await this.prepareSpawnEnvironment(envOverrides);
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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, {
|
|
498
755
|
cwd: params.cwd || process.cwd(),
|
|
756
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
757
|
+
shell: false,
|
|
758
|
+
env: preparedEnvironment.env,
|
|
499
759
|
});
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
stderr: error instanceof Error ? error.message : String(error),
|
|
514
|
-
exitCode: this.errorCode(error) === 'ENOENT' ? 127 : 1,
|
|
515
|
-
hasResult: false,
|
|
516
|
-
});
|
|
517
|
-
});
|
|
518
|
-
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
|
+
};
|
|
519
773
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
const stderrDecoder = new StringDecoder('utf8');
|
|
526
|
-
let stdoutTruncated = false;
|
|
527
|
-
let stderrTruncated = false;
|
|
528
|
-
let capturedBytes = 0;
|
|
529
|
-
let timedOut = false;
|
|
530
|
-
let outputExceeded = false;
|
|
531
|
-
let processError;
|
|
532
|
-
let settled = false;
|
|
533
|
-
let forceKillTimer;
|
|
534
|
-
const executionTimeout = setTimeout(() => {
|
|
535
|
-
timedOut = true;
|
|
536
|
-
this.logger.warn('Execution timeout reached', {
|
|
537
|
-
timeout: this.config.executionTimeout,
|
|
538
|
-
});
|
|
539
|
-
requestTermination();
|
|
540
|
-
}, this.config.executionTimeout);
|
|
541
|
-
const clearTimers = () => {
|
|
542
|
-
clearTimeout(executionTimeout);
|
|
543
|
-
if (forceKillTimer)
|
|
544
|
-
clearTimeout(forceKillTimer);
|
|
545
|
-
};
|
|
546
|
-
const finish = async (code, signal) => {
|
|
547
|
-
if (settled)
|
|
548
|
-
return;
|
|
549
|
-
settled = true;
|
|
550
|
-
clearTimers();
|
|
551
|
-
if (!stdoutTruncated) {
|
|
552
|
-
const tail = stdoutDecoder.end();
|
|
553
|
-
stdoutParts.push(tail);
|
|
554
|
-
stdoutLineParts.push(tail);
|
|
555
|
-
}
|
|
556
|
-
if (!stderrTruncated) {
|
|
557
|
-
stderrParts.push(stderrDecoder.end());
|
|
558
|
-
}
|
|
559
|
-
const trailingLine = stdoutLineParts.join('');
|
|
560
|
-
if (trailingLine.trim()) {
|
|
561
|
-
streamProcessor.processLine(trailingLine);
|
|
562
|
-
}
|
|
563
|
-
stdoutLineParts = [];
|
|
564
|
-
const stdout = stdoutParts.join('');
|
|
565
|
-
const stderr = stderrParts.join('');
|
|
566
|
-
let result = streamProcessor.getResult();
|
|
567
|
-
if (result === null) {
|
|
568
|
-
streamProcessor.processCompleteOutput(stdout);
|
|
569
|
-
result = streamProcessor.getResult();
|
|
570
|
-
}
|
|
571
|
-
let exitCode = code ?? (signal ? 128 + this.signalNumber(signal) : 1);
|
|
572
|
-
if (timedOut)
|
|
573
|
-
exitCode = 124;
|
|
574
|
-
if (outputExceeded || processError)
|
|
575
|
-
exitCode = this.errorCode(processError) === 'ENOENT' ? 127 : 1;
|
|
576
|
-
const errors = [];
|
|
577
|
-
if (stderr)
|
|
578
|
-
errors.push(stderr);
|
|
579
|
-
if (timedOut)
|
|
580
|
-
errors.push(`Execution timeout: ${this.config.executionTimeout}ms`);
|
|
581
|
-
if (outputExceeded) {
|
|
582
|
-
errors.push(`Sub-agent output exceeded ${this.config.maxOutputBytes} bytes`);
|
|
583
|
-
}
|
|
584
|
-
if (processError && !stderr)
|
|
585
|
-
errors.push(processError.message);
|
|
586
|
-
try {
|
|
587
|
-
await preparedEnvironment.cleanup();
|
|
588
|
-
}
|
|
589
|
-
catch (error) {
|
|
590
|
-
this.logger.warn('Failed to clean up per-run environment', {
|
|
591
|
-
error: error instanceof Error ? error.message : String(error),
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
resolve({
|
|
595
|
-
stdout: result ? JSON.stringify(result) : stdout,
|
|
596
|
-
stderr: errors.join('\n'),
|
|
597
|
-
exitCode,
|
|
598
|
-
hasResult: result !== null,
|
|
599
|
-
resultJson: result !== null ? result : undefined,
|
|
600
|
-
});
|
|
601
|
-
};
|
|
602
|
-
const requestTermination = () => {
|
|
603
|
-
childProcess.kill('SIGTERM');
|
|
604
|
-
if (forceKillTimer)
|
|
605
|
-
return;
|
|
606
|
-
forceKillTimer = setTimeout(() => {
|
|
607
|
-
childProcess.kill('SIGKILL');
|
|
608
|
-
}, TERMINATION_GRACE_MS);
|
|
609
|
-
};
|
|
610
|
-
const captureChunk = (data, decoder, markTruncated) => {
|
|
611
|
-
const remaining = this.config.maxOutputBytes - capturedBytes;
|
|
612
|
-
if (remaining <= 0) {
|
|
613
|
-
outputExceeded = true;
|
|
614
|
-
markTruncated();
|
|
615
|
-
requestTermination();
|
|
616
|
-
return '';
|
|
617
|
-
}
|
|
618
|
-
const captured = data.length <= remaining ? data : data.subarray(0, remaining);
|
|
619
|
-
capturedBytes += captured.length;
|
|
620
|
-
if (captured.length < data.length) {
|
|
621
|
-
outputExceeded = true;
|
|
622
|
-
markTruncated();
|
|
623
|
-
requestTermination();
|
|
624
|
-
}
|
|
625
|
-
return decoder.write(captured);
|
|
774
|
+
return {
|
|
775
|
+
stdout: '',
|
|
776
|
+
stderr: toErrorMessage(error),
|
|
777
|
+
exitCode: errorCode(error) === 'ENOENT' ? 127 : 1,
|
|
778
|
+
hasResult: false,
|
|
626
779
|
};
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
stdoutLineParts.push(chunk.slice(chunkOffset));
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
stdoutLineParts.push(chunk.slice(chunkOffset, newlineIndex));
|
|
640
|
-
const line = stdoutLineParts.join('');
|
|
641
|
-
stdoutLineParts = [];
|
|
642
|
-
chunkOffset = newlineIndex + 1;
|
|
643
|
-
if (streamProcessor.processLine(line)) {
|
|
644
|
-
requestTermination();
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
});
|
|
649
|
-
childProcess.stderr?.on('data', (data) => {
|
|
650
|
-
stderrParts.push(captureChunk(data, stderrDecoder, () => {
|
|
651
|
-
stderrTruncated = true;
|
|
652
|
-
}));
|
|
653
|
-
});
|
|
654
|
-
childProcess.on('close', (code, signal) => {
|
|
655
|
-
void finish(code, signal);
|
|
656
|
-
});
|
|
657
|
-
childProcess.on('error', (error) => {
|
|
658
|
-
processError = error;
|
|
659
|
-
void finish(null);
|
|
660
|
-
});
|
|
661
|
-
});
|
|
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
|
+
}
|
|
662
789
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
+
}
|
|
669
800
|
}
|
|
670
801
|
generateRequestId() {
|
|
671
802
|
return `req_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|