@rk0429/agentic-relay 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/relay.mjs +182 -37
  2. package/package.json +1 -1
package/dist/relay.mjs CHANGED
@@ -46,6 +46,129 @@ var init_logger = __esm({
46
46
  }
47
47
  });
48
48
 
49
+ // src/mcp-server/deferred-cleanup-task-store.ts
50
+ import { isTerminal } from "@modelcontextprotocol/sdk/experimental/tasks/interfaces.js";
51
+ import { randomBytes } from "crypto";
52
+ var DeferredCleanupTaskStore;
53
+ var init_deferred_cleanup_task_store = __esm({
54
+ "src/mcp-server/deferred-cleanup-task-store.ts"() {
55
+ "use strict";
56
+ DeferredCleanupTaskStore = class {
57
+ tasks = /* @__PURE__ */ new Map();
58
+ cleanupTimers = /* @__PURE__ */ new Map();
59
+ generateTaskId() {
60
+ return randomBytes(16).toString("hex");
61
+ }
62
+ async createTask(taskParams, requestId, request, _sessionId) {
63
+ const taskId = this.generateTaskId();
64
+ if (this.tasks.has(taskId)) {
65
+ throw new Error(`Task with ID ${taskId} already exists`);
66
+ }
67
+ const actualTtl = taskParams.ttl ?? null;
68
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
69
+ const task = {
70
+ taskId,
71
+ status: "working",
72
+ ttl: actualTtl,
73
+ createdAt,
74
+ lastUpdatedAt: createdAt,
75
+ pollInterval: taskParams.pollInterval ?? 1e3
76
+ };
77
+ this.tasks.set(taskId, { task, request, requestId });
78
+ return { ...task };
79
+ }
80
+ async getTask(taskId, _sessionId) {
81
+ const stored = this.tasks.get(taskId);
82
+ return stored ? { ...stored.task } : null;
83
+ }
84
+ async storeTaskResult(taskId, status, result, _sessionId) {
85
+ const stored = this.tasks.get(taskId);
86
+ if (!stored) {
87
+ throw new Error(`Task with ID ${taskId} not found`);
88
+ }
89
+ if (isTerminal(stored.task.status)) {
90
+ throw new Error(
91
+ `Cannot store result for task ${taskId} in terminal status '${stored.task.status}'. Task results can only be stored once.`
92
+ );
93
+ }
94
+ stored.result = result;
95
+ stored.task.status = status;
96
+ stored.task.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
97
+ this.scheduleCleanup(taskId, stored.task.ttl);
98
+ }
99
+ async getTaskResult(taskId, _sessionId) {
100
+ const stored = this.tasks.get(taskId);
101
+ if (!stored) {
102
+ throw new Error(`Task with ID ${taskId} not found`);
103
+ }
104
+ if (!stored.result) {
105
+ throw new Error(`Task ${taskId} has no result stored`);
106
+ }
107
+ return stored.result;
108
+ }
109
+ async updateTaskStatus(taskId, status, statusMessage, _sessionId) {
110
+ const stored = this.tasks.get(taskId);
111
+ if (!stored) {
112
+ throw new Error(`Task with ID ${taskId} not found`);
113
+ }
114
+ if (isTerminal(stored.task.status)) {
115
+ throw new Error(
116
+ `Cannot update task ${taskId} from terminal status '${stored.task.status}'.`
117
+ );
118
+ }
119
+ stored.task.status = status;
120
+ if (statusMessage) {
121
+ stored.task.statusMessage = statusMessage;
122
+ }
123
+ stored.task.lastUpdatedAt = (/* @__PURE__ */ new Date()).toISOString();
124
+ if (isTerminal(status)) {
125
+ this.scheduleCleanup(taskId, stored.task.ttl);
126
+ }
127
+ }
128
+ async listTasks(cursor, _sessionId) {
129
+ const PAGE_SIZE = 10;
130
+ const allTaskIds = Array.from(this.tasks.keys());
131
+ let startIndex = 0;
132
+ if (cursor) {
133
+ const cursorIndex = allTaskIds.indexOf(cursor);
134
+ if (cursorIndex >= 0) {
135
+ startIndex = cursorIndex + 1;
136
+ } else {
137
+ throw new Error(`Invalid cursor: ${cursor}`);
138
+ }
139
+ }
140
+ const pageTaskIds = allTaskIds.slice(startIndex, startIndex + PAGE_SIZE);
141
+ const tasks = pageTaskIds.map((id) => {
142
+ const stored = this.tasks.get(id);
143
+ return { ...stored.task };
144
+ });
145
+ const nextCursor = startIndex + PAGE_SIZE < allTaskIds.length ? pageTaskIds[pageTaskIds.length - 1] : void 0;
146
+ return { tasks, nextCursor };
147
+ }
148
+ /** Cleanup all timers (for testing or graceful shutdown) */
149
+ cleanup() {
150
+ for (const timer of this.cleanupTimers.values()) {
151
+ clearTimeout(timer);
152
+ }
153
+ this.cleanupTimers.clear();
154
+ this.tasks.clear();
155
+ }
156
+ scheduleCleanup(taskId, ttl) {
157
+ if (!ttl) return;
158
+ const existingTimer = this.cleanupTimers.get(taskId);
159
+ if (existingTimer) {
160
+ clearTimeout(existingTimer);
161
+ }
162
+ const timer = setTimeout(() => {
163
+ this.tasks.delete(taskId);
164
+ this.cleanupTimers.delete(taskId);
165
+ }, ttl);
166
+ this.cleanupTimers.set(taskId, timer);
167
+ }
168
+ };
169
+ }
170
+ });
171
+
49
172
  // src/mcp-server/recursion-guard.ts
