agent-relay-runner 0.27.1 → 0.28.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.27.1",
3
+ "version": "0.28.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.16"
23
+ "agent-relay-sdk": "0.2.17"
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.27.1",
4
+ "version": "0.28.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/adapter.ts CHANGED
@@ -211,9 +211,19 @@ function isPersistedRelayMessage(message: Message): boolean {
211
211
  return Number.isSafeInteger(message.id) && message.id > 0;
212
212
  }
213
213
 
214
+ // #283 — one-line nudge that replaces the reply-scaffold footer for notification-class
215
+ // (replyExpected:false) messages. Deliberately tiny so a bloated context can't drown the
216
+ // no-reply rule established at session start. Shared with the Claude delivery path.
217
+ export const NOTIFICATION_NUDGE = "↪ Notification — no reply needed.";
218
+
219
+ // A notification is a persisted message the server marked replyExpected:false.
220
+ export function isNotificationMessage(message: Message): boolean {
221
+ return isPersistedRelayMessage(message) && message.replyExpected === false;
222
+ }
223
+
214
224
  function latestReplyableMessage(messages: Message[]): Message | undefined {
215
225
  return messages
216
- .filter((message) => isPersistedRelayMessage(message) && !isMemoryInjection(message) && !isReactionNotification(message))
226
+ .filter((message) => isPersistedRelayMessage(message) && !isMemoryInjection(message) && !isReactionNotification(message) && message.replyExpected !== false)
217
227
  .at(-1);
218
228
  }
219
229
 
@@ -316,6 +326,9 @@ export function providerMessageText(messages: Message[]): string {
316
326
  "If you already delivered the useful response through Relay, do not send a separate status-only confirmation.",
317
327
  "If multiple messages arrived together, cover them in one reply instead of answering each line separately.",
318
328
  ].join("\n"));
329
+ } else if (messages.some(isNotificationMessage)) {
330
+ // #283 — pure notification batch: no scaffold, just the one-line no-reply nudge.
331
+ sections.push(NOTIFICATION_NUDGE);
319
332
  }
320
333
  return sections.join("\n\n");
321
334
  }
@@ -1,6 +1,6 @@
1
1
  import type { Message } from "agent-relay-sdk";
2
2
  import { isRecord } from "agent-relay-sdk";
3
- import { providerAttachmentText } from "../adapter";
3
+ import { isNotificationMessage, NOTIFICATION_NUDGE, providerAttachmentText } from "../adapter";
4
4
 
5
5
  const PROVIDER_MESSAGE_BODY_PREVIEW_CHARS = 4000;
6
6
  const REMINDER_EVERY_DELIVERIES = 5;
