@rk0429/agentic-relay 0.4.0 → 0.5.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 +461 -63
  2. package/package.json +1 -1
package/dist/relay.mjs CHANGED
@@ -190,7 +190,7 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
190
190
  sessionId: "",
191
191
  exitCode: 1,
192
192
  stdout: "",
193
- stderr: `Backend "${input.backend}" is not installed`
193
+ stderr: `Backend "${input.backend}" is not available. Use list_available_backends to see available options.`
194
194
  };
195
195
  }
196
196
  const session = await sessionManager2.create({
@@ -219,29 +219,40 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
219
219
  }
220
220
  }
221
221
  try {
222
- const result = await adapter.execute({
223
- prompt: input.prompt,
224
- agent: input.agent,
225
- model: input.model,
226
- maxTurns: input.maxTurns,
227
- resume: input.resumeSessionId,
228
- mcpContext: {
229
- parentSessionId: session.relaySessionId,
230
- depth: envContext.depth + 1,
231
- maxDepth: guard.getConfig().maxDepth,
232
- traceId: envContext.traceId
222
+ let result;
223
+ if (input.resumeSessionId) {
224
+ if (!adapter.continueSession) {
225
+ return {
226
+ sessionId: session.relaySessionId,
227
+ exitCode: 1,
228
+ stdout: "",
229
+ stderr: `Backend "${input.backend}" does not support session continuation (continueSession).`
230
+ };
233
231
  }
234
- });
232
+ result = await adapter.continueSession(input.resumeSessionId, input.prompt);
233
+ } else {
234
+ result = await adapter.execute({
235
+ prompt: input.prompt,
236
+ agent: input.agent,
237
+ systemPrompt: input.systemPrompt,
238
+ model: input.model,
239
+ maxTurns: input.maxTurns,
240
+ mcpContext: {
241
+ parentSessionId: session.relaySessionId,
242
+ depth: envContext.depth + 1,
243
+ maxDepth: guard.getConfig().maxDepth,
244
+ traceId: envContext.traceId
245
+ }
246
+ });
247
+ }
235
248
  if (contextMonitor2) {
236
249
  const estimatedTokens = Math.ceil(
237
250
  (result.stdout.length + result.stderr.length) / 4
238
251
  );
239
- const maxTokens = input.backend === "gemini" ? 128e3 : 2e5;
240
252
  contextMonitor2.updateUsage(
241
253
  session.relaySessionId,
242
254
  input.backend,
243
- estimatedTokens,
244
- maxTokens
255
+ estimatedTokens
245
256
  );
246
257
  }
247
258
  guard.recordSpawn(context);
@@ -270,7 +281,8 @@ async function executeSpawnAgent(input, registry2, sessionManager2, guard, hooks
270
281
  sessionId: session.relaySessionId,
271
282
  exitCode: result.exitCode,
272
283
  stdout: result.stdout,
273
- stderr: result.stderr
284
+ stderr: result.stderr,
285
+ nativeSessionId: result.nativeSessionId
274
286
  };
275
287
  } catch (error) {
276
288
  await sessionManager2.update(session.relaySessionId, { status: "error" });
@@ -293,6 +305,7 @@ var init_spawn_agent = __esm({
293
305
  backend: z2.enum(["claude", "codex", "gemini"]),
294
306
  prompt: z2.string(),
295
307
  agent: z2.string().optional(),
308
+ systemPrompt: z2.string().optional(),
296
309
  resumeSessionId: z2.string().optional(),
297
310
  model: z2.string().optional(),
298
311
  maxTurns: z2.number().optional()
@@ -339,8 +352,14 @@ async function executeGetContextStatus(input, sessionManager2, contextMonitor2)
339
352
  if (usage) {
340
353
  return {
341
354
  sessionId: input.sessionId,
355
+ backendId: usage.backendId,
342
356
  usagePercent: usage.usagePercent,
343
- isEstimated: usage.isEstimated
357
+ isEstimated: usage.isEstimated,
358
+ contextWindow: usage.contextWindow,
359
+ compactThreshold: usage.compactThreshold,
360
+ estimatedTokens: usage.estimatedTokens,
361
+ remainingBeforeCompact: usage.remainingBeforeCompact,
362
+ notifyThreshold: usage.notifyThreshold
344
363
  };
345
364
  }
346
365
  }
@@ -360,6 +379,24 @@ var init_get_context_status = __esm({
360
379
  }
361
380
  });
362
381
 
382
+ // src/mcp-server/tools/list-available-backends.ts
383
+ async function executeListAvailableBackends(registry2) {
384
+ const backends = [];
385
+ for (const adapter of registry2.list()) {
386
+ const health = await adapter.checkHealth();
387
+ backends.push({
388
+ id: adapter.id,
389
+ ...health
390
+ });
391
+ }
392
+ return backends;
393
+ }
394
+ var init_list_available_backends = __esm({
395
+ "src/mcp-server/tools/list-available-backends.ts"() {
396
+ "use strict";
397
+ }
398
+ });
399
+
363
400
  // src/mcp-server/server.ts
364
401
  var server_exports = {};
365
402
  __export(server_exports, {
@@ -379,6 +416,7 @@ var init_server = __esm({
379
416
  init_spawn_agent();
380
417
  init_list_sessions();
381
418
  init_get_context_status();
419
+ init_list_available_backends();
382
420
  init_logger();
383
421
  RelayMCPServer = class {
384
422
  constructor(registry2, sessionManager2, guardConfig, hooksEngine2, contextMonitor2) {
@@ -398,11 +436,14 @@ var init_server = __esm({
398
436
  registerTools() {
399
437
  this.server.tool(
400
438
  "spawn_agent",
401
- "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.",
439
+ "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), or 'systemPrompt' for custom role instructions (all backends).",
402
440
  {
403
441
  backend: z5.enum(["claude", "codex", "gemini"]),
404
442
  prompt: z5.string(),
405
- agent: z5.string().optional(),
443
+ agent: z5.string().optional().describe("Named agent configuration (Claude only)"),
444
+ systemPrompt: z5.string().optional().describe(
445
+ "System prompt / role instructions for the sub-agent (all backends)"
446
+ ),
406
447
  resumeSessionId: z5.string().optional(),
407
448
  model: z5.string().optional(),
408
449
  maxTurns: z5.number().optional()
@@ -494,6 +535,30 @@ ${result.stdout}`;
494
535
  }
495
536
  }
496
537
  );
538
+ this.server.tool(
539
+ "list_available_backends",
540
+ "List all registered backends with their health status. Use this before spawn_agent to check which backends are available.",
541
+ {},
542
+ async () => {
543
+ try {
544
+ const result = await executeListAvailableBackends(this.registry);
545
+ return {
546
+ content: [
547
+ {
548
+ type: "text",
549
+ text: JSON.stringify(result, null, 2)
550
+ }
551
+ ]
552
+ };
553
+ } catch (error) {
554
+ const message = error instanceof Error ? error.message : String(error);
555
+ return {
556
+ content: [{ type: "text", text: `Error: ${message}` }],
557
+ isError: true
558
+ };
559
+ }
560
+ }
561
+ );
497
562
  }
498
563
  async start(options) {
499
564
  const transportType = options?.transport ?? "stdio";
@@ -698,6 +763,42 @@ var BaseAdapter = class {
698
763
  }
699
764
  return result.stdout.trim();
700
765
  }
766
+ async continueSession(_nativeSessionId, _prompt) {
767
+ return {
768
+ exitCode: 1,
769
+ stdout: "",
770
+ stderr: `continueSession not supported for ${this.id}`
771
+ };
772
+ }
773
+ async checkHealth() {
774
+ const HEALTH_TIMEOUT = 5e3;
775
+ const installed = await Promise.race([
776
+ this.isInstalled(),
777
+ new Promise(
778
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
779
+ )
780
+ ]).catch(() => false);
781
+ if (!installed) {
782
+ return {
783
+ installed: false,
784
+ authenticated: false,
785
+ healthy: false,
786
+ message: `${this.id} is not installed`
787
+ };
788
+ }
789
+ const version = await Promise.race([
790
+ this.getVersion(),
791
+ new Promise(
792
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
793
+ )
794
+ ]).catch(() => void 0);
795
+ return {
796
+ installed: true,
797
+ authenticated: true,
798
+ healthy: true,
799
+ version
800
+ };
801
+ }
701
802
  async getMCPConfig() {
702
803
  logger.warn(`getMCPConfig not implemented for ${this.id}`);
703
804
  return [];
@@ -799,6 +900,48 @@ var ClaudeAdapter = class extends BaseAdapter {
799
900
  getConfigPath() {
800
901
  return join(homedir(), ".claude.json");
801
902
  }
903
+ async checkHealth() {
904
+ const HEALTH_TIMEOUT = 5e3;
905
+ const installed = await Promise.race([
906
+ this.isInstalled(),
907
+ new Promise(
908
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
909
+ )
910
+ ]).catch(() => false);
911
+ if (!installed) {
912
+ return {
913
+ installed: false,
914
+ authenticated: false,
915
+ healthy: false,
916
+ message: "claude is not installed"
917
+ };
918
+ }
919
+ const version = await Promise.race([
920
+ this.getVersion(),
921
+ new Promise(
922
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
923
+ )
924
+ ]).catch(() => void 0);
925
+ let authenticated = true;
926
+ try {
927
+ const result = await Promise.race([
928
+ this.processManager.execute(this.command, ["auth", "status"]),
929
+ new Promise(
930
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
931
+ )
932
+ ]);
933
+ authenticated = result.exitCode === 0;
934
+ } catch {
935
+ authenticated = true;
936
+ }
937
+ return {
938
+ installed: true,
939
+ authenticated,
940
+ healthy: authenticated,
941
+ version,
942
+ ...!authenticated ? { message: "claude authentication not configured" } : {}
943
+ };
944
+ }
802
945
  mapFlags(flags) {
803
946
  return {
804
947
  args: mapCommonToNative("claude", flags)
@@ -825,7 +968,8 @@ var ClaudeAdapter = class extends BaseAdapter {
825
968
  env,
826
969
  cwd: process.cwd(),
827
970
  ...flags.model ? { model: flags.model } : {},
828
- ...flags.maxTurns ? { maxTurns: flags.maxTurns } : {}
971
+ ...flags.maxTurns ? { maxTurns: flags.maxTurns } : {},
972
+ ...flags.systemPrompt ? { systemPrompt: flags.systemPrompt } : {}
829
973
  };
830
974
  if (permissionMode === "bypassPermissions") {
831
975
  options.permissionMode = "bypassPermissions";
@@ -877,7 +1021,8 @@ var ClaudeAdapter = class extends BaseAdapter {
877
1021
  env,
878
1022
  cwd: process.cwd(),
879
1023
  ...flags.model ? { model: flags.model } : {},
880
- ...flags.maxTurns ? { maxTurns: flags.maxTurns } : {}
1024
+ ...flags.maxTurns ? { maxTurns: flags.maxTurns } : {},
1025
+ ...flags.systemPrompt ? { systemPrompt: flags.systemPrompt } : {}
881
1026
  };
882
1027
  if (permissionMode === "bypassPermissions") {
883
1028
  options.permissionMode = "bypassPermissions";
@@ -942,6 +1087,44 @@ var ClaudeAdapter = class extends BaseAdapter {
942
1087
  };
943
1088
  }
944
1089
  }
1090
+ async continueSession(nativeSessionId, prompt) {
1091
+ try {
1092
+ const { query } = await loadClaudeSDK();
1093
+ const permissionMode = this.getPermissionMode();
1094
+ const options = {
1095
+ resume: nativeSessionId,
1096
+ maxTurns: 1,
1097
+ cwd: process.cwd()
1098
+ };
1099
+ if (permissionMode === "bypassPermissions") {
1100
+ options.permissionMode = "bypassPermissions";
1101
+ options.allowDangerouslySkipPermissions = true;
1102
+ }
1103
+ const q = query({
1104
+ prompt,
1105
+ options
1106
+ });
1107
+ let resultText = "";
1108
+ for await (const message of q) {
1109
+ if (message.type === "result") {
1110
+ if (message.subtype === "success") {
1111
+ resultText = message.result;
1112
+ }
1113
+ }
1114
+ }
1115
+ return {
1116
+ exitCode: 0,
1117
+ stdout: resultText,
1118
+ stderr: ""
1119
+ };
1120
+ } catch (error) {
1121
+ return {
1122
+ exitCode: 1,
1123
+ stdout: "",
1124
+ stderr: error instanceof Error ? error.message : String(error)
1125
+ };
1126
+ }
1127
+ }
945
1128
  async resumeSession(sessionId, flags) {
946
1129
  await this.processManager.spawnInteractive(
947
1130
  this.command,
@@ -1157,6 +1340,48 @@ var CodexAdapter = class extends BaseAdapter {
1157
1340
  getConfigPath() {
1158
1341
  return join2(homedir2(), ".codex", "config.toml");
1159
1342
  }
1343
+ async checkHealth() {
1344
+ const HEALTH_TIMEOUT = 5e3;
1345
+ const installed = await Promise.race([
1346
+ this.isInstalled(),
1347
+ new Promise(
1348
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1349
+ )
1350
+ ]).catch(() => false);
1351
+ if (!installed) {
1352
+ return {
1353
+ installed: false,
1354
+ authenticated: false,
1355
+ healthy: false,
1356
+ message: "codex is not installed"
1357
+ };
1358
+ }
1359
+ const version = await Promise.race([
1360
+ this.getVersion(),
1361
+ new Promise(
1362
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1363
+ )
1364
+ ]).catch(() => void 0);
1365
+ let authenticated = true;
1366
+ try {
1367
+ const result = await Promise.race([
1368
+ this.processManager.execute(this.command, ["login", "status"]),
1369
+ new Promise(
1370
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1371
+ )
1372
+ ]);
1373
+ authenticated = result.exitCode === 0;
1374
+ } catch {
1375
+ authenticated = true;
1376
+ }
1377
+ return {
1378
+ installed: true,
1379
+ authenticated,
1380
+ healthy: authenticated,
1381
+ version,
1382
+ ...!authenticated ? { message: "codex authentication not configured" } : {}
1383
+ };
1384
+ }
1160
1385
  mapFlags(flags) {
1161
1386
  const args = mapCommonToNative("codex", flags);
1162
1387
  if (flags.outputFormat === "json") {
@@ -1174,15 +1399,39 @@ var CodexAdapter = class extends BaseAdapter {
1174
1399
  }
1175
1400
  await this.processManager.spawnInteractive(this.command, args);
1176
1401
  }
1402
+ /**
1403
+ * Resolve the effective system prompt from flags.
1404
+ * Priority: systemPrompt > agent fallback > none
1405
+ */
1406
+ resolveSystemPrompt(flags) {
1407
+ if (flags.systemPrompt) return flags.systemPrompt;
1408
+ if (flags.agent) {
1409
+ return `You are acting as the "${flags.agent}" agent. Follow the instructions and role defined for this agent.`;
1410
+ }
1411
+ return void 0;
1412
+ }
1413
+ /**
1414
+ * Build the effective prompt with system instructions prepended if needed.
1415
+ * Codex SDK does not support a native instructions/systemPrompt parameter,
1416
+ * so we inject role context via a prompt prefix.
1417
+ */
1418
+ buildEffectivePrompt(prompt, systemPrompt) {
1419
+ if (!systemPrompt) return prompt;
1420
+ return `[System Instructions]
1421
+ ${systemPrompt}
1422
+
1423
+ [User Request]
1424
+ ${prompt}`;
1425
+ }
1177
1426
  async execute(flags) {
1178
1427
  if (!flags.prompt) {
1179
1428
  throw new Error("execute requires a prompt (-p flag)");
1180
1429
  }
1181
- if (flags.agent) {
1182
- logger.warn(
1183
- `Codex CLI does not support --agent flag. Ignoring agent "${flags.agent}".`
1184
- );
1185
- }
1430
+ const systemPrompt = this.resolveSystemPrompt(flags);
1431
+ const effectivePrompt = this.buildEffectivePrompt(
1432
+ flags.prompt,
1433
+ systemPrompt
1434
+ );
1186
1435
  try {
1187
1436
  const { Codex } = await loadCodexSDK();
1188
1437
  const codexOptions = {};
@@ -1200,11 +1449,12 @@ var CodexAdapter = class extends BaseAdapter {
1200
1449
  workingDirectory: process.cwd(),
1201
1450
  approvalPolicy: "never"
1202
1451
  });
1203
- const result = await thread.run(flags.prompt);
1452
+ const result = await thread.run(effectivePrompt);
1204
1453
  return {
1205
1454
  exitCode: 0,
1206
1455
  stdout: result.finalResponse,
1207
- stderr: ""
1456
+ stderr: "",
1457
+ ...thread.id ? { nativeSessionId: thread.id } : {}
1208
1458
  };
1209
1459
  } catch (error) {
1210
1460
  return {
@@ -1218,11 +1468,11 @@ var CodexAdapter = class extends BaseAdapter {
1218
1468
  if (!flags.prompt) {
1219
1469
  throw new Error("executeStreaming requires a prompt (-p flag)");
1220
1470
  }
1221
- if (flags.agent) {
1222
- logger.warn(
1223
- `Codex CLI does not support --agent flag. Ignoring agent "${flags.agent}".`
1224
- );
1225
- }
1471
+ const systemPrompt = this.resolveSystemPrompt(flags);
1472
+ const effectivePrompt = this.buildEffectivePrompt(
1473
+ flags.prompt,
1474
+ systemPrompt
1475
+ );
1226
1476
  try {
1227
1477
  const { Codex } = await loadCodexSDK();
1228
1478
  const codexOptions = {};
@@ -1240,10 +1490,13 @@ var CodexAdapter = class extends BaseAdapter {
1240
1490
  workingDirectory: process.cwd(),
1241
1491
  approvalPolicy: "never"
1242
1492
  });
1243
- const streamedTurn = await thread.runStreamed(flags.prompt);
1493
+ const streamedTurn = await thread.runStreamed(effectivePrompt);
1244
1494
  const completedMessages = [];
1495
+ let threadId;
1245
1496
  for await (const event of streamedTurn.events) {
1246
- if (event.type === "item.started") {
1497
+ if (event.type === "thread.started") {
1498
+ threadId = event.thread_id;
1499
+ } else if (event.type === "item.started") {
1247
1500
  const item = event.item;
1248
1501
  if (item?.type === "agent_message" && item.text) {
1249
1502
  yield { type: "text", text: item.text };
@@ -1291,13 +1544,15 @@ var CodexAdapter = class extends BaseAdapter {
1291
1544
  const finalResponse = completedMessages.join("\n");
1292
1545
  yield {
1293
1546
  type: "done",
1294
- result: { exitCode: 0, stdout: finalResponse, stderr: "" }
1547
+ result: { exitCode: 0, stdout: finalResponse, stderr: "" },
1548
+ nativeSessionId: threadId ?? thread.id ?? void 0
1295
1549
  };
1296
1550
  } else if (event.type === "turn.failed") {
1297
1551
  const errorMessage = event.error?.message ?? "Turn failed";
1298
1552
  yield {
1299
1553
  type: "done",
1300
- result: { exitCode: 1, stdout: "", stderr: errorMessage }
1554
+ result: { exitCode: 1, stdout: "", stderr: errorMessage },
1555
+ nativeSessionId: threadId ?? thread.id ?? void 0
1301
1556
  };
1302
1557
  } else if (event.type === "error") {
1303
1558
  yield {
@@ -1315,6 +1570,28 @@ var CodexAdapter = class extends BaseAdapter {
1315
1570
  };
1316
1571
  }
1317
1572
  }
1573
+ async continueSession(nativeSessionId, prompt) {
1574
+ try {
1575
+ const { Codex } = await loadCodexSDK();
1576
+ const codex = new Codex();
1577
+ const thread = codex.resumeThread(nativeSessionId, {
1578
+ workingDirectory: process.cwd(),
1579
+ approvalPolicy: "never"
1580
+ });
1581
+ const result = await thread.run(prompt);
1582
+ return {
1583
+ exitCode: 0,
1584
+ stdout: result.finalResponse,
1585
+ stderr: ""
1586
+ };
1587
+ } catch (error) {
1588
+ return {
1589
+ exitCode: 1,
1590
+ stdout: "",
1591
+ stderr: error instanceof Error ? error.message : String(error)
1592
+ };
1593
+ }
1594
+ }
1318
1595
  async resumeSession(sessionId, flags) {
1319
1596
  const args = [];
1320
1597
  if (flags.model) {
@@ -1408,6 +1685,38 @@ var GeminiAdapter = class extends BaseAdapter {
1408
1685
  getConfigPath() {
1409
1686
  return join3(homedir3(), ".gemini", "settings.json");
1410
1687
  }
1688
+ async checkHealth() {
1689
+ const HEALTH_TIMEOUT = 5e3;
1690
+ const installed = await Promise.race([
1691
+ this.isInstalled(),
1692
+ new Promise(
1693
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1694
+ )
1695
+ ]).catch(() => false);
1696
+ if (!installed) {
1697
+ return {
1698
+ installed: false,
1699
+ authenticated: false,
1700
+ healthy: false,
1701
+ message: "gemini is not installed"
1702
+ };
1703
+ }
1704
+ const version = await Promise.race([
1705
+ this.getVersion(),
1706
+ new Promise(
1707
+ (_, reject) => setTimeout(() => reject(new Error("timeout")), HEALTH_TIMEOUT)
1708
+ )
1709
+ ]).catch(() => void 0);
1710
+ const hasApiKey = !!process.env["GEMINI_API_KEY"];
1711
+ const hasGoogleAdc = !!process.env["GOOGLE_APPLICATION_CREDENTIALS"] || !!process.env["CLOUDSDK_CONFIG"];
1712
+ const authenticated = hasApiKey || hasGoogleAdc || true;
1713
+ return {
1714
+ installed: true,
1715
+ authenticated,
1716
+ healthy: true,
1717
+ version
1718
+ };
1719
+ }
1411
1720
  mapFlags(flags) {
1412
1721
  const args = mapCommonToNative("gemini", flags);
1413
1722
  if (flags.outputFormat) {
@@ -1428,15 +1737,34 @@ var GeminiAdapter = class extends BaseAdapter {
1428
1737
  }
1429
1738
  await this.processManager.spawnInteractive(this.command, args);
1430
1739
  }
1740
+ /**
1741
+ * Resolve the effective prompt with system instructions prepended if needed.
1742
+ * Gemini CLI has no native system prompt flag, so we use a prompt prefix.
1743
+ * Priority: systemPrompt > agent fallback > none
1744
+ */
1745
+ buildEffectivePrompt(flags) {
1746
+ const prompt = flags.prompt;
1747
+ if (flags.systemPrompt) {
1748
+ return `[System Instructions]
1749
+ ${flags.systemPrompt}
1750
+
1751
+ [User Request]
1752
+ ${prompt}`;
1753
+ }
1754
+ if (flags.agent) {
1755
+ return `[System Instructions]
1756
+ You are acting as the "${flags.agent}" agent.
1757
+
1758
+ [User Request]
1759
+ ${prompt}`;
1760
+ }
1761
+ return prompt;
1762
+ }
1431
1763
  async execute(flags) {
1432
1764
  if (!flags.prompt) {
1433
1765
  throw new Error("execute requires a prompt (-p flag)");
1434
1766
  }
1435
- if (flags.agent) {
1436
- logger.warn(
1437
- `Gemini CLI does not support --agent flag. Ignoring agent "${flags.agent}".`
1438
- );
1439
- }
1767
+ const effectivePrompt = this.buildEffectivePrompt(flags);
1440
1768
  const args = [];
1441
1769
  if (flags.model) {
1442
1770
  args.push("--model", flags.model);
@@ -1447,7 +1775,7 @@ var GeminiAdapter = class extends BaseAdapter {
1447
1775
  if (flags.verbose) {
1448
1776
  args.push("--verbose");
1449
1777
  }
1450
- args.push("-p", flags.prompt);
1778
+ args.push("-p", effectivePrompt);
1451
1779
  return this.processManager.execute(this.command, args);
1452
1780
  }
1453
1781
  async resumeSession(sessionId, flags) {
@@ -1721,6 +2049,10 @@ var hookDefinitionSchema = z.object({
1721
2049
  var hooksConfigSchema = z.object({
1722
2050
  definitions: z.array(hookDefinitionSchema)
1723
2051
  });
2052
+ var backendContextConfigSchema = z.object({
2053
+ contextWindow: z.number().positive().optional(),
2054
+ compactThreshold: z.number().positive().optional()
2055
+ }).optional();
1724
2056
  var relayConfigSchema = z.object({
1725
2057
  defaultBackend: backendIdSchema.optional(),
1726
2058
  mcpServers: z.record(mcpServerConfigSchema).optional(),
@@ -1731,9 +2063,16 @@ var relayConfigSchema = z.object({
1731
2063
  }).optional(),
1732
2064
  hooks: hooksConfigSchema.optional(),
1733
2065
  contextMonitor: z.object({
1734
- enabled: z.boolean(),
1735
- thresholdPercent: z.number().min(0).max(100),
1736
- notifyMethod: z.enum(["stderr", "hook"])
2066
+ enabled: z.boolean().optional(),
2067
+ thresholdPercent: z.number().min(0).max(100).optional(),
2068
+ notifyThreshold: z.number().positive().optional(),
2069
+ notifyPercent: z.number().min(0).max(100).optional(),
2070
+ notifyMethod: z.enum(["stderr", "hook"]).optional(),
2071
+ backends: z.object({
2072
+ claude: backendContextConfigSchema,
2073
+ codex: backendContextConfigSchema,
2074
+ gemini: backendContextConfigSchema
2075
+ }).optional()
1737
2076
  }).optional(),
1738
2077
  mcpServerMode: z.object({
1739
2078
  maxDepth: z.number().int().positive(),
@@ -2129,34 +2468,75 @@ var HooksEngine = class _HooksEngine {
2129
2468
  };
2130
2469
 
2131
2470
  // src/core/context-monitor.ts
2471
+ var DEFAULT_BACKEND_CONTEXT = {
2472
+ claude: { contextWindow: 2e5, compactThreshold: 19e4 },
2473
+ codex: { contextWindow: 272e3, compactThreshold: 258400 },
2474
+ gemini: { contextWindow: 1048576, compactThreshold: 524288 }
2475
+ };
2476
+ var DEFAULT_NOTIFY_PERCENT = 70;
2132
2477
  var DEFAULT_CONFIG = {
2133
2478
  enabled: true,
2134
- thresholdPercent: 75,
2135
- notifyMethod: "stderr"
2479
+ notifyMethod: "hook"
2136
2480
  };
2137
2481
  var ContextMonitor = class {
2138
2482
  constructor(hooksEngine2, config) {
2139
2483
  this.hooksEngine = hooksEngine2;
2140
2484
  this.config = { ...DEFAULT_CONFIG, ...config };
2485
+ if (this.config.thresholdPercent !== void 0 && this.config.notifyPercent === void 0 && this.config.notifyThreshold === void 0) {
2486
+ this.config.notifyPercent = this.config.thresholdPercent;
2487
+ }
2141
2488
  }
2142
2489
  config;
2143
2490
  usageMap = /* @__PURE__ */ new Map();
2491
+ /** Get backend context config, merging user overrides with defaults */
2492
+ getBackendConfig(backendId) {
2493
+ const defaults = DEFAULT_BACKEND_CONTEXT[backendId];
2494
+ const overrides = this.config.backends?.[backendId];
2495
+ return {
2496
+ contextWindow: overrides?.contextWindow ?? defaults.contextWindow,
2497
+ compactThreshold: overrides?.compactThreshold ?? defaults.compactThreshold
2498
+ };
2499
+ }
2500
+ /** Calculate the notification threshold in tokens for a given backend */
2501
+ getNotifyThreshold(backendId) {
2502
+ if (this.config.notifyThreshold !== void 0) {
2503
+ return this.config.notifyThreshold;
2504
+ }
2505
+ const backendConfig = this.getBackendConfig(backendId);
2506
+ const notifyPercent = this.config.notifyPercent ?? DEFAULT_NOTIFY_PERCENT;
2507
+ return Math.round(backendConfig.contextWindow * notifyPercent / 100);
2508
+ }
2144
2509
  /** Update token usage for a session and check threshold */
2145
- updateUsage(sessionId, backendId, estimatedTokens, maxTokens) {
2510
+ updateUsage(sessionId, backendId, estimatedTokens) {
2146
2511
  if (!this.config.enabled) return;
2147
- const usagePercent = maxTokens > 0 ? Math.round(estimatedTokens / maxTokens * 100) : 0;
2512
+ const backendConfig = this.getBackendConfig(backendId);
2513
+ const contextWindow = backendConfig.contextWindow;
2514
+ const usagePercent = contextWindow > 0 ? Math.round(estimatedTokens / contextWindow * 100) : 0;
2148
2515
  const existing = this.usageMap.get(sessionId);
2149
- const wasNotified = existing?.notified ?? false;
2516
+ let wasNotified = existing?.notified ?? false;
2517
+ if (existing && estimatedTokens < existing.estimatedTokens * 0.7) {
2518
+ wasNotified = false;
2519
+ }
2150
2520
  this.usageMap.set(sessionId, {
2151
2521
  estimatedTokens,
2152
- maxTokens,
2522
+ contextWindow,
2523
+ compactThreshold: backendConfig.compactThreshold,
2153
2524
  usagePercent,
2154
2525
  backendId,
2155
2526
  notified: wasNotified
2156
2527
  });
2157
- if (usagePercent >= this.config.thresholdPercent && !wasNotified) {
2158
- this.usageMap.get(sessionId).notified = true;
2159
- this.notify(sessionId, backendId, usagePercent);
2528
+ const notifyAt = this.getNotifyThreshold(backendId);
2529
+ if (estimatedTokens >= notifyAt && !wasNotified) {
2530
+ const entry = this.usageMap.get(sessionId);
2531
+ entry.notified = true;
2532
+ this.notify(
2533
+ sessionId,
2534
+ backendId,
2535
+ usagePercent,
2536
+ estimatedTokens,
2537
+ contextWindow,
2538
+ backendConfig.compactThreshold
2539
+ );
2160
2540
  }
2161
2541
  }
2162
2542
  /** Get usage info for a session */
@@ -2165,17 +2545,31 @@ var ContextMonitor = class {
2165
2545
  if (!entry) return null;
2166
2546
  return {
2167
2547
  usagePercent: entry.usagePercent,
2168
- isEstimated: true
2548
+ isEstimated: true,
2549
+ backendId: entry.backendId,
2550
+ contextWindow: entry.contextWindow,
2551
+ compactThreshold: entry.compactThreshold,
2552
+ estimatedTokens: entry.estimatedTokens,
2553
+ remainingBeforeCompact: Math.max(
2554
+ 0,
2555
+ entry.compactThreshold - entry.estimatedTokens
2556
+ ),
2557
+ notifyThreshold: this.getNotifyThreshold(entry.backendId)
2169
2558
  };
2170
2559
  }
2171
2560
  /** Remove usage tracking for a session */
2172
2561
  removeSession(sessionId) {
2173
2562
  this.usageMap.delete(sessionId);
2174
2563
  }
2175
- notify(sessionId, backendId, usagePercent) {
2564
+ notify(sessionId, backendId, usagePercent, currentTokens, contextWindow, compactThreshold) {
2565
+ const remainingBeforeCompact = Math.max(
2566
+ 0,
2567
+ compactThreshold - currentTokens
2568
+ );
2569
+ const warningMessage = `${backendId} session ${sessionId} at ${usagePercent}% (${currentTokens}/${contextWindow} tokens). Compact in ~${remainingBeforeCompact} tokens. Save your work state now.`;
2176
2570
  if (this.config.notifyMethod === "stderr") {
2177
2571
  process.stderr.write(
2178
- `[relay] Context usage warning: session ${sessionId} is at ${usagePercent}% (threshold: ${this.config.thresholdPercent}%)
2572
+ `[relay] Context warning: ${warningMessage}
2179
2573
  `
2180
2574
  );
2181
2575
  } else if (this.config.notifyMethod === "hook" && this.hooksEngine) {
@@ -2186,7 +2580,10 @@ var ContextMonitor = class {
2186
2580
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2187
2581
  data: {
2188
2582
  usagePercent,
2189
- thresholdPercent: this.config.thresholdPercent
2583
+ currentTokens,
2584
+ contextWindow,
2585
+ compactThreshold,
2586
+ remainingBeforeCompact
2190
2587
  }
2191
2588
  };
2192
2589
  void this.hooksEngine.emit("on-context-threshold", hookInput);
@@ -2323,6 +2720,7 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
2323
2720
  try {
2324
2721
  if (flags.prompt) {
2325
2722
  logger.debug(`Executing prompt on ${backendId}`);
2723
+ let nativeSessionId;
2326
2724
  if (adapter.executeStreaming) {
2327
2725
  for await (const event of adapter.executeStreaming(flags)) {
2328
2726
  switch (event.type) {
@@ -2353,18 +2751,17 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
2353
2751
  break;
2354
2752
  case "usage": {
2355
2753
  if (contextMonitor2 && relaySessionId) {
2356
- const maxTokens = backendId === "gemini" ? 128e3 : 2e5;
2357
2754
  contextMonitor2.updateUsage(
2358
2755
  relaySessionId,
2359
2756
  backendId,
2360
- event.inputTokens + event.outputTokens,
2361
- maxTokens
2757
+ event.inputTokens + event.outputTokens
2362
2758
  );
2363
2759
  }
2364
2760
  break;
2365
2761
  }
2366
2762
  case "done":
2367
2763
  process.exitCode = event.result.exitCode;
2764
+ nativeSessionId = event.nativeSessionId;
2368
2765
  if (event.nativeSessionId && sessionManager2 && relaySessionId) {
2369
2766
  try {
2370
2767
  await sessionManager2.update(relaySessionId, {
@@ -2381,6 +2778,7 @@ function createBackendCommand(backendId, registry2, sessionManager2, hooksEngine
2381
2778
  if (result.stdout) process.stdout.write(result.stdout);
2382
2779
  if (result.stderr) process.stderr.write(result.stderr);
2383
2780
  process.exitCode = result.exitCode;
2781
+ nativeSessionId = result.nativeSessionId;
2384
2782
  if (result.nativeSessionId && sessionManager2 && relaySessionId) {
2385
2783
  try {
2386
2784
  await sessionManager2.update(relaySessionId, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rk0429/agentic-relay",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",