Maniac Docs
Background Tasks

Background control tools

bg_list, bg_check, bg_cancel, and bg_wait — LM-facing tools for supervising background agent runs.

Background control tools

When a run carries an active BackgroundTaskDispatcher and the agent exposes at least one background-eligible tool, the runner auto-injects four built-in tools:

ToolPurpose
bg_listList in-flight tasks for the current agent + thread
bg_checkInspect status and recent trace events for one task
bg_cancelRequest cooperative cancellation
bg_waitBlock until one or more tasks reach a terminal state

These mirror Python tools/builtin/background_control.py and require no per-agent wiring — they read the active RunContext via getActiveRunContext().

Auto-injection gate

Injection fires only when both conditions hold:

  1. RunContext.dispatcher is set (from Maniac.backgroundTasks or RunOptions.dispatcher)
  2. At least one resolved tool has background with enabled !== false, or is a delegated sub-agent wrapper eligible for background dispatch

Without a dispatcher the bg_* tools have nothing to call; without an eligible tool the surface would be noisy for no benefit.

bg_list

Returns every pending task for the active (agent_id, thread_id):

{
  "ok": true,
  "value": {
    "tasks": [
      {
        "task_id": "abc123",
        "status": "running",
        "tool": "research",
        "enqueued_at": "2026-06-08T12:00:00.000Z"
      }
    ]
  }
}

Calling bg_list outside an active run returns ok: false with a descriptive error instead of throwing.

bg_check

Pass a task_id from bg_list or a background tool's synthetic ack. Optionally set last_n_events to include recent trace digests:

await bgCheck.invoke({ task_id: "abc123", last_n_events: 5 });

bg_cancel

Request cancellation of an in-flight task. Returns whether the cancel signal was delivered or the task was already terminal:

{ "ok": true, "value": { "ok": true, "status": "cancelled", "task_id": "abc123" } }

bg_wait

Block until tasks terminate. The schema advertises a single task-targeting field — task_ids: string[] — plus optional mode and timeout_s:

// Single task
await bgWait.invoke({ task_ids: ["abc123"] });

// Fan-in: wait for all
await bgWait.invoke({ task_ids: ["a", "b", "c"], mode: "all" });

// First finisher
await bgWait.invoke({ task_ids: ["a", "b"], mode: "any" });
modeBehavior
"all" (default)Block until every listed task terminates
"any"Return when the first listed task terminates; others keep running

timeout_s returns { status: "timeout", ... } without cancelling in-flight tasks.

Return shapes

  • Single id in task_ids: { status, record, result } — same shape as legacy single-task waits
  • Multiple ids: { status, results, winner? } map keyed by task_id

Legacy task_id fallback

The runtime silently accepts task_id: "<string>" when task_ids is absent (older traces and strict-mode hedges that emit task_id: "" alongside a populated array). When both are present, task_ids wins.

OpenAI-safe schemas

None of the bg_* input_schema objects use top-level oneOf, anyOf, allOf, enum, or not — OpenAI's function-parameter validator rejects those keywords at the schema root.

Supervisor example

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

const dispatcher = new BackgroundTaskDispatcher({
  config: { enabled: true, global_concurrency: 4, per_agent_concurrency: 2 }
});

const spawn = tool({
  name: "spawn_research",
  description: "Start research in the background.",
  inputSchema: {
    type: "object",
    properties: { query: { type: "string" } },
    required: ["query"]
  },
  background: { enabled: true },
  handler: ({ query }) => ({ query })
});

const result = await runAgent(
  { id: "supervisor", instructions: "Spawn then bg_wait.", model, tools: [spawn] },
  "Research competitor pricing.",
  { dispatcher }
);

The model can interleave bg_list / bg_check polling with bg_wait when it needs final answers before continuing.

On this page