How-to: Create a Custom Code Tool
Relevant source files
The following files were used as context for generating this wiki page:This document provides a comprehensive guide on how to create, define, and register a custom "Code Tool" within the Lucky project. Code Tools are TypeScript functions that can be executed by AI agents within a workflow, enabling them to perform complex tasks. The framework is designed to be explicit, type-safe, and provide a consistent development experience.
The system utilizes a unified tool framework that supports both Code Tools (direct TypeScript execution) and MCP Tools (external processes). This guide focuses exclusively on creating Code Tools. The core of this process is the defineTool pattern, which provides runtime validation, type safety, and automatic error handling.
Sources: packages/core/src/tools/README.md:3-13, packages/tools/PROBLEM_STATEMENT.md:5-13
1. The defineTool Pattern
All Code Tools are created using the defineTool function. This factory function standardizes tool creation and ensures consistency across the system. It takes a single configuration object that defines the tool's name, description, input parameters, and execution logic.
Sources: packages/core/src/tools/README.md:19-21, packages/tools/README.md:104-106
defineTool Configuration
The configuration object for defineTool has the following key properties:
| Property | Type | Description |
|---|---|---|
name | string | A unique name for the tool. This name is used for registration and invocation. It must match the entry in the central tool registry. |
description | string | A detailed description of what the tool does, its capabilities, and its limitations. This is used by the AI to decide when to use the tool. |
params | z.ZodObject | A Zod schema that defines the input parameters for the tool. This is used for runtime validation and provides type safety. |
execute|async (params, context) => Promise| The asynchronous function containing the tool's implementation logic. It receives the validated parameters and an execution context. |
Sources: packages/core/src/tools/README.md:24-36, apps/examples/definitions/todo-manager/tool-todo-write.ts:162-168
This diagram illustrates the structure of the defineTool configuration object.
2. Step-by-Step Tool Creation
Creating a new Code Tool involves defining the tool, implementing its logic, and registering it with the system.
Step 2.1: Define the Tool File
Create a new TypeScript file for your tool. The convention is to place tool implementations in their own directories. The defineTool function is used to structure the tool.
Here is a complete example of the todoWrite tool:
// apps/examples/definitions/todo-manager/tool-todo-write.ts
import { Tools } from "@lucky/shared"
import type { CodeToolResult } from "@lucky/tools"
import { defineTool } from "@lucky/tools"
import { z } from "zod"
import type { TodoWriteResult } from "./types"
// Zod schema for input validation
const todoItemSchema = z.object({
id: z.string().describe("Unique identifier for the todo item"),
content: z.string().min(1).describe("The task description or content"),
status: z.enum(["pending", "in_progress", "completed"]).describe("Current status of the task"),
priority: z.enum(["high", "medium", "low"]).describe("Priority level of the task"),
})
const todoWrite = defineTool({
name: "todoWrite",
description:
"Create and manage structured task lists for coding sessions. Overwrites entire todo list, validates constraints (only one in_progress task, unique IDs). LIMITS: overwrites all todos, session-scoped only, no partial updates or merging.",
params: z.object({
todos: z.array(todoItemSchema).min(0, "Todos array is required"),
}),
async execute(params, externalContext): Promise<CodeToolResult<TodoWriteResult>> {
// ... implementation logic ...
try {
// ... validation and logic ...
return Tools.createSuccess<TodoWriteResult>("todoWrite", {
success: true,
message: "Success message",
todos: params.todos,
});
} catch (error) {
return Tools.createFailure("todoWrite", {
location: "todoWrite:error",
error: error instanceof Error ? error.message : String(error),
});
}
},
})
export default todoWrite
Sources: apps/examples/definitions/todo-manager/tool-todo-write.ts:154-233
Step 2.2: Implement the execute Function
The execute function is where your tool's logic resides. It receives two arguments: params and context.
Parameters (params)
The params object contains the input data for the tool, already validated against the Zod schema you provided in the params property of defineTool. You can access these parameters with full type safety.
Sources: packages/core/src/tools/README.md:33-35
Execution Context (context)
The context object provides information about the workflow currently being executed. The ToolExecutionContext interface defines its shape.
| Property | Type | Description |
|---|---|---|
workflowInvocationId | string | The unique ID for the current workflow execution. |
workflowId | string | The ID of the base workflow definition. |
workflowVersionId | string | The specific version ID of the workflow being run. |
workflowFiles | WorkflowFile[] | An array of files available to the workflow. |
expectedOutputType | any | The expected Zod schema for the final output. |
mainWorkflowGoal | string | The main goal or objective of the entire workflow. |
Sources: packages/shared/src/contracts/tools.ts:121-130
Step 2.3: Return a ToolResult
The execute function must return a ToolResult object, which has a standardized format for success and failure cases. The defineTool wrapper automatically handles uncaught exceptions, but you can create custom success and failure responses.
The todoWrite tool demonstrates creating explicit success and failure results using helper functions:
Tools.createSuccess<T>(toolName, data)Tools.createFailure(toolName, errorDetails)
// Success case
return Tools.createSuccess<TodoWriteResult>("todoWrite", {
success: true,
message,
todos,
})
// Failure case
return Tools.createFailure("todoWrite", {
location: "todoWrite:validation:multipleInProgress",
error: `Only one task can be in_progress at a time. Found ${inProgressTodos.length} in_progress tasks.`,
})
Sources: apps/examples/definitions/todo-manager/tool-todo-write.ts:190-196, apps/examples/definitions/todo-manager/tool-todo-write.ts:223-227
The underlying ToolResult structure is:
type ToolResult<T> = {
readonly success: boolean
readonly data: T | null
readonly error: string | null
}
Sources: packages/core/src/tools/README.md:199-204
3. Tool Registration
For a tool to be discoverable and usable by the AI, it must be registered in two places. The system uses explicit, manual registration to avoid "magic" and give developers full control.
Sources: packages/tools/README.md:15-18
This diagram shows the registration and execution flow for a custom tool.
Step 3.1: Add to the Tool Name Registry
First, add the tool's name and a concise description to the TOOLS constant in packages/shared/src/contracts/tools.ts. This file is the single source of truth for all tool names and their high-level descriptions.
// packages/shared/src/contracts/tools.ts
export const TOOLS = {
// ...
code: {
myNewTool: "Description of what my new tool does, including limits",
// ... existing tools
todoWrite: "Create and manage structured task lists for coding sessions. Overwrites entire todo list, validates constraints (only one in_progress task, unique IDs). LIMITS: overwrites all todos, session-scoped only, no partial updates or merging.",
},
} as const
This step makes the tool's name and type (CodeToolName) available throughout the entire monorepo.
Sources: packages/shared/src/contracts/tools.ts:11-16, packages/shared/src/contracts/tools.ts:98-100, packages/core/src/tools/README.md:154-162
Step 3.2: Add to a Tool Toolkit
Next, the tool implementation must be imported and added to a toolkit or a static registry. This allows the system to find and initialize the tool's execute function at startup.
The recommended pattern is to group related tools into "toolkits".
// Example from packages/tools/README.md
import { codeToolRegistry } from "@lucky/tools"
codeToolRegistry.register(myTool)
Sources: packages/tools/README.md:118-120
Alternatively, a static array can be used for registration:
// /src/runtime/code_tools/registry.ts
import { tool as myNewTool } from "./my-new-tool/tool"
export const ALL_TOOLS = [
myNewTool,
// ... other existing tools
]
Sources: packages/core/src/tools/README.md:165-172
Finally, the registerAllTools function is called at application startup to process these toolkits and make the tools available for execution.
Sources: packages/tools/README.md:24-28
Conclusion
Creating a custom Code Tool in the Lucky project is a structured process designed for clarity, safety, and maintainability. By following the defineTool pattern, developers can create powerful, type-safe, and validated functions for AI agents. The two-step registration process—updating the central TOOLS contract and adding the implementation to a toolkit—ensures that tools are explicitly managed and integrated into the system, providing a robust foundation for building complex, AI-driven workflows.