Skip to content

refactor(sdk-swift): wrap relaycast engine SDK in hosted transport#1188

Merged
willwashburn merged 8 commits into
mainfrom
sdk-swift-wrap-relaycast
Jun 24, 2026
Merged

refactor(sdk-swift): wrap relaycast engine SDK in hosted transport#1188
willwashburn merged 8 commits into
mainfrom
sdk-swift-wrap-relaycast

Conversation

@willwashburn

@willwashburn willwashburn commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

Refactors the hosted-participant path of the Swift SDK (AgentRelaySDK) to wrap relaycast's Swift engine SDK (Relaycast, package relaycast-swift) instead of hand-rolling URLSession/WebSocket. The local broker product AgentRelayBrokerSDK is untouched.

The bespoke HostedHTTP (RelayHTTP.swift) and RelayEventTransport (RelayTransport.swift) are deleted; all HTTP + realtime work is now delegated to Relaycast.RelayCast / Relaycast.AgentClient / Relaycast.WsClient. The relay-specific glue is kept on top.

Mapping (relay hosted -> relaycast)

relay hosted call relaycast
register / registerOrRotate (409 -> get + rotate-token) RelayCast.agents.register / RelayCast.registerOrRotate
reconnect by token (GET /v1/agent) RelayCast.reconnect + AgentClient.me
workspace info RelayCast.workspace.info
post channel / dm AgentClient.send / AgentClient.dm (mode .wait)
action register / delete RelayCast.actions.register / .delete
invocation get / complete AgentClient.actions.getInvocation / .completeInvocation
WS subscribe + typed events AgentClient.subscribe + AgentClient.on (message.created, thread.reply, dm.received, group_dm.received, action.invoked)

Kept on top (unchanged behavior / signatures)

  • The action-dispatch loop: action.invoked -> loadInvocation -> handler -> completeInvocation.
  • The RelayChannelEvent shape and the AsyncStream-based public API.
  • All public type signatures of AgentRelayClient so callers don't break.

Hard constraints honored

  • Default base URL https://gateway.relaycast.dev is preserved and passed explicitly into RelayCastOptions.baseURL.
  • SwiftPM dependency added.

RegisterActionRequest upstream check

Relaycast.RegisterActionRequest (relaycast Models.swift) already carries handlerAgent (handler_agent) and inputSchema (input_schema). No upstream relaycast change was required.

SwiftPM dependency wiring (note for reviewers)

relaycast-swift lives in the relaycast monorepo under packages/sdk-swift — not at the repo root — so SwiftPM can't consume it by git URL directly, and its directory basename (sdk-swift) collides with this package's own SwiftPM identity. The dependency is therefore referenced through a committed relative symlink packages/sdk-swift/.relaycast-swift -> ../../../relaycast/packages/sdk-swift (a sibling clone of relaycast). Repoint the symlink for other checkout locations, or swap in a git/tag reference (e.g. from: "4.1.6") once a root-level relaycast-swift mirror is published. Documented in Package.swift and the README.

Verification

swift build and swift test both pass — 69 tests, 0 failures (Swift 6.2.4 toolchain). Hosted tests were rewritten to cover facade config + bridging glue (the old tests mocked now-deleted internal transport types); broker and RelayObserver tests are unchanged.

🤖 Generated with Claude Code

Review in cubic

Reduce AgentRelaySDK's hosted-participant transport to a thin facade over the
published relaycast Swift engine SDK (product `Relaycast`, package
`relaycast-swift`) instead of hand-rolling URLSession/WebSocket networking.

- Replace the bespoke `HostedHTTP` (RelayHTTP.swift) and `RelayEventTransport`
  (RelayTransport.swift) with delegation to `Relaycast.RelayCast` /
  `Relaycast.AgentClient` / `Relaycast.WsClient`:
    - register / registerOrRotate -> RelayCast.agents.register / registerOrRotate
    - reconnect(apiToken:) -> RelayCast.reconnect + AgentClient.me
    - workspaceInfo -> RelayCast.workspace.info
    - post / dm -> AgentClient.send / dm
    - actions register/delete -> RelayCast.actions; invocation get/complete ->
      AgentClient.actions
    - realtime events -> AgentClient.on typed handlers (message.created,
      thread.reply, dm.received, group_dm.received, action.invoked)
