Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 5 additions & 0 deletions responses_api_agents/apex_agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ model turn. MCP calls time out after 60 seconds. Long text output uses a 20,000-
tail excerpt when it exceeds a 24,000-token budget, estimated at four characters per token; Stirrup compresses image
blocks to about one megapixel when serializing them for the model.

Stirrup summarizes the conversation once it reaches 70% of the client's context window. Stirrup 0.1 reads that window
from the same `max_tokens` value it sends as `max_completion_tokens`, so by default summarization starts at 70% of
`max_output_tokens`. Set `context_window_tokens` (`apex_agent_context_window_tokens`) to the served model's context
length to summarize at 70% of the real window instead; requests keep `max_output_tokens` as their output cap.

`finish` is the only submission mechanism. A completed submission is rejected while any todo is pending or in
progress. A max-turn exit or `status="incomplete"` does not call the grader; its initial/final snapshots are still
saved under the configured artifact output directory for inspection.
Expand Down
5 changes: 5 additions & 0 deletions responses_api_agents/apex_agent/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ class ApexAgentConfig(BaseResponsesAPIAgentConfig):
edgar_user_agent: Optional[str]
max_turns: int = Field(gt=0, le=200)
max_output_tokens: int = Field(gt=0)
# Policy context window reported to Stirrup, which summarizes at 70% of it. Unset keeps Stirrup using
# max_output_tokens as the window.
context_window_tokens: Optional[int] = Field(default=None, gt=0)
supports_vision: bool
temperature: float = Field(ge=0.0)
top_p: float = Field(gt=0.0, le=1.0)
Expand Down Expand Up @@ -291,6 +294,8 @@ def _sandbox_spec(self, body: ApexAgentRunRequest, instruction: str) -> SandboxS
"foundry_services": body.foundry_services,
"edgar_user_agent": self.config.edgar_user_agent,
}
if self.config.context_window_tokens is not None:
runner_config["context_window_tokens"] = self.config.context_window_tokens
return SandboxSpec(
image=self._image or self.config.image,
workdir=_GUEST_ROOT,
Expand Down
3 changes: 3 additions & 0 deletions responses_api_agents/apex_agent/configs/apex_agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ apex_agent:
edgar_user_agent: ${oc.select:apex_edgar_user_agent,"Mercor APEX-Agents apex@mercor.com"}
max_turns: ${oc.select:apex_agent_max_turns,200}
max_output_tokens: ${oc.select:apex_agent_max_output_tokens,32768}
# Policy context window for Stirrup's context summarization, which starts at 70% of it. Unset keeps Stirrup
# using max_output_tokens as the window, i.e. summarizing at ~23k tokens with the default 32768.
context_window_tokens: ${oc.select:apex_agent_context_window_tokens,null}
supports_vision: ${oc.select:apex_agent_supports_vision,true}
temperature: ${oc.select:apex_agent_temperature,1.0}
top_p: ${oc.select:apex_agent_top_p,1.0}
Expand Down
24 changes: 22 additions & 2 deletions responses_api_agents/apex_agent/stirrup_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ def truncate_tool_text(text: str) -> str:
return text[:TOOL_OUTPUT_HEAD_CHARACTERS] + marker + text[-TOOL_OUTPUT_TAIL_CHARACTERS:]


def context_window_client(client_class: Any, context_window_tokens: int) -> Any:
"""Subclass a Stirrup client so context summarization uses the policy's real context window.

Stirrup 0.1 uses the client's ``max_tokens`` argument both as each request's ``max_completion_tokens`` and, via
the ``max_tokens`` property, as the context window whose 70% triggers summarization. The runner passes
``max_output_tokens`` there, so without this override summarization starts at ~70% of the output cap. The
subclass keeps the output cap on requests and changes only the property the agent reads.
"""

class ContextWindowClient(client_class):
@property
def max_tokens(self) -> int:
return context_window_tokens

return ContextWindowClient


def mcp_call_arguments(params: Any) -> dict[str, Any]:
"""Forward only concrete MCP arguments; omitted optional fields must stay omitted."""
return params.model_dump(exclude_none=True)
Expand Down Expand Up @@ -878,10 +895,13 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
"temperature": float(config["temperature"]),
"top_p": float(config["top_p"]),
}
client = ChatCompletionsClient(
client_class = ChatCompletionsClient
if config.get("context_window_tokens"):
client_class = context_window_client(ChatCompletionsClient, int(config["context_window_tokens"]))
client = client_class(
model=config["policy_model"],
base_url=config["model_base_url"],
api_key="unused",
api_key="unused", # pragma: allowlist secret
max_tokens=int(config["max_output_tokens"]),
kwargs=model_kwargs,
)
Expand Down
11 changes: 10 additions & 1 deletion responses_api_agents/apex_agent/tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def _agent(
image_build={
"enabled": auto_build,
"source_repo": "https://github.com/Mercor-Intelligence/archipelago.git",
"source_revision": "0cb5c476c219a9df637e0bd37fb86b2361f4ab89",
"source_revision": "0cb5c476c219a9df637e0bd37fb86b2361f4ab89", # pragma: allowlist secret
"source_root": None,
"source_github_token": None,
"dockerfile": "environment/Dockerfile",
Expand Down Expand Up @@ -240,6 +240,15 @@ def test_sandbox_config_never_contains_verifier_secrets() -> None:
assert "FOUNDRY_LOCAL_ROOT" not in spec.env


def test_sandbox_config_passes_the_policy_context_window_when_set() -> None:
agent = _agent()
agent.config.context_window_tokens = 262_144
runner = json.loads(agent._sandbox_spec(_body(), "Do the work").files["/app/apex-gym/runner_config.json"])

assert runner["context_window_tokens"] == 262_144
assert runner["max_output_tokens"] == 32_768


def test_sandbox_config_propagates_text_only_model_capability() -> None:
spec = _agent(supports_vision=False)._sandbox_spec(_body(), "Do the work")
runner = json.loads(spec.files["/app/apex-gym/runner_config.json"])
Expand Down
36 changes: 36 additions & 0 deletions responses_api_agents/apex_agent/tests/test_stirrup_runtime.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

import asyncio
import json
import zipfile
from inspect import getsource
from pathlib import Path
from types import SimpleNamespace

import pytest
from pydantic import BaseModel

from responses_api_agents.apex_agent import stirrup_runtime
Expand All @@ -18,6 +20,40 @@ def test_stirrup_repackages_image_tool_responses_for_chat_completions() -> None:
assert "text_only_tool_responses=True" in source


def test_context_window_client_reports_the_window_and_keeps_the_output_cap() -> None:
ChatCompletionsClient = pytest.importorskip("stirrup.clients.chat_completions_client").ChatCompletionsClient

client_class = stirrup_runtime.context_window_client(ChatCompletionsClient, 262_144)
client = client_class(
model="policy",
base_url="http://127.0.0.1:9/v1",
api_key="unused", # pragma: allowlist secret
max_tokens=32_768,
)
request: dict = {}

async def create(**kwargs):
request.update(kwargs)
raise RuntimeError("request captured")

client._client.chat.completions.create = create
with pytest.raises(RuntimeError, match="request captured"):
asyncio.run(client.generate([], {}))

assert isinstance(client, ChatCompletionsClient)
assert client.max_tokens == 262_144
assert request["max_completion_tokens"] == 32_768


def test_policy_client_uses_the_context_window_only_when_configured() -> None:
source = getsource(stirrup_runtime.run_stirrup_rollout)

assert 'if config.get("context_window_tokens"):' in source
assert 'context_window_client(ChatCompletionsClient, int(config["context_window_tokens"]))' in source
assert "client = client_class(" in source
assert 'max_tokens=int(config["max_output_tokens"])' in source


def test_text_only_model_replaces_tool_images_and_preserves_other_content() -> None:
class FakeImage:
pass
Expand Down
Loading