Aviary
Recipes

Functional tools with DI

Register a tool that needs constructor-injected dependencies without a class — provideAgentTool(factory, inject) resolves it through Nest's container and returns { spec, handler }, auto-discovered at boot exactly like an @AiTool class.

Reach for this when a tool needs a DI-resolved dependency — a repository, an HTTP client, another service — but you'd rather not stand up a whole @AiTool class for it, or you're generating tools from data (e.g. one per configured integration). A functional tool is the same { spec, handler } shape a class-based tool compiles down to; provideAgentTool is how you register one as a Nest provider so AiToolDiscoveryService picks it up at boot.


The contract

From functional-tool.ts:

/** A tool expressed as data + handler, not an `@AiTool` class. */
interface FunctionalTool {
  spec: ToolSpec;
  handler: ToolHandler;
}

// Two forms — a static tool, or a factory with `inject` for DI-resolved dependencies:
function provideAgentTool(tool: FunctionalTool): Provider;
function provideAgentTool(
  factory: (...deps: never[]) => FunctionalTool,
  inject?: FactoryProvider['inject'],
): Provider;

A ToolSpec is the same shape a @AiTool({ ... }) decorator captures — { name, kind, description, inputSchema, roles?, ability? } — and a ToolHandler is the same { execute(input, ctx) } contract a decorated class implements.


A tool with an injected dependency

// src/tools/count-open-orders.tool.ts
import { provideAgentTool, type AiToolCtx, type FunctionalTool } from '@dudousxd/nestjs-agent';
import { z } from 'zod';
import { OrdersService } from '../orders/orders.service.js';

function countOpenOrdersTool(orders: OrdersService): FunctionalTool {
  return {
    spec: {
      name: 'countOpenOrders',
      kind: 'read',
      description: 'How many orders are currently open for a customer.',
      inputSchema: z.object({ customerId: z.string() }),
    },
    handler: {
      async execute(input: { customerId: string }, ctx: AiToolCtx) {
        // Scope to the caller's tenant from ctx.actor, never from tool input.
        return { count: await orders.countOpen(input.customerId, ctx.actor.tenantRef) };
      },
    },
  };
}

export const CountOpenOrdersToolProvider = provideAgentTool(countOpenOrdersTool, [OrdersService]);

Drop the provider in any module's providers — no separate tool registry to keep in sync:

// src/orders/orders.module.ts
import { Module } from '@nestjs/common';
import { CountOpenOrdersToolProvider } from '../tools/count-open-orders.tool.js';
import { OrdersService } from './orders.service.js';

@Module({
  providers: [OrdersService, CountOpenOrdersToolProvider],
})
export class OrdersModule {}

provideAgentTool mints a fresh, unique provide token per call, so the same factory can be used more than once (e.g. one tool per configured integration) without collisions.


A static tool with no dependencies

For a tool with nothing to inject, register it directly on AgentModule.forRoot({ tools: [...] }) instead of a per-module provider:

const echoTool: FunctionalTool = {
  spec: {
    name: 'echo',
    kind: 'read',
    description: 'Echoes the input back — a smoke-test tool.',
    inputSchema: z.object({ text: z.string() }),
  },
  handler: {
    async execute(input: { text: string }) {
      return { text: input.text };
    },
  },
};

AgentModule.forRoot({
  model: myModelProvider,
  tools: [echoTool],
  // ...
});

Functional tool vs. @AiTool class

Both compile to the exact same { spec, handler } shape and are discovered the same way at boot — neither is more "real" than the other. Reach for @AiTool when a tool is naturally a class (it fits alongside your other providers, benefits from method-level readability). Reach for provideAgentTool when the tool is more natural as data — generated per config, wrapping a single function, or needing DI without the ceremony of a class. See Tools for the @AiTool decorator surface, read vs action, and the full AiToolCtx shape — everything there about kind, roles, and ability applies to ToolSpec here unchanged.


  • Tools — the @AiTool decorator surface, read vs action, and AiToolCtx
  • Custom ModelProvider — the other hand-rolled SPI, for the model side instead of the tool side

On this page