agent-relay-runner 0.73.0 → 0.74.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.73.0",
3
+ "version": "0.74.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.48"
23
+ "agent-relay-sdk": "0.2.49"
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.73.0",
4
+ "version": "0.74.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -0,0 +1,56 @@
1
+ type EmitAgentMessage = (body: string) => void;
2
+
3
+ export class CodexAgentMessageCapture {
4
+ private segments: string[][] = [];
5
+ private currentSegment: string[] = [];
6
+ private readonly buffers = new Map<string, string>();
7
+ private readonly flushedLengths = new Map<string, number>();
8
+
9
+ reset(): void {
10
+ this.segments = [];
11
+ this.currentSegment = [];
12
+ this.buffers.clear();
13
+ this.flushedLengths.clear();
14
+ }
15
+
16
+ appendDelta(itemId: string | undefined, delta: string): void {
17
+ if (!itemId || !delta) return;
18
+ this.buffers.set(itemId, `${this.buffers.get(itemId) ?? ""}${delta}`);
19
+ }
20
+
21
+ completeItem(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage): void {
22
+ this.flushText(itemId, text ?? (itemId ? this.buffers.get(itemId) : undefined), emit);
23
+ if (!itemId) return;
24
+ this.buffers.delete(itemId);
25
+ this.flushedLengths.delete(itemId);
26
+ }
27
+
28
+ closeSegment(emit: EmitAgentMessage): void {
29
+ this.flushPending(emit);
30
+ if (!this.currentSegment.length) return;
31
+ this.segments.push(this.currentSegment);
32
+ this.currentSegment = [];
33
+ }
34
+
35
+ finish(emit: EmitAgentMessage): string | undefined {
36
+ this.closeSegment(emit);
37
+ const finalSegment = this.segments.at(-1);
38
+ this.segments = [];
39
+ return finalSegment?.length ? finalSegment.join("\n\n").trim() : undefined;
40
+ }
41
+
42
+ private flushPending(emit: EmitAgentMessage): void {
43
+ for (const [itemId, text] of [...this.buffers.entries()]) this.flushText(itemId, text, emit);
44
+ }
45
+
46
+ private flushText(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage): void {
47
+ if (!text) return;
48
+ const offset = itemId ? (this.flushedLengths.get(itemId) ?? 0) : 0;
49
+ const chunk = text.slice(offset);
50
+ if (itemId) this.flushedLengths.set(itemId, text.length);
51
+ const body = chunk.trim();
52
+ if (!body) return;
53
+ this.currentSegment.push(body);
54
+ emit(body);
55
+ }
56
+ }
@@ -10,6 +10,7 @@ import { tomlString } from "../relay-mcp";
10
10
  import { assembleLaunch, bundledCodexSkillDirs, bundledSkillConfigArgs, materializeLaunchAssembly } from "../launch-assembly";
11
11
  import { logger } from "../logger";
12
12
  import type { SessionEvent } from "../session-insights";
13
+ import { CodexAgentMessageCapture } from "./codex-agent-message-capture";
13
14
 
14
15
  /** Relay context prepended to a Codex agent's first turn: the standard relay
15
16
  * blurb plus, when running in an isolated workspace, the deps caveat (#159). */
