From b6b8ca0ec37b3fbaf13b56eea3c637f77ccdc47b Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 14:41:56 +0200 Subject: [PATCH 01/12] feat(core, bb-kv-store): implement toAgentTools() for KVStore Add buildAgentTools() helper to core that handles filtering (include/ exclude), overrides, scope injection, and fixed values. BBs implement toAgentTools() by defining a tool registry and delegating to the helper. KVStore exposes get, put, delete, scan as agent tools: - put/delete require approval (needsApproval + trustable) - scan has a default limit of 100 entries - Shared tool registry in agent-tools.ts used by both mock and AWS runtimes --- packages/bb-kv-store/src/agent-tools.ts | 52 ++++++ packages/bb-kv-store/src/index.aws.ts | 8 + packages/bb-kv-store/src/index.mock.ts | 8 + packages/bb-kv-store/src/index.test.ts | 51 +++++- packages/core/src/agent-tools.test.ts | 182 ++++++++++++++++++++ packages/core/src/agent-tools.ts | 97 +++++++++++ packages/core/src/index.ts | 1 + test-apps/comprehensive/aws-blocks/index.ts | 23 +++ test-apps/comprehensive/test/agent.test.ts | 8 + 9 files changed, 428 insertions(+), 2 deletions(-) create mode 100644 packages/bb-kv-store/src/agent-tools.ts create mode 100644 packages/core/src/agent-tools.test.ts create mode 100644 packages/core/src/agent-tools.ts diff --git a/packages/bb-kv-store/src/agent-tools.ts b/packages/bb-kv-store/src/agent-tools.ts new file mode 100644 index 00000000..3831f9ff --- /dev/null +++ b/packages/bb-kv-store/src/agent-tools.ts @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { buildAgentTools } from '@aws-blocks/core'; +import type { AgentToolProviderOptions, ToolMethodDef } from '@aws-blocks/core'; +import type { Scope } from '@aws-blocks/core'; + +interface KVStoreLike { + get(key: string): Promise; + put(key: string, value: unknown): Promise; + delete(key: string): Promise; + scan(): AsyncIterable<{ key: string; value: unknown }>; +} + +export const KV_TOOL_METHODS: Record> = { + get: { + description: 'Retrieve a value by key', + parameters: { type: 'object', properties: { key: { type: 'string', description: 'The key to retrieve' } }, required: ['key'] }, + handler: (self) => async ({ input }) => self.get(input.key), + }, + put: { + description: 'Store a value at a key', + parameters: { type: 'object', properties: { key: { type: 'string', description: 'The key to store' }, value: { description: 'The value to store' } }, required: ['key', 'value'] }, + needsApproval: true, + trustable: true, + handler: (self) => async ({ input }) => { await self.put(input.key, input.value); return { success: true }; }, + }, + delete: { + description: 'Delete a key', + parameters: { type: 'object', properties: { key: { type: 'string', description: 'The key to delete' } }, required: ['key'] }, + needsApproval: true, + trustable: true, + handler: (self) => async ({ input }) => { await self.delete(input.key); return { success: true }; }, + }, + scan: { + description: 'List keys and values. Returns up to `limit` entries (default 100).', + parameters: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of entries to return (default 100)' } } }, + handler: (self) => async ({ input }) => { + const max = input.limit ?? 100; + const items: { key: string; value: unknown }[] = []; + for await (const entry of self.scan()) { + items.push(entry); + if (items.length >= max) break; + } + return items; + }, + }, +}; + +export function kvToAgentTools(self: Scope & KVStoreLike, options?: AgentToolProviderOptions): Record { + return buildAgentTools(self, KV_TOOL_METHODS, options); +} diff --git a/packages/bb-kv-store/src/index.aws.ts b/packages/bb-kv-store/src/index.aws.ts index 588f838e..a4e7a306 100644 --- a/packages/bb-kv-store/src/index.aws.ts +++ b/packages/bb-kv-store/src/index.aws.ts @@ -5,6 +5,8 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, GetCommand, PutCommand, DeleteCommand, ScanCommand } from '@aws-sdk/lib-dynamodb'; import { Scope, registerSdkIdentifiers, getSdkIdentifiers } from '@aws-blocks/core'; import type { ScopeParent } from '@aws-blocks/core'; +import type { AgentToolProviderOptions } from '@aws-blocks/core'; +import { kvToAgentTools } from './agent-tools.js'; import { Logger } from '@aws-blocks/bb-logger'; import type { ChildLogger } from '@aws-blocks/bb-logger'; import { BB_NAME, BB_VERSION } from './version.js'; @@ -159,6 +161,12 @@ export class KVStore extends Scope { } while (lastKey); } + // ── Agent tools ────────────────────────────────────────────────────── + + toAgentTools(options?: AgentToolProviderOptions): Record { + return kvToAgentTools(this, options); + } + /** * Wrap an existing DynamoDB table. KVStore will not create or manage * infrastructure for this table. diff --git a/packages/bb-kv-store/src/index.mock.ts b/packages/bb-kv-store/src/index.mock.ts index 76d2f59a..3e6bdfc8 100644 --- a/packages/bb-kv-store/src/index.mock.ts +++ b/packages/bb-kv-store/src/index.mock.ts @@ -3,6 +3,8 @@ import { Scope, registerSdkIdentifiers } from '@aws-blocks/core'; import type { ScopeParent } from '@aws-blocks/core'; +import type { AgentToolProviderOptions } from '@aws-blocks/core'; +import { kvToAgentTools } from './agent-tools.js'; import { Logger } from '@aws-blocks/bb-logger'; import type { ChildLogger } from '@aws-blocks/bb-logger'; import { getMockDataDir } from '@aws-blocks/core/bb-utils'; @@ -170,6 +172,12 @@ export class KVStore extends Scope { return { __brand: 'ExternalTableRef' as const, tableName }; } + // ── Agent tools ────────────────────────────────────────────────────── + + toAgentTools(options?: AgentToolProviderOptions): Record { + return kvToAgentTools(this, options); + } + // ── Disk persistence ────────────────────────────────────────────────── private loadFromDisk(): Map { diff --git a/packages/bb-kv-store/src/index.test.ts b/packages/bb-kv-store/src/index.test.ts index bd06e007..91143bac 100644 --- a/packages/bb-kv-store/src/index.test.ts +++ b/packages/bb-kv-store/src/index.test.ts @@ -1,10 +1,10 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { test, beforeEach } from 'node:test'; +import { test, beforeEach, describe } from 'node:test'; import assert from 'node:assert'; import { rmSync } from 'node:fs'; -import { isBlocksError } from '@aws-blocks/core'; +import { isBlocksError, Scope } from '@aws-blocks/core'; import { KVStore, KVStoreErrors } from './index.mock.js'; // Clean mock data between tests to avoid cross-contamination @@ -215,3 +215,50 @@ test('fullId generation with parent', () => { const store = new KVStore({ id: 'parent' } as any, 'child'); assert.strictEqual(store.fullId, 'parent-child'); }); + +// ── toAgentTools() ────────────────────────────────────────────────────────── + +describe('toAgentTools()', () => { + test('exposes get, put, delete, scan', () => { + const scope = new Scope('app'); + const store = new KVStore(scope, 'memory'); + const tools = store.toAgentTools(); + assert.deepStrictEqual(Object.keys(tools).sort(), ['memory__delete', 'memory__get', 'memory__put', 'memory__scan']); + }); + + test('get handler reads from the store', async () => { + const scope = new Scope('app'); + const store = new KVStore(scope, 'memory'); + await store.put('k', 'v'); + const tools = store.toAgentTools(); + const result = await tools['memory__get'].handler({ input: { key: 'k' }, context: {} }); + assert.strictEqual(result, 'v'); + }); + + test('put handler writes to the store', async () => { + const scope = new Scope('app'); + const store = new KVStore(scope, 'memory'); + const tools = store.toAgentTools(); + await tools['memory__put'].handler({ input: { key: 'k', value: 'v' }, context: {} }); + assert.strictEqual(await store.get('k'), 'v'); + }); + + test('scan handler collects all entries', async () => { + const scope = new Scope('app'); + const store = new KVStore(scope, 'memory'); + await store.put('a', '1'); + await store.put('b', '2'); + const tools = store.toAgentTools(); + const result = await tools['memory__scan'].handler({ input: {}, context: {} }) as any[]; + assert.strictEqual(result.length, 2); + }); + + test('scan handler respects limit', async () => { + const scope = new Scope('app'); + const store = new KVStore(scope, 'memory'); + for (let i = 0; i < 5; i++) await store.put(`key${i}`, `val${i}`); + const tools = store.toAgentTools(); + const result = await tools['memory__scan'].handler({ input: { limit: 2 }, context: {} }) as any[]; + assert.strictEqual(result.length, 2); + }); +}); diff --git a/packages/core/src/agent-tools.test.ts b/packages/core/src/agent-tools.test.ts new file mode 100644 index 00000000..011fd82c --- /dev/null +++ b/packages/core/src/agent-tools.test.ts @@ -0,0 +1,182 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { test, describe } from 'node:test'; +import assert from 'node:assert'; +import { buildAgentTools } from './agent-tools.js'; +import { Scope } from './common/index.js'; +import type { ToolMethodDef } from './agent-tools.js'; + +const scope = new Scope('test-app'); + +// Simulates a BB's tool registry — tests buildAgentTools logic in isolation +// so each BB only needs to test its own handlers, not filtering/scope/overrides. +const MOCK_METHODS: Record> = { + get: { + description: 'Retrieve a value', + parameters: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] }, + handler: (self) => async ({ input }) => `got:${input.key}`, + }, + put: { + description: 'Store a value', + parameters: { type: 'object', properties: { key: { type: 'string' }, value: {} }, required: ['key', 'value'] }, + needsApproval: true, + trustable: true, + handler: (self) => async ({ input }) => ({ stored: input.key }), + }, + delete: { + description: 'Delete a value', + parameters: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] }, + needsApproval: true, + handler: (self) => async ({ input }) => ({ deleted: input.key }), + }, +}; + +// Use a minimal Scope-like object for testing +const mockBB = new Scope('store', { parent: scope }); + +describe('buildAgentTools', () => { + test('returns all methods by default', () => { + const tools = buildAgentTools(mockBB, MOCK_METHODS); + assert.deepStrictEqual(Object.keys(tools).sort(), ['store__delete', 'store__get', 'store__put']); + }); + + test('tool naming is bbId__methodName', () => { + const bb = new Scope('my-store', { parent: scope }); + const tools = buildAgentTools(bb, MOCK_METHODS); + assert.ok('my-store__get' in tools); + assert.ok('my-store__put' in tools); + assert.ok('my-store__delete' in tools); + }); + + test('preserves description, parameters, needsApproval, trustable', () => { + const tools = buildAgentTools(mockBB, MOCK_METHODS); + assert.strictEqual(tools['store__get'].description, 'Retrieve a value'); + assert.strictEqual(tools['store__get'].needsApproval, false); + assert.strictEqual(tools['store__put'].needsApproval, true); + assert.strictEqual(tools['store__put'].trustable, true); + assert.strictEqual(tools['store__delete'].needsApproval, true); + assert.strictEqual(tools['store__delete'].trustable, undefined); + }); + + test('handler is a callable function', async () => { + const tools = buildAgentTools(mockBB, MOCK_METHODS); + const result = await tools['store__get'].handler({ input: { key: 'abc' }, context: {} }); + assert.strictEqual(result, 'got:abc'); + }); + + describe('include/exclude', () => { + test('include filters to specified methods', () => { + const tools = buildAgentTools(mockBB, MOCK_METHODS, { include: ['get'] }); + assert.deepStrictEqual(Object.keys(tools), ['store__get']); + }); + + test('exclude removes specified methods', () => { + const tools = buildAgentTools(mockBB, MOCK_METHODS, { exclude: ['delete'] }); + assert.deepStrictEqual(Object.keys(tools).sort(), ['store__get', 'store__put']); + }); + + test('include and exclude together throws', () => { + assert.throws( + () => buildAgentTools(mockBB, MOCK_METHODS, { include: ['get'], exclude: ['put'] }), + /mutually exclusive/, + ); + }); + }); + + describe('overrides', () => { + test('override description', () => { + const tools = buildAgentTools(mockBB, MOCK_METHODS, { + overrides: { get: { description: 'Custom description' } }, + }); + assert.strictEqual(tools['store__get'].description, 'Custom description'); + }); + + test('override needsApproval', () => { + const tools = buildAgentTools(mockBB, MOCK_METHODS, { + overrides: { get: { needsApproval: true } }, + }); + assert.strictEqual(tools['store__get'].needsApproval, true); + }); + + test('override schema replaces parameters', () => { + const customSchema = { type: 'object', properties: { id: { type: 'number' } } }; + const tools = buildAgentTools(mockBB, MOCK_METHODS, { + overrides: { get: { schema: customSchema } }, + }); + assert.deepStrictEqual(tools['store__get'].parameters, customSchema); + }); + + test('fixed values are injected into handler input', async () => { + const methods: Record> = { + query: { + description: 'Query items', + parameters: { type: 'object', properties: {} }, + handler: () => async ({ input }) => input, + }, + }; + const tools = buildAgentTools(mockBB, methods, { + overrides: { query: { fixed: { region: 'us-east-1' } } }, + }); + const result = await tools['store__query'].handler({ input: { status: 'active' }, context: {} }); + assert.deepStrictEqual(result, { status: 'active', region: 'us-east-1' }); + }); + }); + + describe('scope', () => { + test('scope injects context fields into input', async () => { + const methods: Record> = { + query: { + description: 'Query items', + parameters: { type: 'object', properties: {} }, + handler: () => async ({ input }) => input, + }, + }; + const tools = buildAgentTools(mockBB, methods, { + scope: (ctx: { userId: string }) => ({ userId: ctx.userId }), + }); + const result = await tools['store__query'].handler({ + input: { status: 'active' }, + context: { userId: 'user-123' }, + }); + assert.deepStrictEqual(result, { status: 'active', userId: 'user-123' }); + }); + + test('scope overrides user-provided input for scoped fields', async () => { + const methods: Record> = { + query: { + description: 'Query items', + parameters: { type: 'object', properties: {} }, + handler: () => async ({ input }) => input, + }, + }; + const tools = buildAgentTools(mockBB, methods, { + scope: (ctx: { userId: string }) => ({ userId: ctx.userId }), + }); + const result = await tools['store__query'].handler({ + input: { userId: 'attacker', status: 'active' }, + context: { userId: 'real-user' }, + }); + assert.strictEqual(result.userId, 'real-user'); + }); + + test('scope and fixed combine', async () => { + const methods: Record> = { + query: { + description: 'Query items', + parameters: { type: 'object', properties: {} }, + handler: () => async ({ input }) => input, + }, + }; + const tools = buildAgentTools(mockBB, methods, { + scope: (ctx: { userId: string }) => ({ userId: ctx.userId }), + overrides: { query: { fixed: { limit: 10 } } }, + }); + const result = await tools['store__query'].handler({ + input: { status: 'active' }, + context: { userId: 'user-123' }, + }); + assert.deepStrictEqual(result, { status: 'active', userId: 'user-123', limit: 10 }); + }); + }); +}); diff --git a/packages/core/src/agent-tools.ts b/packages/core/src/agent-tools.ts new file mode 100644 index 00000000..6ffd692e --- /dev/null +++ b/packages/core/src/agent-tools.ts @@ -0,0 +1,97 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Shared types and helper for BBs that expose operations as agent tools via `toAgentTools()`. + * Lives in core so any BB can implement the interface without depending on `bb-agent`. + */ + +import type { Scope } from './common/index.js'; + +// ── Types ─────────────────────────────────────────────────────────────────── + +export interface AgentToolProviderOptions { + /** Only expose these methods (from the BB's tool-eligible set). Mutually exclusive with `exclude`. */ + include?: string[]; + /** Expose all tool-eligible methods except these. Mutually exclusive with `include`. */ + exclude?: string[]; + /** Per-method overrides of the BB's defaults, keyed by method name. */ + overrides?: Record; + /** Maps context fields to fixed constraints applied across all methods. */ + scope?: (context: TContext) => Record; +} + +export interface MethodOverrides { + description?: string; + /** Pin parameter values — injected server-side, stripped from what the model sees. */ + fixed?: Record; + /** Narrow or replace the parameters schema the model sees. */ + schema?: unknown; + needsApproval?: boolean; + trustable?: boolean; +} + +/** A single tool-eligible method definition inside a BB's tool registry. */ +export interface ToolMethodDef { + description: string; + parameters: unknown; + needsApproval?: boolean; + trustable?: boolean; + handler: (self: TSelf) => (args: { input: any; context: any }) => Promise; +} + +/** Contract that BBs implement to expose operations as agent tools. */ +export interface AgentToolProvider { + toAgentTools(options?: AgentToolProviderOptions): Record; +} + +// ── Builder ───────────────────────────────────────────────────────────────── + +/** + * Build agent tools from a BB's tool method registry. + * Handles include/exclude filtering, overrides, and scope injection. + * + * Returns `Record` so the result can be spread directly into + * an Agent's `tools` callback without type conflicts. + */ +export function buildAgentTools( + self: TSelf, + toolMethods: Record>, + options?: AgentToolProviderOptions, +): Record { + if (options?.include && options?.exclude) { + throw new Error('toAgentTools: `include` and `exclude` are mutually exclusive'); + } + + const bbId = self.id; + const result: Record = {}; + + for (const [methodName, def] of Object.entries(toolMethods)) { + if (options?.include && !options.include.includes(methodName)) continue; + if (options?.exclude && options.exclude.includes(methodName)) continue; + + const override = options?.overrides?.[methodName]; + const description = override?.description ?? def.description; + const needsApproval = override?.needsApproval ?? def.needsApproval ?? false; + const trustable = override?.trustable ?? def.trustable; + const parameters = override?.schema ?? def.parameters; + + const baseHandler = def.handler(self); + const handler = async (args: { input: any; context: any }) => { + let input = args.input; + if (options?.scope && args.context) { + const scoped = options.scope(args.context); + input = { ...input, ...scoped }; + } + if (override?.fixed) { + input = { ...input, ...override.fixed }; + } + return baseHandler({ input, context: args.context }); + }; + + const toolName = `${bbId}__${methodName}`; + result[toolName] = { description, parameters, needsApproval, trustable, handler }; + } + + return result; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9af4f1b8..64cdb65e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,3 +20,4 @@ export { type RegisteredRoute, } from './raw-route.js'; export { RawRoute } from './raw-route.mock.js'; +export { buildAgentTools, type AgentToolProvider, type AgentToolProviderOptions, type MethodOverrides, type ToolMethodDef } from './agent-tools.js'; diff --git a/test-apps/comprehensive/aws-blocks/index.ts b/test-apps/comprehensive/aws-blocks/index.ts index 0fd92b6c..7c35202c 100644 --- a/test-apps/comprehensive/aws-blocks/index.ts +++ b/test-apps/comprehensive/aws-blocks/index.ts @@ -378,6 +378,23 @@ const cannedAgent = new Agent(scope, 'canned', { }); +// Agent with toAgentTools() — tests BB-provided tools alongside manual tools +const toolStore = new KVStore(scope, 'tool-store'); +const asToolAgent = new Agent(scope, 'astool', { + removalPolicy: 'destroy', + model: { deployed: { provider: 'canned' }, local: { provider: 'canned' } }, + systemPrompt: 'You are a test agent with BB-provided tools. Use tools when asked.', + tools: (tool) => ({ + ...toolStore.toAgentTools({ include: ['get', 'put'] }), + manualTool: tool({ + description: 'A manually defined echo tool', + parameters: z.object({ msg: z.string() }), + handler: async ({ input }) => ({ echo: input.msg }), + }), + }), +}); + + // Agent with model fallback — first candidate is unreachable, should fall through to canned const fallbackAgent = new Agent(scope, 'fallback', { removalPolicy: 'destroy', @@ -1657,6 +1674,12 @@ export const api = new ApiNamespace(scope, 'api', (context) => ({ return { messages: await cannedAgent.getConversation(conversationId) }; }, + // toAgentTools() e2e — BB-provided tools alongside manual tools + async asToolStream(message: string, conversationId?: string) { + const result = await asToolAgent.stream(message, { conversationId, userId: 'test-user' }); + return { channelId: result.channelId }; + }, + async fallbackStream(message: string) { const channelId = crypto.randomUUID(); const result = await fallbackAgent.stream(message, { channelId, userId: 'test-user' }); diff --git a/test-apps/comprehensive/test/agent.test.ts b/test-apps/comprehensive/test/agent.test.ts index 9704955e..6d56500b 100644 --- a/test-apps/comprehensive/test/agent.test.ts +++ b/test-apps/comprehensive/test/agent.test.ts @@ -197,6 +197,14 @@ export function agentTests(getApi: () => typeof apiType) { }); }); + describe('toAgentTools()', () => { + test('BB tools and manual tools coexist — stream completes', async () => { + const api = getApi(); + const { channelId } = await api.asToolStream('hello'); + assert.ok(channelId, 'should return a channelId'); + }); + }); + describe('Long-Running Agent (>29s)', () => { test('agent with slow tool completes beyond API Gateway timeout', async () => { From fe2529603835915e0bcb083219354b7e04fee8a8 Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 16:09:45 +0200 Subject: [PATCH 02/12] docs: add toAgentTools guide and e2e test for zod schema override - Agent BB README: full guide (filtering, overrides, fixed, supported BBs) - KVStore README: brief section pointing to Agent BB docs - Core docs: BB-author guide for implementing toAgentTools() - E2e test: zod schema override works with BB-provided tools - KVStore delete: trustable defaults to false (each deletion needs approval) --- docs/reference/building-block-structure.md | 74 ++++++++++++ packages/bb-agent/README.md | 124 ++++++++++++++++++++ packages/bb-kv-store/README.md | 14 +++ packages/bb-kv-store/src/agent-tools.ts | 2 +- test-apps/comprehensive/aws-blocks/index.ts | 17 +++ test-apps/comprehensive/test/agent.test.ts | 6 + 6 files changed, 236 insertions(+), 1 deletion(-) diff --git a/docs/reference/building-block-structure.md b/docs/reference/building-block-structure.md index d6164fd9..f678aad8 100644 --- a/docs/reference/building-block-structure.md +++ b/docs/reference/building-block-structure.md @@ -440,6 +440,80 @@ export class MyBlock { } ``` +## Agent Tool Support (`toAgentTools`) + +BBs can expose their operations as agent tools by implementing `toAgentTools()`. This lets users spread BB operations directly into an Agent's `tools` callback without writing manual tool definitions. + +### How to Implement + +1. Create a shared `agent-tools.ts` file with the tool registry (used by both mock and AWS runtimes): + +```typescript +// my-bb/src/agent-tools.ts +import { buildAgentTools } from '@aws-blocks/core'; +import type { AgentToolProviderOptions, ToolMethodDef } from '@aws-blocks/core'; +import type { Scope } from '@aws-blocks/core'; + +interface MyBBLike { + get(key: string): Promise; + put(key: string, value: unknown): Promise; +} + +export const MY_BB_TOOL_METHODS: Record> = { + get: { + description: 'Retrieve a value by key', + parameters: { type: 'object', properties: { key: { type: 'string', description: 'The key' } }, required: ['key'] }, + handler: (self) => async ({ input }) => self.get(input.key), + }, + put: { + description: 'Store a value', + parameters: { type: 'object', properties: { key: { type: 'string' }, value: {} }, required: ['key', 'value'] }, + needsApproval: true, + trustable: true, + handler: (self) => async ({ input }) => { await self.put(input.key, input.value); return { success: true }; }, + }, +}; + +export function myBBToAgentTools(self: Scope & MyBBLike, options?: AgentToolProviderOptions): Record { + return buildAgentTools(self, MY_BB_TOOL_METHODS, options); +} +``` + +2. Add `toAgentTools()` to both runtime classes (one-liner each): + +```typescript +// In index.mock.ts and index.aws.ts +import { myBBToAgentTools } from './agent-tools.js'; +import type { AgentToolProviderOptions } from '@aws-blocks/core'; + +export class MyBB extends Scope { + // ... existing methods ... + + toAgentTools(options?: AgentToolProviderOptions): Record { + return myBBToAgentTools(this, options); + } +} +``` + +### Guidelines + +- **Parameters use JSON Schema** — avoids adding zod as a dependency to your BB. Users can override with zod via the `overrides.schema` option. +- **Read operations** set `needsApproval: false`; **write operations** set `needsApproval: true` (and optionally `trustable: true`); **delete operations** set `needsApproval: true` and `trustable: false` (each deletion requires explicit approval). +- **Descriptions** should be concise and tell the LLM what the tool does and when to use it. +- **Handlers** should return JSON-serializable values. For `AsyncIterable` results (like `scan`), collect into an array with a default limit. +- **Tool names** are generated automatically as `{bbId}__{methodName}` by `buildAgentTools`. +- **The interface** (`MyBBLike`) ensures the tool registry stays in sync with the BB's public API. + +### What `buildAgentTools` Handles + +The `buildAgentTools` helper from `@aws-blocks/core` handles: +- `include`/`exclude` filtering +- `overrides` (description, needsApproval, trustable, schema, fixed) +- `scope` injection (merges context fields into handler input) +- Tool naming (`{bbId}__{methodName}`) + +You only need to define the tool registry and delegate. + ## Best Practices 1. **Keep interfaces consistent** - Runtime and mock should export the same interface diff --git a/packages/bb-agent/README.md b/packages/bb-agent/README.md index 43be853c..48c172ea 100644 --- a/packages/bb-agent/README.md +++ b/packages/bb-agent/README.md @@ -399,6 +399,8 @@ tools: (tool) => ({ The callback form lets TypeScript infer each tool's `input` from its `parameters`. The Record key is the tool's name. +> **Tip:** Some Building Blocks (e.g. KVStore) can expose their operations as agent tools automatically via `toAgentTools()` — no manual tool definitions needed. See [BB-Provided Tools](#bb-provided-tools-toagenttools) for details. + ### Tool Context — Scoping Tools to the Caller Tools often need request-scoped information (e.g. the authenticated `userId`). Pass a `context` object on each `stream()`/`resume()` call; it's forwarded to every tool invocation: @@ -674,6 +676,128 @@ await chat.respondToInterrupt([{ interruptId: 'x', approved: true }]); **Note:** `useChat` is a factory function, not a React hook. Call it **once** (e.g., outside a component or in a ref) — not on every render. It returns a mutable singleton. Message history only includes `user`, `assistant`, and `approval` messages — tool-call/tool-result internals are filtered for UI clarity. Use `getConversation()` directly if you need the full history. +## BB-Provided Tools (`toAgentTools`) + +Building Blocks that support agent tools expose a `toAgentTools()` method. Each BB provides sensible defaults for tool descriptions, parameter schemas, and approval settings — you can use them as-is or override any value. + +### Basic Usage + +Spread `toAgentTools()` into the `tools` callback alongside manual tools: + +```typescript +import { Agent, BedrockModels } from '@aws-blocks/bb-agent'; +import { KVStore } from '@aws-blocks/bb-kv-store'; +import { z } from 'zod'; + +const store = new KVStore(scope, 'memory'); + +const agent = new Agent(scope, 'assistant', { + model: { deployed: BedrockModels.BALANCED }, + systemPrompt: 'You are a helpful assistant.', + tools: (tool) => ({ + ...store.toAgentTools({ include: ['get', 'put'] }), + echo: tool({ + description: 'Echo back the user message', + parameters: z.object({ message: z.string() }), + handler: async ({ input }) => ({ reply: input.message }), + }), + }), +}); +``` + +Tool names follow the convention `{bbId}__{methodName}` (e.g. `memory__get`, `memory__put`). The BB provides defaults for each tool: + +| Property | Default provided by BB | Overridable? | +|---|---|---| +| `description` | Describes the operation | Yes, via `overrides` | +| `parameters` | Schema matching the BB method's input | Yes, via `overrides.schema` | +| `needsApproval` | Read operations: `false`, write/delete: `true` | Yes, via `overrides` | +| `trustable` | Write/delete: `true` (user can trust after first approval) | Yes, via `overrides` | +| `handler` | Calls the BB method directly | No (use a manual `tool()` for custom logic) | + +### Filtering + +Control which operations are exposed to the agent: + +```typescript +// Only read operations +store.toAgentTools({ include: ['get', 'scan'] }) + +// Everything except destructive operations +store.toAgentTools({ exclude: ['delete'] }) + +// All operations (default) +store.toAgentTools() +``` + +`include` and `exclude` are mutually exclusive. + +### Overrides + +Override any default per method. Each key in `overrides` is a method name: + +```typescript +store.toAgentTools({ + overrides: { + get: { + description: 'Look up a customer record by ID', + }, + put: { + needsApproval: false, // trust the agent to write without asking + trustable: false, + }, + scan: { + schema: z.object({ limit: z.number() }), + }, + }, +}) +``` + +Available override fields: + +| Field | Effect | +|---|---| +| `description` | Replace the tool description the model sees | +| `needsApproval` | Override whether the agent pauses for user approval | +| `trustable` | Override whether the user can trust the tool after first approval | +| `schema` | Replace the parameters schema the model sees | +| `fixed` | Inject static values into the input (hidden from the model) | + +### Fixed Values + +Pin a parameter to a constant value — the model never sees it: + +```typescript +store.toAgentTools({ + overrides: { + get: { + fixed: { key: 'config:app-settings' }, // always reads this key + }, + }, +}) +``` + +The model's tool has no `key` parameter — it's injected server-side on every call. + +> **Note:** If a tool requires custom logic or many overrides, a manual `tool()` definition is often simpler and easier to read. + +### Supported BBs + +| BB | Description | +|---|---| +| **KVStore** | Simple key-value storage — get, put, delete, and scan operations | + +More BBs will be added in future releases. Refer to each BB's README for details on its operations and configuration. + +#### KVStore + +| Method | Description | Parameters | `needsApproval` | `trustable` | +|---|---|---|---|---| +| `get` | Retrieve a value by key | `z.object({ key: z.string() })` | `false` | — | +| `put` | Store a value at a key | `z.object({ key: z.string(), value: z.unknown() })` | `true` | `true` | +| `delete` | Delete a key | `z.object({ key: z.string() })` | `true` | `false` | +| `scan` | List keys and values (default limit: 100) | `z.object({ limit: z.number().optional() })` | `false` | — | + ## Full Examples ### 1. End-to-End: Backend + Frontend with `useChat` diff --git a/packages/bb-kv-store/README.md b/packages/bb-kv-store/README.md index 30467823..64b20be2 100644 --- a/packages/bb-kv-store/README.md +++ b/packages/bb-kv-store/README.md @@ -129,6 +129,20 @@ const legacy = new KVStore(scope, 'legacy', { }); ``` +## Agent Tools + +KVStore supports `toAgentTools()` to expose its operations as tools for the [Agent BB](../bb-agent/README.md#bb-provided-tools-toagenttools). Available methods: `get`, `put`, `delete`, `scan`. + +```typescript +const agent = new Agent(scope, 'assistant', { + tools: (tool) => ({ + ...store.toAgentTools({ include: ['get', 'put'] }), + }), +}); +``` + +See the [Agent BB documentation](../bb-agent/README.md#bb-provided-tools-toagenttools) for filtering, overrides, and full usage. + ## Best Practices - Keep keys short and descriptive (e.g., `user:{id}`, `session:{token}`) diff --git a/packages/bb-kv-store/src/agent-tools.ts b/packages/bb-kv-store/src/agent-tools.ts index 3831f9ff..d88f0301 100644 --- a/packages/bb-kv-store/src/agent-tools.ts +++ b/packages/bb-kv-store/src/agent-tools.ts @@ -29,7 +29,7 @@ export const KV_TOOL_METHODS: Record> = { description: 'Delete a key', parameters: { type: 'object', properties: { key: { type: 'string', description: 'The key to delete' } }, required: ['key'] }, needsApproval: true, - trustable: true, + trustable: false, handler: (self) => async ({ input }) => { await self.delete(input.key); return { success: true }; }, }, scan: { diff --git a/test-apps/comprehensive/aws-blocks/index.ts b/test-apps/comprehensive/aws-blocks/index.ts index 7c35202c..e8c32a61 100644 --- a/test-apps/comprehensive/aws-blocks/index.ts +++ b/test-apps/comprehensive/aws-blocks/index.ts @@ -395,6 +395,19 @@ const asToolAgent = new Agent(scope, 'astool', { }); +// Agent with toAgentTools() + zod schema override — tests that zod replaces JSON Schema parameters +const zodOverrideAgent = new Agent(scope, 'astool-zod', { + removalPolicy: 'destroy', + model: { deployed: { provider: 'canned' }, local: { provider: 'canned' } }, + systemPrompt: 'You are a test agent.', + tools: (tool) => ({ + ...toolStore.toAgentTools({ + include: ['get'], + overrides: { get: { schema: z.object({ key: z.string().describe('The record key to look up') }) } }, + }), + }), +}); + // Agent with model fallback — first candidate is unreachable, should fall through to canned const fallbackAgent = new Agent(scope, 'fallback', { removalPolicy: 'destroy', @@ -1679,6 +1692,10 @@ export const api = new ApiNamespace(scope, 'api', (context) => ({ const result = await asToolAgent.stream(message, { conversationId, userId: 'test-user' }); return { channelId: result.channelId }; }, + async asToolZodOverrideStream(message: string) { + const result = await zodOverrideAgent.stream(message, { userId: 'test-user' }); + return { channelId: result.channelId }; + }, async fallbackStream(message: string) { const channelId = crypto.randomUUID(); diff --git a/test-apps/comprehensive/test/agent.test.ts b/test-apps/comprehensive/test/agent.test.ts index 6d56500b..c8e1d670 100644 --- a/test-apps/comprehensive/test/agent.test.ts +++ b/test-apps/comprehensive/test/agent.test.ts @@ -203,6 +203,12 @@ export function agentTests(getApi: () => typeof apiType) { const { channelId } = await api.asToolStream('hello'); assert.ok(channelId, 'should return a channelId'); }); + + test('zod schema override works with BB tools — stream completes', async () => { + const api = getApi(); + const { channelId } = await api.asToolZodOverrideStream('hello'); + assert.ok(channelId, 'should return a channelId'); + }); }); From f5849d7229e8f36eec8e2c31bcbaca720d1426ee Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 18:54:38 +0200 Subject: [PATCH 03/12] fix(bb-kv-store): add toAgentTools synthGuard to CDK and browser stubs Without this, apps calling store.toAgentTools() fail at cdk synth with a cryptic TypeError instead of the actionable synthGuard message. --- packages/bb-kv-store/src/index.browser.ts | 3 +++ packages/bb-kv-store/src/index.cdk.test.ts | 2 +- packages/bb-kv-store/src/index.cdk.ts | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/bb-kv-store/src/index.browser.ts b/packages/bb-kv-store/src/index.browser.ts index 3216dff1..cfd1ebc2 100644 --- a/packages/bb-kv-store/src/index.browser.ts +++ b/packages/bb-kv-store/src/index.browser.ts @@ -4,5 +4,8 @@ // Browser stub - KVStore runs server-side only export class KVStore { constructor(...args: any[]) {} + toAgentTools(..._args: any[]): never { + throw new Error('KVStore.toAgentTools() is not available in the browser.'); + } } export { KVStoreErrors } from './errors.js'; diff --git a/packages/bb-kv-store/src/index.cdk.test.ts b/packages/bb-kv-store/src/index.cdk.test.ts index 5df52a5f..903a5407 100644 --- a/packages/bb-kv-store/src/index.cdk.test.ts +++ b/packages/bb-kv-store/src/index.cdk.test.ts @@ -67,7 +67,7 @@ test('CDK: KVStore.fromExisting returns a branded ref', () => { test('CDK: calling a runtime data method throws an actionable error (not a cryptic TypeError)', () => { const { parent } = setup(); const store = new KVStore(parent, 'sessions') as any; - for (const method of ['get', 'put', 'delete', 'scan']) { + for (const method of ['get', 'put', 'delete', 'scan', 'toAgentTools']) { assert.throws( () => store[method]('k'), /cannot be called during CDK synth/, diff --git a/packages/bb-kv-store/src/index.cdk.ts b/packages/bb-kv-store/src/index.cdk.ts index ff44a1b9..ddc15651 100644 --- a/packages/bb-kv-store/src/index.cdk.ts +++ b/packages/bb-kv-store/src/index.cdk.ts @@ -60,4 +60,5 @@ export class KVStore extends Scope { put(..._args: unknown[]): never { return synthGuard('KVStore', 'put'); } delete(..._args: unknown[]): never { return synthGuard('KVStore', 'delete'); } scan(..._args: unknown[]): never { return synthGuard('KVStore', 'scan'); } + toAgentTools(..._args: unknown[]): never { return synthGuard('KVStore', 'toAgentTools'); } } From 500d3fd658280ba0d1652e88695eb336038b20e0 Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 19:02:52 +0200 Subject: [PATCH 04/12] chore: add changeset for toAgentTools (minor bump core + bb-kv-store) --- .changeset/add-to-agent-tools.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/add-to-agent-tools.md diff --git a/.changeset/add-to-agent-tools.md b/.changeset/add-to-agent-tools.md new file mode 100644 index 00000000..8a27e97d --- /dev/null +++ b/.changeset/add-to-agent-tools.md @@ -0,0 +1,6 @@ +--- +"@aws-blocks/core": minor +"@aws-blocks/bb-kv-store": minor +--- + +Add `toAgentTools()` API for Building Blocks to expose their operations as agent tools. Core provides `buildAgentTools()` helper with filtering, overrides, and scope injection. KVStore exposes get, put, delete, and scan as agent tools with approval controls. From e5bca09545dfbbd51645f40df278dcc3d64f8047 Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 19:09:12 +0200 Subject: [PATCH 05/12] =?UTF-8?q?docs(bb-agent):=20fix=20trustable=20defau?= =?UTF-8?q?lts=20table=20=E2=80=94=20delete=20is=20false,=20clarify=20word?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/bb-agent/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bb-agent/README.md b/packages/bb-agent/README.md index 48c172ea..0818d92a 100644 --- a/packages/bb-agent/README.md +++ b/packages/bb-agent/README.md @@ -712,7 +712,7 @@ Tool names follow the convention `{bbId}__{methodName}` (e.g. `memory__get`, `me | `description` | Describes the operation | Yes, via `overrides` | | `parameters` | Schema matching the BB method's input | Yes, via `overrides.schema` | | `needsApproval` | Read operations: `false`, write/delete: `true` | Yes, via `overrides` | -| `trustable` | Write/delete: `true` (user can trust after first approval) | Yes, via `overrides` | +| `trustable` | Write: `true` (agent may write without reasking); delete: `false` (each call needs approval) | Yes, via `overrides` | | `handler` | Calls the BB method directly | No (use a manual `tool()` for custom logic) | ### Filtering From 043da6c73d4337a0f48eb1d01dfb99c8ae4d5a39 Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 19:13:28 +0200 Subject: [PATCH 06/12] docs(bb-kv-store): add Agent Tools section to DESIGN.md --- packages/bb-kv-store/DESIGN.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/bb-kv-store/DESIGN.md b/packages/bb-kv-store/DESIGN.md index 7145921a..ec5cb126 100644 --- a/packages/bb-kv-store/DESIGN.md +++ b/packages/bb-kv-store/DESIGN.md @@ -32,6 +32,27 @@ When `options.schema` is provided (any `StandardSchemaV1` implementation — Zod - Schema validation on `put()` when configured, throws `ValidationFailedException`. - Validates the 400 KB item size limit; throws `ItemTooLargeException` (`KVStoreErrors.ItemTooLarge`) on oversized items. On AWS, DynamoDB raises a generic `ValidationException`; the runtime narrows on the size-specific message and re-maps only that case to `ItemTooLarge`, so both layers surface the same `error.name`. +## Agent Tools + +`toAgentTools(options?)` exposes KVStore operations as agent tools via the shared `KV_TOOL_METHODS` registry in `agent-tools.ts`. Both mock and AWS runtimes delegate to `kvToAgentTools()` which calls core's `buildAgentTools()` helper. + +### Tool Registry (`KV_TOOL_METHODS`) + +| Method | `needsApproval` | `trustable` | Notes | +|--------|-----------------|-------------|-------| +| `get` | `false` | — | Read-only | +| `put` | `true` | `true` | Agent can repeat writes without re-prompting | +| `delete` | `true` | `false` | Each deletion requires explicit approval | +| `scan` | `false` | — | Default limit of 100 entries; collects AsyncIterable into array | + +### Parameters + +Tool parameters use JSON Schema objects (not zod). Callers can override parameters with a zod schema via `overrides: { methodName: { schema: z.object({...}) } }` — the Agent BB accepts both JSON Schema and `z.ZodType`. + +### Scan Default Limit + +`scan` caps results at 100 entries by default to prevent unbounded responses. The agent can pass `{ limit: N }` to override. The handler breaks out of the AsyncIterable once the limit is reached. + ### Mock vs AWS Behavior Differences | Behavior Difference | Impact | Mitigation | From ddd8a187f3a9ea09c99d9954dbf513cbcd0b7311 Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 19:25:21 +0200 Subject: [PATCH 07/12] fix(core): document scope/fixed precedence, add collision test, explain handler any --- packages/bb-agent/README.md | 2 ++ packages/core/src/agent-tools.test.ts | 21 ++++++++++++++++++++- packages/core/src/agent-tools.ts | 7 ++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/bb-agent/README.md b/packages/bb-agent/README.md index 0818d92a..93f6e0d0 100644 --- a/packages/bb-agent/README.md +++ b/packages/bb-agent/README.md @@ -779,6 +779,8 @@ store.toAgentTools({ The model's tool has no `key` parameter — it's injected server-side on every call. +> **Precedence:** When the same key appears in multiple sources, `fixed` wins over `scope` which wins over model input. + > **Note:** If a tool requires custom logic or many overrides, a manual `tool()` definition is often simpler and easier to read. ### Supported BBs diff --git a/packages/core/src/agent-tools.test.ts b/packages/core/src/agent-tools.test.ts index 011fd82c..8ae91666 100644 --- a/packages/core/src/agent-tools.test.ts +++ b/packages/core/src/agent-tools.test.ts @@ -160,7 +160,7 @@ describe('buildAgentTools', () => { assert.strictEqual(result.userId, 'real-user'); }); - test('scope and fixed combine', async () => { + test('scope and fixed combine on disjoint keys', async () => { const methods: Record> = { query: { description: 'Query items', @@ -178,5 +178,24 @@ describe('buildAgentTools', () => { }); assert.deepStrictEqual(result, { status: 'active', userId: 'user-123', limit: 10 }); }); + + test('fixed wins over scope when both target the same key', async () => { + const methods: Record> = { + query: { + description: 'Query items', + parameters: { type: 'object', properties: {} }, + handler: () => async ({ input }) => input, + }, + }; + const tools = buildAgentTools(mockBB, methods, { + scope: (ctx: { tenant: string }) => ({ tenant: ctx.tenant }), + overrides: { query: { fixed: { tenant: 'pinned-tenant' } } }, + }); + const result = await tools['store__query'].handler({ + input: { status: 'active' }, + context: { tenant: 'context-tenant' }, + }); + assert.strictEqual(result.tenant, 'pinned-tenant'); + }); }); }); diff --git a/packages/core/src/agent-tools.ts b/packages/core/src/agent-tools.ts index 6ffd692e..bd2fd49c 100644 --- a/packages/core/src/agent-tools.ts +++ b/packages/core/src/agent-tools.ts @@ -17,7 +17,10 @@ export interface AgentToolProviderOptions { exclude?: string[]; /** Per-method overrides of the BB's defaults, keyed by method name. */ overrides?: Record; - /** Maps context fields to fixed constraints applied across all methods. */ + /** + * Injects request-scoped fields (e.g. userId) into tool input from context. + * Precedence when the same key appears in multiple sources: fixed > scope > model input. + */ scope?: (context: TContext) => Record; } @@ -37,6 +40,8 @@ export interface ToolMethodDef { parameters: unknown; needsApproval?: boolean; trustable?: boolean; + // `input: any` because parameters are JSON Schema objects — no compile-time type link. + // Core avoids a zod dependency, so we can't derive input types from the schema. handler: (self: TSelf) => (args: { input: any; context: any }) => Promise; } From 52f90019887358bcdfacdc7d5691919488290ec3 Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 20:00:04 +0200 Subject: [PATCH 08/12] chore: regenerate API reports for core and bb-kv-store --- packages/bb-kv-store/API.md | 3 +++ packages/core/API.md | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/packages/bb-kv-store/API.md b/packages/bb-kv-store/API.md index fb1404b5..cc8384ea 100644 --- a/packages/bb-kv-store/API.md +++ b/packages/bb-kv-store/API.md @@ -4,6 +4,7 @@ ```ts +import type { AgentToolProviderOptions } from '@aws-blocks/core'; import type { ChildLogger } from '@aws-blocks/bb-logger'; import { Scope } from '@aws-blocks/core'; import type { ScopeParent } from '@aws-blocks/core'; @@ -44,6 +45,8 @@ export class KVStore extends Scope { key: string; value: T; }>; + // (undocumented) + toAgentTools(options?: AgentToolProviderOptions): Record; } // @public diff --git a/packages/core/API.md b/packages/core/API.md index 4cc4b63e..451fc448 100644 --- a/packages/core/API.md +++ b/packages/core/API.md @@ -4,6 +4,20 @@ ```ts +// @public +export interface AgentToolProvider { + // (undocumented) + toAgentTools(options?: AgentToolProviderOptions): Record; +} + +// @public (undocumented) +export interface AgentToolProviderOptions { + exclude?: string[]; + include?: string[]; + overrides?: Record; + scope?: (context: TContext) => Record; +} + // @public export class ApiError extends Error { constructor(message: string, status: number, options?: { @@ -47,6 +61,9 @@ export type BlocksContext = { }; }; +// @public +export function buildAgentTools(self: TSelf, toolMethods: Record>, options?: AgentToolProviderOptions): Record; + // @public export interface BuildingBlockMeta { readonly bbName: string; @@ -95,6 +112,18 @@ export function matchRoute(method: string, path: string): { params: Record; } | null; +// @public (undocumented) +export interface MethodOverrides { + // (undocumented) + description?: string; + fixed?: Record; + // (undocumented) + needsApproval?: boolean; + schema?: unknown; + // (undocumented) + trustable?: boolean; +} + // @public export function preloadConfig(): Promise; @@ -185,6 +214,23 @@ export type ScopeParent = Scope | { id: string; }; +// @public +export interface ToolMethodDef { + // (undocumented) + description: string; + // (undocumented) + handler: (self: TSelf) => (args: { + input: any; + context: any; + }) => Promise; + // (undocumented) + needsApproval?: boolean; + // (undocumented) + parameters: unknown; + // (undocumented) + trustable?: boolean; +} + // @public export function unlockRouteRegistry(): void; From 7c221b7b180a34152c00c1564148428b3cd44ffa Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 20:08:53 +0200 Subject: [PATCH 09/12] docs(bb-kv-store): simplify parameters section in DESIGN.md --- packages/bb-kv-store/DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bb-kv-store/DESIGN.md b/packages/bb-kv-store/DESIGN.md index ec5cb126..8fe07380 100644 --- a/packages/bb-kv-store/DESIGN.md +++ b/packages/bb-kv-store/DESIGN.md @@ -47,7 +47,7 @@ When `options.schema` is provided (any `StandardSchemaV1` implementation — Zod ### Parameters -Tool parameters use JSON Schema objects (not zod). Callers can override parameters with a zod schema via `overrides: { methodName: { schema: z.object({...}) } }` — the Agent BB accepts both JSON Schema and `z.ZodType`. +Tool parameters are defined as JSON Schema objects. Users can override with zod via `overrides: { methodName: { schema: z.object({...}) } }`. ### Scan Default Limit From 495ff5c4c1e713cc8f0b9dc672103b800fd2e0a8 Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 20:11:12 +0200 Subject: [PATCH 10/12] docs(core): explain Record return type tradeoff in buildAgentTools --- packages/core/src/agent-tools.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/agent-tools.ts b/packages/core/src/agent-tools.ts index bd2fd49c..ff8885df 100644 --- a/packages/core/src/agent-tools.ts +++ b/packages/core/src/agent-tools.ts @@ -56,8 +56,9 @@ export interface AgentToolProvider { * Build agent tools from a BB's tool method registry. * Handles include/exclude filtering, overrides, and scope injection. * - * Returns `Record` so the result can be spread directly into - * an Agent's `tools` callback without type conflicts. + * Returns `Record` — this bypasses the Agent BB's branded AgentTool type + * check. Core can't import the brand without a circular dep, so shape/override errors + * are caught at runtime, not compile time. The tool() factory remains the type-safe path. */ export function buildAgentTools( self: TSelf, From e7226f2e946f78f302f9158ae518aa6811b65e87 Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 20:21:24 +0200 Subject: [PATCH 11/12] fix(bb-kv-store): add compile-time drift check for KVStoreLike interface --- packages/bb-kv-store/src/agent-tools.ts | 70 +++++++++++++++++++------ 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/packages/bb-kv-store/src/agent-tools.ts b/packages/bb-kv-store/src/agent-tools.ts index d88f0301..2a5a7ee6 100644 --- a/packages/bb-kv-store/src/agent-tools.ts +++ b/packages/bb-kv-store/src/agent-tools.ts @@ -1,9 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import type { AgentToolProviderOptions, Scope, ToolMethodDef } from '@aws-blocks/core'; import { buildAgentTools } from '@aws-blocks/core'; -import type { AgentToolProviderOptions, ToolMethodDef } from '@aws-blocks/core'; -import type { Scope } from '@aws-blocks/core'; +import type { KVStore } from './index.mock.js'; interface KVStoreLike { get(key: string): Promise; @@ -15,38 +15,74 @@ interface KVStoreLike { export const KV_TOOL_METHODS: Record> = { get: { description: 'Retrieve a value by key', - parameters: { type: 'object', properties: { key: { type: 'string', description: 'The key to retrieve' } }, required: ['key'] }, - handler: (self) => async ({ input }) => self.get(input.key), + parameters: { + type: 'object', + properties: { key: { type: 'string', description: 'The key to retrieve' } }, + required: ['key'], + }, + handler: + (self) => + async ({ input }) => + self.get(input.key), }, put: { description: 'Store a value at a key', - parameters: { type: 'object', properties: { key: { type: 'string', description: 'The key to store' }, value: { description: 'The value to store' } }, required: ['key', 'value'] }, + parameters: { + type: 'object', + properties: { + key: { type: 'string', description: 'The key to store' }, + value: { description: 'The value to store' }, + }, + required: ['key', 'value'], + }, needsApproval: true, trustable: true, - handler: (self) => async ({ input }) => { await self.put(input.key, input.value); return { success: true }; }, + handler: + (self) => + async ({ input }) => { + await self.put(input.key, input.value); + return { success: true }; + }, }, delete: { description: 'Delete a key', - parameters: { type: 'object', properties: { key: { type: 'string', description: 'The key to delete' } }, required: ['key'] }, + parameters: { + type: 'object', + properties: { key: { type: 'string', description: 'The key to delete' } }, + required: ['key'], + }, needsApproval: true, trustable: false, - handler: (self) => async ({ input }) => { await self.delete(input.key); return { success: true }; }, + handler: + (self) => + async ({ input }) => { + await self.delete(input.key); + return { success: true }; + }, }, scan: { description: 'List keys and values. Returns up to `limit` entries (default 100).', - parameters: { type: 'object', properties: { limit: { type: 'number', description: 'Maximum number of entries to return (default 100)' } } }, - handler: (self) => async ({ input }) => { - const max = input.limit ?? 100; - const items: { key: string; value: unknown }[] = []; - for await (const entry of self.scan()) { - items.push(entry); - if (items.length >= max) break; - } - return items; + parameters: { + type: 'object', + properties: { limit: { type: 'number', description: 'Maximum number of entries to return (default 100)' } }, }, + handler: + (self) => + async ({ input }) => { + const max = input.limit ?? 100; + const items: { key: string; value: unknown }[] = []; + for await (const entry of self.scan()) { + items.push(entry); + if (items.length >= max) break; + } + return items; + }, }, }; export function kvToAgentTools(self: Scope & KVStoreLike, options?: AgentToolProviderOptions): Record { return buildAgentTools(self, KV_TOOL_METHODS, options); } + +// Compile-time check: fails the build if KVStoreLike drifts from the real KVStore class. +const _kvStoreSatisfiesInterface: KVStoreLike = {} as KVStore; From 6b6ed6f925bc46bc83858da505a5d78c6004b610 Mon Sep 17 00:00:00 2001 From: Paul Jakob Kroker Date: Tue, 23 Jun 2026 20:28:17 +0200 Subject: [PATCH 12/12] chore: add bb-agent patch to changeset (docs-only change) --- .changeset/add-to-agent-tools.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/add-to-agent-tools.md b/.changeset/add-to-agent-tools.md index 8a27e97d..e121f862 100644 --- a/.changeset/add-to-agent-tools.md +++ b/.changeset/add-to-agent-tools.md @@ -1,6 +1,7 @@ --- "@aws-blocks/core": minor "@aws-blocks/bb-kv-store": minor +"@aws-blocks/bb-agent": patch --- Add `toAgentTools()` API for Building Blocks to expose their operations as agent tools. Core provides `buildAgentTools()` helper with filtering, overrides, and scope injection. KVStore exposes get, put, delete, and scan as agent tools with approval controls.