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 blockingteam_coordinate()rounds. - The foreground
Agenttool delegates one subtask. On an async parent turn, opt-in async sub-agents can run multipleAgentcalls concurrently. -
BackgroundAgentand/bgare 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.
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.