@@ -49,12 +50,9 @@ export class CodexAdapter implements ProviderAdapter {
49
50
  // Active turn id for the main thread, captured from turn/started so an interrupt
50
51
  // can target the in-flight turn. Cleared on turn/completed.
51
52
  private activeTurnId?: string;
52
- // Assistant message text accumulated across the current turn's agentMessage items,
53
- // flushed as one session response on turn/completed (mirrors Claude's chatCaptureMode).
54
- private turnMessages: string[] = [];
53
+ private readonly agentMessageCapture = new CodexAgentMessageCapture();
55
54
  private readonly itemTextBuffers = new Map<string, string>();
56
55
  private readonly itemTextBufferTypes = new Map<string, string>();
57
- private captureMode: "final" | "full" = "final";
58
56
  // #183/#184: the normalized session-event log for the current process lifetime, fed
59
57
  // from the same completed-item stream that drives the chat mirror. The runner slices
60
58
  // this per-segment (since the last compact/clear/restart) via its own cursor, so we
@@ -81,7 +79,7 @@ export class CodexAdapter implements ProviderAdapter {
81
79
  this.subagentThreads.clear();
82
80
  this.pendingApprovals.clear();
83
81
  this.activeTurnId = undefined;
84
- this.turnMessages = [];
82
+ this.agentMessageCapture.reset();
85
83
  this.itemTextBuffers.clear();
86
84
  this.itemTextBufferTypes.clear();
87
85
  }
@@ -133,7 +131,6 @@ export class CodexAdapter implements ProviderAdapter {
133
131
 
134
132
  async spawn(config: RunnerSpawnConfig): Promise<ManagedProcess> {
135
133
  this.resetProcessState();
136
- this.captureMode = (config.providerConfig as ProviderConfig).chatCaptureMode ?? "final";
137
134
  const args = this.buildSpawnArgs(config, config.providerConfig as ProviderConfig);
138
135
  const appServer = Bun.spawn([args.command, ...args.args], {
139
136
  cwd: args.cwd,
@@ -459,8 +456,9 @@ export class CodexAdapter implements ProviderAdapter {
459
456
  } else {
460
457
  const turn = isRecord(params?.turn) ? params.turn : undefined;
461
458
  this.activeTurnId = stringValue(turn?.id);
462
- this.turnMessages = [];
459
+ this.agentMessageCapture.reset();
463
460
  this.itemTextBuffers.clear();
461
+ this.itemTextBufferTypes.clear();
464
462
  this.statusCb({ status: "busy", reason: "provider-turn", id: this.activeTurnId });
465
463
  }
466
464
  }
@@ -492,29 +490,21 @@ export class CodexAdapter implements ProviderAdapter {
492
490
  }
493
491
  }
494
492
 
495
- // Turn one completed Codex thread item into a session-mirror event. agentMessage
496
- // text is accumulated and flushed as a single response on turn/completed; the rest
497
- // (user prompt echo, reasoning, tool steps) is surfaced as it lands.
493
+ // Turn one completed Codex thread item into a session-mirror event. agentMessage text is
494
+ // mirrored as narration as it lands; only the final contiguous assistant segment is repeated
495
+ // as the turn-final response. The rest (user prompt echo, reasoning, tool steps) is surfaced
496
+ // as it lands.
498
497
  private handleCodexItem(item: Record<string, unknown> | undefined): void {
499
498
  if (!item) return;
500
499
  const type = stringValue(item.type);
501
500
  const turnId = this.activeTurnId;
502
501
  const itemId = codexItemId(item);
502
+ if (type !== "agentMessage") this.agentMessageCapture.closeSegment((body) => this.emitAgentNarration(body));
503
503
  // A completed non-reasoning item ends the reasoning segment that preceded it — flush the
504
504
  // buffered reasoning first so it lands in transcript order, ahead of this tool/message.
505
505
  if (type !== "reasoning") this.flushBufferedReasoning();
506
506
  if (type === "agentMessage") {
507
- const text = (stringValue(item.text) ?? (itemId ? this.itemTextBuffers.get(itemId) : undefined))?.trim();
508
- if (text) {
509
- this.turnMessages.push(text);
510
- this.recordInsightEvent({ type: "turn" }); // a substantive assistant turn
511
- // Stream the assistant text into the trace as narration (Claude parity). The closing
512
- // response bubble at turn end repeats the final block; the dashboard suppresses the
513
- // matching narration so it isn't shown twice.
514
- this.sessionEventCb({ type: "narration", origin: "provider", body: text, ...(turnId ? { turnId } : {}) });
515
- }
516
- if (itemId) this.itemTextBuffers.delete(itemId);
517
- if (itemId) this.itemTextBufferTypes.delete(itemId);
507
+ this.agentMessageCapture.completeItem(itemId, stringValue(item.text), (body) => this.emitAgentNarration(body));
518
508
  return;
519
509
  }
520
510
  if (type === "userMessage") {
@@ -549,6 +539,14 @@ export class CodexAdapter implements ProviderAdapter {
549
539
  if (itemId) this.itemTextBufferTypes.delete(itemId);
550
540
  }
551
541
 
542
+ private emitAgentNarration(body: string): void {
543
+ this.recordInsightEvent({ type: "turn" }); // a substantive assistant turn
544
+ // Stream the assistant text into the trace as narration (Claude parity). The closing
545
+ // response bubble at turn end repeats the final block; the dashboard suppresses the
546
+ // matching narration so it isn't shown twice.
547
+ this.sessionEventCb({ type: "narration", origin: "provider", body, ...(this.activeTurnId ? { turnId: this.activeTurnId } : {}) });
548
+ }
549
+
552
550
  // Emit any buffered reasoning as discrete trace blocks (appended, in transcript order). Codex
553
551
  // streams reasoning as deltas with no reliable completion event, so we flush coarsely at item
554
552
  // and turn boundaries — the signal the Claude reasoning tail uses — never a codex-only timer.
@@ -588,6 +586,7 @@ export class CodexAdapter implements ProviderAdapter {
588
586
  const turnId = this.activeTurnId;
589
587
 
590
588
  if (method.includes("/started") || method.includes(".started")) {
589
+ if (type !== "agentMessage") this.agentMessageCapture.closeSegment((body) => this.emitAgentNarration(body));
591
590
  // A new item starting ends the prior reasoning segment — flush it ahead of this step.
592
591
  this.flushBufferedReasoning();
593
592
  const tool = codexToolSummary(type, item ?? params ?? {});
@@ -596,7 +595,14 @@ export class CodexAdapter implements ProviderAdapter {
596
595
  return;
597
596
  }
598
597
 
599
- if (type === "agentMessage" || type === "reasoning" || type === "plan") {
598
+ if (type === "agentMessage") {
599
+ const delta = codexDeltaText(params);
600
+ if (delta) this.agentMessageCapture.appendDelta(itemId, delta);
601
+ return;
602
+ }
603
+
604
+ if (type === "reasoning" || type === "plan") {
605
+ this.agentMessageCapture.closeSegment((body) => this.emitAgentNarration(body));
600
606
  const delta = codexDeltaText(params);
601
607
  if (delta && itemId) {
602
608
  this.itemTextBuffers.set(itemId, `${this.itemTextBuffers.get(itemId) ?? ""}${delta}`);
@@ -611,15 +617,7 @@ export class CodexAdapter implements ProviderAdapter {
611
617
  }
612
618
 
613
619
  private flushTurnResponse(): void {
614
- const pendingAgentMessages = [...this.itemTextBuffers.entries()]
615
- .filter(([itemId]) => this.itemTextBufferTypes.get(itemId) === "agentMessage")
616
- .map(([, text]) => text.trim())
617
- .filter(Boolean);
618
- const messages = [...this.turnMessages, ...pendingAgentMessages];
619
- if (!messages.length) return;
620
- const joined = this.captureMode === "full" ? messages.join("\n\n") : messages[messages.length - 1]!;
621
- this.turnMessages = [];
622
- const text = joined.trim();
620
+ const text = this.agentMessageCapture.finish((body) => this.emitAgentNarration(body));
623
621
  if (text) this.sessionEventCb({ type: "response", origin: "provider", body: text, final: true, ...(this.activeTurnId ? { turnId: this.activeTurnId } : {}) });
624
622
  }
625
623
 
@@ -631,6 +629,7 @@ export class CodexAdapter implements ProviderAdapter {
631
629
  this.pendingApprovals.clear();
632
630
  this.itemTextBuffers.clear();
633
631
  this.itemTextBufferTypes.clear();
632
+ this.agentMessageCapture.reset();
634
633
  this.statusCb({ status: "idle", reason: "provider-turn", id: turnId });
635
634
  }
636
635