# @isdk/tool-func

> A manager for creating, registering, and executing reusable tool functions with rich metadata.

Latest version **2.0.0** (published 2026-10-09) · MIT license · 0 weekly downloads

## Install

```sh
npm install @isdk/tool-func
pnpm add @isdk/tool-func
yarn add @isdk/tool-func
bun add @isdk/tool-func
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 2.0.0 |
| Published | 2026-10-09 |
| First published | 2025-11-08 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Node | >=20.11.1 |
| Dependencies | 8 |
| Unpacked size | 557.6 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | Riceball LEE <snowyu.lee@gmail.com> |
| Maintainers | isdk |
| Keywords | tool-function, function-calling, ai-agent, modular, registry, async, stream, cancellable, isdk |

## Links

- npm: https://www.npmjs.com/package/@isdk/tool-func
- Homepage: https://github.com/isdk/tool-func.js/#readme
- npm.io page: https://npm.io/package/@isdk/tool-func

## Dependencies (8)

- [util-ex](https://npm.io/package/util-ex.md) ^2.7.1
- [lodash-es](https://npm.io/package/lodash-es.md) ^4.18.1
- [@isdk/hash](https://npm.io/package/@isdk/hash.md) ^0.1.1
- [@isdk/util](https://npm.io/package/@isdk/util.md) ^0.3.12
- [abstract-error](https://npm.io/package/abstract-error.md) ^2.0.1
- [custom-ability](https://npm.io/package/custom-ability.md) ^2.3.1
- [property-manager](https://npm.io/package/property-manager.md) ^4.0.0
- [@isdk/common-error](https://npm.io/package/@isdk/common-error.md) ^0.2.2

## Alternatives

- [byte-size](https://npm.io/package/byte-size.md) — 2.1M weekly downloads
- [speed-limiter](https://npm.io/package/speed-limiter.md) — 16.0K weekly downloads
- [@powersync/node](https://npm.io/package/@powersync/node.md) — 10.9K weekly downloads
- [@ledgerhq/coin-cardano](https://npm.io/package/@ledgerhq/coin-cardano.md) — 1.0K weekly downloads
- [@jayesol/jayeson.lib.streamfinder](https://npm.io/package/@jayesol/jayeson.lib.streamfinder.md) — 1.0K weekly downloads

## Recent versions

- 2.0.0 (latest) — 2026-10-09
- 1.0.0 — 2026-07-05
- 0.1.1 — 2025-11-08

## README

# @isdk/tool-func

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.

## ✨ Core Features

- **📦 Modular & Reusable Tools:** Define functions as `ToolFunc` instances 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 `ToolFunc`s using the `depends` property. 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 `allowOverride` mode 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`) via `this.ctx` and propagate context using the `tool.with(ctx)` chainable API.
- **🔄 Async & Cancellable Tasks:** Transparently integrates cancellation capabilities via `makeToolFuncCancelable`. Automatically injects an `aborter` on every call, supports timeouts and `AbortSignal` linkage, and returns a `task` handle for external lifecycle control.
- **🌊 Streaming Responses:** Easily create streaming outputs using the `stream` property. Process stream events seamlessly with `createCallbacksTransformer`, featuring unified cleanup hooks and zero-copy optimization.
- **🚀 Lifecycle Hooks:** Install `makeToolFuncLifecycle` to turn the `setup` and `dispose` hook functions into a symmetric, registry-driven lifecycle
  (terse API surface, with async setup, readiness gating, and explicit teardown).
- **🧬 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`) or `tags` to functions for flexibility and grouping.

## 📦 Installation

```bash
npm install @isdk/tool-func
```

## 🚀 Basic Usage

### 1. Define a Tool

Create a `ToolFunc` instance to define your tool's metadata and implementation.

```typescript
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' }),
});
```

### 2. Register the Tool

Register the tool to make it available in the global registry.

```typescript
getUser.register();
```

### 3. Run the Tool

Execute the tool from anywhere in your application using the static `run` method.

```typescript
async function main() {
  const user = await ToolFunc.run('getUser', { id: '123' });
  console.log(user); // Outputs: { id: '123', name: 'John Doe' }
}