- Keep the relay-specific glue on top: the action-dispatch loop
  (action.invoked -> loadInvocation -> handler -> completeInvocation), the
  `RelayChannelEvent` shape, the AsyncStream-based public API, and every public
  type/signature of AgentRelayClient so callers don't break.
- Preserve the default host `https://gateway.relaycast.dev`, passed explicitly
  into `RelayCastOptions.baseURL`.
- Add `RelaycastBridge.swift` mapping relaycast types onto AgentRelaySDK's public
  surface (agent type/status, JSONValue, RelayError, and RelayEvent built from
  `Relaycast.WsEvent`).
- Wire the SwiftPM dependency. relaycast-swift is not at its repo root and its
  directory basename (`sdk-swift`) collides with this package's identity, so the
  dependency is referenced through a committed symlink (`.relaycast-swift` ->
  sibling relaycast checkout); documented how to repoint it or swap in a git/tag
  reference once a root-level mirror is published.
- Rewrite the hosted tests to cover the facade config and bridging glue;
  AgentRelayBrokerSDK and the RelayObserver code/tests are left untouched.

RegisterActionRequest in relaycast already carries handler_agent and
input_schema; no upstream change was required.

Verified: `swift build` and `swift test` pass (69 tests).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 05b7c649-5031-4fd1-b50c-ade9f7ce54bb

📥 Commits

Reviewing files that changed from the base of the PR and between 8c891c7 and a8b8f3b.

📒 Files selected for processing (2)
  • CHANGELOG.md
  • packages/sdk-swift/README.md
✅ Files skipped from review due to trivial changes (2)
  • CHANGELOG.md
  • packages/sdk-swift/README.md

📝 Walkthrough

Walkthrough

AgentRelaySDK removes its custom HTTP and WebSocket transport layers and rewires registration, messaging, action handling, and realtime events through the Relaycast Swift engine SDK. It also adds SwiftPM dependency wiring, bridge adapters, updated tests, and documentation for the new package relationship.

Changes

Relaycast Transport Swap

Layer / File(s) Summary
SwiftPM dependency wiring and docs
packages/sdk-swift/Package.swift, packages/sdk-swift/Package.resolved, packages/sdk-swift/README.md
Adds the relaycast SwiftPM dependency, wires AgentRelaySDK to the Relaycast product, pins the resolved package version, and documents the facade/dependency setup.
Bridge adapters and Relay types
packages/sdk-swift/Sources/AgentRelaySDK/RelaycastBridge.swift, packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift
Adds type, status, JSON, error, and websocket event bridging from Relaycast into AgentRelaySDK, and updates RelayEvent plus related internal type declarations.
Workspace core uses Relaycast
packages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swift (lines 2–209)
Updates AgentRelay initialization and base URL resolution, then routes workspace registration, reconnect, agent lookup, and workspace info through Relaycast.
Participant core uses Relaycast engines
packages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swift (lines 313–811)
Reworks participant connection, event delivery, message/DM sending, action registration, invocation handling, and routing normalization around Relaycast engine APIs and removes the legacy transport helpers.
Facade and bridge tests
packages/sdk-swift/Tests/AgentRelaySDKTests/AgentRelaySDKTests.swift
Replaces the hosted-network tests with coverage for Relaycast wiring, bridge conversions, realtime event mapping, registration persistence, and ActionHandle lifecycle.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant AgentRelayClient
  participant HostedWorkspaceCore
  participant RelayCast
  participant HostedParticipantCore
  participant AgentClient

  App->>AgentRelayClient: register(agent)
  AgentRelayClient->>HostedWorkspaceCore: register(agent)
  HostedWorkspaceCore->>RelayCast: relay.agents.register(...)
  RelayCast-->>HostedWorkspaceCore: CreateAgentResponse
  HostedWorkspaceCore-->>AgentRelayClient: AgentRegistration

  App->>AgentRelayClient: connect(registration)
  AgentRelayClient->>HostedParticipantCore: connect()
  HostedParticipantCore->>AgentClient: engine.on.message / engine.on.actionInvoked

  App->>HostedParticipantCore: post(channel, message)
  HostedParticipantCore->>RelayCast: engine.send(..., mode: .wait)

  AgentClient-->>HostedParticipantCore: WsEvent
  HostedParticipantCore->>RelayCast: engine.actions.getInvocation(...)
  HostedParticipantCore->>RelayCast: engine.actions.completeInvocation(...)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • AgentWorkforce/relay#1147: Closely related earlier Swift SDK split and action-registration work that this PR continues by moving transport and action handling onto Relaycast.
  • AgentWorkforce/relay#1199: Shares the same AgentRelayClient/Relaycast dependency direction, including the updated base URL and dependency contract.
  • AgentWorkforce/relay#1197: Overlaps with the same changelog escaping line updated here.

