Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions verifiers/v1/harnesses/bash/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@


class BashHarnessConfig(HarnessConfig):
compaction: bool = True
"""Compact the conversation at the model context limit. The harness asks the model for a
handoff summary, then continues from a fresh context that contains the summary."""

edit: bool = True
"""Offer the local `edit` tool (single-occurrence string replacement in a file) alongside
`bash`. On by default; set `--env.agent.harness.edit false` for a bash-only agent."""
Expand Down Expand Up @@ -77,6 +81,8 @@ async def launch(
]
if tool_interception_url:
args.append(f"--tool-interception-url={tool_interception_url}")
if self.config.compaction:
args.append("--compaction")
if self.config.edit:
args.append("--edit")
if self.config.search:
Expand Down
126 changes: 120 additions & 6 deletions verifiers/v1/harnesses/bash/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,40 @@
from pathlib import Path

import httpx
from openai import AsyncOpenAI
from openai import AsyncOpenAI, BadRequestError
from tenacity import AsyncRetrying, stop_after_attempt, wait_exponential_jitter

SERPER_URL = "https://google.serper.dev/search"

MCP_CALL_ATTEMPTS = 6
MCP_TIMEOUT = 600.0
CONTEXT_COMPACTION_HEADER = "X-Verifiers-Context-Compaction"

CHECKPOINT_COMPACTION_PROMPT = (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use """ """ here

"You are performing a CONTEXT CHECKPOINT COMPACTION. "
"Create a handoff summary for another LLM that will resume the task.\n"
"\n"
"Include:\n"
"- Current progress and key decisions made\n"
"- Important context, constraints, or user preferences\n"
"- What remains to be done (clear next steps)\n"
"- Any critical data, examples, or references needed to continue\n"
"\n"
"Be concise, structured, and focused on helping the next LLM "
"seamlessly continue the work."
)

POST_COMPACTION_FRAMING = (

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

"Another language model started to solve this problem and produced "
"a summary of its thinking process. Use this to build on the work "
"that has already been done and avoid duplicating work. Here is "
"the summary produced by the other language model, use the "
"information in this summary to assist with your own analysis:"
)

COMPACTED_TOOL_RESULT = (
"[tool output dropped because it exceeded the model context limit]"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just say "context limit" here

)