50
173
  import { createHash } from "crypto";
51
174
  var RecursionGuard;
@@ -658,27 +781,45 @@ var init_spawn_agent = __esm({
658
781
  init_recursion_guard();
659
782
  init_logger();
660
783
  spawnAgentInputSchema = z2.object({
661
- backend: z2.enum(["claude", "codex", "gemini"]),
662
- prompt: z2.string(),
663
- agent: z2.string().optional(),
664
- systemPrompt: z2.string().optional(),
665
- resumeSessionId: z2.string().optional(),
666
- model: z2.string().optional(),
667
- maxTurns: z2.number().optional(),
784
+ backend: z2.enum(["claude", "codex", "gemini"]).describe(
785
+ "Required fallback backend. Overridden by automatic selection when BackendSelector is active (priority: preferredBackend > agentType mapping > taskType mapping > default)."
786
+ ),
787
+ prompt: z2.string().describe(
788
+ "The task instruction for the sub-agent. Be specific and include all necessary context for autonomous execution."
789
+ ),
790
+ agent: z2.string().optional().describe(
791
+ "Agent type identifier (e.g., 'coder', 'researcher', 'documenter', 'software-engineer'). Affects backend auto-selection: coder/researcher/software-engineer/devops-engineer\u2192codex, documenter\u2192claude. Also used to resolve agentDefinition files from .agents/agents/<agent>.md."
792
+ ),
793
+ systemPrompt: z2.string().optional().describe(
794
+ "Custom system prompt prepended to the agent's context. Use agentDefinition instead when a .md definition file exists."
795
+ ),
796
+ resumeSessionId: z2.string().optional().describe(
797
+ "Relay session ID to resume. Enables multi-turn continuation of a previous agent execution."
798
+ ),
799
+ model: z2.string().optional().describe(
800
+ "Model to use (e.g., 'opus', 'sonnet', 'o3'). Passed to the backend CLI. Model availability varies by backend."
801
+ ),
802
+ maxTurns: z2.number().optional().describe(
803
+ "Maximum agentic turns (tool-use cycles). Limits execution depth to prevent runaway agents."
804
+ ),
668
805
  skillContext: z2.object({
669
806
  skillPath: z2.string().describe("Path to the skill directory (e.g., '.agents/skills/software-engineer/')"),
670
- subskill: z2.string().optional().describe("Specific subskill to activate")
671
- }).optional().describe("Skill context to inject into the subagent's system prompt"),
807
+ subskill: z2.string().optional().describe("Specific subskill to activate within the skill")
808
+ }).optional().describe("Skill context to inject into the sub-agent's system prompt. Loads SKILL.md/SUBSKILL.md content."),
672
809
  agentDefinition: z2.object({
673
- definitionPath: z2.string().describe("Path to the agent definition file (e.g., '.claude/agents/software-engineer.md')")
674
- }).optional().describe("Agent definition file to inject into the sub-agent's system prompt"),
675
- preferredBackend: z2.enum(["claude", "codex", "gemini"]).optional().describe("Preferred backend override. Takes priority over automatic selection based on agent/task type."),
676
- taskType: z2.enum(["code-writing", "code-review", "document-writing", "document-review", "research", "mixed"]).optional().describe("Task type hint for automatic backend selection when preferredBackend is not specified. code-writing\u2192codex, code-review\u2192claude, document-writing\u2192claude, document-review\u2192codex, research\u2192codex, mixed\u2192claude."),
810
+ definitionPath: z2.string().describe("Path to the agent definition .md file (e.g., '.agents/agents/coder.md')")
811
+ }).optional().describe("Agent definition file to inject into the sub-agent's system prompt as <agent-definition> context."),
812
+ preferredBackend: z2.enum(["claude", "codex", "gemini"]).optional().describe(
813
+ "Preferred backend override. Takes highest priority in backend selection. Use when the automatic agentType/taskType mapping does not match your needs."
814
+ ),
815
+ taskType: z2.enum(["code-writing", "code-review", "document-writing", "document-review", "research", "mixed"]).optional().describe(
816
+ "Task type hint for automatic backend selection (priority 3, after preferredBackend and agentType). Mapping: code-writing\u2192codex, code-review\u2192claude, document-writing\u2192claude, document-review\u2192codex, research\u2192codex, mixed\u2192claude."
817
+ ),
677
818
  timeoutMs: z2.number().optional().describe("Timeout in milliseconds for agent execution. Default: no timeout."),
