agent-relay-server 0.4.16 → 0.4.18

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/README.md CHANGED
@@ -82,18 +82,42 @@ Open a second session and say:
82
82
 
83
83
  ## Configuration
84
84
 
85
- Both integrations share the same four env vars:
85
+ Both integrations share the same provider env vars:
86
86
 
87
87
  | Env var | Default | Purpose |
88
88
  |---------|---------|---------|
89
89
  | `AGENT_RELAY_URL` | `http://localhost:4850` | Relay server URL |
90
90
  | `AGENT_RELAY_TOKEN` | unset | Auth token (required for remote relays) |
91
91
  | `AGENT_RELAY_CAPS` | `chat` | Comma-separated agent capabilities |
92
+ | `AGENT_RELAY_TAGS` | unset | Extra comma-separated tags added to provider defaults |
93
+ | `AGENT_RELAY_LABEL` | unset | Human-friendly agent label set at registration |
94
+ | `AGENT_RELAY_CHANNELS` | all | Comma-separated channel subscriptions; unchannelled direct work still arrives |
95
+ | `AGENT_RELAY_PROFILE` | unset | Named profile loaded from the profiles file |
96
+ | `AGENT_RELAY_PROFILES_FILE` | `~/.config/agent-relay/profiles.json` | JSON profile file |
92
97
  | `AGENT_RELAY_APPROVAL` | `open` | Approval mode: `open`, `guarded`, `read-only` |
93
98
 
99
+ Profiles preconfigure labels, tags, capabilities, channels, approval mode, and meta fields:
100
+
101
+ ```json
102
+ {
103
+ "backend-tester": {
104
+ "label": "backend tester",
105
+ "tags": ["backend", "api", "test"],
106
+ "capabilities": ["chat", "review", "test", "backend"],
107
+ "channels": ["backend", "qa"],
108
+ "approval": "guarded",
109
+ "meta": { "role": "backend-tester" }
110
+ }
111
+ }
112
+ ```
113
+
114
+ Launch with `AGENT_RELAY_PROFILE=backend-tester codex` or
115
+ `AGENT_RELAY_PROFILE=backend-tester claude`. Explicit env vars override the
116
+ profile where they overlap.
117
+
94
118
  Codex has additional tuning vars documented in [codex/README.md](codex/README.md).
95
119
 
96
- Agent IDs are deterministic: `{hostname}-{rig}-{project}-{session-hash}`.
120
+ Agent IDs are deterministic: `{hostname}-{project}-{session-hash}`.
97
121
 
98
122
  ## Message Targeting
99
123
 
@@ -167,6 +191,14 @@ claimable message. Posting `"status": "resolved"` marks the active task `done`.
167
191
  When an integration has `callbackUrl`, Agent Relay posts task lifecycle events
168
192
  (creation, claim, status changes) back to the caller.
169
193
 
194
+ Integration tokens can also be used as scoped API tokens when the full admin
195
+ `AGENT_RELAY_TOKEN` would be too broad. Supported scopes include `stats:read`,
196
+ `health:read`, `events:read`, `agents:read`, `agents:write`, `messages:read`,
197
+ `messages:write`, `tasks:read`, `tasks:write`, `pairs:read`, `pairs:write`,
198
+ `system:write`, and `*`.
199
+ The event ingress endpoint also accepts the legacy `tasks:create` and
200
+ `events:create` scopes.
201
+
170
202
  Task lifecycle API:
171
203
 
172
204
  | Method | Path | Purpose |
@@ -281,6 +313,49 @@ curl -H "X-Agent-Relay-Token: $AGENT_RELAY_TOKEN" http://localhost:4850/api/stat
281
313
  | `DELETE` | `/messages/:id` | Delete message |
282
314
  | `GET` | `/messages/cursor` | Latest message ID (for poller bootstrap) |
283
315
 
