Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion apps/cli/src/bootstrap/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,20 @@ export function createStepExtensionFactories(deps: StepExtensionFactoryDeps): In
usageHost: deps.usageHost,
traceHeaderPolicy: deps.traceHeaderPolicy,
}),
createStepCapabilitiesExtensionInline({ telemetry: deps.telemetry }),
createStepCapabilitiesExtensionInline({
telemetry: deps.telemetry,
subagent: {
backgroundSettings: () => {
const settings = deps.stepSettings?.()?.getStepSettings();
return settings
? {
enabled: settings.subagentBackground,
timeoutSeconds: settings.subagentBackgroundTimeoutSeconds,
}
: undefined;
},
},
}),
createStepCronExtension({ telemetry: deps.telemetry }),
createStepGoalExtension({ telemetry: deps.telemetry }),
{ name: "connect-lark", factory: createLarkConnectionExtension(), hidden: true },
Expand Down
70 changes: 70 additions & 0 deletions docs/orchestration-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,76 @@ the turn as failed and ends the child even for a keep-alive lane. The default is
30 minutes, deliberately generous because a long `run_command` is silent while it
runs; `STEP_SUBAGENT_TURN_IDLE_TIMEOUT_MS` overrides it and `0` disables it.

## Background subagent lanes

`subagent` blocks by default: the call returns once every child has finished.
With `run_in_background: true` it returns a `lane_id` (`lane_` and eight hex
digits) at once instead, and the lane runs one single, parallel, or chain call.
Every check that can reject the call (agent discovery, mode selection, the batch
limit, unknown agent names, and project-agent confirmation) runs before the lane
exists. A rejected call returns an ordinary tool error and sends no
notification.

A lane sends one terminal `<agent-notification lane="…" tasks="N">` message when
it finishes, fails, or is stopped. It wakes an idle parent. The body opens with a
summary such as `Lane lane_1a2b3c4d finished: 2 completed, 1 failed.`. That
summary is followed by duration, usage, child session ids and JSONL paths, any
dialogs the child raised that were auto-cancelled, failure reasons, and the
output. The text passes through the shared [tool-output retention
boundary](../packages/coding-agent/docs/tool-output-retention.md): oversized
output is saved under the session's `tool-output` directory and replaced by a
preview with its path. `subscribe: "progress"` adds throttled progress messages
that do not wake the parent. A child's needs-input request does wake it.

`agent_send` takes `{lane_id, action, prompt?, interrupt?}`:

- `reply` continues a single-agent lane's transcript. While the turn runs it is a
steer (`interrupt: true`) or a queued follow-up; after the lane has ended it
starts another turn of the same agent on the same child session. Parallel and
chain lanes refuse replies; start a new call instead.
- `stop` aborts the run and ends the lane's children. The terminal notification
follows once the run has wound down.
- `status` reads the lane's in-memory state: status, elapsed time, usage, child
sessions, and the last 32,000 characters of its last output. Without `lane_id`
it lists the session's lanes. It never waits.

A session keeps its running lanes and the 16 most recently finished ones. Once an
older finished lane's notification has been sent, the lane is forgotten and its
idle children are stopped; `agent_send` then reports the id as unknown, and the
child transcripts named in the notification stay on disk.

Each background turn has a wall-clock limit, unlike the idle watchdog above, and
output does not reset it. The limit comes from `timeout_seconds` (60–7,200), then
`[subagent] background_timeout_seconds`, then 3,600 seconds. Reaching it fails
the turn with a "timed out" error and ends the child; there is no separate
timed-out status. Lane children stay alive between turns so replies reuse them.
After 10 idle minutes they are stopped, and a later reply respawns the child with
the same session id, which resumes its transcript from disk. A session holds at
most eight running lanes. A ninth call is refused rather than queued; the check
and the reservation happen in one synchronous step, so a concurrent batch of
calls cannot overshoot.

Lanes belong to the session runtime that started them. `session_shutdown`
(quit, signals, `/new`, `/resume`, `/fork`, and reload) aborts every lane of that
runtime, stops its children, and waits up to three seconds. Children still
running after that are killed, and their temporary directories are removed.
The lane list and widget are cleared, and results that arrive later are dropped.
Lanes are not persisted or resumed. Lane children are also registered for the
host's process-exit sweep. Shutting down one runtime never touches another
runtime's lanes. A host that keeps several sessions open, such as a web client,
therefore keeps one runtime per session, and switching which session is viewed
does not stop any lane. Interactive quit prints one `Stopping N background
agents...` line on stderr without asking for confirmation.

