Skip to content

Commit f9b4839

Browse files
authored
Merge pull request #936 from AgentWorkforce/feat/sdk-lifecycle-listeners-v7
feat(sdk): typed multi-listener registry replaces on* callback fields
2 parents fc67c13 + 4936b05 commit f9b4839

37 files changed

Lines changed: 1775 additions & 720 deletions

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Breaking Changes
1111

12+
- `@agent-relay/sdk`: `AgentRelay` events move to a multi-listener registry. Use `relay.addListener('x', handler)` / `removeListener` in place of `relay.onX = handler` — the 13 `on*` fields (`onMessageReceived`, `onMessageSent`, `onAgentSpawned`, `onAgentReleased`, `onAgentExited`, `onAgentReady`, `onWorkerOutput`, `onDeliveryUpdate`, `onAgentExitRequested`, `onAgentIdle`, `onAgentActivityChanged`, `onChannelSubscribed`, `onChannelUnsubscribed`) are removed.
13+
- `@agent-relay/sdk`: `channelSubscribed` / `channelUnsubscribed` handlers receive a single `{ agent, channels }` object instead of positional `(agent, channels)` args.
14+
- `@agent-relay/sdk`: new `beforeAgentSpawn` / `afterAgentSpawn` / `beforeAgentRelease` / `afterAgentRelease` call-site hooks. `beforeAgentSpawn` listeners may return a `SpawnPatch` (shallow-merged in registration order) to mutate the spawn input before the broker POST.
1215
- Broker/SDK wire protocol is now version 2 for delivery terminal events and lifecycle event shape changes.
1316
- `relay.spawn({ task })` now returns `success: false` and terminates the agent when task delivery fails after retries.
1417
- `agent-relay send` now uses the orchestrator identity by default so `agent-relay replies <worker>` can correlate worker DMs.
@@ -19,6 +22,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1922
- Pass `--from` to `agent-relay send` when a script requires a specific sender identity.
2023
- Handle `success: false` from `relay.spawn()` calls that pass `task`; spawns without a task are unchanged.
2124
- Set `POSTHOG_PROJECT_KEY` in GitHub Actions repository variables before publishing telemetry-enabled artifacts.
25+
- Update relay event handlers from field-assignment to `addListener`:
26+
27+
```ts
28+
// Before
29+
relay.onAgentSpawned = (agent) => log(agent.name);
30+
31+
// After
32+
const off = relay.addListener('agentSpawned', (agent) => log(agent.name));
33+
// ...later: off(); // unsubscribe
34+
```
35+
36+
Channel subscribe/unsubscribe handlers receive an object: `({ agent, channels }) => ...`.
2237

2338
### Added
2439

@@ -115,67 +130,88 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
115130
## [6.3.3] - 2026-05-21
116131

