Skip to content

Fire subagent queries by default and bound the sync wait - #206

Merged
TroyHernandez merged 6 commits into
mainfrom
subagent-async-default
Sep 14, 2026
Merged

TroyHernandez merged 6 commits into
mainfrom
subagent-async-default

Conversation

@TroyHernandez

Copy link
Copy Markdown
Contributor

Summary

  • query_subagent defaults to wait = FALSE and gains timeout: the prompt starts at once and collect_subagent fetches the reply, so several subagents can run in parallel while the parent continues. The tool description tells the model so.
  • subagent_query(wait = TRUE) no longer calls session$run(), which has no timeout. Both paths fire one session$call(); the sync path collects with a real deadline (timeout, default 60 s, Inf allowed). A child that has not replied stays pending for subagent_collect() or subagent_kill() and the call returns NULL instead of holding the parent's turn.
  • subagent_collect() maps Inf to processx's -1 rather than NA. /ask in the REPL reports a bounded wait running out and points at /collect. The hall monitor reads a NULL reply as no verdict and escalates, which its contract already stated.
  • Version bump to 0.7.1.48 in its own commit.

Test plan

  • inst/tinytest/test_subagent_async.R drives the sync path with a fake r_session: slow child leaves the slot pending with the poll receiving 10 ms, fast child returns directly, Inf becomes -1, tool default is FALSE and its queued / still-working messages hold. 37 asserts.
  • Full installed suite at home: 4725 tests, 1 failure in test_mcp_handler.R:124, the known order-dependent check that passes alone and fails the same way on main. test_subagent_callr.R (real callr child, real model) passed all 10.

The query_subagent tool now defaults to wait = FALSE: the prompt starts
at once and collect_subagent fetches the reply, so several subagents
can work in parallel while the parent continues. The tool gains a
timeout argument for the cases that do wait.

subagent_query(wait = TRUE) used session$run(), which has no timeout,
so a child that never replied held the parent's turn indefinitely.
Both paths now fire the same one-shot session$call() and the sync path
collects through subagent_collect() with a real deadline. A child that
has not replied by then stays pending for a later collect or kill and
the call returns NULL. subagent_collect() maps Inf to processx's
wait-forever sentinel instead of NA. /ask reports the bounded wait
running out and points at /collect; the hall monitor reads NULL as no
verdict and escalates, as its contract already said.

Tests drive the sync path with a fake r_session: a slow child leaves
the slot pending and the poll receives the deadline in milliseconds, a
fast child returns directly, Inf becomes -1, and the tool's default and
messages hold.
tool_query_subagent() takes timeout after return_name, so a positional
caller passing return_name fourth still reaches the child with it
instead of waiting on a string.

.subagent_poll_ms() validates timeout before a query is fired: one
non-negative number of seconds or Inf, or an error. NA, NaN, strings,
and negatives used to fall through to processx's -1 sentinel and wait
forever. Inf, and finite values too large for integer milliseconds,
map to that sentinel on purpose.

/ask keeps its three outcomes apart inside the handler: a reply, a
bounded wait running out (NULL from a successful call), or an error,
which prints once and never leads to a second lookup outside the
handler. .subagent_still_pending() is gone with it.

Docs say the wait is bounded by default rather than never indefinite,
since Inf opts out.
A finite timeout longer than about 24.8 days used to map to processx's
-1 sentinel and wait forever, which contradicts the bound the caller
asked for. It is now an error that points at Inf, the one value that
opts out of the bound.
# Conflicts:
#	DESCRIPTION
#	NEWS.md
@TroyHernandez
TroyHernandez merged commit 6da0d41 into main Sep 14, 2026
2 checks passed
@TroyHernandez
TroyHernandez deleted the subagent-async-default branch September 14, 2026 02:45
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