BASH_TOOL = {
Expand Down Expand Up @@ -178,14 +205,84 @@ def run_edit(path: str, old_str: str, new_str: str) -> str:


async def chat(
client: AsyncOpenAI, model: str, messages: list[dict], tools: list[dict]
client: AsyncOpenAI,
model: str,
messages: list[dict],
tools: list[dict],
*,
allow_compaction: bool = False,
tool_choice: str | None = None,
):
completion = await client.chat.completions.create(
model=model, messages=messages, tools=tools or None
)
kwargs = {"model": model, "messages": messages, "tools": tools or None}
if allow_compaction:
kwargs["extra_headers"] = {CONTEXT_COMPACTION_HEADER: "1"}
if tools and tool_choice is not None:
kwargs["tool_choice"] = tool_choice
completion = await client.chat.completions.create(**kwargs)
return completion.choices[0].message


def is_context_length_error(error: BadRequestError) -> bool:
details = f"{error} {error.body or ''}".casefold()
return "context_length" in details or "context length" in details


def drop_latest_tool_result(messages: list[dict]) -> bool:
"""Replace one tool result so the checkpoint request can fit in context."""
for index in range(len(messages) - 1, -1, -1):
message = messages[index]
if message.get("role") != "tool":
continue
if message.get("content") == COMPACTED_TOOL_RESULT:
continue
messages[index] = {**message, "content": COMPACTED_TOOL_RESULT}
return True
return False


def can_compact(messages: list[dict]) -> bool:
return any(
message.get("role") == "tool"
and message.get("content") != COMPACTED_TOOL_RESULT
for message in messages
)


async def compact(
client: AsyncOpenAI,
model: str,
messages: list[dict],
tools: list[dict],
) -> list[dict]:
"""Create a handoff summary after removing only the tool output needed to fit it."""
system_messages = [
message for message in messages if message.get("role") == "system"
]
while drop_latest_tool_result(messages):
checkpoint_messages = [
*messages,
{"role": "user", "content": CHECKPOINT_COMPACTION_PROMPT},
]
try:
summary = await chat(
client,
model,
checkpoint_messages,
tools,
allow_compaction=can_compact(messages),
tool_choice="none",
)
except BadRequestError as error:
if not is_context_length_error(error):
raise
if not can_compact(messages):
raise
continue
framed = POST_COMPACTION_FRAMING + "\n\n" + (summary.content or "")
return [*system_messages, {"role": "user", "content": framed}]
raise RuntimeError("context compaction could not make the checkpoint prompt fit")


@asynccontextmanager
async def mcp_session(spec: dict):
"""One fresh streamable-HTTP session to an MCP server, opened and closed within the caller's
Expand Down Expand Up @@ -328,6 +425,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--initial-messages-file", default="")
parser.add_argument("--mcp-config", default="")
parser.add_argument("--tool-interception-url", default="")
parser.add_argument("--compaction", action="store_true")
parser.add_argument("--edit", action="store_true")
parser.add_argument("--search", action="store_true")
parser.add_argument("--serper-key", default="")
Expand Down Expand Up @@ -373,7 +471,23 @@ async def main() -> None:
elif args.prompt:
messages.append({"role": "user", "content": args.prompt})
while True:
message = await chat(client, args.model, messages, tools)
try:
message = await chat(
client,
args.model,
messages,
tools,
allow_compaction=args.compaction and can_compact(messages),
)
except BadRequestError as error:
if (
not args.compaction
or not can_compact(messages)
or not is_context_length_error(error)
):
raise
messages = await compact(client, args.model, messages, tools)
continue
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
break
Expand Down
21 changes: 16 additions & 5 deletions verifiers/v1/interception/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
# Attempt counter the stainless-generated SDKs (OpenAI, Anthropic) send on every request:
# 0 on the first attempt, incremented on each retry of the same request.
RETRY_COUNT_HEADER = "x-stainless-retry-count"
CONTEXT_COMPACTION_HEADER = "X-Verifiers-Context-Compaction"
"""Internal opt-in for a harness that can recover from an overlong prompt."""
IDEMPOTENCY_KEY_HEADER = "Idempotency-Key"
IDEMPOTENCY_CACHE_TTL_SECONDS = 600
IDEMPOTENCY_CACHE_MAX_COMPLETED = 64
Expand Down Expand Up @@ -493,6 +495,12 @@ async def handle_request(
body = dialect.apply_overrides(body, session.ctx.model, session.ctx.sampling)
streaming = dialect.streaming(body)
upstream_headers = dict(request.headers)
context_compaction = request.headers.get(CONTEXT_COMPACTION_HEADER) == "1"
upstream_headers = {
name: value
for name, value in upstream_headers.items()
if name.lower() != CONTEXT_COMPACTION_HEADER.lower()
}
logger.debug(
"intercept %s: id=%s stream=%s",
request.path,
Expand Down Expand Up @@ -715,14 +723,17 @@ async def sample() -> web.Response:
status=400,
)
except OverlongPromptError as e:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why even catch this at all? isn't this doing rewriting of sorts?return web.json_response( dialect.error_body("context_length"), status=400, )

# An overlong prompt is a budget limit, not a crash: end the rollout
# cleanly as a truncation — refuse the call to halt the harness (same
# shape as `refused` above).
error = e
session.trace.stop("context_length")
if not context_compaction:
session.trace.stop("context_length")
logger.debug("prompt too long: id=%s", session.trace.id)
message = (
"context_length"
if context_compaction
else "rollout stopped: context_length"
)
return web.json_response(
dialect.error_body("rollout stopped: context_length"),
dialect.error_body(message),
status=400,
)
except RolloutError as e:
Expand Down