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.
- ph_rlm-0.2.0/.gitignore +31 -0
- ph_rlm-0.2.0/LICENSE +21 -0
- ph_rlm-0.2.0/PKG-INFO +227 -0
- ph_rlm-0.2.0/README.md +198 -0
- ph_rlm-0.2.0/pyproject.toml +56 -0
- ph_rlm-0.2.0/src/ph_rlm/__init__.py +18 -0
- ph_rlm-0.2.0/src/ph_rlm/bindings.py +259 -0
- ph_rlm-0.2.0/src/ph_rlm/bundle.yaml +174 -0
- ph_rlm-0.2.0/src/ph_rlm/context_loader.py +595 -0
- ph_rlm-0.2.0/src/ph_rlm/harness/__init__.py +351 -0
- ph_rlm-0.2.0/src/ph_rlm/harness/auto.py +154 -0
- ph_rlm-0.2.0/src/ph_rlm/harness/invariant.py +73 -0
- ph_rlm-0.2.0/src/ph_rlm/harness/planner.py +327 -0
- ph_rlm-0.2.0/src/ph_rlm/harness/service.py +482 -0
- ph_rlm-0.2.0/src/ph_rlm/harness/state.py +308 -0
- ph_rlm-0.2.0/src/ph_rlm/kernel/__init__.py +28 -0
- ph_rlm-0.2.0/src/ph_rlm/kernel/codec.py +126 -0
- ph_rlm-0.2.0/src/ph_rlm/kernel/manager.py +1338 -0
- ph_rlm-0.2.0/src/ph_rlm/kernel/protocol.py +339 -0
- ph_rlm-0.2.0/src/ph_rlm/kernel/venv.py +221 -0
- ph_rlm-0.2.0/src/ph_rlm/keys.py +31 -0
- ph_rlm-0.2.0/src/ph_rlm/messaging.py +532 -0
- ph_rlm-0.2.0/src/ph_rlm/presentation.py +184 -0
- ph_rlm-0.2.0/src/ph_rlm/prompt.py +255 -0
- ph_rlm-0.2.0/src/ph_rlm/py.typed +0 -0
- ph_rlm-0.2.0/src/ph_rlm/skills.py +142 -0
- ph_rlm-0.2.0/src/ph_rlm/snapshot.py +393 -0
- ph_rlm-0.2.0/src/ph_rlm/subagents.py +932 -0
- ph_rlm-0.2.0/tests/fixture_replay.py +173 -0
- ph_rlm-0.2.0/tests/rlm_fixtures.py +239 -0
- ph_rlm-0.2.0/tests/runtime_helpers.py +86 -0
- ph_rlm-0.2.0/tests/shapes.json +44 -0
- ph_rlm-0.2.0/tests/test_bindings.py +289 -0
- ph_rlm-0.2.0/tests/test_boot_report.py +172 -0
- ph_rlm-0.2.0/tests/test_bundle.py +209 -0
- ph_rlm-0.2.0/tests/test_codec.py +157 -0
- ph_rlm-0.2.0/tests/test_conformance.py +223 -0
- ph_rlm-0.2.0/tests/test_context_loader.py +345 -0
- ph_rlm-0.2.0/tests/test_fixture_replay.py +201 -0
- ph_rlm-0.2.0/tests/test_governance_gate.py +336 -0
- ph_rlm-0.2.0/tests/test_harness.py +663 -0
- ph_rlm-0.2.0/tests/test_harness_invariant.py +108 -0
- ph_rlm-0.2.0/tests/test_harness_planner.py +507 -0
- ph_rlm-0.2.0/tests/test_kernel.py +593 -0
- ph_rlm-0.2.0/tests/test_kernel_confinement.py +341 -0
- ph_rlm-0.2.0/tests/test_lifecycle.py +98 -0
- ph_rlm-0.2.0/tests/test_messaging.py +469 -0
- ph_rlm-0.2.0/tests/test_presentation.py +231 -0
- ph_rlm-0.2.0/tests/test_prompt.py +252 -0
- ph_rlm-0.2.0/tests/test_protocol_mirror.py +229 -0
- ph_rlm-0.2.0/tests/test_rlm_profile.py +153 -0
- ph_rlm-0.2.0/tests/test_runtime_integration.py +213 -0
- ph_rlm-0.2.0/tests/test_skills_python.py +222 -0
- ph_rlm-0.2.0/tests/test_snapshot.py +403 -0
- ph_rlm-0.2.0/tests/test_subagents.py +1260 -0
- ph_rlm-0.2.0/tests/test_venv.py +128 -0
- ph_rlm-0.2.0/tests/test_vocabulary.py +41 -0
ph_rlm-0.2.0/.gitignore
ADDED
|
@@ -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"]
|