main();
```

## 🌟 Advanced Usage

### Dependency Management

Declare dependencies on other tools, and they will be registered automatically.

```typescript
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 `runAsSync` or `runAs`, the framework prioritizes matching keys in the `depends` map (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.

### Lifecycle Hooks: Full Lifecycle Lifecycle with `setup` and `dispose`

A plain `ToolFunc` stores the `setup` and `dispose` hook functions but **never calls them**; they
are configuration fields. Install the lifecycle ability once on the registry class you actually use,
`const Tools = makeToolFuncLifecycle(ToolFunc)`, and the hooks become a symmetric, registry-driven
lifecycle:

- **`register()`** runs `setup` (idempotently, once per registration lifetime, after the instance
  is placed in the registry). It can mutate the options object it receives; the keys it touches are
  replayed back onto the instance through the same `initialize`/`assign` pipeline.
- **`scope` is a compile-time input**: the func is compiled against the tool's `scope` (its keys
  become closure variables, a `this` key becomes the function's `this`), and those values are captured
  while it compiles. A `scope` that `setup` provides — through `this.scope` or the options object — is
  therefore applied *before* the func is compiled, exactly as it was when `setup` still ran in the
  constructor.
- **`dispose`** is the exact inverse — its teardown runs when the tool is physically removed (refCount
  hits zero, or `force` is used); its returned promise is tracked and any rejection is logged.
- **Sync initiates, async awaits**: `unregister()` and `Tools.clear()` are synchronous entry points, so
  they only *start* a teardown. `unregisterAsync()` and `Tools.clearAsync()` await it to the end — the
  tool's own `dispose` *and* the dependency releases chained behind it. An asynchronous dependency
  teardown therefore overlaps its holder's under `clear()`; `clearAsync()` avoids that, and reports
  the layer's failures as one `AggregateError`.
- **Teardown order**: a tool is torn down *before* the dependencies it declares, and those dependencies
  are released last-declared first. A dependency must outlive everything that uses it, so a dependent's
  `dispose` still finds a live dependency to give back what its `setup` took — stopping a timer before
  closing the pool it queries. Declare dependencies in dependency order (a dependency before the tool
  that uses it): that single convention keeps both the acquisition order and the release order (its
  reverse) correct. `Tools.clear()` / `Tools.clearAsync()` release everything the layer owns with that
  same order, instead of just dropping the tables.
- **Re-arm**: a successful `dispose` re-arms the lifecycle, so every unregister/register cycle runs
  `setup` again *and* is torn down again in turn — `dispose` is the exact inverse of `setup`. An
  instance with a `dispose` hook is therefore strictly paired: executing it before `register()` is
  treated as a bug.
- **`cleanup` — the call-scoped twin of `dispose`**: `setup`/`dispose` are tied to the instance's
  *registration* lifetime, `cleanup` to a single *call*. Declaring it releases whatever that call
  acquired, on every terminal path and at most once: a synchronous result or throw, a settled promise
  (resolve *and* reject), a returned `ReadableStream` (once it finishes, fails or is cancelled), and an
  abort of the call's signal — a cancelled call never unwinds its own stack, so a `finally` would never
  run. The body parks the per-call resources on `this` (`this.tx = begin()`) and `cleanup` releases
  exactly those: declaring the hook also makes each call run on its own isolated shadow instance, so
  concurrent calls cannot collide. Write it defensively (`this.tx?.commit()`), since it is invoked for
  a body that threw before acquiring anything too. A synchronous entry point can only *start* an async
  release (its rejection is logged); `run()`/`runWithPos()` already return a promise and therefore
  resolve only after the cleanup settled. When the body *and* the cleanup fail, the call reports one
  `AggregateError`.
- **Async setup**: if `setup` returns a promise, the instance becomes *pending*. `run()` / `runWithPos()`
  await it automatically; `runSync()` / `runWithPosSync()` refuse to run and point at the async entry
  points (`run()` / `await tool.ready`). The **static helpers** `registerAsync()`, `createAsync()`,
  `awaitReady()`, and `whenAllReady()` await setup and (for the first two) the whole dependency tree.

Instance-side surface on every registered tool: `ensureSetup()`/`ensureDispose()`,
`isSetupDone()`/`isSetupPending()`, `registerAsync()`/`unregisterAsync()`, plus the `ready` and `disposed`
promises. The **static surface**: `Tools.registerAsync(...)`, `Tools.unregisterAsync(...)`,
`Tools.createAsync(...)`, `Tools.clearAsync()`, `Tools.awaitReady(name)`, and `Tools.whenAllReady()`.

```typescript
import { ToolFunc, makeToolFuncLifecycle } from '@isdk/tool-func';

const Tools = makeToolFuncLifecycle(ToolFunc);

const db = new Tools({
  name: 'db',
  setup() {
    // runs at register() time, not in the constructor; this is still the instance
    this.conn = connect();
  },
  dispose() {
    this.conn.close();
  },
  func() {
    return this.conn.query('select 1');
  },
});

db.register();            // <- setup() runs here
await Tools.run('db');    // setup has already completed by the time func runs
db.unregister();          // <- dispose() runs here

// Async setup: the instance is pending until setup settles.
const slow = new Tools({
  name: 'slow',
  async setup() { this.readyAt = await Promise.resolve('now'); },
  func() { return this.readyAt; },
});
// slow.runSync() would throw; use the async surface instead:
await Tools.registerAsync(slow);       // waits for setup (and dependencies) to settle
await Tools.whenAllReady();            // waits for every tool in the registry to be ready
await Tools.createAsync({ name: 'temp', setup() { /* ... */ } });  // creates + sets up, without registering

// Per-call resources: `cleanup` is the call-scoped twin of `dispose`.
const tx = new Tools({
  name: 'tx',
  func() {
    this.tx = db.begin();   // acquired by the body, whenever (and if) it is needed
    return this.tx.query('select 1');
  },
  cleanup() { return this.tx?.commit(); },  // runs when the call ends, however it ends
});
await tx.run();  // resolves only after cleanup() has settled
```

> **⚠️ A bare instance is a no-op for lifecycle.** `new ToolFunc({ setup(){...}, dispose(){...} })
> .register()` does nothing with the hooks. If you need lifecycle, always construct from a registry
> class that has been enhanced with `makeToolFuncLifecycle` (or a subclass of one).

### The Per-Call Scope: `cleanup` and Its Terminal Paths

`setup`/`dispose` are **registration** scoped — one resource lifetime per registration. `cleanup` is
**call** scoped — one resource lifetime per `run()`. Declaring it makes the framework close that scope
exactly once, no matter how the call ends:

| Terminal path | When `cleanup` runs |
| --- | --- |
| Synchronous return | right after the body returns its value |
| Synchronous throw | in the throw path, before the error is rethrown |
| Async body (promise) | after it settles — on resolve *and* on reject; `run()` resolves only once the release settled |
| Returned `ReadableStream` | when the stream ends, errors or is cancelled by the consumer |
| Call signal aborts | as soon as `ctx.signal` / `ctx.aborter.signal` fires |

The last two are the ones `try/finally` cannot express, and the reason this belongs to the framework:

- **A stream's value is produced before it is consumed.** `finally` runs when the body *returns* the
  stream — before a single chunk reaches the consumer. The scope stays open until the stream itself
  closes: the framework pipes it through one identity `TransformStream` whose `onClose` is the release.
- **A cancelled call never unwinds its own stack.** An `AbortSignal` only *notifies*; it does not throw
  through your frames, so a `finally` never runs. The scope is closed by the signal instead.

Semantics worth knowing:

- **Exactly once.** Every terminal path funnels into the same release, so a stream that is aborted and
  then cancelled still releases once.
- **`this` is the call's shadow instance.** Declaring `cleanup` also forces per-call isolation, so the
  fields the body parks on `this` are private to that call — concurrent calls cannot collide. Sharing a
  single resource with nested calls is done through `this.ctx`, which `runAs` targets inherit.
- **Write it defensively.** `cleanup` is invoked even for a body that threw before acquiring anything,
  so guard it (`this.tx?.commit()`).
- **Nesting order.** A nested `runAs`/`runAsSync` child releases its own scope when its own call ends —
  before the parent continues, hence before the parent releases. When the parent `await`s the child, the
  child's asynchronous release has already settled by the time the parent resumes.
- **Sync starts, async waits.** As with `unregister()`, a synchronous entry point can only *initiate* an
  asynchronous release (its rejection is logged, never thrown). `run()`/`runWithPos()` already return a
  promise, so they resolve only after the cleanup settled.
- **One error, not two.** If the body *and* the cleanup both fail, the call reports a single
  `AggregateError` carrying both.

### Registration Lifecycle & Reference Counting

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.

#### 1. How Reference Counting Works

- **`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 returns `false` (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)` or `unregister({ force: true })`.

#### 2. Automatic Dependency Lifecycle

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 `ToolFunc` instance 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.

> **🔧 Internal Convention: Circular Dependency Detection via `options._stack` (Plugin Developers)**
>
> When a tool declares `depends`, registration recurses into each dependency. To terminate
> circular chains (A → B → A), the framework threads an internal **stack** — a `Set` of the
> ancestor names currently being registered — through the recursive `register` calls:
>
> - **Carrying**: the stack is passed as `options._stack` (it may sit either in the first-arg
>   config object or in the second-arg options). It is **consumed and removed** during
>   normalization (see `_extractStack`), so it never becomes instance state and is never
>   serialized.
> - **Back-edge behavior**: if a name is already in the stack, `register` returns `false` —
>   the tool is already being registered in the current call chain, so re-entry is skipped.
> - **If you override `register()` / `_acquireDependencies()`**: keep threading the stack via
>   `{ _stack: stack }` in the options object — do not add a third parameter. Only the wrapper
>   object is consumed; the `Set` itself passes through recursion unchanged.

#### 3. Implementation Overriding

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:

```typescript
// 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.

### Hierarchical Registries and Shadowing

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.

#### 1. Isolate Your Registry

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).

```typescript
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!)
```

#### 2. Tool Shadowing (Polymorphism)

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.

```typescript
// 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 version
```

#### 3. Namespace Protection

If 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.

```typescript
MyPluginTools.register('global-tool', {
  func: () => 'oops',
  allowOverride: false // Throws error because 'global-tool' exists in parent
});
```

#### 4. Scoped Unregistration

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.

#### 5. Late-Binding Polymorphism & Binding Strategies

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.

```typescript
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.

