Aviary

Getting Started

Stand up an AI chat in a NestJS app — install, register the module with a model, declare a tool, and stream your first turn.

The fastest path to a working agent is three steps: install, register AgentModule with a model, and send a message. Everything else — the store, who the caller is, which tools they may use, the system prompt — has a default you can override one option at a time.


Prerequisites

  • Node.js 20+, NestJS 10+ (v10, v11 and v12 are supported) on the Express platform
  • TypeScript 5+ with experimentalDecorators and emitDecoratorMetadata enabled
  • A model — any Vercel AI SDK v7 LanguageModel (openai('gpt-5-mini'), anthropic('claude-sonnet-4-5'), an AI Gateway id like 'openai/gpt-5-mini'), or anything implementing the ModelProvider SPI

Step 1 — Install

pnpm add @dudousxd/nestjs-agent @dudousxd/nestjs-agent-ai-sdk @ai-sdk/openai zod

@dudousxd/nestjs-agent is the NestJS module (it re-exports the whole -core surface); @dudousxd/nestjs-agent-ai-sdk adapts an AI SDK model so you write no provider code.


Step 2 — Register the module

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AgentModule } from '@dudousxd/nestjs-agent';
import { aiSdkModel } from '@dudousxd/nestjs-agent-ai-sdk';
import { openai } from '@ai-sdk/openai';

@Module({
  imports: [
    AgentModule.forRoot({
      model: aiSdkModel(openai('gpt-5-mini')),
      systemPrompt: 'You are a helpful assistant for Acme.', // optional
    }),
  ],
})
export class AppModule {}

That boots. At startup you will see two notices, both about defaults you will want to change before production:

WARN [AgentModule] No `store` configured and no store module imported — using the in-memory store. …
WARN [AgentModule] No `actorResolver` configured — the agent endpoints are PUBLIC: every browser gets its own anonymous identity (an HttpOnly cookie). …

Anonymous by default — and what that means

With no actorResolver, anyone who can reach the API can chat, and each browser is its own actor: the server sets an HttpOnly, SameSite=Lax cookie holding a random token and uses a SHA-256 digest of it as the actor id. Threads, quota and attachments are scoped by that id, so visitors never see each other's conversations. To require login, add one line — actorResolver: requestUserActorResolver() reads req.user (Passport, cookie sessions); see Identity & Authorization.


Step 3 — Declare a tool (optional)

A tool is a regular injectable decorated with @AiTool. Only description and input are required: the name defaults to the class name without Tool (GetWeatherTool → getWeather) and the kind to read, which auto-executes. A kind: 'action' tool never auto-executes — the run pauses for approval (see Human-in-the-loop).

// src/tools/get-weather.tool.ts
import { AiTool } from '@dudousxd/nestjs-agent';
import { z } from 'zod';

@AiTool({ description: 'Current weather for a city.', input: z.object({ city: z.string() }) })
export class GetWeatherTool {
  async execute({ city }: { city: string }) {
    return { city, tempC: 21, summary: 'partly cloudy' };
  }
}

List it in a module's providers (providers: [GetWeatherTool]) and the agent discovers it. Because a tool is an ordinary provider, it can inject anything else in your app. By default every tool is callable by every resolved actor; restrict one with @AiTool({ roles: ['ADMIN'] }).


Step 4 — Send a message

The module mounts SSE + REST endpoints under /agent (configurable via path). Start a turn by POSTing to /agent/chat; the answer streams back as Server-Sent Events:

curl -N -c cookies.txt -b cookies.txt http://localhost:3000/agent/chat \
  -H 'content-type: application/json' \
  -d '{ "message": "What is the weather in Lisbon?" }'
event: meta
data: {"runId":"…","threadId":"…"}

id: 1
data: {"kind":"step-start"}

id: 2
data: {"kind":"tool-input-available","id":"call-1","name":"getWeather","input":{"city":"Lisbon"},"toolKind":"read"}

id: 3
data: {"kind":"tool-output","id":"call-1","output":{"city":"Lisbon","tempC":21,"summary":"partly cloudy"}}

id: 4
data: {"kind":"step-finish"}

id: 5
data: {"kind":"step-start"}

id: 6
data: {"kind":"text","text":"It's partly cloudy and 21°C in Lisbon."}

id: 7
data: {"kind":"step-finish"}

event: done
data: {}

The cookie jar keeps your anonymous identity between requests — GET /agent/threads with the same jar lists the thread you just started. The full frame vocabulary is in Stream events.


Step 5 — A React client

pnpm add @dudousxd/nestjs-agent-react ai @ai-sdk/react
import { ChatInput, MessageList, useAgentChat } from '@dudousxd/nestjs-agent-react';

export function Chat() {
  const chat = useAgentChat(); // same origin, `/agent`
  return (
    <>
      <MessageList messages={chat.messages} status={chat.status} />
      <ChatInput onSubmit={(text) => chat.sendMessage({ text })} />
    </>
  );
}

Continue with Hello chat (the smallest end-to-end app) and Full-featured chat (threads, models, attachments, approvals, quota).


The defaults, and how to change each

DefaultOverride
In-memory store — lost on restart, not shared across processesstore, or import a store module (MikroOrmAgentStoreModule.forFeature(), DrizzleAgentStoreModule.forRoot({ db })) — found automatically
Public endpoints, one anonymous actor per browseractorResolver: requestUserActorResolver(), or your own ActorResolver
Every tool callable by any resolved actor@AiTool({ roles }), defaultRoles, or a rolesPolicy
'You are a helpful assistant.'systemPrompt (string or ({ actor, agentName, pageContext }) => string), or @Agent classes
One model, no pickermodel: aiSdkModels({ fast: …, smart: … }, { default: 'fast' })
Usage reported, never enforcedquota: { limits: { day: { tokens: 200_000 }, month: { usd: 20 } } }
Turns run in-processdurable: true + AgentDurableModule (Runners)
An action call holds its turn until someone decidesactionApprovalMode: 'independent' + a backgroundActorResolver — the turn finishes and the call waits as a proposal (Action proposals)
Routes under /agentpath: 'api/agent' (and <AgentProvider path="api/agent"> on the client)

Before you go public

An anonymous deployment without quota limits lets anyone spend your model budget. Set limits — they apply per actor, which in anonymous mode is per browser — or require login.


Try it offline

@dudousxd/nestjs-agent-testing ships a deterministic fake model (FakeModelProvider), so the whole loop — auto-executing reads, a suspending action tool, multi-agent delegation — runs with no API key. See Offline testing; the repo's examples/agent-demo is a runnable app.


Next steps

On this page