Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 7 additions & 1 deletion tests/v1/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,13 @@ def _eval_config(
seat_cfg.setdefault("max_output_tokens", max_tokens)
seat_cfg.setdefault("timeout", {"rollout": rollout_timeout, "scoring": 60})
# Flake resilience: retries are per-agent now (flat RetryConfig).
seat_cfg.setdefault("retries", {"max_retries": 2, "include": ["ProviderError"]})
# HarnessError covers live-model harness flakes (an agent ending a turn with
# no output, an agent-timeout stall); deterministic failures still fail every
# attempt.
seat_cfg.setdefault(
"retries",
{"max_retries": 2, "include": ["ProviderError", "HarnessError"]},
)
return EvalConfig(
env={
"taskset": taskset_cfg,
Expand Down
12 changes: 7 additions & 5 deletions tests/v1/fixtures/echo_tool_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import verifiers.v1 as vf
from verifiers.v1.types import content_text

PHRASE = "hello world"
ECHO_TOKEN = "ok-7f3" # the tool stamps this; only a real tool call can surface it
Expand All @@ -36,11 +37,12 @@ def toolsets(cls, config: EchoToolTaskConfig) -> list[vf.Toolset]:

@vf.reward(weight=1.0)
async def echoed(self, trace: vf.Trace) -> float:
# The stamped token surfaces in an ASSISTANT message only if the model called
# the MCP tool and relayed its result — wherever in the exchange that happened
# (in a conversation the last turn may be a closing pleasantry).
replies = ((m.content or "").lower() for m in trace.assistant_messages)
return float(any(PHRASE in r and ECHO_TOKEN in r for r in replies))
# A stamped TOOL result proves the model called the MCP tool with the phrase —
# wherever in the exchange that happened. Scoring the assistant's relay instead
# would grade model obedience (a conversational reply may drop the stamp), not
# the tool loop this fixture exists to prove.
results = (content_text(m.content).lower() for m in trace.tool_messages)
return float(any(PHRASE in r and ECHO_TOKEN in r for r in results))


class EchoToolConfig(vf.TasksetConfig):
Expand Down
8 changes: 7 additions & 1 deletion verifiers/v1/dialects/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,13 @@ def _content_to_wire(content):

def message_to_wire(message: Message) -> dict:
if message.role == "assistant":
wire: dict = {"role": "assistant", "content": message.content}
# An empty completion records `content=None`; replayed as `"content": null`
# without tool calls, strict providers reject the whole request (422) — an
# empty string is the lossless wire form.
content = message.content
if content is None and not message.tool_calls:
content = ""
wire: dict = {"role": "assistant", "content": content}
if message.provider_state:
wire["reasoning_details"] = message.provider_state
elif message.reasoning_content is not None:
Expand Down
3 changes: 2 additions & 1 deletion verifiers/v1/envs/user_sim/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@

import verifiers.v1 as vf

PERSONA = """You are role-playing a USER talking to an AI assistant. This is your situation and goal:
PERSONA = """You are role-playing a USER talking to an AI assistant. This is your situation — what you want the ASSISTANT to do for you:

{scenario}

Rules:
- Open the conversation with your request, in your own words.
- Stay in character: short, natural user messages; never act as the assistant.
- The assistant does the work, not you: never perform the task yourself — any tool call, answer, or output format it asks for must come from the assistant; ask for it instead.
- Reveal details only when asked, as a real user would.
- When the assistant has fully met your goal — or you are convinced it cannot — reply with exactly {done} and nothing else."""

Expand Down
11 changes: 7 additions & 4 deletions verifiers/v1/harnesses/openclaw/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,13 @@ async def launch(
data: TaskData,
) -> ProgramResult:
system_prompt, prompt = self.resolve_prompt(data)
reasoning = ctx.sampling.reasoning_effort not in (
None,
"none",
) or ctx.model.rsplit("/", 1)[-1].startswith(("gpt-5", "o1", "o3", "o4"))
# Reasoning replay is opt-in (`sampling.reasoning_effort`), never guessed from
# the model name. With `reasoning` on, OpenClaw asks for
# `reasoning.encrypted_content` and replays it from its persisted transcript on
# resume — where its secret redaction masks Prime's dotted `<token>.<metadata>`
# payloads (they fail OpenClaw's opaque-token allowlist), so every resumed
# session then dies on an upstream 400 replaying the mangled token.
reasoning = ctx.sampling.reasoning_effort not in (None, "none")
directory = OPENCLAW_DIR.format(version=self.config.version)
state_dir = f".vf-openclaw/{trace.id}"
config_path = f"{state_dir}/openclaw.json"
Expand Down
Loading