Skip to content

Commit f946736

Browse files
authored
fix(bb-agent): reject empty channelId in stream() (#47)
1 parent 5d2cd0e commit f946736

5 files changed

Lines changed: 30 additions & 3 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@aws-blocks/bb-agent": patch
3+
---
4+
5+
fix(bb-agent): treat empty channelId as unset in stream()
6+
7+
An empty `channelId` now falls back to `conversationId` or a random UUID, preventing all streams from sharing the same channel. Empty strings are treated as unset rather than used literally.

packages/bb-agent/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const agent = new Agent(scope, id, config)
4949
| `getPendingInterrupts(conversationId)` | `Promise<Array<...>>` | Get unanswered interrupts (for reload support). |
5050
| `getChannel(channelId)` | `Promise<RealtimeChannel>` | Get a Realtime channel for subscribing to chunks. |
5151

52-
`stream()` submits the message to AsyncJob and returns immediately — no API Gateway timeout risk. The agent runs asynchronously and publishes chunks to Realtime.
52+
`stream()` submits the message to AsyncJob and returns immediately — no API Gateway timeout risk. The agent runs asynchronously and publishes chunks to Realtime. The channel ID is resolved as `options.channelId || options.conversationId || crypto.randomUUID()` — empty strings are treated as unset and fall through to the next value.
5353

5454
**Important: Subscribe before sending.** The agent starts emitting chunks immediately after `stream()` is called. If you subscribe to the channel after calling `stream()`, early chunks may be dropped. Always subscribe first, await `established`, then send:
5555

packages/bb-agent/src/agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ export class AgentBase<TContext = DefaultToolContext> extends Scope {
403403
*/
404404
async stream(message: string, options?: StreamOptions<TContext>): Promise<AgentStreamResult> {
405405
const conversationId = options?.conversationId;
406-
const channelId = options?.channelId ?? conversationId ?? crypto.randomUUID();
406+
const channelId = options?.channelId || conversationId || crypto.randomUUID();
407407
if (!options?.userId && !this.config.inferenceOnly) throw blocksAgentError(AgentErrors.PersistenceRequired, 'userId is required when persistence is enabled. Pass it via options.userId.');
408408
const userId = options?.userId ?? 'anonymous';
409409
const context = this.resolveContext(options?.context);

packages/bb-agent/src/index.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,26 @@ describe('needsApproval and interrupt mutual exclusivity', () => {
115115
});
116116
});
117117

118+
// ── stream() empty channelId fallback ────────────────────────────────────────
119+
120+
describe('stream() empty channelId fallback', () => {
121+
test('empty channelId is treated as unset', async () => {
122+
const scope = new Scope('test-empty-ch');
123+
const agent = new Agent(scope, 'ec', { systemPrompt: 'test', model: { deployed: { provider: 'canned' }, local: { provider: 'canned' } } });
124+
const result = await agent.stream('hello', { userId: 'test-user', channelId: '' });
125+
assert.notStrictEqual(result.channelId, '');
126+
assert.ok(result.channelId.length > 0);
127+
});
128+
129+
test('empty conversationId is treated as unset', async () => {
130+
const scope = new Scope('test-empty-conv');
131+
const agent = new Agent(scope, 'ev', { systemPrompt: 'test', model: { deployed: { provider: 'canned' }, local: { provider: 'canned' } } });
132+
const result = await agent.stream('hello', { userId: 'test-user', conversationId: '' });
133+
assert.notStrictEqual(result.channelId, '');
134+
assert.ok(result.channelId.length > 0);
135+
});
136+
});
137+
118138
// ── tool factory enforcement (compile-time) ──────────────────────────────────
119139

120140
describe('tool factory enforcement', () => {

packages/bb-agent/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ export interface ToolCallRecord {
251251

252252
export interface StreamOptions<TContext = DefaultToolContext> {
253253
conversationId?: string;
254-
/** Channel ID for Realtime delivery. Defaults to conversationId or a random UUID. */
254+
/** Channel ID for Realtime delivery. Defaults to conversationId or a random UUID. Empty strings are treated as unset. */
255255
channelId?: string;
256256
/** User ID for conversation scoping. Defaults to 'anonymous'. */
257257
userId?: string;

0 commit comments

Comments
 (0)