Background Task Management in Craft-Agents: A Technical Deep Dive

Craft-Agents treats background execution as a first-class concept, using a pipeline that detects background flags, tracks tasks via agent IDs, and groups child activities under parent tasks for clean UI rendering.

Background task management in the Craft-Agents SDK enables long-running operations to execute asynchronously while the UI maintains real-time status updates. When a Task or Bash Shell is invoked with run_in_background: true, the system extracts identifiers, emits tracking events, and aggregates metrics upon completion. This article examines the implementation across packages/shared/src/agent/tool-matching.ts and packages/ui/src/components/chat/turn-utils.ts.

Detecting Background Task Events

The detection logic resides in packages/shared/src/agent/tool-matching.ts, specifically within the detectBackgroundEvents function. When a tool result arrives, the code inspects the payload for the run_in_background flag and extracts the agentId from the result text.

Identifying Task Background Events

For parent tasks, the system checks three conditions: the tool name indicates a parent task, the input contains run_in_background: true, and the result string is present and error-free. A regex captures the agentId, triggering a task_backgrounded event:

// packages/shared/src/agent/tool-matching.ts
if (isParentTaskTool(entry.name) && entry.input?.run_in_background === true
    && !isError && resultStr) {
  const agentIdMatch = resultStr.match(/agentId:\s*([a-zA-Z0-9_-]+)/);
  if (agentIdMatch?.[1]) {
    events.push({
      type: 'task_backgrounded',
      toolUseId,
      taskId: agentIdMatch[1],
      turnId,
      ...(typeof intentValue === 'string' && { intent: intentValue }),
    });
  }
}

Handling Shell Background Events

A similar branch manages Bash shells, looking for shell_id or backgroundTaskId in the result payload to emit shell_backgrounded events. This ensures both task agents and shell processes receive consistent background treatment.

Tracking Task Output and Metrics

Once a background task completes, it returns a TaskOutput tool result containing the original task_id (matching the agentId) and execution metrics. The UI layer in packages/ui/src/components/chat/turn-utils.ts manages this mapping.

Mapping Agent IDs to Task Results

The system builds a lookup table to associate completed TaskOutput activities with their respective agent IDs:

// packages/ui/src/components/chat/turn-utils.ts
const taskOutputByAgentId = new Map<string, TaskOutputData>();
for (const activity of activities) {
  if (activity.toolName === 'TaskOutput' && activity.status === 'completed') {
    const taskId = activity.toolInput?.task_id as string | undefined;
    if (taskId) {
      const data = extractTaskOutputData(activity);
      if (data) taskOutputByAgentId.set(taskId, data);
    }
  }
}

The extractTaskOutputData function parses the JSON payload to extract duration_ms and token usage statistics.

Linking Parent Tasks to Background Jobs

To correlate parent tasks with their background execution data, the code scans activities for completed or backgrounded parent tasks and extracts the agentId from the content string:

// packages/ui/src/components/chat/turn-utils.ts
for (const activity of activities) {
  if (isParentTaskTool(activity.toolName ?? '') &&
      (activity.status === 'completed' || activity.status === 'backgrounded') &&
      activity.content) {
    const agentIdMatch = activity.content.match(/agentId:\s*([a-zA-Z0-9_-]+)/);
    const capturedAgentId = agentIdMatch?.[1];
    if (capturedAgentId && activity.toolUseId) {
      taskToAgentId.set(activity.toolUseId, capturedAgentId);
    }
  }
}

This creates a bridge between toolUseId and agentId, enabling the UI to retrieve metrics from taskOutputByAgentId when rendering the parent task.

Grouping and UI Presentation

The final phase transforms the flat activity list into a hierarchical structure suitable for rendering, hiding implementation details like raw TaskOutput entries while surfacing their data.

Activity Grouping Logic

The groupActivitiesByParent function categorizes activities into standalone items or grouped task structures:

