ph-rlm 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. ph_rlm-0.2.0/.gitignore +31 -0
  2. ph_rlm-0.2.0/LICENSE +21 -0
  3. ph_rlm-0.2.0/PKG-INFO +227 -0
  4. ph_rlm-0.2.0/README.md +198 -0
  5. ph_rlm-0.2.0/pyproject.toml +56 -0
  6. ph_rlm-0.2.0/src/ph_rlm/__init__.py +18 -0
  7. ph_rlm-0.2.0/src/ph_rlm/bindings.py +259 -0
  8. ph_rlm-0.2.0/src/ph_rlm/bundle.yaml +174 -0
  9. ph_rlm-0.2.0/src/ph_rlm/context_loader.py +595 -0
  10. ph_rlm-0.2.0/src/ph_rlm/harness/__init__.py +351 -0
  11. ph_rlm-0.2.0/src/ph_rlm/harness/auto.py +154 -0
  12. ph_rlm-0.2.0/src/ph_rlm/harness/invariant.py +73 -0
  13. ph_rlm-0.2.0/src/ph_rlm/harness/planner.py +327 -0
  14. ph_rlm-0.2.0/src/ph_rlm/harness/service.py +482 -0
  15. ph_rlm-0.2.0/src/ph_rlm/harness/state.py +308 -0
  16. ph_rlm-0.2.0/src/ph_rlm/kernel/__init__.py +28 -0
  17. ph_rlm-0.2.0/src/ph_rlm/kernel/codec.py +126 -0
  18. ph_rlm-0.2.0/src/ph_rlm/kernel/manager.py +1338 -0
  19. ph_rlm-0.2.0/src/ph_rlm/kernel/protocol.py +339 -0
  20. ph_rlm-0.2.0/src/ph_rlm/kernel/venv.py +221 -0
  21. ph_rlm-0.2.0/src/ph_rlm/keys.py +31 -0
  22. ph_rlm-0.2.0/src/ph_rlm/messaging.py +532 -0
  23. ph_rlm-0.2.0/src/ph_rlm/presentation.py +184 -0
  24. ph_rlm-0.2.0/src/ph_rlm/prompt.py +255 -0
  25. ph_rlm-0.2.0/src/ph_rlm/py.typed +0 -0
  26. ph_rlm-0.2.0/src/ph_rlm/skills.py +142 -0
  27. ph_rlm-0.2.0/src/ph_rlm/snapshot.py +393 -0
  28. ph_rlm-0.2.0/src/ph_rlm/subagents.py +932 -0
  29. ph_rlm-0.2.0/tests/fixture_replay.py +173 -0
  30. ph_rlm-0.2.0/tests/rlm_fixtures.py +239 -0
  31. ph_rlm-0.2.0/tests/runtime_helpers.py +86 -0
  32. ph_rlm-0.2.0/tests/shapes.json +44 -0
  33. ph_rlm-0.2.0/tests/test_bindings.py +289 -0
  34. ph_rlm-0.2.0/tests/test_boot_report.py +172 -0
  35. ph_rlm-0.2.0/tests/test_bundle.py +209 -0
  36. ph_rlm-0.2.0/tests/test_codec.py +157 -0
  37. ph_rlm-0.2.0/tests/test_conformance.py +223 -0
  38. ph_rlm-0.2.0/tests/test_context_loader.py +345 -0
  39. ph_rlm-0.2.0/tests/test_fixture_replay.py +201 -0
  40. ph_rlm-0.2.0/tests/test_governance_gate.py +336 -0
  41. ph_rlm-0.2.0/tests/test_harness.py +663 -0
  42. ph_rlm-0.2.0/tests/test_harness_invariant.py +108 -0
  43. ph_rlm-0.2.0/tests/test_harness_planner.py +507 -0
  44. ph_rlm-0.2.0/tests/test_kernel.py +593 -0
  45. ph_rlm-0.2.0/tests/test_kernel_confinement.py +341 -0
  46. ph_rlm-0.2.0/tests/test_lifecycle.py +98 -0
  47. ph_rlm-0.2.0/tests/test_messaging.py +469 -0
  48. ph_rlm-0.2.0/tests/test_presentation.py +231 -0
  49. ph_rlm-0.2.0/tests/test_prompt.py +252 -0
  50. ph_rlm-0.2.0/tests/test_protocol_mirror.py +229 -0
  51. ph_rlm-0.2.0/tests/test_rlm_profile.py +153 -0
  52. ph_rlm-0.2.0/tests/test_runtime_integration.py +213 -0
  53. ph_rlm-0.2.0/tests/test_skills_python.py +222 -0
  54. ph_rlm-0.2.0/tests/test_snapshot.py +403 -0
  55. ph_rlm-0.2.0/tests/test_subagents.py +1260 -0
  56. ph_rlm-0.2.0/tests/test_venv.py +128 -0
  57. ph_rlm-0.2.0/tests/test_vocabulary.py +41 -0
