Maniac Docs
Background Tasks

BackgroundTaskDispatcher

Configure concurrency, enqueue background agent runs, and drain the scheduler from Maniac.

BackgroundTaskDispatcher

BackgroundTaskDispatcher is the in-process scheduler for background agent runs. It is the TypeScript counterpart to Python agents.background_tasks.BackgroundTaskManager, adapted so each task wraps a full runAgent / runAgentStream call rather than a fire-and-forget tool invocation.

Enable on Maniac

Pass backgroundTasks to the Maniac constructor. When enabled: true, the app lazily constructs a shared dispatcher:

import { Maniac } from "@maniac-ai/agents";

const app = new Maniac({
  backgroundTasks: {
    enabled: true,
    global_concurrency: 10,
    per_agent_concurrency: 5,
    default_timeout_s: 300,
    default_retries: 0,
    default_wait_timeout_s: 300,
    backpressure: "queue" // or "reject"
  }
});
FieldDefaultPurpose
global_concurrency10Max in-flight tasks across all agents
per_agent_concurrency5Max in-flight tasks per agent_id
default_timeout_s300Per-task run timeout unless overridden
backpressure"queue""reject" throws when caps are saturated instead of waiting

Wire caps from environment variables in your host app when needed — the SDK reads config objects, not process.env directly:

backgroundTasks: {
  enabled: true,
  global_concurrency: Number(process.env.MANIAC_BG_GLOBAL_CONCURRENCY ?? 10),
  per_agent_concurrency: Number(process.env.MANIAC_BG_PER_AGENT_CONCURRENCY ?? 5)
}

Enqueue programmatically

Maniac.enqueueBackground(agentId, prompt, options?) enqueues a background run and returns the queued BackgroundTaskRecord synchronously. The run starts when a concurrency slot is free.

const record = app.enqueueBackground("researcher", "Summarize the Q3 report.", {
  threadId: "thread-abc",
  timeoutMs: 120_000
});
console.log(record.task_id, record.status); // "queued"

Lower-level callers can construct BackgroundTaskDispatcher directly and call enqueue({ spec, prompt, ... }).

Drain and stream

APIBehavior
dispatcher.runUntilIdle()Block until every enqueued task reaches a terminal state
dispatcher.runStreamUntilIdle()Async generator yielding merged trace envelopes from all children
app.runUntilIdle() / app.runStreamUntilIdle()App-level wrappers; optionally enqueue one more task before draining

Call dispatcher.aclose() (or app.aclose()) on shutdown to refuse new tasks and drain in-flight work.

Spawning from the tool loop

Mark a tool background-eligible so the runner can dispatch it through the scheduler instead of blocking the foreground turn:

import { tool } from "@maniac-ai/agents";

const analyze = tool({
  name: "analyze",
  description: "Kick off analysis in the background.",
  inputSchema: {
    type: "object",
    properties: { topic: { type: "string" } },
    required: ["topic"]
  },
  background: { enabled: true },
  handler: async ({ topic }) => ({ topic })
});

Set background: { enabled: false } to opt a tool out. Delegated sub-agent tools (toolset.asDelegated()) can also participate when they carry a background config.

The LM receives a synthetic ack with a task_id immediately; use bg_list or bg_check to inspect progress and bg_wait to block until terminal.

Orchestrator pattern

A common supervisor setup:

  1. Register child agents with app.agent({ id, instructions, toolsets })
  2. Expose children as background-eligible tools (direct tools or delegated toolsets)
  3. Give the supervisor backgroundTasks and at least one eligible tool so bg_* tools auto-inject
  4. Let the model call spawn tools, then bg_wait({ task_ids: [...] }) for fan-in

RunOptions.dispatcher threads the active dispatcher into nested runAgent calls so children spawned mid-run share the same scheduler.

Lifecycle hooks

Hooks compose in priority order: manager → agent → task. Attach them on BackgroundTasksConfig, Agent.background, or per-task BackgroundConfig:

backgroundTasks: {
  enabled: true,
  on_task_complete: (record) => console.log("done", record.task_id),
  on_task_failed: (record) => console.error("failed", record.error)
}

Hook throws are logged on dispatcher.errors and ignored — they never escape the dispatcher.

Cancellation and persistence

  • dispatcher.cancelTask(taskId) cooperatively aborts via the per-task AbortSignal forwarded into runAgent
  • Optional BackgroundTaskStore (memory-backed adapter) persists records across process restarts, matching Python's durable task store shape
  • Tools can call getActiveAbortSignal() for cooperative cancellation inside long handlers

On this page