Skip to contents

Language: English | 简体中文

Overview

codeagent provides several distinct ways to delegate work. Choose one according to whether tasks are independent, dependency-constrained, foreground, or truly non-blocking:

  • team_run() is fixed fan-out for independent tasks. It runs workers in parallel but waits for every result before returning.
  • team_coordinate() is a blocking work-stealing pool backed by a shared SQLite task board. It supports a dependency DAG and uneven task durations.
  • team_lead() asks an LLM lead to decompose, review, and optionally re-plan a goal over several blocking team_coordinate() rounds.
  • The foreground Agent tool delegates one subtask. On an async parent turn, opt-in async sub-agents can run multiple Agent calls concurrently.
  • BackgroundAgent and /bg are fire-and-forget paths whose results are surfaced on a later turn.

team_run() and team_coordinate() require mirai. Board operations require DBI and RSQLite. team_run() and team_coordinate() retain a worker permission default of "bypass" because daemon processes cannot answer interactive approval prompts; use it only for trusted tasks and environments. team_lead() now defaults to "dont_ask", so operations that would require approval are rejected unless callers explicitly choose a different mode.

Coordination at a glance

team_run(tasks)                       FIXED FAN-OUT (blocking caller)
  N mirai daemons; one codeagent query per task
  -> wait for all tasks -> list of results in input order

team_coordinate(tasks, blocked_by)   WORK-STEALING (blocking caller)
  board_create(): tasks + deps + messages tables in SQLite
  seed all tasks -> wire blocked_by indices -> reject dependency cycles
  N workers, each: repeat {
    board_claim() -- atomic claim of lowest-id eligible pending task
      | none claimable
      +- no unfinished tasks       -> break
      +- reclaim stale claims      -> retry abandoned work
      +- board is truly stalled    -> break
      +- blocker still running     -> sleep(backoff), then retry
    run codeagent(task)
    board_complete(result)
    board_send_message("completed task", recipient = "coordinator")
  }
  -> wait for workers -> final board data.frame

team_lead(goal, max_rounds)           LLM-LEAD LOOP (blocking caller)
  structured decomposition -> tasks + dependency DAG
  -> team_coordinate(...)
  -> structured review: done or follow-up tasks
  -> repeat with a new board until done, no tasks, or max_rounds

Fixed fan-out: team_run()

team_run(tasks, model = NULL, n_workers = NULL, permission_mode = "bypass", cwd = getwd()) creates a fresh client for every task and returns a list in the same order as tasks. A failed task is represented by an "[Error] ..." string rather than aborting the whole result collection.

library(codeagent)

# Review multiple files in parallel
results <- team_run(c(
  "Review R/tool_display.R for any issues",
  "Review R/permissions.R for any issues",
  "Review R/compaction.R for any issues"
))

# Each element of results is the agent's response for that task
cat(results[[1]])

Worker count defaults to min(length(tasks), parallelly::availableCores()), with a conservative fallback when parallelly is unavailable. An explicit n_workers is capped at the same cgroup-aware limit. team_run() has no worktree argument, so tasks that write to the same checkout can collide; reserve it for independent read-only work or otherwise isolated resources.

Work-stealing: team_coordinate()

The current signature is:

team_coordinate(
  tasks,
  model = NULL,
  n_workers = NULL,
  permission_mode = "bypass",
  cwd = getwd(),
  blocked_by = NULL,
  worktree = FALSE,
  backoff = 0.5,
  reclaim_timeout = 300,
  db_path = tempfile(fileext = ".sqlite")
)

Workers repeatedly claim work, so a faster worker can process more than one item. The function itself waits for all worker loops before returning the final board with columns id, prompt, owner, status, and result.

results_df <- team_coordinate(
  tasks = c("task 1", "task 2", "task 3", "task 4", "task 5"),
  n_workers = 2
)
# Returns a data.frame with columns: id, prompt, owner, status, result
print(results_df[, c("prompt", "status", "owner")])

Dependencies

blocked_by[[i]] contains 1-based indices into tasks. A task is claimable only when every valid blocker is done. Cycles are rejected before workers launch; out-of-range indices and self-dependencies are ignored.

dag_result <- team_coordinate(
  tasks = c(
    "Create the schema",
    "Implement against the schema",
    "Run integration checks"
  ),
  blocked_by = list(integer(0), 1L, 2L),
  n_workers = 2
)

