Skip to content

Background subagents - #64

Open
dexhorthy wants to merge 8 commits into
mainfrom
background-subagents
Open

dexhorthy wants to merge 8 commits into
mainfrom
background-subagents

Conversation

@dexhorthy

Copy link
Copy Markdown

HumanLayer Task

Why the change

Subagents can now run in the background while the launching agent keeps working, and the launcher is notified when each one finishes, the same way Claude Code's background agents behave, so hosts like HumanLayer can show and control them through one surface.

Special things to note

  • The log format gains task_started / task_finished entries and an optional origin on user-message. Both are additive, and v1 logs still decode (there are tests for this). Exhaustive matches in fold-cli were updated for the new tags.
  • interrupt() with no target now stops only foreground work; background tasks keep running and still notify. stop() and closing the session stop them. A stop the model makes with stop_agent sends no notification, but stopTask from the host does.
  • The attached video shows the HumanLayer UI built on the matching Claude SDK feature (humanlayer/synclayer). Fold itself was checked with real Codex runs (codex/gpt-5.6-terra): two background agents, a wake per finish, then a clean exit; and list_agents → stop_agent → list_agents. Those runs caught two bugs (a list_agents schema OpenAI rejects, and an exit before the last notification) that are fixed here with regression tests.

Change outline

One background run, from launch to notification:

subagent { prompt, run_in_background: true }
  BackgroundTasks.launch
    write task_started
    fork child run into its own scope (from the session scope)
    return "Background agent started. agent_id: …"      # launcher's turn continues
  ... child works; task-progress deltas (last tool, counts)
  child run ends (completed | failed | stopped)
    write task_finished { status, summary, error, usage }
    release the child's scope, then queue the notification
      launcher busy → follow-up, delivered at its next natural completion
      root idle     → wake: new root run whose input is the notification
                      (same gate + running claim as send; queued notifications coalesce)

New durable rows and the user-message origin:

 LogEntry
+  task_started   { agentId, parentAgentId, toolCallId, taskType: 'subagent', agentType,
+                   description, prompt, isBackgrounded, runCount }
+  task_finished  { agentId, parentAgentId, toolCallId, status: completed|failed|stopped,
+                   summary, error, usage { turns, toolUses, totalTokens }, durationMs, lastToolName }
   user-message   { …,
+                   origin?: { _tag: 'task-notification', taskIds } }
 FoldEvent delta
+  task-progress  { lastToolName, counts }

Public surface:

 subagent tool params { description?, prompt, agent?, agent_id?, fork?, skill?,
+  run_in_background? }
+agentControlTools() → list_agents {}, stop_agent { agent_id }, send_message { agent_id, message }

 FoldSession
+  backgroundTasks: Effect<ReadonlyArray<BackgroundTask>>
+  backgroundTaskChanges: Stream<ReadonlyArray<BackgroundTask>>
+  stopTask(taskId): Effect<boolean, SubagentNotFoundError>
+  awaitBackgroundIdle: Effect<void>
~  interrupt(): foreground only; background tasks keep running

 foldcode
+  --background-timeout <seconds>   # --prompt waits for background agents (default 600)

BackgroundTask fields (taskId, toolCallId, parentAgentId, agentType, description, prompt, status, isBackgrounded, startedAt/endedAt/notifiedAt, usage, lastToolName, progressSummary, summary, error, runCount) map 1:1 onto HumanLayer's session_background_tasks table.

Where it lives:

packages/
├── fold-core/src/Subagents/
│   ├── BackgroundTasks.ts      # new: registry, run scopes, notification routing, wake
│   ├── AgentControlTools.ts    # new: list_agents / stop_agent / send_message
│   ├── SubagentEngine.ts       # dispatch/fork/resume in the background
│   └── SubagentTool.ts         # run_in_background param and receipt
├── fold-core/src/Api/OpenSession.ts       # session API, wake through the root gate
├── fold-core/src/EventLog/Schemas.ts      # new rows, user-message origin
├── fold-agent/src/Mode/{Mode,Rlm}.ts      # coding and RLM modes get the control tools
└── fold-cli/src/
    ├── Run.ts                  # --prompt settles background work before exit
    ├── Renderer.ts             # [background] / [notification] lines
    └── tui/                    # BG badge, "N RUN · M BG", x stops the selected agent, AGENTS row for wakes

