@rk0429/agentic-relay 0.10.1 → 0.12.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/dist/relay.mjs +190 -100
- package/package.json +1 -1
package/dist/relay.mjs
CHANGED
|
@@ -55,8 +55,8 @@ var init_recursion_guard = __esm({
|
|
|
55
55
|
RecursionGuard = class _RecursionGuard {
|
|
56
56
|
constructor(config = {
|
|
57
57
|
maxDepth: 5,
|
|
58
|
-
maxCallsPerSession:
|
|
59
|
-
timeoutSec:
|
|
58
|
+
maxCallsPerSession: 50,
|
|
59
|
+
timeoutSec: 1800
|
|
60
60
|
}) {
|
|
61
61
|
this.config = config;
|
|
62
62
|
}
|
|
@@ -110,10 +110,15 @@ var init_recursion_guard = __esm({
|
|
|
110
110
|
reason: `Max calls per session exceeded: ${currentCount} >= ${this.config.maxCallsPerSession}`
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
|
-
if (this.detectLoop(
|
|
113
|
+
if (this.detectLoop(
|
|
114
|
+
context.traceId,
|
|
115
|
+
context.backend,
|
|
116
|
+
context.promptHash,
|
|
117
|
+
context.taskType
|
|
118
|
+
)) {
|
|
114
119
|
return {
|
|
115
120
|
allowed: false,
|
|
116
|
-
reason: `Loop detected: same (backend=${context.backend}, promptHash=${context.promptHash}) appeared
|
|
121
|
+
reason: `Loop detected: same (backend=${context.backend}, taskType=${context.taskType ?? ""}, promptHash=${context.promptHash}) appeared 5+ times in trace ${context.traceId}`
|
|
117
122
|
};
|
|
118
123
|
}
|
|
119
124
|
return { allowed: true };
|
|
@@ -127,7 +132,7 @@ var init_recursion_guard = __esm({
|
|
|
127
132
|
} else {
|
|
128
133
|
this.callCounts.set(context.traceId, { count: 1, createdAt: Date.now() });
|
|
129
134
|
}
|
|
130
|
-
const key = `${context.backend}:${context.promptHash}`;
|
|
135
|
+
const key = `${context.backend}:${context.taskType ?? ""}:${context.promptHash}`;
|
|
131
136
|
const hashEntry = this.promptHashes.get(context.traceId);
|
|
132
137
|
if (hashEntry) {
|
|
133
138
|
hashEntry.hashes.push(key);
|
|
@@ -135,12 +140,12 @@ var init_recursion_guard = __esm({
|
|
|
135
140
|
this.promptHashes.set(context.traceId, { hashes: [key], createdAt: Date.now() });
|
|
136
141
|
}
|
|
137
142
|
}
|
|
138
|
-
/** Detect if the same (backend + promptHash) combination has appeared
|
|
139
|
-
detectLoop(traceId, backend, promptHash) {
|
|
140
|
-
const key = `${backend}:${promptHash}`;
|
|
143
|
+
/** Detect if the same (backend + taskType + promptHash) combination has appeared 5+ times */
|
|
144
|
+
detectLoop(traceId, backend, promptHash, taskType) {
|
|
145
|
+
const key = `${backend}:${taskType ?? ""}:${promptHash}`;
|
|
141
146
|
const hashes = this.promptHashes.get(traceId)?.hashes ?? [];
|
|
142
147
|
const count = hashes.filter((h) => h === key).length;
|
|
143
|
-
return count >=
|
|
148
|
+
return count >= 5;
|
|
144
149
|
}
|
|
145
150
|
/** Get current config (for testing/inspection) */
|
|
146
151
|
getConfig() {
|
|
@@ -344,7 +349,8 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
|
|
|
344
349
|
traceId: envContext.traceId,
|
|
345
350
|
depth: envContext.depth,
|
|
346
351
|
backend: effectiveBackend,
|
|
347
|
-
promptHash
|
|
352
|
+
promptHash,
|
|
353
|
+
taskType: input.taskType
|
|
348
354
|
};
|
|
349
355
|
const guardResult = guard.canSpawn(context);
|
|
350
356
|
if (!guardResult.allowed) {
|
|
@@ -1041,6 +1047,24 @@ var init_backend_selector = __esm({
|
|
|
1041
1047
|
}
|
|
1042
1048
|
});
|
|
1043
1049
|
|
|
1050
|
+
// src/types/mcp.ts
|
|
1051
|
+
var DEFAULT_TASK_TTL, DEFAULT_POLL_INTERVAL;
|
|
1052
|
+
var init_mcp = __esm({
|
|
1053
|
+
"src/types/mcp.ts"() {
|
|
1054
|
+
"use strict";
|
|
1055
|
+
DEFAULT_TASK_TTL = 5 * 60 * 1e3;
|
|
1056
|
+
DEFAULT_POLL_INTERVAL = 3e3;
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
// src/types/index.ts
|
|
1061
|
+
var init_types = __esm({
|
|
1062
|
+
"src/types/index.ts"() {
|
|
1063
|
+
"use strict";
|
|
1064
|
+
init_mcp();
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1044
1068
|
// src/mcp-server/response-formatter.ts
|
|
1045
1069
|
function formatSpawnAgentResponse(result) {
|
|
1046
1070
|
const isError = result.exitCode !== 0;
|
|
@@ -1113,9 +1137,20 @@ __export(server_exports, {
|
|
|
1113
1137
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1114
1138
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1115
1139
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1140
|
+
import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
1141
|
+
import { InMemoryTaskMessageQueue } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
1116
1142
|
import { createServer } from "http";
|
|
1117
1143
|
import { randomUUID } from "crypto";
|
|
1118
1144
|
import { z as z5 } from "zod";
|
|
1145
|
+
function createMcpServerOptions() {
|
|
1146
|
+
const taskStore = new InMemoryTaskStore();
|
|
1147
|
+
return {
|
|
1148
|
+
capabilities: { tasks: { requests: { tools: { call: {} } } } },
|
|
1149
|
+
taskStore,
|
|
1150
|
+
taskMessageQueue: new InMemoryTaskMessageQueue(),
|
|
1151
|
+
defaultTaskPollInterval: DEFAULT_POLL_INTERVAL
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1119
1154
|
var spawnAgentsParallelInputShape, MAX_CHILD_HTTP_SESSIONS, RelayMCPServer;
|
|
1120
1155
|
var init_server = __esm({
|
|
1121
1156
|
"src/mcp-server/server.ts"() {
|
|
@@ -1128,6 +1163,7 @@ var init_server = __esm({
|
|
|
1128
1163
|
init_list_available_backends();
|
|
1129
1164
|
init_backend_selector();
|
|
1130
1165
|
init_logger();
|
|
1166
|
+
init_types();
|
|
1131
1167
|
init_response_formatter();
|
|
1132
1168
|
spawnAgentsParallelInputShape = {
|
|
1133
1169
|
agents: z5.array(spawnAgentInputSchema).min(1).max(10).describe(
|
|
@@ -1143,10 +1179,10 @@ var init_server = __esm({
|
|
|
1143
1179
|
this.contextMonitor = contextMonitor2;
|
|
1144
1180
|
this.guard = new RecursionGuard(guardConfig);
|
|
1145
1181
|
this.backendSelector = new BackendSelector();
|
|
1146
|
-
this.server = new McpServer(
|
|
1147
|
-
name: "agentic-relay",
|
|
1148
|
-
|
|
1149
|
-
|
|
1182
|
+
this.server = new McpServer(
|
|
1183
|
+
{ name: "agentic-relay", version: "0.12.0" },
|
|
1184
|
+
createMcpServerOptions()
|
|
1185
|
+
);
|
|
1150
1186
|
this.registerTools(this.server);
|
|
1151
1187
|
}
|
|
1152
1188
|
server;
|
|
@@ -1162,94 +1198,148 @@ var init_server = __esm({
|
|
|
1162
1198
|
get childHttpServer() {
|
|
1163
1199
|
return this._childHttpServer;
|
|
1164
1200
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1201
|
+
/**
|
|
1202
|
+
* Run spawn_agent in the background and store the result in taskStore.
|
|
1203
|
+
* Fire-and-forget: errors are caught and stored as failed task results.
|
|
1204
|
+
*/
|
|
1205
|
+
runSpawnAgentInBackground(taskId, params, extra) {
|
|
1206
|
+
const run = async () => {
|
|
1207
|
+
try {
|
|
1208
|
+
const result = await executeSpawnAgent(
|
|
1209
|
+
params,
|
|
1210
|
+
this.registry,
|
|
1211
|
+
this.sessionManager,
|
|
1212
|
+
this.guard,
|
|
1213
|
+
this.hooksEngine,
|
|
1214
|
+
this.contextMonitor,
|
|
1215
|
+
this.backendSelector,
|
|
1216
|
+
this._childHttpUrl
|
|
1217
|
+
);
|
|
1218
|
+
const { text, isError } = formatSpawnAgentResponse(result);
|
|
1219
|
+
const callToolResult = {
|
|
1220
|
+
content: [{ type: "text", text }],
|
|
1221
|
+
isError
|
|
1222
|
+
};
|
|
1223
|
+
await extra.taskStore.storeTaskResult(
|
|
1224
|
+
taskId,
|
|
1225
|
+
"completed",
|
|
1226
|
+
callToolResult
|
|
1227
|
+
);
|
|
1228
|
+
} catch (outerError) {
|
|
1171
1229
|
try {
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
progress: progress.percent ?? 0,
|
|
1180
|
-
total: 100,
|
|
1181
|
-
message: progress.stage
|
|
1182
|
-
}
|
|
1183
|
-
});
|
|
1230
|
+
const message = outerError instanceof Error ? outerError.message : String(outerError);
|
|
1231
|
+
await extra.taskStore.storeTaskResult(
|
|
1232
|
+
taskId,
|
|
1233
|
+
"failed",
|
|
1234
|
+
{
|
|
1235
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1236
|
+
isError: true
|
|
1184
1237
|
}
|
|
1185
|
-
};
|
|
1186
|
-
const result = await executeSpawnAgent(
|
|
1187
|
-
params,
|
|
1188
|
-
this.registry,
|
|
1189
|
-
this.sessionManager,
|
|
1190
|
-
this.guard,
|
|
1191
|
-
this.hooksEngine,
|
|
1192
|
-
this.contextMonitor,
|
|
1193
|
-
this.backendSelector,
|
|
1194
|
-
this._childHttpUrl,
|
|
1195
|
-
onProgress
|
|
1196
1238
|
);
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
};
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1204
|
-
return {
|
|
1205
|
-
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1206
|
-
isError: true
|
|
1207
|
-
};
|
|
1239
|
+
} catch (storeError) {
|
|
1240
|
+
logger.error(
|
|
1241
|
+
`Failed to store task result for ${taskId}: ${storeError instanceof Error ? storeError.message : String(storeError)}`
|
|
1242
|
+
);
|
|
1208
1243
|
}
|
|
1209
1244
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1245
|
+
};
|
|
1246
|
+
void run();
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Run spawn_agents_parallel in the background and store the result in taskStore.
|
|
1250
|
+
* Fire-and-forget: errors are caught and stored as failed task results.
|
|
1251
|
+
*/
|
|
1252
|
+
runSpawnAgentsParallelInBackground(taskId, agents, extra) {
|
|
1253
|
+
const run = async () => {
|
|
1254
|
+
try {
|
|
1255
|
+
const result = await executeSpawnAgentsParallel(
|
|
1256
|
+
agents,
|
|
1257
|
+
this.registry,
|
|
1258
|
+
this.sessionManager,
|
|
1259
|
+
this.guard,
|
|
1260
|
+
this.hooksEngine,
|
|
1261
|
+
this.contextMonitor,
|
|
1262
|
+
this.backendSelector,
|
|
1263
|
+
this._childHttpUrl
|
|
1264
|
+
);
|
|
1265
|
+
const { text, isError } = formatParallelResponse(result);
|
|
1266
|
+
const callToolResult = {
|
|
1267
|
+
content: [{ type: "text", text }],
|
|
1268
|
+
isError
|
|
1269
|
+
};
|
|
1270
|
+
await extra.taskStore.storeTaskResult(
|
|
1271
|
+
taskId,
|
|
1272
|
+
"completed",
|
|
1273
|
+
callToolResult
|
|
1274
|
+
);
|
|
1275
|
+
} catch (outerError) {
|
|
1216
1276
|
try {
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
progress: progress.percent ?? 0,
|
|
1225
|
-
total: 100,
|
|
1226
|
-
message: progress.stage
|
|
1227
|
-
}
|
|
1228
|
-
});
|
|
1277
|
+
const message = outerError instanceof Error ? outerError.message : String(outerError);
|
|
1278
|
+
await extra.taskStore.storeTaskResult(
|
|
1279
|
+
taskId,
|
|
1280
|
+
"failed",
|
|
1281
|
+
{
|
|
1282
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
1283
|
+
isError: true
|
|
1229
1284
|
}
|
|
1230
|
-
};
|
|
1231
|
-
const result = await executeSpawnAgentsParallel(
|
|
1232
|
-
params.agents,
|
|
1233
|
-
this.registry,
|
|
1234
|
-
this.sessionManager,
|
|
1235
|
-
this.guard,
|
|
1236
|
-
this.hooksEngine,
|
|
1237
|
-
this.contextMonitor,
|
|
1238
|
-
this.backendSelector,
|
|
1239
|
-
this._childHttpUrl,
|
|
1240
|
-
onProgress
|
|
1241
1285
|
);
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1286
|
+
} catch (storeError) {
|
|
1287
|
+
logger.error(
|
|
1288
|
+
`Failed to store parallel task result for ${taskId}: ${storeError instanceof Error ? storeError.message : String(storeError)}`
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
void run();
|
|
1294
|
+
}
|
|
1295
|
+
registerTools(server) {
|
|
1296
|
+
server.experimental.tasks.registerToolTask(
|
|
1297
|
+
"spawn_agent",
|
|
1298
|
+
{
|
|
1299
|
+
description: "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.",
|
|
1300
|
+
inputSchema: spawnAgentInputSchema.shape,
|
|
1301
|
+
execution: { taskSupport: "optional" }
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
createTask: async (params, extra) => {
|
|
1305
|
+
const task = await extra.taskStore.createTask({
|
|
1306
|
+
ttl: DEFAULT_TASK_TTL,
|
|
1307
|
+
pollInterval: DEFAULT_POLL_INTERVAL
|
|
1308
|
+
});
|
|
1309
|
+
this.runSpawnAgentInBackground(task.taskId, params, extra);
|
|
1310
|
+
return { task };
|
|
1311
|
+
},
|
|
1312
|
+
getTask: async (_params, extra) => {
|
|
1313
|
+
const task = await extra.taskStore.getTask(extra.taskId);
|
|
1314
|
+
return { task: task ?? void 0 };
|
|
1315
|
+
},
|
|
1316
|
+
getTaskResult: async (_params, extra) => {
|
|
1317
|
+
return await extra.taskStore.getTaskResult(extra.taskId);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
);
|
|
1321
|
+
server.experimental.tasks.registerToolTask(
|
|
1322
|
+
"spawn_agents_parallel",
|
|
1323
|
+
{
|
|
1324
|
+
description: "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.",
|
|
1325
|
+
inputSchema: spawnAgentsParallelInputShape,
|
|
1326
|
+
execution: { taskSupport: "optional" }
|
|
1327
|
+
},
|
|
1328
|
+
{
|
|
1329
|
+
createTask: async (params, extra) => {
|
|
1330
|
+
const task = await extra.taskStore.createTask({
|
|
1331
|
+
ttl: DEFAULT_TASK_TTL,
|
|
1332
|
+
pollInterval: DEFAULT_POLL_INTERVAL
|
|
1333
|
+
});
|
|
1334
|
+
this.runSpawnAgentsParallelInBackground(task.taskId, params.agents, extra);
|
|
1335
|
+
return { task };
|
|
1336
|
+
},
|
|
1337
|
+
getTask: async (_params, extra) => {
|
|
1338
|
+
const task = await extra.taskStore.getTask(extra.taskId);
|
|
1339
|
+
return { task: task ?? void 0 };
|
|
1340
|
+
},
|
|
1341
|
+
getTaskResult: async (_params, extra) => {
|
|
1342
|
+
return await extra.taskStore.getTaskResult(extra.taskId);
|
|
1253
1343
|
}
|
|
1254
1344
|
}
|
|
1255
1345
|
);
|
|
@@ -1451,10 +1541,10 @@ var init_server = __esm({
|
|
|
1451
1541
|
const transport = new StreamableHTTPServerTransport({
|
|
1452
1542
|
sessionIdGenerator: () => randomUUID()
|
|
1453
1543
|
});
|
|
1454
|
-
const server = new McpServer(
|
|
1455
|
-
name: "agentic-relay",
|
|
1456
|
-
|
|
1457
|
-
|
|
1544
|
+
const server = new McpServer(
|
|
1545
|
+
{ name: "agentic-relay", version: "0.12.0" },
|
|
1546
|
+
createMcpServerOptions()
|
|
1547
|
+
);
|
|
1458
1548
|
this.registerTools(server);
|
|
1459
1549
|
transport.onclose = () => {
|
|
1460
1550
|
const sid2 = transport.sessionId;
|
|
@@ -4405,7 +4495,7 @@ function createVersionCommand(registry2) {
|
|
|
4405
4495
|
description: "Show relay and backend versions"
|
|
4406
4496
|
},
|
|
4407
4497
|
async run() {
|
|
4408
|
-
const relayVersion = "0.
|
|
4498
|
+
const relayVersion = "0.12.0";
|
|
4409
4499
|
console.log(`agentic-relay v${relayVersion}`);
|
|
4410
4500
|
console.log("");
|
|
4411
4501
|
console.log("Backends:");
|
|
@@ -4755,7 +4845,7 @@ void configManager.getConfig().then((config) => {
|
|
|
4755
4845
|
var main = defineCommand10({
|
|
4756
4846
|
meta: {
|
|
4757
4847
|
name: "relay",
|
|
4758
|
-
version: "0.
|
|
4848
|
+
version: "0.12.0",
|
|
4759
4849
|
description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
|
|
4760
4850
|
},
|
|
4761
4851
|
subCommands: {
|
package/package.json
CHANGED