Claims use a BEGIN IMMEDIATE SQLite transaction and select the lowest-id eligible task, so dependency-free boards are FIFO. If no task is claimable, workers distinguish a completed board, an in-progress blocker, and a true stall. Idle live workers reclaim claims older than reclaim_timeout; if every worker process has died, no process remains to perform that reclamation.

Worktree isolation

Set worktree = TRUE to request one temporary detached Git worktree per worker:

review <- team_coordinate(
  c("Inspect module A", "Inspect module B"),
  n_workers = 2,
  worktree = TRUE
)

This is best-effort: when Git or a repository worktree is unavailable, the worker falls back to cwd. Temporary worktrees are force-removed when workers exit. codeagent does not merge or copy edits back, so use this mode for isolated analysis unless another explicit workflow preserves the changes.

The shared task board

board_create(db_path = tempfile(fileext = ".sqlite")) creates tasks, deps, and messages tables and invisibly returns the database path. board_claim() is atomic across processes. board_complete() records a result, and board_status() returns the current task table.

Inter-agent messaging

The shared board also supports broadcast and directed message records:

db <- board_create()
board_add_task(db, "analyse the sales data")
board_add_task(db, "generate the summary report")

# Worker 1 claims a task
task <- board_claim(db, worker_id = "w1")
board_send_message(db, sender = "w1", body = "Starting analysis...",
                   recipient = "coordinator")

# Complete with result
board_complete(db, task$id, result = "Analysis complete: 3 trends found")

recipient = NULL creates a broadcast. board_messages(db, "coordinator") returns broadcasts plus messages addressed to that recipient, while board_messages(db) returns the entire log. These messages are not automatically inserted into worker prompts: the built-in coordinator writes a completion notice but does not use the message table as an agent conversation channel.

Watching and displaying a board

board_watch(db, callback, latency = 0.3) starts an event-driven file watcher when the optional watcher package is available and otherwise returns NULL. team_dashboard(db_path, poll_ms = 1500L) uses that watcher and falls back to polling. To observe a live team_coordinate() run, pass a stable db_path and run the blocking coordinator in another process or background job.

db <- tempfile(fileext = ".sqlite")
# In another R process:
# team_coordinate(c("A", "B", "C"), db_path = db)
team_dashboard(db)

LLM-led rounds: team_lead()

team_lead(goal, model = NULL, cwd = getwd(), max_rounds = 3L, n_workers = NULL, permission_mode = "dont_ask", worktree = FALSE, decompose_fn = NULL, review_fn = NULL, coordinate_fn = NULL) first asks the lead model for structured tasks and dependencies. The three callback arguments are injectable seams primarily used for testing or custom orchestration. After each coordinated round it asks whether the goal is complete and, if needed, runs only the follow-up plan. Non-empty executed boards are row-bound with a round column; if decomposition produces no tasks before any round, the empty return has only the base board columns. max_rounds is coerced to at least one. A review error is handled as “done,” so it stops rather than retrying indefinitely.

lead_result <- team_lead(
  "Review the parser and report prioritized findings with verification advice",
  max_rounds = 3,
  n_workers = 2,
  worktree = TRUE
)

Foreground, concurrent, and background sub-agents

The agent can invoke the foreground Agent tool from chat. The normal path is synchronous. Worktree isolation is configured on the client and forces codeagent’s owned Agent implementation, which creates and cleans a temporary worktree:

client <- codeagent_client(chat,
  permission_mode = "bypass",
  worktree_isolation = TRUE   # each sub-agent in its own git worktree
)

As with team worktrees, this is best-effort and does not merge edits into the main checkout.

team_run() and team_coordinate() use parallel workers internally but remain blocking APIs. A different opt-in setting, async_subagents, lets multiple foreground Agent tool calls return promises and execute concurrently only when the parent is already running through an async streaming turn. Sync one-shot/console turns fall back to the synchronous Agent path.

{
  "async_subagents": true,
  "background_agents": true
}

background_agents = true registers the model-facing BackgroundAgent tool. It returns a task id immediately; an in-memory registry polls the dedicated mirai compute profile and injects each completed result into a later system reminder once. The /bg <task> and /bgstatus local commands expose the same background registry. Background agents are disabled while Data Shield is active because a separate process cannot safely inherit the live protected-data index; use the foreground Agent tool in that case.