@@ -61,7 +61,7 @@ function shouldShowReplyReminder(deliveryCount: number): boolean {
61
61
 
62
62
  function latestReplyableMessage(messages: Message[]): Message | undefined {
63
63
  return messages
64
- .filter((message) => isPersistedRelayMessage(message) && !isMemoryInjection(message) && !isReactionNotification(message))
64
+ .filter((message) => isPersistedRelayMessage(message) && !isMemoryInjection(message) && !isReactionNotification(message) && message.replyExpected !== false)
65
65
  .at(-1);
66
66
  }
67
67
 
@@ -121,9 +121,13 @@ export function claudeProviderMessageText(messages: Message[], options: ClaudeDe
121
121
  const relaySurface = options.relaySurface !== false;
122
122
  const sections = messages.map((message) => formatMessage(message, relaySurface));
123
123
  const replyable = latestReplyableMessage(messages);
124
- // Isolated agents have no way to reply through Relay — never append the reminder.
124
+ // Isolated agents have no way to reply through Relay — never append the reminder/nudge.
125
125
  if (relaySurface && replyable && shouldShowReplyReminder(options.deliveryCount)) {
126
126
  sections.push(replyReminder(replyable, options.readOnly === true));
127
+ } else if (relaySurface && !replyable && messages.some(isNotificationMessage)) {
128
+ // #283 — pure notification batch (no message wants a reply): drop the scaffold, append the
129
+ // one-line nudge so a long context can't make the agent forget the session-start no-reply rule.
130
+ sections.push(NOTIFICATION_NUDGE);
127
131
  }
128
132
  return sections.join("\n\n");
129
133
  }
@@ -22,7 +22,7 @@ interface TranscriptBlock {
22
22
  }
23
23
 
24
24
  export interface TurnStep {
25
- type: "reasoning" | "tool";
25
+ type: "narration" | "reasoning" | "tool";
26
26
  text: string;
27
27
  label?: string;
28
28
  }
@@ -147,10 +147,12 @@ export function extractFinalAssistantMessage(jsonl: string): string {
147
147
  * Thinking and tool_use blocks are dropped, matching extractLastAssistantTurn.
148
148
  */
149
149
  /**
150
- * Extract the ordered reasoning and tool steps for the most recent turn (since
151
- * the last real user prompt). Used by the reasoning tailer to stream discreet
152
- * progress into chat while a turn is in flight. Returns steps in transcript order
153
- * so the tailer can emit only the ones it hasn't seen yet by index.
150
+ * Extract the ordered narration, reasoning, and tool steps for the most recent
151
+ * turn (since the last real user prompt). Used by the reasoning tailer to stream
152
+ * progress into chat while a turn is in flight. `narration` is the assistant's
153
+ * intermediate `text` between tool calls (the terminal's `●` lines); it is the
154
+ * primary, default-visible turn content. Returns steps in transcript order so the
155
+ * tailer can emit only the ones it hasn't seen yet.
154
156
  */
155
157
  export function extractLatestTurnSteps(jsonl: string): TurnStep[] {
156
158
  const lines = jsonl.split("\n");
@@ -170,7 +172,9 @@ export function extractLatestTurnSteps(jsonl: string): TurnStep[] {
170
172
  }
171
173
  if (entry.type !== "assistant") continue;
172
174
  for (const b of blocks(entry.message)) {
173
- if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.trim()) {
175
+ if (b.type === "text" && typeof b.text === "string" && b.text.trim()) {
176
+ steps.push({ type: "narration", text: b.text.trim() });
177
+ } else if (b.type === "thinking" && typeof b.thinking === "string" && b.thinking.trim()) {
174
178
  steps.push({ type: "reasoning", text: b.thinking.trim() });
175
179
  } else if (b.type === "tool_use" && typeof b.name === "string" && b.name) {
176
180
  steps.push({ type: "tool", label: b.name, text: summarizeToolUse(b.name, b.input) });
@@ -8,6 +8,7 @@ export const CLAUDE_RELAY_MANUAL = `# Agent Relay
8
8
  - If multiple Relay messages arrive together, answer once to the latest relevant message and cover the current request. Do not separately acknowledge stale greetings or context.
9
9
  - If the useful response was already delivered through Relay, do not send an extra "sent", "done", or "drafts sent" confirmation unless the user explicitly asked for one.
10
10
  - No reply is needed for pure info messages, passive acknowledgements, or reactions that do not ask for action.
11
+ - NEVER reply to a notification-class message. The server marks these and renders a single \`↪ Notification — no reply needed.\` line instead of the reply reminder — it is a fire-and-forget signal (a merge notice, lifecycle event, or FYI). Act on the information if relevant, but do not send any reply, status confirmation, or reaction back.
11
12
  - Use \`agent-relay /react <messageId> <emoji>\` instead of a text reply for lightweight acknowledgement, approval, thanks, or "good job" after a completed work update.
12
13
  - Good reaction uses: acknowledge praise with 👍 or ❤️, mark a completed handoff as seen, approve a proposed next step, or acknowledge a passive FYI.
13
14
  - Do not use reactions when the user asked a question, gave a new task, reported a bug, or needs a textual result.
package/src/runner.ts CHANGED
@@ -1481,10 +1481,12 @@ export class AgentRunner {
1481
1481
  }, INTERRUPT_RECONCILE_DELAY_MS);
1482
1482
  }
1483
1483
 
1484
- // --- Reasoning tailer (item 5) ------------------------------------------------------
1485
- // Tail the in-flight turn's Claude transcript and surface new reasoning/tool steps
1486
- // as discreet session events. Coalesced and coarse; the final response still comes
1487
- // through publishSessionTurn.
1484
+ // --- Turn-step tailer (item 5) ------------------------------------------------------
1485
+ // Tail the in-flight turn's Claude transcript and surface new narration/reasoning/tool
1486
+ // steps as session events, in transcript order. `narration` (the agent's intermediate
1487
+ // text) is the primary visible content; reasoning visibility is a client-side toggle.
1488
+ // Coalesced and coarse; the final response still comes through publishSessionTurn.
1489
+ // `reasoningCapture: false` disables the whole live trace (server-side kill switch).
1488
1490
  private startReasoningTail(transcriptPath: string): void {
1489
1491
  if (this.options.providerConfig.reasoningCapture === false) return;
1490
1492
  this.stopReasoningTail();