316
+ ### Pair Sessions
317
+
318
+ Pair sessions are exclusive two-agent live chats backed by normal relay
319
+ messages. They are useful when you want two agent-sessions to collaborate
320
+ in real time without turning the relay into a group chat.
321
+ The sessions can be claude to codex, codex to codex or claude to claude,
322
+ since agent-relay is provider-independent.
323
+
324
+ ```bash
325
+ agent-relay /pair codex "Debug flaky auth tests"
326
+ agent-relay pair codex --objective "Debug flaky auth tests"
327
+ agent-relay pair accept PAIR_ID --agent "$AGENT_RELAY_ID"
328
+ agent-relay pair send PAIR_ID --from "$AGENT_RELAY_ID" --body "What do you see?"
329
+ agent-relay /message codex "Can you look at that failing action?"
330
+ agent-relay /send-claimable tag:backend "Please claim and fix the failing API test"
331
+ agent-relay /disconnect
332
+ agent-relay /status
333
+ agent-relay /label backend-fixer
334
+ agent-relay /tags backend tests urgent
335
+ ```
336
+
337
+ Inside provider sessions, the Codex plugin and Claude plugin ship command
338
+ skills for `/pair`, `/message`, `/send-claimable`, `/disconnect`, `/status`,
339
+ `/label`, and `/tags`. If one of those names collides with another installed
340
+ skill, invoke the provider-prefixed form such as `/agent-relay:pair` or
341
+ `/agent-relay:message`. The CLI tries to auto-detect the current agent id from
342
+ provider state; pass `--from` or `--agent` if you want to be explicit.
343
+
344
+ If the target is already in a pending or active pair, the relay returns
345
+ `409 Busy`. If a target like `codex` matches multiple available agents, the
346
+ relay asks for a more specific target such as `id:...`, `label:...`, `tag:...`,
347
+ `cap:...` or `machine:...`.
348
+
349
+ | Method | Path | Purpose |
350
+ |--------|------|---------|
351
+ | `POST` | `/pairs` | Create a pair invite |
352
+ | `GET` | `/pairs` | List pairs (`?agent=`, `?status=`) |
353
+ | `GET` | `/pairs/:id` | Get one pair |
354
+ | `POST` | `/pairs/:id/accept` | Accept a pending pair |
355
+ | `POST` | `/pairs/:id/reject` | Reject a pending pair |
356
+ | `POST` | `/pairs/:id/messages` | Send a pair message |
357
+ | `POST` | `/pairs/:id/hangup` | End a pending or active pair |
358
+
284
359
  ### Tasks
285
360
 
286
361
  | Method | Path | Purpose |
@@ -358,3 +433,10 @@ claude --plugin-dir ./claude # test plugin locally
358
433
  ## License
359
434
 
360
435
  AGPL-3.0-or-later. See [LICENSE](LICENSE).