@@ -0,0 +1,31 @@
1
+ # Build and environment
2
+ .venv/
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ __pycache__/
7
+ *.py[cod]
8
+ jjt/
9
+ w2/
10
+
11
+ # Tooling caches
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ # Written by the guest's subprocess collectors and combined at the end of the
17
+ # run; see `conftest.GUEST_COVERAGE_RC`.
18
+ .coverage-guest*
19
+ htmlcov/
20
+ # Dropped at the repo root by pytest-textual-snapshot when a snapshot test
21
+ # fails. The reference snapshots under `__snapshots__/` are the committed
22
+ # expectation; this is the diff viewer for a run that did not match one.
23
+ snapshot_report.html
24
+
25
+ # Reference checkouts of the upstream projects this port reads from. Vendored
26
+ # locally so the plans' citations are verifiable; never part of this repo.
27
+ sources/
28
+
29
+ # Local scratch
30
+ .ph/
31
+ *.local.yaml
ph_rlm-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Charles Tabor
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ph_rlm-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.5
2
+ Name: ph-rlm
3
+ Version: 0.2.0
4
+ Summary: pH RLM bundle: the CPython fd-3 code runtime, bindings, subagents and the Continual Harness.
5
+ Project-URL: Homepage, https://github.com/chastabor/pH
6
+ Project-URL: Repository, https://github.com/chastabor/pH
7
+ Project-URL: Documentation, https://github.com/chastabor/pH/blob/main/docs/README.md
8
+ Project-URL: Issues, https://github.com/chastabor/pH/issues
9
+ Author: Charles Tabor
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,code-mode,llm,rlm,sandbox,subagents
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Software Development
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: dill>=0.3.8
25
+ Requires-Dist: filelock>=3.15
26
+ Requires-Dist: ph-core==0.2.0
27
+ Requires-Dist: ph-runtime-guest==0.2.0
28
+ Description-Content-Type: text/markdown
29
+
30
+ # ph-rlm
31
+
32
+ *Code Mode: the model writes Python, and every call that program makes is a
33
+ governed, logged tool call.*
34
+
35
+ Prime Agent's design — the RLM loop, non-blocking delegation, the nuclear-family
36
+ boundary, the Continual Harness, the doctrine prompts — implemented on pH's
37
+ seams. The rule the whole package follows (§6.8) is: take the *semantics*, not
38
+ the runtime. `ph_rlm.kernel` is pH's own CPython subprocess and
39
+ [`ph-runtime-guest`](../ph-runtime-guest/) is its other half.
40
+
41
+ ```bash
42
+ phern --profile rlm --provider llama --model <model> --mode tui
43
+ phern --profile rlm-stable --provider llama --model <model> --mode tui # gates on
44
+ ```
45
+
46
+ The package registers a **bundle**, so `ph-app` composes the `rlm` profiles
47
+ without depending on this distribution — and an install without it is simply not
48
+ offered them.
49
+
50
+ ## What the model sees
51
+
52
+ One callable. Under `tools.mode: code` the registry presents the reserved
53
+ transport `run_code` as **`ipython`**, with prime-agent's wording ported
54
+ verbatim and its result layout (`stdout / stderr / result / traceback`) kept, so
55
+ a model trained against that surface finds the surface it knows. Everything else
56
+ arrives in the cell's `globals()` as namespaces whose every call is one `call`
57
+ frame out of the kernel and back through the whole tool pipeline:
58
+
59
+ ```python
60
+ files = await tools.glob(pattern="src/**/*.py") # every registered tool
61
+ child = await rlm.run("review this diff", name="reviewer")
62
+ await agent_message.send("done", receiver_role="parent")
63
+ answer = await websearch(query="…") # a Python skill
64
+ ```
65
+
66
+ That is the whole argument for the package. Prime Agent reached the host over an
67
+ `ipykernel.Comm`, which no `tools/pre-execute` listener, no approval and no call
68
+ limit ever observed. There is no such channel here: the governed path is not a
69
+ convention, it is the only path that exists. One cell making forty dispatches
70
+ produces forty `tool/code-dispatch` records, forty permission evaluations, and
71
+ forty independent offload decisions — so one oversized `tools.read` is spilled
72
+ while its siblings pass through untouched.
73
+
74
+ ## The rows
75
+
76
+ Each is a listener on a seam that already exists; the bundle
77
+ (`src/ph_rlm/bundle.yaml`) is what turns them on.
78
+
79
+ | row | what it adds |
80
+ |---|---|
81
+ | `code-runtime-python` | the runtime: one CPython child per agent, fd 3 as the framed channel, resource limits applied in the child |
82
+ | `rlm-presentation` | the transport renamed to `ipython`, and how a settled cell reads |
83
+ | `rlm-bindings` | the `rlm` namespace — `rlm_run`, `rlm_list_subagents`, `rlm_delete_subagent` |
84
+ | `rlm-subagent-provider` | `rlm()` as a `ctx.subagents` provider: admission logged, the handle returns before the child answers |
85
+ | `rlm-messaging` | `agent_message` and `agent_observe` — `agent_message_send`, `agent_message_list_agents`, `agent_observe_list`, `agent_observe_get` |
86
+ | `rlm-prompt` | the doctrine, plus the volatile facts (depth, cwd, family, workspace tier) as a post-cache `context()` snapshot |
87
+ | `rlm-harness` | the Continual Harness and `/refine` |
88
+ | `rlm-harness-invariant` | asserts `harness_state.json` still equals the `harness/*` fold |
89
+ | `rlm-kernel-snapshot` | the per-variable `kernel/snapshot` events that earn `persistence: namespace` |
90
+ | `rlm-skills-python` | a skill directory that is also an installable package, installed into the kernel venv and imported at boot |
91
+ | `rlm-context-loader` | a queryable corpus — `context_search`, `context_chunks`, `context_head`. **Ships disabled** |
92
+
93
+ The bundle also sets `tools.mode: code`, raises `jobs.concurrency.subagent` to
94
+ 8, puts the containment ladder at `advisory` for the person and `worktree` for
95
+ their children, layers the git-worktree tier with `/workspaces` and `/revert` —
96
+ and **disables `subagent-task`**, because `rlm_run` is the same capability in
97
+ the shape the rest of this bundle is designed around, and two ways to delegate
98
+ in one prompt makes the model guess which one the tools were built for.
99
+
100
+ ## The one command
101
+
102
+ `/refine` — refine the Continual Harness, or roll a refinement back
103
+ (`[--global] [--show] [--rollback <id>] [instructions]`).
104
+
105
+ A **command, not a tool**, and deliberately so: a refinement is something the
106
+ human asks for, and routing it through a model turn would put the model in the
107
+ log as having decided it. Harness state is a fold over `harness/refined` events
108
+ rather than a file, which is what makes a fork inherit the harness as of its
109
+ boundary and a rollback derivable from the event that made the change.
110
+ `harness_state.json` is written for humans and never read back — which is
111
+ exactly the arrangement that lets a projection drift, hence the invariant row.
112
+
113
+ Four checks run before anything durable is written, and each refusal is recorded
114
+ *on* the event: the reference must resolve (probed in the runtime the model
115
+ actually uses); the call pattern is **rendered, never accepted** from the model;
116
+ a `scope: global` entry goes through `ctx.approval`; and the base doctrine is
117
+ not editable.
118
+
119
+ ## Including it, and adjusting it
120
+
121
+ `rlm` is `tui` plus this bundle. `rlm-stable` adds `ph-stabilize` and arms the
122
+ rows both bundles ship off. To layer it onto something else, name the bundle —
123
+ or patch a single row:
124
+
125
+ ```bash
126
+ phern --patch '{id: code-runtime-python, config: {python: host}}' --profile rlm -p "…"
127
+ ```
128
+
129
+ ```yaml
130
+ # $PH_HOME/profiles/rlm.yaml — a longer per-cell CPU budget and a skill directory
131
+ - id: code-runtime-python
132
+ config:
133
+ python: managed
134
+ cpuSeconds: 120
135
+ addressSpaceBytes: 2147483648
136
+ maxLogBytes: 65536
137
+ maxValueBytes: 65536
138
+ maxSnapshotBytes: 16777216
139
+ skills: ["acme-websearch"]
140
+
141
+ - id: rlm-skills-python
142
+ config:
143
+ paths: ["~/.ph/skills", "./.ph/skills"] # last source wins, by name
144
+ ```
145
+
146
+ | row | config | default |
147
+ |---|---|---|
148
+ | `code-runtime-python` | `python` (`managed` \| `host`), `interpreter`, `cpuSeconds`, `addressSpaceBytes`, `maxLogBytes`, `maxValueBytes`, `maxSnapshotBytes`, `bootTimeoutSeconds`, `shutdownGraceSeconds`, `cancelGraceSeconds`, `skills`, `sweepOrphans` | `managed`, 30 s, 2 GiB, 64 KiB, 64 KiB, 16 MiB, 30 s, 5 s, 2 s, none, on |
149
+ | `rlm-subagent-provider` | `maxDepth`, `maxConcurrent`, `answerPreviewChars` | `2`, `4` in the bundle (`null` — no cap — as the row default), `240` |
150
+ | `rlm-messaging` | `maxMessageChars`, `maxPending`, `rateCapacity`, `rateRefillSeconds`, `observeMaxMessages` | `16384`, `20`, `3`, `1.0`, `40` |
151
+ | `rlm-harness` | `autoRefine`, `turnsBetweenRefinements`, `cooldownMinutes`, `maxPerKind`, `maxRefinements`, `conversationChars`, `maxTokens` | on, `25`, `20`, `12`, `5`, `80000`, `32000` |
152
+ | `rlm-kernel-snapshot` | `inlineBlobMax` | `65536` |
153
+ | `rlm-bindings` | `provider` | `rlm-child` |
154
+ | `rlm-skills-python` | `paths` | empty — no skills, so the row costs nothing until a deployment configures one (I7) |
155
+ | `rlm-context-loader` | `corpus`, `sources`, `minChars`, `maxMatches` | `context`, none, `0` (`rlm-stable` sets `200000`), `200` |
156
+ | `tools-code-mode` (ph-core) | `maxDispatchesPerRun`, `maxSubagentSpawnsPerRun`, `maxParallelSubCalls` | `256`, `32`, `10` in this bundle |
157
+
158
+ Three knobs are worth understanding before changing them:
159
+
160
+ **`python`** decides what model code can reach. `managed` builds
161
+ `$PH_CACHE/runtime-venv` holding `ph-runtime-guest`, `dill` and the Python
162
+ skills *and nothing else*; `host` is the interpreter pH itself runs on — fast,
163
+ needs no `uv` and no network, which is why the suite uses it, and also what puts
164
+ `ph-core`, pydantic and Textual on the child's `sys.path`. That reaches no live
165
+ objects (a different process shares nothing) but it is a wider surface, and it
166
+ is why it is not the default. `$PH_RUNTIME_PYTHON` or `interpreter:` is the
167
+ third answer, for a deployment whose skills need a particular build.
168
+
169
+ **`cpuSeconds` is per cell, not per kernel.** `RLIMIT_CPU` is cumulative over a
170
+ process and this process is persistent, so the limit is re-armed at each run
171
+ from the CPU already consumed. Exceeding it raises from `BaseException`, so a
172
+ cell cannot `except Exception` its way past it.
173
+
174
+ **`maxDepth` and the two concurrency caps are the fan-out posture.** A child
175
+ beyond `maxDepth` is refused; children past `maxConcurrent` **queue in admission
176
+ order** rather than being refused, which is why the row's own default is `null`
177
+ and the bundle — not the row — picks a number.
178
+
179
+ ## Limitations, and things that are deliberate
180
+
181
+ - **`rlm.run` does not return an answer.** It returns an admission handle; the
182
+ child's reply arrives on a later turn as an ordinary inbox message. A model
183
+ that waits for the answer waits forever, which is why the doctrine states it
184
+ as a rule rather than a hint.
185
+ - **A denial ends the run.** `RunStopped` derives from `BaseException` so a
186
+ program cannot catch a refusal and route around it — retry with a different
187
+ path, fall back to `subprocess`. A *failure* (`ToolFailed`) is the program's
188
+ to handle; a refusal is not (C3). The same applies to a budget (C4).
189
+ - **The namespace does not survive a dead kernel.** A child that dies is
190
+ replaced and the next run gets a fresh kernel prefixed with a reset notice —
191
+ a degraded session rather than a dead harness.
192
+ - **`kernel/snapshot` is per variable, not per namespace**, and `patch` is
193
+ deliberately unimplemented: `dill` output is not byte-stable across processes
194
+ the way a QuickJS heap image is, so per-variable digesting is what actually
195
+ keeps log growth linear. The HMAC tag on a blob is *provenance, not secrecy* —
196
+ it stops a blob from another session being unpickled into this one; it is not
197
+ a defense against a hostile filesystem writer.
198
+ - **`find_models` is absent.** It would need a model catalog on `ctx.llm`,
199
+ which does not exist; a discovery call that could only answer "I don't know"
200
+ is worse than none. A child with no `model` inherits its parent's.
201
+ - **The message rate limit is backpressure, not policy.** It raises from the
202
+ tool body — the program's to handle and retry — because under C3 a *denial*
203
+ would cost the model its whole program over four messages in a second. The
204
+ **family boundary** is the opposite: a `ctx.tools.guard`, deny-only, run last,
205
+ and not re-permittable by any later listener.
206
+ - **Delivery is always steer.** A message reaches the target at its next *step*,
207
+ not its next turn. A busy target reports `queued` rather than `delivered`,
208
+ because those are different facts and a sender can act on the difference.
209
+ - **Orphans are journalled, not hoped away.** `SIGKILL` runs no cleanup and
210
+ POSIX re-parents children to PID 1, so every spawn is journalled and `fsync`ed
211
+ and every pH start sweeps. A stray is killed only when its start token still
212
+ matches the pid; where the token cannot be read, it is **reported and not
213
+ killed** — an honest "there may be a stray" beats a confident kill of
214
+ something else.
215
+
216
+ ## Tests
217
+
218
+ `tests/` — 26 modules. `test_protocol_mirror.py` is the contract between this
219
+ package and `ph-runtime-guest`: the two halves of the fd-3 protocol are written
220
+ twice on purpose (the guest must not import the harness), and that test compares
221
+ `PROTOCOL_VERSION`, every frame's required and optional field set, and the
222
+ truncation marker byte for byte. `test_governance_gate.py` runs real cells in a real kernel against
223
+ the **shipped** profile — not hand-picked rows — to pin the claim the whole
224
+ package rests on: one cell is one tool call, but forty writes are forty
225
+ governance evaluations. `test_conformance.py` inverts the usual arrangement and
226
+ enumerates the protocol's own vocabulary and the mounted registry's own
227
+ namespaces, so an untested frame type is a failure rather than a silence.
ph_rlm-0.2.0/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # ph-rlm
2
+
3
+ *Code Mode: the model writes Python, and every call that program makes is a
4
+ governed, logged tool call.*
5
+
6
+ Prime Agent's design — the RLM loop, non-blocking delegation, the nuclear-family
7
+ boundary, the Continual Harness, the doctrine prompts — implemented on pH's
8
+ seams. The rule the whole package follows (§6.8) is: take the *semantics*, not
9
+ the runtime. `ph_rlm.kernel` is pH's own CPython subprocess and
10
+ [`ph-runtime-guest`](../ph-runtime-guest/) is its other half.
11
+
12
+ ```bash
13
+ phern --profile rlm --provider llama --model <model> --mode tui
14
+ phern --profile rlm-stable --provider llama --model <model> --mode tui # gates on
15
+ ```
16
+
17
+ The package registers a **bundle**, so `ph-app` composes the `rlm` profiles
18
+ without depending on this distribution — and an install without it is simply not
19
+ offered them.
20
+
21
+ ## What the model sees
22
+
23
+ One callable. Under `tools.mode: code` the registry presents the reserved
24
+ transport `run_code` as **`ipython`**, with prime-agent's wording ported
25
+ verbatim and its result layout (`stdout / stderr / result / traceback`) kept, so
26
+ a model trained against that surface finds the surface it knows. Everything else
27
+ arrives in the cell's `globals()` as namespaces whose every call is one `call`
28
+ frame out of the kernel and back through the whole tool pipeline:
29
+
30
+ ```python
31
+ files = await tools.glob(pattern="src/**/*.py") # every registered tool
32
+ child = await rlm.run("review this diff", name="reviewer")
33
+ await agent_message.send("done", receiver_role="parent")
34
+ answer = await websearch(query="…") # a Python skill
35
+ ```
36
+
37
+ That is the whole argument for the package. Prime Agent reached the host over an
38
+ `ipykernel.Comm`, which no `tools/pre-execute` listener, no approval and no call
39
+ limit ever observed. There is no such channel here: the governed path is not a
40
+ convention, it is the only path that exists. One cell making forty dispatches
41
+ produces forty `tool/code-dispatch` records, forty permission evaluations, and
42
+ forty independent offload decisions — so one oversized `tools.read` is spilled
43
+ while its siblings pass through untouched.
44
+
45
+ ## The rows
46
+
47
+ Each is a listener on a seam that already exists; the bundle
48
+ (`src/ph_rlm/bundle.yaml`) is what turns them on.
49
+
50
+ | row | what it adds |
51
+ |---|---|
52
+ | `code-runtime-python` | the runtime: one CPython child per agent, fd 3 as the framed channel, resource limits applied in the child |
53
+ | `rlm-presentation` | the transport renamed to `ipython`, and how a settled cell reads |
54
+ | `rlm-bindings` | the `rlm` namespace — `rlm_run`, `rlm_list_subagents`, `rlm_delete_subagent` |
55
+ | `rlm-subagent-provider` | `rlm()` as a `ctx.subagents` provider: admission logged, the handle returns before the child answers |
56
+ | `rlm-messaging` | `agent_message` and `agent_observe` — `agent_message_send`, `agent_message_list_agents`, `agent_observe_list`, `agent_observe_get` |
57
+ | `rlm-prompt` | the doctrine, plus the volatile facts (depth, cwd, family, workspace tier) as a post-cache `context()` snapshot |
58
+ | `rlm-harness` | the Continual Harness and `/refine` |
59
+ | `rlm-harness-invariant` | asserts `harness_state.json` still equals the `harness/*` fold |
60
+ | `rlm-kernel-snapshot` | the per-variable `kernel/snapshot` events that earn `persistence: namespace` |
61
+ | `rlm-skills-python` | a skill directory that is also an installable package, installed into the kernel venv and imported at boot |
62
+ | `rlm-context-loader` | a queryable corpus — `context_search`, `context_chunks`, `context_head`. **Ships disabled** |
63
+
64
+ The bundle also sets `tools.mode: code`, raises `jobs.concurrency.subagent` to
65
+ 8, puts the containment ladder at `advisory` for the person and `worktree` for
66
+ their children, layers the git-worktree tier with `/workspaces` and `/revert` —
67
+ and **disables `subagent-task`**, because `rlm_run` is the same capability in
68
+ the shape the rest of this bundle is designed around, and two ways to delegate
69
+ in one prompt makes the model guess which one the tools were built for.
70
+
71
+ ## The one command
72
+
73
+ `/refine` — refine the Continual Harness, or roll a refinement back
74
+ (`[--global] [--show] [--rollback <id>] [instructions]`).
75
+
76
+ A **command, not a tool**, and deliberately so: a refinement is something the
77
+ human asks for, and routing it through a model turn would put the model in the
78
+ log as having decided it. Harness state is a fold over `harness/refined` events
79
+ rather than a file, which is what makes a fork inherit the harness as of its
80
+ boundary and a rollback derivable from the event that made the change.
81
+ `harness_state.json` is written for humans and never read back — which is
82
+ exactly the arrangement that lets a projection drift, hence the invariant row.
83
+
84
+ Four checks run before anything durable is written, and each refusal is recorded
85
+ *on* the event: the reference must resolve (probed in the runtime the model
86
+ actually uses); the call pattern is **rendered, never accepted** from the model;
87
+ a `scope: global` entry goes through `ctx.approval`; and the base doctrine is
88
+ not editable.
89
+
90
+ ## Including it, and adjusting it
91
+
92
+ `rlm` is `tui` plus this bundle. `rlm-stable` adds `ph-stabilize` and arms the
93
+ rows both bundles ship off. To layer it onto something else, name the bundle —
94
+ or patch a single row:
95
+
96
+ ```bash
97
+ phern --patch '{id: code-runtime-python, config: {python: host}}' --profile rlm -p "…"
98
+ ```
99
+
100
+ ```yaml
101
+ # $PH_HOME/profiles/rlm.yaml — a longer per-cell CPU budget and a skill directory
102
+ - id: code-runtime-python
103
+ config:
104
+ python: managed
105
+ cpuSeconds: 120
106
+ addressSpaceBytes: 2147483648
107
+ maxLogBytes: 65536
108
+ maxValueBytes: 65536
109
+ maxSnapshotBytes: 16777216
110
+ skills: ["acme-websearch"]
111
+
112
+ - id: rlm-skills-python
113
+ config:
114
+ paths: ["~/.ph/skills", "./.ph/skills"] # last source wins, by name
115
+ ```
116
+
117
+ | row | config | default |
118
+ |---|---|---|
119
+ | `code-runtime-python` | `python` (`managed` \| `host`), `interpreter`, `cpuSeconds`, `addressSpaceBytes`, `maxLogBytes`, `maxValueBytes`, `maxSnapshotBytes`, `bootTimeoutSeconds`, `shutdownGraceSeconds`, `cancelGraceSeconds`, `skills`, `sweepOrphans` | `managed`, 30 s, 2 GiB, 64 KiB, 64 KiB, 16 MiB, 30 s, 5 s, 2 s, none, on |
120
+ | `rlm-subagent-provider` | `maxDepth`, `maxConcurrent`, `answerPreviewChars` | `2`, `4` in the bundle (`null` — no cap — as the row default), `240` |
121
+ | `rlm-messaging` | `maxMessageChars`, `maxPending`, `rateCapacity`, `rateRefillSeconds`, `observeMaxMessages` | `16384`, `20`, `3`, `1.0`, `40` |
122
+ | `rlm-harness` | `autoRefine`, `turnsBetweenRefinements`, `cooldownMinutes`, `maxPerKind`, `maxRefinements`, `conversationChars`, `maxTokens` | on, `25`, `20`, `12`, `5`, `80000`, `32000` |
123
+ | `rlm-kernel-snapshot` | `inlineBlobMax` | `65536` |
124
+ | `rlm-bindings` | `provider` | `rlm-child` |
125
+ | `rlm-skills-python` | `paths` | empty — no skills, so the row costs nothing until a deployment configures one (I7) |
126
+ | `rlm-context-loader` | `corpus`, `sources`, `minChars`, `maxMatches` | `context`, none, `0` (`rlm-stable` sets `200000`), `200` |
127
+ | `tools-code-mode` (ph-core) | `maxDispatchesPerRun`, `maxSubagentSpawnsPerRun`, `maxParallelSubCalls` | `256`, `32`, `10` in this bundle |
128
+
129
+ Three knobs are worth understanding before changing them:
130
+
131
+ **`python`** decides what model code can reach. `managed` builds
132
+ `$PH_CACHE/runtime-venv` holding `ph-runtime-guest`, `dill` and the Python
133
+ skills *and nothing else*; `host` is the interpreter pH itself runs on — fast,
134
+ needs no `uv` and no network, which is why the suite uses it, and also what puts
135
+ `ph-core`, pydantic and Textual on the child's `sys.path`. That reaches no live
136
+ objects (a different process shares nothing) but it is a wider surface, and it
137
+ is why it is not the default. `$PH_RUNTIME_PYTHON` or `interpreter:` is the
138
+ third answer, for a deployment whose skills need a particular build.
139
+
140
+ **`cpuSeconds` is per cell, not per kernel.** `RLIMIT_CPU` is cumulative over a
141
+ process and this process is persistent, so the limit is re-armed at each run
142
+ from the CPU already consumed. Exceeding it raises from `BaseException`, so a
143
+ cell cannot `except Exception` its way past it.
144
+
145
+ **`maxDepth` and the two concurrency caps are the fan-out posture.** A child
146
+ beyond `maxDepth` is refused; children past `maxConcurrent` **queue in admission
147
+ order** rather than being refused, which is why the row's own default is `null`
148
+ and the bundle — not the row — picks a number.
149
+
150
+ ## Limitations, and things that are deliberate
151
+
152
+ - **`rlm.run` does not return an answer.** It returns an admission handle; the
153
+ child's reply arrives on a later turn as an ordinary inbox message. A model
154
+ that waits for the answer waits forever, which is why the doctrine states it
155
+ as a rule rather than a hint.
156
+ - **A denial ends the run.** `RunStopped` derives from `BaseException` so a
157
+ program cannot catch a refusal and route around it — retry with a different
158
+ path, fall back to `subprocess`. A *failure* (`ToolFailed`) is the program's
159
+ to handle; a refusal is not (C3). The same applies to a budget (C4).
160
+ - **The namespace does not survive a dead kernel.** A child that dies is
161
+ replaced and the next run gets a fresh kernel prefixed with a reset notice —
162
+ a degraded session rather than a dead harness.
163
+ - **`kernel/snapshot` is per variable, not per namespace**, and `patch` is
164
+ deliberately unimplemented: `dill` output is not byte-stable across processes
165
+ the way a QuickJS heap image is, so per-variable digesting is what actually
166
+ keeps log growth linear. The HMAC tag on a blob is *provenance, not secrecy* —
167
+ it stops a blob from another session being unpickled into this one; it is not
168
+ a defense against a hostile filesystem writer.
169
+ - **`find_models` is absent.** It would need a model catalog on `ctx.llm`,
170
+ which does not exist; a discovery call that could only answer "I don't know"
171
+ is worse than none. A child with no `model` inherits its parent's.
172
+ - **The message rate limit is backpressure, not policy.** It raises from the
173
+ tool body — the program's to handle and retry — because under C3 a *denial*
174
+ would cost the model its whole program over four messages in a second. The
175
+ **family boundary** is the opposite: a `ctx.tools.guard`, deny-only, run last,
176
+ and not re-permittable by any later listener.
177
+ - **Delivery is always steer.** A message reaches the target at its next *step*,
178
+ not its next turn. A busy target reports `queued` rather than `delivered`,
179
+ because those are different facts and a sender can act on the difference.
180
+ - **Orphans are journalled, not hoped away.** `SIGKILL` runs no cleanup and
181
+ POSIX re-parents children to PID 1, so every spawn is journalled and `fsync`ed
182
+ and every pH start sweeps. A stray is killed only when its start token still
183
+ matches the pid; where the token cannot be read, it is **reported and not
184
+ killed** — an honest "there may be a stray" beats a confident kill of
185
+ something else.
186
+
187
+ ## Tests
188
+
189
+ `tests/` — 26 modules. `test_protocol_mirror.py` is the contract between this
190
+ package and `ph-runtime-guest`: the two halves of the fd-3 protocol are written
191
+ twice on purpose (the guest must not import the harness), and that test compares
192
+ `PROTOCOL_VERSION`, every frame's required and optional field set, and the
193
+ truncation marker byte for byte. `test_governance_gate.py` runs real cells in a real kernel against
194
+ the **shipped** profile — not hand-picked rows — to pin the claim the whole
195
+ package rests on: one cell is one tool call, but forty writes are forty
196
+ governance evaluations. `test_conformance.py` inverts the usual arrangement and
197
+ enumerates the protocol's own vocabulary and the mounted registry's own
198
+ namespaces, so an untested frame type is a failure rather than a silence.
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ph-rlm"
7
+ version = "0.2.0"
8
+ description = "pH RLM bundle: the CPython fd-3 code runtime, bindings, subagents and the Continual Harness."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Charles Tabor" }]
14
+ keywords = ["agent", "llm", "code-mode", "rlm", "sandbox", "subagents"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Operating System :: POSIX :: Linux",
22
+ "Operating System :: MacOS",
23
+ "Topic :: Software Development",
24
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = ["ph-core==0.2.0", "ph-runtime-guest==0.2.0", "dill>=0.3.8", "filelock>=3.15"]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/chastabor/pH"
31
+ Repository = "https://github.com/chastabor/pH"
32
+ Documentation = "https://github.com/chastabor/pH/blob/main/docs/README.md"
33
+ Issues = "https://github.com/chastabor/pH/issues"
34
+
35
+ [project.entry-points."ph.bundles"]
36
+ rlm = "ph_rlm:BUNDLE"
37
+
38
+ [project.entry-points."ph.plugins"]
39
+ code-runtime-python = "ph_rlm.kernel.manager:apply"
40
+ rlm-kernel-snapshot = "ph_rlm.snapshot:apply"
41
+ rlm-presentation = "ph_rlm.presentation:apply"
42
+ rlm-bindings = "ph_rlm.bindings:apply"
43
+ rlm-messaging = "ph_rlm.messaging:apply"
44
+ rlm-prompt = "ph_rlm.prompt:apply"
45
+ rlm-harness = "ph_rlm.harness:apply"
46
+ rlm-harness-invariant = "ph_rlm.harness.invariant:apply"
47
+ rlm-context-loader = "ph_rlm.context_loader:apply"
48
+ rlm-skills-python = "ph_rlm.skills:apply"
49
+ rlm-subagent-provider = "ph_rlm.subagents:apply"
50
+
51
+ [tool.uv.sources]
52
+ ph-core = { workspace = true }
53
+ ph-runtime-guest = { workspace = true }
54
+
55
+ [tool.hatch.build.targets.wheel]
56
+ packages = ["src/ph_rlm"]
@@ -0,0 +1,18 @@
1
+ """pH's RLM bundle: Prime Agent's design implemented on pH's seams.
2
+
3
+ The rule the whole package follows (§6.8): take prime-agent's *semantics* — the
4
+ RLM loop, non-blocking admission, the nuclear-family boundary, the Continual
5
+ Harness, the doctrine prompts — and implement them on pH's seams. Do not take its
6
+ runtime. `ph_rlm.kernel` is pH's own, and `ph_runtime` is its guest half.
7
+
8
+ @module ph_rlm
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pathlib import Path
14
+
15
+ BUNDLE = Path(__file__).parent / "bundle.yaml"
16
+ """The rows the `rlm` profile layers over `ph-base`."""
17
+
18
+ __all__ = ["BUNDLE"]