feat(sdk): addListener overload accepts BeforeAgentSpawnHandler#941
Conversation
`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>
|
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 (4)
📝 WalkthroughWalkthroughEventBus infrastructure adds a generic return type to EventHandler to support typed handler values. AgentRelay and AgentRelayClient add dedicated TypeScript overloads for the ChangesTyped handler returns and event-specific overloads
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 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 |
Summary
AgentRelay.addListener/AgentRelayClient.addListener(andremoveListener) that acceptBeforeAgentSpawnHandlerwhen the event literal is'beforeAgentSpawn'(...args) => void | Promise<void>contract, so a stray return value on, say,agentSpawnedstill fails to type-checkWhy
In v7.0.0 the new
beforeAgentSpawnhook was documented to allow handlers to return aSpawnPatchthat the dispatcher folds into the spawn input:…and the SDK's
runBeforeSpawnalready reads each handler's return and shallow-merges patches into the resolved input. But the publicaddListenersignature was typed as the strict universal shape(...args) => void | Promise<void>, so registering aBeforeAgentSpawnHandler-typed function required a cast at every call site: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.tsEventHandler<Args, R = void>gains an optional return-type generic; defaultR = voidpreserves the strict observe-only contract for callers that don't specify itEventBus.addListener<K, R>/removeListener<K, R>/listeners<K, R>threadRthrough so events that allow non-void returns are still typed preciselyEventHandler<Args, unknown>so one Set can hold handlers from either overload — the dispatcher inrunBeforeSpawnalready casts toArray<BeforeAgentSpawnHandler>when it cares about the returnpackages/sdk/src/client.tsandpackages/sdk/src/relay.tsaddListenerandremoveListenereach get two declaration overloads + one implementation signature. The first overload matches the literal'beforeAgentSpawn'and acceptsBeforeAgentSpawnHandler; the second is the existing strict-voidshape for every other eventpackages/sdk/src/__tests__/lifecycle-hooks.test.tsconst handler: BeforeAgentSpawnHandler = ...registers and unregisters viaclient.addListener/removeListenerwithout any cast, then exercises the patch via a real spawnTest plan
npx tsc -p tsconfig.json --noEmitinpackages/sdk— cleannpx 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)as Parameters<...>cast insrc/main/broker.tsFollow-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 stampingspawner=pearsessions in burn viabeforeAgentSpawn.🤖 Generated with Claude Code