436
+
437
+ ## Related
438
+
439
+ - **[callmux](https://github.com/edimuj/callmux)** - MCP multiplexer: parallel execution, batching, caching, pipelining, and shared infrastructure for any AI agent
440
+ - **[tokenlean](https://github.com/edimuj/tokenlean)** - Make agents less wasteful: Token-efficient CLI tool-toolbox for AI agents
441
+ - **[claude-mneme](https://github.com/edimuj/claude-mneme) / [codex-mneme](https://github.com/edimuj/codex-mneme)** - Lightweight persistent agent memory: so every session picks up where the last one left off
442
+ - **[agent-awareness](https://github.com/edimuj/agent-awareness)** - Real-time situational awareness for your agents
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "scripts": {
20
20
  "start": "bun run src/index.ts",
21
21
  "dev": "bun --watch run src/index.ts",
22
- "test": "bun test src/",
22
+ "test": "bun test",
23
23
  "typecheck": "tsc --noEmit"
24
24
  },
25
25
  "keywords": [
@@ -33,6 +33,7 @@
33
33
  tasks: [],
34
34
  taskEvents: [],
35
35
  stats: {},
36
+ health: null,
36
37
  now: Date.now(),
37
38
  authToken: loadPref("authToken", ""),
38
39
 
@@ -186,6 +187,7 @@
186
187
  es.addEventListener("agent.status", (event) => handleAgentStatus(this, parseEventData(event)));
187
188
  es.addEventListener("agent.removed", (event) => handleAgentRemoved(this, parseEventData(event)));
188
189
  es.addEventListener("message.claimed", (event) => handleMessageClaimed(this, parseEventData(event)));
190
+ es.addEventListener("message.claim_released", (event) => handleMessageClaimReleased(this, parseEventData(event)));
189
191
  es.addEventListener("message.deleted", (event) => handleMessageDeleted(this, parseEventData(event)));
190
192
  registerTaskEvents(this, es);
191
193
  }
@@ -223,7 +225,17 @@
223
225
 
224
226
  function handleMessageClaimed(vm, data) {
225
227
  const msg = vm.messages.find((item) => item.id === data.messageId);
226
- if (msg) msg.claimedBy = data.claimedBy;
228
+ if (!msg) return;
229
+ msg.claimedBy = data.claimedBy;
230
+ msg.claimExpiresAt = data.claimExpiresAt;
231
+ }
232
+
233
+ function handleMessageClaimReleased(vm, data) {
234
+ const msg = vm.messages.find((item) => item.id === data.messageId);
235
+ if (!msg) return;
236
+ delete msg.claimedBy;
237
+ delete msg.claimedAt;
238
+ delete msg.claimExpiresAt;
227
239
  }
228
240
 
229
241
  function handleMessageDeleted(vm, data) {
@@ -265,7 +277,7 @@
265
277
  },
266
278
 
267
279
  async refresh() {
268
- await Promise.all([this.fetchStats(), this.fetchAgents(), this.fetchMessages(), this.fetchTasks()]);
280
+ await Promise.all([this.fetchStats(), this.fetchHealth(), this.fetchAgents(), this.fetchMessages(), this.fetchTasks()]);
269
281
  },
270
282
 
271
283
  async refreshLiveData() {
@@ -285,6 +297,12 @@
285
297
  } catch {}
286
298
  },
287
299
 
300
+ async fetchHealth() {
301
+ try {
302
+ this.health = await this.api("GET", "/health");
303
+ } catch {}
304
+ },
305
+
288
306
  async fetchAgents() {
289
307
  try {
290
308
  this.agents = await this.api("GET", "/agents");
@@ -323,6 +341,7 @@
323
341
  uniqueLabels: { get: getUniqueLabels },
324
342
  uniqueCaps: { get: getUniqueCaps },
325
343
  uniqueTags: { get: getUniqueTags },
344
+ healthIssues: { get: getHealthIssues },
326
345
  };
327
346
  }
328
347
 
@@ -399,6 +418,10 @@
399
418
  return [...new Set(this.agents.flatMap((agent) => agent.tags || []))];
400
419
  }
401
420
 
421
+ function getHealthIssues() {
422
+ return (this.health?.checks || []).filter((check) => check.status !== "ok");
423
+ }
424
+
402
425
  function compareAgents(vm, a, b) {
403
426
  switch (vm.agentSort) {
404
427
  case "name":
@@ -422,6 +445,7 @@
422
445
  agentStatusTitle,
423
446
  timeAgo,
424
447
  fmtTime,
448
+ healthAlertClass,
425
449
  };
426
450
  }
427
451
 
@@ -476,6 +500,12 @@
476
500
  return new Date(iso).toLocaleString();
477
501
  }
478
502
 
503
+ function healthAlertClass(status) {
504
+ if (status === "error") return "alert-danger";
505
+ if (status === "degraded") return "alert-warning";
506
+ return "alert-success";
507
+ }
508
+
479
509
  function createMessageActions() {
480
510
  return {
481
511
  openCompose,
package/public/index.html CHANGED
@@ -207,6 +207,25 @@
207
207
  </div>
208
208
  </div>
209
209
 
210
+ <template x-if="health">
211
+ <div class="alert d-flex align-items-start gap-3 mb-4" :class="healthAlertClass(health.status)">
212
+ <i class="ti ti-heartbeat mt-1"></i>
213
+ <div class="flex-grow-1">
214
+ <div class="fw-bold" x-text="'Relay health: ' + health.status"></div>
215
+ <template x-if="healthIssues.length === 0">
216
+ <div class="small">All checks passing</div>
217
+ </template>
218
+ <template x-if="healthIssues.length > 0">
219
+ <div class="small">
220
+ <template x-for="check in healthIssues" :key="check.name">
221
+ <span class="me-3" x-text="check.detail || check.name"></span>
222
+ </template>
223
+ </div>
224
+ </template>
225
+ </div>
226
+ </div>
227
+ </template>
228
+
210
229
  <!-- Two-column: Agents + Recent messages -->
211
230
  <div class="row g-3">
212
231
  <div class="col-lg-5">