Skip to content

feat(delivery): relaycast target — agents reply over the relay (#254 pt.1)#255

Merged
kjgbot merged 2 commits into
mainfrom
feat/delivery-relaycast-target
Jun 25, 2026
Merged

feat(delivery): relaycast target — agents reply over the relay (#254 pt.1)#255
kjgbot merged 2 commits into
mainfrom
feat/delivery-relaycast-target

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jun 25, 2026

Copy link
Copy Markdown
Member

First piece of #254 (platform-level relay reply). Makes @agentworkforce/delivery able to reply over the relay, so agent-to-agent chat works without per-persona code.

What

  • Adds relaycast as a first-class delivery target next to slack/telegram. The existing "reply to origin transport" pattern (hn-monitor, inbox-buddy, joke-bot) now covers relay automatically.
  • Event-driven, not config-driven: slack/telegram come from persona inputs (SLACK_CHANNEL/TELEGRAM_CHAT); the relaycast reply address is the inbound message's sender, passed via transports.relaycast = { to, sender? }. resolveDeliveryTargets(ctx) stays slack/telegram-only, so existing callers are unchanged — relaycast only becomes a target when an address is supplied.
  • Default sender DMs the peer via POST /v1/dm with the box's injected RELAY_API_KEY (never throws — {ok:false} + log on failure).

Base URL — easy to change (per the cutover to cast.agentrelay.com)

Single source of truth in relaycast.ts:

export const DEFAULT_RELAYCAST_URL = 'https://cast.agentrelay.com';
// resolveRelaycastUrl(): RELAYCAST_URL > RELAY_BASE_URL > default

Change one constant (or set one env var) to move the gateway. Default is now cast.agentrelay.com.

Files

  • types.tsRelaycastRef, RelaycastSender, RelaycastTarget, DeliveryProvider; widened MessageRef / DeliveryClient.targets / onlyTargets.
  • relaycast.ts (new) — DEFAULT_RELAYCAST_URL, resolveRelaycastUrl, defaultRelaycastSender.
  • delivery.ts — relaycast target wiring + sendRelaycast.
  • relaycast.test.ts (new) — URL precedence + relaycast delivery path. 6/6 green, typecheck clean.

Not in this PR (tracked in #254)

  • runtime ctx.relay.reply() + surface the inbound sender on the relay event (so callers can populate to ergonomically)
  • cloud: default inbox_selectors to ['@self'] (relay-on by default)
  • publish @agentworkforce/delivery, consume in the agents repo, drop hn-monitor's slack-only relay fallback

Open question (flag for #254 acceptance)

The cloud-minted workspace key currently authenticates on api.relaycast.dev, while this PR defaults to cast.agentrelay.com and cloud infra injects gateway.relaycast.dev. The env override makes this configurable, but we should converge the box's injected base URL + key onto one gateway before the round-trip works in prod.

Refs #254

🤖 Generated with Claude Code

Review in cubic

…254, part 1)

Adds `relaycast` as a first-class delivery target alongside slack/telegram, so
the existing "reply to origin transport" pattern (hn-monitor/inbox-buddy/joke-bot)
covers agent-to-agent relay replies with no per-persona code.

Unlike slack/telegram (config-driven via persona inputs), the relaycast reply is
EVENT-driven: the `to` address is the inbound message's sender, supplied by the
caller via `transports.relaycast = { to, sender? }`. `resolveDeliveryTargets`
stays slack/telegram-only; relaycast is added in createDelivery when an address
is present, so existing callers are unaffected.

The default sender DMs the peer via `POST /v1/dm` with the box's injected
RELAY_API_KEY. Base URL is a single source of truth (`DEFAULT_RELAYCAST_URL =
https://cast.agentrelay.com`) overridable per-env via `RELAYCAST_URL` >
`RELAY_BASE_URL` — easy to change as the gateway cutover settles.

- types: `RelaycastRef`, `RelaycastSender`, `RelaycastTarget`, `DeliveryProvider`;
  widen `MessageRef`/`DeliveryClient.targets`/`onlyTargets` to include relaycast
- new `relaycast.ts`: `DEFAULT_RELAYCAST_URL`, `resolveRelaycastUrl`, `defaultRelaycastSender`
- tests: URL precedence + relaycast delivery path (6/6 green), typecheck clean

Next (separate): runtime `ctx.relay.reply()` + surface inbound sender on the relay
event; cloud default `inbox_selectors` to @self; then publish + consume in agents
and drop hn-monitor's slack-only relay fallback.

Refs #254

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@khaliqgant, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 47 minutes and 37 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04ae7d88-5706-460a-a562-6ad042f3e562

📥 Commits

Reviewing files that changed from the base of the PR and between 2e8a624 and 61db702.

📒 Files selected for processing (3)
  • packages/delivery/src/delivery.ts
  • packages/delivery/src/relaycast.test.ts
  • packages/delivery/src/relaycast.ts
📝 Walkthrough

Walkthrough

Relaycast is added as a delivery target with new public types, URL resolution, and a default sender. Delivery creation now discovers relaycast, builds its transport config, and send dispatch records relaycast refs and failures. Tests cover URL selection, target filtering, and send behavior.

Changes

Relaycast contracts and helper

Layer / File(s) Summary
Relaycast types, helper, and exports
packages/delivery/src/types.ts, packages/delivery/src/relaycast.ts, packages/delivery/src/index.ts
DeliveryProvider, RelaycastRef, RelaycastSender, and RelaycastTarget are added, relaycast URL resolution and the default sender are implemented, and the package re-exports the new relaycast APIs.
Relaycast helper tests
packages/delivery/src/relaycast.test.ts
Tests cover the relaycast default URL, environment-based URL resolution, and the relaycast sender setup around those helpers.

Relaycast delivery target

Layer / File(s) Summary
Delivery target wiring
packages/delivery/src/delivery.ts
createDelivery now accepts DeliveryProvider targets, includes relaycast when configured, and passes relaycast transport settings into DeliveryClientImpl.
Relaycast send dispatch
packages/delivery/src/delivery.ts, packages/delivery/src/relaycast.test.ts
DeliveryClientImpl.send now tracks RelaycastRef, dispatches a relaycast send branch, and tests cover relaycast target discovery, onlyTargets filtering, successful relaycast sends, and relaycast-only failures.

Sequence Diagram(s)

sequenceDiagram
  participant createDelivery
  participant DeliveryClientImpl
  participant defaultRelaycastSender
  participant RelaycastDMEndpoint
  createDelivery->>DeliveryClientImpl: pass relaycast { to, sender }
  DeliveryClientImpl->>defaultRelaycastSender: dm(to, text)
  defaultRelaycastSender->>RelaycastDMEndpoint: POST /v1/dm with bearer auth
  RelaycastDMEndpoint-->>defaultRelaycastSender: ok / messageId
  defaultRelaycastSender-->>DeliveryClientImpl: RelaycastRef or { ok: false }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • AgentWorkforce/workforce issue 254 — Matches the added relaycast target, type widening, and send-path updates in the delivery package.
  • AgentWorkforce/cloud issue 2376 — Matches the relaycast reply/writeback path implemented through the new relaycast sender and send branch.

Possibly related PRs

Poem

A rabbit hopped with a relaycast grin,
New targets to send and new refs tucked in.
Through env vars and DMs the messages leap,
Boing! went the bunny, then off to sleep.
🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Clearly summarizes adding relaycast as a delivery target for agent replies.
Description check ✅ Passed The description matches the changes and accurately describes relaycast support, URL handling, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/delivery-relaycast-target

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new event-driven relaycast delivery provider for agent-to-agent communication, alongside existing Slack and Telegram providers. It includes the implementation of the Relaycast sender, configuration resolution, type updates, and comprehensive unit tests. The feedback suggests utilizing the project's existing fetchWithTimeout helper instead of the global fetch when sending DMs to prevent potential hanging requests.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1 to +2
import type { WorkforceCtx } from '@agentworkforce/runtime';
import type { RelaycastSender } from './types.js';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import 'fetchWithTimeout' from './helpers.js' to enable making the relaycast HTTP request with a timeout, preventing potential hanging.

Suggested change
import type { WorkforceCtx } from '@agentworkforce/runtime';
import type { RelaycastSender } from './types.js';
import type { WorkforceCtx } from '@agentworkforce/runtime';
import type { RelaycastSender } from './types.js';
import { fetchWithTimeout } from './helpers.js';

Comment thread packages/delivery/src/relaycast.ts Outdated
Comment on lines +38 to +46
const res = await fetch(`${baseUrl}/v1/dm`, {
method: 'POST',
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
body: JSON.stringify({ to, text })
});
if (!res.ok) {
ctx.log?.('warn', 'delivery.relaycast.send-failed', { to, status: res.status });
return { ok: false };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use 'fetchWithTimeout' instead of the global 'fetch' to prevent the request from hanging indefinitely if the relaycast gateway is unresponsive. This aligns with the project's existing pattern of using timeouts for external network requests.

Suggested change
const res = await fetch(`${baseUrl}/v1/dm`, {
method: 'POST',
headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
body: JSON.stringify({ to, text })
});
if (!res.ok) {
ctx.log?.('warn', 'delivery.relaycast.send-failed', { to, status: res.status });
return { ok: false };
}
const res = await fetchWithTimeout(baseUrl + '/v1/dm', {
method: 'POST',
headers: { authorization: 'Bearer ' + apiKey, 'content-type': 'application/json' },
body: JSON.stringify({ to, text })
});
if (!res) {
ctx.log?.('warn', 'delivery.relaycast.send-failed', { to, status: 'timeout or network error' });
return { ok: false };
}
if (!res.ok) {
ctx.log?.('warn', 'delivery.relaycast.send-failed', { to, status: res.status });
return { ok: false };
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e8a6245bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/delivery/src/relaycast.ts Outdated
* rather than crashing the handler.
*/
export function defaultRelaycastSender(ctx: WorkforceCtx): RelaycastSender {
const apiKey = process.env.RELAY_API_KEY?.trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use an agent token for relaycast DMs

This default sender captures RELAY_API_KEY as the bearer for POST /v1/dm, but the official Relaycast docs distinguish the workspace key (RELAY_API_KEY) from the agent token (RELAY_AGENT_TOKEN) and list DMs among commands that act as an agent; the OpenAPI /dm endpoint is secured with agentToken (README, OpenAPI). In real agent boxes this means the default relay reply is authenticated with the workspace key rather than the sender's agent identity, so replies over relaycast will be rejected unless tests inject a fake sender.

Useful? React with 👍 / 👎.

Comment thread packages/delivery/src/relaycast.ts Outdated
Comment on lines +47 to +50
const data = (await res.json().catch(() => null)) as
| { message?: { id?: unknown }; messageId?: unknown; id?: unknown }
| null;
const rawId = data?.message?.id ?? data?.messageId ?? data?.id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Unwrap Relaycast success responses before reading the id

Relaycast REST responses are wrapped as { ok: true, data: ... }, and the /dm response's data contains the DM message/legacy id fields (OpenAPI). This parser only looks for message, messageId, or id at the top level, so a successful real DM returns a RelaycastRef with messageId: '', breaking callers that rely on the delivered message id.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/delivery/src/delivery.ts`:
- Around line 135-142: The relaycast branch in DeliveryClient.publish() still
performs a blocking sendRelaycast() call, which violates the non-blocking
contract defined by DeliveryClient.publish() in the types. Update the publish
flow around the sendRelaycast() path so relaycast is either routed through a
true non-blocking implementation or excluded from publish()/nonBlocking handling
until that exists, and make sure the behavior stays consistent with the existing
task/refs/errors pattern in delivery.ts.
- Around line 244-252: The sendRelaycast flow in delivery.ts is treating an ok
response with a missing messageId as a successful receipt by returning a
RelaycastRef with an empty id. Update sendRelaycast to require a real messageId
from rc.sender.dm: if res.ok is false or messageId is missing/empty, log the
warning and return null instead of constructing a ref. Keep the existing
sendRelaycast and RelaycastRef logic aligned with the Slack/Telegram delivery
behavior so only valid receipts are reported as success.

In `@packages/delivery/src/relaycast.ts`:
- Around line 37-42: The relaycast send/publish path currently uses an unbounded
fetch in the relaycast module, so it can hang indefinitely when the relay
gateway stalls. Update the relaycast request logic in the send/publish flow to
use the same timeout-and-fallback pattern used for bounded delivery writes in
delivery.ts, and ensure failures resolve to a graceful { ok: false } result
instead of blocking. Use the relaycast send/publish function and its fetch call
as the place to apply the timeout wrapper and fallback handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c6325808-6746-4830-80c3-c0e097ba8b6c

📥 Commits

Reviewing files that changed from the base of the PR and between 8d890d1 and 2e8a624.

📒 Files selected for processing (5)
  • packages/delivery/src/delivery.ts
  • packages/delivery/src/index.ts
  • packages/delivery/src/relaycast.test.ts
  • packages/delivery/src/relaycast.ts
  • packages/delivery/src/types.ts

Comment thread packages/delivery/src/delivery.ts
Comment thread packages/delivery/src/delivery.ts Outdated
Comment thread packages/delivery/src/relaycast.ts Outdated
@agent-relay-code

Copy link
Copy Markdown
Contributor

⚠️ pr-reviewer did not push — the PR branch advanced during the review, so fixes were withheld to avoid overwriting newer commits. Re-trigger the review once the branch settles. The notes below are advisory and were not pushed.

Review: PR #255 — feat(delivery): add relaycast target

What the PR does

Adds a third delivery transport, relaycast (agent-to-agent DM over the relay), to @agentworkforce/delivery. It is event-driven rather than config-driven: it only becomes a target when the caller supplies transports.relaycast.to (the inbound sender's address). New relaycast.ts provides defaultRelaycastSender (POST /v1/dm with RELAY_API_KEY) and resolveRelaycastUrl (RELAYCAST_URL > RELAY_BASE_URL > default). The provider union is widened 'slack' | 'telegram'DeliveryProvider = 'slack' | 'telegram' | 'relaycast', and MessageRef gains RelaycastRef.

Correctness assessment

  • Type widening is contained. No package outside packages/delivery imports from @agentworkforce/delivery (verified by repo-wide trace). The dispatch in send() uses independent if (target === ...) blocks (delivery.ts:121/128/135), not an exhaustive switch/never, so adding 'relaycast' breaks no narrowing or exhaustiveness check.
  • resolveDeliveryTargets keeps its narrow Array<'slack'|'telegram'> return and is spread into a DeliveryProvider[] (delivery.ts:46) — a safe widening; relaycast is appended separately. Correct by design.
  • Fail-closed semantics preserved. defaultRelaycastSender returns { ok: false } (never throws) on missing key / non-2xx / network error; sendRelaycast maps !ok to a null ref (logged), and the all-targets-failed path still throws. No success/default was introduced for a failure state.
  • No lifecycle/reaper/dispatch/broker code is touched. No lint/format/typo issues found. Nothing to auto-fix.

CI / verification note (important)

I installed deps and ran the delivery build (tsc) and tests. The build passed. Two tests failed in this sandbox only:

  • relaycast target DMs the inbound sender... → got ['slack','relaycast'], expected ['relaycast']
  • relaycast-only send failure surfaces... → expected rejection, none thrown

Root cause is test-environment leakage, not a PR defect: this reviewer sandbox exports SLACK_CHANNEL=C0ALQ06AAUT. resolveDeliveryTargets calls input(ctx, 'SLACK_CHANNEL'), which resolves ctx → env → default (per the documented contract in types.ts and the Explore trace of helpers.ts). The tests' makeCtx({}) has empty inputs, so they fall through to the ambient SLACK_CHANNEL, adding 'slack' as an unexpected target. In a clean CI env (no SLACK_CHANNEL) these tests pass. I did not modify the tests (that would mask, not fix). If the team wants these tests hermetic, a human should add env isolation (e.g. delete SLACK_CHANNEL/TELEGRAM_CHAT in test setup) — flagged as advisory, no code change made.

Note: the working tree at /home/daytona/workspace accessible to shell tooling drifted to an unrelated commit (909afcd) mid-run with no packages/delivery, so the green tsc build is the reliable signal; the test failures above are fully explained by the env var and are not actionable in the PR code. I made no edits (working tree is clean).

Addressed comments

  • No human reviews or bot comments were present in .workforce/context.json (no review threads to account for).

Advisory Notes

  • Test hermeticity (advisory, no change made): relaycast.test.ts assumes no SLACK_CHANNEL/TELEGRAM_CHAT in the environment. Because input() falls back to process.env, the tests are environment-sensitive. Consider clearing those vars in test setup so the suite is deterministic regardless of the host env. This is a human decision (test change) and outside mechanical cleanup, so I left it untouched.
  • resolveRelaycastUrl() is computed once at sender construction (relaycast.ts:28); env changes after createDelivery won't be picked up for that client instance. Matches the existing slack/telegram construction pattern, so this is consistent — noted only for awareness.

I could not get a clean full-suite run in this sandbox (env-var leak on the test side plus a mid-run filesystem swap of the working tree), and at least one required behavior depends on a human decision about test isolation. Therefore I am not printing READY.

- Bound the relaycast DM with fetchWithTimeout instead of bare fetch (gemini,
  coderabbit) — no indefinite hang if the gateway stalls; degrade to {ok:false}.
- Authenticate /v1/dm with the AGENT token, not the workspace key (codex P1):
  WORKFORCE_AGENT_TOKEN > RELAY_AGENT_TOKEN > RELAY_API_KEY fallback. Workspace
  key alone gets rejected by the agent-scoped /dm endpoint.
- Unwrap the relaycast `{ ok, data }` envelope before reading the message id
  (codex P2) — id lives under data.message.id / data.id; previously parsed
  top-level only and returned messageId:''.
- Treat ok:true with no messageId as a failed delivery (return null), matching
  slack/telegram missing-receipt handling (coderabbit).
- Keep relaycast out of publish()/non-blocking sends — it's a single blocking
  DM with no draft-ref/threading path (coderabbit); skip + debug-log instead.

Tests: +missing-id-is-failure, +publish-skips-relaycast. 8/8 green.

Refs #254
@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #255 in AgentWorkforce/workforce.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@kjgbot kjgbot merged commit d4bbcfa into main Jun 25, 2026
3 checks passed
@kjgbot kjgbot deleted the feat/delivery-relaycast-target branch June 25, 2026 17:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants