New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

@loopstack/custom-tool-example-module

Package Overview
Dependencies
Maintainers
1
Versions
23
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@loopstack/custom-tool-example-module - npm Package Compare versions

Comparing version
0.21.0
to
0.21.1
+2
-2
package.json

@@ -12,3 +12,3 @@ {

],
"version": "0.21.0",
"version": "0.21.1",
"license": "Apache-2.0",

@@ -35,3 +35,3 @@ "author": {

"@loopstack/common": "^0.25.0",
"@loopstack/create-chat-message-tool": "^0.21.0",
"@loopstack/create-chat-message-tool": "^0.21.1",
"@nestjs/common": "^11.1.14",

@@ -38,0 +38,0 @@ "zod": "^4.3.6"

+175
-117

@@ -13,8 +13,9 @@ # @loopstack/custom-tool-example-module

- How to create tools that perform specific tasks within workflows
- How to create tools that extend `BaseTool` with a `call()` method
- The difference between stateless and stateful tools
- How to use dependency injection to keep tools modular and testable
- How to wire tools into workflows using YAML configuration
- How to define workflow input, state, and output
- How to structure and export a reusable module
- How to define input schemas with Zod on the `@Tool` decorator
- How to use NestJS dependency injection in tools
- How to wire tools into workflows using `@InjectTool()`
- How to use `wait: true` on transitions for manual triggers
- How to return output from a workflow

@@ -33,18 +34,18 @@ This is a great starting point before building your own custom tools.

A simple tool that maintains internal state across calls using the `@Tool` decorator and `ToolInterface`:
A simple tool that maintains internal state across calls. It extends `BaseTool` and implements a `call()` method:
```typescript
import { BaseTool, Tool, ToolResult } from '@loopstack/common';
@Tool({
config: {
uiConfig: {
description: 'Counter tool.',
},
})
export class CounterTool implements ToolInterface {
export class CounterTool extends BaseTool {
count: number = 0;
async execute(): Promise<ToolResult> {
call(_args?: object): Promise<ToolResult<number>> {
this.count++;
return Promise.resolve({
data: this.count,
});
return Promise.resolve({ data: this.count });
}

@@ -54,31 +55,36 @@ }

The `count` property persists across calls within the same workflow execution, so each call increments the counter.
#### 2. Tool with Input Schema and Dependency Injection
A tool that accepts typed arguments via `@Input` and uses NestJS dependency injection for services:
A tool that accepts typed arguments via a Zod schema and uses NestJS dependency injection for services:
```typescript
import { Inject } from '@nestjs/common';
import { z } from 'zod';
import { BaseTool, Tool, ToolResult } from '@loopstack/common';
import { MathService } from '../services/math.service';
const MathSumSchema = z
.object({
a: z.number(),
b: z.number(),
})
.strict();
type MathSumArgs = z.infer<typeof MathSumSchema>;
@Tool({
config: {
uiConfig: {
description: 'Math tool calculating the sum of two arguments by using an injected service.',
},
schema: MathSumSchema,
})
export class MathSumTool implements ToolInterface {
export class MathSumTool extends BaseTool {
@Inject()
private mathService: MathService;
@Input({
schema: z
.object({
a: z.number(),
b: z.number(),
})
.strict(),
})
args: MathSumArgs;
async execute(args: MathSumArgs): Promise<ToolResult<number>> {
call(args: MathSumArgs): Promise<ToolResult<number>> {
const sum = this.mathService.sum(args.a, args.b);
return Promise.resolve({
data: sum,
});
return Promise.resolve({ data: sum });
}

@@ -88,3 +94,3 @@ }

The injected `MathService` is a standard NestJS injectable:
The `schema` option on `@Tool` validates incoming arguments. The injected `MathService` is a standard NestJS injectable:

@@ -102,3 +108,3 @@ ```typescript

The workflow class declares input, state, output, tools, and helpers:
The workflow extends `BaseWorkflow<TArgs>` with a typed argument object. The schema is defined in the `@Workflow` decorator:

@@ -108,4 +114,10 @@ ```typescript

uiConfig: __dirname + '/custom-tool-example.ui.yaml',
schema: z
.object({
a: z.number().default(1),
b: z.number().default(2),
})
.strict(),
})
export class CustomToolExampleWorkflow {
export class CustomToolExampleWorkflow extends BaseWorkflow<{ a: number; b: number }> {
@InjectTool() private counterTool: CounterTool;

@@ -115,112 +127,158 @@ @InjectTool() private createChatMessage: CreateChatMessage;

@Input({
schema: z
.object({
a: z.number().default(1),
b: z.number().default(2),
})
.strict(),
})
args: { a: number; b: number };
total?: number;
}
```
@State({
schema: z
.object({
total: z.number().optional(),
count1: z.number().optional(),
count2: z.number().optional(),
count3: z.number().optional(),
})
.strict(),
})
state: { total?: number; count1?: number; count2?: number; count3?: number };
### Key Concepts
@Output()
result() {
return { total: this.state.total };
}
#### 1. Calling Custom Tools
@DefineHelper()
sum(a: number, b: number) {
return a + b;
}
Call tools via `this.tool.call(args)` inside transition methods. Store the result as an instance property:
```typescript
@Initial({ to: 'waiting_for_user' })
async calculate(args: { a: number; b: number }) {
const calcResult = await this.mathTool.call({ a: args.a, b: args.b });
this.total = calcResult.data as number;
await this.createChatMessage.call({
role: 'assistant',
content: `Tool calculation result:\n${args.a} + ${args.b} = ${this.total}`,
});
await this.createChatMessage.call({
role: 'assistant',
content: `Alternatively, using workflow method:\n${args.a} + ${args.b} = ${this.sum(args.a, args.b)}`,
});
}
```
### Workflow YAML
#### 2. Stateful Tool Behavior
#### Using Custom Tools
The counter tool increments on each call, demonstrating that tool state persists within a workflow execution:
Call custom tools and save their results to state using `assign`:
```typescript
const c1 = await this.counterTool.call({});
const c2 = await this.counterTool.call({});
const c3 = await this.counterTool.call({});
```yaml
- id: calculation
tool: mathTool
args:
a: ${{ args.a }}
b: ${{ args.b }}
assign:
total: ${{ result.data }}
await this.createChatMessage.call({
role: 'assistant',
content: `Counter before pause: ${c1.data}, ${c2.data}, ${c3.data}\n\nPress Next to continue...`,
});
```
#### Accessing State and Arguments
#### 3. Wait Transitions
Reference workflow arguments with `args.<name>` and state with `state.<name>`:
Use `wait: true` on a transition to pause the workflow until it is manually triggered (e.g., by user input):
```yaml
- tool: createChatMessage
args:
role: 'assistant'
content: |
Tool calculation result:
{{ args.a }} + {{ args.b }} = {{ state.total }}
```typescript
@Transition({ from: 'waiting_for_user', to: 'resumed', wait: true })
async userContinue() {}
```
#### Using Helper Functions
The workflow pauses at the `waiting_for_user` state until an external signal triggers the `userContinue` transition.
Call workflow helpers in templates:
#### 4. Workflow Output
```yaml
- tool: createChatMessage
args:
role: 'assistant'
content: |
Alternatively, using workflow getter function:
{{ args.a }} + {{ args.b }} = {{ sum args.a args.b }}
A `@Final` method can return data as the workflow output:
```typescript
@Final({ from: 'resumed' })
async continueCount(): Promise<{ total: number | undefined }> {
const c4 = await this.counterTool.call({});
const c5 = await this.counterTool.call({});
const c6 = await this.counterTool.call({});
await this.createChatMessage.call({
role: 'assistant',
content: `Counter after resume: ${c4.data}, ${c5.data}, ${c6.data}\n\nIf state persisted, this should be 4, 5, 6.`,
});
return { total: this.total };
}
```
#### Stateful Tool Behavior
After resuming, the counter continues from where it left off (4, 5, 6), demonstrating that tool state survives a `wait` pause.
The counter tool increments on each call, demonstrating stateful tools:
#### 5. Private Helper Methods
```yaml
- id: count1
tool: counterTool
assign:
count1: ${{ result.data }}
- id: count2
tool: counterTool
assign:
count2: ${{ result.data }}
- id: count3
tool: counterTool
assign:
count3: ${{ result.data }}
- tool: createChatMessage
args:
role: 'assistant'
content: |
Counter tool should count:
Define private methods for reusable logic within the workflow:
{{ state.count1 }}, {{ state.count2 }}, {{ state.count3 }}
```typescript
private sum(a: number, b: number) {
return a + b;
}
```
### Workflow Output
### Complete Workflow
The `@Output()` decorator defines the data returned when the workflow completes:
```typescript
import { z } from 'zod';
import { BaseWorkflow, Final, Initial, InjectTool, Transition, Workflow } from '@loopstack/common';
import { CreateChatMessage } from '@loopstack/create-chat-message-tool';
import { MathSumTool } from '../tools';
import { CounterTool } from '../tools';
```typescript
@Output()
result() {
return { total: this.state.total };
@Workflow({
uiConfig: __dirname + '/custom-tool-example.ui.yaml',
schema: z
.object({
a: z.number().default(1),
b: z.number().default(2),
})
.strict(),
})
export class CustomToolExampleWorkflow extends BaseWorkflow<{ a: number; b: number }> {
@InjectTool() private counterTool: CounterTool;
@InjectTool() private createChatMessage: CreateChatMessage;
@InjectTool() private mathTool: MathSumTool;
total?: number;
@Initial({ to: 'waiting_for_user' })
async calculate(args: { a: number; b: number }) {
const calcResult = await this.mathTool.call({ a: args.a, b: args.b });
this.total = calcResult.data as number;
await this.createChatMessage.call({
role: 'assistant',
content: `Tool calculation result:\n${args.a} + ${args.b} = ${this.total}`,
});
await this.createChatMessage.call({
role: 'assistant',
content: `Alternatively, using workflow method:\n${args.a} + ${args.b} = ${this.sum(args.a, args.b)}`,
});
const c1 = await this.counterTool.call({});
const c2 = await this.counterTool.call({});
const c3 = await this.counterTool.call({});
await this.createChatMessage.call({
role: 'assistant',
content: `Counter before pause: ${c1.data}, ${c2.data}, ${c3.data}\n\nPress Next to continue...`,
});
}
@Transition({ from: 'waiting_for_user', to: 'resumed', wait: true })
async userContinue() {}
@Final({ from: 'resumed' })
async continueCount(): Promise<{ total: number | undefined }> {
const c4 = await this.counterTool.call({});
const c5 = await this.counterTool.call({});
const c6 = await this.counterTool.call({});
await this.createChatMessage.call({
role: 'assistant',
content: `Counter after resume: ${c4.data}, ${c5.data}, ${c6.data}\n\nIf state persisted, this should be 4, 5, 6.`,
});
return { total: this.total };
}
private sum(a: number, b: number) {
return a + b;
}
}

@@ -233,3 +291,3 @@ ```

- `@loopstack/core` - Core framework functionality
- `@loopstack/common` - Base classes, decorators, and tool injection
- `@loopstack/create-chat-message-tool` - Provides `CreateChatMessage` tool

@@ -236,0 +294,0 @@