A powerful TypeScript framework for creating, managing, and executing modular tool functions. It's perfect for building AI agent tools, backend services, and extensible plugin systems with a clean, decoupled architecture.
- π¦ Modular & Reusable Tools: Define functions as
ToolFuncinstances with rich metadata. - π Global Registry: A static registry (
ToolFunc.items) allows any part of an application to access and run registered functions by name. - π Dependency Management: Use the
dependsproperty to declare dependencies on otherToolFuncs, which are then auto-registered. - π·οΈ Aliasing & Tagging: Assign multiple names (
alias) ortagsto a function for flexibility and grouping. - π Lifecycle Hooks: Use the
setupmethod for one-time initialization logic. - π Asynchronous Capabilities: Built-in support for cancellable tasks, timeouts, and concurrency control using
makeToolFuncCancelable. - π Streamable Responses: Easily create and handle streaming responses with the
streamproperty andcreateCallbacksTransformer.
npm install @isdk/tool-funcCreate a ToolFunc instance to define your tool's metadata and implementation.
import { ToolFunc } from '@isdk/tool-func';
const getUser = new ToolFunc({
name: 'getUser',
description: 'Retrieves a user by ID.',
params: { id: { type: 'string', required: true } },
func: (params) => ({ id: params.id, name: 'John Doe' }),
});Register the tool to make it available in the global registry.
getUser.register();Execute the tool from anywhere in your application using the static run method.
async function main() {
const user = await ToolFunc.run('getUser', { id: '123' });
console.log(user); // Outputs: { id: '123', name: 'John Doe' }
}
main();Declare dependencies on other tools, and they will be registered automatically.
const welcomeUser = new ToolFunc({
name: 'welcomeUser',
description: 'Generates a welcome message.',
params: { userId: 'string' },
depends: {
// `getUser` will be auto-registered when `welcomeUser` is registered.
userFetcher: getUser,
},
func: function(params) {
// `this` is the ToolFunc instance, so we can use `runSync`
const user = this.runSync('userFetcher', { id: params.userId });
return `Hello, ${user.name}!`;
},
});
welcomeUser.register();
const message = await ToolFunc.run('welcomeUser', { userId: '456' });
console.log(message); // "Hello, John Doe!"The setup hook provides a way to run one-time initialization logic when a ToolFunc instance is created. This is useful for configuring the instance, setting up initial state, or modifying properties before the tool is registered or used. The this context inside setup refers to the ToolFunc instance itself.
const statefulTool = new ToolFunc({
name: 'statefulTool',
customState: 'initial', // Define a custom property
setup() {
// `this` is the statefulTool instance
console.log(`Setting up ${this.name}...`);
this.customState = 'configured';
this.initializedAt = new Date();
},
func() {
return `State: ${this.customState}, Initialized: ${this.initializedAt.toISOString()}`;
}
});
console.log(statefulTool.customState); // "configured"
statefulTool.register();
console.log(await ToolFunc.run('statefulTool'));
// "State: configured, Initialized: ..."Add powerful async capabilities like cancellation and concurrency control.
import { ToolFunc, makeToolFuncCancelable, AsyncFeatures } from '@isdk/tool-func';
// Create a cancellable version of the ToolFunc class
const CancellableToolFunc = makeToolFuncCancelable(ToolFunc, {
maxTaskConcurrency: 5, // Allow up to 5 concurrent tasks
});
const longRunningTask = new CancellableToolFunc({
name: 'longRunningTask',
asyncFeatures: AsyncFeatures.Cancelable, // Mark as cancelable
func: async function(params, aborter) {
console.log('Task started...');
await new Promise(resolve => setTimeout(resolve, 5000)); // 5s task
aborter.throwIfAborted(); // Check for cancellation
console.log('Task finished!');
return { success: true };
}
});
longRunningTask.register();
// Run the task and get its aborter
const promise = ToolFunc.run('longRunningTask');
const task = promise.task;
// Abort the task after 2 seconds
setTimeout(() => {
task.abort('User cancelled');
}, 2000);To create a tool that can stream its output, follow these steps:
- Enable Streaming Capability: Set
stream: truein the tool's definition. This marks the tool as capable of streaming. - Check for Streaming Request: Inside your
func, use thethis.isStream(params)method. This checks if the current execution was requested as a stream. By default, it looks for astream: trueparameter in the incoming arguments. - Add a Control Parameter (Optional): If your tool should support both streaming and regular value returns, add a
stream: { type: 'boolean' }parameter to yourparamsdefinition. This allows users to choose the return type (e.g., by passing{ stream: true }). If your tool only streams, you don't need this parameter.
The example below demonstrates a flexible tool that can return either a stream or a single value.
import { ToolFunc } from '@isdk/tool-func';
// 1. Define the tool with streaming capability
const streamableTask = new ToolFunc({
name: 'streamableTask',
description: 'A task that can return a value or a stream.',
stream: true, // Mark as stream-capable
params: {
// Declare a 'stream' parameter to control the output type
stream: { type: 'boolean', description: 'Whether to stream the output.' }
},
func: function(params) {
// 2. Check if streaming is requested
if (this.isStream(params)) {
// Return a ReadableStream for streaming output
return new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
controller.enqueue(`Chunk ${i}\n`);
await new Promise(r => setTimeout(r, 100));
}
controller.close();
}
});
} else {
// Return a regular value if not streaming
return 'Completed in one go';
}
}
});
// 3. Register the tool
streamableTask.register();
// 4. Run in both modes
async function main() {
console.log('--- Running in non-streaming mode ---');
const result = await ToolFunc.run('streamableTask', { stream: false });
console.log('Result:', result); // Output: Completed in one go
console.log('\n--- Running in streaming mode ---');
const stream = await ToolFunc.run('streamableTask', { stream: true });
// 5. Consume the stream
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('Stream finished.');
break;
}
process.stdout.write(value); // Output: Chunk 0, Chunk 1, ...
}
}
main();While ToolFunc allows you to return streams, you often need to process the data within a stream. The createCallbacksTransformer utility creates a TransformStream that makes it easy to hook into a stream's lifecycle events. This is useful for logging, data processing, or triggering side effects as data flows through the stream.
It accepts an object with the following optional callback functions:
onStart: Called once when the stream is initialized.onTransform: Called for each chunk of data that passes through the stream.onFinal: Called once the stream is successfully closed.onError: Called if an error occurs during the stream's processing.
Here's how you can use it to observe a stream:
import { createCallbacksTransformer } from '@isdk/tool-func';
async function main() {
// 1. Create a transformer with callbacks
const transformer = createCallbacksTransformer({
onStart: () => console.log('Stream started!'),
onTransform: (chunk) => {
console.log('Received chunk:', chunk);
// You can modify the chunk here if needed
return chunk.toUpperCase();
},
onFinal: () => console.log('Stream finished!'),
onError: (err) => console.error('Stream error:', err),
});
// 2. Create a source ReadableStream
const readableStream = new ReadableStream({
start(controller) {
controller.enqueue('a');
controller.enqueue('b');
controller.enqueue('c');
controller.close();
},
});
// 3. Pipe the stream through the transformer
const transformedStream = readableStream.pipeThrough(transformer);
// 4. Read the results from the transformed stream
const reader = transformedStream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log('Processed chunk:', value);
}
}
main();This example would output:
Stream started!
Received chunk: a
Processed chunk: A
Received chunk: b
Processed chunk: B
Received chunk: c
Processed chunk: C
Stream finished!
ToolFunc supports both object-based and positional parameters for flexibility. While both are functional, object parameters are generally recommended for their clarity and self-documenting nature.
When params is defined as an object, the func receives a single object argument containing all parameters by name. This is the default and most straightforward approach.
const greetUser = new ToolFunc({
name: 'greetUser',
description: 'Greets a user by name and age.',
params: {
name: { type: 'string', required: true },
age: { type: 'number' },
},
func: (args) => {
const { name, age } = args;
return `Hello, ${name}! ${age ? `You are ${age} years old.` : ''}`;
},
});
greetUser.register();
console.log(await ToolFunc.run('greetUser', { name: 'Alice', age: 30 }));
// Outputs: "Hello, Alice! You are 30 years old."If params is defined as an array of FuncParam objects, the func receives arguments in the order they are defined. This can be useful for functions with a fixed, small number of arguments where order is intuitive.
const addNumbers = new ToolFunc({
name: 'addNumbers',
description: 'Adds two numbers.',
params: [
{ name: 'num1', type: 'number', required: true },
{ name: 'num2', type: 'number', required: true },
],
func: (num1, num2) => num1 + num2,
});
addNumbers.register();
console.log(await ToolFunc.runWithPos('addNumbers', 5, 3)); // Use runWithPos for positional arguments
// Outputs: 8Recommendation: For most use cases, defining params as an object and accessing arguments by name within your func is cleaner and less error-prone, especially as your function's parameter list grows.
A key design principle in ToolFunc is the separation of roles between the static class and its instances:
-
The Static Class as Manager: The static side of
ToolFunc(e.g.,ToolFunc.register,ToolFunc.run) acts as a global registry and executor. It manages all tool definitions, allowing any part of your application to discover and run tools by name. -
The Instance as the Tool: An instance (
new ToolFunc(...)) represents a single, concrete tool. It holds the actual function logic, its metadata (name, description, parameters), and any internal state.
This separation provides the best of both worlds: the power of object-oriented encapsulation for defining individual tools and the convenience of a globally accessible service for managing and executing them.
If you would like to contribute to the project, please read the CONTRIBUTING.md file for guidelines on how to get started.
The project is licensed under the MIT License. See the LICENSE-MIT file for more details.