Consolidated reference for Tolokaforge configuration schemas, APIs, and tools.
models:
agent:
provider: "openai" # openai, anthropic, google, openrouter, azure, bedrock, ollama
name: "gpt-4o-mini" # Model name (provider-specific)
temperature: 0.0 # 0.0 = deterministic
max_tokens: 4096
seed: 42 # For reproducibility (OpenAI, Anthropic)
user:
provider: "openai"
name: "gpt-4o-mini"
temperature: 0.7 # Higher for natural variation
orchestrator:
workers: 4 # Parallel worker threads
repeats: 5 # Trials per task (for pass@k)
max_budget_usd: 100.0 # Optional hard spend limit
max_requests_per_second: 2.0 # Optional global request throttle
max_attempt_retries: 1 # Retry transient infra failures
queue_backend: "sqlite" # or "postgres" for distributed workers
queue_postgres_dsn: null # required when queue_backend="postgres"
# max_turns: 60 # run-level cap (default 50); raise to let
# task-authored max_turns above 50 stand
timeouts:
turn_s: 60 # Per-turn timeout
episode_s: 1200 # Total episode timeout
stuck_heuristics:
max_repeated_tool_calls: 5 # Identical tool call threshold
max_idle_turns: 8 # Turns without tool calls
evaluation:
task_packs:
- "/abs/path/private-pack-core"
- "/abs/path/private-pack-mobile"
tasks_glob: "**/task.yaml"
output_dir: "results/run_001"
metrics: [pass@1, pass@4, pass@8]task_id: "unique_task_identifier"
name: "Human-readable task name"
category: "terminal" # e.g. terminal, web, airline, retail
description: |
Detailed task description.
initial_user_message: "Hi, I need help with ..." # optional — pinned opener, delivered verbatim as turn 1
initial_state:
json_db: "initial_state.json" # JSON database seed
filesystem:
copy:
- from: "fixtures/file.json"
to: "/env/fs/agent-visible/file.json"
mock_web:
base_url: "http://mock-web:8080"
rag:
corpus_dir: "rag/corpus"
system_prompt: "../wiki.md" # Custom system prompt (optional)
tools:
agent:
enabled: ["bash", "read_file", "write_file", "db_query"]
mcp_server: "../mcp_server.py" # Custom MCP tools (optional)
user:
enabled: [] # User-side tools (dual-control)
actors:
user:
mode: "llm" # "llm" or "scripted"
persona: "cooperative"
backstory: |
Context for LLM user simulator...
scripted_flow: # For scripted mode
- if_assistant_contains: "name"
user: "My name is Alice."
- default: "Please proceed."
policies:
disallowed_actions:
- "Do not reset entire account"
guidance:
- "Explain steps before executing"
metadata:
complexity: "hard" # optional analytics slices
expected_failure_modes: ["tool_selection", "grader_contract"]
tags: ["multi_app", "long_horizon"]
grading: "grading.yaml"combine:
method: "weighted" # weighted | all | any
weights: # every component the pack configures needs a weight here
state_checks: 0.5
transcript_rules: 0.1
trace_checks: 0.2
llm_judge: 0.2
pass_threshold: 0.8
state_checks:
jsonpaths:
- path: "$.users[?(@.id=='123')].verified"
equals: true
- path: "$.orders[-1].status"
equals: "completed"
hash:
enabled: true
expected_state_hash: "abc123..." # SHA256 of normalized final state
weight: 0.5 # REQUIRED in exactly this shape: hash source
# + non-empty jsonpaths. No default; rejected
# at load without it. See docs/GRADING.md.
transcript_rules:
must_contain: ["confirmation number"]
disallow_regex: ["(?i)password"]
max_turns: 40
min_assistant_turns: 1 # opt-in floor: 0 turns fails the component
tool_expectations: # graded on both substrates
required_tools: ["db_update"] # must have been called SUCCESSFULLY
disallowed_tools: ["bash"] # must not be called at ANY status
trace_checks: # ordering / scoped absence / counting over the timeline
constraints: # closed ten-kind vocabulary; hold whichever route was taken
- id: lookup_before_denial # unique across the whole block, paths included
description: "payment looked up before the case is denied"
weight: 2.0 # default 1.0; must be > 0
on_missing: fail # fail (default) | pass — decides an unmatched anchor
severity: scored # scored (default) | gate — a gate is not scored and must hold
within: { first_turn: 0, last_turn: 5 } # optional inclusive turn window
bind: # optional; the constraint holds under EVERY candidate
match: { kind: tool_call, tool: { equals: deny_case } } # whose events supply candidates
values: # name -> extraction; at least one, each referenced below
case: # the name equals_binding / contains_binding reads
field: args.case_url # tool | text | result | args.<dotted path>
pattern: '(https://[^/]+/cases/[0-9]+)' # optional; exactly one group, always a string
on_unbound: fail # fail (default) | pass — a binder that selected nothing
require: # exactly one constraint kind
before:
left: { quantifier: any, match: { kind: tool_call, tool: { equals: get_payment },
args: { case_url: { equals_binding: case } } } }
right: { quantifier: first, match: { kind: tool_call, tool: { equals: deny_case } } }
alternatives: # two or more routes; one path is a load error
- id: settled_from_the_ledger # each is scored over the shared constraints plus its own,
description: "the amount is confirmed against the ledger" # and the component is the best route's
constraints:
- id: the_ledger_was_read
description: "the ledger entry is read"
require: { present: { match: { kind: tool_call, tool: { equals: get_ledger_entry } } } }
- id: settled_from_the_statement
description: "the amount is confirmed against the statement"
constraints:
- id: the_statement_was_read
description: "the statement is read"
require: { present: { match: { kind: tool_call, tool: { equals: get_statement } } } }
llm_judge: # judge MODEL is run-level (models.judge), not here
rubric: # structured Rubric (NOT free text)
reference: | # optional author-written ground truth shown to the judge
Correct refund is $328.50 (base fare minus 24h-cancellation fee).
criteria:
- id: refund_amount # identifier-safe, unique
description: "Reply quotes the correct refund amount"
kind: binary # binary (0/1) | graded (0–1); default binary
required: true # default false; failed required → rubric fails
weight: 1.0 # default 1.0; per-criterion weight in the judge score
expected: "$328.50" # optional per-criterion author reference
- id: tone
description: "Reply is polite and professional"
kind: graded
weight: 0.5Each typed block above refuses a key its model does not declare. combine,
state_checks and transcript_rules name the closest declared field and the block's
whole accepted set, state_checks's two retired keys excepted — those draw their own
migration message. trace_checks and llm_judge refuse such a key as their model's
bare Extra inputs are not permitted, and llm_judge only on the rubric /
model_ref shapes its migration names. The block names are lenient, so state_cheks:
drops a whole component and is caught only when the correct name carries a weight. See
GRADING.md § Which keys a grading block refuses.
grading.llm_judge.rubric is a structured Rubric, not a free-text blob; the
old rubric: "<text>" shape, the output_schema field, and the per-task
judge-model field were all removed (the judge's structured-output schema is
derived from the rubric's criteria). The judge model is configured once per run under
models.judge (an optional ModelConfig role beside agent / user) and rides
each trial as TrialSpec.judge_model_config; there is no default and no fallback
to the agent model. See GRADING.md for the
judge mechanism, the two weighting layers, the required-gate semantics, and the
fail-loud ERRORED status.
Authors write only criteria (never justifications). The judge, on the output
side, ends every per-criterion justification with a trailing VERDICT: MET /
VERDICT: NOT MET (binary) or SCORE: <value> (graded) marker line that must
match its verdict; the marker is kept verbatim in each criterion_results
justification in grade.yaml.
Optional, beside a task's grading.yaml. Records which rubric criteria the pack's
trace_checks constraints are a candidate for, have narrowed, or have replaced. Nothing
about grading reads it: it is the claim tolokaforge reconcile checks
against recorded judge verdicts, and tolokaforge validate refuses a claim the pack
contradicts. A pack without one is unchanged.
migrations: # at least one entry
- criterion: checked_duplicates_first # a criterion id in this pack's llm_judge.rubric
mode: narrowed # candidate | narrowed | retired
by: # trace_checks ids in this pack; a CONJUNCTION
- the_notes_were_listed_before_the_note_was_added
was: # the criterion's PRE-migration shape
kind: binary # binary | graded; default binary
required: true # default false
weight: 1.0 # default 1.0
description: "<the text the evidence was gathered against>"
residual: # absent for candidate; kind is fixed by mode
kind: text # none (retired) | text (narrowed)
reason: "<what the judge still reads / why nothing remains>"
combine_weights: # post-migration combine.weights map
llm_judge: 0.7 # required for a narrowed/retired SCORED criterion
trace_checks: 0.3
evidence: # required for narrowed/retired, forbidden on candidate
corpus: tests/data/migration_corpora/notes_duplicate_check
observations: 17 # checked against what reconcile measures
kappa: 1.0 # nullable and required; null means undefined
acknowledged: # optional waivers, default []
- trial: <bundle path under evidence.corpus>
reason: "<why the judge's verdict on that trial is the one to discount>"The file is extra="forbid" at every level, so a misspelled key is an error rather than a
claim nothing reads. residual's presence and kind are a total function of mode —
absent, text, none — so a reader can tell what an entry claims from its mode alone. The
two load-time hazard rules (a required: true criterion may only be claimed by shared
severity: gate constraints; a scored one must declare combine_weights) and every other
refusal are in
GRADING.md § Declaring a migration;
the bar the declaration is decided against is in
RUBRIC_MIGRATION.md.
| Variable | Description |
|---|---|
OPENAI_API_KEY |
OpenAI API key |
ANTHROPIC_API_KEY |
Anthropic API key |
GOOGLE_API_KEY |
Google API key |
OPENROUTER_API_KEY |
OpenRouter API key |
AZURE_API_KEY / AZURE_API_BASE |
Azure OpenAI |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
AWS Bedrock |
OLLAMA_API_BASE |
Ollama endpoint (default: localhost:11434) |
from pathlib import Path
from tolokaforge.core.orchestrator import (
Orchestrator,
OrchestratorDeps,
resolve_run_directory,
)
orchestrator = Orchestrator(
config, # RunConfig — validated run configuration
resume=False, # True re-runs only pending / infra-failed trials
verbose=False, # DEBUG level on the orchestrator's structured logger
strict=False, # Raise on any ERROR log record
deps=OrchestratorDeps(...), # Optional injection seams (writers, backend, events, budget)
project=None, # Optional ProjectConfig (project.yaml layered defaults)
)
run_id, run_dir = resolve_run_directory(config.evaluation.output_dir)
run_dir = orchestrator.run(run_id=run_id, output_dir=run_dir)
# Returns the absolute run-dir path. When run_id and output_dir are both None,
# run() calls resolve_run_directory() itself.Resume-in-place uses the same entry point with resume=True and an existing run directory:
from tolokaforge.core.resume import resolve_resume_run_directory
run_id, run_dir = resolve_resume_run_directory(Path("results/prior_run_20260714_193042"))
orchestrator = Orchestrator(config, resume=True)
orchestrator.run(run_id=run_id, output_dir=run_dir)The queue-worker path uses two symmetric entry points:
orchestrator.prepare_run(Path("results/queue_run"), reset_queue=False)
orchestrator.run_worker(Path("results/queue_run"), max_attempts=None)prepare_run seeds the durable queue from the loaded tasks; run_worker leases and executes only pending attempts, so restarting a worker against a populated queue is inherent resume (an INFO log line names the reattach counts).
from tolokaforge.core.runner import TrialRunner
runner = TrialRunner(
task_spec: TaskSpec,
agent_client: LLMClient,
user_simulator: UserSimulator,
tool_executor: ToolExecutor,
max_turns: int = 50,
timeouts: Dict[str, float] = None,
stuck_heuristics: Dict[str, Any] = None,
)
result = runner.run() # Returns trajectory, metrics, final_state, gradefrom tolokaforge.core.model_client import LLMClient
client = LLMClient(
provider: str = "anthropic",
model_name: str = "claude-3-5-sonnet",
temperature: float = 0.0,
max_tokens: int = 4096,
seed: Optional[int] = None,
)
response = client.generate(
system="System prompt",
messages=[{"role": "user", "content": "..."}],
tools=[...], # OpenAI function calling format
tool_choice="auto", # "auto", "none", or specific tool name
)
# Returns: text, tool_calls, usage (Usage dataclass), cost_usd,
# reasoning (StructuredReasoning|None), effective_system_promptfrom tolokaforge.tools.registry import Tool, ToolResult, ToolPolicy, ToolCategory
class MyTool(Tool):
def __init__(self):
policy = ToolPolicy(
timeout_s=30.0,
rate_limit=100, # Max calls per trial
category=ToolCategory.COMPUTE,
visibility=["agent"], # "agent", "user", or both
)
super().__init__(name="my_tool", description="...", policy=policy)
def get_schema(self) -> Dict[str, Any]:
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {...}
}
}
def execute(self, **kwargs) -> ToolResult:
return ToolResult(success=True, output="result", error=None)from tolokaforge.tools.registry import ToolRegistry, get_registry
registry = get_registry() # Global registry
registry.register(MyTool()) # Register tool
tool = registry.get_tool("my_tool") # Get by name
schemas = registry.get_schemas(["bash", "read_file"]) # Get OpenAI schemasfrom tolokaforge.core.grading.combine import GradingEngine
engine = GradingEngine(
grading_config: GradingConfig,
task_domain: str = "general",
task_dir: Path | None = None,
task_initial_state: InitialStateConfig | None = None,
task_mcp_server: str | None = None,
)
grade = engine.grade_trajectory(trajectory: Trajectory, final_env_state: dict)
# Returns: Grade with binary_pass, score, components, reasons, state_diff
# (deterministic components only — GradingEngine does NOT run the rubric judge)
#
# The rubric judge runs Runner-side (see core/grading/judge.LLMJudge),
# taking the run-level judge ModelConfig carried on TrialSpec.judge_model_config
# (sourced from RunConfig.models["judge"]).The last three arguments are the world a state_checks.hash.golden_actions replay is
executed in, and a pack whose effective hash source is golden_actions grades only when
all three are supplied: grade_trajectory otherwise raises
UnbuildableGoldenReplayWorld (tolokaforge.core.grading.golden_replay, a subclass of
GoldenReplayError), naming every absent one in one message — the two task-level ones by
the task.yaml key that supplies them, task_dir as the caller's own omission — and no
component is scored. task_initial_state.json_db has to be a path to a JSON file
under task_dir; an inline mapping is no world to replay in. A golden_actions that is
truthy without being a list is refused ahead of all three, with UnreplayableGoldenSource
out of the same module: there is nothing to replay whatever world is supplied. A truthy
expected_state_hash is compared in process and returns before golden_actions is read,
so a pack declaring both needs none of the three. BaseAdapter.grade resolves all three
from the task it grades — see
GRADING.md § Hash-Based Grading.
| Tool | Description | Key Parameters |
|---|---|---|
read_file |
Read file contents | file_path (relative to /env/fs/agent-visible/) |
write_file |
Write file contents | file_path, content |
list_dir |
List directory | dir_path |
| Tool | Description | Key Parameters |
|---|---|---|
db_query |
Query JSON DB with JSONPath | jsonpath (e.g., $.users[?(@.id=='123')]) |
db_update |
Update JSON DB | ops array with {op, path, value} |
JSONPath syntax: $.field, $.array[0], $.array[-1], $[?(@.field=='value')]
Update operations: replace, add, remove
| Tool | Description | Key Parameters |
|---|---|---|
browser |
Playwright web automation | action string + x, y, text (actions: click_at, type_text_at, scroll_document, select, navigate, etc.) |
http_request |
HTTP requests to mock services | method, url, headers, body |
build_check |
Zero-arg peer-service HTTP probe (compile check) | endpoint declared via tool_config: service, port, path, method, timeout_s |
| Tool | Description | Key Parameters |
|---|---|---|
search_kb |
Hybrid search (BM25 + semantic) | query, top_k, alpha (0=keyword, 1=semantic) |
| Tool | Description | Key Parameters |
|---|---|---|
bash |
Execute shell commands (restricted) | command |
calculator |
Safe arithmetic evaluation | expression |
Base URL: http://json-db:8000
| Endpoint | Method | Description |
|---|---|---|
/reset |
POST | Initialize state (body: JSON object) |
/query |
POST | JSONPath query (body: {jsonpath: "..."}) |
/update |
POST | JSON Patch operations (body: {ops: [...]}) |
/dump |
GET | Get full normalized state |
/health |
GET | Health check |
Base URL: http://rag-service:8001
| Endpoint | Method | Description |
|---|---|---|
/search |
POST | Search documents (body: {query, top_k}) |
/index |
POST | Build index (body: {corpus_dir}) |
/health |
GET | Health check |
Base URL: http://mock-web:8080
Routes defined per-task in mock_web/routes.yaml:
routes:
- path: /api/booking
method: POST
response:
status: 201
body: {"booking_id": "BK123", "status": "confirmed"}Create custom tools via Model Context Protocol:
# mcp_server.py
import json
from typing import Any, Dict
_data = {}
def load_data(path: str):
global _data
with open(path) as f:
_data = json.load(f)
def get_data() -> Dict[str, Any]:
return _data # For state sync
def get_tool_schema(tool_name: str) -> Dict[str, Any]:
schemas = {
"my_tool": {
"type": "function",
"function": {
"name": "my_tool",
"description": "...",
"parameters": {...}
}
}
}
return schemas.get(tool_name, {})
def invoke_tool(tool_name: str, **kwargs) -> str:
if tool_name == "my_tool":
return json.dumps({"result": "..."})
return json.dumps({"error": f"Unknown tool: {tool_name}"})
load_data("data/initial_state.json")Usage in task.yaml:
tools:
agent:
enabled: ["my_tool"]
mcp_server: "../mcp_server.py"- GRADING.md - Detailed grading system (hash algorithm, pass@k)
- ADAPTER_ARCHITECTURE.md - Adapter architecture for task loading
- custom_checks.md - Custom Python validation
- SECURITY.md - Security model
- Multi-container tasks guide - Declaring an
environment_manifestso a task ships its own docker-compose stack