Getting Started
Stand up a governed AI agent in a NestJS app — install, declare a tool, register the module, and stream your first turn.
The fastest path to a working agent is four steps: install the packages, declare a tool as an ordinary injectable, register the module, and hit the SSE chat endpoint. The agent is the mechanism; the domain it governs — which model, which store, which roles — is policy you supply.
Prerequisites
- Node.js 20+
- NestJS 10+ (both v10 and v11 are supported)
- TypeScript 5+ with
experimentalDecoratorsandemitDecoratorMetadataenabled - A model provider — anything implementing the
ModelProviderSPI. The zero-boilerplate path isaiSdkModel(...)from@dudousxd/nestjs-agent-ai-sdk, which adapts any Vercel AI SDK v7LanguageModelso you write no provider code; e.g.aiSdkModel(anthropic('claude-sonnet-4-6')). Any otherModelProviderworks too, and for offline tests there's a deterministic fake in@dudousxd/nestjs-agent-testing.
Step 1 — Install
pnpm add @dudousxd/nestjs-agent @dudousxd/nestjs-agent-core
# persistence + (optional) durable runner:
pnpm add @dudousxd/nestjs-agent-store-mikro-orm @dudousxd/nestjs-durableThe -core package holds the framework-agnostic SPIs and the agent loop; @dudousxd/nestjs-agent
is the NestJS module. Add a store adapter (MikroORM or Drizzle) for persistence, and
@dudousxd/nestjs-durable if you want each turn to run as a durable workflow.
Step 2 — Declare a tool
A tool is a regular NestJS injectable decorated with @AiTool. Its input is a Zod schema, and its
kind decides execution: a read tool auto-executes; an action tool never does — the run pauses
for a human decision (see Human-in-the-loop).
// src/tools/get-weather.tool.ts
import { AiTool, type ToolHandler, type AiToolCtx } from '@dudousxd/nestjs-agent';
import { z } from 'zod';
@AiTool({
name: 'getWeather',
kind: 'read', // 'read' auto-executes; 'action' requires approval
description: 'Current weather for a city.',
input: z.object({ city: z.string() }),
})
export class GetWeatherTool implements ToolHandler<{ city: string }> {
async execute(input: { city: string }, ctx: AiToolCtx) {
return { tempC: 21, summary: 'partly cloudy' };
}
}Tools are injectables
Because a tool is an ordinary provider, it can inject anything else in your app — a repository, an
HTTP client, a queue. The agent discovers @AiTool providers automatically; you just list them in
your module.
Step 3 — Register the module
// src/app.module.ts
import { Module } from '@nestjs/common';
import { AgentModule, HeaderActorResolver } from '@dudousxd/nestjs-agent';
import { aiSdkModel } from '@dudousxd/nestjs-agent-ai-sdk';
import { anthropic } from '@ai-sdk/anthropic';
import { GetWeatherTool } from './tools/get-weather.tool.js';
@Module({
imports: [
AgentModule.forRoot({
model: aiSdkModel(anthropic('claude-sonnet-4-6')), // any ModelProvider; this is the zero-boilerplate path
// store: myAgentStore, // optional — omit it and the store resolves from an imported store module
defaultRoles: ['ADMIN'], // roles a tool requires when its own `roles` is omitted
actorResolver: new HeaderActorResolver(), // who's calling — see below
// path: 'agent', // route prefix (default 'agent')
// durable: true, // run each turn as the durable `agent.run` workflow
// dispatchedSteps: false, // under durable:true this defaults to true — opt out to keep steps in-process
defaultAgent: {
systemPrompt: 'You are a helpful ops assistant.',
},
}),
],
providers: [GetWeatherTool],
})
export class AppModule {}durable: true dispatches the turn across your worker fleet by default
With durable: true, the turn's model call and tool executions are — by default — routed durable
steps (AgentRunSteps.llm / AgentRunSteps.tool), so the run isn't pinned to the pod that started
it; set dispatchedSteps: false to keep them as in-process steps instead. Either way, a multi-pod
deployment under durable: true needs a cross-process TokenStreamSink (e.g.
RedisTokenStreamSink from @dudousxd/nestjs-agent-transport-redis) so the pod that ends up
executing the step can still reach whichever pod holds the client's SSE connection — a boot warning
fires if it's missing. See Architecture and
Runners.
There is no insecure default identity
The agent never invents a caller. Every request's actor — { id, roles?, tenantRef? } — comes from
an ActorResolver you configure. actorResolver is a required field on AgentModule.forRoot —
there's no default to omit it in favor of.
The shipped HeaderActorResolver reads x-actor-id / x-actor-role / x-tenant-ref and is only
safe behind a trusted gateway that strips and re-sets those headers. See
Identity & Authorization.
Step 4 — Start a turn
The module mounts SSE + REST endpoints under /agent (configurable via path). Start a turn by
POSTing to /agent/chat; tokens stream back as Server-Sent Events.
curl -N http://localhost:3000/agent/chat \
-H 'content-type: application/json' \
-H 'x-actor-id: u1' -H 'x-actor-role: ADMIN' \
-d '{ "message": "What is the weather in Lisbon?" }'event: meta
data: {"runId":"...","threadId":"..."}
data: {"delta":"It's "}
data: {"delta":"partly cloudy "}
data: {"delta":"and 21°C in Lisbon."}
event: done
data: {"finishReason":"stop"}The model calls getWeather (a read tool, so it auto-executes), then streams its answer. In a
browser, the -react frontend wraps all of this — threads, the agent
catalog, quota, cancel, and approvals — in a single useAgentChat hook.
The endpoints
| Method & path | Purpose |
|---|---|
POST /agent/chat | Start a turn; streams tokens as SSE |
GET /agent/chat/:runId/stream | Resume an in-flight run's stream |
POST /agent/chat/:runId/cancel | Cancel a run |
POST /agent/tool-call/approve · /reject | Human-in-the-loop decision for an action tool (ownership-scoped: 403 on someone else's tool call) |
GET /agent/threads · /:id · DELETE /:id · POST /:id/fork-from/:messageId | Thread history (:id routes are ownership-scoped: 403/404 outside your own threads) |
GET /agent/agents · GET /agent/quota/today | Agent picker catalog & quota |
Try it offline first
@dudousxd/nestjs-agent-testing ships an in-memory store and a deterministic fake model, so you can
exercise the whole loop — auto-executing reads, a suspending action tool, multi-agent delegation —
with no API key or Redis. The repo's examples/agent-demo is a runnable proof:
pnpm --filter agent-demo demo # the offline scripted proof
pnpm --filter agent-demo start # the full NestJS app + console at /ai-gatewayNext steps
- Tools — the
@AiToolsurface,readvsaction, and the handler context - Identity & Authorization —
ActorResolver, roles vs abilities - Human-in-the-loop & Durability — approval as a durable signal
- Multi-agent — orchestrators and sub-agents
- Cost & Governance — the ledger, reported cost, and the console
- Persistence — the
AgentStoreon MikroORM or Drizzle - Frontend —
useAgentChatand the chat components - Governed SQL — read-only SQL as a safe tool
Agent
A governed AI agent for NestJS — chat, tool-calling, per-tool RBAC, token quota, cost tracking, and human-in-the-loop approval, with each turn optionally a replay-safe durable workflow and wired into the ecosystem's durable, telescope, and diagnostics glue.
Architecture
The library is mechanism — the loop, tool registry, RBAC, quota, cost, audit, HITL, streaming; your domain is policy you supply through a fixed set of SPIs.