15. Subtasks and background execution

Complex tasks often need to be parcelled out to "sub-assistants". AVL Code has built-in subtask scheduling, with both synchronous and asynchronous modes.

15.1 Dispatching subtasks

The assistant can call a "sub-assistant" tool that runs a given prompt in an isolated subtask. The subtask completes and the result merges back into the main conversation. This is transparent to you; the tool-call card shows the subtask details.

15.1.1 Subagents inherit external MCP tools

Dispatched subagents automatically inherit the external MCP tools you configured for the workspace (previously subtasks couldn't use these external tools and you had to come back to the main conversation):

  • Mechanism: at dispatch time, AVL Code calls /admin/routes + /admin/backends to enumerate every external backend's tool namespace in this workspace (e.g. notion / memory / channels) and injects them via the ZAGENT_MCP_INHERIT_PREFIXES env var; the subagent's ZAGENT_ALLOWED_TOOLS whitelist passes those prefixes through unconditionally.
  • Coverage: all subagents that declare tools: (plan / assess / prepare / mcp-admin and any user-defined ones); uniform across all work modes (plan included).
  • Fail-safe: any failure to resolve returns empty → no inheritance, subtask dispatch is not affected.

15.1.2 Subtask model / provider matching fixed

Previously, under multi-provider configurations, subtasks inherited the parent session's model but the provider was always taken as the first in the list — a mismatch would surface as "model not found" and the subtask wouldn't start. This release picks the provider that follows the inherited model — preferring the one that actually contains that model — so subtasks start reliably.

15.1.3 Parallel fan-out: split into many legs at once

Beyond dispatching one subtask at a time, the AI can use the AgentParallel tool to split a task into 2–16 "legs" running simultaneously — each an independent fresh-context subagent, mutually invisible — and aggregate the results automatically when all finish. Ideal for genuinely concurrent work (e.g. reviewing several modules or searching multiple sources at once); much faster than queueing them serially.

  • Each leg carries its own prompt and can set its own subagent type and model; the prompt must be self-contained (a leg can't see the main chat history).
  • Concurrency caps at 16 legs (well below the background-task cap of 64, so one fan-out can't monopolize all slots); fewer than 2 should use a single Agent.
  • The overall wait times out at 600 s by default, 1800 s max; legs not done by then return pending and keep running in the background — follow up with TaskWait / TaskOutput.
  • A failed / blocked leg doesn't drag the batch down; its reason comes back with the results for the AI to handle. Over-long leg output is truncated (with a note to fetch the full text via TaskOutput).
  • Live status while running: the parallel call card appears during execution (no waiting for the terminal state) and reflects each subtask's running status in real time; it stops spinning once stopped, and the result block isn't wrongly collapsed in compact density.

15.2 Foreground vs background

  • Foreground: the main assistant waits for the subtask (default 10-min timeout); good for must-wait tasks.
  • Background: the subtask runs asynchronously and the main assistant gets a task id immediately; later it can pull status or stop the task at will. Background tasks have a cap (preventing fan-out) and auto-cleanup (terminal states age out).

15.2.1 Idle-based watchdog + absolute ceiling

Subtask timeout is now idle-based: as long as it keeps producing output, it isn't considered timed out — the watchdog only catches the truly stuck, no-output-for-a-while case. On top of that there's an absolute ceiling (default 60 min) so an extreme infinite loop / hang can't keep the task running forever.

  • Idle window: 15 minutes with no output = timeout (error reads "subagent idle timeout").
  • Absolute ceiling: 60 minutes hard stop (error reads "absolute ceiling").
  • Long-running healthy tasks are unaffected: any stream output (including thinking blocks, incremental characters) resets the idle counter.
  • RunAgentToolBackground background path doesn't enforce an execution timeout (if you explicitly background it, you accept it may run a while).

15.3 Concurrency limit

To prevent runaway fan-out, AVL Code caps simultaneously-running subtasks. Above the cap the assistant is told "wait a bit before launching" — no avalanches.

15.3.1 Block-and-wait — the TaskWait tool

The main assistant can call once and block until background tasks / subagents complete, instead of polling round after round.

  • Wait for all or any to finish.
  • Configurable timeout (default 5 min, max 30 min); on timeout it returns "still running" so the model can decide whether to keep waiting.
  • During the wait only status and the final result are returned — intermediate progress doesn't get fed back into the main conversation, saving context and tokens.

What you see while it waits:

  • The waiting TaskWait call row shows an overall countdown against this call's timeout budget, so "how much longer" is visible. No countdown is started when nothing is actually running in the background, and reaching zero doesn't leave it sitting at 0ms — it falls back to a grey static budget value.
  • Once the wait is over the row's leading icon becomes a clock with a check mark, so "still waiting" and "finished waiting" are easy to tell apart.
  • The call row supports a compact mode, paired with a subtask status mosaic (one tile per subtask, coloured by state) so you can see how the batch is doing at a glance.
  • Elapsed time no longer ticks in a way that shifts the whole row sideways (the duration format was tightened, with zero-padded seconds in the minutes range).

Two things in the result:

  • other_tasks: the result also carries the background tasks in the same session that this wait didn't cover, so you don't finish one batch and forget another. The workspace-wide picture is available as well.
  • The "final reply to the main agent" is visible in the terminal state: once a waited-for task finishes, the closing message it gave the main agent is readable right in the UI — no digging through JSONL.
  • A task deliberately terminated with TaskStop no longer shows as "✓ completed successfully"; stopped tasks and tasks whose records were already reclaimed are both reported honestly, with no more false "not found, possibly cleaned up" or JSONL-based "done" claims.
  • Running tasks carry their elapsed time in TaskWait / TaskOutput results; terminating a task settles its duration on the spot instead of recording zero.

15.4 Termination

  • On desktop, the Stop button to the right of the input box cancels the main assistant and all subtasks together.
  • From WeChat, /stop does the same cascade.
  • Subtasks can also be stopped individually: each running subtask in the subagent drawer has its own stop button, so you don't have to stop the whole turn — background tasks, parallel fan-out legs and synchronous inline subagents all support it now. When stopping you can choose to keep the subtask records already produced.
  • Only your Stop cascades: sending a new message, an automatic conversation restart and similar internal actions no longer kill the subagents you already dispatched.

15.4.1 Real total consumption is visible (incl. subagents)

Usage stats now include the real consumption of subagents (parallel / subtasks): the GoalStrip and the end-of-run marker both show the real total token consumption + tool-call count, and break it down into "self + subagents" when subagents are present. This is persisted per session, so a long session shows the true running total throughout (it accumulates even without a goal set).

15.4.2 The subtask drawer: visible and controllable

Click "Open the live stream on the right" on a subtask call card and the right-hand drawer opens that subagent's complete run record — and it isn't a read-only log: you can take over the run from there.

What the drawer shows:

  • The task prompt (including the "task input" and "system instructions" from the parent session), the records one by one, and the record count.
  • Status: Running / Paused / Done / Ended / Failed / Stopped / Denied.
  • Once finished, a "Final reply to the main agent" block you can read directly.
  • A tab bar at the top to switch between subtasks running at the same time; overflow is reported as "N more running subtasks not shown".
  • "Copy JSONL path" and "Copy TaskWait args" buttons, for carrying on with the tools yourself (TaskOutput / TaskStop take the same id).

"Ended" is not the same as "Done": in imported sessions, or records re-seeded after an app reload, a subagent that left no explicit terminal state now shows as "Ended" rather than a blanket "Done" — it used to claim "completed", making you think the work had finished. Tasks that really were stopped or that failed are re-seeded from the on-disk record as "Stopped" / "Failed".

Run control (at the bottom of the drawer):

Action Notes
Stop Terminate this subagent; you may choose to keep the records already produced
Pause / Continue Resume whenever you like, optionally adding a line under "extra instruction" before it carries on
Retry Truncate to the last user message and re-run
Run again Re-dispatch with the same type + prompt (disabled with an explanation when the prompt is missing)

Take over per message: every message in the drawer has a message handle on its left (the same convention as the main session). Its menu offers:

  • Edit message / Delete message (editing is refused while running, so nothing gets overwritten mid-edit)
  • Delete a specific tool call (the paired tool result goes with it; imported legacy records without an id are explained rather than silently skipped)
  • Retry from here — everything from this message on is regenerated (a user message keeps the question and is re-answered; assistant / tool messages redo that step)
  • Continue from here — keep everything up to this message, discard the rest, and resume from that point; whatever you typed into "extra instruction" below is carried in
  • Copy / Copy selection
  • The menu header shows that message's timestamp (absolute and relative, two lines)

Every destructive action carries an inline double-confirm ("Confirm delete?", "Confirm retry?", "Confirm continue?").

The editor sits to the side, not in the stream: "Edit message" opens a side-mounted editor outside the list (with a leader line back to the message being edited), and saving goes through incremental updates — editing one message never makes the whole stream jump. If the edited message scrolls out of view the editor says so and offers "Back to the message"; closing with unsaved changes asks "Discard changes / Keep editing".

Navigation and right-click: next to the message handle are "Jump to top / bottom" shortcuts; "Scroll to bottom" keeps watching until it really is at the bottom (and stays pinned while the run continues, instead of stopping halfway). The message area also supports a right-click menu with the same actions as the handle menu.

15.4.3 The subtask strip: everything in flight at a glance

The session status strip carries a "Subtasks" item (see §5.5.1 for its shapes) summarising the subtasks currently running or paused:

  • The strip itself shows "N running / N paused".
  • Opening it lists them; clicking one opens it in the right-hand drawer (pause / continue / retry all live in the drawer).
  • When there are too many it says "+N more not shown" and points you at the "Subagent Runs" panel on the left for the full list.
  • The left-hand "subsessions of this session" list and the "all subsession history" view are grouped and collapsed by last-active time, expanding only the most recent batch by default, so a long history doesn't flood the panel.

Subagents aren't killed in passing: sending a new message, an automatic conversation restart and similar internal actions no longer cascade a termination to the subagents you dispatched — only an explicit Stop from you terminates them and their processes.

15.5 Output aggregation

Background tasks' intermediate logs appear as their own cards in the main stream you can expand; the final result comes back as a tight summary, not flooding the main thread.

15.5.1 AI suggested tasks: one-click to split off

While the AI is working, if it notices out-of-scope but worth doing ideas (dead code, stale docs, confirmed TODOs, security concerns, etc.), it surfaces a "Suggested Task" card in the message stream rather than derailing the current task.

The execute button on the card is a split button:

  • Primary: default "Run in a new session".
  • Dropdown ▾: four more options:
    • Run in a new session (primary default)
    • Run in the current session
    • Copy to input box
    • Copy to clipboard

You decide how to handle it. Executed suggestions are tagged; the AI can also withdraw stale ones. Multiple suggestions aggregate into a strip above the input box for unified management.

15.6 Routines: let the AI run on a schedule

A "Routine" lets you give a workspace a fixed instruction the AI runs on schedule or on demand — e.g. "every morning, pull and summarise yesterday's issues."

Entry:

  • The workspace sidebar has "New Routine" (also reachable from the current-workspace header dropdown).
  • Form fields: name, instruction, execution mode, model (same picker as a normal conversation: mode color cards and model search-by-group), schedule.
  • Auto-saved draft as you write: jumping to Settings or the agent manager mid-form and coming back automatically reopens and restores your in-progress entries.

Schedule types:

Type Behaviour
Hourly + minute Triggers once every hour at the chosen minute; reuses the HH:MM field but only the minute matters — hour is locked to 00, the trigger label shows :MM
Daily / Weekdays / Weekly + time of day Auto-triggered at the time
No schedule (manual) Doesn't auto-run; on save it runs once and the button reads "Run once"; trigger manually thereafter

Time picker is ergonomic:

  • Hour / minute two-column dropdowns (selected option centred; choosing minutes auto-collapses); hourly mode shows only the minute column.
  • Weekday selection uses circular single-character chips, with "Weekdays / Weekend" one-click presets.
  • Schedule precision: 30-second tick; hourly handles day boundaries / DST correctly via time arithmetic.

Unified management:

In Settings → Routines you can see everything by source workspace:

  • Toggle on/off (same Switch component as elsewhere).
  • Run now.
  • See recent run traces.
  • Workspaces not currently open have a badge and a one-click "Open".
  • The sidebar icon jumps straight to this workspace's routines list.
  • Search by name / workspace.

Run behaviour:

  • Each run creates a regular session in the corresponding workspace; you can review the process and the result.
  • A routine that's still running skips the next trigger — no stacking.

15.6.1 Session routines: let the AI remind itself on a timer in the current session

Besides the workspace routines above, which "run independently in a new session each time", the Agent can also set timed prompts inside the current session — e.g. "check whether the build has finished every 10 minutes" or "look at the download result in 30 minutes". When due, the prompt is delivered to the session that set it: if the session is running it is inserted straight into the current loop; if idle, it starts a turn or is skipped, as configured. The two kinds of routine keep separate data, and Settings → Routines only shows workspace routines.

How to create one: just tell the AI what cadence you want and it sets it up by calling RoutineCreate (there is no "New" button in the UI); once the goal is reached or it's no longer needed, it cancels it itself by calling RoutineCancel rather than waiting for all runs to be used up. Available in the main session only (auto / execute modes can create; plan / prepare / assess modes can only list and cancel); not available to subagents or the terminal UI (TUI).

Schedule types (all in this machine's local time):

Type Meaning Constraints
In N minutes Once 1–1440 minutes
Every N minutes Repeating 1–720 minutes; default validity 8–24 hours, default 30 runs
At a time Once at the given time Must be in the future and at most 30 days ahead
Hourly At a given minute every hour Default validity 72 hours
Daily / Weekdays / Weekly Fires at the set time Weekly needs at least one day; default validity 7 days (max 30 days)

How it's delivered when due: while the session is running you can choose "Prioritize" (inserted when the current step finishes — the default) or "Handle now" (inserted after cutting off the reply being generated), with the same meaning as for live guidance (see §5.4.2); when the session is idle, the "When the session is idle" setting decides between "Start a turn" and "Skip".

The "Routines" strip above the input box (forms described in §5.5.1) lists this session's routines; each entry shows its name, schedule, "Next HH:MM · Delivered n/m" and what happens when idle:

  • "Pause / Resume", "Edit" (change name, prompt, schedule, idle and running behaviour, max deliveries and expiry in place) and "Delete".
  • The "Ended" section below lists why each one ended (completed, reached max deliveries, expired, cancelled by the model, you pressed Stop, missed its time, etc.); click "Got it" to dismiss. A notice also pops up when one ends.
  • Each delivery shows in the message stream as a compact row "⏰ Routine · name · 3/30" that expands to the full prompt; it is not a new message from you and doesn't grant new authorisation — the AI checks with you first before going beyond what you originally asked.

Limits and safety valves:

  • Pressing the Stop button also cancels every routine in this session (/stop in companion messaging does the same); deleting a session clears its routines with it, and archiving a session ends them.
  • At most 5 unfinished routines per session and 50 per workspace; at most 10 created per session within 24 hours; at most 300 deliveries per workspace per day.
  • Turns started by a routine pause it automatically after 3 consecutive failures (click "Resume" to reset the count); if the session is running but the prompt can't be inserted, it retries for a while and otherwise skips that run.
  • Survives restarts (stored in the local config directory at workspaces/<workspace ID>/session-routines.yaml); scheduling only happens while the workspace is open, and missed times are not made up.
  • For a turn started by a routine, the reply is forwarded to the companion-messaging peer bound to that session (with the header "⏰ 例行「名称」" — "⏰ Routine 'name'").

15.7 Long-running tasks recover after a crash

A long task you put in the background (a subtask dispatched with run_in_background) has its progress persisted to disk in real time; if the app exits unexpectedly, is killed, crashes, or is interrupted by an upgrade, reopening it lets you pick the unfinished task back up rather than starting over.

How it works:

  • Each non-terminal background task is written as a snapshot on disk (a global, cross-workspace directory); when a task reaches a terminal state (done / error / killed) the snapshot is deleted. So a snapshot still on disk after a restart is a task that was interrupted and never finished.
  • On startup these leftover tasks are scanned asynchronously: first a "dirty-entry cleanup" removes entries whose parent session no longer exists (which can never be recovered), then the remaining tasks are listed. The scan never blocks startup, and with nothing leftover it's silent.
  • Recovery doesn't directly respawn the sub-process; instead it injects a "continue / re-delegate this unfinished background task" message into the original parent session, letting the main agent finish it agent-first — and this step is visible, so you see "recovering task X".

What you see (the "Interrupted" status strip):

  • When interrupted tasks exist, an "Interrupted · N to resume" item appears in the session status strips (above the input box on narrow windows, at the top-right of the message area on wide ones — see §5.5.1); otherwise it isn't shown. It takes no page space, so a long list never pushes the message area down.
  • Open it and a popover lists the tasks under a title like "N background task(s) were interrupted last run". Each row has two buttons: Resume (— injects the continue message and removes the entry from disk) and Ignore (deletes it from disk so it won't prompt again). "Ignore all" sits next to the title. Once every entry is handled the item disappears on its own.
  • On success a toast confirms "Resumed: the agent will continue this task in its session"; on failure it shows the specific reason (e.g. the parent session was deleted, and the dirty entry is cleared).

Auto-resume (optional, off by default):

  • Turn on "Auto-resume interrupted tasks" under Settings → Agent → Reflection and a restart will automatically continue background tasks that were unfinished when the app last crashed or was killed — ideal for unattended / overnight long runs.
  • To prevent a "continue-message storm", startup auto-resume handles at most 5 tasks; the rest stay in the "Interrupted" status strip for manual resume.
  • When the toggle is off (default), all interrupted tasks are simply listed in the "Interrupted" status strip for you to resume or ignore.