agent-relay-server 0.10.21 → 0.10.23
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/docs/openapi.json +91 -1
- package/package.json +1 -1
- package/public/index.html +12 -9
- package/scripts/orchestrator-spawn-smoke.ts +26 -2
- package/src/db.ts +2 -2
- package/src/routes.ts +53 -3
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.10.
|
|
5
|
+
"version": "0.10.21",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
|
@@ -1737,6 +1737,96 @@
|
|
|
1737
1737
|
]
|
|
1738
1738
|
}
|
|
1739
1739
|
},
|
|
1740
|
+
"/api/agents/{id}/prompt": {
|
|
1741
|
+
"post": {
|
|
1742
|
+
"operationId": "postAgentPrompt",
|
|
1743
|
+
"summary": "Inject a prompt into a live agent",
|
|
1744
|
+
"tags": [
|
|
1745
|
+
"Agents"
|
|
1746
|
+
],
|
|
1747
|
+
"description": "Injects a prompt directly into a runner-managed agent's provider session via the live-session lane. Creates a session message for the timeline and emits a prompt.inject bus command. The agent does not consume a relay message — the text goes straight into the provider stdin. Use for agents with liveSession.inject capability.",
|
|
1748
|
+
"parameters": [
|
|
1749
|
+
{
|
|
1750
|
+
"name": "id",
|
|
1751
|
+
"in": "path",
|
|
1752
|
+
"required": true,
|
|
1753
|
+
"schema": {
|
|
1754
|
+
"type": "string"
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
],
|
|
1758
|
+
"requestBody": {
|
|
1759
|
+
"required": true,
|
|
1760
|
+
"content": {
|
|
1761
|
+
"application/json": {
|
|
1762
|
+
"schema": {
|
|
1763
|
+
"type": "object",
|
|
1764
|
+
"properties": {
|
|
1765
|
+
"body": {
|
|
1766
|
+
"type": "string"
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
},
|
|
1773
|
+
"responses": {
|
|
1774
|
+
"200": {
|
|
1775
|
+
"description": "Success",
|
|
1776
|
+
"content": {
|
|
1777
|
+
"application/json": {}
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
"201": {
|
|
1781
|
+
"description": "Created",
|
|
1782
|
+
"content": {
|
|
1783
|
+
"application/json": {}
|
|
1784
|
+
}
|
|
1785
|
+
},
|
|
1786
|
+
"400": {
|
|
1787
|
+
"description": "Bad request",
|
|
1788
|
+
"content": {
|
|
1789
|
+
"application/json": {
|
|
1790
|
+
"schema": {
|
|
1791
|
+
"$ref": "#/components/schemas/Error"
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
},
|
|
1796
|
+
"404": {
|
|
1797
|
+
"description": "Not found",
|
|
1798
|
+
"content": {
|
|
1799
|
+
"application/json": {
|
|
1800
|
+
"schema": {
|
|
1801
|
+
"$ref": "#/components/schemas/Error"
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
"422": {
|
|
1807
|
+
"description": "Unprocessable entity",
|
|
1808
|
+
"content": {
|
|
1809
|
+
"application/json": {
|
|
1810
|
+
"schema": {
|
|
1811
|
+
"$ref": "#/components/schemas/Error"
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
"security": [
|
|
1818
|
+
{
|
|
1819
|
+
"bearerAuth": []
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
"tokenHeader": []
|
|
1823
|
+
},
|
|
1824
|
+
{
|
|
1825
|
+
"tokenQuery": []
|
|
1826
|
+
}
|
|
1827
|
+
]
|
|
1828
|
+
}
|
|
1829
|
+
},
|
|
1740
1830
|
"/api/agents/{id}/permission-decision": {
|
|
1741
1831
|
"post": {
|
|
1742
1832
|
"operationId": "postAgentPermissionDecision",
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -12861,15 +12861,18 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12861
12861
|
if (!body.trim() && attachments.length === 0 || get().chatSending) return;
|
|
12862
12862
|
set({ chatSending: true });
|
|
12863
12863
|
try {
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
|
|
12872
|
-
|
|
12864
|
+
if (get().agentsById[thread.peer]?.providerCapabilities?.liveSession?.inject && attachments.length === 0) await api("POST", "/agents/" + encodeURIComponent(thread.peer) + "/prompt", { body: body.trim() });
|
|
12865
|
+
else {
|
|
12866
|
+
const payload = {
|
|
12867
|
+
from: HUMAN_AGENT_ID,
|
|
12868
|
+
to: thread.peer,
|
|
12869
|
+
body: body.trim() || "Attachment"
|
|
12870
|
+
};
|
|
12871
|
+
if (thread.lastMessage?.channel) payload.channel = thread.lastMessage.channel;
|
|
12872
|
+
if (thread.lastMessage?.id) payload.replyTo = thread.lastMessage.id;
|
|
12873
|
+
if (attachments.length) payload.payload = { attachments };
|
|
12874
|
+
await api("POST", "/messages", payload);
|
|
12875
|
+
}
|
|
12873
12876
|
const next = { ...get().inboxDrafts };
|
|
12874
12877
|
delete next[thread.peer];
|
|
12875
12878
|
set({ inboxDrafts: next });
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
|
|
2
4
|
type Orchestrator = {
|
|
3
5
|
id: string;
|
|
4
6
|
status: string;
|
|
@@ -20,7 +22,11 @@ type Agent = {
|
|
|
20
22
|
const args = process.argv.slice(2);
|
|
21
23
|
let relayUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
|
|
22
24
|
let orchestratorId = process.env.AGENT_RELAY_SMOKE_ORCHESTRATOR || "";
|
|
23
|
-
|
|
25
|
+
// Default cwd is resolved from the selected orchestrator's baseDir below, not
|
|
26
|
+
// the local process.cwd() — the chosen orchestrator may be on a different host
|
|
27
|
+
// (e.g. macOS macmini with /Users/admin/projects) where a local Linux path
|
|
28
|
+
// fails cwd containment.
|
|
29
|
+
let cwd = process.env.AGENT_RELAY_SMOKE_CWD || "";
|
|
24
30
|
let timeoutMs = Number(process.env.AGENT_RELAY_SMOKE_TIMEOUT_MS || 90_000);
|
|
25
31
|
let providers = (process.env.AGENT_RELAY_SMOKE_PROVIDERS || "codex,claude").split(",").filter(Boolean);
|
|
26
32
|
|
|
@@ -86,12 +92,30 @@ function findSpawnedAgent(agents: Agent[], provider: string, label: string, star
|
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
const orchestrators = await api<Orchestrator[]>("GET", "/orchestrators");
|
|
95
|
+
// Default selection prefers the orchestrator on the same host as the relay (the
|
|
96
|
+
// gate runs there): a local spawn is faster and avoids cross-host teardown
|
|
97
|
+
// flakiness. Fall back to any online orchestrator. An explicit --orchestrator
|
|
98
|
+
// always wins — pair it with --cwd (or rely on the baseDir default below) to
|
|
99
|
+
// target a remote host such as a macOS macmini.
|
|
100
|
+
const onlineOrchestrators = orchestrators.filter((orch) => orch.status === "online");
|
|
101
|
+
const localOrchestrator = onlineOrchestrators.find((orch) => orch.id === hostname());
|
|
89
102
|
const orchestrator = orchestratorId
|
|
90
103
|
? orchestrators.find((orch) => orch.id === orchestratorId)
|
|
91
|
-
:
|
|
104
|
+
: localOrchestrator ?? onlineOrchestrators[0];
|
|
92
105
|
if (!orchestrator) throw new Error(orchestratorId ? `orchestrator not found: ${orchestratorId}` : "no online orchestrator found");
|
|
93
106
|
if (orchestrator.status !== "online") throw new Error(`orchestrator ${orchestrator.id} is ${orchestrator.status}`);
|
|
94
107
|
|
|
108
|
+
// Resolve the spawn cwd against the chosen orchestrator's own base directory so
|
|
109
|
+
// the smoke works regardless of which host runs it. An explicit --cwd / env
|
|
110
|
+
// override still wins (and is subject to the orchestrator's containment check).
|
|
111
|
+
if (!cwd) {
|
|
112
|
+
if (!orchestrator.baseDir) {
|
|
113
|
+
throw new Error(`orchestrator ${orchestrator.id} did not report a baseDir; pass --cwd or set AGENT_RELAY_SMOKE_CWD`);
|
|
114
|
+
}
|
|
115
|
+
cwd = orchestrator.baseDir;
|
|
116
|
+
}
|
|
117
|
+
console.log(`using cwd ${cwd} on ${orchestrator.id} (base ${orchestrator.baseDir})`);
|
|
118
|
+
|
|
95
119
|
for (const provider of providers) {
|
|
96
120
|
if (!orchestrator.providers.includes(provider)) {
|
|
97
121
|
console.log(`skip ${provider}: orchestrator ${orchestrator.id} does not support it`);
|
package/src/db.ts
CHANGED
|
@@ -4085,7 +4085,7 @@ export function pollMessages(query: PollQuery): Message[] {
|
|
|
4085
4085
|
}
|
|
4086
4086
|
|
|
4087
4087
|
function messageRequiresReply(message: Message): boolean {
|
|
4088
|
-
if (message.kind === "system" || message.kind === "control") return false;
|
|
4088
|
+
if (message.kind === "system" || message.kind === "control" || message.kind === "session") return false;
|
|
4089
4089
|
if (message.from === "user") return true;
|
|
4090
4090
|
if (message.kind === "task" || message.kind === "channel.event") return true;
|
|
4091
4091
|
return Boolean(message.payload?.source);
|
|
@@ -4133,7 +4133,7 @@ function replyObligationFromMessage(message: Message, agentId: string): ReplyObl
|
|
|
4133
4133
|
...(message.channel ? { channel: message.channel } : {}),
|
|
4134
4134
|
bodyPreview: message.body.length > 240 ? `${message.body.slice(0, 240)}\n[truncated]` : message.body,
|
|
4135
4135
|
createdAt: message.createdAt,
|
|
4136
|
-
replyCommand: `agent-relay /reply ${message.id} --stdin <
|
|
4136
|
+
replyCommand: `agent-relay /reply ${message.id} --stdin < .agent-relay/sessions/${agentId}/tmp/reply.md`,
|
|
4137
4137
|
};
|
|
4138
4138
|
}
|
|
4139
4139
|
|
package/src/routes.ts
CHANGED
|
@@ -2333,6 +2333,52 @@ const postAgentAction: Handler = async (req, params) => {
|
|
|
2333
2333
|
}
|
|
2334
2334
|
};
|
|
2335
2335
|
|
|
2336
|
+
const postAgentPrompt: Handler = async (req, params) => {
|
|
2337
|
+
const parsed = await parseBody<unknown>(req);
|
|
2338
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
2339
|
+
try {
|
|
2340
|
+
if (!isRecord(parsed.body)) return error("body required");
|
|
2341
|
+
const body = cleanString(parsed.body.body, "body", { required: true, max: 100_000 })!;
|
|
2342
|
+
const agent = getAgent(params.id!);
|
|
2343
|
+
if (!agent) return error("agent not found", 404);
|
|
2344
|
+
if (agent.status === "offline") return error("agent is offline", 422);
|
|
2345
|
+
const denied = authorizeRoute(req, {
|
|
2346
|
+
scope: "message:send",
|
|
2347
|
+
resource: { target: agent.id, agentId: "user" },
|
|
2348
|
+
});
|
|
2349
|
+
if (denied) return denied;
|
|
2350
|
+
const message = sendMessage({
|
|
2351
|
+
from: "user",
|
|
2352
|
+
to: agent.id,
|
|
2353
|
+
kind: "session",
|
|
2354
|
+
body,
|
|
2355
|
+
});
|
|
2356
|
+
const command = createCommand({
|
|
2357
|
+
type: "prompt.inject",
|
|
2358
|
+
source: "dashboard",
|
|
2359
|
+
target: agent.id,
|
|
2360
|
+
params: { body, messageId: message.id },
|
|
2361
|
+
});
|
|
2362
|
+
emitCommand(command);
|
|
2363
|
+
emitNewMessage(message);
|
|
2364
|
+
auditEvent({
|
|
2365
|
+
clientId: "server-prompt-inject-" + message.id,
|
|
2366
|
+
kind: "message",
|
|
2367
|
+
title: "Prompt injected",
|
|
2368
|
+
body,
|
|
2369
|
+
meta: `user -> ${agent.id}`,
|
|
2370
|
+
icon: "ti-message-bolt",
|
|
2371
|
+
view: "messages",
|
|
2372
|
+
messageId: message.id,
|
|
2373
|
+
agentId: agent.id,
|
|
2374
|
+
});
|
|
2375
|
+
return json({ ok: true, messageId: message.id, commandId: command.id }, 201);
|
|
2376
|
+
} catch (e) {
|
|
2377
|
+
if (e instanceof ValidationError) return error(e.message, 400);
|
|
2378
|
+
throw e;
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
|
|
2336
2382
|
const postAgentPermissionDecision: Handler = async (req, params) => {
|
|
2337
2383
|
const parsed = await parseBody<unknown>(req);
|
|
2338
2384
|
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
@@ -4716,7 +4762,7 @@ const deleteCommandById: Handler = (_req, params) => {
|
|
|
4716
4762
|
|
|
4717
4763
|
// --- Message routes ---
|
|
4718
4764
|
|
|
4719
|
-
const VALID_MSG_KINDS = ["chat", "channel.event", "task", "pair", "control", "system"];
|
|
4765
|
+
const VALID_MSG_KINDS = ["chat", "channel.event", "task", "pair", "control", "system", "session"];
|
|
4720
4766
|
|
|
4721
4767
|
const getQueuedMessagesRoute: Handler = (req) => {
|
|
4722
4768
|
const url = new URL(req.url);
|
|
@@ -4804,7 +4850,10 @@ const postMessage: Handler = async (req) => {
|
|
|
4804
4850
|
resource: { target: input.to, channel: input.channel, agentId: input.from },
|
|
4805
4851
|
});
|
|
4806
4852
|
if (denied) return denied;
|
|
4807
|
-
|
|
4853
|
+
// "session" = observed assistant turn (Phase 1 live-session lane). It is captured
|
|
4854
|
+
// from the provider transcript and stored for the dashboard chat; it must persist
|
|
4855
|
+
// regardless of target liveness and never be re-delivered into a session.
|
|
4856
|
+
const bypassKinds = ["system", "control", "session"];
|
|
4808
4857
|
if (isDirectTarget(input.to) && !bypassKinds.includes(input.kind ?? "")) {
|
|
4809
4858
|
const target = getAgent(input.to);
|
|
4810
4859
|
if (target && target.status === "offline") {
|
|
@@ -4850,7 +4899,7 @@ const postMessage: Handler = async (req) => {
|
|
|
4850
4899
|
};
|
|
4851
4900
|
|
|
4852
4901
|
function automaticMemoryTarget(message: { to: string; resolvedToAgent?: string; kind: string }): string | null {
|
|
4853
|
-
if (message.kind === "system" || message.kind === "control") return null;
|
|
4902
|
+
if (message.kind === "system" || message.kind === "control" || message.kind === "session") return null;
|
|
4854
4903
|
const target = message.resolvedToAgent ?? message.to;
|
|
4855
4904
|
if (!isDirectTarget(target)) return null;
|
|
4856
4905
|
const agent = getAgent(target);
|
|
@@ -6136,6 +6185,7 @@ const routes: Route[] = [
|
|
|
6136
6185
|
route("PATCH", "/api/agents/:id/tags", patchAgentTags),
|
|
6137
6186
|
route("POST", "/api/agents/:id/heartbeat", postHeartbeat),
|
|
6138
6187
|
route("POST", "/api/agents/:id/actions", postAgentAction),
|
|
6188
|
+
route("POST", "/api/agents/:id/prompt", postAgentPrompt),
|
|
6139
6189
|
route("POST", "/api/agents/:id/permission-decision", postAgentPermissionDecision),
|
|
6140
6190
|
route("POST", "/api/agents/:id/terminal-session", postAgentTerminalSession),
|
|
6141
6191
|
route("DELETE", "/api/agents/:id/terminal-session/:session", deleteAgentTerminalSession),
|