// packages/ui/src/components/chat/turn-utils.ts
if (isParentTaskTool(activity.toolName ?? '')) {
  const children = activity.toolUseId
    ? (childrenByParent.get(activity.toolUseId) || [])
    : [];

  let taskOutputData: TaskOutputData | undefined;
  if (activity.toolUseId) {
    const agentId = taskToAgentId.get(activity.toolUseId);
    if (agentId) taskOutputData = taskOutputByAgentId.get(agentId);
  }

  result.push({
    type: 'group',
    parent: activity,
    children: children.sort((a, b) => a.timestamp - b.timestamp),
    taskOutputData,
  });
}

Orphan activities without parents are added directly to the result array. TaskOutput activities are intentionally skipped in the main loop because their data is already incorporated into the parent group.

Rendering Background Status

UI components consume the grouped structure to display:

  • A "running in background" badge when status === 'backgrounded'
  • Expandable task groups showing child activities
  • Aggregated metrics (durationMs, input_tokens, output_tokens) extracted from taskOutputData

The styling leverages the bg-background class defined in packages/shared/src/config/theme.ts, ensuring consistent visual treatment across components like OverlayErrorBanner.

Complete Code Example

The following example demonstrates the full lifecycle of a background task invocation:

// 1️⃣ Invoke a Task with background flag
await agent.runTool('Task', {
  _intent: 'Search repository',
  run_in_background: true,
  query: 'background task detection',
});

// 2️⃣ The SDK returns something like:
//    "Task completed successfully\n\nagentId: abc123"

// 3️⃣ `detectBackgroundEvents` creates a `task_backgrounded` event.
//    UI marks the turn as "backgrounded".

// 4️⃣ Later a `TaskOutput` tool arrives:
//    {
//      toolName: 'TaskOutput',
//      toolInput: { task_id: 'abc123' },
//      content: '{"duration_ms":1200,"usage":{"input_tokens":30,"output_tokens":45}}'
//    }

// 5️⃣ `groupActivitiesByParent` links `abc123` → parent Task,
//    attaches the duration/tokens, and hides the raw `TaskOutput` activity.
//    The UI now shows "Task (1.2 s, 75 tokens)".

Summary

  • Detection: The detectBackgroundEvents function in packages/shared/src/agent/tool-matching.ts identifies background execution by checking run_in_background: true and extracting agentId or shell_id from results.
  • Tracking: packages/ui/src/components/chat/turn-utils.ts maintains two mappings: taskOutputByAgentId (metrics storage) and taskToAgentId (parent-to-background linkage).
  • Grouping: The groupActivitiesByParent function creates hierarchical structures that hide raw TaskOutput entries while surfacing their metrics in the parent task UI.
  • Rendering: UI components consume grouped data to display background status badges and aggregated statistics (duration, token usage) without exposing implementation details.

Frequently Asked Questions

How does Craft-Agents detect when a task runs in the background?

The SDK checks tool results in packages/shared/src/agent/tool-matching.ts using the detectBackgroundEvents function. When a tool result contains run_in_background: true in its input and the result text includes an agentId (extracted via regex), the system emits a task_backgrounded event. This event signals the UI to mark the turn as running asynchronously.

Where are background task metrics stored after completion?

Completed background tasks return a TaskOutput tool result containing duration_ms and token usage statistics. The UI layer in packages/ui/src/components/chat/turn-utils.ts parses these results into taskOutputByAgentId, a Map that stores metrics keyed by the task_id (matching the original agentId). This allows the system to attach final metrics to the parent task once it completes.

How does the UI handle background task visualization?

The UI uses the groupActivitiesByParent function in packages/ui/src/components/chat/turn-utils.ts to transform flat activity lists into hierarchical groups. When rendering, components check for status === 'backgrounded' to display loading indicators. Once complete, the UI extracts metrics from the grouped taskOutputData and displays aggregated duration and token counts alongside the parent task, while hiding the raw TaskOutput activity from the user view.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →