Skip to content

feat: progress thresholds on watch.agent; agent.completed carries name and answer - #188

Open
hallerite wants to merge 6 commits into
mainfrom
feat/agent-budgets-completion
Open

hallerite wants to merge 6 commits into
mainfrom
feat/agent-budgets-completion

Conversation

@hallerite

@hallerite hallerite commented Sep 15, 2026

Copy link
Copy Markdown
Member

Stacked on #184 (base feat/runtime-instructions). Multi-agent follow-ups from the search-QA probe, kept separate from the SWE consolidation in #184.

What

  • rlm.watch.agent(child, every_turns=N, every_tokens=M): a watch.progress inbox event each time the child's own model calls or new tokens cross the next multiple, with turns, tokens, name, status and the history slice start:end since the previous event. The parent reads (await child.history()).messages[start:end] and steers (await child.steer("report what you have and stop")) if the child should wrap up. No hard per-child budget: the child stays autonomous, the parent stays informed. watch.agent(child) without thresholds is unchanged.
  • agent.completed content now carries name, turns, error and answer (the last 4 KiB of the child's answer) next to agent_id and status, mirroring the output tail on shell.completed. await child.result() still holds the full answer.
  • AgentHandle carries name (set by spawn()/get()): the sibling name is reserved for the session, so it is as immutable as id; the model already assumed it (37 AttributeError: 'AgentHandle' object has no attribute 'name' across 36 delegating episodes).
  • await child.result(yield_after=300) mirrors ShellJob.result(): it waits up to yield_after seconds for the child to finish and always returns an AgentResult (status, answer, usage, turns, session_dir, running). While the child is still working on its first answer, answer is None and running is True, instead of a bare None the model read as failure (6 crashes in the delegate2 run). A persistent child's latest answer stays available while it runs again; terminal failure/cancellation raises.
  • agent.message content is a dictionary like every other inbox event: agent_id, name, text. It was the one event whose content was a bare string, and parents iterating their inbox called .get() on it (4 episodes in the redsearcher delegate2 run).
  • Leaf guide: a child's final answer is its deliverable (it reaches the parent as agent.completed and through result()); send_to_parent is for interim findings, blockers or questions. The old wording ("put a report in your parent's inbox") made children send the report and then repeat it as their answer, so the parent read it twice.
  • AgentInfo.turns: the child's own model-call count on list()/get()/info().
  • watch.progress fires from the agent-step publish point, after the step that crossed the threshold is logged, so the event's start:end slice includes it (it used to fire at model-call time, one step early).
  • Supervisor keeps per-agent turn / new-token counters (record_call now takes the calling agent's id); new subscription kind progress; broker accepts optional every_turns/every_tokens on watch.agent; guide documents both.

Why

Search-QA delegation probe with GLM-5.3 on openseeker/redsearcher (nano-rlm head 11729d5, search+fetch skills, depth 1, ≤2 concurrent children): a general delegation paragraph takes spawning from 4 % to 89 % of episodes and the children are used well — self-contained briefs, parallel work, results collected, children reporting up with send_to_parent in 80/87 episodes. But the parent has no cheap way to keep a long child in view: it polled rlm.agent.list() 467 times and read history() 146 times across 87 episodes and never used watch.agent; a "verify five priors" child consumed 14 of a 35-turn tree unnoticed; 22 % of delegating episodes ran into the rollout cap. Steering does happen when the parent notices ("Please wrap up now and send your final report immediately…", 7 uses), so the missing piece is the notice, not the control. The completion event carried only {agent_id, status}, so every completion cost an inbox.readagent.getresult() round trip. Per-episode review: rlm-v2-study/usage/redsearcher-spawn-37d37b40-review.md.

Tests

test_progress_thresholds_fire_on_turn_and_token_multiples (registry unit test), the real-kernel subscription test now also registers watch.agent(child, every_turns=1) and checks the watch.progress content, and the extended completion-event assertions in test_messaging.py. Full suite passes with the load-sensitive MCP cancel test deselected (as on #184). Imports verified under Python 3.10 (sandbox interpreter).

🤖 Generated with Claude Code


Note

Medium Risk
Changes public delegation and inbox event shapes (especially result() no longer returning None and structured agent.message content), which can break callers that assumed the old behavior.

Overview
Improves parent visibility and control over delegated agents without changing the basic spawn/send/steer model.

await child.result(yield_after=…) now mirrors shell jobs: it can wait up to 300s (default) and always returns an AgentResult with status, answer, usage, turns, session_dir, and running—no more None while the child is still on its first answer. AgentHandle.name and AgentInfo.turns expose sibling names and per-child model-call counts on handles and metadata.

Watching: rlm.watch.agent(child, every_turns=N, every_tokens=M) registers progress subscriptions that emit watch.progress when the child’s own turns or new tokens cross multiples, including a history start:end slice (fired after the step is logged). Plain watch.agent(child) is unchanged.

Inbox payloads: agent.completed now includes name, turns, error, and a 4 KiB answer tail; agent.message content is a dict (agent_id, name, text) like other events. Guides and README reflect these shapes and clarify that send_to_parent is for interim updates, not duplicating the final answer.

Supervisor/broker wiring adds yield_after on agent.result, per-agent turn/token accounting in record_call, and subscription progress lifecycle on agent teardown.

Reviewed by Cursor Bugbot for commit 4bc5694. Bugbot is set up for automated code reviews on this repo. Configure here.

@hallerite
hallerite force-pushed the feat/agent-budgets-completion branch from 8aeca4d to 9df8af8 Compare September 15, 2026 11:43
@hallerite hallerite changed the title feat: per-child budgets on spawn; agent.completed carries name and answer feat: progress thresholds on watch.agent; agent.completed carries name and answer Sep 15, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread src/rlm/supervisor.py Outdated
Comment thread src/rlm/supervisor.py
@hallerite
hallerite force-pushed the feat/agent-budgets-completion branch 4 times, most recently from 860d824 to 9ffcfdf Compare September 15, 2026 17:40

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 80074b7. Configure here.

Comment thread src/rlm/supervisor.py
Base automatically changed from feat/runtime-instructions to main September 15, 2026 23:51
hallerite and others added 6 commits September 16, 2026 01:52
…e and answer

Evidence: the search-QA delegation probe (openseeker/redsearcher, GLM-5.3). A general
delegation paragraph takes spawning from 4 % to 89 % of episodes and the children are
used well, but the parent has no cheap way to keep a long-running child in view: a
"verify five priors" child consumed 14 of a 35-turn tree unnoticed, and 22 % of
delegating episodes ran into the rollout cap. Every completion also cost the parent an
inbox.read -> agent.get -> result() round trip because the event carried only
{agent_id, status}.

- `rlm.watch.agent(child, every_turns=N, every_tokens=M)`: a `watch.progress` inbox
  event each time the child's own model calls or new tokens cross the next multiple,
  with turns, tokens, name, status and the history slice start:end since the previous
  event. The parent reads `child.history().messages[start:end]` and steers
  (`child.steer("report what you have and stop")`) if the child should wrap up. No hard
  per-child budget: the child stays autonomous, the parent stays informed. Without
  thresholds watch.agent behaves as before.
- `agent.completed` content now carries name, turns, error and the last 4 KiB of the
  child's answer next to agent_id and status, mirroring shell.completed's output tail;
  `await child.result()` still holds the full answer.

Supervisor keeps per-agent turn/new-token counters (record_call now takes the calling
agent's id); a new subscription kind "progress"; the guide documents both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
spawn(name=...) reserves the name for the session, so it is as immutable as the id and
belongs on the handle; the model treats it that way already — 37 AttributeErrors on
`.name` across 36 delegating episodes (`[c.name for c in children]`). spawn() and get()
populate it; the guide lists the handle's fields.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
result() mirrors ShellJob.result(): it waits up to yield_after seconds (default 300) for the child to finish and always returns an AgentResult with status, answer, usage, turns and session_dir; answer is None and running is True while the child is still working on its first answer, instead of a bare None.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The watch.progress check moves from the model-call accounting to the agent-step publish point, so the event's start:end slice includes the step that crossed the threshold.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A child's report arrives as {"agent_id", "name", "text"} instead of a bare string, so parents can treat every event's content the same way.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…for interim findings

The leaf guide framed send_to_parent as the way to report, so children sent the report and then repeated it as their answer, and the parent read both.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hallerite
hallerite force-pushed the feat/agent-budgets-completion branch from 5cbf523 to 4bc5694 Compare September 16, 2026 00:39
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