@rk0429/agentic-relay 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/relay.mjs +565 -264
- package/package.json +9 -2
package/README.md
CHANGED
|
@@ -156,6 +156,10 @@ Example configuration:
|
|
|
156
156
|
| `OPENAI_API_KEY` | Passed through to Codex CLI (optional with subscription) | -- |
|
|
157
157
|
| `GEMINI_API_KEY` | Passed through to Gemini CLI (optional with subscription) | -- |
|
|
158
158
|
|
|
159
|
+
### Security Considerations
|
|
160
|
+
|
|
161
|
+
- **Claude adapter permission bypass**: By default, the Claude adapter runs with `bypassPermissions` mode to enable non-interactive sub-agent execution. This means spawned Claude Code agents can execute tools without user confirmation. To change this behavior, set the `RELAY_CLAUDE_PERMISSION_MODE` environment variable to `default`.
|
|
162
|
+
|
|
159
163
|
## MCP Server Mode
|
|
160
164
|
|
|
161
165
|
agentic-relay can act as an MCP (Model Context Protocol) server, allowing any MCP-capable client to spawn sub-agents across all three backends. This is how nested sub-agent orchestration works -- a parent agent calls relay via MCP, which spawns a child agent on any backend, and that child can call relay again to spawn a grandchild.
|
|
@@ -317,7 +321,7 @@ src/
|
|
|
317
321
|
- **Process management**: execa (interactive modes, Gemini CLI)
|
|
318
322
|
- **Validation**: zod
|
|
319
323
|
- **Logging**: consola
|
|
320
|
-
- **Testing**: vitest (
|
|
324
|
+
- **Testing**: vitest (771 tests across 35 files)
|
|
321
325
|
- **Coverage**: @vitest/coverage-v8
|
|
322
326
|
|
|
323
327
|
## License
|
package/dist/relay.mjs
CHANGED
|
@@ -159,7 +159,7 @@ var init_recursion_guard = __esm({
|
|
|
159
159
|
import { z as z2 } from "zod";
|
|
160
160
|
import { nanoid as nanoid2 } from "nanoid";
|
|
161
161
|
import { existsSync, readFileSync } from "fs";
|
|
162
|
-
import { join as join6 } from "path";
|
|
162
|
+
import { join as join6, normalize, resolve, sep } from "path";
|
|
163
163
|
function buildContextInjection(metadata) {
|
|
164
164
|
const parts = [];
|
|
165
165
|
if (metadata.stateContent && typeof metadata.stateContent === "string") {
|
|
@@ -190,15 +190,28 @@ function readPreviousState(dailynoteDir) {
|
|
|
190
190
|
return null;
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
|
-
function
|
|
193
|
+
function validatePathWithinProject(filePath, projectRoot) {
|
|
194
|
+
if (filePath.trim().length === 0) {
|
|
195
|
+
throw new Error("Path is empty");
|
|
196
|
+
}
|
|
197
|
+
const normalizedProjectRoot = normalize(resolve(projectRoot));
|
|
198
|
+
const resolvedPath = normalize(resolve(normalizedProjectRoot, filePath));
|
|
199
|
+
const projectRootPrefix = normalizedProjectRoot.endsWith(sep) ? normalizedProjectRoot : `${normalizedProjectRoot}${sep}`;
|
|
200
|
+
if (resolvedPath !== normalizedProjectRoot && !resolvedPath.startsWith(projectRootPrefix)) {
|
|
201
|
+
throw new Error(`Path traversal detected: ${filePath}`);
|
|
202
|
+
}
|
|
203
|
+
return resolvedPath;
|
|
204
|
+
}
|
|
205
|
+
function readAgentDefinition(definitionPath, projectRoot = process.cwd()) {
|
|
194
206
|
try {
|
|
195
|
-
|
|
196
|
-
|
|
207
|
+
const safeDefinitionPath = validatePathWithinProject(definitionPath, projectRoot);
|
|
208
|
+
if (!existsSync(safeDefinitionPath)) {
|
|
209
|
+
logger.warn(`Agent definition file not found at ${safeDefinitionPath}`);
|
|
197
210
|
return null;
|
|
198
211
|
}
|
|
199
|
-
const content = readFileSync(
|
|
212
|
+
const content = readFileSync(safeDefinitionPath, "utf-8");
|
|
200
213
|
if (content.trim().length === 0) {
|
|
201
|
-
logger.warn(`Agent definition file is empty at ${
|
|
214
|
+
logger.warn(`Agent definition file is empty at ${safeDefinitionPath}`);
|
|
202
215
|
return null;
|
|
203
216
|
}
|
|
204
217
|
return content;
|
|
@@ -209,9 +222,10 @@ function readAgentDefinition(definitionPath) {
|
|
|
209
222
|
return null;
|
|
210
223
|
}
|
|
211
224
|
}
|
|
212
|
-
function readSkillContext(skillContext) {
|
|
225
|
+
function readSkillContext(skillContext, projectRoot = process.cwd()) {
|
|
213
226
|
try {
|
|
214
|
-
const
|
|
227
|
+
const safeSkillPath = validatePathWithinProject(skillContext.skillPath, projectRoot);
|
|
228
|
+
const skillMdPath = validatePathWithinProject(join6(safeSkillPath, "SKILL.md"), projectRoot);
|
|
215
229
|
if (!existsSync(skillMdPath)) {
|
|
216
230
|
logger.warn(
|
|
217
231
|
`SKILL.md not found at ${skillMdPath}`
|
|
@@ -222,12 +236,12 @@ function readSkillContext(skillContext) {
|
|
|
222
236
|
const skillContent = readFileSync(skillMdPath, "utf-8");
|
|
223
237
|
parts.push(skillContent);
|
|
224
238
|
if (skillContext.subskill) {
|
|
225
|
-
const subskillPath = join6(
|
|
226
|
-
|
|
239
|
+
const subskillPath = validatePathWithinProject(join6(
|
|
240
|
+
safeSkillPath,
|
|
227
241
|
"subskills",
|
|
228
242
|
skillContext.subskill,
|
|
229
243
|
"SUBSKILL.md"
|
|
230
|
-
);
|
|
244
|
+
), projectRoot);
|
|
231
245
|
if (existsSync(subskillPath)) {
|
|
232
246
|
const subskillContent = readFileSync(subskillPath, "utf-8");
|
|
233
247
|
parts.push(subskillContent);
|
|
@@ -293,14 +307,22 @@ function buildChildMcpServers(parentMcpServers, childHttpUrl) {
|
|
|
293
307
|
}
|
|
294
308
|
return result;
|
|
295
309
|
}
|
|
310
|
+
function inferFailureReason(stderr, stdout) {
|
|
311
|
+
const combined = `${stderr} ${stdout}`.toLowerCase();
|
|
312
|
+
if (combined.includes("timed out") || combined.includes("timeout")) return "timeout";
|
|
313
|
+
if (combined.includes("max turns") || combined.includes("max_turns") || combined.includes("turn limit")) return "max_turns_exhausted";
|
|
314
|
+
return "adapter_error";
|
|
315
|
+
}
|
|
296
316
|
function buildContextFromEnv() {
|
|
297
317
|
const traceId = process.env["RELAY_TRACE_ID"] ?? `trace-${nanoid2()}`;
|
|
298
318
|
const parentSessionId = process.env["RELAY_PARENT_SESSION_ID"] ?? null;
|
|
299
319
|
const depth = Number(process.env["RELAY_DEPTH"] ?? "0");
|
|
300
320
|
return { traceId, parentSessionId, depth };
|
|
301
321
|
}
|
|
302
|
-
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl) {
|
|
322
|
+
async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress) {
|
|
323
|
+
onProgress?.({ stage: "initializing", percent: 0 });
|
|
303
324
|
let effectiveBackend = input.backend;
|
|
325
|
+
let selectionReason = "direct";
|
|
304
326
|
if (backendSelector) {
|
|
305
327
|
const availableBackends = registry2.listIds();
|
|
306
328
|
const selectionContext = {
|
|
@@ -309,7 +331,9 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
309
331
|
agentType: input.agent,
|
|
310
332
|
taskType: input.taskType
|
|
311
333
|
};
|
|
312
|
-
|
|
334
|
+
const selectionResult = backendSelector.selectBackendWithReason(selectionContext);
|
|
335
|
+
effectiveBackend = selectionResult.backend;
|
|
336
|
+
selectionReason = selectionResult.reason;
|
|
313
337
|
}
|
|
314
338
|
const envContext = buildContextFromEnv();
|
|
315
339
|
const promptHash = RecursionGuard.hashPrompt(input.prompt);
|
|
@@ -326,7 +350,8 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
326
350
|
sessionId: "",
|
|
327
351
|
exitCode: 1,
|
|
328
352
|
stdout: "",
|
|
329
|
-
stderr: `Spawn blocked: ${guardResult.reason}
|
|
353
|
+
stderr: `Spawn blocked: ${guardResult.reason}`,
|
|
354
|
+
failureReason: "recursion_blocked"
|
|
330
355
|
};
|
|
331
356
|
}
|
|
332
357
|
const adapter = registry2.get(effectiveBackend);
|
|
@@ -336,7 +361,8 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
336
361
|
sessionId: "",
|
|
337
362
|
exitCode: 1,
|
|
338
363
|
stdout: "",
|
|
339
|
-
stderr: `Backend "${effectiveBackend}" is not available. Use list_available_backends to see available options
|
|
364
|
+
stderr: `Backend "${effectiveBackend}" is not available. Use list_available_backends to see available options.`,
|
|
365
|
+
failureReason: "backend_unavailable"
|
|
340
366
|
};
|
|
341
367
|
}
|
|
342
368
|
const spawnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -418,41 +444,95 @@ ${defText}
|
|
|
418
444
|
${wrapped}` : wrapped;
|
|
419
445
|
}
|
|
420
446
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
447
|
+
let effectivePrompt = input.prompt;
|
|
448
|
+
if (input.taskInstructionPath) {
|
|
449
|
+
try {
|
|
450
|
+
const projectRoot = process.cwd();
|
|
451
|
+
const safePath = validatePathWithinProject(input.taskInstructionPath, projectRoot);
|
|
452
|
+
if (!existsSync(safePath)) {
|
|
425
453
|
return {
|
|
426
|
-
sessionId:
|
|
454
|
+
sessionId: "",
|
|
427
455
|
exitCode: 1,
|
|
428
456
|
stdout: "",
|
|
429
|
-
stderr: `
|
|
457
|
+
stderr: `Task instruction file not found: ${input.taskInstructionPath}`,
|
|
458
|
+
failureReason: "instruction_file_error"
|
|
430
459
|
};
|
|
431
460
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
461
|
+
const instructionContent = readFileSync(safePath, "utf-8");
|
|
462
|
+
effectivePrompt = `${instructionContent}
|
|
463
|
+
|
|
464
|
+
${input.prompt}`;
|
|
465
|
+
} catch (error) {
|
|
466
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
467
|
+
return {
|
|
468
|
+
sessionId: "",
|
|
469
|
+
exitCode: 1,
|
|
470
|
+
stdout: "",
|
|
471
|
+
stderr: `Failed to read task instruction file: ${message}`,
|
|
472
|
+
failureReason: "instruction_file_error"
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
onProgress?.({ stage: "spawning", percent: 10 });
|
|
477
|
+
try {
|
|
478
|
+
let result;
|
|
479
|
+
const executePromise = (async () => {
|
|
480
|
+
if (input.resumeSessionId) {
|
|
481
|
+
if (!adapter.continueSession) {
|
|
482
|
+
return {
|
|
483
|
+
exitCode: 1,
|
|
484
|
+
stdout: "",
|
|
485
|
+
stderr: `Backend "${effectiveBackend}" does not support session continuation (continueSession).`,
|
|
486
|
+
_noSession: true,
|
|
487
|
+
_failureReason: "session_continuation_unsupported"
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
return adapter.continueSession(input.resumeSessionId, effectivePrompt);
|
|
491
|
+
} else {
|
|
492
|
+
let mcpServers;
|
|
493
|
+
if (childHttpUrl) {
|
|
494
|
+
const parentMcpServers = readProjectMcpJson();
|
|
495
|
+
if (Object.keys(parentMcpServers).length > 0) {
|
|
496
|
+
mcpServers = buildChildMcpServers(parentMcpServers, childHttpUrl);
|
|
497
|
+
}
|
|
439
498
|
}
|
|
499
|
+
return adapter.execute({
|
|
500
|
+
prompt: effectivePrompt,
|
|
501
|
+
agent: input.agent,
|
|
502
|
+
systemPrompt: enhancedSystemPrompt,
|
|
503
|
+
model: input.model,
|
|
504
|
+
maxTurns: input.maxTurns,
|
|
505
|
+
mcpContext: {
|
|
506
|
+
parentSessionId: session.relaySessionId,
|
|
507
|
+
depth: envContext.depth + 1,
|
|
508
|
+
maxDepth: guard.getConfig().maxDepth,
|
|
509
|
+
traceId: envContext.traceId
|
|
510
|
+
},
|
|
511
|
+
...mcpServers ? { mcpServers } : {}
|
|
512
|
+
});
|
|
440
513
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
514
|
+
})();
|
|
515
|
+
if (input.timeoutMs) {
|
|
516
|
+
const timeoutPromise = new Promise(
|
|
517
|
+
(_, reject) => setTimeout(
|
|
518
|
+
() => reject(new Error(`Agent execution timed out after ${input.timeoutMs}ms`)),
|
|
519
|
+
input.timeoutMs
|
|
520
|
+
)
|
|
521
|
+
);
|
|
522
|
+
result = await Promise.race([executePromise, timeoutPromise]);
|
|
523
|
+
} else {
|
|
524
|
+
result = await executePromise;
|
|
525
|
+
}
|
|
526
|
+
if (result && "_noSession" in result) {
|
|
527
|
+
return {
|
|
528
|
+
sessionId: session.relaySessionId,
|
|
529
|
+
exitCode: result.exitCode,
|
|
530
|
+
stdout: result.stdout,
|
|
531
|
+
stderr: result.stderr,
|
|
532
|
+
..."_failureReason" in result ? { failureReason: result._failureReason } : {}
|
|
533
|
+
};
|
|
455
534
|
}
|
|
535
|
+
onProgress?.({ stage: "executing", percent: 50 });
|
|
456
536
|
if (contextMonitor2) {
|
|
457
537
|
const estimatedTokens = Math.ceil(
|
|
458
538
|
(result.stdout.length + result.stderr.length) / 4
|
|
@@ -465,7 +545,19 @@ ${wrapped}` : wrapped;
|
|
|
465
545
|
}
|
|
466
546
|
guard.recordSpawn(context);
|
|
467
547
|
const status = result.exitCode === 0 ? "completed" : "error";
|
|
548
|
+
const failureReason = result.exitCode !== 0 ? inferFailureReason(result.stderr, result.stdout) : void 0;
|
|
468
549
|
await sessionManager2.update(session.relaySessionId, { status });
|
|
550
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
551
|
+
const metadata = {
|
|
552
|
+
durationMs: new Date(completedAt).getTime() - new Date(spawnStartedAt).getTime(),
|
|
553
|
+
selectedBackend: effectiveBackend,
|
|
554
|
+
...input.preferredBackend ? { requestedBackend: input.preferredBackend } : {},
|
|
555
|
+
selectionReason,
|
|
556
|
+
startedAt: spawnStartedAt,
|
|
557
|
+
completedAt,
|
|
558
|
+
...result.tokenUsage ? { tokenUsage: result.tokenUsage } : {}
|
|
559
|
+
};
|
|
560
|
+
onProgress?.({ stage: "completed", percent: 100 });
|
|
469
561
|
if (hooksEngine2) {
|
|
470
562
|
try {
|
|
471
563
|
const postSpawnData = {
|
|
@@ -502,16 +594,20 @@ ${wrapped}` : wrapped;
|
|
|
502
594
|
exitCode: result.exitCode,
|
|
503
595
|
stdout: result.stdout,
|
|
504
596
|
stderr: result.stderr,
|
|
505
|
-
nativeSessionId: result.nativeSessionId
|
|
597
|
+
nativeSessionId: result.nativeSessionId,
|
|
598
|
+
metadata,
|
|
599
|
+
...failureReason ? { failureReason } : {}
|
|
506
600
|
};
|
|
507
601
|
} catch (error) {
|
|
508
602
|
await sessionManager2.update(session.relaySessionId, { status: "error" });
|
|
509
603
|
const message = error instanceof Error ? error.message : String(error);
|
|
604
|
+
const catchFailureReason = message.toLowerCase().includes("timed out") || message.toLowerCase().includes("timeout") ? "timeout" : "unknown";
|
|
510
605
|
return {
|
|
511
606
|
sessionId: session.relaySessionId,
|
|
512
607
|
exitCode: 1,
|
|
513
608
|
stdout: "",
|
|
514
|
-
stderr: message
|
|
609
|
+
stderr: message,
|
|
610
|
+
failureReason: catchFailureReason
|
|
515
611
|
};
|
|
516
612
|
}
|
|
517
613
|
}
|
|
@@ -537,25 +633,123 @@ var init_spawn_agent = __esm({
|
|
|
537
633
|
definitionPath: z2.string().describe("Path to the agent definition file (e.g., '.claude/agents/software-engineer.md')")
|
|
538
634
|
}).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
|
|
539
635
|
preferredBackend: z2.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override. Takes priority over automatic selection based on agent/task type."),
|
|
540
|
-
taskType: z2.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified.")
|
|
636
|
+
taskType: z2.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified."),
|
|
637
|
+
timeoutMs: z2.number().optional().describe("Timeout in milliseconds for agent execution. Default: no timeout."),
|
|
638
|
+
taskInstructionPath: z2.string().optional().describe(
|
|
639
|
+
"Path to a file containing task instructions. Content is prepended to the prompt. Path is resolved relative to the project root and validated against path traversal."
|
|
640
|
+
),
|
|
641
|
+
label: z2.string().optional().describe("Human-readable label for identifying this agent in parallel results")
|
|
541
642
|
});
|
|
542
643
|
}
|
|
543
644
|
});
|
|
544
645
|
|
|
646
|
+
// src/mcp-server/tools/conflict-detector.ts
|
|
647
|
+
import { execFile } from "child_process";
|
|
648
|
+
import { promisify } from "util";
|
|
649
|
+
async function takeSnapshot(cwd) {
|
|
650
|
+
try {
|
|
651
|
+
const [diffResult, lsResult] = await Promise.all([
|
|
652
|
+
execFileAsync("git", ["diff", "--name-only", "HEAD"], { cwd }).catch(() => ({ stdout: "" })),
|
|
653
|
+
execFileAsync("git", ["ls-files", "-m"], { cwd }).catch(() => ({ stdout: "" }))
|
|
654
|
+
]);
|
|
655
|
+
const files = /* @__PURE__ */ new Set();
|
|
656
|
+
for (const line of diffResult.stdout.split("\n")) {
|
|
657
|
+
const trimmed = line.trim();
|
|
658
|
+
if (trimmed) files.add(trimmed);
|
|
659
|
+
}
|
|
660
|
+
for (const line of lsResult.stdout.split("\n")) {
|
|
661
|
+
const trimmed = line.trim();
|
|
662
|
+
if (trimmed) files.add(trimmed);
|
|
663
|
+
}
|
|
664
|
+
return files;
|
|
665
|
+
} catch (error) {
|
|
666
|
+
logger.warn(
|
|
667
|
+
`Failed to take git snapshot: ${error instanceof Error ? error.message : String(error)}`
|
|
668
|
+
);
|
|
669
|
+
return /* @__PURE__ */ new Set();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function extractMentionedPaths(stdout) {
|
|
673
|
+
const paths = /* @__PURE__ */ new Set();
|
|
674
|
+
const patterns = [
|
|
675
|
+
/(?:^|\s|["'`])(\.\/.+?\.[a-zA-Z]{1,5})(?:\s|["'`]|$)/gm,
|
|
676
|
+
/(?:^|\s|["'`])(src\/.+?\.[a-zA-Z]{1,5})(?:\s|["'`]|$)/gm,
|
|
677
|
+
/(?:^|\s|["'`])([a-zA-Z][a-zA-Z0-9_-]*(?:\/[a-zA-Z0-9_.-]+){1,10}\.[a-zA-Z]{1,5})(?:\s|["'`]|$)/gm
|
|
678
|
+
];
|
|
679
|
+
for (const pattern of patterns) {
|
|
680
|
+
let match;
|
|
681
|
+
while ((match = pattern.exec(stdout)) !== null) {
|
|
682
|
+
let path = match[1];
|
|
683
|
+
if (path.startsWith("./")) {
|
|
684
|
+
path = path.slice(2);
|
|
685
|
+
}
|
|
686
|
+
paths.add(path);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return paths;
|
|
690
|
+
}
|
|
691
|
+
async function detectConflicts(before, after, agentResults) {
|
|
692
|
+
const newlyModified = /* @__PURE__ */ new Set();
|
|
693
|
+
for (const file of after) {
|
|
694
|
+
if (!before.has(file)) {
|
|
695
|
+
newlyModified.add(file);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (newlyModified.size === 0) {
|
|
699
|
+
return { conflicts: [], hasConflicts: false };
|
|
700
|
+
}
|
|
701
|
+
const agentPaths = /* @__PURE__ */ new Map();
|
|
702
|
+
for (const agent of agentResults) {
|
|
703
|
+
agentPaths.set(agent.index, extractMentionedPaths(agent.stdout));
|
|
704
|
+
}
|
|
705
|
+
const fileToAgents = /* @__PURE__ */ new Map();
|
|
706
|
+
for (const file of newlyModified) {
|
|
707
|
+
const matchingAgents = [];
|
|
708
|
+
for (const [index, paths] of agentPaths) {
|
|
709
|
+
for (const mentionedPath of paths) {
|
|
710
|
+
if (file === mentionedPath || file.endsWith(mentionedPath) || mentionedPath.endsWith(file)) {
|
|
711
|
+
matchingAgents.push(index);
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (matchingAgents.length > 1) {
|
|
717
|
+
fileToAgents.set(file, matchingAgents);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
const conflicts = Array.from(fileToAgents.entries()).map(
|
|
721
|
+
([path, agents]) => ({ path, agents })
|
|
722
|
+
);
|
|
723
|
+
return {
|
|
724
|
+
conflicts,
|
|
725
|
+
hasConflicts: conflicts.length > 0
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
var execFileAsync;
|
|
729
|
+
var init_conflict_detector = __esm({
|
|
730
|
+
"src/mcp-server/tools/conflict-detector.ts"() {
|
|
731
|
+
"use strict";
|
|
732
|
+
init_logger();
|
|
733
|
+
execFileAsync = promisify(execFile);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
545
737
|
// src/mcp-server/tools/spawn-agents-parallel.ts
|
|
546
|
-
async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl) {
|
|
738
|
+
async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, guard, hooksEngine2, contextMonitor2, backendSelector, childHttpUrl, onProgress) {
|
|
547
739
|
const envContext = buildContextFromEnv();
|
|
548
740
|
if (envContext.depth >= guard.getConfig().maxDepth) {
|
|
549
741
|
const reason = `Max depth exceeded: ${envContext.depth} >= ${guard.getConfig().maxDepth}`;
|
|
550
742
|
logger.warn(`Batch spawn blocked by RecursionGuard: ${reason}`);
|
|
551
743
|
return {
|
|
552
|
-
results: agents.map((
|
|
744
|
+
results: agents.map((agent, index) => ({
|
|
553
745
|
index,
|
|
554
746
|
sessionId: "",
|
|
555
747
|
exitCode: 1,
|
|
556
748
|
stdout: "",
|
|
557
749
|
stderr: `Batch spawn blocked: ${reason}`,
|
|
558
|
-
error: reason
|
|
750
|
+
error: reason,
|
|
751
|
+
failureReason: "recursion_blocked",
|
|
752
|
+
...agent.label ? { label: agent.label } : {}
|
|
559
753
|
})),
|
|
560
754
|
totalCount: agents.length,
|
|
561
755
|
successCount: 0,
|
|
@@ -568,19 +762,25 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
|
|
|
568
762
|
const reason = `Batch would exceed max calls per session: ${currentCount} + ${agents.length} > ${maxCalls}`;
|
|
569
763
|
logger.warn(`Batch spawn blocked by RecursionGuard: ${reason}`);
|
|
570
764
|
return {
|
|
571
|
-
results: agents.map((
|
|
765
|
+
results: agents.map((agent, index) => ({
|
|
572
766
|
index,
|
|
573
767
|
sessionId: "",
|
|
574
768
|
exitCode: 1,
|
|
575
769
|
stdout: "",
|
|
576
770
|
stderr: `Batch spawn blocked: ${reason}`,
|
|
577
|
-
error: reason
|
|
771
|
+
error: reason,
|
|
772
|
+
failureReason: "recursion_blocked",
|
|
773
|
+
...agent.label ? { label: agent.label } : {}
|
|
578
774
|
})),
|
|
579
775
|
totalCount: agents.length,
|
|
580
776
|
successCount: 0,
|
|
581
777
|
failureCount: agents.length
|
|
582
778
|
};
|
|
583
779
|
}
|
|
780
|
+
const cwd = process.cwd();
|
|
781
|
+
const beforeSnapshot = await takeSnapshot(cwd);
|
|
782
|
+
onProgress?.({ stage: "spawning", percent: 5 });
|
|
783
|
+
let completedCount = 0;
|
|
584
784
|
const settled = await Promise.allSettled(
|
|
585
785
|
agents.map(
|
|
586
786
|
(agent) => executeSpawnAgent(
|
|
@@ -592,20 +792,34 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
|
|
|
592
792
|
contextMonitor2,
|
|
593
793
|
backendSelector,
|
|
594
794
|
childHttpUrl
|
|
595
|
-
)
|
|
795
|
+
).then((result) => {
|
|
796
|
+
completedCount++;
|
|
797
|
+
onProgress?.({
|
|
798
|
+
stage: `completed ${completedCount}/${agents.length}`,
|
|
799
|
+
percent: Math.round(completedCount / agents.length * 90) + 5
|
|
800
|
+
});
|
|
801
|
+
return result;
|
|
802
|
+
})
|
|
596
803
|
)
|
|
597
804
|
);
|
|
598
805
|
const results = settled.map((outcome, index) => {
|
|
599
806
|
if (outcome.status === "fulfilled") {
|
|
600
807
|
const r = outcome.value;
|
|
601
|
-
|
|
808
|
+
const base = {
|
|
602
809
|
index,
|
|
603
810
|
sessionId: r.sessionId,
|
|
604
811
|
exitCode: r.exitCode,
|
|
605
812
|
stdout: r.stdout,
|
|
606
813
|
stderr: r.stderr,
|
|
607
|
-
...r.nativeSessionId ? { nativeSessionId: r.nativeSessionId } : {}
|
|
814
|
+
...r.nativeSessionId ? { nativeSessionId: r.nativeSessionId } : {},
|
|
815
|
+
...r.metadata ? { metadata: r.metadata } : {},
|
|
816
|
+
...r.failureReason ? { failureReason: r.failureReason } : {},
|
|
817
|
+
...agents[index]?.label ? { label: agents[index].label } : {}
|
|
608
818
|
};
|
|
819
|
+
if (r.exitCode !== 0) {
|
|
820
|
+
base.originalInput = agents[index];
|
|
821
|
+
}
|
|
822
|
+
return base;
|
|
609
823
|
}
|
|
610
824
|
const errorMessage = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
611
825
|
return {
|
|
@@ -614,21 +828,33 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
|
|
|
614
828
|
exitCode: 1,
|
|
615
829
|
stdout: "",
|
|
616
830
|
stderr: errorMessage,
|
|
617
|
-
error: errorMessage
|
|
831
|
+
error: errorMessage,
|
|
832
|
+
originalInput: agents[index],
|
|
833
|
+
failureReason: "unknown",
|
|
834
|
+
...agents[index]?.label ? { label: agents[index].label } : {}
|
|
618
835
|
};
|
|
619
836
|
});
|
|
620
837
|
const successCount = results.filter((r) => r.exitCode === 0).length;
|
|
838
|
+
const afterSnapshot = await takeSnapshot(cwd);
|
|
839
|
+
const agentResultsForConflict = results.map((r) => ({
|
|
840
|
+
index: r.index,
|
|
841
|
+
stdout: r.stdout
|
|
842
|
+
}));
|
|
843
|
+
const conflictResult = await detectConflicts(beforeSnapshot, afterSnapshot, agentResultsForConflict);
|
|
844
|
+
onProgress?.({ stage: "completed", percent: 100 });
|
|
621
845
|
return {
|
|
622
846
|
results,
|
|
623
847
|
totalCount: agents.length,
|
|
624
848
|
successCount,
|
|
625
|
-
failureCount: agents.length - successCount
|
|
849
|
+
failureCount: agents.length - successCount,
|
|
850
|
+
...conflictResult.hasConflicts ? { conflicts: conflictResult.conflicts, hasConflicts: true } : {}
|
|
626
851
|
};
|
|
627
852
|
}
|
|
628
853
|
var init_spawn_agents_parallel = __esm({
|
|
629
854
|
"src/mcp-server/tools/spawn-agents-parallel.ts"() {
|
|
630
855
|
"use strict";
|
|
631
856
|
init_spawn_agent();
|
|
857
|
+
init_conflict_detector();
|
|
632
858
|
init_logger();
|
|
633
859
|
}
|
|
634
860
|
});
|
|
@@ -745,37 +971,104 @@ var init_backend_selector = __esm({
|
|
|
745
971
|
this.agentToBackendMap = config?.agentToBackendMap ?? DEFAULT_AGENT_TO_BACKEND_MAP;
|
|
746
972
|
}
|
|
747
973
|
selectBackend(context) {
|
|
974
|
+
return this.selectBackendWithReason(context).backend;
|
|
975
|
+
}
|
|
976
|
+
selectBackendWithReason(context) {
|
|
748
977
|
const { availableBackends, preferredBackend, agentType, taskType } = context;
|
|
749
978
|
if (availableBackends.length === 0) {
|
|
750
979
|
throw new Error("No backends available");
|
|
751
980
|
}
|
|
752
981
|
if (preferredBackend && availableBackends.includes(preferredBackend)) {
|
|
753
|
-
return preferredBackend;
|
|
982
|
+
return { backend: preferredBackend, reason: "preferredBackend" };
|
|
754
983
|
}
|
|
755
984
|
if (agentType) {
|
|
756
985
|
const mapped = this.agentToBackendMap[agentType];
|
|
757
986
|
if (mapped && availableBackends.includes(mapped)) {
|
|
758
|
-
return mapped;
|
|
987
|
+
return { backend: mapped, reason: `agentType:${agentType}\u2192${mapped}` };
|
|
759
988
|
}
|
|
760
989
|
if (!mapped && availableBackends.includes("claude")) {
|
|
761
|
-
return "claude";
|
|
990
|
+
return { backend: "claude", reason: `agentType:${agentType}\u2192claude(unmapped)` };
|
|
762
991
|
}
|
|
763
992
|
}
|
|
764
993
|
if (taskType) {
|
|
765
994
|
const mapped = TASK_TYPE_TO_BACKEND_MAP[taskType];
|
|
766
995
|
if (mapped && availableBackends.includes(mapped)) {
|
|
767
|
-
return mapped;
|
|
996
|
+
return { backend: mapped, reason: `taskType:${taskType}\u2192${mapped}` };
|
|
768
997
|
}
|
|
769
998
|
}
|
|
770
999
|
if (availableBackends.includes(this.defaultBackend)) {
|
|
771
|
-
return this.defaultBackend;
|
|
1000
|
+
return { backend: this.defaultBackend, reason: "default" };
|
|
772
1001
|
}
|
|
773
|
-
return availableBackends[0];
|
|
1002
|
+
return { backend: availableBackends[0], reason: "fallback" };
|
|
774
1003
|
}
|
|
775
1004
|
};
|
|
776
1005
|
}
|
|
777
1006
|
});
|
|
778
1007
|
|
|
1008
|
+
// src/mcp-server/response-formatter.ts
|
|
1009
|
+
function formatSpawnAgentResponse(result) {
|
|
1010
|
+
const isError = result.exitCode !== 0;
|
|
1011
|
+
let text;
|
|
1012
|
+
if (isError) {
|
|
1013
|
+
const reasonPart = result.failureReason ? `, reason: ${result.failureReason}` : "";
|
|
1014
|
+
text = `FAILED (exit ${result.exitCode}${reasonPart})`;
|
|
1015
|
+
if (result.stderr) {
|
|
1016
|
+
text += `
|
|
1017
|
+
${result.stderr}`;
|
|
1018
|
+
}
|
|
1019
|
+
} else {
|
|
1020
|
+
text = `Session: ${result.sessionId}
|
|
1021
|
+
|
|
1022
|
+
${result.stdout}`;
|
|
1023
|
+
}
|
|
1024
|
+
if (result.metadata) {
|
|
1025
|
+
text += `
|
|
1026
|
+
|
|
1027
|
+
<metadata>
|
|
1028
|
+
${JSON.stringify(result.metadata, null, 2)}
|
|
1029
|
+
</metadata>`;
|
|
1030
|
+
}
|
|
1031
|
+
return { text, isError };
|
|
1032
|
+
}
|
|
1033
|
+
function formatParallelResponse(result) {
|
|
1034
|
+
const isError = result.failureCount === result.totalCount;
|
|
1035
|
+
const parts = [];
|
|
1036
|
+
if (result.hasConflicts && result.conflicts) {
|
|
1037
|
+
parts.push(`\u26A0 FILE CONFLICTS DETECTED: Multiple agents modified the same files.`);
|
|
1038
|
+
parts.push(`Conflicting files: ${result.conflicts.map((c) => c.path).join(", ")}
|
|
1039
|
+
`);
|
|
1040
|
+
}
|
|
1041
|
+
const durations = result.results.map((r) => r.metadata?.durationMs).filter((d) => d !== void 0);
|
|
1042
|
+
const avgDuration = durations.length > 0 ? (durations.reduce((a, b) => a + b, 0) / durations.length / 1e3).toFixed(1) : "?";
|
|
1043
|
+
parts.push(
|
|
1044
|
+
`${result.totalCount} agents: ${result.successCount} succeeded, ${result.failureCount} failed (avg ${avgDuration}s)
|
|
1045
|
+
`
|
|
1046
|
+
);
|
|
1047
|
+
for (const r of result.results) {
|
|
1048
|
+
const labelPart = r.label ? ` [${r.label}]` : "";
|
|
1049
|
+
const backend = r.metadata?.selectedBackend ?? "?";
|
|
1050
|
+
const duration = r.metadata?.durationMs !== void 0 ? `${(r.metadata.durationMs / 1e3).toFixed(1)}s` : "?s";
|
|
1051
|
+
if (r.exitCode === 0) {
|
|
1052
|
+
parts.push(`--- Agent ${r.index}${labelPart} (${backend}, ${duration}) SUCCESS ---`);
|
|
1053
|
+
parts.push(r.stdout || "(no output)");
|
|
1054
|
+
} else {
|
|
1055
|
+
const reasonPart = r.failureReason ? `, reason: ${r.failureReason}` : "";
|
|
1056
|
+
parts.push(`--- Agent ${r.index}${labelPart} FAILED (${backend}, ${duration}${reasonPart}) ---`);
|
|
1057
|
+
parts.push(r.stderr || r.error || "(no output)");
|
|
1058
|
+
}
|
|
1059
|
+
parts.push("");
|
|
1060
|
+
}
|
|
1061
|
+
parts.push(`<metadata>
|
|
1062
|
+
${JSON.stringify(result, null, 2)}
|
|
1063
|
+
</metadata>`);
|
|
1064
|
+
return { text: parts.join("\n"), isError };
|
|
1065
|
+
}
|
|
1066
|
+
var init_response_formatter = __esm({
|
|
1067
|
+
"src/mcp-server/response-formatter.ts"() {
|
|
1068
|
+
"use strict";
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
|
|
779
1072
|
// src/mcp-server/server.ts
|
|
780
1073
|
var server_exports = {};
|
|
781
1074
|
__export(server_exports, {
|
|
@@ -787,7 +1080,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
787
1080
|
import { createServer } from "http";
|
|
788
1081
|
import { randomUUID } from "crypto";
|
|
789
1082
|
import { z as z5 } from "zod";
|
|
790
|
-
var RelayMCPServer;
|
|
1083
|
+
var spawnAgentsParallelInputShape, MAX_CHILD_HTTP_SESSIONS, RelayMCPServer;
|
|
791
1084
|
var init_server = __esm({
|
|
792
1085
|
"src/mcp-server/server.ts"() {
|
|
793
1086
|
"use strict";
|
|
@@ -799,6 +1092,13 @@ var init_server = __esm({
|
|
|
799
1092
|
init_list_available_backends();
|
|
800
1093
|
init_backend_selector();
|
|
801
1094
|
init_logger();
|
|
1095
|
+
init_response_formatter();
|
|
1096
|
+
spawnAgentsParallelInputShape = {
|
|
1097
|
+
agents: z5.array(spawnAgentInputSchema).min(1).max(10).describe(
|
|
1098
|
+
"Array of agent configurations to execute in parallel (1-10 agents)"
|
|
1099
|
+
)
|
|
1100
|
+
};
|
|
1101
|
+
MAX_CHILD_HTTP_SESSIONS = 100;
|
|
802
1102
|
RelayMCPServer = class {
|
|
803
1103
|
constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2) {
|
|
804
1104
|
this.registry = registry2;
|
|
@@ -809,7 +1109,7 @@ var init_server = __esm({
|
|
|
809
1109
|
this.backendSelector = new BackendSelector();
|
|
810
1110
|
this.server = new McpServer({
|
|
811
1111
|
name: "agentic-relay",
|
|
812
|
-
version: "0.
|
|
1112
|
+
version: "0.9.0"
|
|
813
1113
|
});
|
|
814
1114
|
this.registerTools(this.server);
|
|
815
1115
|
}
|
|
@@ -830,28 +1130,23 @@ var init_server = __esm({
|
|
|
830
1130
|
server.tool(
|
|
831
1131
|
"spawn_agent",
|
|
832
1132
|
"Spawn a sub-agent on the specified backend CLI (Claude Code, Codex CLI, or Gemini CLI). The agent executes the given prompt in non-interactive mode and returns the result. Use 'agent' for named agent configurations (Claude only), 'systemPrompt' for custom role instructions (all backends), 'skillContext' to inject a skill definition (SKILL.md/SUBSKILL.md), 'agentDefinition' to inject an agent definition file into the sub-agent's system prompt, 'preferredBackend' to override automatic backend selection, or 'taskType' to hint at the task nature for backend selection.",
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
prompt: z5.string(),
|
|
836
|
-
agent: z5.string().optional().describe("Named agent configuration (Claude only)"),
|
|
837
|
-
systemPrompt: z5.string().optional().describe(
|
|
838
|
-
"System prompt / role instructions for the sub-agent (all backends)"
|
|
839
|
-
),
|
|
840
|
-
resumeSessionId: z5.string().optional(),
|
|
841
|
-
model: z5.string().optional(),
|
|
842
|
-
maxTurns: z5.number().optional(),
|
|
843
|
-
skillContext: z5.object({
|
|
844
|
-
skillPath: z5.string().describe("Path to the skill directory (e.g., '.agents/skills/software-engineer/')"),
|
|
845
|
-
subskill: z5.string().optional().describe("Specific subskill to activate")
|
|
846
|
-
}).optional().describe("Skill context to inject into the sub-agent's system prompt"),
|
|
847
|
-
agentDefinition: z5.object({
|
|
848
|
-
definitionPath: z5.string().describe("Path to the agent definition file (e.g., '.claude/agents/software-engineer.md')")
|
|
849
|
-
}).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
|
|
850
|
-
preferredBackend: z5.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override. Takes priority over automatic selection based on agent/task type."),
|
|
851
|
-
taskType: z5.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified.")
|
|
852
|
-
},
|
|
853
|
-
async (params) => {
|
|
1133
|
+
spawnAgentInputSchema.shape,
|
|
1134
|
+
async (params, extra) => {
|
|
854
1135
|
try {
|
|
1136
|
+
const onProgress = (progress) => {
|
|
1137
|
+
const progressToken = params._meta ? params._meta.progressToken : void 0;
|
|
1138
|
+
if (progressToken !== void 0) {
|
|
1139
|
+
void extra.sendNotification({
|
|
1140
|
+
method: "notifications/progress",
|
|
1141
|
+
params: {
|
|
1142
|
+
progressToken,
|
|
1143
|
+
progress: progress.percent ?? 0,
|
|
1144
|
+
total: 100,
|
|
1145
|
+
message: progress.stage
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
};
|
|
855
1150
|
const result = await executeSpawnAgent(
|
|
856
1151
|
params,
|
|
857
1152
|
this.registry,
|
|
@@ -860,12 +1155,10 @@ var init_server = __esm({
|
|
|
860
1155
|
this.hooksEngine,
|
|
861
1156
|
this.contextMonitor,
|
|
862
1157
|
this.backendSelector,
|
|
863
|
-
this._childHttpUrl
|
|
1158
|
+
this._childHttpUrl,
|
|
1159
|
+
onProgress
|
|
864
1160
|
);
|
|
865
|
-
const isError = result
|
|
866
|
-
const text = isError ? `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` : `Session: ${result.sessionId}
|
|
867
|
-
|
|
868
|
-
${result.stdout}`;
|
|
1161
|
+
const { text, isError } = formatSpawnAgentResponse(result);
|
|
869
1162
|
return {
|
|
870
1163
|
content: [{ type: "text", text }],
|
|
871
1164
|
isError
|
|
@@ -881,31 +1174,76 @@ ${result.stdout}`;
|
|
|
881
1174
|
);
|
|
882
1175
|
server.tool(
|
|
883
1176
|
"spawn_agents_parallel",
|
|
884
|
-
"Spawn multiple sub-agents in parallel across available backends. Each agent entry accepts the same parameters as spawn_agent. All agents are executed concurrently via Promise.allSettled, and results are returned as an array with per-agent status. RecursionGuard batch pre-validation ensures the entire batch fits within call limits before execution begins.",
|
|
1177
|
+
"Spawn multiple sub-agents in parallel across available backends. Each agent entry accepts the same parameters as spawn_agent. All agents are executed concurrently via Promise.allSettled, and results are returned as an array with per-agent status. RecursionGuard batch pre-validation ensures the entire batch fits within call limits before execution begins. Failed results include 'originalInput' for retry via retry_failed_agents.",
|
|
1178
|
+
spawnAgentsParallelInputShape,
|
|
1179
|
+
async (params, extra) => {
|
|
1180
|
+
try {
|
|
1181
|
+
const onProgress = (progress) => {
|
|
1182
|
+
const progressToken = params._meta ? params._meta.progressToken : void 0;
|
|
1183
|
+
if (progressToken !== void 0) {
|
|
1184
|
+
void extra.sendNotification({
|
|
1185
|
+
method: "notifications/progress",
|
|
1186
|
+
params: {
|
|
1187
|
+
progressToken,
|
|
1188
|
+
progress: progress.percent ?? 0,
|
|
1189
|
+
total: 100,
|
|
1190
|
+
message: progress.stage
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
const result = await executeSpawnAgentsParallel(
|
|
1196
|
+
params.agents,
|
|
1197
|
+
this.registry,
|
|
1198
|
+
this.sessionManager,
|
|
1199
|
+
this.guard,
|
|
1200
|
+
this.hooksEngine,
|
|
1201
|
+
this.contextMonitor,
|
|
1202
|
+
this.backendSelector,
|
|
1203
|
+
this._childHttpUrl,
|
|
1204
|
+
onProgress
|
|
1205
|
+
);
|
|
1206
|
+
const { text, isError } = formatParallelResponse(result);
|
|
1207
|
+
return {
|
|
1208
|
+
content: [{ type: "text", text }],
|
|
1209
|
+
isError
|
|
1210
|
+
};
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1213
|
+
return {
|
|
1214
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1215
|
+
isError: true
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
);
|
|
1220
|
+
server.tool(
|
|
1221
|
+
"retry_failed_agents",
|
|
1222
|
+
"Retry only the failed agents from a previous spawn_agents_parallel call. Pass the failed results array (with originalInput) directly.",
|
|
885
1223
|
{
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
resumeSessionId: z5.string().optional(),
|
|
892
|
-
model: z5.string().optional(),
|
|
1224
|
+
failedResults: z5.array(z5.object({
|
|
1225
|
+
index: z5.number(),
|
|
1226
|
+
originalInput: spawnAgentInputSchema
|
|
1227
|
+
})).min(1).describe("Array of failed results with their original input configurations"),
|
|
1228
|
+
overrides: z5.object({
|
|
893
1229
|
maxTurns: z5.number().optional(),
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
}).optional().describe("Skill context to inject into the sub-agent's system prompt"),
|
|
898
|
-
agentDefinition: z5.object({
|
|
899
|
-
definitionPath: z5.string().describe("Path to the agent definition file")
|
|
900
|
-
}).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
|
|
901
|
-
preferredBackend: z5.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override."),
|
|
902
|
-
taskType: z5.enum(["code", "document", "analysis", "mixed"]).optional().describe("Task type hint for automatic backend selection.")
|
|
903
|
-
})).min(1).max(10).describe("Array of agent configurations to execute in parallel (1-10 agents)")
|
|
1230
|
+
timeoutMs: z5.number().optional(),
|
|
1231
|
+
preferredBackend: z5.enum(["claude", "codex", "gemini"]).optional()
|
|
1232
|
+
}).optional().describe("Parameter overrides applied to all retried agents")
|
|
904
1233
|
},
|
|
905
1234
|
async (params) => {
|
|
906
1235
|
try {
|
|
1236
|
+
const agents = params.failedResults.map((r) => {
|
|
1237
|
+
const input = { ...r.originalInput };
|
|
1238
|
+
if (params.overrides) {
|
|
1239
|
+
if (params.overrides.maxTurns !== void 0) input.maxTurns = params.overrides.maxTurns;
|
|
1240
|
+
if (params.overrides.timeoutMs !== void 0) input.timeoutMs = params.overrides.timeoutMs;
|
|
1241
|
+
if (params.overrides.preferredBackend !== void 0) input.preferredBackend = params.overrides.preferredBackend;
|
|
1242
|
+
}
|
|
1243
|
+
return input;
|
|
1244
|
+
});
|
|
907
1245
|
const result = await executeSpawnAgentsParallel(
|
|
908
|
-
|
|
1246
|
+
agents,
|
|
909
1247
|
this.registry,
|
|
910
1248
|
this.sessionManager,
|
|
911
1249
|
this.guard,
|
|
@@ -914,8 +1252,7 @@ ${result.stdout}`;
|
|
|
914
1252
|
this.backendSelector,
|
|
915
1253
|
this._childHttpUrl
|
|
916
1254
|
);
|
|
917
|
-
const isError = result
|
|
918
|
-
const text = JSON.stringify(result, null, 2);
|
|
1255
|
+
const { text, isError } = formatParallelResponse(result);
|
|
919
1256
|
return {
|
|
920
1257
|
content: [{ type: "text", text }],
|
|
921
1258
|
isError
|
|
@@ -1041,14 +1378,14 @@ ${result.stdout}`;
|
|
|
1041
1378
|
});
|
|
1042
1379
|
this._httpServer = httpServer;
|
|
1043
1380
|
await this.server.connect(httpTransport);
|
|
1044
|
-
await new Promise((
|
|
1045
|
-
httpServer.listen(port, () => {
|
|
1381
|
+
await new Promise((resolve2) => {
|
|
1382
|
+
httpServer.listen(port, "127.0.0.1", () => {
|
|
1046
1383
|
logger.info(`MCP server listening on http://localhost:${port}/mcp`);
|
|
1047
|
-
|
|
1384
|
+
resolve2();
|
|
1048
1385
|
});
|
|
1049
1386
|
});
|
|
1050
|
-
await new Promise((
|
|
1051
|
-
httpServer.on("close",
|
|
1387
|
+
await new Promise((resolve2) => {
|
|
1388
|
+
httpServer.on("close", resolve2);
|
|
1052
1389
|
});
|
|
1053
1390
|
}
|
|
1054
1391
|
/**
|
|
@@ -1079,7 +1416,7 @@ ${result.stdout}`;
|
|
|
1079
1416
|
});
|
|
1080
1417
|
const server = new McpServer({
|
|
1081
1418
|
name: "agentic-relay",
|
|
1082
|
-
version: "0.
|
|
1419
|
+
version: "0.9.0"
|
|
1083
1420
|
});
|
|
1084
1421
|
this.registerTools(server);
|
|
1085
1422
|
transport.onclose = () => {
|
|
@@ -1095,15 +1432,31 @@ ${result.stdout}`;
|
|
|
1095
1432
|
if (sid) {
|
|
1096
1433
|
sessions.set(sid, { transport, server });
|
|
1097
1434
|
logger.debug(`Child MCP session created: ${sid}`);
|
|
1435
|
+
if (sessions.size > MAX_CHILD_HTTP_SESSIONS) {
|
|
1436
|
+
const oldestEntry = sessions.entries().next().value;
|
|
1437
|
+
if (oldestEntry) {
|
|
1438
|
+
const [oldestSessionId, oldestSession] = oldestEntry;
|
|
1439
|
+
sessions.delete(oldestSessionId);
|
|
1440
|
+
logger.warn(
|
|
1441
|
+
`Child MCP session evicted due to limit (${MAX_CHILD_HTTP_SESSIONS}): ${oldestSessionId}`
|
|
1442
|
+
);
|
|
1443
|
+
void oldestSession.transport.close().catch((error) => {
|
|
1444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1445
|
+
logger.debug(
|
|
1446
|
+
`Failed to close evicted child MCP session ${oldestSessionId}: ${message}`
|
|
1447
|
+
);
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1098
1451
|
}
|
|
1099
1452
|
});
|
|
1100
1453
|
this._childHttpServer = httpServer;
|
|
1101
|
-
await new Promise((
|
|
1454
|
+
await new Promise((resolve2) => {
|
|
1102
1455
|
httpServer.listen(0, "127.0.0.1", () => {
|
|
1103
1456
|
const addr = httpServer.address();
|
|
1104
1457
|
this._childHttpUrl = `http://127.0.0.1:${addr.port}/mcp`;
|
|
1105
1458
|
logger.info(`Child MCP server listening on ${this._childHttpUrl}`);
|
|
1106
|
-
|
|
1459
|
+
resolve2();
|
|
1107
1460
|
});
|
|
1108
1461
|
});
|
|
1109
1462
|
}
|
|
@@ -1255,10 +1608,11 @@ var AdapterRegistry = class {
|
|
|
1255
1608
|
|
|
1256
1609
|
// src/adapters/base-adapter.ts
|
|
1257
1610
|
init_logger();
|
|
1258
|
-
var BaseAdapter = class {
|
|
1611
|
+
var BaseAdapter = class _BaseAdapter {
|
|
1259
1612
|
constructor(processManager2) {
|
|
1260
1613
|
this.processManager = processManager2;
|
|
1261
1614
|
}
|
|
1615
|
+
static HEALTH_TIMEOUT_MS = 5e3;
|
|
1262
1616
|
async isInstalled() {
|
|
1263
1617
|
try {
|
|
1264
1618
|
const result = await this.processManager.execute("which", [this.command]);
|
|
@@ -1286,11 +1640,13 @@ var BaseAdapter = class {
|
|
|
1286
1640
|
};
|
|
1287
1641
|
}
|
|
1288
1642
|
async checkHealth() {
|
|
1289
|
-
const HEALTH_TIMEOUT = 5e3;
|
|
1290
1643
|
const installed = await Promise.race([
|
|
1291
1644
|
this.isInstalled(),
|
|
1292
1645
|
new Promise(
|
|
1293
|
-
(_, reject) => setTimeout(
|
|
1646
|
+
(_, reject) => setTimeout(
|
|
1647
|
+
() => reject(new Error("timeout")),
|
|
1648
|
+
_BaseAdapter.HEALTH_TIMEOUT_MS
|
|
1649
|
+
)
|
|
1294
1650
|
)
|
|
1295
1651
|
]).catch(() => false);
|
|
1296
1652
|
if (!installed) {
|
|
@@ -1304,14 +1660,29 @@ var BaseAdapter = class {
|
|
|
1304
1660
|
const version = await Promise.race([
|
|
1305
1661
|
this.getVersion(),
|
|
1306
1662
|
new Promise(
|
|
1307
|
-
(_, reject) => setTimeout(
|
|
1663
|
+
(_, reject) => setTimeout(
|
|
1664
|
+
() => reject(new Error("timeout")),
|
|
1665
|
+
_BaseAdapter.HEALTH_TIMEOUT_MS
|
|
1666
|
+
)
|
|
1308
1667
|
)
|
|
1309
1668
|
]).catch(() => void 0);
|
|
1669
|
+
const authStatus = await Promise.race([
|
|
1670
|
+
this.checkAuthStatus(),
|
|
1671
|
+
new Promise(
|
|
1672
|
+
(_, reject) => setTimeout(
|
|
1673
|
+
() => reject(new Error("timeout")),
|
|
1674
|
+
_BaseAdapter.HEALTH_TIMEOUT_MS
|
|
1675
|
+
)
|
|
1676
|
+
)
|
|
1677
|
+
]).catch(() => ({ authenticated: true }));
|
|
1678
|
+
const authenticated = authStatus.authenticated;
|
|
1679
|
+
const message = authStatus.message ?? (!authenticated ? `${this.id} authentication not configured` : void 0);
|
|
1310
1680
|
return {
|
|
1311
1681
|
installed: true,
|
|
1312
|
-
authenticated
|
|
1313
|
-
healthy:
|
|
1314
|
-
version
|
|
1682
|
+
authenticated,
|
|
1683
|
+
healthy: authenticated,
|
|
1684
|
+
version,
|
|
1685
|
+
...message ? { message } : {}
|
|
1315
1686
|
};
|
|
1316
1687
|
}
|
|
1317
1688
|
async getMCPConfig() {
|
|
@@ -1426,45 +1797,14 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
1426
1797
|
getConfigPath() {
|
|
1427
1798
|
return join(homedir(), ".claude.json");
|
|
1428
1799
|
}
|
|
1429
|
-
async
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
)
|
|
1436
|
-
]).catch(() => false);
|
|
1437
|
-
if (!installed) {
|
|
1438
|
-
return {
|
|
1439
|
-
installed: false,
|
|
1440
|
-
authenticated: false,
|
|
1441
|
-
healthy: false,
|
|
1442
|
-
message: "claude is not installed"
|
|
1443
|
-
};
|
|
1444
|
-
}
|
|
1445
|
-
const version = await Promise.race([
|
|
1446
|
-
this.getVersion(),
|
|
1447
|
-
new Promise(
|
|
1448
|
-
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1449
|
-
)
|
|
1450
|
-
]).catch(() => void 0);
|
|
1451
|
-
let authenticated = true;
|
|
1452
|
-
try {
|
|
1453
|
-
const result = await Promise.race([
|
|
1454
|
-
this.processManager.execute(this.command, ["auth", "status"]),
|
|
1455
|
-
new Promise(
|
|
1456
|
-
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1457
|
-
)
|
|
1458
|
-
]);
|
|
1459
|
-
authenticated = result.exitCode === 0;
|
|
1460
|
-
} catch {
|
|
1461
|
-
authenticated = true;
|
|
1462
|
-
}
|
|
1800
|
+
async checkAuthStatus() {
|
|
1801
|
+
const result = await this.processManager.execute(this.command, [
|
|
1802
|
+
"auth",
|
|
1803
|
+
"status"
|
|
1804
|
+
]);
|
|
1805
|
+
const authenticated = result.exitCode === 0;
|
|
1463
1806
|
return {
|
|
1464
|
-
installed: true,
|
|
1465
1807
|
authenticated,
|
|
1466
|
-
healthy: authenticated,
|
|
1467
|
-
version,
|
|
1468
1808
|
...!authenticated ? { message: "claude authentication not configured" } : {}
|
|
1469
1809
|
};
|
|
1470
1810
|
}
|
|
@@ -1515,7 +1855,16 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
1515
1855
|
let sessionId = "";
|
|
1516
1856
|
let isError = false;
|
|
1517
1857
|
let errorMessages = [];
|
|
1858
|
+
let totalInputTokens = 0;
|
|
1859
|
+
let totalOutputTokens = 0;
|
|
1518
1860
|
for await (const message of q) {
|
|
1861
|
+
if (message.type === "assistant") {
|
|
1862
|
+
const betaMessage = message.message;
|
|
1863
|
+
if (betaMessage?.usage) {
|
|
1864
|
+
totalInputTokens += betaMessage.usage.input_tokens ?? 0;
|
|
1865
|
+
totalOutputTokens += betaMessage.usage.output_tokens ?? 0;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1519
1868
|
if (message.type === "result") {
|
|
1520
1869
|
sessionId = message.session_id;
|
|
1521
1870
|
if (message.subtype === "success") {
|
|
@@ -1527,11 +1876,13 @@ var ClaudeAdapter = class extends BaseAdapter {
|
|
|
1527
1876
|
}
|
|
1528
1877
|
}
|
|
1529
1878
|
logger.debug(`Claude SDK session: ${sessionId}`);
|
|
1879
|
+
const hasTokenUsage = totalInputTokens > 0 || totalOutputTokens > 0;
|
|
1530
1880
|
return {
|
|
1531
1881
|
exitCode: isError ? 1 : 0,
|
|
1532
1882
|
stdout: resultText,
|
|
1533
1883
|
stderr: errorMessages.join("\n"),
|
|
1534
|
-
...sessionId ? { nativeSessionId: sessionId } : {}
|
|
1884
|
+
...sessionId ? { nativeSessionId: sessionId } : {},
|
|
1885
|
+
...hasTokenUsage ? { tokenUsage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens } } : {}
|
|
1535
1886
|
};
|
|
1536
1887
|
} catch (error) {
|
|
1537
1888
|
if (abortController.signal.aborted) {
|
|
@@ -1903,48 +2254,32 @@ var CodexAdapter = class extends BaseAdapter {
|
|
|
1903
2254
|
getConfigPath() {
|
|
1904
2255
|
return join2(homedir2(), ".codex", "config.toml");
|
|
1905
2256
|
}
|
|
1906
|
-
async
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
)
|
|
1913
|
-
]).catch(() => false);
|
|
1914
|
-
if (!installed) {
|
|
1915
|
-
return {
|
|
1916
|
-
installed: false,
|
|
1917
|
-
authenticated: false,
|
|
1918
|
-
healthy: false,
|
|
1919
|
-
message: "codex is not installed"
|
|
1920
|
-
};
|
|
1921
|
-
}
|
|
1922
|
-
const version = await Promise.race([
|
|
1923
|
-
this.getVersion(),
|
|
1924
|
-
new Promise(
|
|
1925
|
-
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1926
|
-
)
|
|
1927
|
-
]).catch(() => void 0);
|
|
1928
|
-
let authenticated = true;
|
|
1929
|
-
try {
|
|
1930
|
-
const result = await Promise.race([
|
|
1931
|
-
this.processManager.execute(this.command, ["login", "status"]),
|
|
1932
|
-
new Promise(
|
|
1933
|
-
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
1934
|
-
)
|
|
1935
|
-
]);
|
|
1936
|
-
authenticated = result.exitCode === 0;
|
|
1937
|
-
} catch {
|
|
1938
|
-
authenticated = true;
|
|
1939
|
-
}
|
|
2257
|
+
async checkAuthStatus() {
|
|
2258
|
+
const result = await this.processManager.execute(this.command, [
|
|
2259
|
+
"login",
|
|
2260
|
+
"status"
|
|
2261
|
+
]);
|
|
2262
|
+
const authenticated = result.exitCode === 0;
|
|
1940
2263
|
return {
|
|
1941
|
-
installed: true,
|
|
1942
2264
|
authenticated,
|
|
1943
|
-
healthy: authenticated,
|
|
1944
|
-
version,
|
|
1945
2265
|
...!authenticated ? { message: "codex authentication not configured" } : {}
|
|
1946
2266
|
};
|
|
1947
2267
|
}
|
|
2268
|
+
buildCodexOptions(flags) {
|
|
2269
|
+
if (!flags.mcpContext) {
|
|
2270
|
+
return {};
|
|
2271
|
+
}
|
|
2272
|
+
const env = {};
|
|
2273
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
2274
|
+
if (value !== void 0) {
|
|
2275
|
+
env[key] = value;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
env.RELAY_TRACE_ID = flags.mcpContext.traceId;
|
|
2279
|
+
env.RELAY_PARENT_SESSION_ID = flags.mcpContext.parentSessionId;
|
|
2280
|
+
env.RELAY_DEPTH = String(flags.mcpContext.depth);
|
|
2281
|
+
return { env };
|
|
2282
|
+
}
|
|
1948
2283
|
mapFlags(flags) {
|
|
1949
2284
|
const args = mapCommonToNative("codex", flags);
|
|
1950
2285
|
if (flags.outputFormat === "json") {
|
|
@@ -1997,16 +2332,7 @@ ${prompt}`;
|
|
|
1997
2332
|
);
|
|
1998
2333
|
try {
|
|
1999
2334
|
const { Codex } = await loadCodexSDK();
|
|
2000
|
-
const
|
|
2001
|
-
if (flags.mcpContext) {
|
|
2002
|
-
codexOptions.env = {
|
|
2003
|
-
...process.env,
|
|
2004
|
-
RELAY_TRACE_ID: flags.mcpContext.traceId,
|
|
2005
|
-
RELAY_PARENT_SESSION_ID: flags.mcpContext.parentSessionId,
|
|
2006
|
-
RELAY_DEPTH: String(flags.mcpContext.depth)
|
|
2007
|
-
};
|
|
2008
|
-
}
|
|
2009
|
-
const codex = new Codex(codexOptions);
|
|
2335
|
+
const codex = new Codex(this.buildCodexOptions(flags));
|
|
2010
2336
|
const thread = codex.startThread({
|
|
2011
2337
|
...flags.model ? { model: flags.model } : {},
|
|
2012
2338
|
workingDirectory: process.cwd(),
|
|
@@ -2038,16 +2364,7 @@ ${prompt}`;
|
|
|
2038
2364
|
);
|
|
2039
2365
|
try {
|
|
2040
2366
|
const { Codex } = await loadCodexSDK();
|
|
2041
|
-
const
|
|
2042
|
-
if (flags.mcpContext) {
|
|
2043
|
-
codexOptions.env = {
|
|
2044
|
-
...process.env,
|
|
2045
|
-
RELAY_TRACE_ID: flags.mcpContext.traceId,
|
|
2046
|
-
RELAY_PARENT_SESSION_ID: flags.mcpContext.parentSessionId,
|
|
2047
|
-
RELAY_DEPTH: String(flags.mcpContext.depth)
|
|
2048
|
-
};
|
|
2049
|
-
}
|
|
2050
|
-
const codex = new Codex(codexOptions);
|
|
2367
|
+
const codex = new Codex(this.buildCodexOptions(flags));
|
|
2051
2368
|
const thread = codex.startThread({
|
|
2052
2369
|
...flags.model ? { model: flags.model } : {},
|
|
2053
2370
|
workingDirectory: process.cwd(),
|
|
@@ -2061,15 +2378,15 @@ ${prompt}`;
|
|
|
2061
2378
|
threadId = event.thread_id;
|
|
2062
2379
|
} else if (event.type === "item.started") {
|
|
2063
2380
|
const item = event.item;
|
|
2064
|
-
if (item
|
|
2381
|
+
if (item.type === "agent_message" && item.text) {
|
|
2065
2382
|
yield { type: "text", text: item.text };
|
|
2066
|
-
} else if (item
|
|
2383
|
+
} else if (item.type === "command_execution") {
|
|
2067
2384
|
yield {
|
|
2068
2385
|
type: "tool_start",
|
|
2069
2386
|
tool: item.command ?? "command",
|
|
2070
2387
|
id: item.id ?? ""
|
|
2071
2388
|
};
|
|
2072
|
-
} else if (item
|
|
2389
|
+
} else if (item.type === "file_change") {
|
|
2073
2390
|
yield {
|
|
2074
2391
|
type: "tool_start",
|
|
2075
2392
|
tool: "file_change",
|
|
@@ -2078,17 +2395,17 @@ ${prompt}`;
|
|
|
2078
2395
|
}
|
|
2079
2396
|
} else if (event.type === "item.completed") {
|
|
2080
2397
|
const item = event.item;
|
|
2081
|
-
if (item
|
|
2398
|
+
if (item.type === "agent_message" && item.text) {
|
|
2082
2399
|
completedMessages.push(item.text);
|
|
2083
2400
|
yield { type: "text", text: item.text };
|
|
2084
|
-
} else if (item
|
|
2401
|
+
} else if (item.type === "command_execution") {
|
|
2085
2402
|
yield {
|
|
2086
2403
|
type: "tool_end",
|
|
2087
2404
|
tool: item.command ?? "command",
|
|
2088
2405
|
id: item.id ?? "",
|
|
2089
2406
|
result: item.aggregated_output
|
|
2090
2407
|
};
|
|
2091
|
-
} else if (item
|
|
2408
|
+
} else if (item.type === "file_change") {
|
|
2092
2409
|
yield {
|
|
2093
2410
|
type: "tool_end",
|
|
2094
2411
|
tool: "file_change",
|
|
@@ -2111,7 +2428,7 @@ ${prompt}`;
|
|
|
2111
2428
|
nativeSessionId: threadId ?? thread.id ?? void 0
|
|
2112
2429
|
};
|
|
2113
2430
|
} else if (event.type === "turn.failed") {
|
|
2114
|
-
const errorMessage = event.error
|
|
2431
|
+
const errorMessage = event.error.message ?? "Turn failed";
|
|
2115
2432
|
yield {
|
|
2116
2433
|
type: "done",
|
|
2117
2434
|
result: { exitCode: 1, stdout: "", stderr: errorMessage },
|
|
@@ -2248,37 +2565,18 @@ var GeminiAdapter = class extends BaseAdapter {
|
|
|
2248
2565
|
getConfigPath() {
|
|
2249
2566
|
return join3(homedir3(), ".gemini", "settings.json");
|
|
2250
2567
|
}
|
|
2251
|
-
async
|
|
2252
|
-
const
|
|
2253
|
-
const
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
2257
|
-
)
|
|
2258
|
-
]).catch(() => false);
|
|
2259
|
-
if (!installed) {
|
|
2568
|
+
async checkAuthStatus() {
|
|
2569
|
+
const hasApiKey = !!process.env["GEMINI_API_KEY"];
|
|
2570
|
+
const hasGoogleAdc = !!process.env["GOOGLE_APPLICATION_CREDENTIALS"] || !!process.env["CLOUDSDK_CONFIG"];
|
|
2571
|
+
const authenticated = hasApiKey || hasGoogleAdc;
|
|
2572
|
+
if (!authenticated) {
|
|
2260
2573
|
return {
|
|
2261
|
-
|
|
2262
|
-
authenticated:
|
|
2263
|
-
|
|
2264
|
-
message: "gemini is not installed"
|
|
2574
|
+
// Optimistic fallback: Gemini may still authenticate via ADC at runtime.
|
|
2575
|
+
authenticated: true,
|
|
2576
|
+
message: "Gemini authentication status unknown - ADC may be available at runtime"
|
|
2265
2577
|
};
|
|
2266
2578
|
}
|
|
2267
|
-
|
|
2268
|
-
this.getVersion(),
|
|
2269
|
-
new Promise(
|
|
2270
|
-
(_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
|
|
2271
|
-
)
|
|
2272
|
-
]).catch(() => void 0);
|
|
2273
|
-
const hasApiKey = !!process.env["GEMINI_API_KEY"];
|
|
2274
|
-
const hasGoogleAdc = !!process.env["GOOGLE_APPLICATION_CREDENTIALS"] || !!process.env["CLOUDSDK_CONFIG"];
|
|
2275
|
-
const authenticated = hasApiKey || hasGoogleAdc || true;
|
|
2276
|
-
return {
|
|
2277
|
-
installed: true,
|
|
2278
|
-
authenticated,
|
|
2279
|
-
healthy: true,
|
|
2280
|
-
version
|
|
2281
|
-
};
|
|
2579
|
+
return { authenticated };
|
|
2282
2580
|
}
|
|
2283
2581
|
mapFlags(flags) {
|
|
2284
2582
|
const args = mapCommonToNative("gemini", flags);
|
|
@@ -3167,6 +3465,7 @@ var HooksEngine = class _HooksEngine {
|
|
|
3167
3465
|
};
|
|
3168
3466
|
|
|
3169
3467
|
// src/core/context-monitor.ts
|
|
3468
|
+
init_logger();
|
|
3170
3469
|
var DEFAULT_BACKEND_CONTEXT = {
|
|
3171
3470
|
claude: { contextWindow: 2e5, compactThreshold: 19e4 },
|
|
3172
3471
|
codex: { contextWindow: 272e3, compactThreshold: 258400 },
|
|
@@ -3285,7 +3584,9 @@ var ContextMonitor = class {
|
|
|
3285
3584
|
remainingBeforeCompact
|
|
3286
3585
|
}
|
|
3287
3586
|
};
|
|
3288
|
-
void this.hooksEngine.emit("on-context-threshold", hookInput)
|
|
3587
|
+
void this.hooksEngine.emit("on-context-threshold", hookInput).catch(
|
|
3588
|
+
(e) => logger.debug("Context threshold hook error:", e)
|
|
3589
|
+
);
|
|
3289
3590
|
}
|
|
3290
3591
|
}
|
|
3291
3592
|
};
|
|
@@ -4061,7 +4362,7 @@ function createVersionCommand(registry2) {
|
|
|
4061
4362
|
description: "Show relay and backend versions"
|
|
4062
4363
|
},
|
|
4063
4364
|
async run() {
|
|
4064
|
-
const relayVersion = "0.
|
|
4365
|
+
const relayVersion = "0.9.0";
|
|
4065
4366
|
console.log(`agentic-relay v${relayVersion}`);
|
|
4066
4367
|
console.log("");
|
|
4067
4368
|
console.log("Backends:");
|
|
@@ -4088,9 +4389,9 @@ import { defineCommand as defineCommand8 } from "citty";
|
|
|
4088
4389
|
import { access, constants, readdir as readdir2 } from "fs/promises";
|
|
4089
4390
|
import { join as join7 } from "path";
|
|
4090
4391
|
import { homedir as homedir5 } from "os";
|
|
4091
|
-
import { execFile } from "child_process";
|
|
4092
|
-
import { promisify } from "util";
|
|
4093
|
-
var
|
|
4392
|
+
import { execFile as execFile2 } from "child_process";
|
|
4393
|
+
import { promisify as promisify2 } from "util";
|
|
4394
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
4094
4395
|
async function checkNodeVersion() {
|
|
4095
4396
|
const version = process.version;
|
|
4096
4397
|
const major = Number(version.slice(1).split(".")[0]);
|
|
@@ -4190,7 +4491,7 @@ async function checkMCPServerCommands(configManager2) {
|
|
|
4190
4491
|
for (const [name, server] of Object.entries(mcpServers)) {
|
|
4191
4492
|
const command = server.command;
|
|
4192
4493
|
try {
|
|
4193
|
-
await
|
|
4494
|
+
await execFileAsync2("which", [command]);
|
|
4194
4495
|
results.push({
|
|
4195
4496
|
label: `MCP server: ${name}`,
|
|
4196
4497
|
ok: true,
|
|
@@ -4386,6 +4687,7 @@ function createInitCommand() {
|
|
|
4386
4687
|
}
|
|
4387
4688
|
|
|
4388
4689
|
// src/bin/relay.ts
|
|
4690
|
+
init_logger();
|
|
4389
4691
|
var processManager = new ProcessManager();
|
|
4390
4692
|
var registry = new AdapterRegistry();
|
|
4391
4693
|
registry.registerLazy("claude", () => new ClaudeAdapter(processManager));
|
|
@@ -4406,12 +4708,11 @@ void configManager.getConfig().then((config) => {
|
|
|
4406
4708
|
if (config.contextMonitor) {
|
|
4407
4709
|
contextMonitor = new ContextMonitor(hooksEngine, config.contextMonitor);
|
|
4408
4710
|
}
|
|
4409
|
-
}).catch(() =>
|
|
4410
|
-
});
|
|
4711
|
+
}).catch((e) => logger.debug("Config load failed:", e));
|
|
4411
4712
|
var main = defineCommand10({
|
|
4412
4713
|
meta: {
|
|
4413
4714
|
name: "relay",
|
|
4414
|
-
version: "0.
|
|
4715
|
+
version: "0.9.0",
|
|
4415
4716
|
description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
|
|
4416
4717
|
},
|
|
4417
4718
|
subCommands: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rk0429/agentic-relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI with MCP-based multi-layer sub-agent orchestration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -40,7 +40,9 @@
|
|
|
40
40
|
"test": "vitest run",
|
|
41
41
|
"test:coverage": "vitest run --coverage",
|
|
42
42
|
"test:watch": "vitest",
|
|
43
|
-
"lint": "tsc --noEmit",
|
|
43
|
+
"lint": "tsc --noEmit && eslint .",
|
|
44
|
+
"format": "prettier --write .",
|
|
45
|
+
"format:check": "prettier --check .",
|
|
44
46
|
"prepublishOnly": "pnpm test && pnpm build"
|
|
45
47
|
},
|
|
46
48
|
"engines": {
|
|
@@ -57,10 +59,15 @@
|
|
|
57
59
|
"zod": "^3.24.2"
|
|
58
60
|
},
|
|
59
61
|
"devDependencies": {
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
60
63
|
"@types/node": "^25.3.0",
|
|
61
64
|
"@vitest/coverage-v8": "^3.2.4",
|
|
65
|
+
"eslint": "^10.0.2",
|
|
66
|
+
"eslint-config-prettier": "^10.1.8",
|
|
67
|
+
"prettier": "^3.8.1",
|
|
62
68
|
"tsup": "^8.4.0",
|
|
63
69
|
"typescript": "^5.7.3",
|
|
70
|
+
"typescript-eslint": "^8.56.1",
|
|
64
71
|
"vitest": "^3.0.7"
|
|
65
72
|
},
|
|
66
73
|
"pnpm": {
|