Skip to content

feat(sdk): split broker binaries into per-platform optional-dep packages#772

Merged
willwashburn merged 9 commits intomainfrom
feat/broker-optional-deps
Apr 24, 2026
Merged

feat(sdk): split broker binaries into per-platform optional-dep packages#772
willwashburn merged 9 commits intomainfrom
feat/broker-optional-deps

Conversation

@willwashburn
Copy link
Copy Markdown
Member

@willwashburn willwashburn commented Apr 22, 2026

Closes #770.

Summary

Publishes the agent-relay-broker binary as five platform-specific npm packages (@agent-relay/broker-{darwin-arm64,darwin-x64,linux-arm64,linux-x64,win32-x64}) declared as optionalDependencies of @agent-relay/sdk. Package managers now install exactly one broker that matches the user's OS/CPU — same pattern esbuild, swc, next, sharp, and friends use.

  • Fresh @agent-relay/sdk install drops from ~55 MB of broker binaries to ~13 MB.
  • First-class Windows support (@agent-relay/broker-win32-x64).
  • scripts/postinstall.js no longer installs the broker binary — the SDK's runtime resolver does it, so --ignore-scripts / Bun / corporate-policy installs work without ancestor-walking or runtime downloads (see fix(sdk): find broker in package-root bin/ without relying on postinstall #768 context).
  • AgentRelayClient.spawn() throws a clear error naming the expected optional-dep package when the binary can't be resolved, instead of a confusing spawn agent-relay-broker ENOENT.

