Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/bugbearer_review-artifact-7f3a92.json
T

1 line
80 KiB
JSON

{"repo": "bugbearer/review-artifact-7f3a92", "files": {"code/experiments/tau2_hf_policy_orchestrator_eval.py": {"cc6a95e0960313ecd2f7de0093c3bfaeda0ed2e1": {"t": 1785746640.618854, "n": 78388, "h": "154f73a7c686c120", "c": "#!/usr/bin/env python3\n\"\"\"Evaluate a HuggingFace policy model inside tau2's official Orchestrator.\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport random\nimport os\nimport socket\nimport statistics\nimport sys\nimport time\nimport urllib.error\nimport urllib.request\nfrom copy import deepcopy\nfrom pathlib import Path\nfrom typing import Any\n\ntry:\n import torch\n from transformers import AutoModelForCausalLM, AutoTokenizer\nexcept ImportError: # API and structured-only evals do not need local HF deps.\n torch = None\n AutoModelForCausalLM = None\n AutoTokenizer = None\n\n\nROOT = Path(__file__).resolve().parents[1]\nTAU2_SRC = ROOT / \"third_party\" / \"tau2-bench\" / \"src\"\nif str(TAU2_SRC) not in sys.path:\n sys.path.insert(0, str(TAU2_SRC))\nEXPERIMENTS_DIR = ROOT / \"experiments\"\nif str(EXPERIMENTS_DIR) not in sys.path:\n sys.path.insert(0, str(EXPERIMENTS_DIR))\n\nfrom build_tau2_policy_sft_data import ( # noqa: E402\n POLICY_SYSTEM_PROMPT,\n build_policy_user_prompt,\n tool_inventory_for_domain,\n)\nfrom train_tau2_airretail_selector_baseline import ( # noqa: E402\n feature_tokens as selector_feature_tokens,\n score_vec as selector_score_vec,\n sigmoid as selector_sigmoid,\n vectorize as selector_vectorize,\n)\ntry:\n from tau2_structured_proposal_orchestrator_eval import ( # noqa: E402\n StructuredPlanner,\n action_key as structured_action_key,\n load_task_dicts,\n parse_tool_content as structured_parse_tool_content,\n )\nexcept ModuleNotFoundError: # Student-only free-form policy evals do not need this large dependency.\n StructuredPlanner = None\n\n def _missing_structured_dependency(*_: Any, **__: Any) -> Any:\n raise RuntimeError(\n \"tau2_structured_proposal_orchestrator_eval.py is required for structured \"\n \"proposal, structured-only, or generated-task evaluation.\"\n )\n\n structured_action_key = _missing_structured_dependency\n load_task_dicts = _missing_structured_dependency\n\n def structured_parse_tool_content(*_: Any, **__: Any) -> list[Any]:\n return []\nfrom tau2_reference_orchestrator_eval import ( # noqa: E402\n STOP_TOKEN,\n build_environment,\n choose_evaluation_type,\n count_tool_calls,\n count_tool_errors,\n get_communicate_text,\n get_domain_tasks,\n import_tau2,\n make_scripted_user_class,\n normalize_action,\n parse_csv_arg,\n reward_breakdown_dict,\n safe_reward,\n tool_error_details,\n)\n\n\nCAR_SRC_DEFAULT = ROOT / \"baseline_repro\" / \"CAR\" / \"upstream\" / \"src\"\n\n\ndef configure_logging() -> None:\n try:\n from loguru import logger\n\n logger.remove()\n logger.add(sys.stderr, level=os.environ.get(\"TAU2_HF_POLICY_LOG_LEVEL\", \"ERROR\"))\n except Exception:\n pass\n\n\ndef configure_tau2_nl_assertion_judge(model: str) -> None:\n if not model:\n return\n if \"/\" not in model and (os.environ.get(\"OPENAI_API_BASE\") or os.environ.get(\"OPENAI_BASE_URL\")):\n model = f\"openai/{model}\"\n import tau2.config as tau2_config\n import tau2.evaluator.evaluator_nl_assertions as nl_assertions\n\n tau2_config.DEFAULT_LLM_NL_ASSERTIONS = model\n nl_assertions.DEFAULT_LLM_NL_ASSERTIONS = model\n\n\ndef load_jsonl(path: Path) -> list[dict[str, Any]]:\n rows = []\n with path.open(encoding=\"utf-8\") as fp:\n for line in fp:\n line = line.strip()\n if line:\n rows.append(json.loads(line))\n return rows\n\n\ndef write_jsonl(path: Path, rows: list[dict[str, Any]]) -> None:\n path.parent.mkdir(parents=True, exist_ok=True)\n with path.open(\"w\", encoding=\"utf-8\") as fp:\n for row in rows:\n fp.write(json.dumps(row, ensure_ascii=False, sort_keys=True, default=str) + \"\\n\")\n\n\ndef extract_json_object(text: str) -> tuple[dict[str, Any] | None, str]:\n decoder = json.JSONDecoder()\n for idx, char in enumerate(text):\n if char != \"{\":\n continue\n try:\n obj, _ = decoder.raw_decode(text[idx:])\n except json.JSONDecodeError:\n continue\n if isinstance(obj, dict):\n return obj, \"\"\n return None, \"no JSON object found\"\n\n\ndef render_prompt(tokenizer: Any, user_prompt: str) -> str:\n messages = [\n {\"role\": \"system\", \"content\": POLICY_SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_prompt},\n ]\n if getattr(tokenizer, \"chat_template\", None):\n return tokenizer.apply_chat_template(\n messages,\n tokenize=False,\n add_generation_prompt=True,\n )\n return f\"System:\\n{POLICY_SYSTEM_PROMPT}\\n\\nUser:\\n{user_prompt}\\n\\nAssistant:\\n\"\n\n\ndef call_chat_completion(\n *,\n base_url: str,\n api_key: str,\n model: str,\n user_prompt: str,\n max_tokens: int,\n temperature: float,\n timeout: int,\n retries: int,\n response_format: str,\n) -> str:\n url = base_url.rstrip(\"/\") + \"/chat/completions\"\n payload: dict[str, Any] = {\n \"model\": model,\n \"messages\": [\n {\"role\": \"system\", \"content\": POLICY_SYSTEM_PROMPT},\n {\"role\": \"user\", \"content\": user_prompt},\n ],\n \"temperature\": temperature,\n \"max_tokens\": max_tokens,\n \"stream\": False,\n }\n if response_format != \"none\":\n payload[\"response_format\"] = {\"type\": response_format}\n data = json.dumps(payload).encode(\"utf-8\")\n headers = {\n \"Authorization\": f\"Bearer {api_key}\",\n \"Content-Type\": \"application/json\",\n }\n last_error: Exception | None = None\n for attempt in range(retries + 1):\n req = urllib.request.Request(url, data=data, headers=headers, method=\"POST\")\n try:\n with urllib.request.urlopen(req, timeout=timeout) as response:\n raw = response.read().decode(\"utf-8\")\n parsed = json.loads(raw)\n return str(parsed[\"choices\"][0][\"message\"][\"content\"])\n except (\n urllib.error.URLError,\n urllib.error.HTTPError,\n KeyError,\n json.JSONDecodeError,\n TimeoutError,\n socket.timeout,\n ) as exc:\n last_error = exc\n if attempt >= retries:\n break\n time.sleep(2**attempt)\n raise RuntimeError(f\"API call failed after {retries + 1} attempts: {last_error}\")\n\n\ndef generate_many_api(\n *,\n prompt: str,\n args: argparse.Namespace,\n) -> list[str]:\n api_key = os.environ.get(args.api_key_env)\n if not api_key:\n raise RuntimeError(f\"{args.api_key_env} is not set\")\n outputs = []\n for idx in range(max(args.num_candidates, 1)):\n try:\n outputs.append(\n call_chat_completion(\n base_url=args.api_base_url,\n api_key=api_key,\n model=args.api_model,\n user_prompt=prompt,\n max_tokens=args.max_new_tokens,\n temperature=0.0 if idx == 0 else args.candidate_temperature,\n timeout=args.api_timeout,\n retries=args.api_retries,\n response_format=args.api_response_format,\n )\n )\n except Exception as exc:\n if not args.api_failure_as_stop:\n raise\n outputs.append(\n json.dumps(\n {\n \"type\": \"stop\",\n \"content\": STOP_TOKEN,\n \"api_error\": f\"{type(exc).__name__}: {str(exc)[:240]}\",\n },\n ensure_ascii=False,\n sort_keys=True,\n )\n )\n return outputs\n\n\ndef hard_mode_prompt_suffix(domain: str) -> str:\n common = [\n \"Hard-mode decision rules:\",\n \"- Do not finish with ###STOP### when the user asked to change, cancel, return, exchange, update, or buy something and no successful state-changing tool has been called yet.\",\n \"- Before a state-changing tool call, use only IDs that appeared in the conversation or in prior tool outputs. Do not invent reservation, order, item, product, or payment IDs.\",\n \"- If the last tool call returned an error, do not repeat the same failing call. Query details, choose a different ID from tool outputs, or fix the arguments first.\",\n \"- After a successful state-changing tool call, check whether the user requested additional items, orders, flights, addresses, payments, or follow-up actions before stopping.\",\n \"- If you have only queried information and have found a plausible target, prefer the required state-changing tool over a final response.\",\n ]\n if domain == \"airline\":\n common.extend(\n [\n \"- For airline changes or cancellations, first identify the user's real reservation_id from get_user_details or get_reservation_details before calling update_reservation_flights, cancel_reservation, or related mutation tools.\",\n \"- When the user mentions multiple convenient airports or dates, compare the relevant search results, then update exactly the existing reservation rather than making up a reservation_id.\",\n ]\n )\n if domain == \"retail\":\n common.extend(\n [\n \"- For retail tasks, inspect the user's relevant orders before modifying them. Match item IDs to the order that actually contains them.\",\n \"- For exchanges and returns, item_ids are the purchased item IDs and new_item_ids are replacement product or item IDs required by the tool schema; do not set them to the same value unless the tool output proves that is correct.\",\n \"- If the user has multiple requested changes, complete all compatible order, item, address, payment, cancellation, return, or exchange operations before responding.\",\n ]\n )\n return \"\\n\".join(common)\n\n\ndef generate_one(\n *,\n model: Any,\n tokenizer: Any,\n prompt: str,\n max_input_tokens: int,\n max_new_tokens: int,\n json_prefill: bool = False,\n) -> str:\n if torch is None:\n raise RuntimeError(\"torch/transformers are required for local HF policy generation\")\n text = render_prompt(tokenizer, prompt)\n prefix = \"{\" if json_prefill else \"\"\n if prefix:\n text += prefix\n inputs = tokenizer(\n text,\n return_tensors=\"pt\",\n truncation=True,\n max_length=max_input_tokens,\n )\n device = next(model.parameters()).device\n inputs = {key: value.to(device) for key, value in inputs.items()}\n with torch.inference_mode():\n output = model.generate(\n **inputs,\n do_sample=False,\n max_new_tokens=max_new_tokens,\n pad_token_id=tokenizer.eos_token_id,\n )\n new_tokens = output[0, inputs[\"input_ids\"].shape[-1] :]\n return prefix + tokenizer.decode(new_tokens, skip_special_tokens=True)\n\n\ndef generate_many(\n *,\n model: Any,\n tokenizer: Any,\n prompt: str,\n max_input_tokens: int,\n max_new_tokens: int,\n num_candidates: int,\n temperature: float,\n top_p: float,\n json_prefill: bool = False,\n) -> list[str]:\n if torch is None:\n raise RuntimeError(\"torch/transformers are required for local HF policy generation\")\n greedy = generate_one(\n model=model,\n tokenizer=tokenizer,\n prompt=prompt,\n max_input_tokens=max_input_tokens,\n max_new_tokens=max_new_tokens,\n json_prefill=json_prefill,\n )\n if num_candidates <= 1:\n return [greedy]\n\n raw_samples = []\n # Generate stochastic candidates one-by-one. Batching them with\n # num_return_sequences multiplies KV-cache memory and OOMs 14B at 5k context\n # on 40GB A800s.\n text = render_prompt(tokenizer, prompt)\n prefix = \"{\" if json_prefill else \"\"\n if prefix:\n text += prefix\n device = next(model.parameters()).device\n for _ in range(num_candidates - 1):\n inputs = tokenizer(\n text,\n return_tensors=\"pt\",\n truncation=True,\n max_length=max_input_tokens,\n )\n inputs = {key: value.to(device) for key, value in inputs.items()}\n with torch.inference_mode():\n output = model.generate(\n **inputs,\n do_sample=True,\n temperature=temperature,\n top_p=top_p,\n max_new_tokens=max_new_tokens,\n pad_token_id=tokenizer.eos_token_id,\n )\n new_tokens = output[0, inputs[\"input_ids\"].shape[-1] :]\n raw_samples.append(prefix + tokenizer.decode(new_tokens, skip_special_tokens=True))\n del inputs, output\n if torch.cuda.is_available():\n torch.cuda.empty_cache()\n return [greedy] + raw_samples\n\n\ndef action_to_message(\n action: dict[str, Any] | None,\n *,\n parse_error: str,\n counter: int,\n solo_mode: bool,\n) -> tuple[Any, dict[str, Any]]:\n tau2 = import_tau2()\n AssistantMessage = tau2[\"AssistantMessage\"]\n ToolCall = tau2[\"ToolCall\"]\n\n meta = {\"parse_ok\": int(action is not None and not parse_error), \"parse_error\": parse_error, \"invalid_action\": 0}\n if action is None:\n meta[\"invalid_action\"] = 1\n return AssistantMessage(role=\"assistant\", content=STOP_TOKEN), meta\n\n action_type = str(action.get(\"type\") or \"\").lower()\n if not action_type and action.get(\"name\"):\n action_type = \"tool_call\"\n if action_type not in {\"tool_call\", \"tool_calls\", \"respond\", \"stop\", \"done\"} and action.get(\"name\"):\n action_type = \"tool_call\"\n\n calls = []\n if action_type == \"tool_call\":\n calls = [action]\n elif action_type == \"tool_calls\":\n calls = action.get(\"calls\") or []\n\n if calls:\n tool_calls = []\n for call_idx, call in enumerate(calls):\n name = call.get(\"name\")\n arguments = call.get(\"arguments\") or {}\n if not isinstance(name, str) or not name or not isinstance(arguments, dict):\n meta[\"invalid_action\"] = 1\n return AssistantMessage(role=\"assistant\", content=STOP_TOKEN), meta\n tool_calls.append(\n ToolCall(\n id=f\"policy-{counter}-{call_idx}\",\n name=name,\n arguments=deepcopy(arguments),\n requestor=\"assistant\",\n )\n )\n return AssistantMessage(role=\"assistant\", tool_calls=tool_calls), meta\n\n if action_type in {\"respond\", \"stop\", \"done\"} or action.get(\"content\") is not None:\n content = str(action.get(\"content\") or STOP_TOKEN)\n if action_type in {\"stop\", \"done\"} and STOP_TOKEN not in content:\n content = content.rstrip() + \"\\n\" + STOP_TOKEN\n return AssistantMessage(role=\"assistant\", content=content), meta\n\n meta[\"invalid_action\"] = 1\n return AssistantMessage(role=\"assistant\", content=STOP_TOKEN), meta\n\n\ndef is_tool_message(message: Any) -> bool:\n return bool(getattr(message, \"tool_calls\", None))\n\n\ndef replay_tool_error_count(message: Any, environment: Any) -> int | None:\n if not is_tool_message(message):\n return None\n try:\n env_copy = deepcopy(environment)\n errors = 0\n for call in message.tool_calls or []:\n result = env_copy.get_response(call)\n errors += int(bool(getattr(result, \"error\", False)))\n return errors\n except Exception:\n return 999\n\n\ndef tool_action_key(item: dict[str, Any]) -> str | None:\n action = item.get(\"parsed_action\")\n if not isinstance(action, dict):\n return None\n action_type = str(action.get(\"type\") or \"\").lower()\n if not action_type and action.get(\"name\"):\n action_type = \"tool_call\"\n if action_type != \"tool_call\":\n return None\n return json.dumps(action, sort_keys=True, separators=(\",\", \":\"))\n\n\ndef parsed_tool_action(item: dict[str, Any]) -> dict[str, Any] | None:\n action = item.get(\"parsed_action\")\n if not isinstance(action, dict):\n return None\n action_type = str(action.get(\"type\") or \"\").lower()\n if not action_type and action.get(\"name\"):\n action_type = \"tool_call\"\n if action_type != \"tool_call\":\n return None\n name = action.get(\"name\")\n arguments = action.get(\"arguments\") or {}\n if not isinstance(name, str) or not isinstance(arguments, dict):\n return None\n return {\"name\": name, \"arguments\": arguments}\n\n\ndef parsed_tool_action_with_requestor(item: dict[str, Any]) -> dict[str, Any] | None:\n action = parsed_tool_action(item)\n if action is None:\n return None\n return {\n \"name\": action[\"name\"],\n \"requestor\": \"assistant\",\n \"arguments\": action[\"arguments\"],\n }\n\n\ndef selected_message_tool_actions(message: Any) -> list[dict[str, Any]]:\n actions = []\n for call in getattr(message, \"tool_calls\", None) or []:\n name = getattr(call, \"name\", None)\n arguments = getattr(call, \"arguments\", None)\n if isinstance(name, str) and isinstance(arguments, dict):\n actions.append({\"name\": name, \"arguments\": deepcopy(arguments)})\n return actions\n\n\ndef structured_action_to_policy_action(action: dict[str, Any] | None) -> dict[str, Any] | None:\n if not isinstance(action, dict):\n return None\n name = action.get(\"name\")\n arguments = action.get(\"arguments\") or {}\n if name == \"__respond__\":\n return {\n \"type\": \"respond\",\n \"content\": str(arguments.get(\"content\") or STOP_TOKEN),\n }\n if not isinstance(name, str) or not isinstance(arguments, dict):\n return None\n return {\n \"type\": \"tool_call\",\n \"name\": name,\n \"arguments\": deepcopy(arguments),\n }\n\n\ndef schema_normalize_action(\n action: dict[str, Any] | None,\n inventory: list[dict[str, Any]],\n) -> dict[str, Any] | None:\n \"\"\"Drop arguments that are not present in the selected tool schema.\n\n This is a student-only syntactic postprocessor. It does not call a teacher,\n replay the environment, or fill missing values; it only prevents malformed\n extra keyword arguments from reaching tau2 tools.\n \"\"\"\n if not isinstance(action, dict):\n return action\n schema_by_name = {\n item.get(\"name\"): set((item.get(\"arguments\") or {}).keys())\n for item in inventory\n if item.get(\"name\")\n }\n\n def normalize_call(call: dict[str, Any]) -> dict[str, Any]:\n out = deepcopy(call)\n name = out.get(\"name\")\n args = out.get(\"arguments\") or {}\n allowed = schema_by_name.get(name)\n if allowed is not None and isinstance(args, dict):\n out[\"arguments\"] = {key: value for key, value in args.items() if key in allowed}\n return out\n\n action_type = str(action.get(\"type\") or \"\").lower()\n if not action_type and action.get(\"name\"):\n action_type = \"tool_call\"\n if action_type == \"tool_call\" or action.get(\"name\"):\n return normalize_call(action)\n if action_type == \"tool_calls\" and isinstance(action.get(\"calls\"), list):\n out = deepcopy(action)\n out[\"calls\"] = [\n normalize_call(call) if isinstance(call, dict) else call\n for call in action.get(\"calls\", [])\n ]\n return out\n return action\n\n\ndef task_user_scenario_text(task: Any) -> str:\n return str(getattr(task, \"user_scenario\", \"\") or \"\")\n\n\ndef load_learned_selector(path: str) -> dict[str, Any] | None:\n if not path:\n return None\n payload = json.loads(Path(path).read_text(encoding=\"utf-8\"))\n payload[\"vocab\"] = {str(key): int(value) for key, value in payload[\"vocab\"].items()}\n payload[\"weights\"] = [float(value) for value in payload[\"weights\"]]\n payload[\"bias\"] = float(payload[\"bias\"])\n return payload\n\n\ndef learned_selector_score(\n *,\n selector_model: dict[str, Any],\n domain: str,\n task: Any,\n previous_tool_actions: list[dict[str, Any]],\n candidate: dict[str, Any],\n) -> float | None:\n candidate_action = parsed_tool_action_with_requestor(candidate)\n if candidate_action is None:\n return None\n row = {\n \"domain\": domain,\n \"user_scenario\": task_user_scenario_text(task),\n \"prefix_actions\": previous_tool_actions,\n \"candidate_action\": candidate_action,\n }\n # Keep this call here as a cheap schema canary: if future feature sets add\n # required fields, this will fail before a large remote evaluation starts.\n selector_feature_tokens(row, selector_model[\"feature_set\"])\n vec = selector_vectorize(row, selector_model[\"vocab\"], selector_model[\"feature_set\"])\n return selector_score_vec(selector_model[\"weights\"], selector_model[\"bias\"], vec)\n\n\ndef pick_learned_selector_candidate(\n *,\n candidates: list[dict[str, Any]],\n indices: list[int],\n selector_model: dict[str, Any] | None,\n domain: str,\n task: Any,\n previous_tool_actions: list[dict[str, Any]],\n) -> int | None:\n if selector_model is None or not indices:\n return None\n scored: list[tuple[float, int]] = []\n for idx in indices:\n score = learned_selector_score(\n selector_model=selector_model,\n domain=domain,\n task=task,\n previous_tool_actions=previous_tool_actions,\n candidate=candidates[idx],\n )\n if score is None:\n continue\n candidates[idx][\"learned_selector_score\"] = score\n candidates[idx][\"learned_selector_prob\"] = selector_sigmoid(score)\n scored.append((score, idx))\n if not scored:\n return None\n scored.sort(key=lambda item: (-item[0], item[1]))\n return scored[0][1]\n\n\ndef action_name_from_key(key: str) -> str | None:\n try:\n action = json.loads(key)\n except Exception:\n return None\n if not isinstance(action, dict):\n return None\n return action.get(\"name\") if isinstance(action.get(\"name\"), str) else None\n\n\ndef is_mutation_tool(name: str | None) -> bool:\n if not name:\n return False\n prefixes = (\n \"book_\",\n \"cancel_\",\n \"exchange_\",\n \"modify_\",\n \"return_\",\n \"update_\",\n )\n return name.startswith(prefixes)\n\n\ndef is_lookup_tool(name: str | None) -> bool:\n if not name:\n return False\n return name.startswith((\"find_\", \"get_\", \"list_\", \"search_\"))\n\n\ndef is_safe_tool_candidate(item: dict[str, Any]) -> bool:\n return (\n item.get(\"message_kind\") == \"tool_call\"\n and not item[\"meta\"][\"invalid_action\"]\n and item.get(\"replay_tool_errors\") == 0\n )\n\n\ndef is_safe_structured_candidate(item: dict[str, Any]) -> bool:\n if item.get(\"candidate_source\") != \"structured\" or item[\"meta\"][\"invalid_action\"]:\n return False\n return is_safe_tool_candidate(item) or item.get(\"message_kind\") == \"respond\"\n\n\ndef normalize_policy_action(action: dict[str, Any] | None) -> dict[str, Any] | None:\n if not isinstance(action, dict):\n return None\n action_type = str(action.get(\"type\") or \"\").lower()\n if not action_type and action.get(\"name\"):\n action_type = \"tool_call\"\n if action_type == \"tool_call\":\n name = action.get(\"name\")\n arguments = action.get(\"arguments\") or {}\n if not isinstance(name, str) or not isinstance(arguments, dict):\n return None\n return {\n \"type\": \"tool_call\",\n \"name\": name,\n \"arguments\": deepcopy(arguments),\n }\n if action_type in {\"respond\", \"stop\", \"done\"} or action.get(\"content\") is not None:\n return {\n \"type\": \"respond\",\n \"content\": str(action.get(\"content\") or \"\"),\n }\n return None\n\n\ndef canonical_policy_action(action: dict[str, Any] | None, *, ignore_respond_content: bool = True) -> str | None:\n normalized = normalize_policy_action(action)\n if normalized is None:\n return None\n if ignore_respond_content and normalized[\"type\"] == \"respond\":\n normalized = {\"type\": \"respond\"}\n return json.dumps(normalized, sort_keys=True, separators=(\",\", \":\"))\n\n\ndef reference_actions_for_task(task: Any, *, solo_mode: bool) -> list[dict[str, Any]]:\n criteria = task.evaluation_criteria\n actions = [normalize_action(action) for action in (criteria.actions or [])]\n if not solo_mode:\n actions = [action for action in actions if action[\"requestor\"] == \"assistant\"]\n out = [\n {\n \"type\": \"tool_call\",\n \"name\": action[\"name\"],\n \"arguments\": deepcopy(action.get(\"arguments\") or {}),\n }\n for action in actions\n ]\n out.append({\"type\": \"respond\", \"content\": get_communicate_text(task)})\n return out\n\n\ndef oracle_action_score(candidate: dict[str, Any], target: dict[str, Any]) -> tuple[int, int, int]:\n if candidate[\"meta\"][\"invalid_action\"]:\n return (10_000, 0, 0)\n cand = normalize_policy_action(candidate.get(\"parsed_action\"))\n if cand is None:\n return (10_000, 0, 0)\n if target[\"type\"] == \"respond\":\n if cand[\"type\"] != \"respond\":\n return (1_000, 0, 0)\n has_stop = int(STOP_TOKEN in str(cand.get(\"content\", \"\")))\n return (0 if has_stop else 5, 1, has_stop)\n if cand[\"type\"] != \"tool_call\":\n return (1_000, 0, 0)\n if cand.get(\"name\") != target.get(\"name\"):\n return (100, 0, 0)\n cand_args = cand.get(\"arguments\") or {}\n target_args = target.get(\"arguments\") or {}\n keys = set(cand_args) | set(target_args)\n mismatches = sum(1 for key in keys if cand_args.get(key) != target_args.get(key))\n exact = int(mismatches == 0)\n return (0 if exact else 10 + mismatches, 1, exact)\n\n\ndef select_reference_oracle_candidate(\n candidates: list[dict[str, Any]],\n args: argparse.Namespace,\n selector_model: dict[str, Any] | None,\n domain: str,\n task: Any,\n reference_actions: list[dict[str, Any]],\n reference_index: int,\n previous_action_keys: list[str],\n previous_tool_actions: list[dict[str, Any]],\n) -> tuple[int, dict[str, Any]]:\n fallback_idx = select_candidate(\n candidates,\n args,\n selector_model,\n domain,\n task,\n previous_action_keys,\n previous_tool_actions,\n )\n meta = {\n \"reference_oracle_used\": int(args.reference_oracle_selector != \"none\"),\n \"reference_index_before\": reference_index,\n \"reference_target\": None,\n \"reference_oracle_score\": None,\n \"reference_oracle_same_name\": 0,\n \"reference_oracle_exact\": 0,\n \"reference_oracle_advanced\": 0,\n \"reference_oracle_fallback\": 1,\n }\n if args.reference_oracle_selector == \"none\" or reference_index >= len(reference_actions):\n return fallback_idx, meta\n target = reference_actions[reference_index]\n meta[\"reference_target\"] = target\n scored = [\n (oracle_action_score(item, target), idx)\n for idx, item in enumerate(candidates)\n ]\n scored.sort(key=lambda item: (item[0], item[1]))\n best_score, best_idx = scored[0]\n score_value, same_name, exact = best_score\n threshold = 0 if args.reference_oracle_selector == \"exact\" else 99\n if score_value > threshold:\n return fallback_idx, meta\n meta.update(\n {\n \"reference_oracle_score\": score_value,\n \"reference_oracle_same_name\": same_name,\n \"reference_oracle_exact\": exact,\n \"reference_oracle_advanced\": int(bool(exact)),\n \"reference_oracle_fallback\": 0,\n }\n )\n return best_idx, meta\n\n\ndef pick_progress_candidate(candidates: list[dict[str, Any]], indices: list[int], previous_keys: list[str]) -> int:\n if not indices:\n return 0\n seen = set(previous_keys)\n last = previous_keys[-1] if previous_keys else None\n\n def score(idx: int) -> tuple[int, int]:\n key = tool_action_key(candidates[idx])\n repeat_penalty = 0\n if key is not None and key in seen:\n repeat_penalty += 10\n if key is not None and key == last:\n repeat_penalty += 20\n return repeat_penalty, idx\n\n return min(indices, key=score)\n\n\ndef semantic_progress_score(\n candidates: list[dict[str, Any]],\n idx: int,\n previous_keys: list[str],\n args: argparse.Namespace,\n) -> tuple[int, int, int, int, int, int]:\n item = candidates[idx]\n key = tool_action_key(item)\n parsed = parsed_tool_action(item)\n name = parsed[\"name\"] if parsed else None\n previous_names = [action_name_from_key(prev) for prev in previous_keys]\n seen = set(previous_keys)\n last = previous_keys[-1] if previous_keys else None\n tool_steps = len(previous_keys)\n mutation_steps = sum(int(is_mutation_tool(name)) for name in previous_names)\n has_mutation_candidate = any(\n is_mutation_tool((parsed_tool_action(candidates[j]) or {}).get(\"name\"))\n for j in range(len(candidates))\n if candidates[j].get(\"message_kind\") == \"tool_call\"\n and not candidates[j][\"meta\"][\"invalid_action\"]\n and candidates[j].get(\"replay_tool_errors\") == 0\n )\n\n repeat_penalty = 0\n if key is not None and key in seen:\n repeat_penalty += 40\n if key is not None and key == last:\n repeat_penalty += 80\n\n lookup_penalty = 0\n if is_lookup_tool(name) and has_mutation_candidate and tool_steps >= args.progress_mutation_min_tools:\n lookup_penalty += 12\n if is_lookup_tool(name) and tool_steps >= args.progress_query_budget:\n lookup_penalty += 8\n\n mutation_bonus = 0\n if is_mutation_tool(name) and tool_steps >= args.progress_mutation_min_tools:\n mutation_bonus -= 12\n if is_mutation_tool(name) and mutation_steps == 0 and tool_steps >= args.progress_first_mutation_min_tools:\n mutation_bonus -= 12\n\n error_score = int(item.get(\"replay_tool_errors\") or 0)\n return (\n error_score,\n repeat_penalty,\n lookup_penalty + mutation_bonus,\n 0 if is_mutation_tool(name) else 1,\n 0 if is_lookup_tool(name) else 1,\n idx,\n )\n\n\ndef pick_semantic_progress_candidate(\n candidates: list[dict[str, Any]],\n indices: list[int],\n previous_keys: list[str],\n args: argparse.Namespace,\n) -> int:\n if not indices:\n return 0\n return min(indices, key=lambda idx: semantic_progress_score(candidates, idx, previous_keys, args))\n\n\ndef load_car_effects(car_src: str) -> tuple[Any, Any, Any, str]:\n \"\"\"Load CAR's published outcome-distribution estimator without vendoring it.\"\"\"\n src = str(Path(car_src))\n if src not in sys.path:\n sys.path.insert(0, src)\n try:\n from car.attribute.effects import OutcomeDistribution, prob_label_effect\n from car.schemas.trajectory import Outcome\n\n return Outcome, OutcomeDistribution, prob_label_effect, \"official_car\"\n except ImportError:\n class Outcome: # type: ignore[no-redef]\n def __init__(self, *, label: str, score: float) -> None:\n self.label = label\n self.score = score\n\n class OutcomeDistribution: # type: ignore[no-redef]\n def __init__(self, *, scores: list[float], labels: list[str]) -> None:\n self.scores = scores\n self.labels = labels\n\n @classmethod\n def from_outcomes(cls, outcomes: list[Any]) -> Any:\n return cls(\n scores=[float(outcome.score) for outcome in outcomes],\n labels=[str(outcome.label) for outcome in outcomes],\n )\n\n @property\n def n(self) -> int:\n return len(self.scores)\n\n def label_indicator(self, label: str) -> list[float]:\n return [1.0 if item == label else 0.0 for item in self.labels]\n\n class EffectEstimate:\n def __init__(\n self,\n *,\n point: float,\n ci_low: float,\n ci_high: float,\n confidence: float,\n n_baseline: int,\n n_intervened: int,\n ) -> None:\n self.point = point\n self.ci_low = ci_low\n self.ci_high = ci_high\n self.confidence = confidence\n self.n_baseline = n_baseline\n self.n_intervened = n_intervened\n\n @property\n def is_significant(self) -> bool:\n return self.ci_low > 0.0 or self.ci_high < 0.0\n\n def prob_label_effect( # type: ignore[no-redef]\n baseline: Any,\n intervened: Any,\n label: str,\n *,\n confidence: float = 0.95,\n n_boot: int = 2000,\n seed: int = 0,\n ) -> EffectEstimate:\n base = baseline.label_indicator(label)\n branch = intervened.label_indicator(label)\n\n def mean(items: list[float]) -> float:\n return sum(items) / len(items) if items else 0.0\n\n point = mean(branch) - mean(base)\n rng = random.Random(seed)\n diffs = []\n for _ in range(n_boot):\n rb = [branch[rng.randrange(len(branch))] for _ in branch] if branch else []\n ra = [base[rng.randrange(len(base))] for _ in base] if base else []\n diffs.append(mean(rb) - mean(ra))\n diffs.sort()\n alpha = (1.0 - confidence) / 2.0\n low_idx = min(max(int(alpha * len(diffs)), 0), len(diffs) - 1) if diffs else 0\n high_idx = min(max(int((1.0 - alpha) * len(diffs)) - 1, 0), len(diffs) - 1) if diffs else 0\n return EffectEstimate(\n point=point,\n ci_low=diffs[low_idx] if diffs else point,\n ci_high=diffs[high_idx] if diffs else point,\n confidence=confidence,\n n_baseline=baseline.n,\n n_intervened=intervened.n,\n )\n\n return Outcome, OutcomeDistribution, prob_label_effect, \"fallback_bootstrap\"\n\n\ndef car_candidate_rescues_immediate_failure(item: dict[str, Any]) -> bool:\n \"\"\"Immediate executable-outcome proxy for CAR inside an online tau2 rollout.\n\n CAR's native baseline re-runs the policy forward and scores final outcomes.\n In this online free-form adapter we can faithfully measure only the local\n executable outcome before committing the action: invalid parses and tool\n errors are bad outcomes, zero-error tool calls are rescued outcomes. Valid\n final responses are treated as rescuing the immediate tool-error outcome;\n premature-stop risk is measured later by tau2's official final reward.\n \"\"\"\n if item[\"meta\"][\"invalid_action\"]:\n return False\n if item.get(\"message_kind\") == \"tool_call\":\n return int(item.get(\"replay_tool_errors\") or 0) == 0\n if item.get(\"message_kind\") == \"respond\":\n return True\n return False\n\n\ndef select_car_candidate(candidates: list[dict[str, Any]], args: argparse.Namespace) -> tuple[int, dict[str, Any]]:\n if not candidates or args.published_selector == \"none\":\n return 0, {}\n greedy = candidates[0]\n if (\n greedy.get(\"message_kind\") == \"respond\"\n and not greedy[\"meta\"][\"invalid_action\"]\n and not args.published_selector_override_respond\n ):\n return 0, {\n \"published_selector_used\": 1,\n \"published_selector_mode\": args.published_selector,\n \"published_selector_fallback\": 1,\n \"published_selector_effect_point\": None,\n \"published_selector_effect_low\": None,\n \"published_selector_effect_high\": None,\n \"published_selector_significant\": 0,\n \"published_selector_reason\": \"kept_greedy_respond\",\n }\n\n Outcome, OutcomeDistribution, prob_label_effect, car_impl = load_car_effects(args.car_src)\n baseline = OutcomeDistribution.from_outcomes([Outcome(label=\"fail\", score=1.0)])\n outcomes = [\n Outcome(\n label=\"ok\" if car_candidate_rescues_immediate_failure(item) else \"fail\",\n score=0.0 if car_candidate_rescues_immediate_failure(item) else 1.0,\n )\n for item in candidates[: args.car_replay_budget]\n ]\n dist = OutcomeDistribution.from_outcomes(outcomes)\n effect = prob_label_effect(\n baseline,\n dist,\n \"fail\",\n confidence=args.car_confidence,\n seed=args.seed,\n )\n rescuing = [\n idx\n for idx, item in enumerate(candidates[: args.car_replay_budget])\n if car_candidate_rescues_immediate_failure(item)\n ]\n select_rescue = False\n reason = \"no_rescuing_candidate\"\n if rescuing:\n if args.published_selector == \"car_ci\":\n select_rescue = bool(effect.is_significant and effect.point < 0.0)\n reason = \"ci_rescue\" if select_rescue else \"ci_not_significant\"\n elif args.published_selector == \"car_point\":\n select_rescue = True\n reason = \"point_rescue\"\n selected = rescuing[0] if select_rescue else 0\n return selected, {\n \"published_selector_used\": 1,\n \"published_selector_mode\": args.published_selector,\n \"published_selector_fallback\": int(not select_rescue),\n \"published_selector_effect_point\": float(effect.point),\n \"published_selector_effect_low\": float(effect.ci_low),\n \"published_selector_effect_high\": float(effect.ci_high),\n \"published_selector_significant\": int(bool(effect.is_significant)),\n \"published_selector_reason\": reason,\n \"published_selector_impl\": car_impl,\n \"published_selector_rescuing_candidates\": len(rescuing),\n \"published_selector_candidate_outcomes\": [\n \"ok\" if car_candidate_rescues_immediate_failure(item) else \"fail\"\n for item in candidates[: args.car_replay_budget]\n ],\n }\n\n\ndef select_policy_lookup_structured_critical_candidate(\n candidates: list[dict[str, Any]],\n args: argparse.Namespace,\n previous_action_keys: list[str],\n) -> int | None:\n \"\"\"Route-B hybrid gate: policy handles low-risk lookup; structured handles critical steps.\"\"\"\n structured_safe = [\n idx\n for idx, item in enumerate(candidates)\n if is_safe_structured_candidate(item)\n ]\n if not structured_safe:\n return None\n structured_idx = structured_safe[0]\n structured_key = canonical_policy_action(candidates[structured_idx].get(\"parsed_action\"))\n\n seen = set(previous_action_keys)\n policy_safe_tools = [\n idx\n for idx, item in enumerate(candidates)\n if item.get(\"candidate_source\", \"policy\") == \"policy\"\n and is_safe_tool_candidate(item)\n and tool_action_key(item) not in seen\n ]\n\n # If the student independently proposes the same verified action as the\n # fallback planner, credit the student and execute the policy action.\n for idx in policy_safe_tools:\n policy_key = canonical_policy_action(candidates[idx].get(\"parsed_action\"))\n if policy_key is not None and policy_key == structured_key:\n return idx\n\n # Allow a small number of student-chosen information-gathering steps. State\n # changes and final stopping remain critical steps and fall back to the\n # verified structured source unless the policy exactly matches it above.\n if len(previous_action_keys) < args.hybrid_policy_lookup_budget:\n lookup_policy = [\n idx\n for idx in policy_safe_tools\n if is_lookup_tool((parsed_tool_action(candidates[idx]) or {}).get(\"name\"))\n ]\n if lookup_policy:\n return pick_semantic_progress_candidate(candidates, lookup_policy, previous_action_keys, args)\n\n return structured_idx\n\n\ndef select_candidate(\n candidates: list[dict[str, Any]],\n args: argparse.Namespace,\n selector_model: dict[str, Any] | None,\n domain: str,\n task: Any,\n previous_action_keys: list[str],\n previous_tool_actions: list[dict[str, Any]],\n) -> int:\n if not candidates:\n return 0\n greedy = candidates[0]\n if not args.replay_filter_tool_errors:\n return 0\n if args.published_selector != \"none\":\n selected_idx, _ = select_car_candidate(candidates, args)\n return selected_idx\n zero_error_tools = [\n idx\n for idx, item in enumerate(candidates)\n if is_safe_tool_candidate(item)\n ]\n if args.hybrid_rollout_mode == \"policy_lookup_structured_critical\":\n hybrid_idx = select_policy_lookup_structured_critical_candidate(\n candidates,\n args,\n previous_action_keys,\n )\n if hybrid_idx is not None:\n return hybrid_idx\n if args.structured_priority:\n structured_safe = [\n idx\n for idx, item in enumerate(candidates)\n if is_safe_structured_candidate(item)\n ]\n if structured_safe:\n return structured_safe[0]\n # If greedy decides to finish, keep that decision unless a progress selector\n # is explicitly allowed to continue. This avoids changing established runs\n # while letting bottleneck experiments test premature-stop mitigation.\n if greedy.get(\"message_kind\") == \"respond\" and not greedy[\"meta\"][\"invalid_action\"]:\n allow_continue = (\n args.progress_loop_guard\n and args.loop_guard_override_respond\n and zero_error_tools\n ) or (\n args.semantic_progress_selector\n and zero_error_tools\n and len(previous_action_keys) < args.progress_min_tools_before_respond\n )\n if not allow_continue:\n return 0\n if args.progress_loop_guard and args.loop_guard_override_respond and greedy.get(\"message_kind\") == \"respond\":\n non_repeated = [\n idx\n for idx in zero_error_tools\n if tool_action_key(candidates[idx]) not in set(previous_action_keys)\n ]\n if non_repeated:\n return pick_progress_candidate(candidates, non_repeated, previous_action_keys)\n if zero_error_tools:\n learned_idx = pick_learned_selector_candidate(\n candidates=candidates,\n indices=zero_error_tools,\n selector_model=selector_model,\n domain=domain,\n task=task,\n previous_tool_actions=previous_tool_actions,\n )\n if learned_idx is not None:\n return learned_idx\n if args.semantic_progress_selector:\n return pick_semantic_progress_candidate(candidates, zero_error_tools, previous_action_keys, args)\n if args.progress_loop_guard:\n return pick_progress_candidate(candidates, zero_error_tools, previous_action_keys)\n return zero_error_tools[0]\n valid = [\n idx\n for idx, item in enumerate(candidates)\n if not item[\"meta\"][\"invalid_action\"]\n ]\n return valid[0] if valid else 0\n\n\ndef make_hf_policy_agent_class(\n *,\n model: Any,\n tokenizer: Any,\n prompt_assets: dict[str, tuple[str, list[dict[str, Any]]]],\n args: argparse.Namespace,\n) -> Any:\n tau2 = import_tau2()\n HalfDuplexAgent = tau2[\"HalfDuplexAgent\"]\n MultiToolMessage = tau2[\"MultiToolMessage\"]\n selector_model = load_learned_selector(args.selector_model)\n\n class GymAgent(HalfDuplexAgent[dict[str, Any]]):\n \"\"\"Class name intentionally matches tau2's solo-mode allowlist.\"\"\"\n\n def __init__(\n self,\n tools: list[Any],\n domain_policy: str,\n task: Any,\n *,\n domain: str,\n solo_mode: bool,\n environment: Any,\n ) -> None:\n super().__init__(tools=tools, domain_policy=domain_policy)\n self.task = task\n self.domain = domain\n self.solo_mode = solo_mode\n self.environment = environment\n self.reference_actions = reference_actions_for_task(task, solo_mode=solo_mode)\n self.structured_planner = (\n StructuredPlanner(domain, task, environment)\n if args.structured_proposal_source\n else None\n )\n self.generation_logs: list[dict[str, Any]] = []\n\n @classmethod\n def is_stop(cls, message: Any) -> bool:\n return bool(getattr(message, \"content\", None) and STOP_TOKEN in message.content)\n\n def set_seed(self, seed: int) -> None:\n return None\n\n def get_init_state(self, message_history: list[Any] | None = None) -> dict[str, Any]:\n state = {\n \"messages\": list(message_history or []),\n \"next_action\": 0,\n \"selected_action_keys\": [],\n \"selected_tool_actions\": [],\n \"reference_index\": 0,\n }\n if self.structured_planner is not None:\n state[\"structured_state\"] = self.structured_planner.init_state()\n return state\n\n def _ingest_structured_tool_messages(self, message: Any, state: dict[str, Any]) -> None:\n if self.structured_planner is None or message is None:\n return\n structured_state = state.setdefault(\"structured_state\", self.structured_planner.init_state())\n if isinstance(message, MultiToolMessage):\n tool_messages = list(message.tool_messages)\n structured_state[\"messages\"].extend(tool_messages)\n else:\n tool_messages = [message] if message.__class__.__name__ == \"ToolMessage\" else []\n structured_state[\"messages\"].append(message)\n pending = list(structured_state.get(\"pending_actions\") or [])\n for action, tool_message in zip(pending, tool_messages):\n parsed = structured_parse_tool_content(getattr(tool_message, \"content\", None))\n self.structured_planner.record_result(\n structured_state,\n action,\n parsed,\n bool(getattr(tool_message, \"error\", False)),\n )\n structured_state[\"pending_actions\"] = []\n\n def _structured_candidate(self, state: dict[str, Any], next_candidate_index: int) -> dict[str, Any] | None:\n if self.structured_planner is None:\n return None\n structured_state = state.setdefault(\"structured_state\", self.structured_planner.init_state())\n structured_action = self.structured_planner.next_action(structured_state)\n if structured_action is None:\n return None\n if structured_action.get(\"name\") == \"__respond__\" and not args.structured_proposal_include_respond:\n return None\n action = structured_action_to_policy_action(structured_action)\n if action is None:\n return None\n assistant_message, meta = action_to_message(\n action,\n parse_error=\"\",\n counter=state[\"next_action\"],\n solo_mode=self.solo_mode,\n )\n replay_errors = replay_tool_error_count(assistant_message, self.environment)\n message_kind = (\n \"tool_call\"\n if is_tool_message(assistant_message)\n else \"respond\"\n if getattr(assistant_message, \"content\", None)\n else \"invalid\"\n )\n return {\n \"candidate_index\": next_candidate_index,\n \"candidate_source\": \"structured\",\n \"raw_generation\": json.dumps(action, ensure_ascii=False, sort_keys=True),\n \"parsed_action\": action,\n \"structured_rule\": structured_action.get(\"rule\"),\n \"meta\": meta,\n \"message\": assistant_message,\n \"message_kind\": message_kind,\n \"replay_tool_errors\": replay_errors,\n }\n\n def _track_selected_tool_for_structured(self, message: Any, state: dict[str, Any]) -> None:\n if self.structured_planner is None:\n return\n actions = selected_message_tool_actions(message)\n if not actions:\n return\n structured_state = state.setdefault(\"structured_state\", self.structured_planner.init_state())\n structured_state[\"pending_actions\"] = deepcopy(actions)\n for action in actions:\n structured_state[\"action_keys\"].add(structured_action_key(action))\n\n def generate_next_message(\n self, message: Any, state: dict[str, Any]\n ) -> tuple[Any, dict[str, Any]]:\n if message is not None:\n if isinstance(message, MultiToolMessage):\n state[\"messages\"].extend(message.tool_messages)\n else:\n state[\"messages\"].append(message)\n self._ingest_structured_tool_messages(message, state)\n\n history = [\n item.model_dump(mode=\"json\") if hasattr(item, \"model_dump\") else dict(item)\n for item in state[\"messages\"]\n ]\n domain_policy, inventory = prompt_assets[self.domain]\n user_prompt = build_policy_user_prompt(\n domain=self.domain,\n task_id=str(self.task.id),\n task=self.task,\n domain_policy=domain_policy,\n inventory=inventory,\n history=history,\n max_tools=args.max_tools,\n max_policy_chars=args.max_policy_chars,\n max_tool_content_chars=args.max_tool_content_chars,\n max_history_messages=args.max_history_messages,\n tool_description_chars=args.tool_description_chars,\n tool_arg_description_chars=args.tool_arg_description_chars,\n omit_tool_descriptions=args.omit_tool_descriptions,\n compact_json_prompt=args.compact_json_prompt,\n )\n if args.hard_mode_prompt:\n user_prompt = user_prompt + \"\\n\\n\" + hard_mode_prompt_suffix(self.domain)\n if args.structured_only:\n raw_candidates = []\n else:\n raw_candidates = generate_many(\n model=model,\n tokenizer=tokenizer,\n prompt=user_prompt,\n max_input_tokens=args.max_input_tokens,\n max_new_tokens=args.max_new_tokens,\n num_candidates=args.num_candidates,\n temperature=args.candidate_temperature,\n top_p=args.candidate_top_p,\n json_prefill=args.json_prefill,\n ) if not args.api_model else generate_many_api(prompt=user_prompt, args=args)\n candidates = []\n for cand_idx, raw in enumerate(raw_candidates):\n action, parse_error = extract_json_object(raw)\n if args.schema_normalize_actions:\n action = schema_normalize_action(action, inventory)\n assistant_message, meta = action_to_message(\n action,\n parse_error=parse_error,\n counter=state[\"next_action\"],\n solo_mode=self.solo_mode,\n )\n replay_errors = replay_tool_error_count(\n assistant_message,\n self.environment,\n )\n message_kind = (\n \"tool_call\"\n if is_tool_message(assistant_message)\n else \"respond\"\n if getattr(assistant_message, \"content\", None)\n else \"invalid\"\n )\n candidates.append(\n {\n \"candidate_index\": cand_idx,\n \"candidate_source\": \"policy\",\n \"raw_generation\": raw,\n \"parsed_action\": action,\n \"meta\": meta,\n \"message\": assistant_message,\n \"message_kind\": message_kind,\n \"replay_tool_errors\": replay_errors,\n }\n )\n structured_candidate = self._structured_candidate(state, len(candidates))\n if structured_candidate is not None:\n candidates.append(structured_candidate)\n if not candidates:\n action = {\"type\": \"stop\", \"content\": STOP_TOKEN}\n assistant_message, meta = action_to_message(\n action,\n parse_error=\"\",\n counter=state[\"next_action\"],\n solo_mode=self.solo_mode,\n )\n candidates.append(\n {\n \"candidate_index\": 0,\n \"candidate_source\": \"fallback_stop\",\n \"raw_generation\": json.dumps(action, ensure_ascii=False, sort_keys=True),\n \"parsed_action\": action,\n \"meta\": meta,\n \"message\": assistant_message,\n \"message_kind\": \"respond\",\n \"replay_tool_errors\": None,\n }\n )\n selected_idx, selector_meta = select_reference_oracle_candidate(\n candidates,\n args,\n selector_model,\n self.domain,\n self.task,\n self.reference_actions,\n int(state.get(\"reference_index\", 0)),\n state.get(\"selected_action_keys\", []),\n state.get(\"selected_tool_actions\", []),\n )\n if args.published_selector != \"none\":\n selected_idx, published_meta = select_car_candidate(candidates, args)\n selector_meta.update(published_meta)\n selected = candidates[selected_idx]\n assistant_message = selected[\"message\"]\n meta = selected[\"meta\"]\n selected_action_key = tool_action_key(selected)\n log_row = {\n \"step\": state[\"next_action\"],\n \"raw_generation\": selected[\"raw_generation\"],\n \"parsed_action\": selected[\"parsed_action\"],\n \"selected_action_key\": selected_action_key,\n \"selected_candidate\": selected_idx,\n \"selected_candidate_source\": selected.get(\"candidate_source\", \"policy\"),\n \"num_candidates\": len(candidates),\n \"message_kind\": selected[\"message_kind\"],\n \"replay_tool_errors\": selected[\"replay_tool_errors\"],\n \"structured_proposal_source\": int(bool(args.structured_proposal_source)),\n \"structured_candidate_added\": int(any(item.get(\"candidate_source\") == \"structured\" for item in candidates)),\n \"learned_selector_model\": args.selector_model,\n \"learned_selector_score\": selected.get(\"learned_selector_score\"),\n \"learned_selector_prob\": selected.get(\"learned_selector_prob\"),\n \"progress_loop_guard\": int(bool(args.progress_loop_guard)),\n \"loop_guard_override_respond\": int(bool(args.loop_guard_override_respond)),\n **selector_meta,\n \"candidate_logs\": [\n {\n \"candidate_index\": item[\"candidate_index\"],\n \"candidate_source\": item.get(\"candidate_source\", \"policy\"),\n \"raw_generation\": item[\"raw_generation\"],\n \"parsed_action\": item[\"parsed_action\"],\n \"structured_rule\": item.get(\"structured_rule\"),\n \"action_key\": tool_action_key(item),\n \"message_kind\": item[\"message_kind\"],\n \"replay_tool_errors\": item[\"replay_tool_errors\"],\n \"learned_selector_score\": item.get(\"learned_selector_score\"),\n \"learned_selector_prob\": item.get(\"learned_selector_prob\"),\n **item[\"meta\"],\n }\n for item in candidates\n ],\n **meta,\n }\n self.generation_logs.append(log_row)\n state[\"next_action\"] += 1\n if selector_meta.get(\"reference_oracle_advanced\"):\n state[\"reference_index\"] = int(state.get(\"reference_index\", 0)) + 1\n if selected_action_key is not None:\n state.setdefault(\"selected_action_keys\", []).append(selected_action_key)\n selected_tool_action = parsed_tool_action_with_requestor(selected)\n if selected_tool_action is not None:\n state.setdefault(\"selected_tool_actions\", []).append(selected_tool_action)\n self._track_selected_tool_for_structured(assistant_message, state)\n state[\"messages\"].append(assistant_message)\n return assistant_message, state\n\n return GymAgent\n\n\ndef task_lookup(domains: list[str], split: str) -> dict[tuple[str, str], Any]:\n out = {}\n for domain in domains:\n for task in get_domain_tasks(domain, split):\n out[(domain, str(task.id))] = task\n return out\n\n\ndef select_tasks(args: argparse.Namespace) -> list[tuple[str, Any]]:\n domains = parse_csv_arg(args.domains)\n if args.generated_tasks:\n from tau2.data_model.tasks import Task\n\n selected = []\n for raw in load_task_dicts(Path(args.generated_tasks)):\n task = Task.model_validate(raw)\n instructions = task.user_scenario.instructions\n domain = getattr(instructions, \"domain\", None)\n if not isinstance(domain, str) or not domain:\n task_id = str(task.id)\n if \"-airline-\" in task_id:\n domain = \"airline\"\n elif \"-retail-\" in task_id:\n domain = \"retail\"\n else:\n raise ValueError(f\"cannot infer domain for generated task {task.id}\")\n selected.append((domain, task))\n return selected\n\n if args.eval_tasks:\n rows = load_jsonl(Path(args.eval_tasks))\n lookup = task_lookup(sorted({row[\"domain\"] for row in rows}), args.split)\n return [(row[\"domain\"], lookup[(row[\"domain\"], str(row[\"task_id\"]))]) for row in rows]\n\n selected = []\n rng = __import__(\"random\").Random(args.seed)\n for domain in domains:\n tasks = list(get_domain_tasks(domain, args.split))\n rng.shuffle(tasks)\n if args.max_tasks_per_domain is not None:\n tasks = tasks[: args.max_tasks_per_domain]\n selected.extend((domain, task) for task in tasks)\n return selected\n\n\ndef run_policy_simulation(\n *,\n domain: str,\n task: Any,\n model: Any,\n tokenizer: Any,\n prompt_assets: dict[str, tuple[str, list[dict[str, Any]]]],\n args: argparse.Namespace,\n) -> tuple[dict[str, Any], Any, list[dict[str, Any]]]:\n tau2 = import_tau2()\n EvaluationType = tau2[\"EvaluationType\"]\n Orchestrator = tau2[\"Orchestrator\"]\n DummyUser = tau2[\"DummyUser\"]\n run_simulation = tau2[\"run_simulation\"]\n PolicyAgent = make_hf_policy_agent_class(\n model=model,\n tokenizer=tokenizer,\n prompt_assets=prompt_assets,\n args=args,\n )\n ScriptedUser = make_scripted_user_class()\n\n solo_mode = domain in {\"telecom\", \"telecom-workflow\"}\n env = build_environment(domain, solo_mode=solo_mode)\n agent = PolicyAgent(\n tools=env.get_tools(),\n domain_policy=env.get_policy(),\n task=task,\n domain=domain,\n solo_mode=solo_mode,\n environment=env,\n )\n user = DummyUser() if solo_mode else ScriptedUser(task)\n evaluation_type = choose_evaluation_type(domain, args.evaluation_type, task)\n orchestrator = Orchestrator(\n domain=env.get_domain_name(),\n agent=agent,\n user=user,\n environment=env,\n task=task,\n max_steps=args.max_steps,\n max_errors=args.max_errors,\n seed=args.seed,\n solo_mode=solo_mode,\n validate_communication=args.validate_communication,\n )\n sim = run_simulation(orchestrator, evaluation_type=EvaluationType(evaluation_type))\n breakdown = reward_breakdown_dict(sim.reward_info)\n logs = agent.generation_logs\n row = {\n \"domain\": domain,\n \"task_id\": str(task.id),\n \"seed\": args.seed,\n \"model\": args.model,\n \"evaluation_type\": evaluation_type,\n \"solo_mode\": solo_mode,\n \"termination_reason\": str(sim.termination_reason.value),\n \"official_reward\": safe_reward(sim.reward_info),\n \"official_success\": int(safe_reward(sim.reward_info) >= 1.0),\n \"reward_breakdown\": breakdown,\n \"db_reward\": breakdown.get(\"db\"),\n \"env_assertion_reward\": breakdown.get(\"env_assertion\"),\n \"action_reward\": breakdown.get(\"action\"),\n \"communicate_reward\": breakdown.get(\"communicate\"),\n \"num_messages\": len(sim.messages or []),\n \"num_tool_calls\": count_tool_calls(sim.messages),\n \"num_tool_errors\": count_tool_errors(sim.messages),\n \"tool_error_details\": tool_error_details(sim.messages),\n \"policy_steps\": len(logs),\n \"policy_parse_errors\": sum(1 for item in logs if not item[\"parse_ok\"]),\n \"policy_invalid_actions\": sum(1 for item in logs if item[\"invalid_action\"]),\n \"policy_replay_selected_non_greedy\": sum(\n 1 for item in logs if int(item.get(\"selected_candidate\", 0)) != 0\n ),\n \"policy_replay_zero_error_tool_steps\": sum(\n 1 for item in logs if item.get(\"message_kind\") == \"tool_call\" and item.get(\"replay_tool_errors\") == 0\n ),\n \"policy_candidate_steps\": sum(\n 1\n for item in logs\n if any(cand.get(\"candidate_source\", \"policy\") == \"policy\" for cand in item.get(\"candidate_logs\", []))\n ),\n \"policy_zero_error_tool_candidate_steps\": sum(\n 1\n for item in logs\n if any(\n cand.get(\"candidate_source\", \"policy\") == \"policy\"\n and cand.get(\"message_kind\") == \"tool_call\"\n and cand.get(\"replay_tool_errors\") == 0\n and not cand.get(\"invalid_action\", 0)\n for cand in item.get(\"candidate_logs\", [])\n )\n ),\n \"policy_mutation_candidate_steps\": sum(\n 1\n for item in logs\n if any(\n cand.get(\"candidate_source\", \"policy\") == \"policy\"\n and is_mutation_tool((parsed_tool_action(cand) or {}).get(\"name\"))\n for cand in item.get(\"candidate_logs\", [])\n )\n ),\n \"policy_selected_steps\": sum(\n 1 for item in logs if item.get(\"selected_candidate_source\") == \"policy\"\n ),\n \"policy_mutation_selected_steps\": sum(\n 1\n for item in logs\n if item.get(\"selected_candidate_source\") == \"policy\"\n and is_mutation_tool((parsed_tool_action({\"parsed_action\": item.get(\"parsed_action\")}) or {}).get(\"name\"))\n ),\n \"policy_structured_candidate_steps\": sum(\n int(item.get(\"structured_candidate_added\", 0)) for item in logs\n ),\n \"policy_structured_selected_steps\": sum(\n 1 for item in logs if item.get(\"selected_candidate_source\") == \"structured\"\n ),\n \"policy_structured_mutation_candidate_steps\": sum(\n 1\n for item in logs\n if any(\n cand.get(\"candidate_source\") == \"structured\"\n and is_mutation_tool((parsed_tool_action(cand) or {}).get(\"name\"))\n for cand in item.get(\"candidate_logs\", [])\n )\n ),\n \"policy_structured_mutation_selected_steps\": sum(\n 1\n for item in logs\n if item.get(\"selected_candidate_source\") == \"structured\"\n and is_mutation_tool((parsed_tool_action({\"parsed_action\": item.get(\"parsed_action\")}) or {}).get(\"name\"))\n ),\n \"reference_oracle_steps\": sum(int(item.get(\"reference_oracle_used\", 0)) for item in logs),\n \"reference_oracle_exact_steps\": sum(int(item.get(\"reference_oracle_exact\", 0)) for item in logs),\n \"reference_oracle_fallback_steps\": sum(int(item.get(\"reference_oracle_fallback\", 0)) for item in logs),\n \"published_selector_steps\": sum(int(item.get(\"published_selector_used\", 0)) for item in logs),\n \"published_selector_fallback_steps\": sum(int(item.get(\"published_selector_fallback\", 0)) for item in logs),\n \"duration\": float(sim.duration),\n }\n return row, sim, logs\n\n\ndef mean(rows: list[dict[str, Any]], key: str) -> float:\n values = [float(row[key]) for row in rows if row.get(key) is not None]\n return statistics.fmean(values) if values else 0.0\n\n\ndef summarize(rows: list[dict[str, Any]]) -> str:\n lines = [\n \"# tau2 HF Policy Orchestrator Evaluation\",\n \"\",\n \"| group | n | reward | success | env_assertion | action | communicate | tool errors | parse errors | invalid actions | agent_stop | policy selected | policy mut selected | structured cand | structured selected | structured mut selected | oracle exact | oracle fallback | published selector | published fallback |\",\n \"|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|\",\n ]\n groups: dict[str, list[dict[str, Any]]] = {\"all\": rows}\n for domain in sorted({row[\"domain\"] for row in rows}):\n groups[domain] = [row for row in rows if row[\"domain\"] == domain]\n for group, group_rows in groups.items():\n lines.append(\n f\"| {group} | {len(group_rows)} | \"\n f\"{mean(group_rows, 'official_reward'):.3f} | \"\n f\"{mean(group_rows, 'official_success'):.3f} | \"\n f\"{mean(group_rows, 'env_assertion_reward'):.3f} | \"\n f\"{mean(group_rows, 'action_reward'):.3f} | \"\n f\"{mean(group_rows, 'communicate_reward'):.3f} | \"\n f\"{sum(int(row['num_tool_errors']) for row in group_rows)} | \"\n f\"{sum(int(row['policy_parse_errors']) for row in group_rows)} | \"\n f\"{sum(int(row['policy_invalid_actions']) for row in group_rows)} | \"\n f\"{mean([{'x': 1.0 if row['termination_reason'] == 'agent_stop' else 0.0} for row in group_rows], 'x'):.3f} | \"\n f\"{sum(int(row.get('policy_selected_steps', 0)) for row in group_rows)} | \"\n f\"{sum(int(row.get('policy_mutation_selected_steps', 0)) for row in group_rows)} | \"\n f\"{sum(int(row.get('policy_structured_candidate_steps', 0)) for row in group_rows)} | \"\n f\"{sum(int(row.get('policy_structured_selected_steps', 0)) for row in group_rows)} | \"\n f\"{sum(int(row.get('policy_structured_mutation_selected_steps', 0)) for row in group_rows)} | \"\n f\"{sum(int(row.get('reference_oracle_exact_steps', 0)) for row in group_rows)} | \"\n f\"{sum(int(row.get('reference_oracle_fallback_steps', 0)) for row in group_rows)} | \"\n f\"{sum(int(row.get('published_selector_steps', 0)) for row in group_rows)} | \"\n f\"{sum(int(row.get('published_selector_fallback_steps', 0)) for row in group_rows)} |\"\n )\n failures = [row for row in rows if row[\"official_reward\"] < 1.0 or row[\"num_tool_errors\"] or row[\"policy_invalid_actions\"]]\n lines.extend([\"\", f\"Failures or imperfect rewards: {len(failures)}\"])\n for row in failures[:20]:\n lines.append(\n \"- \"\n f\"{row['domain']}:{row['task_id']} reward={row['official_reward']:.3f} \"\n f\"termination={row['termination_reason']} tools={row['num_tool_calls']} \"\n f\"errors={row['num_tool_errors']} parse_errors={row['policy_parse_errors']} \"\n f\"invalid={row['policy_invalid_actions']}\"\n )\n if len(failures) > 20:\n lines.append(f\"- ... {len(failures) - 20} more\")\n lines.append(\"\")\n return \"\\n\".join(lines)\n\n\ndef main() -> None:\n parser = argparse.ArgumentParser()\n parser.add_argument(\"--model\", default=\"\")\n parser.add_argument(\"--api-model\", default=\"\", help=\"OpenAI-compatible chat-completion model name.\")\n parser.add_argument(\"--api-base-url\", default=\"https://api.deepseek.com\")\n parser.add_argument(\"--api-key-env\", default=\"DEEPSEEK_API_KEY\")\n parser.add_argument(\"--api-timeout\", type=int, default=120)\n parser.add_argument(\"--api-retries\", type=int, default=2)\n parser.add_argument(\"--api-response-format\", choices=[\"json_object\", \"none\"], default=\"json_object\")\n parser.add_argument(\n \"--api-failure-as-stop\",\n action=\"store_true\",\n help=\"Convert API failures into a stop candidate so other proposal sources can still be selected.\",\n )\n parser.add_argument(\n \"--structured-only\",\n action=\"store_true\",\n help=\"Skip HF/API policy generation and execute only structured proposal candidates in this harness.\",\n )\n parser.add_argument(\"--domains\", default=\"retail,airline,telecom\")\n parser.add_argument(\"--split\", default=\"base\")\n parser.add_argument(\"--eval-tasks\", default=\"\")\n parser.add_argument(\"--generated-tasks\", default=\"\", help=\"JSON/JSONL generated tau2 Task payloads.\")\n parser.add_argument(\"--seed\", type=int, default=17)\n parser.add_argument(\"--max-tasks-per-domain\", type=int, default=None)\n parser.add_argument(\"--evaluation-type\", choices=[\"auto\", \"env\", \"all\", \"action\", \"communicate\"], default=\"auto\")\n parser.add_argument(\n \"--nl-assertion-model\",\n default=\"\",\n help=\"Override tau2's default LLM judge for NL assertions; useful for OpenAI-compatible mirrors.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=80)\n parser.add_argument(\"--max-errors\", type=int, default=10)\n parser.add_argument(\"--validate-communication\", action=\"store_true\")\n parser.add_argument(\"--max-input-tokens\", type=int, default=8192)\n parser.add_argument(\"--max-new-tokens\", type=int, default=256)\n parser.add_argument(\"--max-tools\", type=int, default=0)\n parser.add_argument(\"--max-policy-chars\", type=int, default=12000)\n parser.add_argument(\"--max-tool-content-chars\", type=int, default=6000)\n parser.add_argument(\"--max-history-messages\", type=int, default=24)\n parser.add_argument(\"--tool-description-chars\", type=int, default=500)\n parser.add_argument(\"--tool-arg-description-chars\", type=int, default=240)\n parser.add_argument(\"--omit-tool-descriptions\", action=\"store_true\")\n parser.add_argument(\"--compact-json-prompt\", action=\"store_true\")\n parser.add_argument(\"--hard-mode-prompt\", action=\"store_true\")\n parser.add_argument(\"--num-candidates\", type=int, default=1)\n parser.add_argument(\"--candidate-temperature\", type=float, default=0.7)\n parser.add_argument(\"--candidate-top-p\", type=float, default=0.95)\n parser.add_argument(\n \"--json-prefill\",\n action=\"store_true\",\n help=\"Student-only constrained decode: prefill the assistant response with '{' before generation.\",\n )\n parser.add_argument(\n \"--schema-normalize-actions\",\n action=\"store_true\",\n help=\"Student-only syntactic postprocess: drop tool arguments absent from the active tau2 tool schema.\",\n )\n parser.add_argument(\"--replay-filter-tool-errors\", action=\"store_true\")\n parser.add_argument(\"--selector-model\", default=\"\", help=\"Optional saved tau2 air/retail selector_model.json.\")\n parser.add_argument(\n \"--structured-proposal-source\",\n action=\"store_true\",\n help=\"Append the deterministic structured proposer as an extra policy candidate source.\",\n )\n parser.add_argument(\n \"--structured-proposal-include-respond\",\n action=\"store_true\",\n help=\"Allow structured proposer response/stop candidates in addition to tool-call candidates.\",\n )\n parser.add_argument(\n \"--structured-priority\",\n action=\"store_true\",\n help=\"Prefer safe structured candidates over policy candidates when both are present.\",\n )\n parser.add_argument(\n \"--hybrid-rollout-mode\",\n choices=[\"none\", \"policy_lookup_structured_critical\"],\n default=\"none\",\n help=\"Route-B hybrid selector: policy handles low-risk lookup, verified structured source handles critical mutation/stop fallback.\",\n )\n parser.add_argument(\n \"--hybrid-policy-lookup-budget\",\n type=int,\n default=2,\n help=\"Maximum non-repeated policy lookup steps before Route-B hybrid falls back to structured candidates.\",\n )\n parser.add_argument(\"--progress-loop-guard\", action=\"store_true\")\n parser.add_argument(\"--loop-guard-override-respond\", action=\"store_true\")\n parser.add_argument(\"--semantic-progress-selector\", action=\"store_true\")\n parser.add_argument(\"--progress-min-tools-before-respond\", type=int, default=0)\n parser.add_argument(\"--progress-mutation-min-tools\", type=int, default=3)\n parser.add_argument(\"--progress-first-mutation-min-tools\", type=int, default=4)\n parser.add_argument(\"--progress-query-budget\", type=int, default=8)\n parser.add_argument(\"--reference-oracle-selector\", choices=[\"none\", \"exact\", \"soft\"], default=\"none\")\n parser.add_argument(\n \"--published-selector\",\n choices=[\"none\", \"car_ci\", \"car_point\"],\n default=\"none\",\n help=\"Published-method selector adapter. CAR uses its official outcome-distribution estimator over the current candidate set.\",\n )\n parser.add_argument(\"--car-src\", default=str(CAR_SRC_DEFAULT))\n parser.add_argument(\"--car-replay-budget\", type=int, default=8)\n parser.add_argument(\"--car-confidence\", type=float, default=0.95)\n parser.add_argument(\n \"--published-selector-override-respond\",\n action=\"store_true\",\n help=\"Allow the CAR adapter to override a valid greedy final response.\",\n )\n parser.add_argument(\"--local-files-only\", action=\"store_true\", default=True)\n parser.add_argument(\"--save-simulations\", action=\"store_true\")\n parser.add_argument(\"--resume\", action=\"store_true\", help=\"Resume from an existing eval_rows.jsonl in out-dir.\")\n parser.add_argument(\"--out-dir\", default=\"outputs/tau2_hf_policy_orchestrator_eval\")\n args = parser.parse_args()\n\n configure_logging()\n configure_tau2_nl_assertion_judge(args.nl_assertion_model)\n if (\n args.structured_only\n or args.structured_proposal_source\n or args.generated_tasks\n ) and StructuredPlanner is None:\n raise RuntimeError(\n \"structured proposal/generation mode requires \"\n \"experiments/tau2_structured_proposal_orchestrator_eval.py\"\n )\n if args.structured_only:\n args.structured_proposal_source = True\n args.structured_proposal_include_respond = True\n args.replay_filter_tool_errors = True\n args.model = args.model or \"structured-only\"\n tokenizer = None\n model = None\n elif args.api_model:\n if not os.environ.get(args.api_key_env):\n raise RuntimeError(f\"{args.api_key_env} is not set\")\n args.model = args.api_model\n tokenizer = None\n model = None\n else:\n if not args.model:\n parser.error(\"--model is required unless --api-model is set\")\n if torch is None or AutoTokenizer is None or AutoModelForCausalLM is None:\n raise RuntimeError(\"torch and transformers are required for --model local HF evaluation\")\n tokenizer = AutoTokenizer.from_pretrained(\n args.model,\n trust_remote_code=True,\n local_files_only=args.local_files_only,\n )\n tokenizer.truncation_side = \"left\"\n if tokenizer.pad_token_id is None:\n tokenizer.pad_token = tokenizer.eos_token\n model = AutoModelForCausalLM.from_pretrained(\n args.model,\n torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,\n trust_remote_code=True,\n local_files_only=args.local_files_only,\n low_cpu_mem_usage=True,\n )\n model.eval()\n if torch.cuda.is_available():\n model.to(\"cuda\")\n\n tasks = select_tasks(args)\n prompt_assets = {\n domain: tool_inventory_for_domain(domain)\n for domain in sorted({domain for domain, _ in tasks})\n }\n\n out_dir = Path(args.out_dir)\n out_dir.mkdir(parents=True, exist_ok=True)\n sims_dir = out_dir / \"simulations\"\n logs_dir = out_dir / \"generation_logs\"\n if args.save_simulations:\n sims_dir.mkdir(parents=True, exist_ok=True)\n logs_dir.mkdir(parents=True, exist_ok=True)\n\n eval_rows_path = out_dir / \"eval_rows.jsonl\"\n summary_path = out_dir / \"summary.md\"\n rows = load_jsonl(eval_rows_path) if args.resume and eval_rows_path.exists() else []\n completed = {(row[\"domain\"], str(row[\"task_id\"])) for row in rows}\n for idx, (domain, task) in enumerate(tasks, start=1):\n task_key = (domain, str(task.id))\n if task_key in completed:\n print(f\"[{idx}/{len(tasks)}] {domain}:{task.id} skipped_existing=1\")\n continue\n row, sim, logs = run_policy_simulation(\n domain=domain,\n task=task,\n model=model,\n tokenizer=tokenizer,\n prompt_assets=prompt_assets,\n args=args,\n )\n rows.append(row)\n write_jsonl(logs_dir / f\"{domain}__{task.id}.jsonl\", logs)\n if args.save_simulations:\n (sims_dir / f\"{domain}__{task.id}.json\").write_text(\n sim.model_dump_json(indent=2),\n encoding=\"utf-8\",\n )\n write_jsonl(eval_rows_path, rows)\n summary_path.write_text(summarize(rows), encoding=\"utf-8\")\n print(\n f\"[{idx}/{len(tasks)}] {domain}:{task.id} \"\n f\"reward={row['official_reward']:.3f} success={row['official_success']} \"\n f\"termination={row['termination_reason']} parse_errors={row['policy_parse_errors']}\"\n )\n\n summary = summarize(rows)\n write_jsonl(eval_rows_path, rows)\n summary_path.write_text(summary, encoding=\"utf-8\")\n print(summary)\n print(f\"Wrote {eval_rows_path}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}}}}