@rk0429/agentic-relay 0.8.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.
Files changed (2) hide show
  1. package/dist/relay.mjs +141 -45
  2. package/package.json +1 -1
package/dist/relay.mjs CHANGED
@@ -307,6 +307,12 @@ function buildChildMcpServers(parentMcpServers, childHttpUrl) {
307
307
  }
308
308
  return result;
309
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
+ }
310
316
  function buildContextFromEnv() {
311
317
  const traceId = process.env["RELAY_TRACE_ID"] ?? `trace-${nanoid2()}`;
312
318
  const parentSessionId = process.env["RELAY_PARENT_SESSION_ID"] ?? null;
@@ -344,7 +350,8 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
344
350
  sessionId: "",
345
351
  exitCode: 1,
346
352
  stdout: "",
347
- stderr: `Spawn blocked: ${guardResult.reason}`
353
+ stderr: `Spawn blocked: ${guardResult.reason}`,
354
+ failureReason: "recursion_blocked"
348
355
  };
349
356
  }
350
357
  const adapter = registry2.get(effectiveBackend);
@@ -354,7 +361,8 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
354
361
  sessionId: "",
355
362
  exitCode: 1,
356
363
  stdout: "",
357
- 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"
358
366
  };
359
367
  }
360
368
  const spawnStartedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -446,7 +454,8 @@ ${wrapped}` : wrapped;
446
454
  sessionId: "",
447
455
  exitCode: 1,
448
456
  stdout: "",
449
- stderr: `Task instruction file not found: ${input.taskInstructionPath}`
457
+ stderr: `Task instruction file not found: ${input.taskInstructionPath}`,
458
+ failureReason: "instruction_file_error"
450
459
  };
451
460
  }
452
461
  const instructionContent = readFileSync(safePath, "utf-8");
@@ -459,7 +468,8 @@ ${input.prompt}`;
459
468
  sessionId: "",
460
469
  exitCode: 1,
461
470
  stdout: "",
462
- stderr: `Failed to read task instruction file: ${message}`
471
+ stderr: `Failed to read task instruction file: ${message}`,
472
+ failureReason: "instruction_file_error"
463
473
  };
464
474
  }
465
475
  }
@@ -473,7 +483,8 @@ ${input.prompt}`;
473
483
  exitCode: 1,
474
484
  stdout: "",
475
485
  stderr: `Backend "${effectiveBackend}" does not support session continuation (continueSession).`,
476
- _noSession: true
486
+ _noSession: true,
487
+ _failureReason: "session_continuation_unsupported"
477
488
  };
478
489
  }
479
490
  return adapter.continueSession(input.resumeSessionId, effectivePrompt);
@@ -517,7 +528,8 @@ ${input.prompt}`;
517
528
  sessionId: session.relaySessionId,
518
529
  exitCode: result.exitCode,
519
530
  stdout: result.stdout,
520
- stderr: result.stderr
531
+ stderr: result.stderr,
532
+ ..."_failureReason" in result ? { failureReason: result._failureReason } : {}
521
533
  };
522
534
  }
523
535
  onProgress?.({ stage: "executing", percent: 50 });
@@ -533,6 +545,7 @@ ${input.prompt}`;
533
545
  }
534
546
  guard.recordSpawn(context);
535
547
  const status = result.exitCode === 0 ? "completed" : "error";
548
+ const failureReason = result.exitCode !== 0 ? inferFailureReason(result.stderr, result.stdout) : void 0;
536
549
  await sessionManager2.update(session.relaySessionId, { status });
537
550
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
538
551
  const metadata = {
@@ -541,7 +554,8 @@ ${input.prompt}`;
541
554
  ...input.preferredBackend ? { requestedBackend: input.preferredBackend } : {},
542
555
  selectionReason,
543
556
  startedAt: spawnStartedAt,
544
- completedAt
557
+ completedAt,
558
+ ...result.tokenUsage ? { tokenUsage: result.tokenUsage } : {}
545
559
  };
546
560
  onProgress?.({ stage: "completed", percent: 100 });
