Skip to content

Commit c577f78

Browse files
committed
updated compaction logic
1 parent bc1bd6c commit c577f78

4 files changed

Lines changed: 137 additions & 47 deletions

File tree

‎examples/compaction_example.py‎

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
"""
22
Compaction example: run RLM with compaction enabled and a low threshold
3-
to trigger summarization on gpt-5-nano.
3+
to trigger summarization.
44
5-
Uses a low compaction_threshold_pct so compaction runs after several iterations.
6-
The task forces many separate REPL turns with substantial output so the root
7-
context grows and compaction definitely triggers. The REPL variable `history`
8-
holds trajectory segments and summaries.
5+
The task uses random data so earlier results are impossible to recompute —
6+
the model *must* recover them from `history` after compaction fires.
7+
8+
Phase 1: Generate random datasets and compute statistics (fills context).
9+
Phase 2: Compaction fires, summarizing the conversation.
10+
Phase 3: Combine the earlier statistics into a final answer. Since the data
11+
was random, the model cannot just re-derive the numbers.
12+
13+
Usage:
14+
PORTKEY_API_KEY=... python examples/compaction_example.py
915
"""
1016

1117
import os
@@ -17,9 +23,9 @@
1723

1824
load_dotenv()
1925

20-
# Low threshold so compaction triggers after a few iterations (~2% of context).
26+
# Low threshold so compaction triggers after a few iterations.
2127
# Use 0.85 in production.
22-
COMPACTION_THRESHOLD_PCT = 0.03
28+
COMPACTION_THRESHOLD_PCT = 0.02
2329

2430
logger = RLMLogger()
2531
rlm = RLM(
@@ -31,35 +37,32 @@
3137
environment="local",
3238
environment_kwargs={},
3339
max_depth=1,
34-
max_iterations=10,
40+
max_iterations=12,
3541
compaction=True,
3642
compaction_threshold_pct=COMPACTION_THRESHOLD_PCT,
3743
verbose=True,
3844
logger=logger,
3945
)
4046

41-
# Hard task that forces many iterations: find the 50th prime using at least 8
42-
# separate REPL blocks, one per turn. Each block must produce visible output.
43-
# This grows message history (long reasoning + code + execution results) so
44-
# compaction triggers.
4547
prompt = (
46-
"Find the 50th prime number. You MUST use at least 8 separate REPL blocks, "
47-
"each in its own response (one block per message). Do NOT combine steps.\n\n"
48-
"Required structure: "
49-
"Block 1: Define a function is_prime(n) and test it on a few numbers; print the results. "
50-
"Block 2: Write a loop that counts primes and print the first 10 primes. "
51-
"Block 3: Extend to count up to 20 primes and print them. "
52-
"Block 4: Count up to 30 primes and print the 25th. "
53-
"Block 5: Count up to 40 primes and print the 35th. "
54-
"Block 6: Count up to 50 primes and print the 45th. "
55-
"Block 7: Print the full list of the first 50 primes (so we see all 50). "
56-
"Block 8: Set answer = (the 50th prime) and call FINAL_VAR(answer).\n\n"
57-
"Each block must run alone. Show your reasoning briefly before each block. "
58-
"After each block, wait for the execution result before writing the next block."
48+
"Complete the following steps, each in its own REPL block (one block per message). "
49+
"Do NOT combine steps.\n\n"
50+
"Step 1: Use `import random; random.seed(42)` then generate a list of 50 random "
51+
"integers between 1 and 1000. Print ALL of them. Store as `data_a`.\n\n"
52+
"Step 2: Generate another 50 random integers (continuing the same RNG stream) "
53+
"between 1 and 1000. Print ALL of them. Store as `data_b`.\n\n"
54+
"Step 3: Compute and print the mean, median, min, max, and standard deviation "
55+
"of `data_a`. Store the mean as `mean_a`.\n\n"
56+
"Step 4: Compute and print the mean, median, min, max, and standard deviation "
57+
"of `data_b`. Store the mean as `mean_b`.\n\n"
58+
"Step 5: You previously computed `mean_a` and `mean_b`. "
59+
"Compute final_answer = round(mean_a + mean_b, 2) and call FINAL_VAR(final_answer)."
5960
)
6061

6162
result = rlm.completion(prompt, root_prompt=prompt)
6263

