-
Notifications
You must be signed in to change notification settings - Fork 656
feat: add bash context compaction #2448
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
cf0ad84
b60b651
c1a13ee
4bb99de
1472123
43a0b6d
c5b1b2e
ff011aa
70e8723
5b48a26
f63d13c
dffe17d
db96ac1
daec369
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| """Context-compaction E2E scenarios for harness agent loops.""" | ||
|
|
||
| from typing import Literal | ||
|
|
||
| from pydantic import Field | ||
|
|
||
| import verifiers.v1 as vf | ||
|
|
||
|
|
||
| class OverflowToolsetConfig(vf.ToolsetConfig): | ||
| payload_chars: int = Field(65_536, gt=0) | ||
|
|
||
|
|
||
| class OverflowToolset(vf.Toolset[OverflowToolsetConfig]): | ||
| TOOL_PREFIX = "overflow" | ||
|
|
||
| def __init__(self, config: OverflowToolsetConfig): | ||
| super().__init__(config) | ||
| self.called = False | ||
|
|
||
| @vf.tool | ||
| def overflow_context(self) -> str: | ||
| """Return a payload that is intentionally larger than the model context.""" | ||
| if self.called: | ||
| return "The overflow already occurred. Answer `recovered` now." | ||
| self.called = True | ||
| block = "0123456789abcdef " | ||
| repeats = self.config.payload_chars // len(block) + 1 | ||
| return (block * repeats)[: self.config.payload_chars] | ||
|
|
||
|
|
||
| class ContextCompactionTaskConfig(vf.TaskConfig): | ||
| scenario: Literal["decode", "tool_result"] = "decode" | ||
| payload_chars: int = Field(65_536, gt=0) | ||
| tools: OverflowToolsetConfig = OverflowToolsetConfig() | ||
|
|
||
|
|
||
| class ContextCompactionTask( | ||
| vf.Task[vf.TaskData, vf.State, ContextCompactionTaskConfig] | ||
| ): | ||
| @classmethod | ||
| def toolsets(cls, config: ContextCompactionTaskConfig) -> list[vf.Toolset]: | ||
| if config.scenario != "tool_result": | ||
| return [] | ||
| tool_config = config.tools.model_copy( | ||
| update={"payload_chars": config.payload_chars} | ||
| ) | ||
| return [OverflowToolset(tool_config)] | ||
|
|
||
| @vf.reward | ||
| async def compacted(self, trace: vf.Trace) -> float: | ||
| return float(trace.num_branches > 1) | ||
|
|
||
|
|
||
| class ContextCompactionConfig(vf.TasksetConfig): | ||
| task: ContextCompactionTaskConfig = ContextCompactionTaskConfig() | ||
|
|
||
|
|
||
| class ContextCompactionTaskset( | ||
| vf.Taskset[ContextCompactionTask, ContextCompactionConfig] | ||
| ): | ||
| def load(self) -> list[ContextCompactionTask]: | ||
| if self.config.task.scenario == "decode": | ||
| prompt = ( | ||
| "Write `x ` repeatedly. Do not use tools and do not stop. " | ||
| "Continue until the model context ends the decode." | ||
| ) | ||
| else: | ||
| prompt = ( | ||
| "Call the `overflow_context` tool exactly once, then answer `recovered`. " | ||
| "In an RLM IPython session, call it with " | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dont mention rlm here
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bc we use this test for bash as well no? |
||
| "`result = await overflow_overflow_context(); print(result)`." | ||
| ) | ||
| return [ | ||
| ContextCompactionTask( | ||
| vf.TaskData(idx=0, prompt=prompt), | ||
| self.config.task, | ||
| ) | ||
| ] | ||
|
|
||
|
|
||
| __all__ = ["ContextCompactionTaskset"] | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| OverflowToolset(OverflowToolsetConfig()).run() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,12 @@ | |
| with distinct networking — instead of fanning the full cross product. prime/modal rows | ||
| are local-only (their marks are excluded in CI).""" | ||
|
|
||
| import os | ||
|
|
||
| import pytest | ||
|
|
||
| from verifiers.v1.utils.loaders import harness_config_type | ||
|
|
||
| mark = pytest.mark | ||
|
|
||
|
|
||
|
|
@@ -156,6 +160,66 @@ async def test_single_turn(run_v1, harness, harness_runtime, tmp_path): | |
| assert call.time.duration > 0 | ||
|
|
||
|
|
||
| @pytest.mark.e2e | ||
| @pytest.mark.compaction | ||
| @pytest.mark.docker | ||
| @pytest.mark.parametrize("scenario", ["decode", "tool_result"]) | ||
| @pytest.mark.parametrize("harness_id", ["bash", "rlm"]) | ||
| async def test_context_compaction_matrix(run_v1, scenario, harness_id, tmp_path): | ||
| """Both in-house loops recover when decoding or tool output fills context.""" | ||
| base_url = os.environ.get("VF_COMPACTION_E2E_BASE_URL") | ||
| model = os.environ.get("VF_COMPACTION_E2E_MODEL") | ||
| context_window = int(os.environ.get("VF_COMPACTION_E2E_CONTEXT_WINDOW", "4096")) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm no env vars wtf |
||
| if not base_url or not model: | ||
| pytest.skip("needs a local compaction E2E model") | ||
|
|
||
| harness = { | ||
| "id": harness_id, | ||
| "summarize_at_tokens": context_window, | ||
| **({"compaction": True} if harness_id == "bash" else {}), | ||
| } | ||
| sampling = ( | ||
| {"extra_body": {"ignore_eos": True}} | ||
| if scenario == "decode" | ||
| else {"temperature": 0.0} | ||
| ) | ||
| (trace,) = await run_v1( | ||
| "context-compaction-v1", | ||
| harness=harness_config_type(harness_id).model_validate(harness), | ||
| runtime={"type": "docker"}, | ||
| env={"agent": {"sampling": sampling, "max_output_tokens": None}}, | ||
| client={ | ||
| "type": "eval", | ||
| "base_url": base_url, | ||
| "api_key_var": "VF_COMPACTION_E2E_API_KEY", | ||
| }, | ||
| model=model, | ||
| max_tokens=None if scenario == "decode" else 512, | ||
| max_turns=8, | ||
| rollout_timeout=600, | ||
| taskset_overrides={ | ||
| "task": { | ||
| "scenario": scenario, | ||
| "payload_chars": context_window * 12, | ||
| } | ||
| }, | ||
| output_dir=tmp_path / f"{scenario}-{harness_id}", | ||
| ) | ||
|
|
||
| assert trace.ok, trace.errors | ||
| assert trace.num_branches > 1 | ||
| assert trace.rewards["compacted"].score == 1.0 | ||
| if scenario == "decode": | ||
| assert any(call.finish_reason == "length" for call in trace.calls) | ||
| else: | ||
| assert any( | ||
| call.error is not None and call.error.type == "OverlongPromptError" | ||
| for call in trace.calls | ||
| ) | ||
| if harness_id == "rlm": | ||
| assert trace.metrics["num_compactions"] >= 1 | ||
|
|
||
|
|
||
| @pytest.mark.e2e | ||
| @pytest.mark.browser_use | ||
| @pytest.mark.docker | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
drop the context_