agent-relay-runner 0.19.2 → 0.20.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "directory": "runner"
21
21
  },
22
22
  "dependencies": {
23
- "agent-relay-sdk": "0.2.10"
23
+ "agent-relay-sdk": "0.2.11"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "latest",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.19.2",
4
+ "version": "0.20.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/adapter.ts CHANGED
@@ -40,6 +40,8 @@ export interface ProviderSessionEvent {
40
40
  origin?: "chat" | "terminal" | "provider";
41
41
  turnId?: string;
42
42
  label?: string;
43
+ status?: "running" | "completed" | "failed";
44
+ streaming?: boolean;
43
45
  }
44
46
 
45
47
  export interface ProviderConfig {
@@ -7,6 +7,7 @@ import { tmuxCommand, tmuxHasSession } from "agent-relay-sdk/tmux-utils";
7
7
  import { sanitizeFsName } from "agent-relay-sdk/fs-name";
8
8
  import { profileAllowsRelayFeature, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderStatusUpdate, type RunnerSpawnConfig, type SemanticStatus, type SpawnArgs } from "../adapter";
9
9
  import { prepareClaudeProfileHome, profileUsesHostProviderGlobals } from "../profile-home";
10
+ import { relayMcpClaudeConfigArg } from "../relay-mcp";
10
11
  import { claudeProviderMessageText } from "./claude-delivery";
11
12
 
12
13
  export class ClaudeAdapter implements ProviderAdapter {
@@ -203,6 +204,7 @@ export class ClaudeAdapter implements ProviderAdapter {
203
204
  const args = [
204
205
  ...rigPrefix,
205
206
  ...pluginDirs.flatMap((dir) => ["--plugin-dir", dir]),
207
+ ...(profileAllowsRelayFeature(config, "mcp") ? relayMcpClaudeConfigArg(config.relayUrl) : []),
206
208
  ...(profileAllowsRelayFeature(config, "statusLine") ? sessionStatusLineSettingsArgs(defaultArgs, config.providerArgs) : []),
207
209
  ...(config.systemPromptAppend ? ["--append-system-prompt", config.systemPromptAppend] : []),
208
210
  ...providerArgs,
@@ -6,6 +6,7 @@ import { isRecord, stringValue } from "agent-relay-sdk";
6
6
  import { isPidAlive, killPid, waitForPidsExit } from "agent-relay-sdk/process-utils";
7
7
  import { profileAllowsRelayFeature, providerMessageText, RELAY_CONTEXT, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderPermissionDecisionInput, type ProviderSessionEvent, type ProviderStatusUpdate, type RunnerSpawnConfig, type SpawnArgs, type TerminalAttachSpec } from "../adapter";
8
8
  import { workspaceDepsNoteFromEnv } from "../relay-instructions";
9
+ import { relayMcpCodexConfigArgs, tomlString } from "../relay-mcp";
9
10
  import { logger } from "../logger";
10
11
 
11
12
  /** Relay context prepended to a Codex agent's first turn: the standard relay
@@ -36,6 +37,7 @@ export class CodexAdapter implements ProviderAdapter {
36
37
  // Assistant message text accumulated across the current turn's agentMessage items,
37
38
  // flushed as one session response on turn/completed (mirrors Claude's chatCaptureMode).
38
39
  private turnMessages: string[] = [];
40
+ private readonly itemTextBuffers = new Map<string, string>();
39
41
  private captureMode: "final" | "full" = "final";
40
42
 
41
43
  onStatusChange(cb: (status: ProviderStatusUpdate) => void): void {
@@ -257,6 +259,7 @@ export class CodexAdapter implements ProviderAdapter {
257
259
  ...codexModelConfigArgs(config.model, config.effort),
258
260
  ...codexApprovalConfigArgs(config.approvalMode),
259
261
  ...(profileAllowsRelayFeature(config, "skills") ? bundledSkillConfigArgs() : []),
262
+ ...(profileAllowsRelayFeature(config, "mcp") ? relayMcpCodexConfigArgs(config.relayUrl) : []),
260
263
  ...codexManagedConfigArgs(),
261
264
  "--listen",
262
265
  appServerUrl,
@@ -331,6 +334,7 @@ export class CodexAdapter implements ProviderAdapter {
331
334
  const turn = isRecord(params?.turn) ? params.turn : undefined;
332
335
  this.activeTurnId = stringValue(turn?.id);
333
336
  this.turnMessages = [];
337
+ this.itemTextBuffers.clear();
334
338
  this.statusCb({ status: "busy", reason: "provider-turn", id: this.activeTurnId });
335
339
  }
336
340
  }
@@ -347,6 +351,7 @@ export class CodexAdapter implements ProviderAdapter {
347
351
  if ((method.includes("item/completed") || method.includes("item.completed")) && !isSubagent) {
348
352
  this.handleCodexItem(isRecord(params?.item) ? params.item : undefined);
349
353
  }
354
+ if (!isSubagent) this.handleCodexItemDelta(method, params);
350
355
  if (method.includes("thread/status")) {
351
356
  const status = statusType(params?.status);
352
357
  if (threadId && this.subagentThreads.has(threadId)) {
@@ -366,9 +371,11 @@ export class CodexAdapter implements ProviderAdapter {
366
371
  if (!item) return;
367
372
  const type = stringValue(item.type);
368
373
  const turnId = this.activeTurnId;
374
+ const itemId = codexItemId(item);
369
375
  if (type === "agentMessage") {
370
- const text = stringValue(item.text)?.trim();
376
+ const text = (stringValue(item.text) ?? (itemId ? this.itemTextBuffers.get(itemId) : undefined))?.trim();
371
377
  if (text) this.turnMessages.push(text);
378
+ if (itemId) this.itemTextBuffers.delete(itemId);
372
379
  return;
373
380
  }
374
381
  if (type === "userMessage") {
@@ -377,12 +384,39 @@ export class CodexAdapter implements ProviderAdapter {
377
384
  return;
378
385
  }
379
386
  if (type === "reasoning") {
380
- const text = codexReasoningText(item);
387
+ const buffered = itemId ? this.itemTextBuffers.get(itemId) : undefined;
388
+ const text = (codexReasoningText(item) || buffered || "").trim();
381
389
  if (text) this.sessionEventCb({ type: "reasoning", origin: "provider", body: text, ...(turnId ? { turnId } : {}) });
390
+ if (itemId) this.itemTextBuffers.delete(itemId);
382
391
  return;
383
392
  }
384
393
  const tool = codexToolSummary(type, item);
385
- if (tool) this.sessionEventCb({ type: "tool", origin: "provider", body: tool.body, label: tool.label, ...(turnId ? { turnId } : {}) });
394
+ if (tool) this.sessionEventCb({ type: "tool", origin: "provider", body: tool.body, label: tool.label, status: "completed", ...(turnId ? { turnId } : {}) });
395
+ if (itemId) this.itemTextBuffers.delete(itemId);
396
+ }
397
+
398
+ private handleCodexItemDelta(method: string, params: Record<string, unknown> | undefined): void {
399
+ if (!method.includes("item/") && !method.includes("item.")) return;
400
+ const item = isRecord(params?.item) ? params.item : undefined;
401
+ const itemId = codexItemId(params) ?? codexItemId(item);
402
+ const type = codexItemTypeFromMethod(method) ?? stringValue(item?.type);
403
+ const turnId = this.activeTurnId;
404
+
405
+ if (method.includes("/started") || method.includes(".started")) {
406
+ const tool = codexToolSummary(type, item ?? params ?? {});
407
+ if (tool) this.sessionEventCb({ type: "tool", origin: "provider", body: tool.body, label: tool.label, status: "running", streaming: true, ...(turnId ? { turnId } : {}) });
408
+ return;
409
+ }
410
+
411
+ if (type === "agentMessage" || type === "reasoning" || type === "plan") {
412
+ const delta = codexDeltaText(params);
413
+ if (delta && itemId) this.itemTextBuffers.set(itemId, `${this.itemTextBuffers.get(itemId) ?? ""}${delta}`);
414
+ return;
415
+ }
416
+
417
+ // Raw stdout/stderr deltas can be huge. Surface the tool lifecycle via started/completed
418
+ // summaries only; never persist raw process output into the chat mirror.
419
+ if (method.includes("outputDelta") || method.includes("output_delta") || method.includes("progress")) return;
386
420
  }
387
421
 
388
422
  private flushTurnResponse(): void {
@@ -401,6 +435,25 @@ export class CodexAdapter implements ProviderAdapter {
401
435
  }
402
436
  }
403
437
 
438
+ function codexItemId(item: Record<string, unknown> | undefined): string | undefined {
439
+ return stringValue(item?.itemId) ?? stringValue(item?.id);
440
+ }
441
+
442
+ function codexDeltaText(params: Record<string, unknown> | undefined): string {
443
+ const delta = params?.delta;
444
+ if (typeof delta === "string") return delta;
445
+ if (isRecord(delta)) {
446
+ const text = stringValue(delta.text) ?? stringValue(delta.value) ?? stringValue(delta.content);
447
+ if (text) return text;
448
+ }
449
+ return stringValue(params?.text) ?? stringValue(params?.chunk) ?? stringValue(params?.content) ?? "";
450
+ }
451
+
452
+ function codexItemTypeFromMethod(method: string): string | undefined {
453
+ const match = method.match(/item[/.]([^/.]+)[/.]/);
454
+ return match?.[1];
455
+ }
456
+
404
457
  function codexApprovalFromServerRequest(message: { id: string | number; method: string; params?: unknown }): { pending: PendingCodexApproval; view: Record<string, unknown> } | null {
405
458
  if (!isRecord(message.params)) return null;
406
459
  const method = message.method;
@@ -841,6 +894,10 @@ export function bundledSkillConfigArgs(skillDirs = bundledCodexSkillDirs()): str
841
894
  return ["-c", `skills.config=[${skillsConfig}]`];
842
895
  }
843
896
 
897
+ // tomlString now lives in ../relay-mcp (shared with the relay MCP injection args);
898
+ // re-exported here so existing codex.ts consumers/tests keep their import path.
899
+ export { tomlString };
900
+
844
901
  export function codexAppServerConfigArgs(...argLists: string[][]): string[] {
845
902
  const result: string[] = [];
846
903
  for (const args of argLists) {
@@ -860,9 +917,6 @@ export function codexAppServerConfigArgs(...argLists: string[][]): string[] {
860
917
  return result;
861
918
  }
862
919
 
863
- function tomlString(value: string): string {
864
- return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
865
- }
866
920
 
867
921
  async function connectWithRetry(client: CodexAppClient, attempts = 40): Promise<void> {
868
922
  // Give the freshly-spawned app-server a moment to bind its socket before the
@@ -0,0 +1,50 @@
1
+ // Single home for the relay HTTP MCP endpoint descriptor + per-provider injection
2
+ // args. Both adapters (and Stage 2's proxy) import from here so the endpoint path,
3
+ // server name, and token-handling rules live in exactly one place.
4
+ //
5
+ // Token handling: the bearer token is NEVER placed in argv (it would leak via `ps`
6
+ // and the agent's own process inspection). Claude expands `${AGENT_RELAY_TOKEN}`
7
+ // from the env at MCP-config parse time; Codex reads it from the named env var.
8
+
9
+ export const RELAY_MCP_SERVER_NAME = "agent-relay";
10
+ export const RELAY_MCP_PATH = "/api/mcp";
11
+
12
+ export function relayMcpEndpoint(relayUrl: string): string {
13
+ return `${relayUrl.replace(/\/+$/, "")}${RELAY_MCP_PATH}`;
14
+ }
15
+
16
+ // Claude: additive `--mcp-config` JSON (NOT --strict-mcp-config, which would clobber
17
+ // the user's own servers). HTTP transport, token via env-var expansion so it never
18
+ // hits argv. Returns the full ["--mcp-config", "<json>"] arg pair.
19
+ export function relayMcpClaudeConfigArg(relayUrl: string): string[] {
20
+ return [
21
+ "--mcp-config",
22
+ JSON.stringify({
23
+ mcpServers: {
24
+ [RELAY_MCP_SERVER_NAME]: {
25
+ type: "http",
26
+ url: relayMcpEndpoint(relayUrl),
27
+ headers: { Authorization: "Bearer ${AGENT_RELAY_TOKEN}" },
28
+ },
29
+ },
30
+ }),
31
+ ];
32
+ }
33
+
34
+ // Codex: `-c mcp_servers.<name>.*` overrides. `bearer_token_env_var` tells Codex to
35
+ // read the token from the env var itself → transport resolves to streamable_http.
36
+ export function relayMcpCodexConfigArgs(relayUrl: string): string[] {
37
+ const key = `mcp_servers.${RELAY_MCP_SERVER_NAME}`;
38
+ return [
39
+ "-c",
40
+ `${key}.url=${tomlString(relayMcpEndpoint(relayUrl))}`,
41
+ "-c",
42
+ `${key}.bearer_token_env_var=${tomlString("AGENT_RELAY_TOKEN")}`,
43
+ ];
44
+ }
45
+
46
+ // Shared TOML string escaper for Codex `-c key=value` overrides. One home; codex.ts
47
+ // imports this rather than re-declaring it.
48
+ export function tomlString(value: string): string {
49
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
50
+ }
package/src/runner.ts CHANGED
@@ -1158,8 +1158,9 @@ export class AgentRunner {
1158
1158
  return;
1159
1159
  }
1160
1160
  if (event.type === "response") {
1161
- // If a relay message is awaiting the agent's own reply, let the agent answer
1162
- // it (Codex agents reply via their relay skills) instead of double-posting.
1161
+ // Dashboard prompt injection is already answered by this captured App Server
1162
+ // response. Other Relay inbox obligations still belong to the agent's explicit
1163
+ // reply path, so those keep suppressing auto-capture to avoid duplicates.
1163
1164
  let replyToMessageId: number | undefined;
1164
1165
  const pendingPrompt = this.pendingPromptMessageId;
1165
1166
  if (pendingPrompt) {
@@ -1176,6 +1177,7 @@ export class AgentRunner {
1176
1177
  ...(replyToMessageId ? { replyTo: replyToMessageId } : {}),
1177
1178
  session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}) },
1178
1179
  });
1180
+ if (replyToMessageId) this.obligationCache.markDirty();
1179
1181
  return;
1180
1182
  }
1181
1183
  if (this.options.providerConfig.reasoningCapture === false) return;
@@ -1183,7 +1185,14 @@ export class AgentRunner {
1183
1185
  from: this.agentId,
1184
1186
  to: "user",
1185
1187
  body,
1186
- session: { type: event.type, origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}), ...(event.label ? { label: event.label } : {}) },
1188
+ session: {
1189
+ type: event.type,
1190
+ origin: event.origin ?? "provider",
1191
+ ...(turnId ? { turnId } : {}),
1192
+ ...(event.label ? { label: event.label } : {}),
1193
+ ...(event.status ? { status: event.status } : {}),
1194
+ ...(event.streaming !== undefined ? { streaming: event.streaming } : {}),
1195
+ },
1187
1196
  });
1188
1197
  }
1189
1198