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 CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Relay API",
5
- "version": "0.10.20",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.10.21",
3
+ "version": "0.10.23",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
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
- const payload = {
12865
- from: HUMAN_AGENT_ID,
12866
- to: thread.peer,
12867
- body: body.trim() || "Attachment"
12868
- };
12869
- if (thread.lastMessage?.channel) payload.channel = thread.lastMessage.channel;
12870
- if (thread.lastMessage?.id) payload.replyTo = thread.lastMessage.id;
12871
- if (attachments.length) payload.payload = { attachments };
12872
- await api("POST", "/messages", payload);
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
- let cwd = process.env.AGENT_RELAY_SMOKE_CWD || process.cwd();
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
- : orchestrators.find((orch) => orch.status === "online");
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 < response.md`,
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
- const bypassKinds = ["system", "control"];
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),