Skip to content

feat(sdk): addListener overload accepts BeforeAgentSpawnHandler#941

Merged
willwashburn merged 1 commit into
mainfrom
feat/sdk-before-spawn-handler-overload
May 22, 2026
Merged

feat(sdk): addListener overload accepts BeforeAgentSpawnHandler#941
willwashburn merged 1 commit into
mainfrom
feat/sdk-before-spawn-handler-overload

Conversation

@willwashburn

Copy link
Copy Markdown
Member

Summary

  • Adds dedicated overloads on AgentRelay.addListener / AgentRelayClient.addListener (and removeListener) that accept BeforeAgentSpawnHandler when the event literal is 'beforeAgentSpawn'
  • All other events keep the strict (...args) => void | Promise<void> contract, so a stray return value on, say, agentSpawned still fails to type-check
  • Runtime behavior is unchanged — only the public type surface widens

Why

In v7.0.0 the new beforeAgentSpawn hook was documented to allow handlers to return a SpawnPatch that the dispatcher folds into the spawn input:

export type BeforeAgentSpawnHandler = (
  ctx: BeforeAgentSpawnContext,
) => void | SpawnPatch | Promise<void | SpawnPatch>;

…and the SDK's runBeforeSpawn already reads each handler's return and shallow-merges patches into the resolved input. But the public addListener signature was typed as the strict universal shape (...args) => void | Promise<void>, so registering a BeforeAgentSpawnHandler-typed function required a cast at every call site:

client.addListener(
  'beforeAgentSpawn',
  burnHandler as Parameters<typeof client.addListener<'beforeAgentSpawn'>>[1]
)

That's the workaround Pear is currently shipping in its burn-stamping integration. With this PR Pear can drop the cast.

What changed

packages/sdk/src/event-bus.ts

  • EventHandler<Args, R = void> gains an optional return-type generic; default R = void preserves the strict observe-only contract for callers that don't specify it
  • EventBus.addListener<K, R> / removeListener<K, R> / listeners<K, R> thread R through so events that allow non-void returns are still typed precisely
  • Internal Set storage widens to EventHandler<Args, unknown> so one Set can hold handlers from either overload — the dispatcher in runBeforeSpawn already casts to Array<BeforeAgentSpawnHandler> when it cares about the return

packages/sdk/src/client.ts and packages/sdk/src/relay.ts

  • addListener and removeListener each get two declaration overloads + one implementation signature. The first overload matches the literal 'beforeAgentSpawn' and accepts BeforeAgentSpawnHandler; the second is the existing strict-void shape for every other event

packages/sdk/src/__tests__/lifecycle-hooks.test.ts

  • New regression test: a separately-typed const handler: BeforeAgentSpawnHandler = ... registers and unregisters via client.addListener / removeListener without any cast, then exercises the patch via a real spawn

Test plan

  • npx tsc -p tsconfig.json --noEmit in packages/sdk — clean
  • npx vitest run src/__tests__/event-bus.test.ts src/__tests__/lifecycle-hooks.test.ts — 23/23 pass (11 EventBus + 12 lifecycle-hook, +1 new regression)
  • Full CI suite
  • After this lands and republishes, Pear PR can drop the as Parameters<...> cast in src/main/broker.ts

Follow-up

Once this ships in @agent-relay/sdk (a minor bump per semver — only widens accepted handler types, no breaking changes), I'll open the Pear PR consuming the new SDK and stamping spawner=pear sessions in burn via beforeAgentSpawn.

🤖 Generated with Claude Code

`addListener` and `removeListener` previously typed every handler as
`(...args) => void | Promise<void>`, which rejected the one event that
is documented to allow a non-void return: `beforeAgentSpawn`. A
`BeforeAgentSpawnHandler` returns `void | SpawnPatch | Promise<void |
SpawnPatch>` so the dispatcher can shallow-merge the patch into the
spawn input before the broker POST.

Callers had to cast at the registration site to satisfy the type:

  client.addListener(
    'beforeAgentSpawn',
    burnHandler as Parameters<typeof client.addListener<'beforeAgentSpawn'>>[1]
  );

Adds dedicated overloads on `AgentRelay.addListener` /
`AgentRelayClient.addListener` (and their `remove` counterparts) that
accept `BeforeAgentSpawnHandler` when the event literal is
`'beforeAgentSpawn'`. All other events keep the strict `void` return
contract, so an `agentSpawned` handler that accidentally returns a
value still fails to type-check.

`EventBus.addListener` / `removeListener` / `listeners` get an optional
return-type generic `R` (default `void`) plus a widened internal Set
type so the bus can hold handlers from either overload — the
dispatcher in `runBeforeSpawn` already casts to
`Array<BeforeAgentSpawnHandler>` to capture patches, so runtime behavior
is unchanged.

Test added in `lifecycle-hooks.test.ts` exercises the new overload: a
separately-typed `BeforeAgentSpawnHandler` registers via
`client.addListener('beforeAgentSpawn', handler)` and unregisters via
`removeListener` without any cast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@willwashburn willwashburn requested a review from khaliqgant as a code owner May 21, 2026 20:29
@coderabbitai

