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"
}
});| Field | Default | Purpose |
|---|---|---|
global_concurrency | 10 | Max in-flight tasks across all agents |
per_agent_concurrency | 5 | Max in-flight tasks per agent_id |
default_timeout_s | 300 | Per-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
| API | Behavior |
|---|---|
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:
- Register child agents with
app.agent({ id, instructions, toolsets }) - Expose children as background-eligible tools (direct tools or delegated toolsets)
- Give the supervisor
backgroundTasksand at least one eligible tool sobg_*tools auto-inject - 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-taskAbortSignalforwarded intorunAgent- 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