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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ dependencies = [
"prime-pydantic-config[toml]>=0.4.3",
"loguru>=0.7.0",
"tomli-w>=1.0.0",
"renderers[multimodal]>=0.1.13.dev16",
"renderers[multimodal]>=0.1.13.dev17",
"typing_extensions>=4.12.2",
"mcp==2.0.0",
]
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
from verifiers.v1.clients import EvalClientConfig, ModelContext
from verifiers.v1.configs.cli.eval import EvalConfig
from verifiers.v1.configs.harness import HarnessConfig
from verifiers.v1.configs.sampling import SamplingConfig
from verifiers.v1.episode import Episode
from verifiers.v1.types import SamplingConfig
from verifiers.v1.utils.loaders import harness_config_type, load_environment

CI_MODEL = "internal/deepseek-v4.1-flash"
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_dialects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import pytest

from verifiers.v1.configs.sampling import SamplingConfig
from verifiers.v1.dialects import AnthropicDialect, ChatDialect, ResponsesDialect
from verifiers.v1.types import SamplingConfig

SAMPLING = SamplingConfig(temperature=0.3, top_p=0.9, reasoning_effort="low")

Expand Down
30 changes: 15 additions & 15 deletions tests/unit/test_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ def test_parallel_commit_reconciles_only_token_identical_prefixes():
)

roots = [node for node in trace.nodes if node.parent is None]
assert [node.token_ids for node in roots] == [[1, 2], [1, 99]]
assert [node.token_ids.tolist() for node in roots] == [[1, 2], [1, 99]]
assert trace.num_branches == 3


Expand Down Expand Up @@ -504,7 +504,7 @@ def test_parallel_commit_reconciles_unspanned_assistant_tokens():
)

assert trace.num_branches == 1
assert [node.token_ids for node in trace.nodes] == [[1], [2, 3], [4], [5]]
assert [node.token_ids.tolist() for node in trace.nodes] == [[1], [2, 3], [4], [5]]


def test_unspanned_reconciliation_stops_at_next_message_boundary():
Expand Down Expand Up @@ -561,8 +561,8 @@ def test_unspanned_reconciliation_stops_at_next_message_boundary():
)

follow_up_node = next(node for node in trace.nodes if node.message == follow_up)
assert follow_up_node.token_ids == [4]
assert trace.nodes[follow_up_node.parent].token_ids == [2, 3]
assert list(follow_up_node.token_ids) == [4]
assert list(trace.nodes[follow_up_node.parent].token_ids) == [2, 3]


def test_renderer_level_break_forks_by_token_id():
Expand Down Expand Up @@ -614,7 +614,7 @@ def second_turn(trace, prompt_ids):
first_turn(linear)
second_turn(linear, [1, 2, 3, 4, 5, 6, 7])
assert linear.num_branches == 1
assert linear.branches[0].token_ids == [1, 2, 3, 4, 5, 6, 7, 8]
assert list(linear.branches[0].token_ids) == [1, 2, 3, 4, 5, 6, 7, 8]

# Break: the assistant turn retokenizes (4 -> 99), so prompt_ids diverge at that node.
broken = vf.Trace(
Expand All @@ -624,7 +624,7 @@ def second_turn(trace, prompt_ids):
first_turn(broken)
second_turn(broken, [1, 2, 3, 99, 5, 6, 7])
assert broken.num_branches == 2
assert sorted(b.token_ids for b in broken.branches) == [
assert sorted(b.token_ids.tolist() for b in broken.branches) == [
[1, 2, 3, 4, 5],
[1, 2, 3, 99, 5, 6, 7, 8],
]
Expand All @@ -637,11 +637,11 @@ def test_expanded_prompt_is_attributed_while_bridge_uses_logical_tokens():
)
user = vf.UserMessage(content="image")
assistant = vf.AssistantMessage(content="looked")
assert graph.MessageNode(message=user, token_ids=[9]).logical_ids == [9]
assert graph.MessageNode(message=user, token_ids=[9]).logical_ids.tolist() == [9]
assert (
graph.MessageNode(
message=user, token_ids=[9], renderer_token_ids=[]
).logical_ids
).logical_ids.tolist()
== []
)
graph.prepare_turn(trace, [user]).commit(
Expand All @@ -663,10 +663,10 @@ def test_expanded_prompt_is_attributed_while_bridge_uses_logical_tokens():
)
)

assert trace.branches[0].token_ids == [1, 9, 9, 3, 4]
assert trace.branches[0].mm_token_type_ids == [0, 1, 1, 0, 0]
assert trace.nodes[0].token_ids == [1, 9, 9]
assert trace.nodes[0].is_content == [False, True, True]
assert list(trace.branches[0].token_ids) == [1, 9, 9, 3, 4]
assert list(trace.branches[0].mm_token_type_ids) == [0, 1, 1, 0, 0]
assert list(trace.nodes[0].token_ids) == [1, 9, 9]
assert list(trace.nodes[0].is_content) == [False, True, True]
turn = graph.prepare_turn(trace, [user, assistant, vf.UserMessage(content="next")])
assert turn.previous_renderer_token_ids() == ([1, 9, 3], [4])

Expand Down Expand Up @@ -711,9 +711,9 @@ def test_expanded_prompt_is_attributed_while_bridge_uses_logical_tokens():

tool_node = trace.nodes[-2]
assert tool_node.message == tool
assert tool_node.token_ids == [5, 6]
assert tool_node.renderer_token_ids == [5, 6]
assert tool_node.is_content == [False, True]
assert list(tool_node.token_ids) == [5, 6]
assert tool_node.renderer_token_ids.tolist() == [5, 6]
assert list(tool_node.is_content) == [False, True]


def test_prompt_supplied_assistant_messages_are_not_sampled_turns():
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/test_semantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,9 +368,9 @@ def test_acp_derives_compaction_attempt_branch_trainability():
)

assert trace.nodes[3].sampled is True
assert trace.nodes[3].mask == [True, True]
assert trace.nodes[4].mask == [True, True]
assert trace.nodes[6].mask == [True]
assert list(trace.nodes[3].mask) == [True, True]
assert list(trace.nodes[4].mask) == [True, True]
assert list(trace.nodes[6].mask) == [True]
assert trace.nodes[3].semantic_parents == [
vf.ParentLink(node=1, type="compaction_attempt")
]
Expand All @@ -388,8 +388,8 @@ def test_acp_derives_compaction_attempt_branch_trainability():

restored = vf.WireTrace.model_validate_json(trace.model_dump_json())
assert restored.nodes[3].sampled is True
assert restored.nodes[3].mask == [True, True]
assert restored.nodes[4].mask == [True, True]
assert list(restored.nodes[3].mask) == [True, True]
assert list(restored.nodes[4].mask) == [True, True]
restored_branches = {
branch.nodes[-1].message.content: branch for branch in restored.branches
}
Expand Down
158 changes: 158 additions & 0 deletions tests/unit/test_token_arrays.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import random

import numpy as np

from verifiers.v1.graph import MessageNode
from verifiers.v1.serve.delta import dump, pack, unpack
from verifiers.v1.trace import Branch
from verifiers.v1.types import AssistantMessage, UserMessage


def assistant(**fields) -> MessageNode:
return MessageNode(message=AssistantMessage(content="a"), sampled=True, **fields)


def test_validated_streams_are_typed_arrays():
node = assistant(
token_ids=[1, 2, 3],
mask=[False, True, True],
is_content=[False, True, True],
logprobs=[-0.1, -0.2],
advantages=[0.5, 0.5],
loss_weights={"rl": [0.0, 1.0, 1.0]},
)
dtypes = {
"token_ids": np.int32,
"mask": np.bool_,
"is_content": np.bool_,
"logprobs": np.float32,
"advantages": np.float32,
}
for name, dtype in dtypes.items():
value = getattr(node, name)
assert isinstance(value, np.ndarray) and value.dtype == dtype, name
assert node.loss_weights["rl"].dtype == np.float32
# Defaults are typed arrays too.
bare = assistant()
assert bare.token_ids.dtype == np.int32 and bare.logprobs.dtype == np.float32