547
561
  if (hooksEngine2) {
@@ -581,16 +595,19 @@ ${input.prompt}`;
581
595
  stdout: result.stdout,
582
596
  stderr: result.stderr,
583
597
  nativeSessionId: result.nativeSessionId,
584
- metadata
598
+ metadata,
599
+ ...failureReason ? { failureReason } : {}
585
600
  };
586
601
  } catch (error) {
587
602
  await sessionManager2.update(session.relaySessionId, { status: "error" });
588
603
  const message = error instanceof Error ? error.message : String(error);
604
+ const catchFailureReason = message.toLowerCase().includes("timed out") || message.toLowerCase().includes("timeout") ? "timeout" : "unknown";
589
605
  return {
590
606
  sessionId: session.relaySessionId,
591
607
  exitCode: 1,
592
608
  stdout: "",
593
- stderr: message
609
+ stderr: message,
610
+ failureReason: catchFailureReason
594
611
  };
595
612
  }
596
613
  }
@@ -620,7 +637,8 @@ var init_spawn_agent = __esm({
620
637
  timeoutMs: z2.number().optional().describe("Timeout in milliseconds for agent execution. Default: no timeout."),
621
638
  taskInstructionPath: z2.string().optional().describe(
622
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."
623
- )
640
+ ),
641
+ label: z2.string().optional().describe("Human-readable label for identifying this agent in parallel results")
624
642
  });
625
643
  }
626
644
  });
@@ -723,13 +741,15 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
723
741
  const reason = `Max depth exceeded: ${envContext.depth} >= ${guard.getConfig().maxDepth}`;
724
742
  logger.warn(`Batch spawn blocked by RecursionGuard: ${reason}`);
725
743
  return {
726
- results: agents.map((_, index) => ({
744
+ results: agents.map((agent, index) => ({
727
745
  index,
728
746
  sessionId: "",
729
747
  exitCode: 1,
730
748
  stdout: "",
731
749
  stderr: `Batch spawn blocked: ${reason}`,
732
- error: reason
750
+ error: reason,
751
+ failureReason: "recursion_blocked",
752
+ ...agent.label ? { label: agent.label } : {}
733
753
  })),
734
754
  totalCount: agents.length,
735
755
  successCount: 0,
@@ -742,13 +762,15 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
742
762
  const reason = `Batch would exceed max calls per session: ${currentCount} + ${agents.length} > ${maxCalls}`;
743
763
  logger.warn(`Batch spawn blocked by RecursionGuard: ${reason}`);
744
764
  return {
745
- results: agents.map((_, index) => ({
765
+ results: agents.map((agent, index) => ({
746
766
  index,
747
767
  sessionId: "",
748
768
  exitCode: 1,
749
769
  stdout: "",
750
770
  stderr: `Batch spawn blocked: ${reason}`,
751
- error: reason
771
+ error: reason,
772
+ failureReason: "recursion_blocked",
773
+ ...agent.label ? { label: agent.label } : {}
752
774
  })),
753
775
  totalCount: agents.length,
754
776
  successCount: 0,
@@ -790,7 +812,9 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
790
812
  stdout: r.stdout,
791
813
  stderr: r.stderr,
792
814
  ...r.nativeSessionId ? { nativeSessionId: r.nativeSessionId } : {},
793
- ...r.metadata ? { metadata: r.metadata } : {}
815
+ ...r.metadata ? { metadata: r.metadata } : {},
816
+ ...r.failureReason ? { failureReason: r.failureReason } : {},
817
+ ...agents[index]?.label ? { label: agents[index].label } : {}
794
818
  };
795
819
  if (r.exitCode !== 0) {
796
820
  base.originalInput = agents[index];
@@ -805,7 +829,9 @@ async function executeSpawnAgentsParallel(agents, registry2, sessionManager2, gu
805
829
  stdout: "",
806
830
  stderr: errorMessage,
807
831
  error: errorMessage,
808
- originalInput: agents[index]
832
+ originalInput: agents[index],
833
+ failureReason: "unknown",
834
+ ...agents[index]?.label ? { label: agents[index].label } : {}
809
835
  };
810
836
  });
811
837
  const successCount = results.filter((r) => r.exitCode === 0).length;
@@ -979,6 +1005,70 @@ var init_backend_selector = __esm({
979
1005
  }
980
1006
  });
981
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
+
982
1072
  // src/mcp-server/server.ts
983
1073
  var server_exports = {};
984
1074
  __export(server_exports, {
@@ -1002,6 +1092,7 @@ var init_server = __esm({
1002
1092
  init_list_available_backends();
1003
1093
  init_backend_selector();
1004
1094
  init_logger();
1095
+ init_response_formatter();
1005
1096
  spawnAgentsParallelInputShape = {
1006
1097
  agents: z5.array(spawnAgentInputSchema).min(1).max(10).describe(
1007
1098
  "Array of agent configurations to execute in parallel (1-10 agents)"
@@ -1018,7 +1109,7 @@ var init_server = __esm({
1018
1109
  this.backendSelector = new BackendSelector();
1019
1110
  this.server = new McpServer({
1020
1111
  name: "agentic-relay",
1021
- version: "0.8.0"
1112
+ version: "0.9.0"
1022
1113
  });
1023
1114
  this.registerTools(this.server);
1024
1115
  }
@@ -1067,17 +1158,7 @@ var init_server = __esm({
1067
1158
  this._childHttpUrl,
1068
1159
  onProgress
1069
1160
  );
1070
- const isError = result.exitCode !== 0;
1071
- let text = isError ? `Error (exit ${result.exitCode}): ${result.stderr || result.stdout}` : `Session: ${result.sessionId}
1072
-
1073
- ${result.stdout}`;
1074
- if (result.metadata) {
1075
- text += `
1076
-
1077
- <metadata>
1078
- ${JSON.stringify(result.metadata, null, 2)}
1079
- </metadata>`;
1080
- }
1161
+ const { text, isError } = formatSpawnAgentResponse(result);
1081
1162
  return {
1082
1163
  content: [{ type: "text", text }],
1083
1164
  isError
@@ -1122,15 +1203,7 @@ ${JSON.stringify(result.metadata, null, 2)}
1122
1203
  this._childHttpUrl,
1123
1204
  onProgress
1124
1205
  );
1125
- const isError = result.failureCount === result.totalCount;
1126
- let text = "";
1127
- if (result.hasConflicts) {
1128
- text += "\u26A0 FILE CONFLICTS DETECTED: Multiple agents modified the same files.\n";
1129
- text += `Conflicting files: ${result.conflicts.map((c) => c.path).join(", ")}
1130
-
1131
- `;
1132
- }
1133
- text += JSON.stringify(result, null, 2);
1206
+ const { text, isError } = formatParallelResponse(result);
1134
1207
  return {
1135
1208
  content: [{ type: "text", text }],
1136
1209
  isError
@@ -1151,11 +1224,24 @@ ${JSON.stringify(result.metadata, null, 2)}
1151
1224
  failedResults: z5.array(z5.object({
1152
1225
  index: z5.number(),
1153
1226
  originalInput: spawnAgentInputSchema
1154
- })).min(1).describe("Array of failed results with their original input configurations")
1227
+ })).min(1).describe("Array of failed results with their original input configurations"),
1228
+ overrides: z5.object({
1229
+ maxTurns: z5.number().optional(),
1230
+ timeoutMs: z5.number().optional(),
1231
+ preferredBackend: z5.enum(["claude", "codex", "gemini"]).optional()
1232
+ }).optional().describe("Parameter overrides applied to all retried agents")
1155
1233
  },
1156
1234
  async (params) => {
1157
1235
  try {
1158
- const agents = params.failedResults.map((r) => r.originalInput);
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
+ });
1159
1245
  const result = await executeSpawnAgentsParallel(
1160
1246
  agents,
1161
1247
  this.registry,
@@ -1166,8 +1252,7 @@ ${JSON.stringify(result.metadata, null, 2)}
1166
1252
  this.backendSelector,
1167
1253
  this._childHttpUrl
1168
1254
  );
1169
- const isError = result.failureCount === result.totalCount;
1170
- const text = JSON.stringify(result, null, 2);
1255
+ const { text, isError } = formatParallelResponse(result);
1171
1256
  return {
1172
1257
  content: [{ type: "text", text }],
1173
1258
  isError
@@ -1331,7 +1416,7 @@ ${JSON.stringify(result.metadata, null, 2)}
1331
1416
  });
1332
1417
  const server = new McpServer({
1333
1418
  name: "agentic-relay",
1334
- version: "0.8.0"
1419
+ version: "0.9.0"
1335
1420
  });
1336
1421
  this.registerTools(server);
1337
1422
  transport.onclose = () => {
@@ -1770,7 +1855,16 @@ var ClaudeAdapter = class extends BaseAdapter {
1770
1855
  let sessionId = "";
1771
1856
  let isError = false;
1772
1857
  let errorMessages = [];
1858
+ let totalInputTokens = 0;
1859
+ let totalOutputTokens = 0;
1773
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
+ }
1774
1868
  if (message.type === "result") {
1775
1869
  sessionId = message.session_id;
1776
1870
  if (message.subtype === "success") {
@@ -1782,11 +1876,13 @@ var ClaudeAdapter = class extends BaseAdapter {
1782
1876
  }
1783
1877
  }
1784
1878
  logger.debug(`Claude SDK session: ${sessionId}`);
1879
+ const hasTokenUsage = totalInputTokens > 0 || totalOutputTokens > 0;
1785
1880
  return {
1786
1881
  exitCode: isError ? 1 : 0,
1787
1882
  stdout: resultText,
1788
1883
  stderr: errorMessages.join("\n"),
1789
- ...sessionId ? { nativeSessionId: sessionId } : {}
1884
+ ...sessionId ? { nativeSessionId: sessionId } : {},
1885
+ ...hasTokenUsage ? { tokenUsage: { inputTokens: totalInputTokens, outputTokens: totalOutputTokens } } : {}
1790
1886
  };
1791
1887
  } catch (error) {
1792
1888
  if (abortController.signal.aborted) {
@@ -4266,7 +4362,7 @@ function createVersionCommand(registry2) {
4266
4362
  description: "Show relay and backend versions"
4267
4363
  },
4268
4364
  async run() {
4269
- const relayVersion = "0.8.0";
4365
+ const relayVersion = "0.9.0";
4270
4366
  console.log(`agentic-relay v${relayVersion}`);
4271
4367
  console.log("");
4272
4368
  console.log("Backends:");
@@ -4616,7 +4712,7 @@ void configManager.getConfig().then((config) => {
4616
4712
  var main = defineCommand10({
4617
4713
  meta: {
4618
4714
  name: "relay",
4619
- version: "0.8.0",
4715
+ version: "0.9.0",
4620
4716
  description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
4621
4717
  },
4622
4718
  subCommands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rk0429/agentic-relay",
3
- "version": "0.8.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",