Demo of the same feature in HumanLayer (Claude SDK side):

background-agents-demo.mp4

HumanLayer session

dexhorthy and others added 8 commits October 13, 2026 04:20
Additive v1 schema changes for background subagents:
- task_started / task_finished log entries (status, summary, error, usage,
  duration, last tool, run count)
- optional user-message origin { _tag: 'task-notification', taskIds } so
  runtime-written turns are distinguishable; old logs decode unchanged
- task-progress ephemeral delta part

SessionControls queues now carry an origin, adjacent queued task
notifications drain as one message, and runningAgentIds exposes the
running registry. AgentRuntime and Session.send pass origins through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a13295-e128-724d-bb90-666e8fbf2d01
- subagent tool: run_in_background for dispatch, fork, and resume; the
  call returns a receipt once the agent has started
- BackgroundTasks service: per-run task scopes forked from the session
  scope, durable task rows, live progress, and notification delivery
  (follow-up to a running launcher, otherwise the root: follow-up when
  busy, a synthetic task-notification wake when idle; coalesced)
- session API: backgroundTasks, backgroundTaskChanges, stopTask,
  awaitBackgroundIdle; interrupt() leaves background tasks running;
  stop() stops them with deferred notices; close records them stopped;
  reopening rebuilds the registry and closes orphaned runs
- list_agents, stop_agent, send_message model tools (agentControlTools)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a13295-e128-724d-bb90-666e8fbf2d01
Install list_agents, stop_agent, and send_message next to the subagent
tool, and tell the coding agent when to launch subagents in the
background.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a13295-e128-724d-bb90-666e8fbf2d01
runPrompt now waits after the prompt's own turn until background agents
and their task-notification turns are done, and reports the root's
latest outcome. Agents still running after --background-timeout seconds
(default 600) are stopped; a signal while waiting stops them too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a13295-e128-724d-bb90-666e8fbf2d01
- SUBAGENTS rail: BG badge, status from the task rows, X STOP hint; the
  rail header and activity label count running background agents
- x on a selected running background agent stops it (session.stopTask)
- a task-notification wake renders as an AGENTS notification row with
  the per-agent summaries instead of a USER row
- line renderer prints [background] start/finish lines and a
  [notification] line for wakes; JSON verbose mode encodes task-progress

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a13295-e128-724d-bb90-666e8fbf2d01
Fixture with a running and a finished background agent plus a
task-notification wake; checks the rail badge and counts, the AGENTS
notification row, and stopping the selected agent with x. Runs with
FOLD_TUI_TERMINAL_TEST=1 like the other terminal-control tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a13295-e128-724d-bb90-666e8fbf2d01
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a13295-e128-724d-bb90-666e8fbf2d01
…eporting idle

- list_agents used Schema.Struct({}), whose JSON Schema root is an anyOf
  (an empty struct also accepts arrays). OpenAI's strict tool schemas
  reject that, so every Codex/OpenAI request failed in prepareTools. Use
  Tool.EmptyParams like other parameterless tools.
- awaitBackgroundIdle could resolve between a run's task_finished row and
  the queueing of its notification, so a headless run exited before the
  last wake. Runs now count as unsettled until their notification is
  queued, in the same state update.
- Regression tests: every tool the coding and RLM modes advertise (root,
  subagent, and fork-agent tools, with and without RPI) converts through
  the OpenAI provider's toCodecOpenAI to an object root without
  anyOf/oneOf; awaitBackgroundIdle watched from before a run ends waits
  for its notification turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a13295-e128-724d-bb90-666e8fbf2d01
@humanlayer-channels-app humanlayer-channels-app Bot added the enhancement New feature or request label Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant