Skip to content

feat(subagent): let the model choose foreground or background lanes - #282

Merged
liuedcson merged 1 commit into
mainfrom
feat/subagent-background-lanes
Oct 21, 2026
Merged

liuedcson merged 1 commit into
mainfrom
feat/subagent-background-lanes

Conversation

@liuedcson

Copy link
Copy Markdown
Collaborator

Problem

The model could not choose between running a subagent in the foreground and in the background. Background lanes existed only as internal plumbing, and their control surface had several defects:

  • agent_send reply rebuilt the call from the lane's original params, so replying to a parallel or chain lane re-ran the whole batch.
  • Addressing by alias, group, or to.all made a single command fan out to unrelated lanes.
  • A malformed or unapproved call could create a lane before validation failed.
  • Session replacement (/new, /resume, /fork, reload) and quit did not stop a session's lanes or their rpc children, so late results could reach a replaced session.
  • A background turn had no wall-clock limit, idle keep-alive children never expired, and finished lanes stayed in memory for the life of the session.

Changes

  • subagent gains run_in_background (default false) and timeout_seconds (60–7,200, background only). The model-facing schemas for subagent, its task and chain items, and agent_send set additionalProperties: false.
  • One background call creates one lane, lane_ followed by eight hex digits. When the lane finishes, fails, or is stopped, it sends one <agent-notification lane="…" tasks="N">. The notification opens with a summary such as Lane lane_1a2b3c4d finished: 2 completed, 1 failed., followed by duration, usage, child session ids and JSONL paths, auto-cancelled dialogs, failure reasons, and output. Oversized output goes through the existing tool-output retention boundary.
  • agent_send is now {lane_id?, action: "reply" | "stop" | "status", prompt?, interrupt?}. Reply works only for single-agent lanes and runs only the follow-up task. Status is non-blocking: it reads in-memory state and returns the tail of the last output, or lists lanes when lane_id is omitted.
  • All rejecting checks run before a lane exists: discovery, mode selection, batch size, unknown agents, and project-agent confirmation.
  • Lifecycle:
    • Each background turn has a wall-clock limit enforced in the rpc adapter; output does not reset it.
    • Idle keep-alive children stop after 10 minutes. A later reply respawns the child on the same session id.
    • A session holds at most 8 running lanes. The check and the reservation happen in one synchronous step, so a concurrent batch cannot overshoot.
    • Only the 16 most recent finished lanes are kept, and a lane is not forgotten before its notification is sent.
    • session_shutdown stops only the current runtime's lanes. It waits up to 3 seconds, then kills the remaining children and removes their temp directories. It clears the widget and drops late callbacks through an epoch check.
    • rpc children are registered with the host's process-exit sweep.
  • Hosts: TUI, RPC, and SDK hosts bound with tui or rpc allow background lanes. Print and JSON modes, STEP_DISABLE_SUBAGENT_BACKGROUND=1, and [subagent] background = false run the call in the foreground. The result then starts with a notice explaining why.
  • Configuration: [subagent] background_timeout_seconds sets the default limit. Out-of-range values are reported as settings warnings and ignored. The StepCode JSON config (STEPCODE_CONFIG_PATH) reads and writes the same settings under subagent.
  • Telemetry: execution is now "background" for lane runs and "blocking" otherwise. Previously a foreground parallel call reported "background".

Behavior and compatibility

  • Model-visible text changed in the subagent and agent_send tool descriptions and in the system prompt's Delegation section. The new text says to use the foreground when the next step needs the result, and not to sleep or poll after starting a lane. It also documents the three agent_send actions and removes alias, group, and all addressing. Every Step session with the subagent tool receives this text.
  • Removed inputs: alias, group, subscribe: "none", and agent_send's to object. Extra fields in model calls now fail validation instead of being ignored.
  • Notification wrapper attributes changed from agentId, alias, event, and status to lane and tasks. The renderer still reads details.agentId, so older transcripts render as before.
  • New configuration keys: [subagent] background and [subagent] background_timeout_seconds. New environment variable: STEP_DISABLE_SUBAGENT_BACKGROUND.
  • Lanes are not persisted or resumed across processes. Workflows are unchanged.

Validation

  • pnpm run check: passed.
  • From packages/coding-agent: pnpm exec vitest --run test/step-subagent-events.test.ts test/subagent-*.test.ts test/step-capabilities-extension.test.ts test/step-system-prompt.test.ts test/stepcode-config.test.ts test/step-settings-manager.test.ts test/step-tool-profile.test.ts test/workflow-*.test.ts test/session-index: 22 files and 390 tests passed.
  • New regressions cover:
    • schema strictness
    • reply refusal for parallel and chain lanes, and single-lane follow-up
    • preflight failures and declined confirmation creating no lane
    • print/JSON fallback, the environment switch, and the settings switch
    • timeout precedence
    • the 8-lane cap under concurrent calls
    • finished-lane retention
    • status output
    • oversized output written to disk
    • shutdown isolating one session runtime from another
    • a single notification on stop
    • idle-TTL respawn without a restart notice
    • the wall-clock limit against a streaming fake rpc child
    • settings and StepCode config validation
  • Manual (macOS arm64, locally built binary): parallel background lane with 5 tasks; the parent stayed responsive while the lane ran, and one notification arrived with child session paths. Foreground parallel delegation was also checked.

Limitations

  • ./test.sh was not used as evidence. On the author's machine it fails in unrelated git-dependent suites because the isolated HOME breaks a local git wrapper; those suites also fail on main.
  • Interactive quit with running lanes and the stderr notice were not exercised manually.

The subagent tool now exposes run_in_background and timeout_seconds. A
background call returns a lane id at once; the lane sends one terminal
<agent-notification> with a summary, usage, child session paths, and the
output (oversized output goes through the tool-output retention boundary).

agent_send takes {lane_id, action: reply|stop|status}. Reply only applies to
single-agent lanes and no longer re-runs a parallel or chain batch. Status
reads in-memory state without waiting. Aliases, groups, and fan-out
addressing are removed, and the model-facing schemas reject unknown fields.

Every rejecting check (discovery, mode, batch size, unknown agents, project
agent confirmation) runs before a lane exists. Each background turn has a
wall-clock limit, idle keep-alive children stop after 10 minutes, a session
holds at most 8 running lanes and keeps the 16 most recent finished ones.
session_shutdown stops the runtime's own lanes, waits up to 3 seconds, and
forces the rest down; rpc children join the process-exit sweep.

Print and JSON modes, STEP_DISABLE_SUBAGENT_BACKGROUND=1, and
[subagent] background = false run background requests in the foreground with
a notice. [subagent] background_timeout_seconds sets the default limit and is
validated in both config.toml and the StepCode JSON config.
@liuedcson
liuedcson merged commit 118348c into main Oct 21, 2026
2 checks passed
@liuedcson
liuedcson deleted the feat/subagent-background-lanes branch October 21, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant