Skip to content
Merged
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
19 changes: 19 additions & 0 deletions docs/v1/harbor.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ resource_multiplier = 2.0

The `timeout_multiplier` multiplies both the agent and verifier timeout, while the `resource_multiplier` multiplies the task's CPU, memory and disk space. You might want to use these multipliers when the tasks set too tight limits and/or the agent is slow.

## Rewards

`reward.json` takes precedence over `reward.txt`. An existing JSON file must contain
an object of finite numeric values; invalid, unreadable, or oversized JSON fails
grading without falling back to text. When JSON is absent, `reward.txt` supplies
the numeric `reward` value. A verifier's nonzero exit does not override a valid
reward file.

The `reward` key supplies the scalar reward by default. To select another key:

```toml
[env.taskset.task]
reward_key = "accuracy"
```

Other named values are retained as trace metrics. A missing selected key fails
grading; named values are never implicitly summed. The same selection applies to
shared and separate verifiers.

## Docker Compose

Select `runtime.type = "docker"` to run Compose tasks locally.
Expand Down
78 changes: 48 additions & 30 deletions verifiers/v1/tasksets/harbor/taskset.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
from collections.abc import Iterator
from functools import lru_cache
from pathlib import Path, PurePosixPath
from typing import Annotated

from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, ValidationError

Expand All @@ -50,12 +49,18 @@
REWARD_JSON = "/logs/verifier/reward.json"
MAX_REWARD_BYTES = 1024 * 1024
REWARD_JSON_ADAPTER = TypeAdapter(
float | Annotated[dict[str, float], Field(min_length=1)],
dict[str, float],
config=ConfigDict(strict=True, allow_inf_nan=False),
)

REWARD_SCALAR_ADAPTER = TypeAdapter(
float, config=ConfigDict(strict=True, allow_inf_nan=False)
)


class HarborTaskConfig(TaskConfig):
reward_key: str = Field("reward", min_length=1)
"""Named verifier value used as the training reward; other values remain metrics."""
mcp_servers: list[dict] = Field(default_factory=list)
"""Task-declared connections, bound from HarborData during construction."""

Expand Down Expand Up @@ -380,44 +385,57 @@ async def run_verifier(
["bash", "/tests/test.sh"], resolve_env(self.data.verifier_env)
)
scores = await self.read_reward_json(runtime)
if scores is not None:
if isinstance(scores, dict) and "reward" in scores:
trace.record_metrics(
{key: value for key, value in scores.items() if key != "reward"}
)
return {"reward": scores["reward"]}
return scores
try:
reward = (
(
await runtime.read(
"/logs/verifier/reward.txt", max_bytes=MAX_REWARD_BYTES
if scores is None:
try:
value = (
(
await runtime.read(
"/logs/verifier/reward.txt", max_bytes=MAX_REWARD_BYTES
)
)
.decode()
.strip()
)
.decode()
.strip()
)
return REWARD_JSON_ADAPTER.validate_python(float(reward))
except (SandboxError, OSError, ValueError) as exc:
scores = {"reward": REWARD_SCALAR_ADAPTER.validate_python(float(value))}
except (SandboxError, OSError, ValueError) as exc:
raise TaskError(
"Harbor verifier produced no reward.json and no valid reward.txt "
f"(exit {result.exit_code}): "
f"{(result.stderr or result.stdout).strip()[-500:]}"
) from exc
key = self.config.reward_key
if key not in scores:
raise TaskError(
"Harbor verifier produced no valid reward.json or reward.txt "
f"(exit {result.exit_code}): "
f"{(result.stderr or result.stdout).strip()[-500:]}"
) from exc
f"Harbor verifier reward key {key!r} is missing; "
f"available keys: {sorted(scores)}. "
"Set task.reward_key to select a named verifier reward."
)
trace.record_metrics(
{name: value for name, value in scores.items() if name != key}
)
return {key: scores[key]}

async def read_reward_json(
self, runtime: Runtime
) -> float | dict[str, float] | None:
"""Read Harbor's scalar or keyed JSON reward, if it is valid.
async def read_reward_json(self, runtime: Runtime) -> dict[str, float] | None:
"""JSON takes precedence whenever present; invalid or unreadable JSON fails.

Bounded: this is a grading input, and nothing guarantees its size.
Only absence permits text fallback. Reads remain bounded at the source.
"""
exists = await runtime.run(
["sh", "-c", 'test -e "$1" || test -L "$1"', "vf-reward", REWARD_JSON], {}
)
if exists.exit_code == 1:
return None
if exists.exit_code:
raise TaskError(f"Cannot inspect Harbor reward file {REWARD_JSON}")
try:
return REWARD_JSON_ADAPTER.validate_json(
await runtime.read(REWARD_JSON, max_bytes=MAX_REWARD_BYTES)
)
except (SandboxError, OSError, ValidationError):
return None
except (SandboxError, OSError, ValidationError) as exc:
raise TaskError(
"Harbor verifier produced invalid or unreadable reward.json; "
"expected an object of finite numeric rewards"
) from exc


def verifier_box_data(data: HarborData) -> HarborData:
Expand Down
Loading