```typescript
// Force using parent's original dependency even if child has a shadow
Child.runSync('main', {}, { binding: 'early' }); // Outputs: "parent-dep"
```

### Execution Context and Concurrency Isolation

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**.

#### 1. `ToolFuncContext` Core Interface

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 in `ctx`, setting this to `true` will trigger the creation of a shadow instance, ensuring concurrency safety.
- **`inheritContext`**: `boolean` (optional). Core implementation, controls automatic context propagation. Defaults to `true`. If set to `false`, 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 via `this.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 injecting `Cancelable` ability, it will be automatically injected and managed here.
- **`Custom Properties`**: You can spread any business-related Metadata (like `userId`, `traceId`) directly on the context object.

> **⚠️ Note on non-plain objects:**
> If you pass a `ctx` with 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.

#### 2. Accessing Context: `static ctx` vs `instance.ctx`

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 use `ToolFunc.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 because `ToolFunc` instances have core metadata like `name`, `params`, `title`, etc. If the context happened to have a `name` field, flattening it would destroy the tool definition and lead to hard-to-debug bugs. `this.ctx` provides a safe isolated space.

#### 3. Core Mechanism: Shadow Instance and Root Tracking (_origin)

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()`:

1. **Create Shadow**: The framework executes `Object.create(tool)`.
2. **Root Tracking**: Every shadow instance has a hidden `_origin` property 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".
3. **Inject Properties**: Mount `ctx: { user: 'Alice' }` on the resulting shadow object.
4. **Logic Execution**: The shadow object executes `func`. At this point, `this` points to the shadow object, so `this.ctx` returns Alice; meanwhile, thanks to the prototype chain, `this.name` still 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.

#### 4. Dual Forms of Fluent API

We provide a chained calling interface that reads like natural language:

##### Static Form: `ToolFunc.with(ctx)`

Used to preset the execution environment at a global level or before getting an instance. It returns a "static proxy class".

```typescript
// 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 });
```

##### Instance Form: `tool.with(ctx)`

Used for fine-grained environment configuration for a specific tool. It returns an "execution-time shadow instance".

```typescript
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 });
```

#### 5. Advanced Extension Hooks (for Plugin Developers)

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.
  - **`ctx` parameter**: Specifically refers to the "call-time context" explicitly passed by the user when calling `run(params, ctx)` or `runSync(params, ctx)`.
  - **Logic**:
    1. If the user passed `ctx`, it must be isolated to apply these overrides.
    2. If the current instance is already a shadow instance (has its own `ctx` property) and the user didn't pass a new `ctx`, it won't re-isolate and will reuse the current one.
    3. If the tool has async features like `Cancelable` enabled, it must be forced to isolate to ensure aborter isolation.
  - **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.ctx` object held by the shadow instance after it's created.
  - **Core Logic - Prototype Inheritance**:
      1. It first gets the "parent context" (i.e., the existing `this.ctx` of the current instance).
      2. If `inheritContext` is `true` (default), it executes `Object.create(parentCtx)` to achieve property inheritance.
      3. **Automatic Ability Injection**: For example, the `Cancelable` plugin overrides this method to automatically inject an `aborter` instance here and link it with external `signal/signals`.
      4. Finally, overlay the `ctx` explicitly passed by the user onto the top of this new object.
  - **Custom Scenario**: Plugins (like auto-logging) override this method to automatically inject a `logger` instance, achieving transparent feature injection for business logic.

  > **⚠️ Note**: When overriding these methods, be sure to call `super._shouldIsolate` or `super._prepareContext` to ensure normal operation of core framework features.

