diff --git a/src/rlm/config.py b/src/rlm/config.py index c16597c..939b719 100644 --- a/src/rlm/config.py +++ b/src/rlm/config.py @@ -37,8 +37,13 @@ def _positive_int(value: str | int, name: str) -> int: def _summarize_at_tokens(value: str | int | None) -> int | None: + """Unset -> the 256k default; "" or "0" -> disabled; else a positive threshold.""" + if value is None: + return 256_000 + if value in ("", "0", 0): + return None parsed = _optional_positive_int(value, "summarize_at_tokens") - if value not in (None, "") and parsed is None: + if parsed is None: raise ValueError(f"summarize_at_tokens must be positive (got {value})") return parsed @@ -129,10 +134,10 @@ def child(self) -> InvocationContext: class ExecutionPolicy(_ConfigModel): """Resource and context-management policy for one RLM engine.""" - max_depth: int = Field(default=0, ge=0) + max_depth: int = Field(default=1, ge=0) exec_timeout: int = Field(default=300, gt=0) max_tokens: int | None = Field(default=None, gt=0) - summarize_at_tokens: int | None = Field(default=None, gt=0) + summarize_at_tokens: int | None = Field(default=256_000, gt=0) max_compactions: int | None = Field(default=None, gt=0) max_concurrent_subagents: int = Field(default=4, gt=0) max_subagent_calls: int = Field(default=64, gt=0) @@ -166,7 +171,7 @@ def from_env( ) -> RuntimeConfig: env = os.environ if environ is None else environ raw_skills = env.get("RLM_SKILLS") - max_depth = int(env.get("RLM_MAX_DEPTH", "0")) + max_depth = int(env.get("RLM_MAX_DEPTH", "1")) default_concurrency = max(4, max_depth) max_concurrent_subagents = _positive_int( env.get("RLM_MAX_CONCURRENT_SUBAGENTS", str(default_concurrency)), diff --git a/tests/test_acp.py b/tests/test_acp.py index 8baa467..48d4e82 100644 --- a/tests/test_acp.py +++ b/tests/test_acp.py @@ -176,7 +176,7 @@ async def test_engine_prompt_preserves_conversation(session): first = await engine.prompt("one") second = await engine.prompt("two") finally: - engine.close() + await engine.aclose() assert first.answer == "first" assert second.answer == "second" @@ -262,7 +262,7 @@ async def test_engine_prompt_preserves_ipython_kernel(session): await engine.prompt("remember a value") result = await engine.prompt("use that value") finally: - engine.close() + await engine.aclose() tool_messages = [ message for message in client.calls[-1]["messages"] if message["role"] == "tool" @@ -295,7 +295,7 @@ async def block_first_prompt(**kwargs): try: result = await engine.prompt("continue") finally: - engine.close() + await engine.aclose() assert result.answer == "continued" assert result.turns == 1 @@ -330,7 +330,7 @@ async def flaky_first_call(**kwargs): model="test-model", provider=ProviderConfig(base_url=None, api_key="test-key"), invocation=InvocationContext(), - policy=ExecutionPolicy(summarize_at_tokens=1), + policy=ExecutionPolicy(summarize_at_tokens=1, max_depth=0), ) engine = RLMEngine( client=client, # type: ignore[arg-type] @@ -341,7 +341,7 @@ async def flaky_first_call(**kwargs): try: result = await engine.prompt("compact") finally: - engine.close() + await engine.aclose() assert result.answer == "done" assert ( @@ -391,7 +391,7 @@ async def block_prompt(**kwargs): pending.cancel() with pytest.raises(asyncio.CancelledError): await pending - engine.close() + await engine.aclose() meta = json.loads((Path(session.dir) / "meta.json").read_text()) assert meta["status"] == "running" @@ -424,7 +424,7 @@ async def test_compaction_counts_seed_prompt(session): try: await engine._compact_branch(messages, turn=0, active_tools=[]) finally: - engine.close() + await engine.aclose() assert engine._metrics.num_compactions == 1 assert engine._metrics.compaction_chars_dropped_mean == len("original promptwork") @@ -445,7 +445,7 @@ async def test_engine_failed_prompt_can_be_retried(session): try: result = await engine.prompt("continue") finally: - engine.close() + await engine.aclose() assert result.answer == "continued" assert result.turns == 1 @@ -496,7 +496,7 @@ async def test_failed_prompt_restores_pre_compaction_context(session): try: result = await engine.prompt("continue") finally: - engine.close() + await engine.aclose() request_ids = [ call["extra_headers"]["X-ACP-Lineage-Request-ID"] for call in client.calls @@ -563,7 +563,7 @@ def shutdown(self): try: result = await engine.prompt("continue") finally: - engine.close() + await engine.aclose() assert result.answer == "continued" assert repl.finished is True @@ -614,7 +614,7 @@ async def test_engine_cancelled_tool_recovers_kernel(session, tmp_path): try: result = await engine.prompt("continue") finally: - engine.close() + await engine.aclose() tool_messages = [ message for message in client.calls[-1]["messages"] if message["role"] == "tool" diff --git a/tests/test_config.py b/tests/test_config.py index ce2b4da..2bdce53 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -64,3 +64,15 @@ def test_runtime_config_rejects_unsafe_recursive_and_environment_values(): provider.headers["Idempotency-Key"] = "forged-after-construction" with pytest.raises(ValueError, match="reserved names"): make_client(provider) + + +def test_default_policy_enables_compaction_and_recursion(): + config = RuntimeConfig.from_env(environ={}) + assert config.policy.summarize_at_tokens == 256_000 + assert config.policy.max_depth == 1 + + +def test_summarize_at_tokens_disabled_by_zero_or_empty(): + for raw in ("", "0"): + config = RuntimeConfig.from_env(environ={"RLM_SUMMARIZE_AT_TOKENS": raw}) + assert config.policy.summarize_at_tokens is None