diff --git a/.changeset/fix-bb-agent-stream-result-tojson.md b/.changeset/fix-bb-agent-stream-result-tojson.md new file mode 100644 index 00000000..4a4cfc1f --- /dev/null +++ b/.changeset/fix-bb-agent-stream-result-tojson.md @@ -0,0 +1,7 @@ +--- +"@aws-blocks/bb-agent": patch +--- + +fix(bb-agent): add toJSON() to AgentStreamResult + +`AgentStreamResult` now serializes to `{ channelId, channel: null }` when returned from API methods. Previously `channel` serialized to an empty object `{}`; it is now explicitly `null` to signal it is server-side only. diff --git a/packages/bb-agent/API.md b/packages/bb-agent/API.md index 8b90feb1..85464c55 100644 --- a/packages/bb-agent/API.md +++ b/packages/bb-agent/API.md @@ -93,6 +93,10 @@ export interface AgentStreamResult { channel: Promise>; channelId: string; complete: () => Promise; + toJSON(): { + channelId: string; + channel: null; + }; } // @public diff --git a/packages/bb-agent/src/agent.ts b/packages/bb-agent/src/agent.ts index 211c81d6..b281118e 100644 --- a/packages/bb-agent/src/agent.ts +++ b/packages/bb-agent/src/agent.ts @@ -428,6 +428,7 @@ export class AgentBase extends Scope { } }); }), + toJSON() { return { channelId, channel: null }; }, }; } diff --git a/packages/bb-agent/src/index.test.ts b/packages/bb-agent/src/index.test.ts index 4c053a58..436f4ebc 100644 --- a/packages/bb-agent/src/index.test.ts +++ b/packages/bb-agent/src/index.test.ts @@ -115,6 +115,19 @@ describe('needsApproval and interrupt mutual exclusivity', () => { }); }); +// ── AgentStreamResult.toJSON() ─────────────────────────────────────────────── + +describe('AgentStreamResult.toJSON()', () => { + test('serializes to { channelId, channel: null }', async () => { + const scope = new Scope('test-tojson'); + const agent = new Agent(scope, 'tj', { systemPrompt: 'test', model: { deployed: { provider: 'canned' }, local: { provider: 'canned' } } }); + const result = await agent.stream('hello', { userId: 'test-user' }); + const serialized = JSON.parse(JSON.stringify(result)); + assert.deepStrictEqual(serialized, { channelId: result.channelId, channel: null }); + assert.strictEqual('complete' in serialized, false); + }); +}); + // ── tool factory enforcement (compile-time) ────────────────────────────────── describe('tool factory enforcement', () => { diff --git a/packages/bb-agent/src/types.ts b/packages/bb-agent/src/types.ts index 39f632cd..33ff49b7 100644 --- a/packages/bb-agent/src/types.ts +++ b/packages/bb-agent/src/types.ts @@ -264,14 +264,30 @@ export interface StreamOptions { context?: TContext; } -/** Returned by stream(). Provides the channelId and server-side convenience methods. */ +/** + * Returned by stream(). Provides the channelId and server-side convenience methods. + * + * Safe to return directly from API methods — `toJSON()` serializes to + * `{ channelId, channel: null }`. Only `channelId` is meaningful client-side; + * `channel` is explicitly `null` to signal the live handle is server-side only, + * and the `complete()` helper is dropped (functions don't serialize). + */ export interface AgentStreamResult { /** Realtime channel ID where chunks are published. */ channelId: string; - /** Realtime channel handle — subscribe to streaming chunks or return to client as Transferable. */ + /** + * Realtime channel handle (server-side only). Nulled by `toJSON()` — clients subscribe from `channelId` instead. + * + * @remarks + * Unlike `RealtimeChannel.toJSON()` which produces a hydratable descriptor, this is nulled + * because it's a `Promise` that can't round-trip. Clients reconstruct a subscribe-only + * channel from `channelId` via the `useChat` `subscribe` callback. + */ channel: Promise>; /** Wait for the complete response (server-side). Resolves when the done chunk arrives. */ complete: () => Promise; + /** Only `{ channelId, channel: null }` is serialized when this object crosses the RPC boundary. */ + toJSON(): { channelId: string; channel: null }; } export interface AgentStreamChunk {