#### 6. Automatic Context Propagation

In tool chain calls (e.g., tool A calling `this.runAs('B')` in its implementation), context flows automatically:

- **Default Behavior**: B automatically inherits all `ctx` properties of A.
- **Explicit Control**: A new `ctx` can be passed in `runAs(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 a `ctx` argument, you **must** use `this.with(ctx).runWithPos(...)` to ensure correct context injection.

### Asynchronous & Cancellable Tasks

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.

#### 1. Core Mechanism: Transparent Context Integration

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 as `aborter`) into `this.ctx` of the shadow instance.
- **Environment Isolation**: Each concurrent task has an independent aborter, without interference.
- **Signal Linking**: If external `signal` or `signals` are passed in the context (`ctx`), the injected `aborter` will automatically link with these signals. As soon as an external signal aborts, the internal task will be notified immediately.

#### 2. Usage Example

The example below shows how to define a long-running loop task that supports abortion:

```typescript
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"
}
```

#### 3. Key Points Analysis

- **`aborter.throwIfAborted()`**: This is the recommended way to check. It ensures that when an abort occurs, the business logic exits with a standard `AbortError`, triggering the correct resource cleanup process.
- **Task Handle**: The `task` object is attached to the Promise returned by `ToolFunc.run`. This allows callers to control the task lifecycle directly without needing to know context details.
- **Timeout Support**: You can pass a `timeout` parameter (via `params` or `ctx`) directly when calling, and the framework will automatically set a timer and trigger `aborter.abort()` after timeout.

#### 4. Abort Rejections & Unhandled Rejection Safety

An aborted task **rejects** its `TaskPromise` with an `AbortError` (because of a `timeout`, an external `signal`, or an explicit `task.abort()`). When several concurrent tasks **share one aborter** — for example, multiple `run()` calls on the same `tool.with(ctx)` runner, or one external signal / timeout that fans out to a whole group — they all reject at (nearly) the same moment.

The library intentionally **does not swallow** these rejections: doing so would hide genuine task failures and disable Node's `unhandledRejection` diagnostics. It is therefore **the caller's responsibility to attach a rejection handler to every task promise**. Register the handlers *before* awaiting any of them; otherwise the not-yet-awaited promises can be reported as unhandled rejections (and, under Node's default `--unhandled-rejections=throw`, can even terminate the process):

```typescript
const p1 = runner.run({ timeout: 40 })
const p2 = runner.run() // shares the runner's aborter → rejects together with p1

