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, maintaining a clear separation between static management and instance logic. - π Global Registry: The static registry (
ToolFunc.items) allows any part of your application to access and run registered functions by name. - ποΈ Hierarchical Registries & Polymorphism: Supports isolating registries via the prototype chain (
isolateRegistry). Enables plugin systems to "shadow" parent tools and provides intelligent late-binding to ensure dependency polymorphism and sibling stability. - π Dependency Management: Declare dependencies on other
ToolFuncs using thedependsproperty. Supports local aliasing, and dependencies are automatically registered. - π’ Reference Counted Registration: Allows multiple modules to share the same tool. Automatically manages dependency lifecycles, only physically unregistering a tool when all references are released.
- βοΈ Controlled Overrides: Explicitly supports
allowOverridemode to safely update tool implementations (e.g., hot-reloading) while maintaining dependency chain integrity. - π§© Execution Context & Concurrency Isolation: Achieves concurrency safety with minimal memory overhead using prototype-chain-based "Shadow Instances". Safely access environmental data (like
traceId,signal) viathis.ctxand propagate context using thetool.with(ctx)chainable API. - π Async & Cancellable Tasks: Transparently integrates cancellation capabilities via
makeToolFuncCancelable. Automatically injects anaborteron every call, supports timeouts andAbortSignallinkage, and returns ataskhandle for external lifecycle control. - π Streaming Responses: Easily create streaming outputs using the
streamproperty. Process stream events seamlessly withcreateCallbacksTransformer, featuring unified cleanup hooks and zero-copy optimization. - π Lifecycle Hooks: Use the
setupmethod to execute one-time initialization logic and safely modify instance state. - 𧬠Flexible Argument Normalization: Supports smart pattern recognition (strings, functions, objects) and deep merging during construction and registration to easily compose tool metadata.
- π Dual-Mode Parameter Support: Supports both semantically clear object parameters (
run) and fixed-order positional parameters (runWithPos). - π·οΈ Aliases & Tags: Assign multiple names (
alias) ortagsto functions for flexibility and grouping.
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 `runAsSync` to run dependencies
const user = this.runAsSync('userFetcher', { id: params.userId });
return `Hello, ${user.name}!`;
},
});
welcomeUser.register();
const message = await ToolFunc.run('welcomeUser', { userId: '456' });
console.log(message); // "Hello, John Doe!"π‘ Pro Tip: Local Dependency Aliasing In
runAsSyncorrunAs, the framework prioritizes matching keys in thedependsmap (e.g.,userFetcher). This allows you to define "local names" for dependencies that are only valid within the current tool, without polluting the global registry.
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: ..."In complex plugin systems, multiple tools might share the same underlying dependency. To safely manage these shared tools, @isdk/tool-func introduces a Reference Counting mechanism.
register(): Each time you call register, the reference count for that tool name is incremented. If the tool already exists and override mode is not enabled, it simply increments the count and returnsfalse(indicating no new instance was created).unregister(): Each time you call unregister, the reference count is decremented. The tool is only physically removed from the global registry when its count reaches zero.- Forced Unregistration: You can bypass the count and remove a tool immediately using
ToolFunc.unregister(name, true)orunregister({ force: true }).
When you register a tool with depends, the framework automatically handles the lifecycle of its dependencies:
- Auto-Registration: Registering a parent tool automatically registers all
ToolFuncinstance dependencies (incrementing their refCounts). - Auto-Unregistration: When a parent tool is completely removed (refCount reaches zero), it automatically triggers unregistration requests for all its dependencies (decrementing their refCounts).
This ensures that as long as at least one parent tool is active, its required child tools will not be accidentally unloaded.
If you need to dynamically update the logic of an already registered tool (e.g., for hot-reloading or plugin replacement), use the allowOverride option:
// Initial registration
ToolFunc.register({ name: 'calc', func: () => 1 });
// Attempt to override (without allowOverride, this only increments the refCount)
ToolFunc.register({
name: 'calc',
func: () => 2,
allowOverride: true // Forcefully replace the existing implementation
});
console.log(ToolFunc.runSync('calc')); // Outputs: 2
β οΈ Note: A warning is issued if the tool being overridden is still held by other references (refCount > 1). Overriding is atomic: if a new tool's alias conflicts with another existing tool, the override fails and the old version is preserved.
For complex systems with plugin architectures or multi-tenant environments, you might need to isolate certain tools while still inheriting others from a parent registry. @isdk/tool-func supports Hierarchical Registries using JavaScript's prototype chain.
Use ToolFunc.isolateRegistry() to branch the current registry from its parent. This creates a new scope where registrations are local, but parent tools are still visible (and can be shadowed).
class MyPluginTools extends ToolFunc {
static {
// Branch the registry: isolate items, aliases, and refCounts
this.isolateRegistry();
}
}
// Parent has 'global-tool'
ToolFunc.register('global-tool', { func: () => 'global' });
// MyPluginTools inherits 'global-tool' but can register its own 'local-tool'
MyPluginTools.register('local-tool', { func: () => 'local' });
console.log(MyPluginTools.get('global-tool')); // Returns the global tool
console.log(MyPluginTools.get('local-tool')); // Returns the local tool
console.log(ToolFunc.get('local-tool')); // undefined (isolated!)When a registry is isolated, you can register a tool with the same name as one in the parent. This "shadows" the parent tool within the current scope.
// Shadowing the parent's 'calc' tool
MyPluginTools.register('calc', { func: () => 'plugin-version' });
console.log(ToolFunc.runSync('calc')); // Original version
console.log(MyPluginTools.runSync('calc')); // Plugin versionIf you want to ensure a name is globally unique and prevent accidental shadowing, use allowOverride: false. The registry will check the entire prototype chain and throw an error if the name is already taken.
MyPluginTools.register('global-tool', {
func: () => 'oops',
allowOverride: false // Throws error because 'global-tool' exists in parent
});The unregister method supports a scope option to control how deeply to remove a tool:
scope: 'local'(default): Only remove the tool if it's "owned" by the current registry. If you unregister a shadow tool, the parent tool will "re-appear".scope: 'inherited': Search up the chain and remove the first occurrence.scope: 'all': Remove the tool from the current registry and all its parents.
In complex plugin systems, a parent tool may depend on other tools. When a child registry "shadows" these dependencies, the system intelligently senses the rootRegistry (entry-point caller) and switches implementations accordingly.
class Parent extends ToolFunc {
static {
const depP = new ToolFunc({ name: 'dep', func: () => 'parent-dep' });
this.register(depP);
this.register({
name: 'main',
depends: { d: depP },
func: function() { return this.runAsSync('dep'); }
});
}
}
class Child extends Parent {
static {
this.isolateRegistry();
// Shadow the dependency
this.register({ name: 'dep', func: () => 'child-dep' });
}
}
// Auto mode: Child's shadow is used when called from Child
console.log(Child.runSync('main')); // Outputs: "child-dep"
console.log(Parent.runSync('main')); // Outputs: "parent-dep" (Stability protection)You can explicitly control the dependency binding behavior via ctx.binding:
'auto'(Default): Smart Sensing. Switches to late-binding only if the caller is a descendant of the definer and has a shadow. This achieves polymorphism while preserving stability for same-scope calls.'early': Early Binding (Safety First). Always uses the original instance bound at registration, ignoring any shadows.'late': Late Binding (Environment First). Forced resolution from the rootRegistry, regardless of lineage.
// Force using parent's original dependency even if child has a shadow
Child.runSync('main', {}, { binding: 'early' }); // Outputs: "parent-dep"In production-grade applications, tool functions often don't run in isolation. They need to be aware of and respond to changes in the "execution environment". For example: carrying a traceId in distributed tracing, knowing the current userId in a web service, or responding to an AbortSignal in long-running tasks.
To support these complex requirements without compromising the purity of the tool functions (i.e., "separation of logic and environment"), @isdk/tool-func introduces a context management mechanism based on prototype chain shadow instances.
The context object is not just a data carrier; it's also a configuration set for controlling tool execution behavior:
isolated:boolean(optional). Core implementation, forces an independent execution scope for this call. Even if there are no other properties inctx, setting this totruewill trigger the creation of a shadow instance, ensuring concurrency safety.inheritContext:boolean(optional). Core implementation, controls automatic context propagation. Defaults totrue. If set tofalse, this call will have a brand new context environment that doesn't inherit from the parent.signal:AbortSignal(optional). Recommendation, standard Web API. When an external abort occurs, the tool can catch it viathis.ctx.signal.signals:AbortSignal[](optional). Recommendation, support for passing multiple abort signals. Any signal aborting will trigger the task to stop.aborter:Aborter(optional). Recommendation, custom aborter. After injectingCancelableability, it will be automatically injected and managed here.Custom Properties: You can spread any business-related Metadata (likeuserId,traceId) directly on the context object.
β οΈ Note on non-plain objects: If you pass actxwith a non-standard prototype (e.g., it's an instance of a class), the framework will shallow copy and "flatten" it via{...ctx}before mounting it to the context prototype chain. This ensures you can access its properties while maintaining the inheritance structure.
The framework maintains ctx properties at both the class level (static) and object level (instance), with clear responsibilities:
- Static
ToolFunc.ctx: This is a global or proxy-level "default environment". When you useToolFunc.with(ctx), it returns a class shadow with this property. - Instance
this.ctx: This is the only legitimate entry point for tool internal logic (func) to access context. it guarantees that you always get data "belonging to this call", regardless of concurrency.
π‘ Architectural Trade-off: Why not "flatten" context? We strictly forbid mounting context data directly on
this(e.g.,this.user). This is becauseToolFuncinstances have core metadata likename,params,title, etc. If the context happened to have anamefield, flattening it would destroy the tool definition and lead to hard-to-debug bugs.this.ctxprovides a safe isolated space.
This is the most ingenious design of this framework. To solve concurrency conflicts, we don't use heavy deep cloning, but leverage JavaScript's Prototype Chain.
When you call tool.with({ user: 'Alice' }).run():
- Create Shadow: The framework executes
Object.create(tool). - Root Tracking: Every shadow instance has a hidden
_originproperty pointing to the original tool instance. This ensures that even in complex nested shadows, concurrency control state (like semaphores, running task counts) is still managed by the original tool, avoiding "state drift". - Inject Properties: Mount
ctx: { user: 'Alice' }on the resulting shadow object. - Logic Execution: The shadow object executes
func. At this point,thispoints to the shadow object, sothis.ctxreturns Alice; meanwhile, thanks to the prototype chain,this.namestill correctly accesses the name defined in the original tool.
Advantages of this design:
- State Synchronization: Ensures global validity of single-instance concurrency limits (
maxTaskConcurrency) and other resource-tracking states via_origin. This prevents state drift by centralizing management on the original tool instance. - Extremely Low Memory: Shadow objects are just a very thin layer of properties and don't hold logic copies.
- Concurrency Safety: Each shadow object is independent. 100 concurrent requests correspond to 100 shadow objects, without interference.
- Dynamic Inheritance: You can call
.with().with()continuously, forming a chain of context inheritance.
We provide a chained calling interface that reads like natural language:
Used to preset the execution environment at a global level or before getting an instance. It returns a "static proxy class".
// All subsequent calls will carry current user info
const AuthorizedRunner = ToolFunc.with({ token: 'abc-123', role: 'admin' });
// Run any tool, they can all get admin via this.ctx.role
await AuthorizedRunner.run('deleteUser', { id: 789 });Used for fine-grained environment configuration for a specific tool. It returns an "execution-time shadow instance".
const uploadTool = ToolFunc.get('uploadFile');
// Set trace ID and abort signal for a single upload task
const controller = new AbortController();
const runner = uploadTool.with({
traceId: 'T-555',
signal: controller.signal
});
await runner.run({ id: 789 });If you are developing AoP (Aspect Oriented Programming) plugins (e.g., auto-logging, permission interception, performance tracking) or need to customize the isolation behavior of tools, you need to deeply understand the following two core internal hooks. They are the foundation of framework extensibility:
-
_shouldIsolate(params, ctx): The "Admission Switch" for shadow instances.- Role: Decides whether this call needs to create a brand new shadow instance.
ctxparameter: Specifically refers to the "call-time context" explicitly passed by the user when callingrun(params, ctx)orrunSync(params, ctx).- Logic:
- If the user passed
ctx, it must be isolated to apply these overrides. - If the current instance is already a shadow instance (has its own
ctxproperty) and the user didn't pass a newctx, it won't re-isolate and will reuse the current one. - If the tool has async features like
Cancelableenabled, it must be forced to isolate to ensure aborter isolation.
- If the user passed
- Custom Scenario: You can override this method to force isolation based on specific fields in
params(e.g.,forceNewScope: true).
-
_prepareContext(params, ctx): The "Processing Factory" for context.- Role: Responsible for building the final
this.ctxobject held by the shadow instance after it's created. - Core Logic - Prototype Inheritance:
- It first gets the "parent context" (i.e., the existing
this.ctxof the current instance). - If
inheritContextistrue(default), it executesObject.create(parentCtx)to achieve property inheritance. - Automatic Ability Injection: For example, the
Cancelableplugin overrides this method to automatically inject anaborterinstance here and link it with externalsignal/signals. - Finally, overlay the
ctxexplicitly passed by the user onto the top of this new object.
- It first gets the "parent context" (i.e., the existing
- Custom Scenario: Plugins (like auto-logging) override this method to automatically inject a
loggerinstance, achieving transparent feature injection for business logic.
β οΈ Note: When overriding these methods, be sure to callsuper._shouldIsolateorsuper._prepareContextto ensure normal operation of core framework features. - Role: Responsible for building the final
In tool chain calls (e.g., tool A calling this.runAs('B') in its implementation), context flows automatically:
- Default Behavior: B automatically inherits all
ctxproperties of A. - Explicit Control: A new
ctxcan be passed inrunAs(params?, ctx?: ToolFuncContext), which will be merged (inherited) as a sub-context into the current call. - Positional Argument Support: Since positional argument functions (
runWithPos) don't accept actxargument, you must usethis.with(ctx).runWithPos(...)to ensure correct context injection.
When dealing with AI agent requests, big data processing, or complex async workflows, tasks often take a long time. Cancelable Ability allows developers to safely abort a task in the middle of its execution, avoiding invalid computation and resource waste.
After giving a tool "Cancelable" ability via makeToolFuncCancelable, the framework automatically participates in the construction of the execution context:
- Automatic Injection: Every time the tool is called, the framework automatically injects a
TaskAbortController(referred to asaborter) intothis.ctxof the shadow instance. - Environment Isolation: Each concurrent task has an independent aborter, without interference.
- Signal Linking: If external
signalorsignalsare passed in the context (ctx), the injectedaborterwill automatically link with these signals. As soon as an external signal aborts, the internal task will be notified immediately.
The example below shows how to define a long-running loop task that supports abortion:
import { ToolFunc, makeToolFuncCancelable, AsyncFeatures } from '@isdk/tool-func';
// 1. Give the ToolFunc class cancelable capability
const CancellableToolFunc = makeToolFuncCancelable(ToolFunc);
// 2. Define a specific long-running tool
const myLongTask = new CancellableToolFunc({
name: 'myLongTask',
asyncFeatures: AsyncFeatures.Cancelable, // Declare cancelable feature
func: async function(params) {
// Get the auto-injected aborter from context
const aborter = this.ctx.aborter;
for (let i = 0; i < 100; i++) {
// Do actual work
await doSomeWork();
// Core step: Check for abort status. Throws AbortError if aborted.
aborter.throwIfAborted();
}
return 'Task completed successfully';
}
});
myLongTask.register();
// 3. Run the task and get the control handle
// Async execution returns a Promise with a .task property
const promise = ToolFunc.run('myLongTask');
const task = promise.task; // Get the task controller for this call
// Simulate discovery that results are no longer needed after 1 second, initiate abort
setTimeout(() => task.abort('Result no longer needed'), 1000);
try {
await promise;
} catch (err) {
console.log(err.message); // Outputs: "Result no longer needed"
}aborter.throwIfAborted(): This is the recommended way to check. It ensures that when an abort occurs, the business logic exits with a standardAbortError, triggering the correct resource cleanup process.- Task Handle: The
taskobject is attached to the Promise returned byToolFunc.run. This allows callers to control the task lifecycle directly without needing to know context details. - Timeout Support: You can pass a
timeoutparameter (viaparamsorctx) directly when calling, and the framework will automatically set a timer and triggeraborter.abort()after timeout.
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 or ensure robust resource cleanup. The createCallbacksTransformer utility creates a TransformStream that makes it easy to hook into a stream's lifecycle events.
- Unified Cleanup: The
onClosehook is guaranteed to run exactly once, regardless of how the stream ended (success, error, or cancel). This is the ideal place to release resources likeActiveTaskHandle. - Zero-Copy Optimization: If you omit
onTransform, the transformer acts as a high-performance "Identity Transform", letting data pass through with minimal overhead. - RPC & Cancellation Friendly: Explicitly supports the
onCancelhook to detect client disconnections or aborts.
onStart(controller): Called once when the stream is initialized.onTransform(chunk, controller): Called for each chunk. (Omit for zero-copy path).onFinal(controller): Called once the stream is successfully closed (upstreamclose).onCancel(reason): Called if the reader cancels the stream.onError(err): Called if an error occurs.onClose(status, reason): The recommended cleanup hook.statusis'final','error', or'cancel'.
import { createCallbacksTransformer } from '@isdk/tool-func';
async function main() {
// 1. Create a transformer with comprehensive callbacks
const transformer = createCallbacksTransformer({
onStart: () => console.log('Stream started!'),
onTransform: (chunk) => {
console.log('Received chunk:', chunk);
return chunk.toUpperCase();
},
onFinal: () => console.log('Stream finished normally!'),
onError: (err) => console.error('Stream error:', err),
onClose: (status, reason) => {
console.log(`Resource Cleanup: Stream closed with status [${status}]`);
if (reason) console.log('Reason/Error:', reason);
// myTaskHandle.release();
}
});
// 2. Create a source ReadableStream
const readableStream = new ReadableStream({
start(controller) {
controller.enqueue('a');
controller.enqueue('b');
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
Stream finished normally!
Resource Cleanup: Stream closed with status [final]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.
The framework features a "Smart Argument Normalization" system for both the ToolFunc constructor and the ToolFunc.register method. This system uses Pattern Recognition to identify your intent and applies Deep Merging (via defaultsDeep) to combine your inputs.
In all patterns involving two arguments (arg1, arg2), arg1 is the primary authority, and arg2 provides deep default values. This means if both arguments define the same property (like title), the value in arg1 will be preserved.
The system automatically recognizes the following patterns:
-
(string, options):- The first argument is the fixed
name. optionsprovides everything else as defaults.const tool = new ToolFunc('myTool', { title: 'Default Title' });
- The first argument is the fixed
-
(function, options):- The first argument is the implementation
func. - Its
nameis used as a fallback if nonameis provided inoptions. - Metadata Awareness: If the function was enriched via
funcWithMeta, its metadata is automatically extracted and used with high priority. const tool = new ToolFunc(function myTask() {}, { description: '...' });
- The first argument is the implementation
-
(object, options):- The first argument is a configuration object or an existing
ToolFuncinstance. - The second argument fills in missing properties recursively.
const tool = new ToolFunc({ name: 'task', title: 'Main' }, { title: 'Fallback' }); // title will be 'Main'
- The first argument is a configuration object or an existing
Because it uses defaultsDeep, you can provide partial defaults for nested structures like params, depends, or result schemas.
ToolFunc.register(
{ name: 'complex', params: { id: { type: 'string' } } },
{ params: { apiKey: { type: 'string', required: true } } }
);
// The resulting tool will have BOTH 'id' and 'apiKey' in its params.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.