Suggested reviewers

  • khaliqgant

Poem

🐇 The rabbit hops from sockets old,
to Relaycast paths both swift and bold.
With bridges built and tests aligned,
the SDK leaves the past behind.
Hoppity—clean streams now shine ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the summary well, but it omits the required Test Plan checkboxes and Screenshots section. Add the Test Plan section with the required checkboxes and include a Screenshots section or mark it not applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main Swift SDK refactor to wrap relaycast.
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 sdk-swift-wrap-relaycast

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.

@willwashburn willwashburn marked this pull request as ready for review June 23, 2026 12:41
@willwashburn willwashburn requested a review from khaliqgant as a code owner June 23, 2026 12:41

@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: 9535b3c225

ℹ️ 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/sdk-swift/Package.swift Outdated
.library(name: "AgentRelayBrokerSDK", targets: ["AgentRelayBrokerSDK"])
],
dependencies: [
.package(name: "relaycast-swift", path: ".relaycast-swift")

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 a resolvable relaycast dependency

When this package is checked out without a sibling relaycast clone (the normal SPM consumer case and a clean checkout of this repo), this local path dependency follows the committed symlink to ../../../relaycast/packages/sdk-swift, which is outside the repository. I checked swift build --package-path packages/sdk-swift, and SwiftPM fails before target resolution with the package at .../.relaycast-swift cannot be accessed, so both Swift SDK products are unbuildable unless every user manually recreates that local checkout.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 641f4dd. Removed the committed .relaycast-swift symlink and replaced the local-path dependency with a real git-URL SwiftPM dependency.

The proper fix is a two-repo dependency chain:

  1. relaycast (AgentWorkforce/relaycast#208, draft): relaycast's Swift SDK lives in the packages/sdk-swift subdirectory, so SwiftPM can't consume it by git URL directly (git deps require Package.swift at the repo root). That PR adds a root-level Package.swift that vends the existing Relaycast library/target (the target path: points at packages/sdk-swift/Sources/Relaycast — no source duplication). swift build against it resolves and compiles cleanly.

  2. here: Package.swift now uses .package(url: "https://github.com/AgentWorkforce/relaycast.git", revision: "24c9140824518bf371a6c09f8be1f2a298efaf56"), pinned to a revision of relaycast's swift-root-package branch.

Verified locally against that revision: swift build and swift test both pass (70 tests, 0 failures); SwiftPM fetches relaycast from the git URL and resolves the working copy at the pinned SHA.

Dependency-chain caveat (honest): CI here will fully pass against the pinned branch revision, but the dependency is still on an unmerged relaycast branch. Once relaycast#208 is merged and the monorepo is re-tagged, the revision: pin should be swapped for a from: "x.y.z" version requirement (a TODO is noted in the manifest and README).

Comment on lines +145 to +147
) { [weak self] id, agentName, token in
guard let self else {
fatalError("HostedWorkspaceCore deallocated before AgentRegistration.asClient()")

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 Keep registrations usable after the client is released

If a caller persists the returned AgentRegistration and lets the AgentRelay instance go out of scope before calling registration.asClient(), this weak capture allows HostedWorkspaceCore to deallocate and the factory traps in fatalError. The previous implementation captured the needed transport state directly, so AgentRegistration.asClient() stayed usable independently; capture the relay/base URL strongly or otherwise avoid the trap to preserve that public contract.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 641f4dd. makeRegistration(_:) no longer captures self (HostedWorkspaceCore) weakly. It now captures the reusable transport state strongly — relay (Relaycast.RelayCast) and baseURL — and the factory builds the per-agent HostedParticipantCore from that captured state via a new static HostedWorkspaceCore.makeParticipantCore(relay:baseURL:...). So a persisted AgentRegistration stays usable independently of the owning AgentRelay, and the fatalError trap is gone. The public contract (AgentRegistration.asClient()) is unchanged.

Added a regression test testRegistrationAsClientSurvivesCoreRelease: it builds a registration, drops the owning core (core = nil), then calls asClient() and asserts the returned client's id/name/token — passes (no trap).

@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: 5

🤖 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/sdk-swift/Package.swift`:
- Around line 45-47: The relaycast-swift dependency in the Package.swift
manifest currently uses only a local path reference which is not portable and
breaks CI environments. Replace or supplement the path-based dependency with a
portable alternative by specifying a git repository URL with a specific tag or
branch, or implement a conditional dependency that uses the local path when
available during development but falls back to a git/registry-based dependency
for CI and distributed builds. This ensures the Package.swift manifest can
resolve dependencies consistently across different environments.

In `@packages/sdk-swift/README.md`:
- Around line 39-41: The fenced code block containing the symlink path
`.relaycast-swift -> ../../../relaycast/packages/sdk-swift` is missing a
language identifier and triggers markdownlint rule MD040. Add a language tag
(text) immediately after the opening triple backticks to specify the code block
language, changing the opening fence from ``` to ```text to properly document
the code block type.

In `@packages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swift`:
- Around line 78-84: The initialization of the Relaycast.RelayCast instance in
the AgentRelayClient init method uses try? followed by force-unwrap (!) which
suppresses thrown errors and crashes on failure instead of propagating
RelayError. Remove the try? and force-unwrap pattern from the RelayCast
initialization (around line 78-84) and the relay.asAgent(token) call (around
line 134). Consider making the HostedWorkspaceCore.init or makeParticipantCore
methods async-throwing to allow proper error propagation, or defer the RelayCast
construction to a lazy property or first async call. This will allow
initialization errors to be caught and translated to RelayError consistently
with how errors are already handled elsewhere in the codebase (such as in the
other error handling blocks around lines 92-94, 103-105, 114-116).
- Around line 455-466: The disconnected handler only notifies the connection
state without resetting the connected flag to false, creating an asymmetry with
the manual disconnect() method (line 322) which does reset it. This prevents
ensureConnected() from attempting reconnection after engine-initiated
disconnects. In the disconnected event handler where
notifyConnectionStateAsync(.disconnected) is called, add code to reset the
connected flag to false to maintain consistency and restore proper reconnection
semantics.
- Around line 469-471: The ingest function spawns independent unstructured tasks
that can be scheduled out-of-order by Swift's task scheduler, causing events
from multiple engine callbacks to reach routeEvent in a different order than
received. Instead of spawning individual Task calls for each event in ingest,
implement a single serialized buffer or AsyncStream to queue incoming events and
process them sequentially through routeEvent, ensuring event ordering is
maintained throughout the messaging stream.
🪄 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: 224f451a-c8b2-489a-9f52-0102a23d4a72

📥 Commits

Reviewing files that changed from the base of the PR and between 2eb86d0 and 9535b3c.

📒 Files selected for processing (9)
  • packages/sdk-swift/.relaycast-swift
  • packages/sdk-swift/Package.swift
  • packages/sdk-swift/README.md
  • packages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swift
  • packages/sdk-swift/Sources/AgentRelaySDK/RelayHTTP.swift
  • packages/sdk-swift/Sources/AgentRelaySDK/RelayTransport.swift
  • packages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swift
  • packages/sdk-swift/Sources/AgentRelaySDK/RelaycastBridge.swift
  • packages/sdk-swift/Tests/AgentRelaySDKTests/AgentRelaySDKTests.swift
💤 Files with no reviewable changes (2)
  • packages/sdk-swift/Sources/AgentRelaySDK/RelayHTTP.swift
  • packages/sdk-swift/Sources/AgentRelaySDK/RelayTransport.swift

Comment thread packages/sdk-swift/Package.swift
Comment thread packages/sdk-swift/README.md Outdated
Comment thread packages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swift Outdated
Comment thread packages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swift
Comment thread packages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swift Outdated
willwashburn and others added 3 commits June 23, 2026 08:58
Comment 1 (CI-blocking): replace the committed external symlink
(.relaycast-swift -> ../../../relaycast/packages/sdk-swift), which does
not resolve on clean checkouts or for normal SPM consumers, with a
git-URL SwiftPM dependency on AgentWorkforce/relaycast pinned to the
revision of the new root-level Package.swift packaging branch. The
relaycast root manifest (AgentWorkforce/relaycast#208) vends the
Relaycast library/target so it can be consumed by git URL. Update the
README accordingly and drop the symlink.

Comment 2: AgentRegistration.asClient() previously captured
HostedWorkspaceCore weakly and trapped in fatalError if the owning
AgentRelay was released first. Capture the relay/baseURL transport state
strongly so a persisted AgentRegistration stays usable independently,
preserving the public contract. Add a regression test
(testRegistrationAsClientSurvivesCoreRelease).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… events

Address CodeRabbit review on PR #1188:

- Stop force-unwrapping `try? RelayCast(options:)` and `try? relay.asAgent`.
  Both can throw on bad config (empty apiKey / invalid baseURL) and were
  crashing the process. The RelayCast build result is captured eagerly and
  rethrown as a translated `RelayError` on first use; the per-agent engine is
  built lazily inside the actor so `asAgent` errors propagate through the first
  async call. Public (non-throwing) initializers are unchanged.

- Reset `connected = false` on engine-initiated disconnect so `ensureConnected()`
  re-issues `engine.connect()`, matching the manual `disconnect()` path.

- Route inbound engine events through a single serialized AsyncStream drained by
  one consumer task instead of spawning an unstructured Task per event, so
  closely-spaced events keep their delivery order. The pump is torn down on
  disconnect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
relaycast#208 (root Package.swift) is merged and published as v4.2.0, so switch
the SwiftPM dependency from the pinned `swift-root-package` revision to
`from: "4.2.0"`. swift build + swift test pass resolving the published tag
(70 tests). Updated the README note accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@willwashburn

Copy link
Copy Markdown
Member Author

Re-pinned the relaycast SwiftPM dependency from the throwaway swift-root-package revision to the published tag: .package(url: "https://github.com/AgentWorkforce/relaycast.git", from: "4.2.0") (relaycast#208 merged + v4.2.0 published). swift build + swift test resolve and pass against the tag (70 tests). This clears the dependency-chain caveat — the dep is now a normal versioned package.

willwashburn and others added 2 commits June 23, 2026 15:48
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Default to cast.agentrelay.com (canonical host). gateway/api.relaycast.dev now only persist in already-shipped versions, served by the legacy-router strangler. Existing pre-cutover users with old-DB workspaces who rely on the default should set RELAY_BASE_URL or migrate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@willwashburn

Copy link
Copy Markdown
Member Author

Updated default base URL to https://cast.agentrelay.com (canonical host); legacy gateway.relaycast.dev now only served by the legacy-router strangler.

willwashburn and others added 2 commits June 24, 2026 16:48
# Conflicts:
#	packages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swift
#	packages/sdk-swift/Tests/AgentRelaySDKTests/AgentRelaySDKTests.swift
@willwashburn willwashburn merged commit 877b72e into main Jun 24, 2026
38 checks passed
@willwashburn willwashburn deleted the sdk-swift-wrap-relaycast branch June 24, 2026 20:52
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.

1 participant