// ❌ p2 may already have rejected while you are still awaiting p1
// await p1
// await p2

// ✅ Attach handlers to both first, then await.
await Promise.allSettled([p1, p2])

// or keep the settled values:
const [r1, r2] = await Promise.all([p1.catch(e => e), p2.catch(e => e)])
```

### Streaming Responses

To create a tool that can stream its output, follow these steps:

1. **Enable Streaming Capability**: Set `stream: true` in the tool's definition. This marks the tool as *capable* of streaming.
2. **Check for Streaming Request**: Inside your `func`, use the `this.isStream(params)` method. This checks if the current execution was requested as a stream. By default, it looks for a `stream: true` parameter in the incoming arguments.
3. **Add a Control Parameter (Optional)**: If your tool should support *both* streaming and regular value returns, add a `stream: { type: 'boolean' }` parameter to your `params` definition. 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.

```typescript
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();
```

### Handling Stream Events with `createCallbacksTransformer`

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.

#### Key Features

- **Unified Cleanup**: The `onClose` hook is guaranteed to run exactly once, regardless of how the stream ended (success, error, or cancel). This is the ideal place to release resources like `ActiveTaskHandle`.
- **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 `onCancel` hook to detect client disconnections or aborts.

#### Callback Functions

- `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 (upstream `close`).
- `onCancel(reason)`: Called if the reader cancels the stream.
- `onError(err)`: Called if an error occurs.
- `onClose(status, reason)`: **The recommended cleanup hook**. `status` is `'final'`, `'error'`, or `'cancel'`.