coderabbitai Bot commented May 21, 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: 30fc1624-9296-4f91-9eea-0fa61399257f

📥 Commits

Reviewing files that changed from the base of the PR and between 23e07f0 and c13ee31.

📒 Files selected for processing (4)
  • packages/sdk/src/__tests__/lifecycle-hooks.test.ts
  • packages/sdk/src/client.ts
  • packages/sdk/src/event-bus.ts
  • packages/sdk/src/relay.ts

📝 Walkthrough

Walkthrough

EventBus infrastructure adds a generic return type to EventHandler to support typed handler values. AgentRelay and AgentRelayClient add dedicated TypeScript overloads for the 'beforeAgentSpawn' event using BeforeAgentSpawnHandler. Regression test verifies typed handlers work end-to-end without casting.

Changes

Typed handler returns and event-specific overloads

Layer / File(s) Summary
EventBus typed return infrastructure
packages/sdk/src/event-bus.ts
EventHandler gains a generic return type R (default void), and the internal handler registry stores handlers with return types erased to unknown to support heterogeneous handler types. addListener, removeListener, and listeners signatures are generalized to preserve and propagate the typed return parameter.
AgentRelay event-specific overloads
packages/sdk/src/relay.ts
AgentRelay imports BeforeAgentSpawnHandler and adds dedicated overloads for addListener/removeListener when event: 'beforeAgentSpawn', while keeping generic overloads for other AgentRelayEvents. Implementation signatures union BeforeAgentSpawnHandler to accept both event types.
AgentRelayClient event-specific overloads
packages/sdk/src/client.ts
AgentRelayClient adds dedicated overloads for 'beforeAgentSpawn' using BeforeAgentSpawnHandler, expands addListener documentation to clarify the handler's ability to return a SpawnPatch, and adjusts implementation signatures with proper casting when delegating to eventBus.addListener and eventBus.removeListener.
Regression test for typed handlers
packages/sdk/src/__tests__/lifecycle-hooks.test.ts
Imports BeforeAgentSpawnHandler and adds a test verifying that a typed BeforeAgentSpawnHandler can be passed to addListener without casting, is correctly applied as a SpawnPatch in spawn requests, can be removed via removeListener without casting, and ceases to apply after removal.

Sequence Diagram

sequenceDiagram
  participant Test
  participant AgentRelayClient
  participant EventBus
  participant Handler as BeforeAgentSpawnHandler
  
  Test->>AgentRelayClient: addListener('beforeAgentSpawn', typedHandler)
  AgentRelayClient->>EventBus: addListener('beforeAgentSpawn', typedHandler)
  EventBus->>EventBus: Store handler with erased return type
  EventBus-->>AgentRelayClient: unsubscribe function
  AgentRelayClient-->>Test: unsubscribe function
  
  Test->>AgentRelayClient: spawn(args)
  AgentRelayClient->>EventBus: emit('beforeAgentSpawn', spawnPayload)
  EventBus->>Handler: invoke with spawnPayload
  Handler-->>EventBus: SpawnPatch
  EventBus->>EventBus: Store return value
  EventBus-->>AgentRelayClient: void (ignores return)
  AgentRelayClient-->>Test: spawn with patch applied
  
  Test->>AgentRelayClient: removeListener('beforeAgentSpawn', typedHandler)
  AgentRelayClient->>EventBus: removeListener('beforeAgentSpawn', typedHandler)
  EventBus-->>AgentRelayClient: void
  AgentRelayClient-->>Test: void
  
  Test->>AgentRelayClient: spawn(args)
  AgentRelayClient->>EventBus: emit('beforeAgentSpawn', spawnPayload)
  EventBus-->>AgentRelayClient: void (no handlers)
  AgentRelayClient-->>Test: spawn without patch
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • AgentWorkforce/relay#936: Introduces the typed lifecycle hook surface and multi-listener/event-bus infrastructure for beforeAgentSpawn that this PR extends with typed handler return values and event-specific overloads.

Suggested reviewers

  • khaliqgant
  • barryollama

Poem

🐰 A rabbit's delight in typed returns so fine,
EventBus now holds values of every design,
BeforeAgentSpawn handlers, no casting required,
Return what they wish—SpawnPatches inspired!
Tests prove the journey, from add to remove,
Type safety in motion, a handler's groove. 🎯

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title clearly and specifically describes the main change: adding overloads for the addListener method to accept BeforeAgentSpawnHandler.
Description check ✅ Passed The pull request description comprehensively covers the summary, detailed explanation of changes across all affected files, and a clear test plan with specific commands executed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sdk-before-spawn-handler-overload

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 and usage tips.

@devin-ai-integration devin-ai-integration 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.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

@willwashburn willwashburn merged commit 66879e0 into main May 22, 2026
43 checks passed
@willwashburn willwashburn deleted the feat/sdk-before-spawn-handler-overload branch May 22, 2026 03:14
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