Persistent sub-agents - #230
Conversation
…andle.wait Port the persistent sub-agent layer from the rlm persistent-tools branch. The model can now launch named, long-lived sub-agents that survive across tool calls and hold multi-turn conversations. New kernel-side API (prime-agent-runtime): - rlm.send(prompt, name='helper') -> Handle (returns immediately) - handle.poll() -> ToolState (status, results FIFO, queued, error) - await handle.wait() -> RLMResult (blocks for next result) - Re-sending the same name continues the agent's conversation - <skill>.send(...) for background skill execution New host-side handlers: - rlm.send.create: creates a persistent sub-agent session - rlm.send.advance: sends a prompt to an existing sub-agent - rlm.send.close: tears down a persistent sub-agent Infrastructure: - async_runtime.py: BackgroundWorker, Handle, Registry, ToolState (ported from rlm's async_runtime.py) - attach_background() adds .send to skill modules in bootstrap - Persistent children tracked in AgentSession._persistentRlmChildren - Named session dirs (sub-<name>) for stable, human-readable paths - Cleanup in disposeAsync() tears down all persistent children System prompt updated to document send/poll/wait lifecycle. Tests updated to reflect new prompt text (zero new test failures).
- send() is now sync (not async), matching rlm's API: handle = rlm.send(...) instead of handle = await rlm.send(...) - Session creation (host_request rlm.send.create) is deferred to the worker's first process() call, so send() returns immediately without blocking - Processor lazily creates the host session and updates worker.session_dir - Prompt updated: 'handle = rlm.send(...)' (no await)
…ebuild The kernel venv was caching the old prime-agent-runtime wheel (0.1.0) which did not include async_runtime.py. Bumping to 0.2.0 busts the uv wheel cache, and bumping BOOTSTRAP_SCHEMA to 8 forces a full venv rebuild on next start.
…t sub-agents Also fix pre-existing unused-parameter warning in agent-session.ts.
The persistent/background sub-agent path (rlm.send / handle.poll / handle.wait) was wired on both sides but only exercised by the system-prompt assertion. TS (AgentSession): create -> advance -> advance under the same name -> close, reusing the rlm.run harness with a scripted streamFn. Covers multi-turn continuation reusing one sub-<name> session, create-twice dedup, the unknown-name advance error, and the recursion depth-cap error. Python (prime-agent-runtime): async_runtime worker semantics with a fake processor: resident continuation under one name, non-consuming FIFO poll, running->finished status, error-halt plus re-send rebuild, ephemeral one-shot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…agents-2026-06-18
| const request: RlmSendCreateRequest = { | ||
| name: payload.name, | ||
| max_tokens: typeof payload.max_tokens === "number" ? payload.max_tokens : undefined, | ||
| }; |
There was a problem hiding this comment.
🟢 Low core/rlm-runtime.ts:67
createRlmSendCreateHostHandler constructs RlmSendCreateRequest by explicitly copying only name and max_tokens, so any additional properties in the payload are silently dropped even though the interface accepts arbitrary keys via its index signature. Add ...payload to the request object so extra properties are forwarded to the handler.
| const request: RlmSendCreateRequest = { | |
| name: payload.name, | |
| max_tokens: typeof payload.max_tokens === "number" ? payload.max_tokens : undefined, | |
| }; | |
| const request: RlmSendCreateRequest = { | |
| ...payload, | |
| name: payload.name, | |
| max_tokens: typeof payload.max_tokens === "number" ? payload.max_tokens : undefined, | |
| }; |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/rlm-runtime.ts around lines 67-70:
`createRlmSendCreateHostHandler` constructs `RlmSendCreateRequest` by explicitly copying only `name` and `max_tokens`, so any additional properties in the payload are silently dropped even though the interface accepts arbitrary keys via its index signature. Add `...payload` to the request object so extra properties are forwarded to the handler.
Evidence trail:
packages/coding-agent/src/core/rlm-runtime.ts lines 34-38 (RlmSendCreateRequest interface with index signature), lines 62-74 (createRlmSendCreateHostHandler only copying name and max_tokens), prime-agent-runtime/src/rlm/__init__.py lines 200-214 (Python sender spreading **self._kwargs into payload), packages/coding-agent/src/core/agent-session.ts lines 3694-3696 (handler registration only using request.name and request.max_tokens)
| * Create a persistent sub-agent session that survives across host requests. | ||
| * Called by the rlm.send.create host handler. | ||
| */ | ||
| private async _createPersistentRlmChild(name: string, _maxTokens?: number): Promise<{ session_dir: string | null }> { |
There was a problem hiding this comment.
🟡 Medium core/agent-session.ts:4316
The existing check and the set on line 4345 are separated by an awaited async call. If two concurrent requests use the same name, the second overwrites the first in _persistentRlmChildren, so the first runtime is leaked — _closeAllPersistentRlmChildren only iterates the final map keys and never disposes it. Consider storing a creation promise in the map so concurrent calls with the same name share one runtime, or re-checking the map after the await and disposing the duplicate before overwriting.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/coding-agent/src/core/agent-session.ts around line 4316:
The `existing` check and the `set` on line 4345 are separated by an awaited async call. If two concurrent requests use the same `name`, the second overwrites the first in `_persistentRlmChildren`, so the first runtime is leaked — `_closeAllPersistentRlmChildren` only iterates the final map keys and never disposes it. Consider storing a creation promise in the map so concurrent calls with the same `name` share one runtime, or re-checking the map after the await and disposing the duplicate before overwriting.
Evidence trail:
packages/coding-agent/src/core/agent-session.ts lines 4316-4351 (REVIEWED_COMMIT): _createPersistentRlmChild method with check-then-await-then-set pattern. Line 4318: map.get check, line 4344: await _createRlmSubagentRuntime, line 4345: map.set. Lines 4405-4414: _closeAllPersistentRlmChildren iterates only current map keys. Line 3694-3695: caller passes request.name from kernel host handler.
…wn race, and ignored max_tokens - _advancePersistentRlmChild attributed cumulative child usage on every advance instead of the delta since the last advance, inflating parent session cost totals and the RlmRunResult returned to Python callers. Track lastAttributedUsage and lastReturnedRlmUsage per persistent child and compute only the per-advance delta. - _closePersistentRlmChild disposed the child runtime without aborting an in-flight advance first, so disposeAsync could tear down state while _advancePersistentRlmChild was mid-prompt/waitForIdle. Abort the child session before releasing the runtime, and guard the advance against the child being removed from the map during the await. - rlm.send.create accepted max_tokens from the Python caller but discarded it (_maxTokens). Add maxTokens to AgentOptions, forward it through CreateRlmSubagentRuntimeOptions, and apply it when constructing the child Agent so per-request output token caps take effect. - Add regression test for usage delta correctness across multiple advances.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 4395fbe. Configure here.
| subtractAssistantUsage(usageDelta, child.lastAttributedUsage); | ||
| child.lastAttributedUsage = cloneUsage(cumulativeAssistantUsage); | ||
|
|
||
| this._attributeRlmChildUsageToParent(usageDelta, parentAssistant); |
There was a problem hiding this comment.
Deferred advance wrong usage parent
Medium Severity
For rlm.send.advance, child token usage is tied to whichever assistant message is last on the parent when the host handler runs, not when the user queued the background send. After the parent completes another turn, usage from an earlier sub-agent job can be attributed to the wrong assistant message.
Reviewed by Cursor Bugbot for commit 4395fbe. Configure here.
| subtractAssistantUsage(usageDelta, child.lastAttributedUsage); | ||
| child.lastAttributedUsage = cloneUsage(cumulativeAssistantUsage); | ||
|
|
||
| this._attributeRlmChildUsageToParent(usageDelta, parentAssistant); |
There was a problem hiding this comment.
Close race drops child usage
Medium Severity
If rlm.send.close runs while rlm.send.advance is in flight, an advance that already finished the child turn can hit the post-waitForIdle guard, throw, and skip _attributeRlmChildUsageToParent even though the sub-agent consumed tokens.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 4395fbe. Configure here.
|
I think this should reuse the existing continual harness + user-orchestrator abstractions instead of adding a parallel persistent-agent lifecycle. I double-checked the underlying code paths below so this is grounded in the current implementation. Existing continual harness paths:
So if this PR is adding named persistent RLM agents, I think the durable definition should extend/reuse the existing harness rlm.harness.create_subagent(
title=...,
content=...,
id=name,
path="rlm",
metadata={...},
)
rlm.harness.update_subagent(...)
rlm.harness.delete_subagent(...)Important correction to the earlier shorthand: this is not a Then live execution should be separate and reuse the existing runtime/orchestration paths: Existing live subagent runtime paths:
Existing user-orchestrator messaging/observation paths:
So the shape I’d expect is: Concrete places where this PR currently seems to fork that stack:
This makes a private
These look like a separate host protocol for creating, messaging, and closing subagents, even though the existing abstractions above already cover durable harness entries, live daemon subagents, message delivery, and observation.
The Python-facing I’d also update the tests to assert public behavior rather than private fields. Right now
I’d prefer tests for:
That keeps persistent RLM subagents composable with continual harness refinement, while also reusing the messaging/observation machinery from #207 instead of creating another control plane. |


Introduces persistent sub-agents via a
.send()mechanism.Note
Medium Risk
New cross-language lifecycle (create/advance/close/abort) and usage attribution for nested sessions; mistakes could leak child sessions or mis-attribute tokens, but behavior is covered by new tests and idempotent close paths.
Overview
Adds named persistent sub-agents that stay alive across IPython cells and host requests, alongside one-off
await rlm(...)runs.The Python runtime (
prime-agent-runtime0.2.0) introducesrlm.send(...)and a newasync_runtimemodule withRegistry,BackgroundWorker, andHandle(poll()/wait()). The TypeScript host wiresrlm.send.create,rlm.send.advance, andrlm.send.closeso the kernel can create children under stablesub-<name>session dirs, send further prompts on the same session, and tear down (including on parentdisposeAsync). Usage returned per advance is delta-only so parent billing is not double-counted.Agentand child sessions gain optionalmaxTokensfor sub-agent completion caps. Wrapped Python skills get.sendfor ephemeral background runs via the same handle API. Bootstrap schema bumps to 8 and readiness checks requirerlm.send; RLM system prompts document persistent vs blocking sub-agents and skill.send.Reviewed by Cursor Bugbot for commit 4395fbe. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add persistent sub-agents to the coding agent via
rlm.sendrlm.send.create,rlm.send.advance, andrlm.send.closehost bridge requests handled inagent-session.ts.rlm.async_runtimemodule withBackgroundWorker,Handle,Registry, andattach_backgroundprimitives;rlm.send()submits work to named persistent workers backed by the host bridge.sub-<safeName>) instead of random UUIDs, and per-advance token usage deltas are computed and attributed to the parent assistant message.rlm.sendto be present before the kernel is considered ready; updates system prompt guidance to referencerlm.sendandhandle.poll()/wait()flows.Macroscope summarized 4395fbe.