#### Example: Processing and Robust Cleanup

```typescript
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:

```sh
Stream started!
Received chunk: a
Processed chunk: A
Received chunk: b
Processed chunk: B
Stream finished normally!
Resource Cleanup: Stream closed with status [final]
```

### Parameter Handling: Object vs. Positional

`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.

#### Object Parameters (Recommended)

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.

```typescript
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."
```

#### Positional Parameters

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.

```typescript
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: 8
```

**Recommendation:** 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.

### Flexible Argument Normalization

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.

#### 1. Core Principle: The "Authority" vs. "Defaults"

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.

#### 2. Supported Patterns

The system automatically recognizes the following patterns:

- **`(string, options)`**:
  - The first argument is the fixed `name`.
  - `options` provides everything else as defaults.
  - `const tool = new ToolFunc('myTool', { title: 'Default Title' });`

- **`(function, options)`**:
  - The first argument is the implementation `func`.
  - Its `name` is used as a fallback if no `name` is provided in `options`.
  - **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: '...' });`

- **`(object, options)`**:
  - The first argument is a configuration object or an existing `ToolFunc` instance.
  - The second argument fills in missing properties recursively.
  - `const tool = new ToolFunc({ name: 'task', title: 'Main' }, { title: 'Fallback' }); // title will be 'Main'`

- **`(string, funcString)`**:
  - The first argument is the fixed `name`, the second is the implementation as a **function-expression string**.
  - `ToolFunc.register('add', '(a, b) => a + b');`

- **`(string, funcString, config)`**:
  - Same as above, plus an optional third **config object** describing `params`, `description`, `title`, etc. Works the same way for the constructor:
  - `ToolFunc.register('add', '(a, b) => a + b', { params: [{ name: 'a' }, { name: 'b' }], description: 'Adds two numbers' });`
  - `const add = new ToolFunc('add', '(a, b) => a + b', { params: [{ name: 'a' }, { name: 'b' }], description: 'Adds two numbers' });`

#### 3. String Functions

A `func` can be provided as a string and is compiled at construction/registration time. This is especially useful when loading tool definitions from persisted data (the framework itself exports `func` as a string when serializing a tool).

- **Accepted formats** — the string must be a **function expression**: an arrow expression (`'(a, b) => a + b'`), a function expression (`'function(a, b) { return a + b }'`), or a named function expression (`'function greet(name) { return name; }'`).
- **Bare expressions are rejected** — a string like `'a + b'` evaluates to a value instead of a function, so it throws a clear error. Use an arrow form instead.
- **Calling convention** — a string func like `'(a, b) => ...'` is positional, so declare `params` as an array (`[{ name: 'a' }, { name: 'b' }]`) to use `run`/`runSync` with named params, or call it with `runWithPos`/`runWithPosSync`.
- **Name derivation** — when no `name` is configured, the name is derived from a named function expression (e.g. `'function add(a, b) {...}'` → `add`).
- **Scope** — the `scope` option provides closure variables: `new ToolFunc({ name: 't', scope: { secret: 42 }, func: '() => secret' })`. It also applies to a *function* value (compiled from its own source, so a method-shorthand source cannot bind a scope and is used as-is), while an empty or absent scope leaves a function value untouched — its lexical closure survives. With the lifecycle ability installed, a `setup` hook may provide the scope too (see [Lifecycle Hooks](#lifecycle-hooks-full-lifecycle-lifecycle-with-setup-and-dispose)).

> **⚠️ Security note:** string funcs are compiled with `new Function`, i.e. arbitrary code execution. Only register strings from trusted sources (e.g. your own persisted data).

#### 4. Deep Merging Benefits

Because it uses `defaultsDeep`, you can provide partial defaults for nested structures like `params`, `depends`, or `result` schemas.

```typescript
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.
```

---

## 🏛️ Core Architecture: Static vs. Instance

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.

## 🤝 Contributing

If you would like to contribute to the project, please read the [CONTRIBUTING.md](./CONTRIBUTING.md) file for guidelines on how to get started.

## 📄 License

The project is licensed under the MIT License. See the [LICENSE-MIT](./LICENSE-MIT) file for more details.

---
_Source: https://npm.io/package/@isdk/tool-func · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
