Daydreams Logo
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

database-service.ts
// 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

discord-service.ts
// 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:

manual-connection-nightmare.ts
// ❌ 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:

clean-service-solution.ts
// ✅ 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

container-example.ts
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

container-methods.ts
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):

service-lifecycle.ts
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

trading-service.ts
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

logging-service.ts
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:

service-dependencies.ts
// 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

environment-services.ts
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

service-configuration.ts
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

single-responsibility.ts
// ✅ 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

clear-dependencies.ts
// ✅ 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

error-handling.ts
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

resource-cleanup.ts
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

missing-dependency-error.ts
// 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

circular-dependency-fix.ts
// ❌ 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

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.