Skip to content
Draft
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/add-to-agent-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
74 changes: 74 additions & 0 deletions docs/reference/building-block-structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;
put(key: string, value: unknown): Promise<void>;
}

export const MY_BB_TOOL_METHODS: Record<string, ToolMethodDef<MyBBLike>> = {
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<string, any> {
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<string, any> {
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
Expand Down
126 changes: 126 additions & 0 deletions packages/bb-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -674,6 +676,130 @@ 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: `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

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.

> **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

| 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`
Expand Down
3 changes: 3 additions & 0 deletions packages/bb-kv-store/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -44,6 +45,8 @@ export class KVStore<T = string> extends Scope {
key: string;
value: T;
}>;
// (undocumented)
toAgentTools(options?: AgentToolProviderOptions): Record<string, any>;
}

// @public
Expand Down
21 changes: 21 additions & 0 deletions packages/bb-kv-store/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 are defined as JSON Schema objects. Users can override with zod via `overrides: { methodName: { schema: z.object({...}) } }`.

### 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 |
Expand Down
14 changes: 14 additions & 0 deletions packages/bb-kv-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down
88 changes: 88 additions & 0 deletions packages/bb-kv-store/src/agent-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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 { KVStore } from './index.mock.js';

interface KVStoreLike {
get(key: string): Promise<unknown>;
put(key: string, value: unknown): Promise<void>;
delete(key: string): Promise<void>;
scan(): AsyncIterable<{ key: string; value: unknown }>;
}

export const KV_TOOL_METHODS: Record<string, ToolMethodDef<KVStoreLike>> = {
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: false,
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<string, any> {
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;
Loading
Loading