Overview
Implement a tool helper function system for TypeScript that allows developers to easily define tools that can be used by Strands agents. This implementation is based on the architectural decision to use Zod 4 with a helper function approach.
Parent Task
This is a sub-task of #40 (Task 06: Create Tool Decorator System)
Architecture Decision Summary
After evaluating TypeBox vs Zod and considering the repository context, the following approach was chosen:
- Schema Library: Zod 4 (includes built-in JSON Schema export)
- API Style:
tool() helper function (no decorators)
- Context Injection: Always included as 2nd parameter in callbacks (optional for direct calls)
- Base Implementation: Leverage existing
FunctionTool class
- Dependencies: Zod as production dependency
Rationale
- Zod 4 provides both type inference AND JSON Schema export (best of both worlds)
- Helper function approach avoids experimental TypeScript decorators
- Always including context parameter simplifies the API and matches Python SDK behavior
- Building on
FunctionTool ensures consistency with existing patterns
Clarified Requirements
Design Decisions (From Review Discussion)
1. Schema Support
- ✅ Zod schemas only for initial implementation
- ✅ Design API to allow future JSON Schema support via function overloading
- ✅ Type inference only works with Zod schemas
2. Callable Behavior
The tool instance supports two usage modes:
Direct Invocation (for testing/standalone use):
const result = await myTool(input, context?)
// - Validates input with Zod
// - Returns unwrapped callback value
// - Throws on validation or execution error
// - Context parameter is optional
Stream Invocation (Tool interface for agent use):
for await (const event of myTool.stream(context)) {
// - Validates input with Zod
// - Returns ToolResult (wrapped)
// - Catches all errors into error ToolResult
// - Context from agent is always provided
}
Rationale: This asymmetric pattern (validate input, unwrap output) is consistent with TypeScript best practices (Zod, tRPC, GraphQL). See research comment for detailed analysis.
3. Zod Version
- ✅ Use Zod 4.1.12 (latest stable as of October 2025)
- ✅ Zod 4 has built-in JSON Schema export via
.toJsonSchema()
- ✅ No need for
zod-to-json-schema library
4. Zod Refinements and Transforms
- ✅ Refinements (
.refine()) and transforms (.transform()) are used in validation
- ✅ Leverage Zod's native JSON Schema generation (refinements/transforms may not appear in schema)
- ✅ Apply transforms before passing to callback (Zod handles via
.parse())
- ✅ Document limitations in TSDoc
5. Context Parameter
- ✅ Always passed as 2nd parameter to callback
- ✅ Optional for direct invocation (not all tools need it)
- ✅ Required for
.stream() method (provided by agent)
- ✅ Contains:
toolUse and invocationState
6. Test Coverage
- ✅ Focus on integration testing (our code's behavior)
- ✅ Test common Zod schema types and features
- ✅ Don't extensively test Zod's validation logic (that's Zod's responsibility)
- ✅ 80%+ code coverage required
Work Required
1. Add Zod Dependency
Files to modify:
package.json - Add Zod to dependencies
2. Create Tool Helper Function
New file: src/tools/tool-helper.ts
Implement tool() helper function with the following signature:
import { z } from 'zod'
import type { Tool, ToolContext } from './tool'
interface ToolConfig<TInput extends z.ZodType> {
name: string
description: string
inputSchema: TInput
callback: (
input: z.infer<TInput>,
context?: ToolContext
) => Promise<unknown> | AsyncGenerator<unknown, unknown, never> | unknown
}
export function tool<TInput extends z.ZodType>(
config: ToolConfig<TInput>
): Tool & ((input: z.infer<TInput>, context?: ToolContext) => ReturnType<typeof config.callback>) {
// Implementation details
}
Key requirements:
- Accept Zod schema for type-safe input validation
- Convert Zod schema to JSON Schema for
toolSpec.inputSchema
- Wrap callback in
FunctionTool or extend it appropriately
- Return a Tool instance that is also callable as a function
- Always pass context as 2nd parameter to callback (optional in type)
- Context parameter is optional for direct calls, required for stream calls
- Handle validation errors gracefully
- Support sync, async, and async generator callbacks
3. Schema Conversion
Function: Convert Zod schemas to JSON Schema format
function zodToJsonSchema(schema: z.ZodType): JSONSchema {
// Use Zod 4's built-in JSON Schema export
return schema.toJsonSchema()
}
Requirements:
- Use Zod 4's built-in
.toJsonSchema() method
- Refinements and transforms are used in validation but may not appear in generated schema
- Generate proper OpenAPI-compatible JSON Schema
4. Input Validation
Behavior:
- Validate input against Zod schema before calling callback
- For
.stream() calls: On validation failure, return error ToolResult (don't throw)
- For direct calls: On validation failure, throw error
- Include clear error messages indicating which fields failed validation
const result = schema.safeParse(input)
if (!result.success) {
// For stream: return createValidationErrorResult(result.error, toolUseId)
// For direct call: throw validation error
}
5. Context Parameter Injection
Requirements:
- Context is ALWAYS the 2nd parameter (but optional in type signature)
- Context parameter is optional for direct invocation
- Context parameter is required for
.stream() method invocation
- Context includes:
toolUse: Contains name, toolUseId, and input
invocationState: Caller-provided state
- Properly type the context parameter for TypeScript inference
callback: (input: z.infer<TInput>, context?: ToolContext) => ...
6. Tool Instance with Callable Behavior
Requirements:
- Returned object implements
Tool interface
- Returned object is also callable as a function
- Direct call: Validates input, returns unwrapped value, throws on error
- Stream call: Validates input, returns ToolResult, catches all errors
- Supports both
.stream() method and direct invocation
const myTool = tool({ ... })
// Works as Tool interface
for await (const event of myTool.stream(context)) { ... }
// Also works as function (context optional)
const result = await myTool(input)
const result2 = await myTool(input, context)
7. Streaming Support
Requirements:
- Detect callback type (sync, async, async generator)
- Wrap non-generator functions to work with
stream() method
- Pass through async generator yields as ToolStreamEvents
- Wrap final return value in ToolResult
- Leverage existing
FunctionTool streaming logic where possible
8. Error Handling
Requirements:
- For
.stream() calls: Always catch errors in callbacks and return error ToolResults
- For direct calls: Let errors throw (no catching)
- Return errors as error ToolResults for stream calls
- Preserve error messages and stack traces for debugging
- Consistent with existing
FunctionTool error handling
9. Testing
New file: src/tools/__tests__/tool-helper.test.ts
Test coverage required:
- Tool creation with various Zod schemas
- Type inference works correctly
- JSON Schema generation from Zod schemas using
.toJsonSchema()
- Input validation success and failure cases
- Context parameter is optional for direct calls
- Context parameter is always passed correctly to callback
- Callable behavior (direct function invocation)
- Direct calls validate input and throw on error
- Direct calls return unwrapped values
- Direct calls work with and without context
- Stream method behavior
- Stream calls validate input and return error ToolResult
- Stream calls return wrapped ToolResult
- Error handling differences between direct and stream calls
- Sync, async, and async generator callbacks
- Complex Zod schemas (nested objects, arrays, unions, etc.)
- Zod refinements and custom error messages
- Zod transforms are applied before callback
- Integration with existing Tool interface
- 80%+ code coverage
10. Documentation
Files to update:
src/tools/tool-helper.ts - Comprehensive TSDoc comments
src/tools/index.ts - Export tool helper
README.md - Add usage examples
AGENTS.md - Document tool helper patterns
Example usage for documentation:
import { z } from 'zod'
import { tool } from '@strands-agents/sdk'
// Basic tool
const calculator = tool({
name: 'calculator',
description: 'Performs arithmetic operations',
inputSchema: z.object({
operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
a: z.number().describe('First operand'),
b: z.number().describe('Second operand')
}),
callback: (input, context) => {
console.log('Tool use ID:', context?.toolUse.toolUseId)
switch (input.operation) {
case 'add': return input.a + input.b
case 'subtract': return input.a - input.b
case 'multiply': return input.a * input.b
case 'divide': return input.a / input.b
}
}
})
// Async tool with streaming
const fetchData = tool({
name: 'fetch_data',
description: 'Fetches data from an API',
inputSchema: z.object({
url: z.string().url(),
method: z.enum(['GET', 'POST']).default('GET')
}),
callback: async function* (input, context) {
yield 'Fetching data...'
const response = await fetch(input.url, { method: input.method })
yield 'Processing response...'
const data = await response.json()
return data
}
})
// Tool with complex schema and transforms
const createUser = tool({
name: 'create_user',
description: 'Creates a new user',
inputSchema: z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
age: z.number().int().positive().optional(),
name: z.string().transform(s => s.trim().toLowerCase()),
roles: z.array(z.enum(['admin', 'user', 'guest'])).default(['user'])
}),
callback: async (input, context) => {
const userId = context?.invocationState.userId
// Create user logic (name will be transformed to lowercase)
return { id: 'user-123', ...input }
}
})
// Use as Tool interface (in agent)
for await (const event of calculator.stream(toolContext)) {
console.log(event)
}
// Or call directly (for testing/standalone)
const result = await calculator({ operation: 'add', a: 5, b: 3 }) // No context needed
const result2 = await calculator({ operation: 'add', a: 5, b: 3 }, context) // With context
Document both usage modes clearly:
/**
* Creates a tool from a Zod schema and callback function.
*
* The returned tool can be used in two ways:
*
* 1. **Direct invocation** (testing/standalone):
* - Validates input and throws on validation error
* - Returns unwrapped callback value
* - Context parameter is optional
*
* 2. **Stream invocation** (agent use):
* - Validates input and returns error ToolResult on failure
* - Returns wrapped ToolResult
* - Context parameter is required (provided by agent)
*
* @example
* const myTool = tool({ ... })
*
* // Direct call
* const result = await myTool(input)
*
* // Stream call (Tool interface)
* for await (const event of myTool.stream(context)) { }
*/
Technical Context
Existing Infrastructure
Tool interface in src/tools/tool.ts
ToolContext interface with toolUse and invocationState
FunctionTool class in src/tools/function-tool.ts (leverage for implementation)
- Test patterns in
src/tools/__tests__/tool.test.ts
Zod 4 Features to Leverage
- Built-in JSON Schema export via
.toJsonSchema()
- Type inference with
z.infer<>
- Safe parsing with
.safeParse()
- Schema descriptions with
.describe()
- Default values and optional fields
- Refinements with
.refine()
- Transforms with
.transform()
Implementation Strategy
- Start with basic
tool() function that wraps FunctionTool
- Add Zod schema validation layer
- Convert Zod schemas to JSON Schema using
.toJsonSchema()
- Implement dual behavior (direct vs stream)
- Ensure callable behavior works for both modes
- Add comprehensive tests
- Document with examples
Exit Criteria
Dependencies
References
Review Notes
Clarifications Addressed
- ✅ Schema Support: Zod-only, but designed for future JSON Schema support
- ✅ Callable Behavior: Asymmetric pattern (research confirms this is TypeScript best practice)
- ✅ Zod Version: Zod 4.1.12 confirmed available with built-in JSON Schema
- ✅ Refinements/Transforms: Use Zod's native JSON Schema generation
- ✅ Context Parameter: Optional for direct calls, required for stream
- ✅ Test Coverage: Focus on integration testing, 80%+ coverage
Implementation Notes
- The asymmetric pattern (validate input, unwrap output for direct calls) is consistent with TypeScript ecosystem standards (Zod, tRPC, GraphQL)
- Two error handling modes are intentional and well-documented
- Building on
FunctionTool ensures consistency with existing SDK patterns
- Zod 4's built-in
.toJsonSchema() simplifies implementation
Overview
Implement a tool helper function system for TypeScript that allows developers to easily define tools that can be used by Strands agents. This implementation is based on the architectural decision to use Zod 4 with a helper function approach.
Parent Task
This is a sub-task of #40 (Task 06: Create Tool Decorator System)
Architecture Decision Summary
After evaluating TypeBox vs Zod and considering the repository context, the following approach was chosen:
tool()helper function (no decorators)FunctionToolclassRationale
FunctionToolensures consistency with existing patternsClarified Requirements
Design Decisions (From Review Discussion)
1. Schema Support
2. Callable Behavior
The tool instance supports two usage modes:
Direct Invocation (for testing/standalone use):
Stream Invocation (Tool interface for agent use):
Rationale: This asymmetric pattern (validate input, unwrap output) is consistent with TypeScript best practices (Zod, tRPC, GraphQL). See research comment for detailed analysis.
3. Zod Version
.toJsonSchema()zod-to-json-schemalibrary4. Zod Refinements and Transforms
.refine()) and transforms (.transform()) are used in validation.parse())5. Context Parameter
.stream()method (provided by agent)toolUseandinvocationState6. Test Coverage
Work Required
1. Add Zod Dependency
Files to modify:
package.json- Add Zod to dependencies2. Create Tool Helper Function
New file:
src/tools/tool-helper.tsImplement
tool()helper function with the following signature:Key requirements:
toolSpec.inputSchemaFunctionToolor extend it appropriately3. Schema Conversion
Function: Convert Zod schemas to JSON Schema format
Requirements:
.toJsonSchema()method4. Input Validation
Behavior:
.stream()calls: On validation failure, return error ToolResult (don't throw)5. Context Parameter Injection
Requirements:
.stream()method invocationtoolUse: Contains name, toolUseId, and inputinvocationState: Caller-provided state6. Tool Instance with Callable Behavior
Requirements:
Toolinterface.stream()method and direct invocation7. Streaming Support
Requirements:
stream()methodFunctionToolstreaming logic where possible8. Error Handling
Requirements:
.stream()calls: Always catch errors in callbacks and return error ToolResultsFunctionToolerror handling9. Testing
New file:
src/tools/__tests__/tool-helper.test.tsTest coverage required:
.toJsonSchema()10. Documentation
Files to update:
src/tools/tool-helper.ts- Comprehensive TSDoc commentssrc/tools/index.ts- ExporttoolhelperREADME.md- Add usage examplesAGENTS.md- Document tool helper patternsExample usage for documentation:
Document both usage modes clearly:
Technical Context
Existing Infrastructure
Toolinterface insrc/tools/tool.tsToolContextinterface withtoolUseandinvocationStateFunctionToolclass insrc/tools/function-tool.ts(leverage for implementation)src/tools/__tests__/tool.test.tsZod 4 Features to Leverage
.toJsonSchema()z.infer<>.safeParse().describe().refine().transform()Implementation Strategy
tool()function that wrapsFunctionTool.toJsonSchema()Exit Criteria
tool()helper function implemented insrc/tools/tool-helper.ts.toJsonSchema()toolhelper exported fromsrc/tools/index.tsDependencies
References
Review Notes
Clarifications Addressed
Implementation Notes
FunctionToolensures consistency with existing SDK patterns.toJsonSchema()simplifies implementation