ypi 0.3.0 → 0.4.0
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.
- package/CHANGELOG.md +25 -0
- package/README.md +27 -3
- package/SYSTEM_PROMPT.md +66 -61
- package/package.json +4 -2
- package/rlm_query +14 -5
- package/rlm_sessions +219 -0
- package/ypi +31 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,31 @@
|
|
|
3
3
|
All notable changes to ypi are documented here.
|
|
4
4
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
5
5
|
|
|
6
|
+
## [0.4.0] - 2026-02-13
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- **`rlm_sessions` command**: inspect, read, and search session logs from sibling and parent agents in the recursive tree (`rlm_sessions --trace`, `rlm_sessions read <file>`, `rlm_sessions grep <pattern>`)
|
|
10
|
+
- **Symbolic prompt access** (`RLM_PROMPT_FILE`): agents can grep/sed the original prompt as a file instead of copying tokens from context memory
|
|
11
|
+
- **Contrib extensions**: `colgrep.ts` (semantic code search via ColBERT), `dirpack.ts` (repository index), `treemap.ts` (visual tree maps) — opt-in extensions in `contrib/extensions/`
|
|
12
|
+
- **Encryption workflow**: `scripts/encrypt-prose` and `scripts/decrypt-prose` for sops/age encryption of private execution state before pushing
|
|
13
|
+
- **`.sops.yaml`**: age encryption rules for `.prose/runs/`, `.prose/agents/`, `experiments/`, `private/`
|
|
14
|
+
- **`.githooks/pre-commit`**: safety net blocking unencrypted private files on direct git push
|
|
15
|
+
- **OpenProse programs**: `release.prose`, `land.prose`, `incorporate-insight.prose`, `recursive-development.prose`, `self-experiment.prose`, `check-upstream.prose`
|
|
16
|
+
- **Experiment infrastructure**: `experiments/` directory with pipe-vs-filename, session-sharing, and tree-awareness experiments with results
|
|
17
|
+
- E2E tests: expanded coverage (+90 lines), gemini-flash as default e2e model
|
|
18
|
+
- Guardrail tests: `rlm_sessions` tests (G48-G51), session sharing toggle
|
|
19
|
+
- Unit tests: `RLM_PROMPT_FILE` tests (T14d)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **SYSTEM_PROMPT.md**: added symbolic access principle (SECTION 2), refined depth awareness guidance
|
|
23
|
+
- **AGENTS.md**: expanded with experiment workflow (tmux rules), self-experimentation, session history reading, OpenProse program references
|
|
24
|
+
- **README.md**: updated feature list and project description
|
|
25
|
+
- Removed hardcoded provider/model defaults from `rlm_query` — inherits from environment only
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Kill orphan `rlm_parse_json` processes after timeout in E2E tests
|
|
29
|
+
- Contrib extension GitHub links (dirpack, colgrep) now point to correct URLs
|
|
30
|
+
|
|
6
31
|
## [0.3.0] - 2026-02-13
|
|
7
32
|
|
|
8
33
|
### Added
|
package/README.md
CHANGED
|
@@ -72,15 +72,12 @@ ypi --provider anthropic --model claude-sonnet-4-5-20250929 "What does this code
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
### How It Works
|
|
75
|
-
|
|
76
75
|
**Three pieces** (same architecture as Python RLM):
|
|
77
|
-
|
|
78
76
|
| Piece | Python RLM | ypi |
|
|
79
77
|
|---|---|---|
|
|
80
78
|
| System prompt | `RLM_SYSTEM_PROMPT` | `SYSTEM_PROMPT.md` |
|
|
81
79
|
| Context / REPL | Python `context` variable | `$CONTEXT` file + bash |
|
|
82
80
|
| Sub-call function | `llm_query("prompt")` | `rlm_query "prompt"` |
|
|
83
|
-
|
|
84
81
|
**Recursion:** `rlm_query` spawns a child Pi process with the same system prompt and tools. The child can call `rlm_query` too:
|
|
85
82
|
|
|
86
83
|
```
|
|
@@ -91,6 +88,18 @@ Depth 0 (root) → full Pi with bash + rlm_query
|
|
|
91
88
|
|
|
92
89
|
**File isolation with jj:** Each recursive child gets its own [jj workspace](https://martinvonz.github.io/jj/latest/working-copy/). The parent's working copy is untouched. Review child work with `jj diff -r <change-id>`, absorb with `jj squash --from <change-id>`.
|
|
93
90
|
|
|
91
|
+
### Why It Works
|
|
92
|
+
|
|
93
|
+
The design has three properties that compound:
|
|
94
|
+
|
|
95
|
+
1. **Self-similarity** — Every depth runs the same prompt, same tools, same agent. No specialized "scout" or "planner" roles. The intelligence is in *decomposition*, not specialization. The system prompt teaches one pattern — size-first → search → chunk → delegate → combine — and it works at every scale.
|
|
96
|
+
|
|
97
|
+
2. **Self-hosting** — The system prompt (SECTION 6) contains the full source of `rlm_query`. The agent reads its own recursion machinery. When it modifies `rlm_query`, it's modifying itself. This isn't a metaphor — it's the actual execution model.
|
|
98
|
+
|
|
99
|
+
3. **Bounded recursion** — Five concentric guardrails (depth limit, PATH scrubbing, call count, budget, timeout) guarantee termination. The system prompt also installs *cognitive* pressure: deeper agents are told to be more conservative, preferring direct action over spawning more children.
|
|
100
|
+
|
|
101
|
+
4. **Symbolic access** — Anything the agent needs to manipulate precisely is a file, not just tokens in context. `$CONTEXT` holds the data, `$RLM_PROMPT_FILE` holds the original prompt, and hashline provides line-addressed edits. Agents `grep`/`sed`/`cat` instead of copying tokens from memory.
|
|
102
|
+
|
|
94
103
|
### Guardrails
|
|
95
104
|
|
|
96
105
|
| Feature | Env var | What it does |
|
|
@@ -111,6 +120,21 @@ rlm_cost # "$0.042381"
|
|
|
111
120
|
rlm_cost --json # {"cost": 0.042381, "tokens": 12450, "calls": 3}
|
|
112
121
|
```
|
|
113
122
|
|
|
123
|
+
### Pi Compatibility
|
|
124
|
+
|
|
125
|
+
ypi is a thin layer on top of Pi. We strive not to break or duplicate what Pi already does:
|
|
126
|
+
|
|
127
|
+
| Pi feature | ypi behavior | Tests |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| **Session history** | Uses Pi's native `~/.pi/agent/sessions/` dir. Child sessions go in the same dir with trace-encoded filenames. No separate session store. | G24–G30 |
|
|
130
|
+
| **Extensions** | Passed through to Pi. Children inherit extensions by default. `RLM_EXTENSIONS=0` disables. | G34–G38 |
|
|
131
|
+
| **System prompt** | Built from `SYSTEM_PROMPT.md` + `rlm_query` source, written to a temp file, passed via `--system-prompt` (file path, never inlined as shell arg). | T8–T9 |
|
|
132
|
+
| **`-p` mode** | All child Pi calls run non-interactive (`-p`). ypi never fakes a terminal. | T3–T4 |
|
|
133
|
+
| **`--session` flag** | Used when `RLM_SESSION_DIR` is set; `--no-session` otherwise. Never both. | G24, G28 |
|
|
134
|
+
| **Provider/model** | Never hardcoded. ypi and `rlm_query` use Pi's defaults unless the user sets `RLM_PROVIDER`/`RLM_MODEL`. | T14, T14c |
|
|
135
|
+
|
|
136
|
+
If Pi changes how sessions or extensions work, our guardrail tests should catch it.
|
|
137
|
+
|
|
114
138
|
---
|
|
115
139
|
|
|
116
140
|
## Contributing
|
package/SYSTEM_PROMPT.md
CHANGED
|
@@ -7,85 +7,84 @@
|
|
|
7
7
|
- Sub‑agents inherit the same capabilities and receive their own isolated context.
|
|
8
8
|
- All actions should aim to be **deterministic and reproducible**.
|
|
9
9
|
|
|
10
|
-
## SECTION 2 –
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
## SECTION 2 – Recursive Decomposition
|
|
11
|
+
You solve problems by **decomposing them**: break big tasks into smaller ones, delegate to sub‑agents, combine results. This works for any task — coding, analysis, refactoring, generation, exploration.
|
|
12
|
+
|
|
13
|
+
Your original prompt is also available as a file at `$RLM_PROMPT_FILE` — use it when you need to manipulate the question programmatically (e.g., extracting exact strings, counting characters) rather than copying tokens from memory.
|
|
14
|
+
|
|
15
|
+
If a `$CONTEXT` file is set, it contains data relevant to your task. Treat it like any other file — read it, search it, chunk it.
|
|
16
|
+
|
|
17
|
+
**Core pattern: size up → search → delegate → combine**
|
|
18
|
+
1. **Size up the problem** – How big is it? Can you do it directly, or does it need decomposition? For files: `wc -l` / `wc -c`. For code tasks: how many files, how complex?
|
|
19
|
+
2. **Search & explore** – `grep`, `find`, `ls`, `head` — orient yourself before diving in.
|
|
20
|
+
3. **Delegate** – use `rlm_query` to hand sub‑tasks to child agents. Two patterns:
|
|
18
21
|
```bash
|
|
19
|
-
# Pipe
|
|
20
|
-
sed -n '100,200p'
|
|
22
|
+
# Pipe data as the child's context
|
|
23
|
+
sed -n '100,200p' bigfile.txt | rlm_query "Summarize this section"
|
|
21
24
|
|
|
22
|
-
#
|
|
23
|
-
rlm_query "
|
|
25
|
+
# Child inherits your environment (files, cwd, $CONTEXT)
|
|
26
|
+
rlm_query "Refactor the error handling in src/api.py"
|
|
24
27
|
```
|
|
25
|
-
|
|
28
|
+
4. **Combine** – aggregate results, deduplicate, resolve conflicts, produce the final output.
|
|
29
|
+
5. **Do it directly when it's small** – don't delegate what you can do in one step.
|
|
26
30
|
|
|
27
|
-
###
|
|
31
|
+
### Examples
|
|
28
32
|
|
|
29
|
-
**Example 1 –
|
|
33
|
+
**Example 1 – Small task, do it directly**
|
|
30
34
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
cat
|
|
34
|
-
# Now
|
|
35
|
+
# A 30-line file? Just read it and act.
|
|
36
|
+
wc -l src/config.py
|
|
37
|
+
cat src/config.py
|
|
38
|
+
# Now edit it directly — no need to delegate
|
|
35
39
|
```
|
|
36
40
|
|
|
37
|
-
**Example 2 –
|
|
41
|
+
**Example 2 – Multi-file refactor, delegate per file**
|
|
38
42
|
```bash
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
head -50 "$CONTEXT"
|
|
42
|
-
grep -n "Chapter" "$CONTEXT"
|
|
43
|
+
# Find all files that need updating
|
|
44
|
+
grep -rl "old_api_call" src/
|
|
43
45
|
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
+
# Delegate each file to a sub-agent (each gets its own jj workspace)
|
|
47
|
+
for f in $(grep -rl "old_api_call" src/); do
|
|
48
|
+
rlm_query "In $f, replace all old_api_call() with new_api_call(). Update the imports too."
|
|
49
|
+
done
|
|
46
50
|
```
|
|
47
51
|
|
|
48
|
-
**Example 3 –
|
|
52
|
+
**Example 3 – Large file analysis, chunk and search**
|
|
49
53
|
```bash
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
# Search for keywords first
|
|
55
|
-
grep -n "graduation\|degree\|university" "$CONTEXT"
|
|
54
|
+
# Too big to read at once — search first, then delegate relevant sections
|
|
55
|
+
wc -l data/logs.txt
|
|
56
|
+
grep -n "ERROR\|FATAL" data/logs.txt
|
|
56
57
|
|
|
57
|
-
# Delegate
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
# Delegate the interesting section
|
|
59
|
+
sed -n '480,600p' data/logs.txt | rlm_query "What caused this error? Suggest a fix."
|
|
60
|
+
```
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
**Example 4 – Parallel sub-tasks with different goals**
|
|
63
|
+
```bash
|
|
64
|
+
# Break a complex task into independent pieces
|
|
65
|
+
SUMMARY=$(rlm_query "Read README.md and summarize what this project does in one paragraph.")
|
|
66
|
+
ISSUES=$(rlm_query "Run the test suite and report any failures.")
|
|
67
|
+
DEPS=$(rlm_query "Check for outdated dependencies in package.json.")
|
|
68
|
+
|
|
69
|
+
# Combine into a report
|
|
70
|
+
echo "Summary: $SUMMARY"
|
|
71
|
+
echo "Test issues: $ISSUES"
|
|
72
|
+
echo "Dependency status: $DEPS"
|
|
64
73
|
```
|
|
65
74
|
|
|
66
|
-
**Example
|
|
75
|
+
**Example 5 – Iterative chunking over a huge file**
|
|
67
76
|
```bash
|
|
68
77
|
TOTAL=$(wc -l < "$CONTEXT")
|
|
69
78
|
CHUNK=500
|
|
70
79
|
for START in $(seq 1 $CHUNK $TOTAL); do
|
|
71
80
|
END=$((START + CHUNK - 1))
|
|
72
|
-
RESULT=$(sed -n "${START},${END}p" "$CONTEXT" | rlm_query "Extract any
|
|
81
|
+
RESULT=$(sed -n "${START},${END}p" "$CONTEXT" | rlm_query "Extract any TODO items. Return a numbered list, or 'none' if none found.")
|
|
73
82
|
if [ "$RESULT" != "none" ]; then
|
|
74
83
|
echo "Lines $START-$END: $RESULT"
|
|
75
84
|
fi
|
|
76
85
|
done
|
|
77
86
|
```
|
|
78
87
|
|
|
79
|
-
**Example 5 – Temporal reasoning with computation**
|
|
80
|
-
```bash
|
|
81
|
-
grep -n "started\|began\|finished\|completed" "$CONTEXT"
|
|
82
|
-
|
|
83
|
-
START_DATE=$(sed -n '300,500p' "$CONTEXT" | rlm_query "When exactly did the user start this project? Return ONLY the date in YYYY-MM-DD format.")
|
|
84
|
-
END_DATE=$(sed -n '2000,2200p' "$CONTEXT" | rlm_query "When exactly did the user finish this project? Return ONLY the date in YYYY-MM-DD format.")
|
|
85
|
-
|
|
86
|
-
python3 -c "from datetime import date; d1=date.fromisoformat('$START_DATE'); d2=date.fromisoformat('$END_DATE'); print((d2-d1).days, 'days')"
|
|
87
|
-
```
|
|
88
|
-
|
|
89
88
|
## SECTION 3 – Coding and File Editing
|
|
90
89
|
- You may be asked to **modify code, add files, or restructure the repository**.
|
|
91
90
|
- First, check whether you are inside a **jj workspace**:
|
|
@@ -107,16 +106,22 @@ python3 -c "from datetime import date; d1=date.fromisoformat('$START_DATE'); d2=
|
|
|
107
106
|
rlm_cost --json # {"cost": 0.042381, "tokens": 12450, "calls": 3}
|
|
108
107
|
```
|
|
109
108
|
Use this to decide whether to make more sub‑calls or work directly. If spend is high relative to the task, prefer direct Bash actions over spawning sub‑agents.
|
|
109
|
+
- **`rlm_sessions`** – view session logs from sibling and parent agents in the same recursive tree:
|
|
110
|
+
```bash
|
|
111
|
+
rlm_sessions --trace # list sessions from this call tree
|
|
112
|
+
rlm_sessions read <file> # read a session as clean transcript
|
|
113
|
+
rlm_sessions grep <pattern> # search across sessions
|
|
114
|
+
```
|
|
115
|
+
Available for debugging and reviewing what other agents in the tree have done.
|
|
110
116
|
- **Depth awareness** – at deeper `RLM_DEPTH` levels, prefer **direct actions** (e.g., file edits, single‑pass searches) over spawning many sub‑agents.
|
|
111
117
|
- Always **clean up temporary files** and respect `trap` handlers defined by the infrastructure.
|
|
112
118
|
|
|
113
|
-
## SECTION 5 – Rules
|
|
114
|
-
1. **
|
|
115
|
-
2. **Validate
|
|
116
|
-
3. **
|
|
117
|
-
4. **
|
|
118
|
-
5. **
|
|
119
|
-
6. **
|
|
120
|
-
7. **
|
|
121
|
-
8. **
|
|
122
|
-
9. **Safety** – never execute untrusted commands without explicit intent; rely on the provided tooling.
|
|
119
|
+
## SECTION 5 – Rules
|
|
120
|
+
1. **Size up first** – before delegating, check if the task is small enough to do directly. Read small files, edit simple things, answer obvious questions — don't over‑decompose.
|
|
121
|
+
2. **Validate sub‑agent output** – if a sub‑call returns unexpected output, re‑query or do it yourself; never guess.
|
|
122
|
+
3. **Computation over memorization** – use `python3`, `date`, `wc`, `grep -c` for counting, dates, and math. Don't eyeball it.
|
|
123
|
+
4. **Act, don't describe** – when instructed to edit code, write files, or make changes, **do it** immediately.
|
|
124
|
+
5. **Small, focused sub‑agents** – each `rlm_query` call should have a clear, bounded task. Keep the call count low.
|
|
125
|
+
6. **Depth preference** – deeper depths ⇒ fewer sub‑calls, more direct Bash actions.
|
|
126
|
+
7. **Say "I don't know" only when true** – only when the required information is genuinely absent from the context, repo, or environment.
|
|
127
|
+
8. **Safety** – never execute untrusted commands without explicit intent; rely on the provided tooling.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ypi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "ypi — a recursive coding agent. Pi that can call itself via rlm_query.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Raymond Weitekamp",
|
|
@@ -26,13 +26,15 @@
|
|
|
26
26
|
"ypi": "./ypi",
|
|
27
27
|
"rlm_query": "./rlm_query",
|
|
28
28
|
"rlm_cost": "./rlm_cost",
|
|
29
|
-
"rlm_parse_json": "./rlm_parse_json"
|
|
29
|
+
"rlm_parse_json": "./rlm_parse_json",
|
|
30
|
+
"rlm_sessions": "./rlm_sessions"
|
|
30
31
|
},
|
|
31
32
|
"files": [
|
|
32
33
|
"ypi",
|
|
33
34
|
"rlm_query",
|
|
34
35
|
"rlm_cost",
|
|
35
36
|
"rlm_parse_json",
|
|
37
|
+
"rlm_sessions",
|
|
36
38
|
"SYSTEM_PROMPT.md",
|
|
37
39
|
"install.sh",
|
|
38
40
|
"README.md",
|
package/rlm_query
CHANGED
|
@@ -72,8 +72,8 @@ if [ "$NEXT_DEPTH" -gt "$MAX_DEPTH" ]; then
|
|
|
72
72
|
exit 1
|
|
73
73
|
fi
|
|
74
74
|
|
|
75
|
-
PROVIDER="${RLM_PROVIDER:-
|
|
76
|
-
MODEL="${RLM_MODEL:-
|
|
75
|
+
PROVIDER="${RLM_PROVIDER:-}"
|
|
76
|
+
MODEL="${RLM_MODEL:-}"
|
|
77
77
|
SYSTEM_PROMPT_FILE="${RLM_SYSTEM_PROMPT:-}"
|
|
78
78
|
|
|
79
79
|
# ----------------------------------------------------------------------
|
|
@@ -152,6 +152,11 @@ fi
|
|
|
152
152
|
CHILD_CONTEXT=$(mktemp /tmp/rlm_ctx_d${NEXT_DEPTH}_XXXXXX.txt)
|
|
153
153
|
COMBINED_PROMPT=""
|
|
154
154
|
|
|
155
|
+
# Write prompt to a file for symbolic access — agents can grep/sed the original
|
|
156
|
+
# question instead of relying on in-context token copying.
|
|
157
|
+
PROMPT_FILE=$(mktemp /tmp/rlm_prompt_d${NEXT_DEPTH}_XXXXXX.txt)
|
|
158
|
+
echo "$PROMPT" > "$PROMPT_FILE"
|
|
159
|
+
|
|
155
160
|
# ----------------------------------------------------------------------
|
|
156
161
|
# jj workspace isolation — give recursive children their own working copy
|
|
157
162
|
# ----------------------------------------------------------------------
|
|
@@ -170,7 +175,7 @@ fi
|
|
|
170
175
|
|
|
171
176
|
# Cleanup: remove temp context + forget jj workspace (updated in run section below)
|
|
172
177
|
trap '{
|
|
173
|
-
rm -f "$CHILD_CONTEXT"
|
|
178
|
+
rm -f "$CHILD_CONTEXT" "$PROMPT_FILE"
|
|
174
179
|
rm -f "${COMBINED_PROMPT:-}"
|
|
175
180
|
if [ -n "$JJ_WS_NAME" ]; then
|
|
176
181
|
jj workspace forget "$JJ_WS_NAME" 2>/dev/null || true
|
|
@@ -210,6 +215,7 @@ fi
|
|
|
210
215
|
# Spawn child Pi with tools, extensions, and session
|
|
211
216
|
# ----------------------------------------------------------------------
|
|
212
217
|
export CONTEXT="$CHILD_CONTEXT"
|
|
218
|
+
export RLM_PROMPT_FILE="$PROMPT_FILE"
|
|
213
219
|
export RLM_DEPTH="$NEXT_DEPTH"
|
|
214
220
|
export RLM_MAX_DEPTH="$MAX_DEPTH"
|
|
215
221
|
export RLM_PROVIDER="$PROVIDER"
|
|
@@ -221,6 +227,7 @@ export RLM_TRACE_ID="${RLM_TRACE_ID}"
|
|
|
221
227
|
export RLM_SESSION_DIR="${RLM_SESSION_DIR:-}"
|
|
222
228
|
export RLM_BUDGET="${RLM_BUDGET:-}"
|
|
223
229
|
export RLM_COST_FILE="${RLM_COST_FILE:-}"
|
|
230
|
+
export RLM_SHARED_SESSIONS="${RLM_SHARED_SESSIONS:-1}"
|
|
224
231
|
|
|
225
232
|
# At max depth: remove rlm_query from PATH so the child can't recurse.
|
|
226
233
|
# The child still gets full tools (bash, read, write, edit) — it just
|
|
@@ -235,7 +242,9 @@ if [ -n "$CHILD_SESSION_FILE" ]; then
|
|
|
235
242
|
export RLM_SESSION_FILE="$CHILD_SESSION_FILE"
|
|
236
243
|
fi
|
|
237
244
|
|
|
238
|
-
CMD_ARGS=(-p
|
|
245
|
+
CMD_ARGS=(-p)
|
|
246
|
+
[ -n "$PROVIDER" ] && CMD_ARGS+=(--provider "$PROVIDER")
|
|
247
|
+
[ -n "$MODEL" ] && CMD_ARGS+=(--model "$MODEL")
|
|
239
248
|
|
|
240
249
|
# Extensions: on by default, configurable per-instance like model routing
|
|
241
250
|
CHILD_EXT="${RLM_EXTENSIONS:-1}"
|
|
@@ -298,7 +307,7 @@ fi
|
|
|
298
307
|
# ----------------------------------------------------------------------
|
|
299
308
|
COST_OUT=$(mktemp /tmp/rlm_cost_out_XXXXXX.json)
|
|
300
309
|
trap '{
|
|
301
|
-
rm -f "$CHILD_CONTEXT"
|
|
310
|
+
rm -f "$CHILD_CONTEXT" "$PROMPT_FILE"
|
|
302
311
|
rm -f "${COMBINED_PROMPT:-}"
|
|
303
312
|
rm -f "$COST_OUT"
|
|
304
313
|
if [ -n "$JJ_WS_NAME" ]; then
|
package/rlm_sessions
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# rlm_sessions — List and read Pi session logs for the current recursive tree.
|
|
3
|
+
#
|
|
4
|
+
# Sub-agents can use this to see what other agents have done — shared memory
|
|
5
|
+
# through session transcripts.
|
|
6
|
+
#
|
|
7
|
+
# Environment:
|
|
8
|
+
# RLM_SESSION_DIR — path to Pi session directory (set by ypi/rlm_query)
|
|
9
|
+
# RLM_TRACE_ID — current trace ID (filters to this recursive tree)
|
|
10
|
+
# RLM_SHARED_SESSIONS — set to "0" to disable (exit silently). Default: 1.
|
|
11
|
+
#
|
|
12
|
+
# Usage:
|
|
13
|
+
# rlm_sessions # List all sessions for this project
|
|
14
|
+
# rlm_sessions --trace # List only sessions from current trace
|
|
15
|
+
# rlm_sessions read <file> # Read a session as clean transcript
|
|
16
|
+
# rlm_sessions read --last # Read the most recent session
|
|
17
|
+
# rlm_sessions grep <pattern> # Search across all sessions
|
|
18
|
+
# rlm_sessions grep -t <pattern> # Search only current trace's sessions
|
|
19
|
+
|
|
20
|
+
set -euo pipefail
|
|
21
|
+
|
|
22
|
+
# Gate: disabled when RLM_SHARED_SESSIONS=0
|
|
23
|
+
if [ "${RLM_SHARED_SESSIONS:-1}" = "0" ]; then
|
|
24
|
+
echo "Session sharing disabled (RLM_SHARED_SESSIONS=0)." >&2
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
SESSION_DIR="${RLM_SESSION_DIR:-}"
|
|
29
|
+
TRACE_ID="${RLM_TRACE_ID:-}"
|
|
30
|
+
|
|
31
|
+
if [ -z "$SESSION_DIR" ] || [ ! -d "$SESSION_DIR" ]; then
|
|
32
|
+
echo "No session directory found." >&2
|
|
33
|
+
echo " RLM_SESSION_DIR=${SESSION_DIR:-<not set>}" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# ─── Helper: render a session JSONL to readable transcript ────────────────
|
|
38
|
+
|
|
39
|
+
render_session() {
|
|
40
|
+
local file="$1"
|
|
41
|
+
python3 -c "
|
|
42
|
+
import json, sys
|
|
43
|
+
|
|
44
|
+
with open('$file') as f:
|
|
45
|
+
for line in f:
|
|
46
|
+
r = json.loads(line)
|
|
47
|
+
|
|
48
|
+
# Session metadata
|
|
49
|
+
if r.get('type') == 'session':
|
|
50
|
+
ts = r.get('timestamp', '?')
|
|
51
|
+
cwd = r.get('cwd', '?')
|
|
52
|
+
print(f'=== Session: {ts} ===')
|
|
53
|
+
print(f' cwd: {cwd}')
|
|
54
|
+
print()
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
if r.get('type') != 'message':
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
msg = r['message']
|
|
61
|
+
role = msg.get('role', '?')
|
|
62
|
+
content = msg.get('content', '')
|
|
63
|
+
|
|
64
|
+
if role == 'toolResult':
|
|
65
|
+
tool = msg.get('toolName', '?')
|
|
66
|
+
if isinstance(content, list):
|
|
67
|
+
text = ''.join(p.get('text', '') for p in content if isinstance(p, dict))
|
|
68
|
+
else:
|
|
69
|
+
text = str(content)
|
|
70
|
+
# Truncate long tool results
|
|
71
|
+
if len(text) > 500:
|
|
72
|
+
text = text[:500] + '... [truncated]'
|
|
73
|
+
print(f'[{tool} result]: {text}')
|
|
74
|
+
print()
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if isinstance(content, str):
|
|
78
|
+
print(f'{role}: {content}')
|
|
79
|
+
print()
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# content is a list of parts
|
|
83
|
+
for part in content:
|
|
84
|
+
if not isinstance(part, dict):
|
|
85
|
+
continue
|
|
86
|
+
ptype = part.get('type', '')
|
|
87
|
+
if ptype == 'text':
|
|
88
|
+
text = part.get('text', '')
|
|
89
|
+
if len(text) > 1000:
|
|
90
|
+
text = text[:1000] + '... [truncated]'
|
|
91
|
+
print(f'{role}: {text}')
|
|
92
|
+
print()
|
|
93
|
+
elif ptype == 'toolCall':
|
|
94
|
+
name = part.get('name', '?')
|
|
95
|
+
args = part.get('arguments', {})
|
|
96
|
+
if name == 'bash':
|
|
97
|
+
cmd = args.get('command', '')
|
|
98
|
+
if len(cmd) > 200:
|
|
99
|
+
cmd = cmd[:200] + '...'
|
|
100
|
+
print(f'{role}: [bash] {cmd}')
|
|
101
|
+
else:
|
|
102
|
+
argstr = json.dumps(args)
|
|
103
|
+
if len(argstr) > 200:
|
|
104
|
+
argstr = argstr[:200] + '...'
|
|
105
|
+
print(f'{role}: [{name}] {argstr}')
|
|
106
|
+
print()
|
|
107
|
+
elif ptype == 'thinking':
|
|
108
|
+
# Skip thinking blocks — they're internal
|
|
109
|
+
pass
|
|
110
|
+
" 2>/dev/null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ─── Commands ─────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
case "${1:-list}" in
|
|
116
|
+
list|--trace)
|
|
117
|
+
FILTER=""
|
|
118
|
+
if [ "${1:-}" = "--trace" ] && [ -n "$TRACE_ID" ]; then
|
|
119
|
+
FILTER="$TRACE_ID"
|
|
120
|
+
echo "Sessions for trace $TRACE_ID:"
|
|
121
|
+
else
|
|
122
|
+
echo "All sessions in $SESSION_DIR:"
|
|
123
|
+
fi
|
|
124
|
+
echo ""
|
|
125
|
+
|
|
126
|
+
for f in "$SESSION_DIR"/*.jsonl; do
|
|
127
|
+
[ -f "$f" ] || continue
|
|
128
|
+
base=$(basename "$f")
|
|
129
|
+
|
|
130
|
+
# Filter by trace if requested
|
|
131
|
+
if [ -n "$FILTER" ] && [[ "$base" != "${FILTER}"* ]]; then
|
|
132
|
+
continue
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Get basic info
|
|
136
|
+
size=$(wc -c < "$f")
|
|
137
|
+
msgs=$(grep -c '"type":"message"' "$f" 2>/dev/null || echo 0)
|
|
138
|
+
ts=$(python3 -c "
|
|
139
|
+
import json
|
|
140
|
+
with open('$f') as fh:
|
|
141
|
+
r = json.loads(fh.readline())
|
|
142
|
+
print(r.get('timestamp', '?')[:19])
|
|
143
|
+
" 2>/dev/null || echo "?")
|
|
144
|
+
|
|
145
|
+
printf " %-50s %6s bytes %3s msgs %s\n" "$base" "$size" "$msgs" "$ts"
|
|
146
|
+
done
|
|
147
|
+
;;
|
|
148
|
+
|
|
149
|
+
read)
|
|
150
|
+
shift
|
|
151
|
+
if [ "${1:-}" = "--last" ]; then
|
|
152
|
+
FILE=$(ls -t "$SESSION_DIR"/*.jsonl 2>/dev/null | head -1)
|
|
153
|
+
if [ -z "$FILE" ]; then
|
|
154
|
+
echo "No sessions found." >&2
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
157
|
+
else
|
|
158
|
+
FILE="${1:?Usage: rlm_sessions read <file|--last>}"
|
|
159
|
+
# Allow bare filename (without path)
|
|
160
|
+
if [ ! -f "$FILE" ] && [ -f "$SESSION_DIR/$FILE" ]; then
|
|
161
|
+
FILE="$SESSION_DIR/$FILE"
|
|
162
|
+
fi
|
|
163
|
+
fi
|
|
164
|
+
render_session "$FILE"
|
|
165
|
+
;;
|
|
166
|
+
|
|
167
|
+
grep)
|
|
168
|
+
shift
|
|
169
|
+
TRACE_ONLY=false
|
|
170
|
+
if [ "${1:-}" = "-t" ]; then
|
|
171
|
+
TRACE_ONLY=true
|
|
172
|
+
shift
|
|
173
|
+
fi
|
|
174
|
+
PATTERN="${1:?Usage: rlm_sessions grep [-t] <pattern>}"
|
|
175
|
+
|
|
176
|
+
for f in "$SESSION_DIR"/*.jsonl; do
|
|
177
|
+
[ -f "$f" ] || continue
|
|
178
|
+
base=$(basename "$f")
|
|
179
|
+
|
|
180
|
+
if [ "$TRACE_ONLY" = true ] && [ -n "$TRACE_ID" ]; then
|
|
181
|
+
[[ "$base" == "${TRACE_ID}"* ]] || continue
|
|
182
|
+
fi
|
|
183
|
+
|
|
184
|
+
# Search in message text content
|
|
185
|
+
matches=$(python3 -c "
|
|
186
|
+
import json, re
|
|
187
|
+
pattern = re.compile(r'$PATTERN', re.IGNORECASE)
|
|
188
|
+
with open('$f') as fh:
|
|
189
|
+
for line in fh:
|
|
190
|
+
r = json.loads(line)
|
|
191
|
+
if r.get('type') != 'message': continue
|
|
192
|
+
msg = r['message']
|
|
193
|
+
content = msg.get('content', '')
|
|
194
|
+
if isinstance(content, str):
|
|
195
|
+
if pattern.search(content):
|
|
196
|
+
role = msg.get('role', '?')
|
|
197
|
+
match = content[:150]
|
|
198
|
+
print(f'{role}: {match}')
|
|
199
|
+
elif isinstance(content, list):
|
|
200
|
+
for part in content:
|
|
201
|
+
text = part.get('text', '') if isinstance(part, dict) else ''
|
|
202
|
+
if text and pattern.search(text):
|
|
203
|
+
role = msg.get('role', '?')
|
|
204
|
+
print(f'{role}: {text[:150]}')
|
|
205
|
+
" 2>/dev/null)
|
|
206
|
+
|
|
207
|
+
if [ -n "$matches" ]; then
|
|
208
|
+
echo "--- $base ---"
|
|
209
|
+
echo "$matches"
|
|
210
|
+
echo ""
|
|
211
|
+
fi
|
|
212
|
+
done
|
|
213
|
+
;;
|
|
214
|
+
|
|
215
|
+
*)
|
|
216
|
+
echo "Usage: rlm_sessions [list|--trace|read <file>|grep <pattern>]" >&2
|
|
217
|
+
exit 1
|
|
218
|
+
;;
|
|
219
|
+
esac
|
package/ypi
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
# ypi --provider anthropic --model claude-sonnet-4-5-20250929 "question"
|
|
12
12
|
#
|
|
13
13
|
# Environment overrides:
|
|
14
|
-
# RLM_PROVIDER — LLM provider for
|
|
15
|
-
# RLM_MODEL — LLM model for
|
|
14
|
+
# RLM_PROVIDER — LLM provider for sub-calls (default: Pi's default)
|
|
15
|
+
# RLM_MODEL — LLM model for sub-calls (default: Pi's default)
|
|
16
16
|
# RLM_MAX_DEPTH — max recursion depth (default: 3)
|
|
17
17
|
# RLM_TIMEOUT — wall-clock seconds for entire recursive tree (default: none)
|
|
18
18
|
# RLM_MAX_CALLS — max total rlm_query invocations (default: none)
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
# RLM_CHILD_EXTENSIONS — override extensions for depth > 0 (default: same as parent)
|
|
24
24
|
# RLM_BUDGET — max dollar spend for entire recursive tree (default: none)
|
|
25
25
|
# RLM_JSON — set to "0" to disable JSON mode / cost tracking (default: 1)
|
|
26
|
+
# RLM_SHARED_SESSIONS — set to "0" to disable session log sharing (default: 1)
|
|
26
27
|
# PI_TRACE_FILE — path to trace log for all calls with timing (default: none)
|
|
27
28
|
|
|
28
29
|
set -euo pipefail
|
|
@@ -37,8 +38,8 @@ export PATH="$SCRIPT_DIR:$PATH"
|
|
|
37
38
|
# Initialize RLM environment — pass through all guardrails
|
|
38
39
|
export RLM_DEPTH="${RLM_DEPTH:-0}"
|
|
39
40
|
export RLM_MAX_DEPTH="${RLM_MAX_DEPTH:-3}"
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
[ -n "${RLM_PROVIDER:-}" ] && export RLM_PROVIDER
|
|
42
|
+
[ -n "${RLM_MODEL:-}" ] && export RLM_MODEL
|
|
42
43
|
export RLM_SYSTEM_PROMPT="$SCRIPT_DIR/SYSTEM_PROMPT.md"
|
|
43
44
|
|
|
44
45
|
# Guardrails — pass through if set, don't override
|
|
@@ -52,6 +53,7 @@ export RLM_EXTENSIONS="${RLM_EXTENSIONS:-1}"
|
|
|
52
53
|
[ -n "${RLM_CHILD_EXTENSIONS:-}" ] && export RLM_CHILD_EXTENSIONS
|
|
53
54
|
[ -n "${RLM_BUDGET:-}" ] && export RLM_BUDGET
|
|
54
55
|
export RLM_JSON="${RLM_JSON:-1}"
|
|
56
|
+
export RLM_SHARED_SESSIONS="${RLM_SHARED_SESSIONS:-1}"
|
|
55
57
|
|
|
56
58
|
# Session tree tracing — generate a trace ID that links all recursive sessions
|
|
57
59
|
export RLM_TRACE_ID="${RLM_TRACE_ID:-$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')}"
|
|
@@ -88,6 +90,30 @@ if [ -f "$SCRIPT_DIR/extensions/ypi.ts" ]; then
|
|
|
88
90
|
YPI_EXT_ARGS=(-e "$SCRIPT_DIR/extensions/ypi.ts")
|
|
89
91
|
fi
|
|
90
92
|
|
|
93
|
+
# Parse --append-system-prompt from args so ypi works like pi with rp
|
|
94
|
+
# We append to the combined prompt file rather than passing through,
|
|
95
|
+
# since pi already gets --system-prompt from us.
|
|
96
|
+
PASS_ARGS=()
|
|
97
|
+
while [[ $# -gt 0 ]]; do
|
|
98
|
+
case "$1" in
|
|
99
|
+
--append-system-prompt)
|
|
100
|
+
shift
|
|
101
|
+
printf '\n%s\n' "$1" >> "$COMBINED_PROMPT"
|
|
102
|
+
shift
|
|
103
|
+
;;
|
|
104
|
+
--system-prompt)
|
|
105
|
+
# User overriding ypi's system prompt entirely
|
|
106
|
+
echo "⚠️ Overriding ypi's system prompt. Did you mean --append-system-prompt?" >&2
|
|
107
|
+
shift
|
|
108
|
+
cat "$1" > "$COMBINED_PROMPT" 2>/dev/null || echo "$1" > "$COMBINED_PROMPT"
|
|
109
|
+
shift
|
|
110
|
+
;;
|
|
111
|
+
*)
|
|
112
|
+
PASS_ARGS+=("$1")
|
|
113
|
+
shift
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
done
|
|
91
117
|
# Launch Pi with the combined system prompt, passing all args through
|
|
92
118
|
# User's own extensions (hashline, etc.) are discovered automatically by Pi.
|
|
93
|
-
exec pi --system-prompt "$COMBINED_PROMPT" "${YPI_EXT_ARGS[@]}" "
|
|
119
|
+
exec pi --system-prompt "$COMBINED_PROMPT" "${YPI_EXT_ARGS[@]}" "${PASS_ARGS[@]}"
|