Resolver order (new)

  1. BROKER_BINARY_PATH / AGENT_RELAY_BIN env override
  2. Source-checkout target/release|debug (dev mode)
  3. @agent-relay/broker-<platform>-<arch> optional-dep package ← new primary production path
  4. Bundled packages/sdk/bin/ (kept one release cycle for mixed-version installs)
  5. Ancestor bin/ walk (kept one release cycle, from fix(sdk): find broker in package-root bin/ without relying on postinstall #768)
  6. Cargo dev paths
  7. PATH lookup via which / where

Release flow

  • build-broker matrix gains x86_64-pc-windows-msvc.
  • New publish-broker-packages matrix job publishes each per-platform package with a 3-attempt retry wrapper — it runs before publish-packages (SDK) so the exact-version optional deps exist on the registry by the time the SDK references them.
  • publish-main no longer bundles bin/agent-relay-broker-* in the root CLI tarball (the bundled @agent-relay/sdk still carries them for one release cycle).

Test plan

  • npx tsc -p packages/sdk/tsconfig.json --noEmit passes
  • npm run build in packages/sdk succeeds
  • Symlinking @agent-relay/broker-darwin-arm64 into node_modules/ + hiding target/*/agent-relay-broker → resolver returns the optional-dep path
  • Hiding all binaries → getBrokerBinaryPath() returns null and formatBrokerNotFoundError() emits the expected message
  • CI dry-run of the publish workflow with package=sdk — verify publish-broker-packages runs before publish-sdk-only and all 5 packages publish (including Windows)
  • Post-release: fresh npm i @agent-relay/sdk on each of macOS arm64/x64, Linux x64/arm64, Windows x64 spawns a broker without --include=optional tweaks

Notes

🤖 Generated with Claude Code


Open in Devin Review

Publish the agent-relay-broker binary as five platform-specific npm
packages (@agent-relay/broker-{darwin-arm64,darwin-x64,linux-arm64,
linux-x64,win32-x64}) declared as optionalDependencies of
@agent-relay/sdk. Package managers now install exactly one broker that
matches the user's os/cpu, shrinking the fresh-install footprint from
~55MB to ~13MB and opening the door to Windows support.

getBrokerBinaryPath() resolves the binary via require.resolve of the
matching optional-dep package as its primary production path. Existing
bundled/ancestor fallbacks remain for one release cycle so mixed-version
installs keep working. When nothing resolves, spawn() now throws a clear
error that names the expected package and the detected platform/arch
instead of an inscrutable ENOENT.

The top-level agent-relay CLI stops bundling bin/agent-relay-broker-* in
its tarball (the bundled SDK still ships broker binaries for one
release cycle as a belt-and-braces fallback), and scripts/postinstall.js
no longer installs the broker — the runtime resolver handles it.

Publish workflow:
- build-broker matrix adds x86_64-pc-windows-msvc.
- new publish-broker-packages matrix job publishes each per-platform
  package before the SDK, with a 3-attempt retry wrapper so registry
  flakes don't strand a partial release.
- publish-packages (SDK) and publish-sdk-only wait for it.
- publish-main drops its broker-binary bundling step.

Closes #770

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

@barryollama barryollama left a comment

Choose a reason for hiding this comment

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

✅ Code Review Complete

Summary

This is an excellent architectural improvement that follows battle-tested patterns from esbuild, swc, and sharp.

What I Like

  1. Proper platform abstraction - The 5 platform-specific packages with correct / constraints in their package.json files
  2. Clean versioning - The 5.0.0 major bump and synchronized optional dependency versions
  3. Graceful degradation - The multi-tier resolution fallback (env → source → optional-dep → bundled → ancestor → cargo → PATH)
  4. User-friendly errors - The function provides actionable guidance instead of cryptic ENOENT
  5. Windows support - First-class Windows x64 support with proper handling
  6. Clean postinstall - Removes the problematic broker download code entirely

Release Flow Considerations

The job dependency chain is correct: runs before , ensuring the optional deps exist before the SDK references them.

Minor Notes

  • The bundled binaries in are kept for one release cycle as a fallback—consider removing in 6.0
  • The workspace exclusions in root prevent the broker packages from being managed by npm workspaces at development time

LGTM - approve for merge

This solves #770 elegantly and addresses the pain points from #768 without breaking existing installs.

barryollama
barryollama previously approved these changes Apr 22, 2026
Copy link
Copy Markdown

@barryollama barryollama left a comment

Choose a reason for hiding this comment

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

✅ Code Review Complete

Summary

This is an excellent architectural improvement that follows battle-tested patterns from esbuild, swc, and sharp.

What I Like

  1. Proper platform abstraction - The 5 platform-specific packages with correct os/cpu constraints in their package.json files
  2. Clean versioning - The 5.0.0 major bump and synchronized optional dependency versions
  3. Graceful degradation - The multi-tier resolution fallback (env, source, optional-dep, bundled, ancestor, cargo, PATH)
  4. User-friendly errors - The formatBrokerNotFoundError() function provides actionable guidance instead of cryptic ENOENT
  5. Windows support - First-class Windows x64 support with proper .exe handling
  6. Clean postinstall - Removes the problematic broker download code entirely

Release Flow

The job dependency chain is correct: publish-broker-packages runs before publish-packages, ensuring the optional deps exist before the SDK references them.

Minor Notes

  • The bundled binaries in packages/sdk/bin/ are kept for one release cycle as a fallback
  • The workspace exclusions in root package.json prevent the broker packages from being managed by npm workspaces at development time

LGTM - approve for merge

This solves #770 elegantly and addresses the pain points from #768 without breaking existing installs.

Copy link
Copy Markdown

@barryollama barryollama left a comment

Choose a reason for hiding this comment

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

Code review for PR #772

Comment thread packages/sdk/src/broker-path.ts
Comment thread packages/sdk/src/broker-path.ts Outdated
Comment thread .github/workflows/publish.yml
The previous cut excluded `packages/broker-*` from workspaces to avoid
EBADPLATFORM on `os`/`cpu` constraints that can't be satisfied by every
dev host. That broke `npm ci` because the SDK's optionalDependencies at
exact version `5.0.0` have no resolution source — those packages aren't
on the registry yet and they aren't workspaces either.

Flip the approach: keep all five broker packages as plain workspaces
during development (no `os`/`cpu` in the committed package.json) and
inject the constraints at publish time in the publish-broker-packages
job. This keeps the lockfile populated, lets `npm ci` succeed locally
and in CI, and still publishes correctly constrained packages that npm
picks per-platform on consumer installs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
barryollama
barryollama previously approved these changes Apr 22, 2026

This comment was marked as resolved.

Add a new smoke-broker-packages matrix job that exercises the full
optional-dep resolution path on every target platform (macOS arm64/x64,
Linux x64/arm64, Windows x64) before we publish. Each leg stages the
platform's broker binary into its package tree, injects os/cpu,
npm-packs both the SDK and the matching broker, installs them into a
scratch project, and then runs two assertions:

- Resolver smoke: getBrokerBinaryPath() returns an executable path that
  goes through the optional-dep package (not a bundled fallback).
- Spawn smoke: AgentRelayClient.spawn() actually starts a broker and
  client.shutdown() tears it down cleanly.

The Linux x64 leg additionally exercises the negative path: installing
the SDK without the broker tarball via --no-optional must produce the
clear, platform-named error from formatBrokerNotFoundError, not ENOENT.

publish-broker-packages now gates on this job — we do not ship if any
platform fails. Ubuntu 24.04 arm64 runners are free on public repos,
so all five legs run natively without QEMU.

Verified locally on darwin-arm64 end-to-end (pack → install → spawn →
shutdown, plus the negative case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The package-validation workflow walks packages/*/ and asserts every
workspace has dist/index.{js,d.ts} — meant to catch packages that
didn't build. The five @agent-relay/broker-* packages ship a Rust
binary in bin/ and have no TypeScript or JS dist to validate, so they
join build-plans and brand in the SKIP_PACKAGES list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
barryollama
barryollama previously approved these changes Apr 23, 2026
Copy link
Copy Markdown

@barryollama barryollama left a comment

Choose a reason for hiding this comment

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

This PR follows the established pattern used by esbuild, swc, next, and sharp for distributing platform-specific binaries as optional dependencies — a proven approach. The resolver fallback hierarchy (env vars → dev build → optional dep → bundled → ancestor walk → cargo → PATH) is well-designed and maintains backward compatibility during migration.

Key strengths:

  • Clean separation between the SDK and broker binaries reduces install size (55 MB → 13 MB)
  • Strong backward compatibility with legacy bundled binaries and ancestor walks
  • Comprehensive CI testing with smoke tests across all 5 platforms
  • Better error messages via formatBrokerNotFoundError() instead of confusing ENOENT

Found minor issues for potential follow-up:

packages/sdk/src/broker-path.ts:27-35 — The getResolutionReferences() function includes a potentially surprising fallback. Consider whether the cwd package.json lookup might cause unexpected resolution behavior when running the SDK from a deep subdirectory, though the multi-path try-loop mitigates this.

packages/broker-*/package.json — All five platform packages should have os and cpu fields set at publish time (which they do via the CI injection), but consider adding these to the source files as well so local testing validates the same constraints.

scripts/postinstall.js — The postinstall still references patching agent-trajectories and @relayauth/core. Verify these patches still apply successfully after removing the ~125 lines of broker binary handling (they weren't shown in the diff, so assuming they remain unaffected).

Overall LGTM — the design is sound and the test coverage is thorough.

This comment was marked as resolved.

- publish workflow: rewrite @agent-relay/* references in
  optionalDependencies too, not just dependencies/devDependencies/
  peerDependencies. Without this, future releases would leave the
  SDK pinned at the previous release's broker packages (critical bug
  flagged by devin and Copilot).

- broker-path: fix misleading comment that blamed --ignore-scripts
  for missing optional deps; it's --no-optional / --omit=optional
  that suppresses them.

- broker-path: document why the cwd/package.json reference is part
  of the resolution chain (globally-installed SDKs, bundler setups,
  REPL experimentation).

- add targeted vitest tests for getOptionalDepPackageName,
  formatBrokerNotFoundError, and the BROKER_BINARY_PATH override.
  Full optional-dep resolution is covered end-to-end by the
  smoke-broker-packages CI matrix — staging tmp fs layouts inside
  the dev tree is too fragile because source-checkout and
  ancestor-bin fallbacks win before the scratch cwd.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willwashburn
Copy link
Copy Markdown
Member Author

Review round-up (all inline threads resolved)

Fixed in 080f6be:

  • Critical — version-sync missed optionalDependencies (devin, Copilot). Added 'optionalDependencies' to DEP_SECTIONS in the publish workflow so the SDK's broker pins bump in lockstep with every other internal ref. Without this, every release after v5.0.0 would have shipped an SDK pinned at the previous release's broker packages — real correctness bug.
  • Misleading --ignore-scripts docstring (Copilot). Now references --no-optional / --omit=optional, the flag families that actually suppress optional installs.
  • Comment on cwd/package.json resolution anchor (barryollama). Spelled out the globally-installed / bundler / REPL cases that motivate it.
  • Test coverage (Copilot ×2). Added packages/sdk/src/__tests__/broker-path.test.ts covering the deterministic pieces — getOptionalDepPackageName, formatBrokerNotFoundError, BROKER_BINARY_PATH override. End-to-end optional-dep resolution is authoritatively covered by the smoke-broker-packages CI matrix, which packs real tarballs on all 5 targets and exercises AgentRelayClient.spawn() + shutdown().

Responded, no change:

  • bundledDependencies won't install the SDK's optional deps (Copilot, PR-level comment on publish.yml:1155, and Copilot again on the SDK's bundled broker binaries). This is the deliberate shape of the one-release-cycle migration: during this cycle the SDK tarball still bundles all platform broker binaries in packages/sdk/bin/ as a fallback, so npm i agent-relay works via the bundled SDK even though npm skips the optional deps of a bundled package. New direct npm i @agent-relay/sdk installs take the optional-dep path and get only the matching platform (~13 MB instead of ~55 MB). Next major drops the bundled fallback; at that point we'll need to either add the broker packages as optional deps of the root agent-relay CLI or stop bundling the SDK — I'll open a follow-up issue to track that cleanup.
  • Spawn no longer falls back to 'agent-relay-broker' on PATH (Copilot). Intentional per issue TS SDK: split broker binaries into per-platform optional-dependency packages #770 — the whole point of the change was to replace spawn … ENOENT with an error that names the expected optional-dep package. The error message already names BROKER_BINARY_PATH as the escape hatch for hosts with a user-curated broker on PATH. If we ever find an environment where which/where is unavailable but the broker is still on PATH, we can harden the PATH step inside getBrokerBinaryPath to do a manual scan — that's a surgical change that keeps the clear-error contract.
  • os/cpu should also be in source packages/broker-*/package.json (barryollama). Tried that in the first pass — it breaks npm install / npm ci with EBADPLATFORM on every dev host that isn't every platform simultaneously. The publish-broker-packages job injects os/cpu at publish time so the registry-served versions carry the constraints.

All 9 inline review threads are now resolved. Ready for another look.

devin-ai-integration[bot]

This comment was marked as resolved.

willwashburn and others added 3 commits April 24, 2026 08:07
The smoke-broker-packages job runs after the build job bumps every
workspace to NEW_VERSION but before any package has shipped to the
registry. The SDK's `dependencies` pins `@agent-relay/config` at that
version, so `npm install` of the locally-packed SDK tarball fails with
`ETARGET / No matching version found for @agent-relay/config@<NEW>`
— every smoke run on a version-bumped workflow breaks at that point.

Pack `packages/config` into a tarball alongside the SDK + broker and
install all three together. The negative-smoke leg gets the same
treatment so it can still reach the `AgentRelayClient.spawn()` path
that produces the clear "optional dep missing" error.

`@agent-relay/config` is currently the SDK's only internal required
dep; if another lands later the smoke job will fail loudly with the
same ETARGET signature.

Repro'd locally with a synthetic 5.99.99-smoketest bump: install
without config fails ETARGET, install with config succeeds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add verify-publish-sdk.yml — a matrix job that, after every release,
installs the just-published @agent-relay/sdk@<version> into a scratch
project on each of the five target runners (macOS arm64/x64, Linux
x64/arm64, Windows x64) and asserts:

- the matching @agent-relay/broker-<platform>-<arch> optional dep was
  auto-selected by npm based on os/cpu (and no sibling brokers were
  installed alongside — a sanity check that the os/cpu injection in
  publish-broker-packages landed correctly)
- getBrokerBinaryPath() resolves through that package and returns an
  executable file
- AgentRelayClient.spawn() actually starts the registry-installed
  broker end-to-end and shutdown() tears it down

Catches the failure modes the pre-publish smoke job can't see:
registry round-trip, CDN propagation lag, missing or wrong os/cpu in
the published manifest. Runs with exponential-backoff retry on
npm view so the first run after publish isn't a flake.

Wired into publish.yml's post-publish gate via workflow_call so every
release exercises it automatically. Also available on-demand via
workflow_dispatch for re-verifying an existing version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

verify-publish-sdk previously gated on publish-packages alone. With
package='all' that's the right gate, but with package='sdk' (SDK-only
release) publish-packages is skipped and publish-sdk-only does the
actual publish — so the verify gate never fired, leaving post-publish
cross-platform smoke uncovered for SDK-only releases.

Add publish-sdk-only to `needs` and accept success from either path.
The two jobs are mutually exclusive so at most one will be 'success'
at a time; the other will be 'skipped'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willwashburn willwashburn merged commit c949195 into main Apr 24, 2026
40 checks passed
@willwashburn willwashburn deleted the feat/broker-optional-deps branch April 24, 2026 12:49
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.

TS SDK: split broker binaries into per-platform optional-dependency packages

3 participants