Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fix-bb-agent-stream-result-tojson.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 4 additions & 0 deletions packages/bb-agent/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export interface AgentStreamResult {
channel: Promise<RealtimeChannel<AgentStreamChunk>>;
channelId: string;
complete: () => Promise<AgentStreamChunk>;
toJSON(): {
channelId: string;
channel: null;
};
}

// @public
Expand Down
1 change: 1 addition & 0 deletions packages/bb-agent/src/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
import { FileBucket } from '@aws-blocks/bb-file-bucket';
import { Logger } from '@aws-blocks/bb-logger';
import type { ChildLogger } from '@aws-blocks/bb-logger';
import { Agent as StrandsAgent, tool, SessionManager, ModelStreamUpdateEvent, AfterToolCallEvent, BeforeToolCallEvent, AgentResultEvent, InterruptEvent } from '@strands-agents/sdk';

Check warning on line 12 in packages/bb-agent/src/agent.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedImports

Several of these imports are unused.
import type { AgentResult } from '@strands-agents/sdk';

Check warning on line 13 in packages/bb-agent/src/agent.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedImports

This import is unused.
import { InterruptResponseContent } from '@strands-agents/sdk';
import { z } from 'zod';
import { createStrandsModel, checkModelHealth } from './model-factory.js';
import type { SnapshotStorage } from '@strands-agents/sdk';
import { messageSchema, conversationSchema, agentStreamChunkSchema } from './schemas.js';
import type { AgentConfig, AgentStreamChunk, AgentStreamResult, StreamOptions, Message, Conversation, TokenUsage, ConversationManagerConfig, ModelConfig, JSONValue, InterruptResponse, DefaultToolContext, AgentTool, ToolDefinition } from './types.js';

Check warning on line 19 in packages/bb-agent/src/agent.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/correctness/noUnusedImports

Several of these imports are unused.
import { AgentErrors, blocksAgentError, InterruptError } from './errors.js';
import { SlidingWindowConversationManager, SummarizingConversationManager } from '@strands-agents/sdk';
import { BB_NAME, BB_VERSION } from './version.js';
Expand Down Expand Up @@ -67,7 +67,7 @@
* @see https://strandsagents.com/docs/user-guide/concepts/agents/conversation-management/
*/
function createConversationManager(config?: ConversationManagerConfig) {
if (!config || !config.strategy || config.strategy === 'sliding-window') {

Check warning on line 70 in packages/bb-agent/src/agent.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/complexity/useOptionalChain

Change to an optional chain.
const windowSize = config && 'windowSize' in config ? config.windowSize : undefined;
return new SlidingWindowConversationManager({ windowSize });
}
Expand Down Expand Up @@ -313,13 +313,13 @@

const strandsTools = toolDefs.map(t => tool({
// `name` is always set by resolveTools() from the Record key.
name: t.name!,

Check warning on line 316 in packages/bb-agent/src/agent.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
description: t.description,
inputSchema: t.parameters,
callback: (input, context) => t.handler({
input,
context: readContext(context),
interrupt: (params) => context!.interrupt(params),

Check warning on line 322 in packages/bb-agent/src/agent.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
}),
}));

Expand Down Expand Up @@ -428,6 +428,7 @@
}
});
}),
toJSON() { return { channelId, channel: null }; },
};
}

Expand Down
13 changes: 13 additions & 0 deletions packages/bb-agent/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,19 @@
});
});

// ── 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', () => {
Expand Down Expand Up @@ -727,7 +740,7 @@

await chat.sendMessage('hello');
// Simulate error chunk from server
chunkHandler!({ type: 'error', error: 'model throttled' });

Check warning on line 743 in packages/bb-agent/src/index.test.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.

assert.strictEqual(errorReceived, 'model throttled');
assert.strictEqual(loadingStates.at(-1), false, 'loading should be false after error');
Expand All @@ -753,11 +766,11 @@
});

await chat.sendMessage('hello');
chunkHandler!({ type: 'interrupt', interrupts: [{ id: 'int-1', name: 'approve:deleteRecords', reason: { tool: 'deleteRecords' } }] });

Check warning on line 769 in packages/bb-agent/src/index.test.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.

assert.ok(interruptsReceived, 'onInterrupt should be called');
assert.strictEqual(interruptsReceived!.length, 1);

Check warning on line 772 in packages/bb-agent/src/index.test.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
assert.strictEqual(interruptsReceived![0].name, 'approve:deleteRecords');

Check warning on line 773 in packages/bb-agent/src/index.test.ts

View workflow job for this annotation

GitHub Actions / Build, Unit Tests, E2E Local

lint/style/noNonNullAssertion

Forbidden non-null assertion.
assert.strictEqual(loadingStates.at(-1), false, 'loading should be false after interrupt');
});

Expand Down
20 changes: 18 additions & 2 deletions packages/bb-agent/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,14 +264,30 @@ export interface StreamOptions<TContext = DefaultToolContext> {
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<RealtimeChannel<AgentStreamChunk>>;
/** Wait for the complete response (server-side). Resolves when the done chunk arrives. */
complete: () => Promise<AgentStreamChunk>;
/** Only `{ channelId, channel: null }` is serialized when this object crosses the RPC boundary. */
toJSON(): { channelId: string; channel: null };
}

export interface AgentStreamChunk {
Expand Down
Loading