Advanced
Services
Dependency Injection & Lifecycle Management.
What Are Services?
Services are infrastructure managers for your agent. Think of them like the utilities in a city - you don't think about electricity or water pipes, but everything depends on them working properly.
Real Examples
Here's what services handle in your agent:
Database Connections
// Service manages database connection lifecycle
const databaseService = service({
name: "database",
register: (container) => {
// HOW to create database connection
container.singleton("db", () => new MongoDB(process.env.MONGODB_URI));
},
boot: async (container) => {
// WHEN to connect (agent startup)
const db = container.resolve("db");
await db.connect();
console.log("✅ Database connected!");
},
});
// Now any action can use the database:
// const db = ctx.container.resolve("db");
// await db.collection("users").findOne({ id: userId });
API Client Management
// Service manages Discord client lifecycle
const discordService = service({
name: "discord",
register: (container) => {
// HOW to create Discord client
container.singleton(
"discordClient",
() =>
new Discord.Client({
intents: [Discord.GatewayIntentBits.Guilds],
token: process.env.DISCORD_TOKEN,
})
);
},
boot: async (container) => {
// WHEN to connect (agent startup)
const client = container.resolve("discordClient");
await client.login();
console.log("✅ Discord bot online!");
},
});
// Now any action can send Discord messages:
// const client = ctx.container.resolve("discordClient");
// await client.channels.get(channelId).send("Hello!");
The Problem: Manual Connection Management
Without services, you'd have to manage connections manually in every action:
// ❌ Without services - repeated connection code everywhere
const sendDiscordMessageAction = action({
name: "send-discord-message",
handler: async ({ channelId, message }) => {
// Create new Discord client every time (slow!)
const client = new Discord.Client({
intents: [Discord.GatewayIntentBits.Guilds],
token: process.env.DISCORD_TOKEN,
});
// Connect every time (slow!)
await client.login();
// Send message
await client.channels.get(channelId).send(message);
// Close connection
await client.destroy();
},
});
const banUserAction = action({
name: "ban-user",
handler: async ({ userId, guildId }) => {
// Same connection code repeated (DRY violation!)
const client = new Discord.Client({
intents: [Discord.GatewayIntentBits.Guilds],
token: process.env.DISCORD_TOKEN,
});
await client.login(); // Slow reconnection every time!
const guild = await client.guilds.fetch(guildId);
await guild.members.ban(userId);
await client.destroy();
},
});
// Problems:
// 🐌 Slow - reconnecting for every action
// 🔄 Repetitive - same connection code everywhere
// 💔 Unreliable - connection failures not handled
// 📈 Expensive - multiple connections to same service
The Solution: Services Manage Infrastructure
With services, connections are created once and shared:
// ✅ With services - clean, fast, reliable
// 1. Service handles connection once
const discordService = service({
name: "discord",
register: (container) => {
container.singleton(
"discordClient",
() =>
new Discord.Client({
intents: [Discord.GatewayIntentBits.Guilds],
token: process.env.DISCORD_TOKEN,
})
);
},
boot: async (container) => {
const client = container.resolve("discordClient");
await client.login(); // Connect once at startup
},
});
// 2. Actions just use the shared client
const sendDiscordMessageAction = action({
name: "send-discord-message",
handler: async ({ channelId, message }, ctx) => {
// Get already-connected client (fast!)
const client = ctx.container.resolve("discordClient");
// Send message immediately (no connection delay)
await client.channels.get(channelId).send(message);
},
});
const banUserAction = action({
name: "ban-user",
handler: async ({ userId, guildId }, ctx) => {
// Same client, no duplication (DRY!)
const client = ctx.container.resolve("discordClient");
const guild = await client.guilds.fetch(guildId);
await guild.members.ban(userId);
},
});
// Benefits:
// ⚡ Fast - client connected once at startup
// 🔄 DRY - no repeated connection code
// 💪 Reliable - connection managed centrally
// 💰 Efficient - one connection shared across actions
How Services Work: The Container
Services use a dependency injection container - think of it like a storage system for shared resources:
Container Basics
import { createContainer } from "@daydreamsai/core";
const container = createContainer();
// Store things in the container
container.singleton("databaseUrl", () => process.env.DATABASE_URL);
container.singleton("database", (c) => new MongoDB(c.resolve("databaseUrl")));
// Get things from the container
const db = container.resolve("database");
await db.connect();
Container Methods
const container = createContainer();
// singleton() - Create once, reuse everywhere
container.singleton("apiClient", () => new ApiClient());
const client1 = container.resolve("apiClient");
const client2 = container.resolve("apiClient");
// client1 === client2 (same instance)
// register() - Create new instance each time
container.register("requestId", () => Math.random().toString());
const id1 = container.resolve("requestId");
const id2 = container.resolve("requestId");
// id1 !== id2 (different instances)
// instance() - Store pre-created object
const config = { apiKey: "secret123" };
container.instance("config", config);
const retrievedConfig = container.resolve("config");
// retrievedConfig === config (exact same object)
// alias() - Create alternative name
container.alias("db", "database");
const db = container.resolve("db"); // Same as resolve("database")
Service Lifecycle
Services have two phases: register (setup) and boot (initialize):
const redisService = service({
name: "redis",
// Phase 1: REGISTER - Define how to create things
register: (container) => {
// Just define the factory functions
container.singleton("redisConfig", () => ({
host: process.env.REDIS_HOST || "localhost",
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD,
}));
container.singleton(
"redisClient",
(c) => new Redis(c.resolve("redisConfig"))
);
console.log("📝 Redis service registered");
},
// Phase 2: BOOT - Actually connect/initialize
boot: async (container) => {
// Now connect to Redis
const client = container.resolve("redisClient");
await client.connect();
// Test the connection
await client.ping();
console.log("🚀 Redis service booted and connected!");
},
});
// Lifecycle order:
// 1. All services run register() first
// 2. Then all services run boot()
// 3. This ensures dependencies are available when needed
Real-World Service Examples
Trading Service
const tradingService = service({
name: "trading",
register: (container) => {
// Register trading client
container.singleton(
"alpacaClient",
() =>
new Alpaca({
key: process.env.ALPACA_KEY,
secret: process.env.ALPACA_SECRET,
paper: process.env.NODE_ENV !== "production",
})
);
// Register portfolio tracker
container.singleton(
"portfolio",
(c) => new PortfolioTracker(c.resolve("alpacaClient"))
);
// Register risk manager
container.singleton(
"riskManager",
() =>
new RiskManager({
maxPositionSize: 0.1, // 10% of portfolio
stopLossPercent: 0.05, // 5% stop loss
})
);
},
boot: async (container) => {
// Initialize connections
const client = container.resolve("alpacaClient");
await client.authenticate();
const portfolio = container.resolve("portfolio");
await portfolio.sync(); // Load current positions
console.log("💰 Trading service ready!");
},
});
// Now trading actions can use these:
const buyStockAction = action({
name: "buy-stock",
handler: async ({ symbol, quantity }, ctx) => {
const client = ctx.container.resolve("alpacaClient");
const riskManager = ctx.container.resolve("riskManager");
// Check risk before buying
if (riskManager.canBuy(symbol, quantity)) {
return await client.createOrder({
symbol,
qty: quantity,
side: "buy",
type: "market",
});
} else {
throw new Error("Risk limits exceeded");
}
},
});
Logging Service
const loggingService = service({
name: "logging",
register: (container) => {
// Different loggers for different purposes
container.singleton(
"appLogger",
() =>
new Logger({
level: process.env.LOG_LEVEL || "info",
format: "json",
transports: [new FileTransport("app.log"), new ConsoleTransport()],
})
);
container.singleton(
"actionLogger",
() =>
new Logger({
level: "debug",
prefix: "[ACTION]",
transports: [new FileTransport("actions.log")],
})
);
container.singleton(
"errorLogger",
() =>
new Logger({
level: "error",
format: "detailed",
transports: [
new FileTransport("errors.log"),
new SlackTransport(process.env.SLACK_WEBHOOK),
],
})
);
},
boot: async (container) => {
const appLogger = container.resolve("appLogger");
appLogger.info("🚀 Application starting up");
},
});
// Actions can use appropriate logger:
const dangerousAction = action({
name: "delete-user",
handler: async ({ userId }, ctx) => {
const actionLogger = ctx.container.resolve("actionLogger");
const errorLogger = ctx.container.resolve("errorLogger");
try {
actionLogger.info(`Attempting to delete user ${userId}`);
// ... deletion logic ...
actionLogger.info(`Successfully deleted user ${userId}`);
} catch (error) {
errorLogger.error(`Failed to delete user ${userId}`, error);
throw error;
}
},
});
Service Dependencies
Services can depend on other services:
// Base database service
const databaseService = service({
name: "database",
register: (container) => {
container.singleton("db", () => new MongoDB(process.env.DB_URI));
},
boot: async (container) => {
const db = container.resolve("db");
await db.connect();
},
});
// Cache service that depends on database
const cacheService = service({
name: "cache",
register: (container) => {
// Redis for fast cache
container.singleton("redis", () => new Redis(process.env.REDIS_URL));
// Cache manager that uses both Redis and MongoDB
container.singleton(
"cacheManager",
(c) =>
new CacheManager({
fastCache: c.resolve("redis"), // From this service
slowCache: c.resolve("db"), // From database service
ttl: 3600, // 1 hour
})
);
},
boot: async (container) => {
const redis = container.resolve("redis");
await redis.connect();
const cacheManager = container.resolve("cacheManager");
await cacheManager.initialize();
console.log("💾 Cache service ready!");
},
});
// Extensions can use both services
const dataExtension = extension({
name: "data",
services: [databaseService, cacheService], // Order doesn't matter
actions: [
action({
name: "get-user",
handler: async ({ userId }, ctx) => {
const cache = ctx.container.resolve("cacheManager");
// Try cache first
let user = await cache.get(`user:${userId}`);
if (!user) {
// Cache miss - get from database
const db = ctx.container.resolve("db");
user = await db.collection("users").findOne({ _id: userId });
// Store in cache for next time
await cache.set(`user:${userId}`, user);
}
return user;
},
}),
],
});
Advanced Patterns
Environment-Based Services
const storageService = service({
name: "storage",
register: (container) => {
if (process.env.NODE_ENV === "production") {
// Production: Use S3
container.singleton(
"storage",
() =>
new S3Storage({
bucket: process.env.S3_BUCKET,
region: process.env.AWS_REGION,
})
);
} else {
// Development: Use local filesystem
container.singleton(
"storage",
() => new LocalStorage({ path: "./uploads" })
);
}
},
boot: async (container) => {
const storage = container.resolve("storage");
await storage.initialize();
if (process.env.NODE_ENV === "production") {
console.log("☁️ S3 storage ready");
} else {
console.log("📁 Local storage ready");
}
},
});
Service Configuration
const notificationService = service({
name: "notifications",
register: (container) => {
// Configuration
container.singleton("notificationConfig", () => ({
slack: {
webhook: process.env.SLACK_WEBHOOK,
channel: process.env.SLACK_CHANNEL || "#alerts",
},
email: {
smtpHost: process.env.SMTP_HOST,
smtpPort: process.env.SMTP_PORT,
from: process.env.EMAIL_FROM,
},
discord: {
webhookUrl: process.env.DISCORD_WEBHOOK,
},
}));
// Notification clients
container.singleton("slackNotifier", (c) => {
const config = c.resolve("notificationConfig");
return new SlackNotifier(config.slack);
});
container.singleton("emailNotifier", (c) => {
const config = c.resolve("notificationConfig");
return new EmailNotifier(config.email);
});
container.singleton("discordNotifier", (c) => {
const config = c.resolve("notificationConfig");
return new DiscordNotifier(config.discord);
});
// Unified notification manager
container.singleton(
"notificationManager",
(c) =>
new NotificationManager({
slack: c.resolve("slackNotifier"),
email: c.resolve("emailNotifier"),
discord: c.resolve("discordNotifier"),
})
);
},
boot: async (container) => {
const manager = container.resolve("notificationManager");
await manager.testConnections();
console.log("📢 Notification service ready!");
},
});
Best Practices
1. Single Responsibility
// ✅ Good - each service handles one thing
const databaseService = service({
name: "database",
// Only database connection management
});
const cacheService = service({
name: "cache",
// Only caching functionality
});
const loggingService = service({
name: "logging",
// Only logging configuration
});
// ❌ Bad - one service doing everything
const everythingService = service({
name: "everything",
register: (container) => {
// Database + cache + logging + API clients + notifications...
// Too many responsibilities!
},
});
2. Clear Dependencies
// ✅ Good - clear what this service provides
const authService = service({
name: "auth",
register: (container) => {
container.singleton("jwtSecret", () => process.env.JWT_SECRET);
container.singleton(
"tokenManager",
(c) => new TokenManager(c.resolve("jwtSecret"))
);
container.singleton(
"userAuthenticator",
(c) =>
new UserAuthenticator({
tokenManager: c.resolve("tokenManager"),
database: c.resolve("db"), // Depends on database service
})
);
},
});
// ❌ Bad - unclear what's provided
const helperService = service({
name: "helper",
register: (container) => {
container.singleton("stuff", () => new Thing());
container.singleton("helper", () => new Helper());
// What do these do? How do they relate?
},
});
3. Graceful Error Handling
const apiService = service({
name: "external-api",
register: (container) => {
container.singleton(
"apiClient",
() =>
new ApiClient({
baseUrl: process.env.API_URL,
apiKey: process.env.API_KEY,
timeout: 10000,
retries: 3,
})
);
},
boot: async (container) => {
try {
const client = container.resolve("apiClient");
await client.healthCheck();
console.log("✅ External API connection verified");
} catch (error) {
console.error("❌ External API connection failed:", error.message);
// Don't crash the agent - just log the error
// Actions can handle API unavailability gracefully
console.warn(
"⚠️ Agent will start but external API features may be limited"
);
}
},
});
4. Resource Cleanup
const databaseService = service({
name: "database",
register: (container) => {
container.singleton("db", () => {
const db = new MongoDB(process.env.DB_URI);
// Register cleanup when process exits
process.on("SIGINT", async () => {
console.log("🔄 Closing database connection...");
await db.close();
console.log("✅ Database connection closed");
process.exit(0);
});
return db;
});
},
boot: async (container) => {
const db = container.resolve("db");
await db.connect();
},
});
Troubleshooting
Missing Dependencies
// Error: "Token 'databaseClient' not found in container"
// ❌ Problem - trying to resolve unregistered token
const action = action({
handler: async (args, ctx) => {
const db = ctx.container.resolve("databaseClient"); // Not registered!
},
});
// ✅ Solution - ensure service registers the token
const databaseService = service({
register: (container) => {
container.singleton("databaseClient", () => new Database());
// ^^^^^^^^^^^^^^ Must match resolve token
},
});
Circular Dependencies
// ❌ Problem - services depending on each other
const serviceA = service({
register: (container) => {
container.singleton("a", (c) => new A(c.resolve("b"))); // Needs B
},
});
const serviceB = service({
register: (container) => {
container.singleton("b", (c) => new B(c.resolve("a"))); // Needs A
},
});
// ✅ Solution - introduce a coordinator service
const coordinatorService = service({
register: (container) => {
container.singleton("a", () => new A());
container.singleton("b", () => new B());
container.singleton(
"coordinator",
(c) => new Coordinator(c.resolve("a"), c.resolve("b"))
);
},
boot: async (container) => {
const coordinator = container.resolve("coordinator");
coordinator.wireComponents(); // Set up relationships after creation
},
});
Next Steps
- Extensions vs Services - When to use services vs extensions
- Extensions Guide - Build complete feature packages
- Built-in Services - See real service examples
Key Takeaways
- Services manage infrastructure - Database connections, API clients, utilities
- Container provides shared access - One connection used by all actions
- Two-phase lifecycle - Register (setup) then boot (initialize)
- Dependency injection - Components don't create their own dependencies
- Clean separation - Infrastructure separate from business logic
Services let you build reliable agents with proper resource management and clean architecture.