Skip to content

Commit 7807b08

Browse files
authored
Merge pull request #1 from popuku-dog/feat/configurable-max-output-chars
feat(config): expose dspy.RLM max_output_chars as a budget knob
2 parents f81509c + 331f79f commit 7807b08

5 files changed

Lines changed: 32 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@ All notable changes to `rlm-kit`. Format loosely follows
44
[Keep a Changelog](https://keepachangelog.com/). Versions track
55
`rlm_kit/__init__.__version__` and `pyproject.toml` (kept in sync).
66

7+
## [Unreleased]
8+
9+
### Added
10+
11+
- **`max_output_chars` is now configurable** (`RLMConfig.max_output_chars`, env
12+
`RLM_MAX_OUTPUT_CHARS`, default `10000` — dspy's own default, so behaviour is
13+
unchanged). dspy.RLM head+tail-truncates each REPL output to this many CHARACTERS
14+
before it enters the planner prompt — the planner never sees the omitted middle.
15+
Previously the knob was pinned at dspy's default; now it rides the same best-effort
16+
passthrough as `max_iterations` / `max_llm_calls`. (Distinct from `max_tokens`,
17+
which caps the model's own generation.)
18+
719
## [0.2.0] — 2026-06-21
820

921
Harness-engineering layer, plus the first round of hardening surfaced by

rlm_kit/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ class RLMConfig:
9797
max_iterations: int = 10
9898
max_llm_calls: int = 30
9999

100+
# Head+tail cap (in CHARACTERS — unrelated to ``max_tokens``) that dspy.RLM applies to each
101+
# REPL output before it enters the planner prompt; the planner never sees the omitted middle.
102+
# Default matches dspy's own. Raise it when the planner must read large printed results whole,
103+
# but prefer slicing/summarising in REPL code — retained chars cost prompt tokens every turn.
104+
max_output_chars: int = 10_000
105+
100106
# Retry policy in _retry.py: how many times to run the WHOLE task (a full RLM trajectory) until
101107
# its output coerces into output_model. Default 1 = no retry, because a retry re-runs the entire
102108
# RLM from scratch — silently MULTIPLYING the max_iterations budget (3 retries ⇒ up to 3×
@@ -158,6 +164,8 @@ def from_env(cls) -> "RLMConfig":
158164
- ``RLM_ALLOW_INSECURE_SANDBOX`` (default ``false``).
159165
- ``RLM_MAX_ITERATIONS`` (default ``10``).
160166
- ``RLM_MAX_LLM_CALLS`` (default ``30``).
167+
- ``RLM_MAX_OUTPUT_CHARS`` (default ``10000``) — head+tail character cap on REPL
168+
output fed back to the planner (distinct from ``RLM_MAX_TOKENS``).
161169
- ``RLM_MAX_RETRIES`` (default ``3``).
162170
- ``RLM_OBSERVE`` (default ``false``).
163171
"""
@@ -183,6 +191,7 @@ def from_env(cls) -> "RLMConfig":
183191
allow_insecure_sandbox=_env_bool("RLM_ALLOW_INSECURE_SANDBOX", False),
184192
max_iterations=_env_int("RLM_MAX_ITERATIONS", 10),
185193
max_llm_calls=_env_int("RLM_MAX_LLM_CALLS", 30),
194+
max_output_chars=_env_int("RLM_MAX_OUTPUT_CHARS", 10_000),
186195
max_retries=_env_int("RLM_MAX_RETRIES", 1),
187196
observe=_env_bool("RLM_OBSERVE", False),
188197
)

rlm_kit/task.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,11 @@ def _build_rlm(self) -> "dspy.RLM":
157157

158158
# Budget controls are passed best-effort: dspy's exact kwarg names have
159159
# shifted across releases, so tolerate their absence rather than crash.
160+
# (All-or-nothing: one unknown kwarg drops the whole dict to dspy defaults.)
160161
budget = {
161162
"max_iterations": self._config.max_iterations,
162163
"max_llm_calls": self._config.max_llm_calls,
164+
"max_output_chars": self._config.max_output_chars,
163165
}
164166
try:
165167
return dspy.RLM(signature, **kwargs, **budget)

tests/test_config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"RLM_ALLOW_INSECURE_SANDBOX",
1818
"RLM_MAX_ITERATIONS",
1919
"RLM_MAX_LLM_CALLS",
20+
"RLM_MAX_OUTPUT_CHARS",
2021
"RLM_MAX_RETRIES",
2122
"RLM_OBSERVE",
2223
]
@@ -40,6 +41,7 @@ def test_defaults():
4041
assert cfg.max_retries == 1 # default: no whole-RLM retry (it would multiply the iteration budget)
4142
assert cfg.max_iterations == 10
4243
assert cfg.max_llm_calls == 30
44+
assert cfg.max_output_chars == 10_000 # matches dspy.RLM's own default
4345

4446

4547
def test_sub_model_defaults_to_main(monkeypatch):
@@ -106,6 +108,12 @@ def test_max_tokens_from_env(monkeypatch):
106108
assert RLMConfig.from_env().max_tokens == 4096 # explicit override
107109

108110

111+
def test_max_output_chars_from_env(monkeypatch):
112+
assert RLMConfig.from_env().max_output_chars == 10_000 # unset → dspy's default
113+
monkeypatch.setenv("RLM_MAX_OUTPUT_CHARS", "50000")
114+
assert RLMConfig.from_env().max_output_chars == 50_000 # explicit override
115+
116+
109117
def test_unknown_adapter_rejected():
110118
with pytest.raises(ValueError):
111119
RLMConfig(main_model="m", sub_model="m", adapter="bogus")

tests/test_integration_dspy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class T(RLMTask):
5151
# Budget kwargs were accepted by the real constructor (no TypeError fallback).
5252
assert rlm.max_iterations == 10
5353
assert rlm.max_llm_calls == 30
54+
assert rlm.max_output_chars == 10_000
5455

5556

5657
def test_custom_output_type_resolves_without_frame_help():

0 commit comments

Comments
 (0)