def test_token_arrays_wire_round_trip():
node = assistant(
token_ids=[1, 2, 130_000],
mask=[False, True, True],
logprobs=[-0.125, -0.25],
loss_weights={"rl": [0.0, 1.0, 1.0]},
)
wire = unpack(pack(dump(node, context={"token_arrays": True})))
assert wire["token_ids"]["__nd__"] and wire["loss_weights"]["rl"]["__nd__"]
back = MessageNode.model_validate(wire)
assert back.token_ids.tolist() == [1, 2, 130_000]
assert back.mask.tolist() == [False, True, True]
assert back.logprobs.tolist() == [-0.125, -0.25]
assert back.loss_weights["rl"].tolist() == [0.0, 1.0, 1.0]
# Without the context the wire keeps lists, which validate the same way.
plain = unpack(pack(dump(node)))
assert plain["token_ids"] == [1, 2, 130_000]
assert MessageNode.model_validate(plain).token_ids.dtype == np.int32


def test_json_records_stay_lists():
node = assistant(token_ids=[1, 2], mask=[False, True], logprobs=[-0.123456])
record = node.model_dump(mode="json")
assert record["token_ids"] == [1, 2] and record["mask"] == [False, True]
assert record["logprobs"] == [-0.1235]
assert MessageNode.model_validate(record).token_ids.dtype == np.int32


def spread_lists(nodes: list[MessageNode], values) -> list[float]:
"""`Branch.spread` as it was on lists, the reference the array version must match."""
out: list[float] = []
for node in nodes:
mask, node_values = list(node.mask), values(node)
node_values = [] if node_values is None else [float(v) for v in node_values]
sampled = sum(mask) if node_values else 0
if not sampled or all(mask[-sampled:]):
out += [0.0] * (len(mask) - sampled) + node_values[:sampled]
out += [0.0] * max(0, sampled - len(node_values))
continue
li = 0
for is_sampled in mask:
if is_sampled:
out.append(node_values[li] if li < len(node_values) else 0.0)
li += 1
else:
out.append(0.0)
return out


def test_spread_matches_the_list_implementation():
rng = random.Random(0)
for _ in range(300):
nodes = []
for _ in range(rng.randint(0, 6)):
n = rng.randint(0, 12)
if rng.random() < 0.5: # canonical: scaffold then sampled suffix
k = rng.randint(0, n)
mask = [False] * (n - k) + [True] * k
else:
mask = [rng.random() < 0.5 for _ in range(n)]
values = [rng.uniform(-5, 0) for _ in range(rng.randint(0, n + 2))]
node_cls = (
assistant
if rng.random() < 0.5
else (lambda **f: MessageNode(message=UserMessage(content="u"), **f))
)
node = node_cls(token_ids=list(range(n)), mask=mask, logprobs=values)
if rng.random() < 0.5: # env-server nodes hold lists
node = MessageNode.model_construct(
**{**node.__dict__, "mask": mask, "logprobs": values}
)
nodes.append(node)
branch = Branch(index=0, nodes=nodes)
expected = np.asarray(
spread_lists(nodes, lambda node: node.logprobs), np.float32
)
got = branch.logprobs
assert got.dtype == np.float32 and np.array_equal(got, expected)
assert branch.token_ids.dtype == np.int32
assert len(branch.token_ids) == len(branch.sampled_mask) == len(got)


def test_committed_nodes_own_typed_arrays():
"""The env server builds nodes unvalidated: each still holds typed arrays, owns them (a view
would retain the turn's full-context prompt) and keeps no copy of identical renderer ids."""
import verifiers.v1 as vf
from verifiers.v1 import graph
from verifiers.v1.types import Response, TurnTokens

