AI agent capabilities for the Moleculer microservices framework.
Turn any Moleculer service into an AI agent by adding a mixin. Your existing service actions automatically become LLM tools — no manual schema definitions needed.
A Moleculer action definition is structurally identical to an LLM tool definition. Just add a description field and the conversion is automatic:
// This Moleculer action...
actions: {
getWeather: {
description: "Get current weather for a city",
params: {
city: { type: "string", description: "City name" }
},
async handler(ctx) { /* ... */ }
}
}
// ...automatically becomes an LLM tool that AI agents can call.No new infrastructure needed — the framework already provides everything an AI agent system requires:
- Agent discovery — Service Registry knows which agents are alive and what they can do
- Load balancing — Multiple agent instances are automatically balanced
- Fault tolerance — Circuit breaker, retry, timeout at action level
- Event coordination — Native pub/sub for agent-to-agent events
- Distributed transport — NATS, Redis, Kafka for multi-node agent networks
- Observability — Built-in distributed tracing and metrics
npm install @moleculer/agentsPeer dependency:
npm install moleculerFor OpenAI adapter:
npm install openaiFor Anthropic adapter:
npm install @anthropic-ai/sdkThe LLM service wraps a provider adapter and makes it callable by agents:
import { LLMService } from "@moleculer/agents";
export default {
name: "llm.openai",
mixins: [LLMService()],
settings: {
adapter: "OpenAI",
apiKey: process.env.OPENAI_API_KEY,
model: "gpt-4o"
}
};Add AgentMixin to any service. Actions with a description field automatically become tools:
import { AgentMixin } from "@moleculer/agents";
export default {
name: "weather-agent",
mixins: [AgentMixin()],
settings: {
agent: {
description: "Weather assistant",
instructions: "Help users with weather questions. Be concise.",
llm: "llm.openai"
}
},
actions: {
getCurrent: {
description: "Get current weather for a city",
params: {
city: { type: "string", description: "City name" }
},
async handler(ctx) {
const data = await fetchWeatherAPI(ctx.params.city);
return { temp: data.temp, condition: data.condition };
}
},
getForecast: {
description: "Multi-day weather forecast",
params: {
city: { type: "string", description: "City name" },
days: { type: "number", description: "Number of days (1-7)" }
},
async handler(ctx) {
return await fetchForecastAPI(ctx.params.city, ctx.params.days);
}
}
}
};// One-shot task
const result = await broker.call("weather-agent.run", {
task: "What's the weather like in Budapest?"
});
// Multi-turn conversation (requires MemoryMixin, see below)
const response = await broker.call("weather-agent.chat", {
message: "What about tomorrow?",
sessionId: "user-123"
});Add MemoryMixin for multi-turn conversations. It stores history in Moleculer's cacher (works with Redis, Memory, or any other cacher):
import { AgentMixin, MemoryMixin } from "@moleculer/agents";
export default {
name: "assistant",
mixins: [MemoryMixin(), AgentMixin()],
settings: {
agent: {
description: "General assistant with memory",
instructions: "You are a helpful assistant. Remember the conversation context.",
llm: "llm.openai",
historyTtl: 1800, // Remember for 30 minutes
maxHistoryMessages: 100 // Keep last 100 messages (sliding window)
}
},
actions: {
// ... your tool actions
}
};Important: Your broker must have a cacher configured:
const broker = new ServiceBroker({
cacher: "Memory" // or "Redis" for distributed environments
});The chat action uses sessionId to maintain separate conversations:
// User A's conversation
await broker.call("assistant.chat", { message: "Hi!", sessionId: "user-a" });
await broker.call("assistant.chat", { message: "What did I just say?", sessionId: "user-a" });
// User B's separate conversation
await broker.call("assistant.chat", { message: "Hello!", sessionId: "user-b" });Use OrchestratorMixin to coordinate multiple agent services. An orchestrator can discover other agents and delegate tasks to them.
The orchestrator delegates tasks to sub-agents via delegateTo(). Each sub-agent runs its own ReAct loop independently. Results flow back to the orchestrator, which feeds them to the LLM for a final synthesized response.
The orchestrator's own actions explicitly call delegateTo() to route tasks:
import { AgentMixin, OrchestratorMixin } from "@moleculer/agents";
export default {
name: "trip-planner",
mixins: [OrchestratorMixin(), AgentMixin()],
settings: {
agent: {
description: "Trip planner orchestrator",
instructions: "Plan trips by coordinating weather and hotel agents.",
llm: "llm.openai",
strategy: "direct"
}
},
actions: {
planTrip: {
description: "Plan a complete trip",
params: {
destination: { type: "string", description: "Destination city" },
days: { type: "number", description: "Number of days" }
},
async handler(ctx) {
const [weather, hotels] = await Promise.all([
this.delegateTo("weather-agent", `Weather in ${ctx.params.destination} for ${ctx.params.days} days`),
this.delegateTo("hotel-agent", `Hotels in ${ctx.params.destination} for ${ctx.params.days} nights`)
]);
return `Weather: ${weather}\nHotels: ${hotels}`;
}
}
}
};The LLM decides which agent to delegate to. A _routeToAgent tool is automatically generated with the list of discovered agents:
export default {
name: "smart-router",
mixins: [OrchestratorMixin(), AgentMixin()],
settings: {
agent: {
description: "Smart task router",
instructions: "Route tasks to the most appropriate agent.",
llm: "llm.openai",
strategy: "llm-router"
}
}
};The discoverAgents() method queries the Moleculer service registry for all agent services:
const agents = this.discoverAgents();
// [{ name: "weather-agent", description: "Weather assistant", actions: ["getCurrent", "getForecast"] }]The delegateTo() method calls another agent's run action:
const result = await this.delegateTo("weather-agent", "What's the weather in Paris?");Works with OpenAI, OpenRouter, Together, Groq, Fireworks, and any OpenAI-compatible API:
import { LLMService } from "@moleculer/agents";
// Standard OpenAI
export default {
name: "llm.openai",
mixins: [LLMService()],
settings: {
adapter: "OpenAI",
apiKey: process.env.OPENAI_API_KEY,
model: "gpt-4o"
}
};
// OpenRouter (or any compatible API)
export default {
name: "llm.openrouter",
mixins: [LLMService()],
settings: {
adapter: {
type: "OpenAI",
apiKey: process.env.OPENROUTER_API_KEY,
model: "anthropic/claude-sonnet-4-20250514",
baseURL: "https://openrouter.ai/api/v1"
}
}
};Note: Requires
npm install openai
export default {
name: "llm.anthropic",
mixins: [LLMService()],
settings: {
adapter: "Anthropic",
apiKey: process.env.ANTHROPIC_API_KEY,
model: "claude-sonnet-4-20250514"
}
};Note: Requires
npm install @anthropic-ai/sdk
Deterministic adapter that returns scripted responses — no API calls:
import { LLMService, Adapters } from "@moleculer/agents";
const adapter = new Adapters.Fake({
responses: [
// Tool call response
{
content: null,
finish_reason: "tool_calls",
tool_calls: [{
id: "call_1",
type: "function",
function: {
name: "getCurrent",
arguments: JSON.stringify({ city: "Budapest" })
}
}]
},
// Final text response
"The weather in Budapest is 18°C and sunny."
]
});
export default {
name: "llm.test",
mixins: [LLMService()],
settings: { adapter }
};Register your own adapter for any LLM provider:
import { Adapters } from "@moleculer/agents";
import BaseAdapter from "@moleculer/agents/src/adapters/base.ts";
class MyCustomAdapter extends BaseAdapter {
async chat(messages, tools) {
// Call your provider, return OpenAI-format response
return { content: "...", finish_reason: "stop" };
}
convertToolSchema(name, description, params) {
// Convert Moleculer params to your provider's format
return { /* ... */ };
}
}
Adapters.register("MyCustom", MyCustomAdapter);settings: {
agent: {
// Required
description: string, // What this agent does (used for discovery)
llm: string, // Name of the LLM service (e.g., "llm.openai")
// Optional
instructions: string, // System prompt for the agent
maxIterations: number, // Max ReAct loop iterations (default: 10)
historyTtl: number, // Session history TTL in seconds (default: 3600)
maxHistoryMessages: number // Sliding window size (default: 50)
}
}Only these fastest-validator types are converted to tool schemas. Actions using unsupported types will have those params excluded with a warning:
| Type | JSON Schema | Notes |
|---|---|---|
string |
string |
|
number |
number |
|
boolean |
boolean |
|
object |
object |
Nested properties or props supported |
array |
array |
With items definition |
enum |
string + enum |
Values from .values array |
email |
string |
Format hint in description |
url |
string |
Format hint in description |
date |
string |
Format hint in description |
uuid |
string |
Format hint in description |
The description field on both actions and individual params is critical — it's what the LLM uses to understand the tool. Actions without description are not exposed as tools.
The AgentMixin implements a ReAct (Reason + Act) loop:
- User sends a task via
runorchataction - Agent loads conversation history (if MemoryMixin is present)
- Agent sends the task + available tools to the LLM
- If the LLM returns a tool call: execute the action, feed the result back to the LLM, repeat
- If the LLM returns a text response: save history and return to the user
- If max iterations exceeded: throw an error
User → run("What's the weather?")
│
▼
┌─── ReAct Loop ───┐
│ │
│ LLM: "I need to │
│ call getCurrent" │
│ │ │
│ ▼ │
│ Execute tool │
│ getCurrent() │
│ │ │
│ ▼ │
│ LLM: "It's 18°C │
│ and sunny" │
│ │
└───────────────────┘
│
▼
Return "It's 18°C and sunny"
Runnable examples are in the examples/ directory:
# Simple agent with tool calling
npx tsx examples/simple-agent.ts
# Multi-turn chat with conversation memory
npx tsx examples/multi-turn-chat.ts
# Multi-agent orchestration
npx tsx examples/orchestrator.ts# Install dependencies
npm install
# Run tests
npm test
# Run unit tests only
npm run test:unit
# Type check
npm run check
# Build (CJS + ESM + types)
npm run build
# Lint
npm run lint- Node.js >= 22
- Moleculer >= 0.15.0-beta
MIT
