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 +84 -2
- package/package.json +2 -2
- package/public/dashboard.js +32 -2
- package/public/index.html +19 -0
- package/src/cli.ts +441 -0
- package/src/config.ts +1 -0
- package/src/db.ts +597 -28
- package/src/index.ts +13 -3
- package/src/routes.ts +330 -13
- package/src/security.ts +31 -1
- package/src/sse.ts +8 -2
- package/src/types.ts +65 -0
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
|
|
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}-{
|
|
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.
|
|
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
|
|
22
|
+
"test": "bun test",
|
|
23
23
|
"typecheck": "tsc --noEmit"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
package/public/dashboard.js
CHANGED
|
@@ -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)
|
|
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">
|