63-
print("Response:", result.response)
64-
print("Execution time:", result.execution_time)
65-
print("Metadata:", result.metadata)
64+
print("\n" + "=" * 60)
65+
print("RESULT")
66+
print("=" * 60)
67+
print(f"Response: {result.response}")
68+
print(f"Execution time: {result.execution_time:.2f}s")
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Compaction + history retrieval example.
3+
4+
Like compaction_example.py but with more intermediate results to recover.
5+
Uses random data across 4 groups so none of the statistics are re-derivable
6+
after compaction — the model must look in `history` to find them.
7+
8+
Usage:
9+
PORTKEY_API_KEY=... python examples/compaction_history_retrieval_example.py
10+
"""
11+
12+
import os
13+
14+
from dotenv import load_dotenv
15+
16+
from rlm import RLM
17+
from rlm.logger import RLMLogger
18+
19+
load_dotenv()
20+
21+
# Very low threshold so compaction fires after the first few steps.
22+
COMPACTION_THRESHOLD_PCT = 0.02
23+
24+
logger = RLMLogger()
25+
rlm = RLM(
26+
backend="portkey",
27+
backend_kwargs={
28+
"model_name": "@openai/gpt-5-nano",
29+
"api_key": os.getenv("PORTKEY_API_KEY"),
30+
},
31+
environment="local",
32+
environment_kwargs={},
33+
max_depth=1,
34+
max_iterations=15,
35+
compaction=True,
36+
compaction_threshold_pct=COMPACTION_THRESHOLD_PCT,
37+
verbose=True,
38+
logger=logger,
39+
)
40+
41+
prompt = (
42+
"Complete the following steps, each in its own REPL block (one block per message). "
43+
"Do NOT combine steps.\n\n"
44+
"Step 1: `import random; random.seed(99)`. Generate 40 random floats in [0, 100). "
45+
"Print all of them. Store as `group_1`.\n\n"
46+
"Step 2: Generate another 40 random floats (same RNG stream). Print all. "
47+
"Store as `group_2`.\n\n"
48+
"Step 3: Generate another 40 random floats. Print all. Store as `group_3`.\n\n"
49+
"Step 4: Generate another 40 random floats. Print all. Store as `group_4`.\n\n"
50+
"Step 5: Compute the mean of each group. Print all four means. "
51+
"Store as `m1`, `m2`, `m3`, `m4`.\n\n"
52+
"Step 6: Compute the standard deviation of each group. Print all four. "
53+
"Store as `s1`, `s2`, `s3`, `s4`.\n\n"
54+
"Step 7: You previously computed four means and four standard deviations. "
55+
"Compute final_answer = round((m1 + m2 + m3 + m4) / (s1 + s2 + s3 + s4), 4). "
56+
"Print the full equation with values and call FINAL_VAR(final_answer)."
57+
)
58+
59+
result = rlm.completion(prompt, root_prompt=prompt)
60+
61+
print("\n" + "=" * 60)
62+
print("RESULT")
63+
print("=" * 60)
64+
print(f"Response: {result.response}")
65+
print(f"Execution time: {result.execution_time:.2f}s")

‎rlm/core/rlm.py‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ def completion(
301301
with self._spawn_completion_context(prompt) as (lm_handler, environment):
302302
message_history = self._setup_prompt(prompt)
303303

304+
compaction_count = 0
304305
try:
305306
for i in range(self.max_iterations):
306307
# Check timeout before each iteration
@@ -315,9 +316,10 @@ def completion(
315316
current_tokens, threshold_tokens, max_tokens
316317
)
317318
if current_tokens >= threshold_tokens:
319+
compaction_count += 1
318320
self.verbose.print_compaction()
319321
message_history = self._compact_history(
320-
lm_handler, environment, message_history
322+
lm_handler, environment, message_history, compaction_count
321323
)
322324

323325
# Current prompt = message history + additional prompt suffix
@@ -541,6 +543,7 @@ def _compact_history(
541543
lm_handler: LMHandler,
542544
environment: BaseEnv,
543545
message_history: list[dict[str, Any]],
546+
compaction_count: int = 1,
544547
) -> list[dict[str, Any]]:
545548
"""
546549
Summarize current trajectory, append summary to REPL history, and return
@@ -549,7 +552,15 @@ def _compact_history(
549552
summary_prompt = message_history + [
550553
{
551554
"role": "user",
552-
"content": "Very concisely summarize what you have been doing so far in 1–3 short paragraphs. Be extremely brief. This summary will be used to continue the conversation.",
555+
"content": (
556+
"Summarize your progress so far. Include:\n"
557+
"1. Which steps/sub-tasks you have completed and which remain.\n"
558+
"2. Any concrete intermediate results (numbers, values, variable names) "
559+
"you computed — preserve these exactly.\n"
560+
"3. What your next action should be.\n"
561+
"Be concise (1–3 paragraphs) but preserve all key results and your "
562+
"current position in the task."
563+
),
553564
}
554565
]
555566
summary = lm_handler.completion(summary_prompt)
@@ -560,7 +571,13 @@ def _compact_history(
560571
{"role": "assistant", "content": summary},
561572
{
562573
"role": "user",
563-
"content": "Continue from the above summary. The full history (including this summary) is in the REPL variable `history`. Your next action:",
574+
"content": (
575+
f"Your conversation has been compacted {compaction_count} time(s). "
576+
"Continue from the above summary. Do NOT repeat work you have already "
577+
"completed. Use SHOW_VARS() to check which REPL variables exist, "
578+
"and check `history` for full context. "
579+
"Your next action:"
580+
),
564581
},
565582
]
566583
return new_history

‎rlm/utils/prompts.py‎

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@
2121
- Use `llm_query` for simple, one-shot tasks: extracting info from a chunk, summarizing text, answering a factual question, classifying content. These are fast single LLM calls.
2222
- Use `rlm_query` when the subtask itself requires deeper thinking: multi-step reasoning, solving a sub-problem that needs its own REPL and iteration, or tasks where a single LLM call might not be enough. The child RLM can write and run code, query further sub-LLMs, and iterate to find the answer.
2323
24+
**Breaking down problems:** You must break problems into more digestible components—whether that means chunking or summarizing a large context, or decomposing a hard task into easier sub-problems and delegating them via `llm_query` / `rlm_query`. Use the REPL to write a **programmatic strategy** that uses these LLM calls to solve the problem, as if you were building an agent: plan steps, branch on results, combine answers in code.
25+
2426
You will only be able to see truncated outputs from the REPL environment, so you should use the query LLM function on variables you want to analyze. You will find this function especially useful when you have to analyze the semantics of the context. Use these variables as buffers to build up your final answer.
25-
Make sure to explicitly look through the entire context in REPL before answering your query. An example strategy is to first look at the context and figure out a chunking strategy, then break up the context into smart chunks, and query an LLM per chunk with a particular question and save the answers to a buffer, then query an LLM with all the buffers to produce your final answer.
27+
Make sure to explicitly look through the entire context in REPL before answering your query. Break the context and the problem into digestible pieces: e.g. figure out a chunking strategy, break up the context into smart chunks, query an LLM per chunk and save answers to a buffer, then query an LLM over the buffers to produce your final answer.
2628
2729
You can use the REPL environment to help you understand your context, especially if it is huge. Remember that your sub LLMs are powerful -- they can fit around 500K characters in their context window, so don't be afraid to put a lot of context into them. For example, a viable strategy is to feed 10 documents per sub-LLM query. Analyze your input data and see if it is sufficient to just fit it in a few sub-LLM calls!
2830
@@ -66,25 +68,28 @@
6668
final_answer = llm_query(f"Aggregating all the answers per chunk, answer the original query about total number of jobs: {{query}}\\n\\nAnswers:\\n" + "\\n".join(answers))
6769
```
6870
69-
For subtasks that require deeper reasoning (e.g. solving a complex sub-problem), use `rlm_query` instead:
71+
For subtasks that require deeper reasoning (e.g. solving a complex sub-problem), use `rlm_query` instead. The child gets its own REPL to iterate; you can then use the result in parent logic:
7072
```repl
71-
# The child RLM gets its own REPL and can iterate to solve this sub-problem
72-
hard_answer = rlm_query(f"Analyze this complex dataset and determine the trend: {{data}}")
73-
print(hard_answer)
73+
# Child RLM solves the sub-problem in its own REPL; we use the result in code
74+
trend = rlm_query(f"Analyze this dataset and conclude with one word: up, down, or stable: {{data}}")
75+
if "up" in trend.lower():
76+
recommendation = "Consider increasing exposure."
77+
elif "down" in trend.lower():
78+
recommendation = "Consider hedging."
79+
else:
80+
recommendation = "Hold position."
81+
final_answer = llm_query(f"Given trend={{trend}} and recommendation={{recommendation}}, one-sentence summary for the user.")
7482
```
7583
76-
As a final example, after analyzing the context and realizing its separated by Markdown headers, we can maintain state through buffers by chunking the context by headers, and iteratively querying an LLM over it:
84+
As a final example, implement the solution as a **program**: try one approach via `rlm_query`; inspect the result and branch. If it suffices, use it. If not, break into one easier subproblem and delegate that only. More branches, one path runs—don't load the model. Example: prove √2 irrational.
7785
```repl
78-
# After finding out the context is separated by Markdown headers, we can chunk, summarize, and answer
79-
import re
80-
sections = re.split(r'### (.+)', context["content"])
81-
buffers = []
82-
for i in range(1, len(sections), 2):
83-
header = sections[i]
84-
info = sections[i+1]
85-
summary = llm_query(f"Summarize this {{header}} section: {{info}}")
86-
buffers.append(f"{{header}}: {{summary}}")
87-
final_answer = llm_query(f"Based on these summaries, answer the original query: {{query}}\\n\\nSummaries:\\n" + "\\n".join(buffers))
86+
r = rlm_query("Prove √2 irrational. Give a 1-2 sentence proof, or reply only: USE_LEMMA or USE_CONTRADICTION.")
87+
if "USE_LEMMA" in r.upper():
88+
final_answer = rlm_query("Prove 'n^2 even => n even' then use it to show √2 irrational. Two sentences.")
89+
elif "USE_CONTRADICTION" in r.upper():
90+
final_answer = rlm_query("Prove √2 irrational by contradiction. Two sentences.")
91+
else:
92+
final_answer = r
8893
```
8994
In the next step, we can return FINAL_VAR(final_answer).
9095

0 commit comments

Comments
 (0)