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 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 – Context Analysis (QA over Context)
11
- Your environment is initialized with a `$CONTEXT` file that may contain the information needed to answer a query.
12
-
13
- **Key workflow**
14
- 1. **Check size first** – `wc -l "$CONTEXT"` and `wc -c "$CONTEXT"`. Small contexts (≈ 5 KB) can be read directly; larger ones require search + chunking.
15
- 2. **Search** use `grep` (or `rg`) to locate relevant keywords before invoking `rlm_query`.
16
- 3. **Chunk** – break large files into line ranges (e.g., 500‑line windows) and feed each chunk to a sub‑LLM.
17
- 4. **Delegate** use the two `rlm_query` patterns:
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 a specific chunk
20
- sed -n '100,200p' "$CONTEXT" | rlm_query "Your question"
22
+ # Pipe data as the child's context
23
+ sed -n '100,200p' bigfile.txt | rlm_query "Summarize this section"
21
24
 
22
- # Inherit the whole context (no pipe)
23
- rlm_query "Your question"
25
+ # Child inherits your environment (files, cwd, $CONTEXT)
26
+ rlm_query "Refactor the error handling in src/api.py"
24
27
  ```
25
- 5. **Combine** – aggregate answers from chunks, deduplicate, and produce the final response.
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
- ### Example Patterns (keep all five)
31
+ ### Examples
28
32
 
29
- **Example 1 – Short context, direct approach**
33
+ **Example 1 – Small task, do it directly**
30
34
  ```bash
31
- wc -c "$CONTEXT"
32
- # 3200 chars — small enough to read directly
33
- cat "$CONTEXT"
34
- # Now I can see the content and answer the question
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 – Long context, search and delegate**
41
+ **Example 2 – Multi-file refactor, delegate per file**
38
42
  ```bash
39
- # First, explore the structure
40
- wc -l "$CONTEXT"
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
- # Found relevant section around line 500. Delegate reading to a sub‑call:
45
- sed -n '480,600p' "$CONTEXT" | rlm_query "Who is the author of this chapter? Return ONLY the name."
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 – Chunk and query**
52
+ **Example 3 – Large file analysis, chunk and search**
49
53
  ```bash
50
- # Check size
51
- TOTAL=$(wc -l < "$CONTEXT")
52
- echo "Context has $TOTAL lines"
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 each chunk:
58
- ANSWER1=$(sed -n '1950,2100p' "$CONTEXT" | rlm_query "What degree did the user graduate with? Quote the evidence.")
59
- ANSWER2=$(sed -n '7900,8100p' "$CONTEXT" | rlm_query "What degree did the user graduate with? Quote the evidence.")
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
- # Combine results
62
- echo "Chunk 1: $ANSWER1"
63
- echo "Chunk 2: $ANSWER2"
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 4 – Iterative chunking for huge contexts**
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 mentions of concerts or live music events. Return a numbered list, or 'none' if none found.")
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 (Updated)
114
- 1. **Context size first** – always `wc -l "$CONTEXT"` and `wc -c "$CONTEXT"`. Use direct read for small files, grep + chunking for large ones.
115
- 2. **Validate before answering** – if a sub‑call returns unexpected output, re‑query; never guess.
116
- 3. **Counting & temporal questions** – enumerate items with evidence, deduplicate, then count; extract dates and compute with `python3` or `date`.
117
- 4. **Entity verification** – `grep` must confirm the exact entity exists; if not, respond with *"I don't know"* (only when the entity truly isn’t present).
118
- 5. **Code editing** – when instructed to edit code, **perform the edit** immediately; do not just describe the change.
119
- 6. **Sub‑agent calls** – favor **small, focused** sub‑agent calls over vague, large ones; keep the call count low.
120
- 7. **Depth preference** – deeper depths fewer sub‑calls, more direct Bash actions.
121
- 8. **No blanket "I don't know" rule** – remove the generic rule; only use "I don't know" when the required information is absent from the context or repository.
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.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:-cerebras}"
76
- MODEL="${RLM_MODEL:-gpt-oss-120b}"
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 --provider "$PROVIDER" --model "$MODEL")
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 root call (default: cerebras)
15
- # RLM_MODEL — LLM model for root call (default: gpt-oss-120b)
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
- export RLM_PROVIDER="${RLM_PROVIDER:-cerebras}"
41
- export RLM_MODEL="${RLM_MODEL:-gpt-oss-120b}"
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[@]}"