feat(coding-agent): unify RLM subagent lifecycle, messaging, and observation on rlm
- rlm() and rlm.list_subagents() return one mutable RLMSubagent handle owning refresh, send, observe, info, result, and delete; RLMSpawnHandle is removed - agent messaging and observation move into rlm (send_parent, send_sibling, send_child, family, observe, info); agent_message.* and agent_observe.* host routes register whenever their controllers exist, independent of skill visibility - bundled agent-message and agent-observe skills are no longer model-visible; prompts document the rlm-native surface only - composable schema-validated result and context-admission contracts for RLM subagents - docs and changelog fragments updated for the unified surface
2 unmodified lines34567789101181 unmodified lines9394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035155 unmodified lines40740840941016041141241341441561 unmodified lines2 unmodified linesimport asyncioimport importlibimport unittestfrom pathlib import Pathfrom unittest.mock import AsyncMock, patchfrom unittest.mock import AsyncMock, call, patch
rlm_module = importlib.import_module("rlm")
81 unmodified lines self.assertEqual(result.rlm_child_id, "sub-a1b2c3d4") self.assertEqual(result.name, "api-reviewer") self.assertEqual(result.model, "deepseek/deepseek-v4-flash")
def test_parses_contract_result_path_from_subagent(self) -> None: host_request = AsyncMock( return_value={ "rlm_child_id": "sub-a1b2c3d4", "name": "contract-worker", "session_dir": "/tmp/parent/sub-a1b2c3d4", "model": "test/model", "result_path": "/tmp/parent/sub-a1b2c3d4/rlm-result.json", } ) with patch.object(rlm_module, "host_request", host_request): result = asyncio.run( rlm_module.rlm( "check", result={"schema": {"type": "object", "properties": {}}}, context={"result": {"fields": []}}, ) ) self.assertEqual(result.result_path, Path("/tmp/parent/sub-a1b2c3d4/rlm-result.json"))
def test_rejects_host_bound_integers_outside_javascript_safe_range(self) -> None: too_large = 2**53 host_request = AsyncMock() with patch.object(rlm_module, "host_request", host_request): with self.assertRaisesRegex(ValueError, "safe range"): asyncio.run( rlm_module.rlm( "check", result={ "schema": { "type": "object", "properties": {"count": {"const": too_large}}, } }, ) ) with self.assertRaisesRegex(ValueError, "safe range"): asyncio.run(rlm_module.rlm.submit_result({"count": too_large})) host_request.assert_not_awaited()
def test_submits_json_result_without_spoofable_identity(self) -> None: host_request = AsyncMock( return_value={ "child_id": "sub-a1b2c3d4", "generation": 0, "path": "/tmp/result.json", "sha256": "a" * 64, } ) with patch.object(rlm_module, "host_request", host_request): receipt = asyncio.run(rlm_module.rlm.submit_result({"status": "approved"})) self.assertEqual(receipt.rlm_child_id, "sub-a1b2c3d4") self.assertEqual(receipt.generation, 0) host_request.assert_awaited_once_with( "rlm.submit_result", {"value": {"status": "approved"}}, ) with self.assertRaisesRegex(ValueError, "non-finite"): asyncio.run(rlm_module.submit_result({"score": float("nan")})) with self.assertRaisesRegex(TypeError, "JSON values"): asyncio.run(rlm_module.submit_result({"value": {1, 2}}))
def test_reads_only_requested_contract_result_fields(self) -> None: host_request = AsyncMock( return_value={ "child_id": "sub-a1b2c3d4", "generation": 1, "path": "/tmp/result.json", "sha256": "b" * 64, "result": {"status": "approved"}, } ) handle = rlm_module.RLMSubagent( rlm_child_id="sub-a1b2c3d4", name="worker", session_dir=Path("/tmp/worker"), model="test/model", ) with patch.object(rlm_module, "host_request", host_request): result = asyncio.run(rlm_module.rlm.get_result(handle, fields=("status",))) self.assertEqual(result.generation, 1) self.assertEqual(result.value, {"status": "approved"}) host_request.assert_awaited_once_with( "rlm.get_result", {"target": "sub-a1b2c3d4", "fields": ["status"]}, ) with self.assertRaisesRegex(ValueError, "duplicates"): asyncio.run(rlm_module.get_result(handle, fields=("status", "status"))) with self.assertRaisesRegex(TypeError, "generation"): asyncio.run(rlm_module.get_result(handle, generation=True))
def test_run_returns_mutable_subagent_with_handle_api(self) -> None: host_request = AsyncMock( return_value={ "rlm_child_id": "sub-a1b2c3d4", "name": "worker", "session_dir": "/tmp/parent/sub-a1b2c3d4", "model": "test/model", } ) with patch.object(rlm_module, "host_request", host_request): child = asyncio.run(rlm_module.rlm("check")) self.assertIsInstance(child, rlm_module.RLMSubagent) self.assertEqual(child.id, "sub-a1b2c3d4") self.assertEqual(child.session_name, "worker") self.assertTrue(child.running) for method in ("refresh", "send", "observe", "info", "result", "delete"): self.assertTrue(callable(getattr(child, method)), method)
def test_subagent_send_and_observe_route_through_host(self) -> None: child = rlm_module.RLMSubagent( rlm_child_id="sub-a1b2c3d4", name="worker", session_dir=Path("/tmp/parent/sub-a1b2c3d4"), model="test/model", ) replies = [ {"deliveryStatus": "sent"}, {"target": "worker", "messages": []}, ] host_request = AsyncMock(side_effect=replies) with patch.object(rlm_module, "host_request", host_request), patch.object( rlm_module, "emit" ): receipt = asyncio.run(child.send("status?")) observed = asyncio.run(child.observe(limit=3, max_chars=64)) self.assertEqual(receipt, {"deliveryStatus": "sent"}) self.assertEqual(observed, {"target": "worker", "messages": []}) self.assertEqual( host_request.await_args_list, [ call("agent_message.send", {"message": "status?", "receiver_role": "child", "receiver_name": "worker"}), call("agent_observe.recent", {"target": "worker", "limit": 3, "max_chars": 64}), ], )
def test_subagent_send_rejects_deleted_children(self) -> None: child = rlm_module.RLMSubagent( rlm_child_id="sub-a1b2c3d4", name="worker", session_dir=Path("/tmp/parent/sub-a1b2c3d4"), status="deleted", ) with patch.object(rlm_module, "host_request", AsyncMock()) as host_request: with self.assertRaisesRegex(RuntimeError, "deleted"): asyncio.run(child.send("hello")) host_request.assert_not_awaited()
def test_subagent_delete_merges_state_in_place(self) -> None: child = rlm_module.RLMSubagent( rlm_child_id="sub-a1b2c3d4", name="worker", session_dir=Path("/tmp/old"), model="test/model", status="running", ) host_request = AsyncMock( return_value={ "subagent": { "rlm_child_id": "sub-a1b2c3d4", "active_session_id": None, "session_id": "session-child", "session_name": "worker", "session_dir": "/tmp/parent/sub-a1b2c3d4", "status": "completed", } } ) with patch.object(rlm_module, "host_request", host_request): returned = asyncio.run(child.delete()) self.assertIs(returned, child) self.assertEqual(child.status, "deleted") self.assertEqual(child.session_id, "session-child") self.assertEqual(child.session_dir, Path("/tmp/parent/sub-a1b2c3d4")) with patch.object(rlm_module, "host_request", AsyncMock()) as host_request: asyncio.run(child.delete()) host_request.assert_not_awaited()
def test_subagent_refresh_merges_registry_entry(self) -> None: child = rlm_module.RLMSubagent( rlm_child_id="sub-a1b2c3d4", name="worker", session_dir=Path("/tmp/old"), status="admitted", ) host_request = AsyncMock( return_value={ "subagents": [ { "rlm_child_id": "sub-a1b2c3d4", "active_session_id": "active-child", "session_id": "session-child", "session_name": "worker", "session_dir": "/tmp/parent/sub-a1b2c3d4", "status": "completed", } ] } ) with patch.object(rlm_module, "host_request", host_request): refreshed = asyncio.run(child.refresh()) self.assertIs(refreshed, child) self.assertEqual(child.status, "completed") self.assertEqual(child.active_session_id, "active-child")
with patch.object(rlm_module, "host_request", AsyncMock(return_value={"subagents": []})): missing = asyncio.run(child.refresh()) self.assertIs(missing, child) self.assertEqual(child.status, "missing")
def test_send_parent_sibling_child_route_through_host(self) -> None: host_request = AsyncMock(side_effect=[{"ok": True}, {"ok": True}, {"ok": True}]) with patch.object(rlm_module, "host_request", host_request), patch.object( rlm_module, "emit" ): asyncio.run(rlm_module.send_parent("to parent")) asyncio.run(rlm_module.send_sibling("beta", "to sibling")) asyncio.run(rlm_module.send_child("worker", "to child")) self.assertEqual( host_request.await_args_list, [ call("agent_message.send", {"message": "to parent", "receiver_role": "parent", "receiver_name": None}), call("agent_message.send", {"message": "to sibling", "receiver_role": "sibling", "receiver_name": "beta"}), call("agent_message.send", {"message": "to child", "receiver_role": "child", "receiver_name": "worker"}), ], )
def test_send_validates_message_and_receiver(self) -> None: with patch.object(rlm_module, "host_request", AsyncMock()) as host_request: with self.assertRaisesRegex(TypeError, "non-empty str"): asyncio.run(rlm_module.send_parent("")) with self.assertRaisesRegex(ValueError, "receiver_name is required"): asyncio.run(rlm_module._send_agent_message("hi", receiver_role="sibling")) with self.assertRaisesRegex(ValueError, "invalid receiver_role"): asyncio.run(rlm_module._send_agent_message("hi", receiver_role="cousin", receiver_name="x")) with self.assertRaisesRegex(TypeError, "non-empty str"): asyncio.run(rlm_module.observe(" ")) host_request.assert_not_awaited()
def test_family_and_agent_info_route_through_host(self) -> None: host_request = AsyncMock(side_effect=[{"entries": []}, {"name": "worker"}]) with patch.object(rlm_module, "host_request", host_request): roster = asyncio.run(rlm_module.family()) info = asyncio.run(rlm_module.agent_info("worker")) self.assertEqual(roster, {"entries": []}) self.assertEqual(info, {"name": "worker"}) self.assertEqual( host_request.await_args_list, [call("agent_message.list_agents"), call("agent_observe.get", {"target": "worker"})], )
def test_finds_authenticated_models_through_host(self) -> None: host_request = AsyncMock( return_value={ "models": [55 unmodified lines subagent = rlm_module.RLMSubagent( rlm_child_id="sub-a1b2c3d4", active_session_id=None, session_id="session-child", session_name="api-reviewer", name="api-reviewer", session_dir=Path("/tmp/parent/sub-a1b2c3d4"), status="running", ) host_request = AsyncMock(61 unmodified lines1- Added composable schema-validated result and context-admission contracts for RLM subagents.12- Unified the recursive sub-agent surface on `rlm`: `rlm(...)` and `rlm.list_subagents()` return the same mutable `RLMSubagent` handle owning the whole lifecycle (`refresh`, `send`, `observe`, `info`, `result`, `delete`), replacing `RLMSpawnHandle`.- Moved agent messaging and observation into `rlm` (`send_parent`, `send_sibling`, `send_child`, `family`, `observe`, `info`); `agent_message.*` and `agent_observe.*` host routes now register whenever their controllers exist, and the bundled `agent-message` and `agent-observe` skills are no longer model-visible.13 unmodified lines141516171818192021223 unmodified lines2627282930303132333410 unmodified lines4546474849495051525354555657575859606143 unmodified lines1051061071081091091101111121132 unmodified lines1161171181191201211221231241251201261271281291221301311321333 unmodified lines1371381391401341411421431441451461471481491501511521531541551561571581 unmodified line1601611621631511641651661671689 unmodified lines17817918018116918218318418518618718818917719019119219319466 unmodified lines26126226326426526626726826913 unmodified lines
session -->|"owns"| manager manager <-->|"JSON lines over stdio"| kernel kernel --> runtime --> code code -->|"rlm.run · goal.* · agent_message.*"| runtime code -->|"rlm.run · rlm.* · goal.*"| runtime runtime -->|"host_request events"| manager manager -->|"typed dispatch"| session```
3 unmodified lineshandle = await rlm("inspect the API", name="api-reviewer")print(handle.rlm_child_id, handle.name, handle.session_dir, handle.model)```
the call travels as a `host_request` event over the runtime's stdio protocol. `ReplKernelManager` dispatches request type `rlm.run` to the parent `AgentSession`, which starts a child through the same TypeScript agent machinery as the parent. The call returns over the same bridge immediately after task admission with a child handle; it never waits for or returns the child's answer. Results arrive only through explicit `agent_message` replies or files.the call travels as a `host_request` event over the runtime's stdio protocol. `ReplKernelManager` dispatches request type `rlm.run` to the parent `AgentSession`, which starts a child through the same TypeScript agent machinery as the parent. The call returns over the same bridge immediately after task admission with a child handle; it never waits for or returns the child's answer. Without a result contract, results arrive through explicit `rlm.send_parent(...)` replies or files. Contracted children submit through the host bridge and parents read an admitted projection.
The same bridge supports other typed host requests. Bundled Python skills such as `goal` call `rlm.host_request("goal.get", ...)`; state and policy remain in the TypeScript host.
## Delegation Flow10 unmodified lines H->>K: execute await rlm("inspect the API") K->>H: host_request · rlm.run H->>H: check depth and resolve model H->>H: admit child task and update registry H-->>K: RLMSpawnHandle H-->>K: RLMSubagent K-->>H: tool output H-->>M: Python result H->>C: create child runtime and prompt loop Child agent loop C->>P: stream model request P-->>C: response or tool call end C-->>H: explicit agent_message reply C-->>H: explicit rlm.send_parent reply H-->>M: ordinary agent message H->>H: update registry and attribute usage```
43 unmodified lines```pythonhandle = await rlm("subtask")```
The runtime ships the call to the host as a `host_request` event and keeps its event loop free while awaiting the reply. The host dispatches the typed request and answers with a `host_reply` request carrying the same id, so a cell can block on admission without stalling other runtime work. Child answers do not use this response path; they arrive later through explicit `agent_message` replies or files.The runtime ships the call to the host as a `host_request` event and keeps its event loop free while awaiting the reply. The host dispatches the typed request and answers with a `host_reply` request carrying the same id, so a cell can block on admission without stalling other runtime work. Child answers do not use this response path; they arrive later through explicit `rlm.send_parent(...)` replies or files.
## Python API
`prime-agent-runtime` exports:2 unmodified linesrlmrun(prompt: str, **kwargs)find_models(query: str = "", limit: int = 8)list_subagents()family()send_parent(message)send_sibling(name, message)send_child(name, message)observe(target, limit=8, max_chars=800)info(target)delete_subagent(selector)delete_subagent(target)submit_result(value)get_result(target, fields=None, generation=None)host_request(request_type: str, payload: dict | None = None)RLMSpawnHandleRLMModelRLMSubagent```
3 unmodified linesawait rlm("subtask")await rlm.run("subtask")```
`RLMSpawnHandle` contains `rlm_child_id`, `name`, `session_dir`, and `model`. It confirms admission only and never contains the child's answer.`rlm(...)`, `rlm.list_subagents()`, and `rlm.delete_subagent()` all return the same mutable `RLMSubagent` handle. It contains `rlm_child_id`, `name`, `session_dir`, `model`, and `status`, plus `result_path` when a result contract is present. It confirms admission only and never contains the child's answer; lifecycle methods (`refresh`, `send`, `observe`, `info`, `result`, `delete`) issue their own host requests and merge state in place.
Supported `rlm.run` options are:
- `name`: a unique readable child session name;- `model`: an exact `provider/model` selector from `rlm.find_models()`; and- `thinking`: an explicit child reasoning level; must be valid for the resolved child model, defaults to the parent level (clamped to the child model).- `result`: an optional host-bound JSON Schema, file sink, and required-result flag.- `context`: independent terminal, direct-message, observation, and admitted-result-field policies.
Contracted children call `await rlm.submit_result(value)`. Parents call `await rlm.get_result(child, fields=[...])`; the host validates the stored result and returns only fields admitted by the spawn contract. These policies are composable fields, not named modes or presets. Omitted contracts preserve the native lifecycle.
Every new child also receives an internal `.rlm-contract.json` lifecycle journal. It records parent follow-up completion, reply attribution, and durable notice admission even when no public result or context contract was requested. This internal state is not added to model context and does not change the spawn handle or messaging API.
Unknown options fail instead of being ignored. Model search is bounded to active, non-expired credentials. If an exact selection is unavailable or fails auth preflight, spawn fails instead of silently falling back to another model. A child otherwise inherits the parent model.
## Child Execution
1 unmodified line
1. Check `RLM_DEPTH < RLM_MAX_DEPTH`.2. Resolve the requested model or inherit the parent model.3. Create a `sub-xxxxxxxx` child directory under the parent artifact directory.4. Admit the task into the parent registry and return its `RLMSpawnHandle`.4. Admit the task into the parent registry and return its `RLMSubagent` handle.5. In detached work, create a child `SessionManager`, `Agent`, and `AgentSession`.6. Reuse provider hooks, resource loader, model registry, tools, transport, retry settings, and thinking configuration.7. Run the child prompt, retain its session, and update lifecycle state independently of the admission call.8. Attribute child usage to the parent assistant turn and persist the attribution.9 unmodified linestest_review = await rlm("review the tests", name="test-reviewer")audit = await rlm("slow independent audit", name="audit-reviewer")```
End the turn instead of waiting for completion. Children send requested answers with `await agent_message.send(message, receiver_role="parent")`, and replies arrive as ordinary agent messages over later turns. A child may instead write results to files for the parent to read. The host runs each admitted child as an independent `AgentSession`; daemon-backed children can be retained as independently addressable session workers.End the turn instead of waiting for completion. Children send requested answers with `await rlm.send_parent(message)`, and replies arrive as ordinary agent messages over later turns. A child may instead write results to files for the parent to read. The host runs each admitted child as an independent `AgentSession`; daemon-backed children can be retained as independently addressable session workers.
## Parent-Scoped Sub-Agent Registry
The TypeScript parent maintains the authoritative direct-child registry. `await rlm.list_subagents()` returns stable child IDs, active-session IDs when daemon-backed, session IDs, names, directories, and running/completed status.
This registry survives kernel restart, compaction, and parent restore. Successfully completed daemon-backed children are rehydrated from the parent artifact registry. Inline children remain inspectable in the current process but have no active-session ID.
The parent can continue a retained daemon child with `await agent_message.send(..., receiver_role="child", receiver_name=child.session_name)`. `rlm.delete_subagent()` accepts an exact child ID, active-session ID, session ID, or unique name. Deletion cancels or closes the runtime, writes a durable tombstone, and removes the child from messaging and observation. It does not erase the transcript or artifacts on disk.The parent can continue a retained daemon child with `await child.send(...)` or `await rlm.send_child(name, message)`. `rlm.delete_subagent()` accepts a handle, an exact child ID, active-session ID, session ID, or unique name. Deletion cancels or closes the runtime, writes a durable tombstone, and removes the child from messaging and observation. It does not erase the transcript or artifacts on disk.
Registry scope follows the parent transcript. An unrelated new parent session does not inherit children.
## Usage and Cost Attribution66 unmodified lines| Requested model unavailable | Spawn fails instead of substituting another model. || Host connection closed | Pending `host_request` calls fail with `RuntimeError` so awaiting cells unblock. || Child cancellation | Host aborts the child and removes failed/cancelled registry entries. || Parent teardown | Active descendants are cancelled and their runtimes are closed. || Daemon restart during an incomplete parent follow-up | The persisted turn is closed as failed with a before/after durable-prompt phase, its parent notice is replayed, and the interrupted provider turn is not executed twice. |
## Focused Validation
From the repository root, the implementation is covered by focused kernel, recursion, context-tree, daemon RLM, and runtime tests. When changing child creation or accounting, include `agent-session-recursion.test.ts`; when changing the stdio runtime protocol, include the `repl-kernel-*.test.ts` suites; when changing daemon retention, include the daemon RLM lifecycle tests.17 unmodified lines1819202122222324252641 unmodified lines686970717272737475757677787879808182838182838484858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131901321331341351361371381391401411421431441451461471481491509715115215315415515610315715815916016125 unmodified lines1871881891901371911921931941954 unmodified lines17 unmodified lines parent -->|"Python call"| kernel kernel <-->|"inspect · search · transform"| data kernel <-->|"call functions"| skills kernel -->|"spawn focused work"| children children -->|"agent messages · files"| parent children -->|"agent messages · structured results · files"| parent kernel -->|"admission handle"| parent parent --> answer```
41 unmodified linestest_review = await rlm("Review the test coverage", name="test-reviewer")integration_audit = await rlm("Run the slow integration audit", name="integration-audit")```
Results arrive only through explicit `agent_message` replies or files, never as an `rlm()` return value. Children reply when an answer is needed:Without a result contract, results arrive through explicit replies or files, never as an `rlm()` return value. Children reply when an answer is needed, and the parent can follow up with a retained child, all through the same `rlm` surface:
```pythonawait agent_message.send(message, receiver_role="parent")await rlm.send_parent(message) # from inside a childawait api_review.send("Check the newly added regression test.")```
The parent can follow up with a retained child:#### Composable result and context contracts
A child can receive independent result and context policies without selecting a named mode or preset:
```pythonawait agent_message.send( "Check the newly added regression test.", receiver_role="child", receiver_name=api_review.name,schema = { "type": "object", "properties": { "status": {"type": "string", "maxLength": 32}, "finding_ids": {"type": "array", "items": {"type": "string"}}, "private_notes": {"type": "string", "maxLength": 4000}, }, "required": ["status", "finding_ids", "private_notes"], "additionalProperties": False,}
review = await rlm( "Review the change and submit the contracted result.", result={"schema": schema, "sink": {"kind": "file"}, "required": True}, context={ "terminal": {"payload": False, "notify": True}, "direct_messages": {"payload": False, "notify": True}, "observe": False, "result": {"fields": ["status", "finding_ids"]}, },)```
The host binds the contract to the admitted child identity. With `required=True`, the child submits exactly one value per task turn:
```pythonawait rlm.submit_result({ "status": "approved", "finding_ids": [], "private_notes": "not admitted to parent context",})```
The parent explicitly reads a schema-validated projection:
```pythonresult = await rlm.get_result(review, fields=["status"])print(result.value)```
Set `required=False` when a task turn may finish without a structured result. The child prompt marks submission as optional. An omitted result creates no ready generation and leaves the latest settled result unchanged. Any submitted value must still match the schema.
Each context field acts independently. `payload=False` prevents child-authored terminal or direct-message text from reaching the recipient; `notify=True` retains a fixed host-authored notice. `observe=False` removes transcript previews and recent messages when a parent or sibling observes the contracted child. `context.result.fields` is the maximum result projection the parent can request. Omitting `result` and `context` preserves ordinary Prime behavior. The host still keeps an internal per-child follow-up journal so daemon restart cannot lose an unadmitted completion notice; this journal is not model-visible and does not add fields to the public handle.
Schemas use a bounded JSON Schema subset. External references, unsupported keywords, oversized schemas and results, invalid values, integers outside JavaScript's safe range, duplicate submissions, and field requests outside the admission list fail closed. Contracts contain model context; they do not restrict same-UID filesystem access or make admitted string fields semantically trustworthy.
#### Child handles and lifecycle
An admission handle contains `rlm_child_id`, `name`, `session_dir`, and `model`. Child usage is attributed to the parent session while remaining distinguishable in context-tree reporting.`rlm(...)` and `rlm.list_subagents()` return the same mutable `RLMSubagent` handle. It contains `rlm_child_id`, `name`, `session_dir`, `model`, and `status`; contracted results also add `result_path`. Child usage is attributed to the parent session while remaining distinguishable in context-tree reporting.
The handle owns the whole lifecycle:
```pythonawait child.refresh() # merge the latest registry state in placeawait child.send("Follow-up note.") # direct message to the childawait child.observe() # bounded recent-message previewsawait child.result(fields=["status"]) # admitted structured result, when contractedawait child.delete() # remove the child when no longer needed```
Module-level equivalents exist on `rlm` itself: `rlm.send_parent(message)`, `rlm.send_sibling(name, message)`, `rlm.send_child(name, message)`, `rlm.family()`, `rlm.observe(name)`, `rlm.info(name)`, and `rlm.delete_subagent(child_or_id)`.
The parent-scoped child registry survives compaction, kernel restart, and parent restoration:
```pythonchildren = await rlm.list_subagents()for child in children: print(child.session_name, child.status, child.active_session_id) print(child.name, child.status, child.active_session_id)```
Successfully completed daemon-backed children remain addressable while their parent session is open. Delete a child only when its context is no longer needed:
```pythonawait rlm.delete_subagent(children[0])await children[0].delete()```
The default recursion depth allows a root agent to create children. Raising the configured depth allows descendants to recurse further.
25 unmodified linesSee [Long-Running and Background Agents](long-running-agents.md) for these lifecycle features.
## Host Bridge
Python skills use typed host requests for capabilities whose authoritative state belongs outside the kernel. For example, the `goal`, `agent_message`, `rlm_heartbeat`, and `compact` skills call `rlm.host_request(...)`; the TypeScript host validates the request and owns the state transition.Python code uses typed host requests for capabilities whose authoritative state belongs outside the kernel. The `rlm` callable itself sends messaging and observation requests this way, and the `goal`, `rlm_heartbeat`, and `compact` skills call `rlm.host_request(...)`; the TypeScript host validates the request and owns the state transition.
This keeps credentials, provider execution, transcript writes, worker routing, and scheduling out of Python while retaining a programmatic model interface.
## Trust Model4 unmodified lines183 unmodified linesExpand all184185186187188189190191192193194195196197215 unmodified linesExpand all183 unmodified linesExpand all // Tool factories (for custom cwd) createEditTool, createIpythonTool, type PromptTemplate, type RlmChildContextContract, type RlmChildContract, type RlmPayloadContextPolicy, type RlmResultContextPolicy, type RlmResultContract, type RlmResultController, type RlmSubagentRuntime, type SubagentRuntimeHost,} from "./core/sdk.js";export type { SessionActionSnapshot } from "./core/session-action-store.js";215 unmodified linesExpand all710 unmodified linesExpand all7117127137147157167177187191000 unmodified linesExpand all710 unmodified linesExpand all initialActiveToolNames: runtimeSessionOptions?.initialActiveToolNames, allowedToolNames: runtimeSessionOptions?.allowedToolNames, includeGoals: runtimeSessionOptions?.includeGoals, includeCompactSkill: runtimeSessionOptions?.includeCompactSkill, rlmResultController: runtimeSessionOptions?.rlmResultController, rlmHeartbeatController: runtimeSessionOptions?.rlmHeartbeatController, agentMessageController: runtimeSessionOptions?.agentMessageController, agentObserveController: runtimeSessionOptions?.agentObserveController, autonomous:1000 unmodified linesExpand all