Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

Task 6.2: Tool Decorator Implementation #52

Description

@github-actions

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

npm install zod@^4.1.12

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

  1. Start with basic tool() function that wraps FunctionTool
  2. Add Zod schema validation layer
  3. Convert Zod schemas to JSON Schema using .toJsonSchema()
  4. Implement dual behavior (direct vs stream)
  5. Ensure callable behavior works for both modes
  6. Add comprehensive tests
  7. Document with examples

Exit Criteria

  • Architecture decision finalized (Zod 4 + helper function)
  • Review complete, all clarifications addressed
  • Zod dependency added to package.json
  • tool() helper function implemented in src/tools/tool-helper.ts
  • Zod schema to JSON Schema conversion works correctly using .toJsonSchema()
  • Input validation with Zod works (success and error cases)
  • Context parameter optional for direct calls, always passed to callback
  • Tool instance is callable as function (both with and without context)
  • Direct calls validate and throw, return unwrapped values
  • Stream calls validate and catch, return ToolResult
  • Tool instance implements Tool interface correctly
  • Streaming support for all callback types (sync, async, generator)
  • Error handling differs between direct and stream calls
  • All unit tests pass with 80%+ coverage
  • TSDoc documentation complete (including usage mode differences)
  • tool helper exported from src/tools/index.ts
  • README.md updated with examples
  • AGENTS.md updated with patterns
  • No TypeScript errors
  • No ESLint errors
  • Code properly formatted with Prettier
  • Pre-commit hooks pass

Dependencies

References

Review Notes

Clarifications Addressed

  1. Schema Support: Zod-only, but designed for future JSON Schema support
  2. Callable Behavior: Asymmetric pattern (research confirms this is TypeScript best practice)
  3. Zod Version: Zod 4.1.12 confirmed available with built-in JSON Schema
  4. Refinements/Transforms: Use Zod's native JSON Schema generation
  5. Context Parameter: Optional for direct calls, required for stream
  6. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Language

None yet

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions