@rk0429/agentic-relay 22.3.1 → 22.4.1

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/bin/relay.js CHANGED
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import { createInterface } from "node:readline";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import { promisify } from "node:util";
10
+ import { AgentManagementService } from "../application/agent-management-service.js";
10
11
  import { ChatDaemonService, } from "../application/chat/chat-daemon-service.js";
11
12
  import { ChatInboundHandler } from "../application/chat/chat-inbound-handler.js";
12
13
  import { ChatOutboundHandler } from "../application/chat/chat-outbound-handler.js";
@@ -306,6 +307,33 @@ async function main() {
306
307
  }
307
308
  return;
308
309
  }
310
+ if (command.kind === "agent-list" ||
311
+ command.kind === "agent-show" ||
312
+ command.kind === "agent-stop" ||
313
+ command.kind === "agent-log") {
314
+ const agentService = new AgentManagementService({ store });
315
+ const exitCode = await handleAgentCommand(command, {
316
+ service: agentService,
317
+ stdout: process.stdout,
318
+ stderr: process.stderr,
319
+ now: () => new Date(),
320
+ fileExists: async (filePath) => {
321
+ try {
322
+ await access(filePath);
323
+ return true;
324
+ }
325
+ catch {
326
+ return false;
327
+ }
328
+ },
329
+ readTextFile: (filePath) => readFile(filePath, "utf8"),
330
+ readFileStream: createFollowReader(),
331
+ });
332
+ if (exitCode !== undefined) {
333
+ process.exitCode = exitCode;
334
+ }
335
+ return;
336
+ }
309
337
  if (command.kind === "chat-setup" ||
310
338
  command.kind === "chat-start" ||
311
339
  command.kind === "chat-stop" ||
@@ -351,6 +379,7 @@ async function main() {
351
379
  const server = createRelayMcpServer({
352
380
  service,
353
381
  taskService,
382
+ agentManagementService: new AgentManagementService({ store }),
354
383
  store,
355
384
  routineStatusQueryService: buildRoutineStatusQueryService({
356
385
  store,
@@ -633,6 +662,72 @@ export async function handleRoutineCommand(command, dependencies) {
633
662
  }
634
663
  }
635
664
  }
665
+ export async function handleAgentCommand(command, dependencies) {
666
+ switch (command.kind) {
667
+ case "agent-list": {
668
+ const agents = await dependencies.service.listActiveAgents({
669
+ includeStale: command.includeStale,
670
+ });
671
+ if (command.format === "json") {
672
+ dependencies.stdout.write(`${JSON.stringify(agents, null, 2)}\n`);
673
+ return 0;
674
+ }
675
+ dependencies.stdout.write(renderAgentTable(agents));
676
+ return 0;
677
+ }
678
+ case "agent-show": {
679
+ const detail = await dependencies.service.getAgentDetail({
680
+ relaySessionId: command.relaySessionId,
681
+ });
682
+ if (!detail) {
683
+ writeLine(dependencies.stderr, `No session found for relay session id: ${command.relaySessionId}`);
684
+ return 1;
685
+ }
686
+ dependencies.stdout.write(renderAgentDetail(detail));
687
+ return 0;
688
+ }
689
+ case "agent-stop": {
690
+ const result = await dependencies.service.stopAgent({
691
+ relaySessionId: command.relaySessionId,
692
+ actorId: dependencies.actorId ?? "local-cli-operator",
693
+ gracePeriodMs: command.gracePeriodMs,
694
+ });
695
+ if (command.format === "json") {
696
+ dependencies.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
697
+ return 0;
698
+ }
699
+ writeLine(dependencies.stdout, renderStopAgentResult(result));
700
+ return 0;
701
+ }
702
+ case "agent-log": {
703
+ const logPath = dependencies.service.readSessionLogPath({
704
+ relaySessionId: command.relaySessionId,
705
+ now: dependencies.now(),
706
+ });
707
+ const exists = await dependencies.fileExists(logPath);
708
+ if (!exists) {
709
+ writeLine(dependencies.stderr, `No log file found for today at ${logPath}.`);
710
+ return 1;
711
+ }
712
+ const raw = await dependencies.readTextFile(logPath);
713
+ for (const line of filterAgentLogLines(raw, command.relaySessionId)) {
714
+ writeLine(dependencies.stdout, line);
715
+ }
716
+ if (!command.follow) {
717
+ return 0;
718
+ }
719
+ if (!dependencies.readFileStream) {
720
+ throw new ValidationError("Agent log follow is unavailable in this environment.");
721
+ }
722
+ await dependencies.readFileStream(logPath, {
723
+ follow: true,
724
+ sessionId: command.relaySessionId,
725
+ onLine: (line) => writeLine(dependencies.stdout, line),
726
+ });
727
+ return undefined;
728
+ }
729
+ }
730
+ }
636
731
  async function suppressLoopStateWarnings(fn) {
637
732
  const origWarn = console.warn;
638
733
  console.warn = (...args) => {
@@ -655,6 +750,9 @@ export function renderHelpText(topic, scriptName = path.basename(fileURLToPath(i
655
750
  if (isRoutineHelpTopic(topic)) {
656
751
  return renderRoutineHelp(topic, scriptName);
657
752
  }
753
+ if (isAgentHelpTopic(topic)) {
754
+ return renderAgentHelp(topic, scriptName);
755
+ }
658
756
  if (isChatHelpTopic(topic)) {
659
757
  return renderChatHelp(topic, scriptName);
660
758
  }
@@ -669,6 +767,10 @@ export function renderHelpText(topic, scriptName = path.basename(fileURLToPath(i
669
767
  ` ${scriptName} routine cleanup [--config <dir>] [--dry-run]`,
670
768
  ` ${scriptName} routine list [--config <dir>] [--format <table|tsv|json>]`,
671
769
  ` ${scriptName} routine status [name] [--config <dir>] [--format <table|tsv|json>] [--limit <n>] [--run-id <uuid>]`,
770
+ ` ${scriptName} agent list [--include-stale] [--format <table|json>]`,
771
+ ` ${scriptName} agent show <session-id>`,
772
+ ` ${scriptName} agent stop <session-id> [--grace-ms <n>] [--json]`,
773
+ ` ${scriptName} agent log <session-id> [--follow]`,
672
774
  ` ${scriptName} chat setup (--slack | --discord) [--tokens-only]`,
673
775
  ` ${scriptName} chat start [--foreground]`,
674
776
  ` ${scriptName} chat stop`,
@@ -687,6 +789,10 @@ export function renderHelpText(topic, scriptName = path.basename(fileURLToPath(i
687
789
  ` ${commandName} routine cleanup [options] Remove stale routine metadata`,
688
790
  ` ${commandName} routine list [options] List known routines`,
689
791
  ` ${commandName} routine status [options] Show routine status details`,
792
+ ` ${commandName} agent list [options] List active agent processes`,
793
+ ` ${commandName} agent show [options] Show one agent session`,
794
+ ` ${commandName} agent stop [options] Stop one running agent`,
795
+ ` ${commandName} agent log [options] Show session logs for one agent`,
690
796
  ` ${commandName} chat setup [options] Configure Slack or Discord integration`,
691
797
  ` ${commandName} chat start [options] Start the chat daemon`,
692
798
  ` ${commandName} chat stop Stop the chat daemon`,
@@ -746,9 +852,71 @@ function renderCoreSubcommandHelp(topic, scriptName) {
746
852
  function isRoutineHelpTopic(topic) {
747
853
  return topic.startsWith("routine");
748
854
  }
855
+ function isAgentHelpTopic(topic) {
856
+ return topic.startsWith("agent");
857
+ }
749
858
  function isChatHelpTopic(topic) {
750
859
  return topic.startsWith("chat");
751
860
  }
861
+ function renderAgentHelp(topic, scriptName) {
862
+ if (topic === "agent") {
863
+ return renderAgentAggregateHelp(scriptName);
864
+ }
865
+ const commandName = path.parse(scriptName).name;
866
+ const allSubcommandsReference = `See '${commandName} agent --help' for all subcommands.`;
867
+ switch (topic) {
868
+ case "agent-list":
869
+ return [
870
+ `Usage: ${scriptName} agent list [options]`,
871
+ "",
872
+ "List active agent processes.",
873
+ "",
874
+ "Options:",
875
+ " --include-stale Include stale agent sessions",
876
+ " --format <fmt> Output format (table, json)",
877
+ " --json Alias for --format json",
878
+ " -h, --help Show this help",
879
+ "",
880
+ allSubcommandsReference,
881
+ ].join("\n");
882
+ case "agent-show":
883
+ return [
884
+ `Usage: ${scriptName} agent show <session-id>`,
885
+ "",
886
+ "Show one agent session and its related task.",
887
+ "",
888
+ "Options:",
889
+ " -h, --help Show this help",
890
+ "",
891
+ allSubcommandsReference,
892
+ ].join("\n");
893
+ case "agent-stop":
894
+ return [
895
+ `Usage: ${scriptName} agent stop <session-id> [options]`,
896
+ "",
897
+ "Stop one running agent process.",
898
+ "",
899
+ "Options:",
900
+ " --grace-ms <n> SIGTERM grace period before SIGKILL (0-600000)",
901
+ " --json Output StopAgentResult as JSON",
902
+ " -h, --help Show this help",
903
+ "",
904
+ allSubcommandsReference,
905
+ ].join("\n");
906
+ case "agent-log":
907
+ return [
908
+ `Usage: ${scriptName} agent log <session-id> [options]`,
909
+ "",
910
+ "Show session logs for one agent.",
911
+ "",
912
+ "Options:",
913
+ " --follow Follow appended log lines",
914
+ " -h, --help Show this help",
915
+ "",
916
+ allSubcommandsReference,
917
+ ].join("\n");
918
+ }
919
+ }
752
920
  function renderChatHelp(topic, scriptName) {
753
921
  if (topic === "chat") {
754
922
  return [
@@ -952,9 +1120,199 @@ function renderRoutineAggregateHelp(scriptName) {
952
1120
  " -h, --help Show this help",
953
1121
  ].join("\n");
954
1122
  }
1123
+ function renderAgentAggregateHelp(scriptName) {
1124
+ return [
1125
+ `Usage: ${scriptName} agent list [options]`,
1126
+ ` ${scriptName} agent show <session-id>`,
1127
+ ` ${scriptName} agent stop <session-id> [options]`,
1128
+ ` ${scriptName} agent log <session-id> [options]`,
1129
+ "",
1130
+ "Agent commands:",
1131
+ " list List active agent processes",
1132
+ " show Show one agent session",
1133
+ " stop Stop one running agent",
1134
+ " log Show session logs for one agent",
1135
+ "",
1136
+ "Options:",
1137
+ " --include-stale Include stale agent sessions",
1138
+ " --format <fmt> Output format for `agent list` (table, json)",
1139
+ " --json Alias for JSON output (`agent list`, `agent stop`)",
1140
+ " --grace-ms <n> SIGTERM grace period before SIGKILL (0-600000)",
1141
+ " --follow Follow appended log lines",
1142
+ " -h, --help Show this help",
1143
+ ].join("\n");
1144
+ }
955
1145
  function printHelp(topic) {
956
1146
  console.error(renderHelpText(topic));
957
1147
  }
1148
+ function renderAgentTable(agents) {
1149
+ if (agents.length === 0) {
1150
+ return "No active agents.\n";
1151
+ }
1152
+ const headers = ["agent_id", "session_id", "role", "backend", "running_for", "status"];
1153
+ const rows = agents.map((agent) => [
1154
+ agent.agent_id,
1155
+ agent.relay_session_id.slice(0, 8),
1156
+ agent.role ?? "-",
1157
+ agent.backend ?? "-",
1158
+ formatRunningFor(agent.running_for_seconds),
1159
+ agent.status,
1160
+ ]);
1161
+ const widths = headers.map((header, column) => Math.max(header.length, ...rows.map((row) => row[column].length)));
1162
+ const lines = [
1163
+ headers.map((header, column) => header.padEnd(widths[column])).join(" "),
1164
+ ...rows.map((row) => row.map((cell, column) => cell.padEnd(widths[column])).join(" ")),
1165
+ ];
1166
+ return `${lines.join("\n")}\n`;
1167
+ }
1168
+ function formatRunningFor(totalSeconds) {
1169
+ const hours = Math.floor(totalSeconds / 3600);
1170
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
1171
+ const seconds = totalSeconds % 60;
1172
+ return [hours, minutes, seconds]
1173
+ .map((value) => String(value).padStart(2, "0"))
1174
+ .join(":");
1175
+ }
1176
+ function renderAgentDetail(detail) {
1177
+ const metadataOrder = [
1178
+ "relaySessionId",
1179
+ "agentId",
1180
+ "role",
1181
+ "parentRelaySessionId",
1182
+ "continuationKind",
1183
+ "backend",
1184
+ "backendSessionId",
1185
+ "ownerPid",
1186
+ "ownerStartedAt",
1187
+ "backendChildPid",
1188
+ "backendChildObservedAt",
1189
+ "status",
1190
+ "phase",
1191
+ "taskType",
1192
+ "cwd",
1193
+ "createdAt",
1194
+ "updatedAt",
1195
+ "lastHeartbeatAt",
1196
+ "lastProgressAt",
1197
+ "handoffFrom",
1198
+ ];
1199
+ const lines = metadataOrder
1200
+ .flatMap((key) => {
1201
+ const value = detail.metadata[key];
1202
+ return value === undefined ? [] : [`${key}: ${formatAgentValue(value)}`];
1203
+ });
1204
+ if (detail.task) {
1205
+ lines.push("", "Task:");
1206
+ lines.push(` title: ${detail.task.title}`);
1207
+ lines.push(` status: ${detail.task.status}`);
1208
+ }
1209
+ if (detail.metadata.stopRequestedAt && detail.metadata.stopRequestedBy) {
1210
+ lines.push("", "Stop history:");
1211
+ lines.push(` requested_at: ${detail.metadata.stopRequestedAt}`);
1212
+ lines.push(` requested_by: ${detail.metadata.stopRequestedBy}`);
1213
+ if (detail.metadata.stopReason) {
1214
+ lines.push(` reason: ${detail.metadata.stopReason}`);
1215
+ }
1216
+ }
1217
+ return `${lines.join("\n")}\n`;
1218
+ }
1219
+ function formatAgentValue(value) {
1220
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1221
+ return String(value);
1222
+ }
1223
+ return JSON.stringify(value);
1224
+ }
1225
+ function renderStopAgentResult(result) {
1226
+ switch (result.kind) {
1227
+ case "stopped":
1228
+ return `Stopped agent (pid ${result.pid}) after ${result.durationMs}ms.`;
1229
+ case "killed":
1230
+ return `Force-killed agent (pid ${result.pid}) after SIGTERM grace.`;
1231
+ case "no_child":
1232
+ return "No child process recorded — nothing to stop.";
1233
+ case "already_exited":
1234
+ return `Child process (pid ${result.pid}) has already exited.`;
1235
+ }
1236
+ }
1237
+ function filterAgentLogLines(raw, relaySessionId) {
1238
+ return raw
1239
+ .split(/\r?\n/u)
1240
+ .filter((line) => line.length > 0)
1241
+ .filter((line) => matchesAgentLogLine(line, relaySessionId));
1242
+ }
1243
+ function matchesAgentLogLine(line, relaySessionId) {
1244
+ try {
1245
+ const parsed = JSON.parse(line);
1246
+ return parsed.session_id === relaySessionId;
1247
+ }
1248
+ catch {
1249
+ return false;
1250
+ }
1251
+ }
1252
+ function createFollowReader() {
1253
+ return async (filePath, options) => {
1254
+ let cursor = 0;
1255
+ let buffer = "";
1256
+ try {
1257
+ cursor = (await readFile(filePath, "utf8")).length;
1258
+ }
1259
+ catch {
1260
+ cursor = 0;
1261
+ }
1262
+ const flushAppended = async () => {
1263
+ const content = await readFile(filePath, "utf8");
1264
+ if (content.length < cursor) {
1265
+ cursor = 0;
1266
+ buffer = "";
1267
+ }
1268
+ if (content.length === cursor) {
1269
+ return;
1270
+ }
1271
+ buffer += content.slice(cursor);
1272
+ cursor = content.length;
1273
+ const segments = buffer.split(/\r?\n/u);
1274
+ buffer = segments.pop() ?? "";
1275
+ for (const line of segments) {
1276
+ if (matchesAgentLogLine(line, options.sessionId)) {
1277
+ options.onLine(line);
1278
+ }
1279
+ }
1280
+ };
1281
+ if (!options.follow) {
1282
+ return;
1283
+ }
1284
+ await new Promise((resolve, reject) => {
1285
+ let settled = false;
1286
+ const cleanup = () => {
1287
+ if (settled) {
1288
+ return;
1289
+ }
1290
+ settled = true;
1291
+ clearInterval(timer);
1292
+ process.off("SIGINT", onSigint);
1293
+ options.signal?.removeEventListener("abort", onAbort);
1294
+ resolve();
1295
+ };
1296
+ const fail = (error) => {
1297
+ if (settled) {
1298
+ return;
1299
+ }
1300
+ settled = true;
1301
+ clearInterval(timer);
1302
+ process.off("SIGINT", onSigint);
1303
+ options.signal?.removeEventListener("abort", onAbort);
1304
+ reject(error);
1305
+ };
1306
+ const onAbort = () => cleanup();
1307
+ const onSigint = () => cleanup();
1308
+ const timer = setInterval(() => {
1309
+ void flushAppended().catch(fail);
1310
+ }, 1000);
1311
+ process.once("SIGINT", onSigint);
1312
+ options.signal?.addEventListener("abort", onAbort, { once: true });
1313
+ });
1314
+ };
1315
+ }
958
1316
  function createRoutineCatalogLoader(allowCommandRun) {
959
1317
  return async (yamlPath, repo, vars) => {
960
1318
  const loaded = await loadRoutine(yamlPath, repo, vars);