Daydreams Logo
Concepts

Tasks

Managing asynchronous operations and concurrency.

What is a Task?

A task is any operation that takes time to complete, like:

  • Calling a weather API (might take 500ms)
  • Saving data to a database (might take 200ms)
  • Processing an image (might take 2 seconds)
  • Sending an email (might take 1 second)

The Problem

Imagine your agent needs to do 10 things at once:

problem-example.ts
// User asks: "What's the weather in 5 cities?"
// Agent needs to call weather API 5 times
// Without task management:
await getWeather("New York"); // 500ms
await getWeather("London"); // 500ms
await getWeather("Tokyo"); // 500ms
await getWeather("Paris"); // 500ms
await getWeather("Sydney"); // 500ms
// Total: 2.5 seconds (slow!)

Even worse - what if your agent tries to make 100 API calls at once? The external service might block you or your server might crash.

The Solution: Task Management

Daydreams automatically manages these operations for you:

solution-example.ts
// With task management:
// Runs 3 operations at the same time (concurrent)
// Queues the rest until the first ones finish
// Total: ~1 second (much faster!)
 
const results = await Promise.all([
  getWeather("New York"), // Starts immediately
  getWeather("London"), // Starts immediately
  getWeather("Tokyo"), // Starts immediately
  getWeather("Paris"), // Waits in queue
  getWeather("Sydney"), // Waits in queue
]);

How It Works in Your Agent

When you write action handlers, this happens automatically:

weather-action.ts
const weatherAction = action({
  name: "get-weather",
  description: "Get weather for a city",
  schema: z.object({
    city: z.string(),
  }),
  handler: async ({ city }) => {
    // This handler runs as a "task"
    // Daydreams automatically:
    // 1. Limits how many run at once (default: 3)
    // 2. Queues extras until slots open up
    // 3. Handles errors and retries
 
    const response = await fetch(`https://api.weather.com/${city}`);
    return await response.json();
  },
});

When your LLM calls this action multiple times:

<action_call name="get-weather">{"city": "New York"}</action_call>
<action_call name="get-weather">{"city": "London"}</action_call>
<action_call name="get-weather">{"city": "Tokyo"}</action_call>
<action_call name="get-weather">{"city": "Paris"}</action_call>

Daydreams automatically:

  • Runs the first 3 immediately
  • Queues "Paris" until one finishes
  • Prevents your agent from overwhelming the weather API

Configuring Task Limits

You can control how many tasks run simultaneously:

agent-config.ts
import { createDreams, TaskRunner } from "@daydreamsai/core";
 
// Default: 3 tasks at once
const agent = createDreams({
  model: openai("gpt-4o"),
  extensions: [weatherExtension],
});
 
// Custom: 5 tasks at once (for faster APIs)
const fasterAgent = createDreams({
  model: openai("gpt-4o"),
  extensions: [weatherExtension],
  taskRunner: new TaskRunner(5),
});
 
// Custom: 1 task at once (for rate-limited APIs)
const slowAgent = createDreams({
  model: openai("gpt-4o"),
  extensions: [weatherExtension],
  taskRunner: new TaskRunner(1),
});

Best Practices for Action Handlers

1. Use async/await Properly

good-handler.ts
// ✅ Good - properly handles async operations
handler: async ({ userId }) => {
  const user = await database.getUser(userId);
  const preferences = await database.getPreferences(userId);
  return { user, preferences };
};
 
// ❌ Bad - doesn't wait for async operations
handler: ({ userId }) => {
  database.getUser(userId); // This returns a Promise!
  return { status: "done" }; // Completes before database call finishes
};

2. Handle Cancellation for Long Operations

cancellation-example.ts
handler: async ({ largeDataset }, ctx) => {
  for (let i = 0; i < largeDataset.length; i++) {
    // Check if the agent wants to cancel this task
    if (ctx.abortSignal.aborted) {
      throw new Error("Operation cancelled");
    }
 
    await processItem(largeDataset[i]);
  }
};

3. Make Handlers Idempotent (for Retries)

idempotent-example.ts
// ✅ Good - safe to run multiple times
handler: async ({ email, message }) => {
  // Check if email already sent
  const existing = await emailLog.findByKey(`${email}-${hash(message)}`);
  if (existing) {
    return { status: "already_sent", messageId: existing.messageId };
  }
 
  // Send email and log it
  const result = await emailService.send(email, message);
  await emailLog.create({
    email,
    messageHash: hash(message),
    messageId: result.id,
  });
  return { status: "sent", messageId: result.id };
};
 
// ❌ Bad - creates duplicate emails if retried
handler: async ({ email, message }) => {
  const result = await emailService.send(email, message);
  return { status: "sent", messageId: result.id };
};

Advanced: Custom Task Types

Most users won't need this, but you can define custom task types:

custom-task.ts
import { task } from "@daydreamsai/core";
 
const processVideoTask = task(
  "agent:video:process",
  async (params: { videoUrl: string }, ctx) => {
    ctx.debug("processVideo", "Starting video processing", params);
 
    // Long-running video processing
    const result = await videoProcessor.process(params.videoUrl);
 
    return { processedUrl: result.url, duration: result.duration };
  },
  {
    retry: 2, // Retry twice on failure
  }
);
 
// Use it in your agent
agent.taskRunner.enqueueTask(processVideoTask, { videoUrl: "https://..." });

Key Takeaways

  • Tasks happen automatically - Your action handlers become tasks
  • Concurrency is controlled - Default limit is 3 simultaneous tasks
  • Queuing prevents overload - Extra tasks wait their turn
  • Write async handlers properly - Use async/await and handle cancellation
  • Configure based on your APIs - Increase limit for fast APIs, decrease for rate-limited ones

The task system ensures your agent performs well without overwhelming external services or consuming excessive resources.