678
819
  taskInstructionPath: z2.string().optional().describe(
679
820
  "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."
680
821
  ),
681
- label: z2.string().optional().describe("Human-readable label for identifying this agent in parallel results")
822
+ label: z2.string().optional().describe("Human-readable label for identifying this agent in parallel results and logs.")
682
823
  });
683
824
  }
684
825
  });
@@ -994,7 +1135,10 @@ var init_backend_selector = __esm({
994
1135
  "skill-developer": "codex",
995
1136
  "security-auditor": "codex",
996
1137
  "analytics-engineer": "codex",
997
- "payments-billing": "codex"
1138
+ "payments-billing": "codex",
1139
+ "coder": "codex",
1140
+ "researcher": "codex",
1141
+ "documenter": "claude"
998
1142
  };
999
1143
  TASK_TYPE_TO_BACKEND_MAP = {
1000
1144
  "code-writing": "codex",
@@ -1137,13 +1281,12 @@ __export(server_exports, {
1137
1281
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1138
1282
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1139
1283
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1140
- import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
1141
1284
  import { InMemoryTaskMessageQueue } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
1142
1285
  import { createServer } from "http";
1143
1286
  import { randomUUID } from "crypto";
1144
1287
  import { z as z5 } from "zod";
1145
1288
  function createMcpServerOptions() {
1146
- const taskStore = new InMemoryTaskStore();
1289
+ const taskStore = new DeferredCleanupTaskStore();
1147
1290
  return {
1148
1291
  capabilities: { tasks: { requests: { tools: { call: {} } } } },
1149
1292
  taskStore,
@@ -1155,6 +1298,7 @@ var spawnAgentsParallelInputShape, MAX_CHILD_HTTP_SESSIONS, RelayMCPServer;
1155
1298
  var init_server = __esm({
1156
1299
  "src/mcp-server/server.ts"() {
1157
1300
  "use strict";
1301
+ init_deferred_cleanup_task_store();
1158
1302
  init_recursion_guard();
1159
1303
  init_spawn_agent();
1160
1304
  init_spawn_agents_parallel();
@@ -1180,7 +1324,7 @@ var init_server = __esm({
1180
1324
  this.guard = new RecursionGuard(guardConfig);
1181
1325
  this.backendSelector = new BackendSelector();
1182
1326
  this.server = new McpServer(
1183
- { name: "agentic-relay", version: "0.12.0" },
1327
+ { name: "agentic-relay", version: "0.12.2" },
1184
1328
  createMcpServerOptions()
1185
1329
  );
1186
1330
  this.registerTools(this.server);
@@ -1296,7 +1440,7 @@ var init_server = __esm({
1296
1440
  server.experimental.tasks.registerToolTask(
1297
1441
  "spawn_agent",
1298
1442
  {
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.",
1443
+ description: "Spawn a sub-agent on a backend CLI (Claude Code, Codex CLI, or Gemini CLI). The agent executes the given prompt in non-interactive mode and returns the result. Backend is auto-selected by priority: preferredBackend > agentType mapping (coder/researcher\u2192codex, documenter\u2192claude) > taskType mapping > default (claude). Use 'agentDefinition' to inject an agent .md file, 'skillContext' for a SKILL.md, or 'systemPrompt' for custom instructions.",
1300
1444
  inputSchema: spawnAgentInputSchema.shape,
1301
1445
  execution: { taskSupport: "optional" }
1302
1446
  },
@@ -1310,18 +1454,19 @@ var init_server = __esm({
1310
1454
  return { task };
1311
1455
  },
1312
1456
  getTask: async (_params, extra) => {
1313
- const task = await extra.taskStore.getTask(extra.taskId);
1314
- return { task: task ?? void 0 };
1457
+ return await extra.taskStore.getTask(extra.taskId);
1315
1458
  },
1316
1459
  getTaskResult: async (_params, extra) => {
1317
- return await extra.taskStore.getTaskResult(extra.taskId);
1460
+ return await extra.taskStore.getTaskResult(
1461
+ extra.taskId
1462
+ );
1318
1463
  }
1319
1464
  }
1320
1465
  );
1321
1466
  server.experimental.tasks.registerToolTask(
1322
1467
  "spawn_agents_parallel",
1323
1468
  {
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.",
1469
+ description: "Spawn multiple sub-agents in parallel across available backends. Use this when you have 2+ independent tasks that can execute concurrently. Each agent entry accepts the same parameters as spawn_agent. Results are returned as an array with per-agent status (success/failure). RecursionGuard batch pre-validation ensures the batch fits within call limits. Failed results include 'originalInput' for retry via retry_failed_agents.",
1325
1470
  inputSchema: spawnAgentsParallelInputShape,
1326
1471
  execution: { taskSupport: "optional" }
1327
1472
  },
@@ -1335,11 +1480,12 @@ var init_server = __esm({
1335
1480
  return { task };
1336
1481
  },
1337
1482
  getTask: async (_params, extra) => {
1338
- const task = await extra.taskStore.getTask(extra.taskId);
1339
- return { task: task ?? void 0 };
1483
+ return await extra.taskStore.getTask(extra.taskId);
1340
1484
  },
1341
1485
  getTaskResult: async (_params, extra) => {
1342
- return await extra.taskStore.getTaskResult(extra.taskId);
1486
+ return await extra.taskStore.getTaskResult(
1487
+ extra.taskId
1488
+ );
1343
1489
  }
1344
1490
  }
1345
1491
  );
@@ -1396,8 +1542,8 @@ var init_server = __esm({
1396
1542
  "list_sessions",
1397
1543
  "List relay sessions, optionally filtered by backend.",
1398
1544
  {
1399
- backend: z5.enum(["claude", "codex", "gemini"]).optional(),
1400
- limit: z5.number().optional()
1545
+ backend: z5.enum(["claude", "codex", "gemini"]).optional().describe("Filter sessions by backend type."),
1546
+ limit: z5.number().optional().describe("Maximum number of sessions to return. Default: 10.")
1401
1547
  },
1402
1548
  async (params) => {
1403
1549
  try {
@@ -1426,7 +1572,7 @@ var init_server = __esm({
1426
1572
  "get_context_status",
1427
1573
  "Get the context usage status of a relay session. Returns usage data from ContextMonitor when available, otherwise estimated values.",
1428
1574
  {
1429
- sessionId: z5.string()
1575
+ sessionId: z5.string().describe("Relay session ID to query context usage for.")
1430
1576
  },
1431
1577
  async (params) => {
1432
1578
  try {
@@ -1542,7 +1688,7 @@ var init_server = __esm({
1542
1688
  sessionIdGenerator: () => randomUUID()
1543
1689
  });
1544
1690
  const server = new McpServer(
1545
- { name: "agentic-relay", version: "0.12.0" },
1691
+ { name: "agentic-relay", version: "0.12.2" },
1546
1692
  createMcpServerOptions()
1547
1693
  );
1548
1694
  this.registerTools(server);
@@ -3414,7 +3560,8 @@ var HooksEngine = class _HooksEngine {
3414
3560
  const message = error instanceof Error ? error.message : String(error);
3415
3561
  if (strategy === "abort") {
3416
3562
  throw new Error(
3417
- `Hook "${def.command}" failed (abort): ${message}`
3563
+ `Hook "${def.command}" failed (abort): ${message}`,
3564
+ { cause: error }
3418
3565
  );
3419
3566
  }
3420
3567
  if (strategy === "warn") {
@@ -3461,7 +3608,8 @@ var HooksEngine = class _HooksEngine {
3461
3608
  const message = error instanceof Error ? error.message : String(error);
3462
3609
  if (strategy === "abort") {
3463
3610
  throw new Error(
3464
- `Hook "${def.command}" failed (abort): ${message}`
3611
+ `Hook "${def.command}" failed (abort): ${message}`,
3612
+ { cause: error }
3465
3613
  );
3466
3614
  }
3467
3615
  if (strategy === "warn") {
@@ -3853,7 +4001,6 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
3853
4001
  try {
3854
4002
  if (flags.prompt) {
3855
4003
  logger.debug(`Executing prompt on ${backendId}`);
3856
- let nativeSessionId;
3857
4004
  if (adapter.executeStreaming) {
3858
4005
  for await (const event of adapter.executeStreaming(flags)) {
3859
4006
  switch (event.type) {
@@ -3894,7 +4041,6 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
3894
4041
  }
3895
4042
  case "done":
3896
4043
  process.exitCode = event.result.exitCode;
3897
- nativeSessionId = event.nativeSessionId;
3898
4044
  if (event.nativeSessionId && sessionManager2 && relaySessionId) {
3899
4045
  try {
3900
4046
  await sessionManager2.update(relaySessionId, {
@@ -3911,7 +4057,6 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
3911
4057
  if (result.stdout) process.stdout.write(result.stdout);
3912
4058
  if (result.stderr) process.stderr.write(result.stderr);
3913
4059
  process.exitCode = result.exitCode;
3914
- nativeSessionId = result.nativeSessionId;
3915
4060
  if (result.nativeSessionId && sessionManager2 && relaySessionId) {
3916
4061
  try {
3917
4062
  await sessionManager2.update(relaySessionId, {
@@ -4495,7 +4640,7 @@ function createVersionCommand(registry2) {
4495
4640
  description: "Show relay and backend versions"
4496
4641
  },
4497
4642
  async run() {
4498
- const relayVersion = "0.12.0";
4643
+ const relayVersion = "0.12.2";
4499
4644
  console.log(`agentic-relay v${relayVersion}`);
4500
4645
  console.log("");
4501
4646
  console.log("Backends:");
@@ -4845,7 +4990,7 @@ void configManager.getConfig().then((config) => {
4845
4990
  var main = defineCommand10({
4846
4991
  meta: {
4847
4992
  name: "relay",
4848
- version: "0.12.0",
4993
+ version: "0.12.2",
4849
4994
  description: "Unified CLI proxy for Claude Code, Codex CLI, and Gemini CLI"
4850
4995
  },
4851
4996
  subCommands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rk0429/agentic-relay",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
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",