refactor(sdk-swift): wrap relaycast engine SDK in hosted transport#1188
Conversation
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>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
✅ Files skipped from review due to trivial changes (2)
📝 WalkthroughWalkthrough
ChangesRelaycast Transport Swap
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(...)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
💡 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".
| .library(name: "AgentRelayBrokerSDK", targets: ["AgentRelayBrokerSDK"]) | ||
| ], | ||
| dependencies: [ | ||
| .package(name: "relaycast-swift", path: ".relaycast-swift") |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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:
-
relaycast (
AgentWorkforce/relaycast#208, draft): relaycast's Swift SDK lives in thepackages/sdk-swiftsubdirectory, so SwiftPM can't consume it by git URL directly (git deps requirePackage.swiftat the repo root). That PR adds a root-levelPackage.swiftthat vends the existingRelaycastlibrary/target (the targetpath:points atpackages/sdk-swift/Sources/Relaycast— no source duplication).swift buildagainst it resolves and compiles cleanly. -
here:
Package.swiftnow uses.package(url: "https://github.com/AgentWorkforce/relaycast.git", revision: "24c9140824518bf371a6c09f8be1f2a298efaf56"), pinned to a revision of relaycast'sswift-root-packagebranch.
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).
| ) { [weak self] id, agentName, token in | ||
| guard let self else { | ||
| fatalError("HostedWorkspaceCore deallocated before AgentRegistration.asClient()") |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
packages/sdk-swift/.relaycast-swiftpackages/sdk-swift/Package.swiftpackages/sdk-swift/README.mdpackages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swiftpackages/sdk-swift/Sources/AgentRelaySDK/RelayHTTP.swiftpackages/sdk-swift/Sources/AgentRelaySDK/RelayTransport.swiftpackages/sdk-swift/Sources/AgentRelaySDK/RelayTypes.swiftpackages/sdk-swift/Sources/AgentRelaySDK/RelaycastBridge.swiftpackages/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 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>
|
Re-pinned the relaycast SwiftPM dependency from the throwaway |
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>
|
Updated default base URL to https://cast.agentrelay.com (canonical host); legacy gateway.relaycast.dev now only served by the legacy-router strangler. |
# Conflicts: # packages/sdk-swift/Sources/AgentRelaySDK/AgentRelayClient.swift # packages/sdk-swift/Tests/AgentRelaySDKTests/AgentRelaySDKTests.swift
Summary
Refactors the hosted-participant path of the Swift SDK (
AgentRelaySDK) to wrap relaycast's Swift engine SDK (Relaycast, packagerelaycast-swift) instead of hand-rolling URLSession/WebSocket. The local broker productAgentRelayBrokerSDKis untouched.The bespoke
HostedHTTP(RelayHTTP.swift) andRelayEventTransport(RelayTransport.swift) are deleted; all HTTP + realtime work is now delegated toRelaycast.RelayCast/Relaycast.AgentClient/Relaycast.WsClient. The relay-specific glue is kept on top.Mapping (relay hosted -> relaycast)
RelayCast.agents.register/RelayCast.registerOrRotateGET /v1/agent)RelayCast.reconnect+AgentClient.meRelayCast.workspace.infoAgentClient.send/AgentClient.dm(mode.wait)RelayCast.actions.register/.deleteAgentClient.actions.getInvocation/.completeInvocationAgentClient.subscribe+AgentClient.on(message.created, thread.reply, dm.received, group_dm.received, action.invoked)Kept on top (unchanged behavior / signatures)
action.invoked->loadInvocation-> handler ->completeInvocation.RelayChannelEventshape and theAsyncStream-based public API.AgentRelayClientso callers don't break.Hard constraints honored
https://gateway.relaycast.devis preserved and passed explicitly intoRelayCastOptions.baseURL.RegisterActionRequest upstream check
Relaycast.RegisterActionRequest(relaycastModels.swift) already carrieshandlerAgent(handler_agent) andinputSchema(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 symlinkpackages/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 inPackage.swiftand the README.Verification
swift buildandswift testboth 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 andRelayObservertests are unchanged.🤖 Generated with Claude Code