trace = vf.Trace(
agent=vf.AgentInfo(config=vf.AgentConfig()),
task=vf.TraceTask(type="Task", data=vf.TaskData(idx=0, prompt="x")),
)
graph.prepare_turn(trace, [UserMessage(content="u")]).commit(
Response(
id="a",
created=0,
model="t",
message=AssistantMessage(content="a"),
finish_reason="stop",
tokens=TurnTokens(
prompt_ids=np.arange(10, 14, dtype=np.int32),
completion_ids=np.asarray([20, 21], dtype=np.int32),
completion_logprobs=np.asarray([-0.5, -0.25], dtype=np.float32),
message_spans=[(0, 3)],
is_content=np.asarray([False, True, True, False]),
),
)
)
user, reply = trace.nodes
assert user.token_ids.tolist() == [10, 11, 12]
assert reply.token_ids.tolist() == [13, 20, 21]
assert reply.mask.tolist() == [False, True, True]
assert reply.logprobs.tolist() == [-0.5, -0.25]
for node in (user, reply):
assert node.token_ids.dtype == np.int32 and node.token_ids.base is None
assert node.mask.dtype == node.is_content.dtype == np.bool_
assert node.logprobs.dtype == np.float32
assert node.renderer_token_ids is None
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 5 additions & 7 deletions verifiers/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
SingleAgentEnvConfig,
default_agent_harness,
)
from verifiers.v1.configs.harness import HarnessConfig
from verifiers.v1.configs.harness import ID, HarnessConfig
from verifiers.v1.configs.judge import JudgeConfig, Judges
from verifiers.v1.configs.retries import RetryConfig, RetryRule
from verifiers.v1.configs.runtime import (
Expand All @@ -41,6 +41,7 @@
RuntimeConfig,
SubprocessConfig,
)
from verifiers.v1.configs.sampling import Sampling, SamplingConfig
from verifiers.v1.configs.scoring import ScoringConfig
from verifiers.v1.configs.select import (
SelectCLIConfig,
Expand Down Expand Up @@ -143,7 +144,6 @@
WireTrace,
)
from verifiers.v1.types import (
ID,
AssistantMessage,
ContentPart,
ImageUrlContentPart,
Expand All @@ -153,8 +153,6 @@
Messages,
Request,
Response,
Sampling,
SamplingConfig,
SamplingMask,
SystemMessage,
TextContentPart,
Expand Down Expand Up @@ -241,7 +239,7 @@
"HarnessConfig": "verifiers.v1.configs.harness",
"HarnessError": "verifiers.v1.errors",
"HarnessSession": "verifiers.v1.harness",
"ID": "verifiers.v1.types",
"ID": "verifiers.v1.configs.harness",
"ImageUrlContentPart": "verifiers.v1.types",
"ImageUrlSource": "verifiers.v1.types",
"Interaction": "verifiers.v1.agent",
Expand Down Expand Up @@ -282,8 +280,8 @@
"RuntimeConfig": "verifiers.v1.configs.runtime",
"RuntimeInfo": "verifiers.v1.runtimes",
"RuntimeProcess": "verifiers.v1.runtimes",
"Sampling": "verifiers.v1.types",
"SamplingConfig": "verifiers.v1.types",
"Sampling": "verifiers.v1.configs.sampling",
"SamplingConfig": "verifiers.v1.configs.sampling",
"SamplingMask": "verifiers.v1.types",
"SandboxError": "verifiers.v1.errors",
"ScoringConfig": "verifiers.v1.configs.scoring",
Expand Down
3 changes: 2 additions & 1 deletion verifiers/v1/clients/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
ClientConfig,
TrainClientConfig,
)
from verifiers.v1.configs.sampling import Sampling, SamplingConfig
from verifiers.v1.dialects import Dialect
from verifiers.v1.graph import PendingTurn
from verifiers.v1.types import Response, Sampling, SamplingConfig
from verifiers.v1.types import Response

SESSION_ID_HEADER = "X-Session-ID"
"""Per-rollout routing header (the trace id, same value every turn), so a session-affinity
Expand Down
Loading
Loading