117132
### Product Perspective
133+
118134
#### User-Impacting Fixes
135+
119136
- Detect opencode api key completion (#934) (#934)
120137

121138
### Technical Perspective
139+
122140
#### Releases
141+
123142
- v6.3.3
124143

125144
---
126145

127146
## [6.3.2] - 2026-05-20
128147

129148
### Product Perspective
149+
130150
#### User-Impacting Fixes
151+
131152
- Stop worker stderr from rendering inside agent xterm (#931) (#931)
132153

133154
### Technical Perspective
155+
134156
#### Releases
157+
135158
- v6.3.2
136159

137160
---
138161

139162
## [6.3.1] - 2026-05-20
140163

141164
### Product Perspective
165+
142166
#### User-Impacting Fixes
167+
143168
- Pre-register Claude PTY workers so Relaycast MCP boots fast (#926)
144169

145170
### Technical Perspective
171+
146172
#### Dependencies & Tooling
173+
147174
- Retrigger flaky macOS Rust Tests
148175
- Drop change-implying framing from PTY pre-register note
149176

150177
#### Releases
178+
151179
- v6.3.1
152180

153181
---
154182

155183
## [6.3.0] - 2026-05-20
156184

157185
### Technical Perspective
186+
158187
#### Releases
188+
159189
- v6.3.0
160190

161191
---
162192

163193
## [6.2.8] - 2026-05-20
164194

165195
### Product Perspective
196+
166197
#### User-Impacting Fixes
198+
167199
- Tighten PTY chrome scrubbing, document idle override, tame stale-state warning (#930) (#930)
168200

169201
### Technical Perspective
202+
170203
#### Releases
204+
171205
- v6.2.8
172206

173207
---
174208

175209
## [6.2.7] - 2026-05-20
176210

177211
### Technical Perspective
212+
178213
#### Releases
214+
179215
- v6.2.7
180216

181217
---

packages/acp-bridge/src/acp-agent.ts

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,7 @@
77
import { randomUUID } from 'node:crypto';
88
import * as acp from '@agentclientprotocol/sdk';
99
import { AgentRelay, type Agent, type Message } from '@agent-relay/sdk';
10-
import type {
11-
ACPBridgeConfig,
12-
SessionState,
13-
RelayMessage,
14-
BridgePromptResult,
15-
} from './types.js';
10+
import type { ACPBridgeConfig, SessionState, RelayMessage, BridgePromptResult } from './types.js';
1611

1712
/**
1813
* Bounded circular cache for message deduplication.
@@ -145,15 +140,15 @@ export class RelayACPAgent implements acp.Agent {
145140
private setupRelayHandlers(): void {
146141
if (!this.relay) return;
147142

148-
this.relay.onMessageReceived = (msg: Message) => {
143+
this.relay.addListener('messageReceived', (msg: Message) => {
149144
this.handleRelayMessage({
150145
id: msg.eventId,
151146
from: msg.from,
152147
body: msg.text,
153148
thread: msg.threadId,
154149
timestamp: Date.now(),
155150
});
156-
};
151+
});
157152
}
158153

159154
/**
@@ -408,7 +403,7 @@ export class RelayACPAgent implements acp.Agent {
408403
};
409404
}
410405

411-
if (!await this.ensureRelayReady()) {
406+
if (!(await this.ensureRelayReady())) {
412407
await this.connection.sessionUpdate({
413408
sessionId,
414409
update: {
@@ -454,7 +449,7 @@ export class RelayACPAgent implements acp.Agent {
454449

455450
// Send "thinking" indicator with target info
456451
const targetInfo = hasTargets
457-
? `Sending to ${targets.map(t => `@${t}`).join(', ')}...\n\n`
452+
? `Sending to ${targets.map((t) => `@${t}`).join(', ')}...\n\n`
458453
: 'Broadcasting to all agents...\n\n';
459454

460455
await this.connection.sessionUpdate({
@@ -503,13 +498,13 @@ export class RelayACPAgent implements acp.Agent {
503498
await this.connection.sessionUpdate({
504499
sessionId,
505500
update: {
506-
sessionUpdate: 'agent_message_chunk',
507-
content: {
508-
type: 'text',
509-
text: 'Failed to send message to relay agents. Please check the relay broker connection.',
510-
},
501+
sessionUpdate: 'agent_message_chunk',
502+
content: {
503+
type: 'text',
504+
text: 'Failed to send message to relay agents. Please check the relay broker connection.',
511505
},
512-
});
506+
},
507+
});
513508

514509
return {
515510
success: false,
@@ -731,7 +726,7 @@ export class RelayACPAgent implements acp.Agent {
731726
return true;
732727
}
733728

734-
if (!await this.ensureRelayReady()) {
729+
if (!(await this.ensureRelayReady())) {
735730
await this.sendTextUpdate(sessionId, 'Relay broker is not connected (cannot spawn).');
736731
return true;
737732
}
@@ -769,7 +764,7 @@ export class RelayACPAgent implements acp.Agent {
769764
return true;
770765
}
771766

772-
if (!await this.ensureRelayReady()) {
767+
if (!(await this.ensureRelayReady())) {
773768
await this.sendTextUpdate(sessionId, 'Relay broker is not connected (cannot release).');
774769
return true;
775770
}
@@ -794,7 +789,7 @@ export class RelayACPAgent implements acp.Agent {
794789
}
795790

796791
private async handleListAgentsCommand(sessionId: string): Promise<boolean> {
797-
if (!await this.ensureRelayReady()) {
792+
if (!(await this.ensureRelayReady())) {
798793
await this.sendTextUpdate(sessionId, 'Relay broker is not connected (cannot list agents).');
799794
return true;
800795
}

packages/openclaw/src/spawn/process.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class ProcessSpawnProvider implements SpawnProvider {
132132
},
133133
cwd: workspacePath,
134134
stdio: ['pipe', 'pipe', 'pipe'],
135-
},
135+
}
136136
);
137137

138138
gatewayProcess.stderr?.on('data', (data: Buffer) => {
@@ -176,22 +176,22 @@ export class ProcessSpawnProvider implements SpawnProvider {
176176
cli: 'node',
177177
args: [bridgePath],
178178
channels,
179-
task: options.systemPrompt
180-
? `${options.systemPrompt}\n\n${identityTask}`
181-
: identityTask,
179+
task: options.systemPrompt ? `${options.systemPrompt}\n\n${identityTask}` : identityTask,
182180
});
183181

184-
relay.onAgentExited = (agent) => {
185-
process.stderr.write(`[spawn:${options.name}] Agent exited: ${agent.name} code=${agent.exitCode ?? 'none'}\n`);
186-
};
182+
relay.addListener('agentExited', (agent) => {
183+
process.stderr.write(
184+
`[spawn:${options.name}] Agent exited: ${agent.name} code=${agent.exitCode ?? 'none'}\n`
185+
);
186+
});
187187
} catch (err) {
188188
// If SDK broker spawn fails, clean up gateway and propagate
189189
gatewayProcess.kill('SIGTERM');
190190
if (relay) {
191191
await relay.shutdown().catch(() => {});
192192
}
193193
throw new Error(
194-
`Failed to start broker for "${options.name}": ${err instanceof Error ? err.message : String(err)}`,
194+
`Failed to start broker for "${options.name}": ${err instanceof Error ? err.message : String(err)}`
195195
);
196196
}
197197

packages/sdk/src/__tests__/agent-activity.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ function wireRelay(relay: AgentRelay, client: ReturnType<typeof createMockFacade
2727
(relay as any).wireEvents(client);
2828
}
2929

30-
describe('AgentRelay onAgentActivityChanged', () => {
30+
describe('AgentRelay agentActivityChanged listener', () => {
3131
it('emits active on first delivery event', () => {
3232
const relay = new AgentRelay();
3333
const { client, emit } = createMockFacadeClient();
3434
wireRelay(relay, client);
3535
const changes: AgentActivityChange[] = [];
36-
relay.onAgentActivityChanged = (change) => changes.push(change);
36+
relay.addListener('agentActivityChanged', (change) => changes.push(change));
3737

3838
emit({ kind: 'delivery_queued', name: 'worker-1', delivery_id: 'd1', event_id: 'e1', timestamp: 1 });
3939

@@ -53,7 +53,7 @@ describe('AgentRelay onAgentActivityChanged', () => {
5353
const { client, emit } = createMockFacadeClient();
5454
wireRelay(relay, client);
5555
const changes: AgentActivityChange[] = [];
56-
relay.onAgentActivityChanged = (change) => changes.push(change);
56+
relay.addListener('agentActivityChanged', (change) => changes.push(change));
5757

5858
emit({ kind: 'delivery_queued', name: 'worker-1', delivery_id: 'd1', event_id: 'e1', timestamp: 1 });
5959
emit({ kind: 'delivery_injected', name: 'worker-1', delivery_id: 'd1', event_id: 'e1', timestamp: 2 });
@@ -69,7 +69,7 @@ describe('AgentRelay onAgentActivityChanged', () => {
6969
const { client, emit } = createMockFacadeClient();
7070
wireRelay(relay, client);
7171
const changes: AgentActivityChange[] = [];
72-
relay.onAgentActivityChanged = (change) => changes.push(change);
72+
relay.addListener('agentActivityChanged', (change) => changes.push(change));
7373

7474
emit({ kind: 'delivery_queued', name: 'worker-1', delivery_id: 'd1', event_id: 'e1', timestamp: 1 });
7575
emit({
@@ -94,7 +94,7 @@ describe('AgentRelay onAgentActivityChanged', () => {
9494
const { client, emit } = createMockFacadeClient();
9595
wireRelay(relay, client);
9696
const changes: AgentActivityChange[] = [];
97-
relay.onAgentActivityChanged = (change) => changes.push(change);
97+
relay.addListener('agentActivityChanged', (change) => changes.push(change));
9898

9999
emit({ kind: 'delivery_queued', name: 'worker-1', delivery_id: 'd1', event_id: 'e1', timestamp: 1 });
100100
emit({ kind: 'agent_idle', name: 'worker-1', idle_secs: 30 });
@@ -113,7 +113,7 @@ describe('AgentRelay onAgentActivityChanged', () => {
113113
const { client, emit } = createMockFacadeClient();
114114
wireRelay(relay, client);
115115
const changes: AgentActivityChange[] = [];
116-
relay.onAgentActivityChanged = (change) => changes.push(change);
116+
relay.addListener('agentActivityChanged', (change) => changes.push(change));
117117

118118
emit({ kind: 'delivery_queued', name: 'worker-1', delivery_id: 'd1', event_id: 'e1', timestamp: 1 });
119119
emit({ kind: 'agent_idle', name: 'worker-1', idle_secs: 30 });
@@ -142,7 +142,7 @@ describe('AgentRelay onAgentActivityChanged', () => {
142142
const { client, emit } = createMockFacadeClient();
143143
wireRelay(relay, client);
144144
const changes: AgentActivityChange[] = [];
145-
relay.onAgentActivityChanged = (change) => changes.push(change);
145+
relay.addListener('agentActivityChanged', (change) => changes.push(change));
146146

147147
emit({ kind: 'delivery_queued', name: 'worker-1', delivery_id: 'd1', event_id: 'e1', timestamp: 1 });
148148
emit({ kind: 'delivery_queued', name: 'worker-1', delivery_id: 'd2', event_id: 'e2', timestamp: 2 });
@@ -166,7 +166,7 @@ describe('AgentRelay onAgentActivityChanged', () => {
166166
const { client, emit } = createMockFacadeClient();
167167
wireRelay(relay, client);
168168
const changes: AgentActivityChange[] = [];
169-
relay.onAgentActivityChanged = (change) => changes.push(change);
169+
relay.addListener('agentActivityChanged', (change) => changes.push(change));
170170

171171
emit({ kind: 'delivery_queued', name: 'worker-1', delivery_id: 'd1', event_id: 'e1', timestamp: 1 });
172172
emit({ kind: 'agent_exited', name: 'worker-1', code: 0, signal: undefined });

packages/sdk/src/__tests__/builder-resume-persistence.test.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,7 @@ vi.mock('@relaycast/sdk', () => ({
5555
const mockRelayInstance = {
5656
shutdown: vi.fn().mockResolvedValue(undefined),
5757
onBrokerStderr: vi.fn().mockReturnValue(() => {}),
58-
onMessageReceived: null as any,
59-
onAgentSpawned: null as any,
60-
onAgentReleased: null as any,
61-
onAgentExited: null as any,
62-
onAgentIdle: null as any,
63-
onWorkerOutput: null as any,
64-
onDeliveryUpdate: null as any,
58+
addListener: vi.fn(() => () => {}),
6559
};
6660

6761
vi.mock('../relay.js', () => ({

0 commit comments

Comments
 (0)