Background execution follows the extension mode (`ctx.mode`). It is available in
the TUI, RPC, and SDK stdio hosts, and in SDK embeddings that bind extensions
with a `tui` or `rpc` mode. Print and JSON runs, and the default `print` mode of
an unbound session, end with their prompt, so a background request there runs in
the foreground. Its result starts with `Background execution is not available in
this run mode; ran in the foreground.`. `STEP_DISABLE_SUBAGENT_BACKGROUND=1` or
`[subagent] background = false` does the same everywhere, with a matching
notice.

## Cron scheduling and delivery

`cron_create` accepts numeric five-field local-time cron expressions. Fields
Expand Down
21 changes: 21 additions & 0 deletions docs/step-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,27 @@ Step screens pass the Step agent directory; generic TUI callers default to the
system temporary directory. Rendering equivalence tests select the uncached
renderer through a test-process argument.

### Background subagents

The `[subagent]` table controls background subagent lanes:

```toml
[subagent]
background = true # false runs run_in_background calls in the foreground
background_timeout_seconds = 3600 # default per-turn wall-clock limit of a lane
```

A trusted project's `config.toml` overrides the global values.
`background_timeout_seconds` must be an integer from 60 to 7,200; any other
value is reported as a settings warning and ignored, so the 3,600-second default
applies. A call's own `timeout_seconds` takes precedence. When
`STEPCODE_CONFIG_PATH` selects a StepCode JSON config, the same settings live
under `subagent.background` and `subagent.backgroundTimeoutSeconds`.
`STEP_DISABLE_SUBAGENT_BACKGROUND=1` disables background lanes regardless of the
file. Print and JSON runs never start lanes. See the [orchestration
lifecycle](orchestration-lifecycle.md#background-subagent-lanes) for lane
behavior.

### Plan file storage

Native Markdown plans default to
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Features connect through existing extension and session-event interfaces. Reuse
- Tool permissions run through Step's `tool_call` policy; UI presents approvals. Handle dangerous matches and incomplete analysis separately: Ask / Approve for Me cannot bypass either gate through automatic tool overrides or unattended allow. Only risk-acknowledged Full Access uses the `skipCommandPolicy` exception; explicit tool deny still applies. Follow `src/step/permissions.ts` and [command permissions](../../docs/command-permissions.md).
- Use the existing mutation queue for changes to the same file. Final text results pass through the shared output-retention boundary, including MCP, extensions, hook replacements, and errors. Save full oversized output before publishing a preview; storage failure must not automatically retry calls with side effects.
- Preserve extension ordering: pre-execution `tool_call`, then return-stage `tool_result`. Publish catalogs in batches through `registerTools` to avoid repeated tool and prompt refreshes; tools added after a request starts become available to later requests.
- Keep persistent RPC sessions for subagents. Build child environments through `buildSubagentChildEnv`, retaining the child marker and workflow / cron / goal disabling flags. Timeouts, failures, and cancellation must settle lanes and release resources.
- Keep persistent RPC sessions for subagents. Build child environments through `buildSubagentChildEnv`, retaining the child marker and workflow / cron / goal disabling flags. Timeouts, failures, and cancellation must settle lanes and release resources. Background lanes belong to the session runtime that started them: run every rejecting check before a lane exists, send one terminal notification per lane, and stop a runtime's lanes on `session_shutdown` without touching another runtime's lanes (see [orchestration lifecycle](../../docs/orchestration-lifecycle.md#background-subagent-lanes)).
- Run user workflow code in the existing QuickJS VM through the restricted host bridge. Path ACLs and fan-out depth are separate contracts.
- Plan mode and structured task tracking are independent. Validate plan write paths; do not infer task state from reply numbering or completion markers.
- Persist goal controls before applying them. Clearing a goal revokes continuation and cancels its corresponding run. Deliver cron jobs when the host is idle with no pending user messages; preserve durable-store locking, atomic replacement, and rollback on failure.
Expand Down
Loading
Loading