From 0c0dfd64c80efb44dc3acb0dd42520d99ef96293 Mon Sep 17 00:00:00 2001 From: Yichao Liang Date: Sun, 30 Aug 2026 09:54:22 -0400 Subject: [PATCH 1/6] log_viewer: a certified plan's query-free replay gets its own chip The explorer replays a cycle's belief-certified plan for the cycle's remaining requests without a new query, so the replay's interaction verdict has no transcript and the parser dropped it: a cycle that certified a plan showed one explore chip and a 1/1 tally while the early-stop rule judged two attempts. The parser now reads the explorer's capture-gate and replay lines, marks the certified session (diamond) and books each replay as a dashed chip after it, the per-cycle tally counts replays as attempts, and the session page labels the replay's interaction video. Claude-Session: https://claude.ai/code/session_01Pbv9TvedRD2iKyoKMJKx6H --- scripts/log_viewer.py | 122 ++++++++++++++++++++++++++++++++++----- tests/test_log_viewer.py | 84 ++++++++++++++++++++++++++- 2 files changed, 189 insertions(+), 17 deletions(-) diff --git a/scripts/log_viewer.py b/scripts/log_viewer.py index 692cb544b..e42fbd40f 100644 --- a/scripts/log_viewer.py +++ b/scripts/log_viewer.py @@ -140,6 +140,20 @@ # requests (see _parse_info_log). SAVED_EP_RE = re.compile(r"Saved local sandbox query/response to .*[/\\]" r"(\d{3})_([a-z]+)(?:_task\d+)?_\d{8}_\d{6}\.md") +# agent_bilevel_explorer's two certified-plan lines. The first says the +# session just saved (the newest pending explore) submitted a plan that +# passed the belief's capture gate and executes it verbatim as a solve +# attempt. The second says a later request of the same cycle re-executes +# that plan WITHOUT a new query, so its interaction verdict arrives with +# no transcript of its own: the parser books it as a replay of the +# certified session rather than dropping it (or mis-pairing it with the +# next cycle's first explore). +CERTIFIED_RE = re.compile(r"agent_bilevel explorer: the agent's " + r"tool-validated plan passed the belief's " + r"capture gate") +REPLAY_RE = re.compile(r"agent_bilevel explorer: replaying this cycle's " + r"belief-certified plan for train task (\d+) " + r"\((\d+) steps\) without a new query\.") # utils.save_video's announcement of a written video file. Its # //seed/run_ tail names the video # dir the run actually wrote, which the mirrored-layout assumption gets @@ -863,6 +877,10 @@ def _parse_info_log(path: str) -> Dict[str, Any]: "rounds": [{task_idx0: {"solved": bool, "msg": str, "reward": float?}, ...}, ...], "explore": {episode_num: {"reward": float, "terminated": bool, + "certified": bool (the session's plan passed the belief's + capture gate and ran verbatim), "replays": [verdict dicts + of the cycle's later requests that re-executed that plan + without a new query, in order], "accepted": bool, "msg": str, "cycle": int|None}, ...}, "test_round": {episode_num: round_idx, ...}, "round_cycles": [cycle_id|None, ...] parallel to rounds -- the @@ -894,9 +912,13 @@ def _parse_info_log(path: str) -> Dict[str, Any]: explore: Dict[int, Dict[str, Any]] = {} session_cycles: Dict[int, Optional[int]] = {} test_round: Dict[int, int] = {} - pending: List[int] = [] + # Verdict slots (a session's entry in ``explore`` or one of its replay + # entries) awaiting their interaction line, in request order. + pending: List[Dict[str, Any]] = [] pending_test: List[int] = [] - last_explore: Optional[int] = None + last_explore: Optional[Dict[str, Any]] = None + certified: Optional[Dict[str, Any]] = None + newest_session: Optional[Dict[str, Any]] = None cycle: Optional[int] = None resume_cycle: Optional[int] = None done = False @@ -925,6 +947,7 @@ def _parse_info_log(path: str) -> Dict[str, Any]: m = CYCLE_HEADER_RE.match(line) if m: cycle = int(m.group(1)) + certified = None continue if resume_cycle is None: m = RESUME_RE.search(line) @@ -987,33 +1010,48 @@ def _parse_info_log(path: str) -> Dict[str, Any]: # eval). session_cycles[int(m.group(1))] = cycle if m.group(2) == "explore": - pending.append(int(m.group(1))) + newest_session = explore.setdefault( + int(m.group(1)), {}) + pending.append(newest_session) elif m.group(2) == "learn": pending.clear() + certified = None elif m.group(2) == "test": pending_test.append(int(m.group(1))) continue + if CERTIFIED_RE.search(line): + # Belongs to the newest explore session (it saved its + # transcript just before the explorer read the capture). + certified = newest_session + if certified is not None: + certified["certified"] = True + continue + m = REPLAY_RE.search(line) + if m: + if certified is not None: + replay = {"steps": int(m.group(2))} + certified.setdefault("replays", []).append(replay) + pending.append(replay) + continue m = INTERACTION_RE.match(line) if m: last_explore = pending.pop(0) if pending else None if last_explore is not None: - explore[last_explore] = { + last_explore.update({ "reward": float(m.group(1)), "terminated": m.group(2) == "True", "accepted": m.group(3) == "True", "msg": "", "cycle": cycle, - } + }) continue m = INTERACTION_REJECT_RE.match(line) if m and last_explore is not None: - explore[last_explore]["msg"] = ("REJECTED: " + - m.group(1).strip()) + last_explore.update(msg="REJECTED: " + m.group(1).strip()) continue m = INTERACTION_BAR_RE.match(line) if m and last_explore is not None: - explore[last_explore]["msg"] = ("solved but " + - m.group(1).strip()) + last_explore.update(msg="solved but " + m.group(1).strip()) if current: # run still in progress or crashed mid-round rounds.append(current) round_cycles.append(cycle) @@ -1057,11 +1095,15 @@ def _explore_results( out: List[Tuple[Optional[int], int, int, float]] = [] for (kind, num) in sorted(by_key): eps = by_key[(kind, num)] - rewards = [ep["env_reward"] for ep in eps if "env_reward" in ep] - if not rewards: + # A certified plan's query-free replays are real attempts too (the + # early-stop rule counts every attempt), so they join the tally. + attempts = [ep for ep in eps if "env_reward" in ep] + attempts += [r for ep in eps for r in ep.get("replays", [])] + if not attempts: continue - solved = sum(1 for ep in eps if ep.get("env_accepted")) - out.append((num if kind == 0 else None, solved, len(eps), + rewards = [a["env_reward"] for a in attempts] + solved = sum(1 for a in attempts if a.get("env_accepted")) + out.append((num if kind == 0 else None, solved, len(attempts), sum(rewards) / len(rewards))) return out @@ -1166,7 +1208,7 @@ def _cycle_row(ep: Dict[str, Any]) -> Optional[int]: interactions_seen = 0 if ep["kind"] == "explore": verdict = explore_verdicts.get(ep["num"]) - if verdict is not None: + if verdict is not None and "accepted" in verdict: ep["env_reward"] = verdict["reward"] ep["env_terminated"] = verdict["terminated"] ep["env_accepted"] = verdict["accepted"] @@ -1178,6 +1220,26 @@ def _cycle_row(ep: Dict[str, Any]) -> Optional[int]: # i-th among the cycle's verdict-earning explore sessions. ep["interaction_idx"] = interactions_seen interactions_seen += 1 + if verdict is not None and verdict.get("certified"): + ep["certified"] = True + # The cycle's later requests replayed this session's plan + # without a query: real attempts (they count for the + # early-stop rule and own the next __ep videos) that + # left no transcript, so they ride on this session. + replays = [] + for r in verdict.get("replays", []): + if "accepted" not in r: + continue + replays.append({ + "env_reward": r["reward"], + "env_terminated": r["terminated"], + "env_accepted": r["accepted"], + "env_msg": r["msg"], + "steps": r["steps"], + "interaction_idx": interactions_seen, + }) + interactions_seen += 1 + ep["replays"] = replays round_i = test_round.get(ep["num"]) if (ep["kind"] == "test" and round_i is not None and round_i < len(round_cycles)): @@ -1701,6 +1763,7 @@ def ansi_to_html(text: str) -> str: .chip.bad { color: var(--bad); border-color: var(--bad); } .chip.kind-explore { color: #b083f0; border-color: #b083f0; } .chip.kind-learn { color: #daaa3f; border-color: #daaa3f; } +.chip.replay { border-style: dashed; } .chip.sup { opacity: .5; } /* Lifecycle, not verdict: green and red stay reserved for env evals. */ .chip.live { color: var(--accent); border-color: var(--accent); @@ -2403,13 +2466,34 @@ def _test_chip(ep: Dict[str, Any]) -> str: def _misc_chip(ep: Dict[str, Any]) -> str: - """Chip for one non-test episode, e.g. "002 explore ✓ 0.70".""" + """Chip(s) for one non-test episode, e.g. "002 explore ✓ 0.70". + + A belief-certified explore session is marked ◆, and each of the + cycle's query-free replays of its plan follows as its own "↻ 002 ✗ + 0.00" chip: the replay is a real interaction episode with an env + verdict but no transcript, so without the chip a cycle that certified + a plan reads as a single attempt while the early-stop rule judged two. + """ label = f"{int(ep['num']):03} {ep['kind']}" title = "" if "env_accepted" in ep: mark, _, title = explore_mark(ep) label += " " + mark - return _lineage_chip(ep, label, "kind-" + ep["kind"], title) + if ep.get("certified"): + label += " ◆" + title = ("belief-certified: the submitted plan passed the capture " + "gate and ran verbatim as a solve attempt" + + ("; " + title if title else "")) + chips = [_lineage_chip(ep, label, "kind-" + ep["kind"], title)] + for i, rep in enumerate(ep.get("replays", [])): + mark, _, rtitle = explore_mark(rep) + rtitle = (f"replay {i + 1} of session {int(ep['num']):03}'s " + f"certified plan ({rep['steps']} steps) without a new " + f"query; " + rtitle) + chips.append( + _lineage_chip(ep, f"↻ {int(ep['num']):03} {mark}", + "kind-" + ep["kind"] + " replay", rtitle)) + return " ".join(chips) # ----------------------------------------------------------------- pages @@ -3009,6 +3093,12 @@ def episode_videos(ep: Dict[str, Any], run_rel: str) -> str: "of this cycle)") elif ep_idx == ep.get("interaction_idx"): label = "interaction video" + elif ep_idx in { + r["interaction_idx"] + for r in ep.get("replays", []) + }: + label = ("interaction video (query-free replay of this " + "session's certified plan)") else: continue # another explore session's episode url = "/rawvideo?p=" + q(video_url_rel(run_rel, name)) diff --git a/tests/test_log_viewer.py b/tests/test_log_viewer.py index e0cd69244..c8c15a0d1 100644 --- a/tests/test_log_viewer.py +++ b/tests/test_log_viewer.py @@ -4,7 +4,8 @@ from pathlib import Path from typing import Any, Dict -from scripts.log_viewer import _parse_info_log, chain_summary, resume_chains +from scripts.log_viewer import _explore_results, _misc_chip, _parse_info_log, \ + chain_summary, resume_chains _SAVE = ("INFO: Saved local sandbox query/response to logs/x/sandbox/" "session_logs/{name}.md") @@ -227,3 +228,84 @@ def test_chain_summary_single_run_keeps_rows() -> None: merged = chain_summary([a], summaries) assert merged["episodes"][0]["round"] == 4 assert merged["episodes"][0]["superseded"] is False + + +_CERTIFIED = ("INFO: agent_bilevel explorer: the agent's tool-validated plan " + "passed the belief's capture gate (validation: 8/8 rollouts " + "ok); executing it verbatim as this episode's solve attempt " + "(mental model solved the goal).") +_REPLAY = ("INFO: agent_bilevel explorer: replaying this cycle's " + "belief-certified plan for train task 0 (26 steps) without a new " + "query.") +_VERDICT = ("INFO: Interaction episode on train task 0: reward={r}, " + "terminated={t}, accepted={a}") + + +def test_certified_plan_replay_is_booked_on_its_session( + tmp_path: Path) -> None: + """The second request of a cycle that certified a plan re-executes it + without a new query, so its verdict has no transcript: it is recorded as a + replay of the certified session (not dropped, and not paired with the next + cycle's first explore).""" + path = _write_log(tmp_path, [ + "ONLINE LEARNING CYCLE 2", + _SAVE.format(name="018_explore_20260830_090000"), + _CERTIFIED, + _REPLAY, + _VERDICT.format(r="1.00", t="True", a="True"), + _VERDICT.format(r="0.00", t="False", a="False"), + _SAVE.format(name="019_learn_20260830_100000"), + "ONLINE LEARNING CYCLE 3", + _SAVE.format(name="020_explore_20260830_110000"), + _VERDICT.format(r="0.50", t="True", a="True"), + ]) + parsed = _parse_info_log(path) + ex = parsed["explore"] + assert ex[18]["certified"] is True + assert ex[18]["accepted"] is True + assert [(r["steps"], r["accepted"]) for r in ex[18]["replays"]] == \ + [(26, False)] + assert ex[20] == { + "reward": 0.5, + "terminated": True, + "accepted": True, + "msg": "", + "cycle": 3, + } + + +def test_replay_chip_and_cycle_tally() -> None: + """The grid draws the replay as its own dashed chip after the certified + session's chip, and the per-cycle tally counts it as an attempt (the early- + stop rule judges every attempt).""" + ep = { + "num": + 18, + "kind": + "explore", + "cycle_tag": + "cycle2", + "env_reward": + 1.0, + "env_terminated": + True, + "env_accepted": + True, + "env_msg": + "", + "certified": + True, + "replays": [{ + "env_reward": 0.0, + "env_terminated": False, + "env_accepted": False, + "env_msg": "", + "steps": 26, + "interaction_idx": 1, + }], + } + html = _misc_chip(ep) + assert "018 explore ✓ 1.00 ◆" in html + assert "↻ 018 ✗ 0.00" in html + assert "replay" in html and "without a new query" in html + assert _explore_results([ep]) == [(2, 1, 2, 0.5)] From 38110806c4644ab318cc375531f84c20755e1d80 Mon Sep 17 00:00:00 2001 From: Yichao Liang Date: Sun, 30 Aug 2026 10:16:43 -0400 Subject: [PATCH 2/6] explorer: a certified plan is followed by a query for a different certified plan, not a replay Two real executions of one plan test repeatability, not the belief model: in the Aug-29 policy-s0 run the same model solved the train task twice and failed the test task. The cycle's later requests now always query the agent, which sees the certified plan under the plans already scheduled and is asked, in order of preference, for a structurally different goal-reaching plan validated through the same capture gate, then the same structure with materially different parameters, then the certified plan unchanged as a last resort. The early-stop rule is unchanged: every attempt must be certified and solve for real, so a failed alternative blocks the stop that cycle and lands its data where the model is wrong. agent_explorer_replay_certified_plan is renamed agent_explorer_execute_certified_plan (it only gates the verbatim execution now); cycle_certified_plans and the explorer's replay branch are gone, and the log viewer drops the replay chip it grew for that branch while keeping the certified marker. Claude-Session: https://claude.ai/code/session_01Pbv9TvedRD2iKyoKMJKx6H --- predicators/agent_sdk/sketch_prompts.py | 24 ++++- predicators/agent_sdk/tools/context.py | 8 -- .../approaches/agent_model_free_approach.py | 1 - .../explorers/agent_bilevel_explorer.py | 28 ++---- predicators/settings.py | 13 ++- scripts/log_viewer.py | 94 ++++--------------- .../explorers/test_agent_bilevel_explorer.py | 19 ++-- tests/test_log_viewer.py | 78 ++++----------- 8 files changed, 81 insertions(+), 184 deletions(-) diff --git a/predicators/agent_sdk/sketch_prompts.py b/predicators/agent_sdk/sketch_prompts.py index b4eaa49aa..b410c36d8 100644 --- a/predicators/agent_sdk/sketch_prompts.py +++ b/predicators/agent_sdk/sketch_prompts.py @@ -176,10 +176,10 @@ def build_solve_prompt( "unchanged) is how the loop concludes." + (" A plan that passes submit_plan's validation " "gate (goal reached in every fresh belief rollout) is " - "executed VERBATIM as this episode's solve attempt and " - "replayed for the cycle's remaining episodes; only an " - "unvalidated sketch is treated as an experiment." - if CFG.agent_explorer_replay_certified_plan else "")) + "executed VERBATIM as this episode's solve attempt; " + "only an unvalidated sketch is treated as an " + "experiment." + if CFG.agent_explorer_execute_certified_plan else "")) elif CFG.online_learning_early_stopping_by_test_solve_rate: n_perfect = ( CFG.online_learning_early_stopping_consecutive_perfect_tests) @@ -321,7 +321,21 @@ def build_solve_prompt( "scheduled plans will not is the better use of this episode. " "Only if the model is believed correct everywhere and no " "meaningfully different goal-reaching plan exists, repeat " - "the best plan.\n") + "the best plan.\n" + "\nIf a scheduled plan is marked belief-certified, this " + "episode is the second test of the belief model, and one " + "success of one plan is weak evidence. In order of " + "preference: (1) a STRUCTURALLY different goal-reaching plan " + "- a different option sequence, order, grasp, or contact " + "arrangement - validated through the same submit_plan gate; " + "(2) when no structurally different plan exists for this " + "goal, the same structure with materially different " + "parameters (a different placement pose, offset, or timing, " + "not a jitter), validated the same way; (3) only as a last " + "resort, the certified plan resubmitted unchanged. State " + "which of the three you chose and why. A certified plan " + "that then fails for real is the most informative outcome " + "this episode can produce, not a loss.\n") strategy_section = "" if strategy: diff --git a/predicators/agent_sdk/tools/context.py b/predicators/agent_sdk/tools/context.py index 702e4c6b9..5a2806aec 100644 --- a/predicators/agent_sdk/tools/context.py +++ b/predicators/agent_sdk/tools/context.py @@ -149,14 +149,6 @@ class ToolContext: # explore prompt so the agent proposes a complementary plan instead of # repeating the identical one for every request. cycle_scheduled_plans: List[str] = field(default_factory=list) - # Grounded plans that passed the belief's capture gate this cycle, - # keyed by train task index. Cleared by get_interaction_requests per - # cycle, written by AgentBilevelExplorer when an explore session's - # tool-validated capture reached the goal; the cycle's remaining - # requests on that task replay the plan (no new query), so a plan - # the belief certifies and reality solves on every attempt ends the - # loop (see agent_explorer_replay_certified_plan). - cycle_certified_plans: Dict[int, List[Any]] = field(default_factory=dict) # Digest of the latest rollout system-ID fit's weak spots # (unexplainable segments, unidentified/insensitive params, # cross-cycle conflicts), synced from the sim-learning approach. diff --git a/predicators/approaches/agent_model_free_approach.py b/predicators/approaches/agent_model_free_approach.py index 1ac4771e3..68b13be02 100644 --- a/predicators/approaches/agent_model_free_approach.py +++ b/predicators/approaches/agent_model_free_approach.py @@ -410,7 +410,6 @@ def get_interaction_requests(self) -> List[InteractionRequest]: # the explorer shows each query the plans already scheduled this # cycle and asks for a complementary one. Fresh list per cycle. self._tool_context.cycle_scheduled_plans = [] - self._tool_context.cycle_certified_plans = {} for _ in range(CFG.online_nsrt_learning_requests_per_cycle): task_idx = self._rng.choice(len(self._train_tasks)) # Clear so a planning explorer's verdict is read fresh per diff --git a/predicators/explorers/agent_bilevel_explorer.py b/predicators/explorers/agent_bilevel_explorer.py index d56fb5afd..599952ca8 100644 --- a/predicators/explorers/agent_bilevel_explorer.py +++ b/predicators/explorers/agent_bilevel_explorer.py @@ -73,22 +73,6 @@ def _get_exploration_strategy(self, train_task_idx: int, # producing one. self._tool_context.last_mental_model_solved = None - # A plan this cycle already certified on this task (see the - # capture branch below) is replayed for the cycle's remaining - # requests without a new query: the train-driven early-stop rule - # needs EVERY attempt of the cycle to solve, and a second real - # execution of the certified plan is the cheapest evidence. - certified = self._tool_context.cycle_certified_plans.get( - train_task_idx) - if certified is not None and \ - CFG.agent_explorer_replay_certified_plan: - logging.info( - "agent_bilevel explorer: replaying this cycle's " - "belief-certified plan for train task %d (%d steps) " - "without a new query.", train_task_idx, len(certified)) - self._tool_context.last_mental_model_solved = True - return self._certified_plan_strategy(certified) - # Point the agent's interactive tools (submit_plan, the # sim probe) at the EXPLORE task. They # default to ctx.current_task when the agent omits task_idx, and @@ -158,12 +142,16 @@ def _get_exploration_strategy(self, train_task_idx: int, # (submit_plan, N fresh # rollouts). ``reached_goal`` is the gate's verdict. capture = self._tool_context.take_plan_capture() - if CFG.agent_explorer_replay_certified_plan and capture.plan \ + if CFG.agent_explorer_execute_certified_plan and capture.plan \ and capture.reached_goal is True: # Certified: the mental model solves the task with THIS # plan, so run it verbatim as a solve attempt instead of # re-searching (or boundary-probing) its parameters. A - # real success now counts for early stopping. + # real success now counts for early stopping. The cycle's + # later requests see it under "plans already scheduled" + # and are asked for a DIFFERENT certified plan (a second + # test of the model), resubmitting this one only as a + # last resort. plan = list(capture.plan) logging.info( "agent_bilevel explorer: the agent's tool-validated " @@ -181,12 +169,10 @@ def _get_exploration_strategy(self, train_task_idx: int, for s in capture.sketch ] self._tool_context.last_mental_model_solved = True - self._tool_context.cycle_certified_plans[train_task_idx] = plan self._tool_context.cycle_scheduled_plans.append( self._format_plan(plan) + "\n NOTE: belief-certified; executes verbatim as a " - "solve attempt and is replayed for this cycle's " - "remaining episodes.") + "solve attempt.") return self._certified_plan_strategy(plan) if not plan_text and not capture.plan: raise ValueError("agent returned empty plan text") diff --git a/predicators/settings.py b/predicators/settings.py index f94fbf981..21b18a8f7 100644 --- a/predicators/settings.py +++ b/predicators/settings.py @@ -1818,11 +1818,14 @@ class GlobalSettings: # (submit_plan: goal reached in # agent_plan_validation_rollouts fresh belief rollouts) is executed # verbatim as the episode's solve attempt with mental_model_solved= - # True, and the cycle's remaining requests on that task replay it - # without a new query - so a certified plan that solves for real on - # every attempt satisfies the train-driven early-stop rule. Off - # feeds the capture into the experiment search as seeds instead. - agent_explorer_replay_certified_plan = True + # True. The cycle's remaining requests on that task still query the + # agent, which sees the certified plan among the plans already + # scheduled and is asked for a different certified plan (a second, + # independent test of the belief), resubmitting the same one only as + # a last resort; every certified attempt solving for real satisfies + # the train-driven early-stop rule. Off feeds the capture into the + # experiment search as seeds instead. + agent_explorer_execute_certified_plan = True # Per-parameter jitter as a fraction of the ParamSpec box width, for # the uniform-fallback ensemble only (see calibrated flag below). agent_explorer_info_perturb_frac = 0.15 diff --git a/scripts/log_viewer.py b/scripts/log_viewer.py index e42fbd40f..64a0db808 100644 --- a/scripts/log_viewer.py +++ b/scripts/log_viewer.py @@ -140,20 +140,12 @@ # requests (see _parse_info_log). SAVED_EP_RE = re.compile(r"Saved local sandbox query/response to .*[/\\]" r"(\d{3})_([a-z]+)(?:_task\d+)?_\d{8}_\d{6}\.md") -# agent_bilevel_explorer's two certified-plan lines. The first says the -# session just saved (the newest pending explore) submitted a plan that -# passed the belief's capture gate and executes it verbatim as a solve -# attempt. The second says a later request of the same cycle re-executes -# that plan WITHOUT a new query, so its interaction verdict arrives with -# no transcript of its own: the parser books it as a replay of the -# certified session rather than dropping it (or mis-pairing it with the -# next cycle's first explore). +# agent_bilevel_explorer's line saying the session just saved (the newest +# explore session) submitted a plan that passed the belief's capture gate +# and executes it verbatim as a solve attempt. CERTIFIED_RE = re.compile(r"agent_bilevel explorer: the agent's " r"tool-validated plan passed the belief's " r"capture gate") -REPLAY_RE = re.compile(r"agent_bilevel explorer: replaying this cycle's " - r"belief-certified plan for train task (\d+) " - r"\((\d+) steps\) without a new query\.") # utils.save_video's announcement of a written video file. Its # //seed/run_ tail names the video # dir the run actually wrote, which the mirrored-layout assumption gets @@ -878,9 +870,7 @@ def _parse_info_log(path: str) -> Dict[str, Any]: "reward": float?}, ...}, ...], "explore": {episode_num: {"reward": float, "terminated": bool, "certified": bool (the session's plan passed the belief's - capture gate and ran verbatim), "replays": [verdict dicts - of the cycle's later requests that re-executed that plan - without a new query, in order], + capture gate and ran verbatim), "accepted": bool, "msg": str, "cycle": int|None}, ...}, "test_round": {episode_num: round_idx, ...}, "round_cycles": [cycle_id|None, ...] parallel to rounds -- the @@ -912,12 +902,11 @@ def _parse_info_log(path: str) -> Dict[str, Any]: explore: Dict[int, Dict[str, Any]] = {} session_cycles: Dict[int, Optional[int]] = {} test_round: Dict[int, int] = {} - # Verdict slots (a session's entry in ``explore`` or one of its replay - # entries) awaiting their interaction line, in request order. + # Explore sessions' verdict entries awaiting their interaction line, + # in request order. pending: List[Dict[str, Any]] = [] pending_test: List[int] = [] last_explore: Optional[Dict[str, Any]] = None - certified: Optional[Dict[str, Any]] = None newest_session: Optional[Dict[str, Any]] = None cycle: Optional[int] = None resume_cycle: Optional[int] = None @@ -947,7 +936,6 @@ def _parse_info_log(path: str) -> Dict[str, Any]: m = CYCLE_HEADER_RE.match(line) if m: cycle = int(m.group(1)) - certified = None continue if resume_cycle is None: m = RESUME_RE.search(line) @@ -1015,23 +1003,14 @@ def _parse_info_log(path: str) -> Dict[str, Any]: pending.append(newest_session) elif m.group(2) == "learn": pending.clear() - certified = None elif m.group(2) == "test": pending_test.append(int(m.group(1))) continue if CERTIFIED_RE.search(line): # Belongs to the newest explore session (it saved its # transcript just before the explorer read the capture). - certified = newest_session - if certified is not None: - certified["certified"] = True - continue - m = REPLAY_RE.search(line) - if m: - if certified is not None: - replay = {"steps": int(m.group(2))} - certified.setdefault("replays", []).append(replay) - pending.append(replay) + if newest_session is not None: + newest_session["certified"] = True continue m = INTERACTION_RE.match(line) if m: @@ -1095,15 +1074,11 @@ def _explore_results( out: List[Tuple[Optional[int], int, int, float]] = [] for (kind, num) in sorted(by_key): eps = by_key[(kind, num)] - # A certified plan's query-free replays are real attempts too (the - # early-stop rule counts every attempt), so they join the tally. - attempts = [ep for ep in eps if "env_reward" in ep] - attempts += [r for ep in eps for r in ep.get("replays", [])] - if not attempts: + rewards = [ep["env_reward"] for ep in eps if "env_reward" in ep] + if not rewards: continue - rewards = [a["env_reward"] for a in attempts] - solved = sum(1 for a in attempts if a.get("env_accepted")) - out.append((num if kind == 0 else None, solved, len(attempts), + solved = sum(1 for ep in eps if ep.get("env_accepted")) + out.append((num if kind == 0 else None, solved, len(eps), sum(rewards) / len(rewards))) return out @@ -1222,24 +1197,6 @@ def _cycle_row(ep: Dict[str, Any]) -> Optional[int]: interactions_seen += 1 if verdict is not None and verdict.get("certified"): ep["certified"] = True - # The cycle's later requests replayed this session's plan - # without a query: real attempts (they count for the - # early-stop rule and own the next __ep videos) that - # left no transcript, so they ride on this session. - replays = [] - for r in verdict.get("replays", []): - if "accepted" not in r: - continue - replays.append({ - "env_reward": r["reward"], - "env_terminated": r["terminated"], - "env_accepted": r["accepted"], - "env_msg": r["msg"], - "steps": r["steps"], - "interaction_idx": interactions_seen, - }) - interactions_seen += 1 - ep["replays"] = replays round_i = test_round.get(ep["num"]) if (ep["kind"] == "test" and round_i is not None and round_i < len(round_cycles)): @@ -1763,7 +1720,6 @@ def ansi_to_html(text: str) -> str: .chip.bad { color: var(--bad); border-color: var(--bad); } .chip.kind-explore { color: #b083f0; border-color: #b083f0; } .chip.kind-learn { color: #daaa3f; border-color: #daaa3f; } -.chip.replay { border-style: dashed; } .chip.sup { opacity: .5; } /* Lifecycle, not verdict: green and red stay reserved for env evals. */ .chip.live { color: var(--accent); border-color: var(--accent); @@ -2466,13 +2422,10 @@ def _test_chip(ep: Dict[str, Any]) -> str: def _misc_chip(ep: Dict[str, Any]) -> str: - """Chip(s) for one non-test episode, e.g. "002 explore ✓ 0.70". + """Chip for one non-test episode, e.g. "002 explore ✓ 0.70". - A belief-certified explore session is marked ◆, and each of the - cycle's query-free replays of its plan follows as its own "↻ 002 ✗ - 0.00" chip: the replay is a real interaction episode with an env - verdict but no transcript, so without the chip a cycle that certified - a plan reads as a single attempt while the early-stop rule judged two. + A belief-certified explore session (its submitted plan passed the + capture gate and ran verbatim as a solve attempt) is marked ◆. """ label = f"{int(ep['num']):03} {ep['kind']}" title = "" @@ -2484,16 +2437,7 @@ def _misc_chip(ep: Dict[str, Any]) -> str: title = ("belief-certified: the submitted plan passed the capture " "gate and ran verbatim as a solve attempt" + ("; " + title if title else "")) - chips = [_lineage_chip(ep, label, "kind-" + ep["kind"], title)] - for i, rep in enumerate(ep.get("replays", [])): - mark, _, rtitle = explore_mark(rep) - rtitle = (f"replay {i + 1} of session {int(ep['num']):03}'s " - f"certified plan ({rep['steps']} steps) without a new " - f"query; " + rtitle) - chips.append( - _lineage_chip(ep, f"↻ {int(ep['num']):03} {mark}", - "kind-" + ep["kind"] + " replay", rtitle)) - return " ".join(chips) + return _lineage_chip(ep, label, "kind-" + ep["kind"], title) # ----------------------------------------------------------------- pages @@ -3093,12 +3037,6 @@ def episode_videos(ep: Dict[str, Any], run_rel: str) -> str: "of this cycle)") elif ep_idx == ep.get("interaction_idx"): label = "interaction video" - elif ep_idx in { - r["interaction_idx"] - for r in ep.get("replays", []) - }: - label = ("interaction video (query-free replay of this " - "session's certified plan)") else: continue # another explore session's episode url = "/rawvideo?p=" + q(video_url_rel(run_rel, name)) diff --git a/tests/explorers/test_agent_bilevel_explorer.py b/tests/explorers/test_agent_bilevel_explorer.py index e637d081b..fb676ec6b 100644 --- a/tests/explorers/test_agent_bilevel_explorer.py +++ b/tests/explorers/test_agent_bilevel_explorer.py @@ -405,11 +405,11 @@ def _make_certified_capture(pick_params, place_params): return grounded_plan, captured_sketch -def test_certified_capture_executes_verbatim_and_is_replayed(): +def test_certified_capture_executes_verbatim_and_next_request_queries(): """A plan the session validated through the capture gate (reached_goal True) is executed verbatim as a solve attempt with a True mental-model - verdict; the cycle's next request on the task replays it with no new - query.""" + verdict; the cycle's next request on the task queries the agent again + (asking for a different certified plan) rather than replaying it.""" _reset_config(agent_explorer_info_seeking=True) option_model = MagicMock() option_model.get_next_state_and_num_actions.return_value = (_make_state( @@ -436,26 +436,29 @@ async def query_impl(msg, **_kw): assert not option_model.get_next_state_and_num_actions.called assert tool_context.last_mental_model_solved is True assert tool_context.solved_plan is None - assert tool_context.cycle_certified_plans[0] is not None assert "belief-certified" in tool_context.cycle_scheduled_plans[-1] + assert "replayed" not in tool_context.cycle_scheduled_plans[-1] assert tool_context.last_sketch_options == [("Pick", ["block0"]), ("Place", ["block0", "block1"])] # The policy runs the captured options with their captured params. act = policy(_make_state()) assert isinstance(act, Action) - # Second request of the cycle on the same task: replay, no query. + # Second request of the cycle on the same task: a new query that + # shows the certified plan as already scheduled. tool_context.last_mental_model_solved = None policy2, _ = explorer._get_exploration_strategy(0, timeout=5) assert callable(policy2) - assert len(queries) == 1 + assert len(queries) == 2 + assert "belief-certified" in queries[1] + assert "STRUCTURALLY different" in queries[1] assert tool_context.last_mental_model_solved is True def test_uncertified_capture_executes_its_plan_verbatim(): """A capture whose gate verdict is not True (best-effort, flaky) is not certified: it executes at its captured params as an experiment, with a - False mental-model verdict and no replay for the cycle.""" + False mental-model verdict.""" _reset_config() option_model = MagicMock() grounded_plan, captured_sketch = _make_captured([0.42], [0.11, 0.22]) @@ -471,5 +474,5 @@ async def query_impl(_msg, **_kw): policy, _ = explorer._get_exploration_strategy(0, timeout=5) assert callable(policy) assert not option_model.get_next_state_and_num_actions.called - assert 0 not in tool_context.cycle_certified_plans + assert "belief-certified" not in tool_context.cycle_scheduled_plans[-1] assert tool_context.last_mental_model_solved is False diff --git a/tests/test_log_viewer.py b/tests/test_log_viewer.py index c8c15a0d1..e1771f91a 100644 --- a/tests/test_log_viewer.py +++ b/tests/test_log_viewer.py @@ -234,78 +234,40 @@ def test_chain_summary_single_run_keeps_rows() -> None: "passed the belief's capture gate (validation: 8/8 rollouts " "ok); executing it verbatim as this episode's solve attempt " "(mental model solved the goal).") -_REPLAY = ("INFO: agent_bilevel explorer: replaying this cycle's " - "belief-certified plan for train task 0 (26 steps) without a new " - "query.") _VERDICT = ("INFO: Interaction episode on train task 0: reward={r}, " "terminated={t}, accepted={a}") -def test_certified_plan_replay_is_booked_on_its_session( - tmp_path: Path) -> None: - """The second request of a cycle that certified a plan re-executes it - without a new query, so its verdict has no transcript: it is recorded as a - replay of the certified session (not dropped, and not paired with the next - cycle's first explore).""" +def test_certified_session_is_marked(tmp_path: Path) -> None: + """The capture-gate line marks the newest explore session as certified; the + cycle's second request is an ordinary session with its own transcript and + verdict.""" path = _write_log(tmp_path, [ "ONLINE LEARNING CYCLE 2", _SAVE.format(name="018_explore_20260830_090000"), _CERTIFIED, - _REPLAY, + _SAVE.format(name="019_explore_20260830_093000"), _VERDICT.format(r="1.00", t="True", a="True"), _VERDICT.format(r="0.00", t="False", a="False"), - _SAVE.format(name="019_learn_20260830_100000"), - "ONLINE LEARNING CYCLE 3", - _SAVE.format(name="020_explore_20260830_110000"), - _VERDICT.format(r="0.50", t="True", a="True"), ]) - parsed = _parse_info_log(path) - ex = parsed["explore"] - assert ex[18]["certified"] is True - assert ex[18]["accepted"] is True - assert [(r["steps"], r["accepted"]) for r in ex[18]["replays"]] == \ - [(26, False)] - assert ex[20] == { - "reward": 0.5, - "terminated": True, - "accepted": True, - "msg": "", - "cycle": 3, - } + ex = _parse_info_log(path)["explore"] + assert ex[18]["certified"] is True and ex[18]["accepted"] is True + assert "certified" not in ex[19] and ex[19]["accepted"] is False -def test_replay_chip_and_cycle_tally() -> None: - """The grid draws the replay as its own dashed chip after the certified - session's chip, and the per-cycle tally counts it as an attempt (the early- - stop rule judges every attempt).""" +def test_certified_chip_mark() -> None: + """A certified session's chip carries the diamond and its tooltip; the + tally is unchanged.""" ep = { - "num": - 18, - "kind": - "explore", - "cycle_tag": - "cycle2", - "env_reward": - 1.0, - "env_terminated": - True, - "env_accepted": - True, - "env_msg": - "", - "certified": - True, - "replays": [{ - "env_reward": 0.0, - "env_terminated": False, - "env_accepted": False, - "env_msg": "", - "steps": 26, - "interaction_idx": 1, - }], + "num": 18, + "kind": "explore", + "env_reward": 1.0, + "env_terminated": True, + "env_accepted": True, + "env_msg": "", + "certified": True, } html = _misc_chip(ep) assert "018 explore ✓ 1.00 ◆" in html - assert "↻ 018 ✗ 0.00" in html - assert "replay" in html and "without a new query" in html - assert _explore_results([ep]) == [(2, 1, 2, 0.5)] + assert "belief-certified" in html + assert _explore_results([dict(ep, cycle_tag="cycle2")]) == [(2, 1, 1, 1.0)] From 0b2e667e4c0a596185f9328b43f21b23f49e5ac2 Mon Sep 17 00:00:00 2001 From: Yichao Liang Date: Sun, 30 Aug 2026 11:24:50 -0400 Subject: [PATCH 3/6] prompts: one rule per layer, Markdown templates, golden renders The solve, explore, and learn prompts had accumulated the same rules in two to four places across the system prompt, the query, and the sandbox CLAUDE.md, with incident anecdotes and em dashes in the shipped text. Rewrite them as Markdown templates under predicators/agent_sdk/prompts/ (section markers, __PLACEHOLDER__ substitution that fails on a missing or unused value, prose unwrapping before data is substituted) with one owner per rule: - system prompt: identity, deliverable, plan grammar, tool semantics, working principles, run-record protocol, and (explore) the exploration setting, including the early-stop note; - query: task data, run-record contents, scheduled plans, open questions, and one short instruction block; - CLAUDE.md: sandbox mechanics only. The learn system prompt absorbs the deliverables that lived in the first message (decision record, hypothesis rule, declared uncertainty, GO/NO-GO, open_questions.md, strategy.md) and the threshold-fitting protocol from CLAUDE.md; the first message carries this cycle's data. Domain-specific names stay out of the prompt text. sketch_prompts gains build_solve_system_prompt and build_early_stop_note; learn_prompts is new; build_claude_md takes no phase. Golden renders of every phase and mode live in tests/agent_sdk/prompt_goldens and are checked by test_prompt_goldens.py (UPDATE_PROMPT_GOLDENS=1 regenerates). Claude-Session: https://claude.ai/code/session_01Pbv9TvedRD2iKyoKMJKx6H --- predicators/agent_sdk/bilevel_sketch.py | 10 +- predicators/agent_sdk/docker_sandbox.py | 4 +- predicators/agent_sdk/learn_prompts.py | 231 ++++ predicators/agent_sdk/local_sandbox.py | 4 +- predicators/agent_sdk/prompt_templates.py | 137 +++ .../agent_sdk/prompts/learn_message.md | 148 +++ .../prompts/learn_partial_observability.md | 178 +++ .../prompts/learn_predicate_invention.md | 140 +++ predicators/agent_sdk/prompts/learn_system.md | 555 +++++++++ .../agent_sdk/prompts/sandbox_claude_md.md | 62 + predicators/agent_sdk/prompts/solve_query.md | 163 +++ predicators/agent_sdk/prompts/solve_system.md | 362 ++++++ predicators/agent_sdk/sandbox_prompts.py | 218 +--- predicators/agent_sdk/session_base.py | 2 +- predicators/agent_sdk/sketch_prompts.py | 977 ++++----------- .../approaches/agent_model_based_approach.py | 139 +-- .../approaches/agent_sim_learning_approach.py | 1060 ++--------------- .../agent_sim_predicate_invention_approach.py | 256 +--- .../explorers/agent_bilevel_explorer.py | 1 - setup.py | 1 + .../agent_sdk/prompt_goldens/explore_query.md | 60 + .../agent_sdk/prompt_goldens/learn_message.md | 72 ++ .../agent_sdk/prompt_goldens/learn_system.md | 218 ++++ .../learn_system_po_invention.md | 366 ++++++ .../prompt_goldens/sandbox_claude_md.md | 37 + tests/agent_sdk/prompt_goldens/solve_query.md | 70 ++ .../prompt_goldens/solve_system_explore.md | 59 + .../prompt_goldens/solve_system_plan.md | 57 + .../prompt_goldens/solve_system_policy.md | 70 ++ tests/agent_sdk/test_prompt_goldens.py | 323 +++++ tests/agent_sdk/test_solve_prompt_strategy.py | 250 ++-- tests/agent_sdk/test_solve_restart_journal.py | 3 +- .../test_agent_model_based_approach.py | 2 +- .../test_agent_sim_prompt_formatting.py | 25 +- 34 files changed, 3856 insertions(+), 2404 deletions(-) create mode 100644 predicators/agent_sdk/learn_prompts.py create mode 100644 predicators/agent_sdk/prompt_templates.py create mode 100644 predicators/agent_sdk/prompts/learn_message.md create mode 100644 predicators/agent_sdk/prompts/learn_partial_observability.md create mode 100644 predicators/agent_sdk/prompts/learn_predicate_invention.md create mode 100644 predicators/agent_sdk/prompts/learn_system.md create mode 100644 predicators/agent_sdk/prompts/sandbox_claude_md.md create mode 100644 predicators/agent_sdk/prompts/solve_query.md create mode 100644 predicators/agent_sdk/prompts/solve_system.md create mode 100644 tests/agent_sdk/prompt_goldens/explore_query.md create mode 100644 tests/agent_sdk/prompt_goldens/learn_message.md create mode 100644 tests/agent_sdk/prompt_goldens/learn_system.md create mode 100644 tests/agent_sdk/prompt_goldens/learn_system_po_invention.md create mode 100644 tests/agent_sdk/prompt_goldens/sandbox_claude_md.md create mode 100644 tests/agent_sdk/prompt_goldens/solve_query.md create mode 100644 tests/agent_sdk/prompt_goldens/solve_system_explore.md create mode 100644 tests/agent_sdk/prompt_goldens/solve_system_plan.md create mode 100644 tests/agent_sdk/prompt_goldens/solve_system_policy.md create mode 100644 tests/agent_sdk/test_prompt_goldens.py diff --git a/predicators/agent_sdk/bilevel_sketch.py b/predicators/agent_sdk/bilevel_sketch.py index 6feed94c5..ff66682b1 100644 --- a/predicators/agent_sdk/bilevel_sketch.py +++ b/predicators/agent_sdk/bilevel_sketch.py @@ -8,8 +8,9 @@ - ``sketch_types``: shared dataclasses (``GroundSampler``, ``SketchStep``) that parsing constructs and refinement/execution consume. -- ``sketch_prompts``: ``build_solve_prompt``, the solve/explore prompt - builder. +- ``sketch_prompts``: ``build_solve_system_prompt`` and + ``build_solve_prompt``, the solve/explore system-prompt and query + builders (rendered from ``prompts/*.md``). - ``sketch_parsing``: the sketch-line grammar - step/plan formatters and the parsers for subgoal / ``~`` ground-sampler annotations and continuous params. @@ -26,7 +27,8 @@ parse_region_annotations, parse_sketch_from_text, \ parse_subgoal_annotations, strip_code_fences, strip_region_annotations, \ strip_subgoal_annotations -from predicators.agent_sdk.sketch_prompts import build_solve_prompt +from predicators.agent_sdk.sketch_prompts import build_early_stop_note, \ + build_solve_prompt, build_solve_system_prompt from predicators.agent_sdk.sketch_refinement import DeepestFailure, \ InfoScorer, RefineOutcome, StepProbeSuggestion, ground_step, \ refine_and_validate_report, refine_sketch, resolve_refine_timeout, \ @@ -42,7 +44,9 @@ "SketchStep", "StepOutcome", "StepProbeSuggestion", + "build_early_stop_note", "build_solve_prompt", + "build_solve_system_prompt", "execute_plan_forward", "format_plan_lines", "format_sketch_lines", diff --git a/predicators/agent_sdk/docker_sandbox.py b/predicators/agent_sdk/docker_sandbox.py index 16607556a..69ec6dd8c 100644 --- a/predicators/agent_sdk/docker_sandbox.py +++ b/predicators/agent_sdk/docker_sandbox.py @@ -73,8 +73,8 @@ _STDERR_TAIL_LINES = 20 # Build Docker-specific prompts from shared templates. -# CLAUDE.md is built per-instance with the phase tag so the agent reads -# phase-appropriate strategy guidance every turn (see build_claude_md). +# CLAUDE.md (sandbox mechanics only; see build_claude_md) is written +# into the sandbox when it is populated. _SANDBOX_SYSTEM_PROMPT = build_sandbox_system_prompt( env_description="an isolated Docker sandbox", workspace_description="/sandbox/", diff --git a/predicators/agent_sdk/learn_prompts.py b/predicators/agent_sdk/learn_prompts.py new file mode 100644 index 000000000..9173a12cc --- /dev/null +++ b/predicators/agent_sdk/learn_prompts.py @@ -0,0 +1,231 @@ +"""Prompt construction for the learning (simulator synthesis) phase. + +Rendered from ``learn_system.md``, ``learn_message.md``, +``learn_predicate_invention.md``, and ``learn_partial_observability.md`` +in ``predicators/agent_sdk/prompts`` (see :mod:`prompt_templates`). The +approach classes gather the per-instance values (digests, data roster, +reports, paths) and call these pure builders, so every prompt can be +rendered and reviewed without a live session. +""" +import re +from typing import Any, Mapping, Sequence + +from predicators.agent_sdk.prompt_templates import render + +_BLANK_RUN_RE = re.compile(r"\n{3,}") + + +def _join(parts: Sequence[str]) -> str: + text = "\n\n".join(p.strip("\n") for p in parts if p and p.strip()) + return _BLANK_RUN_RE.sub("\n\n", text).strip("\n") + "\n" + + +# --------------------------------------------------------------------------- +# System prompt +# --------------------------------------------------------------------------- + + +def build_learn_system_prompt( + *, + partially_observable: bool, + residual_rule_signature: str, + scene_viz_hint: str, + physical_params_section: str = "", + extra_sections: Sequence[str] = (), + latent_extra_sections: Sequence[str] = (), + workflow_extra: str = "", +) -> str: + """Compose the synthesis system prompt. + + ``partially_observable`` selects the recurrent 5-argument rule + signature and appends the recurrent-rules tutorial; + ``residual_rule_signature`` is the matching ``def`` line for the + geometric-gate example. ``physical_params_section`` is the rendered + system-identification section (empty when the env reveals no + parameters). ``extra_sections`` (subclass additions such as + predicate invention) are inserted after the validation guidance; + ``latent_extra_sections`` follow the recurrent-rules tutorial (only + rendered when ``partially_observable``); ``workflow_extra`` is + appended to the workflow's validation step. + """ + signature = render( + "learn_system", + "rule_signature_po" if partially_observable else "rule_signature_fo") + parts = [ + render("learn_system", "intro"), + render("learn_system", "produce"), + physical_params_section, + signature, + render("learn_system", "cmds"), + render("learn_system", "multi_object"), + render("learn_system", "timing"), + render("learn_system", + "geometric_gates", + residual_rule_signature=residual_rule_signature, + scene_viz_hint=scene_viz_hint), + render("learn_system", "paramspec"), + render("learn_system", "preinjected"), + render("learn_system", "tools"), + render("learn_system", "validation"), + *extra_sections, + ] + if partially_observable: + parts.append(render("learn_partial_observability", "rules")) + parts.extend(latent_extra_sections) + parts += [ + render("learn_system", "plan_format"), + render("learn_system", "deliverables"), + render("learn_system", + "workflow", + workflow_extra=(" " + + workflow_extra) if workflow_extra else ""), + ] + return _join(parts) + + +def render_physical_params_section( + info: Mapping[str, Mapping[str, Any]]) -> str: + """The ``PHYSICAL_PARAMS`` section for a revealed parameter menu. + + ``info`` maps a parameter name to its ``default``, ``lo``, ``hi``, + ``description``, and optional ``scale``; empty input renders + nothing, so envs without a menu never see the feature mentioned. + """ + if not info: + return "" + lines = [] + for name, meta in info.items(): + scale_note = (", fitted in log-space" + if meta.get("scale") == "log" else "") + lines.append(f"- `{name}` (built-in {meta['default']:.4g}, fit " + f"box [{meta['lo']:.4g}, {meta['hi']:.4g}]" + f"{scale_note}): {meta['description']}") + return render("learn_system", + "physical_params", + param_list="\n".join(lines)) + + +def render_predicate_invention_section(scene_workbench: str) -> str: + """The predicate-invention system-prompt section.""" + return render("learn_predicate_invention", + "system", + scene_workbench=scene_workbench) + + +def render_predicate_latent_section() -> str: + """The predicate-side latent guidance (invention arms, PO only).""" + return render("learn_partial_observability", "predicates") + + +def render_predicate_workflow_extra() -> str: + """The invention arm's addition to the workflow's validation step.""" + return render("learn_predicate_invention", "workflow_extra") + + +# --------------------------------------------------------------------------- +# First message +# --------------------------------------------------------------------------- + + +def build_learn_message( + *, + n_trajs: int, + n_transitions: int, + n_demos: int, + n_interaction: int, + trajectory_listing: str, + structs_ref: str, + inferred_hint: str, + predicate_listing: str, + types_digest: str, + options_digest: str, + simulator_file: str, + objective_block: str = "", + prior_state_block: str = "", + divergence_block: str = "", + base_sim_block: str = "", + tools_block: str = "", + extra_messages: Sequence[str] = (), +) -> str: + """Compose the synthesis session's first message. + + Every block argument is already rendered (see the ``render_*`` + helpers below) or empty. ``extra_messages`` (predicate invention, + partial observability, sampler synthesis) are appended in order. + """ + body = render( + "learn_message", + "skeleton", + n_trajs=str(n_trajs), + n_transitions=str(n_transitions), + n_demos=str(n_demos), + n_interaction=str(n_interaction), + trajectory_listing=trajectory_listing.strip("\n"), + objective_block=objective_block, + prior_state_block=prior_state_block, + divergence_block=divergence_block, + structs_ref=structs_ref, + base_sim_block=base_sim_block, + inferred_hint=inferred_hint, + predicate_listing=predicate_listing, + types_digest=types_digest.strip("\n"), + options_digest=options_digest.strip("\n"), + tools_block=tools_block, + simulator_file=simulator_file, + ) + return _join([body, *extra_messages]) + + +def render_divergence_block(report: str, has_prior_model: bool) -> str: + """The start-of-session residual report section.""" + return render("learn_message", + "divergence_prior" if has_prior_model else "divergence_base", + report=report.strip("\n")) + + +def render_base_sim_block(refs: Sequence[str]) -> str: + """The base-simulator source listing, or empty.""" + if not refs: + return "" + return render("learn_message", + "base_sim", + ref_listing="\n".join(f" - {r}" for r in refs)) + + +def render_tools_block(tool_names: Sequence[str]) -> str: + """The session's tool roster, or empty.""" + if not tool_names: + return "" + return render("learn_message", + "tools", + tool_listing="\n".join(f" - {t}" for t in tool_names)) + + +def render_objective_block(description: str) -> str: + """The env's public task objective section, or empty.""" + if not description: + return "" + return render("learn_message", "objective", description=description) + + +def render_prior_state_block(prior_files: Sequence[str]) -> str: + """The prior-cycle-state paragraph for the artifacts found, or empty.""" + if not prior_files: + return "" + return render("learn_message", + "prior_state", + prior_files=" and ".join(prior_files)) + + +def render_predicate_invention_message(predicates_file: str, + goal_block: str) -> str: + """The invention arm's addition to the first message.""" + return render("learn_predicate_invention", + "message", + predicates_file=predicates_file, + goal_block=goal_block.strip("\n")) + + +def render_partial_observability_message() -> str: + """The short partial-observability note for the first message.""" + return render("learn_partial_observability", "message") diff --git a/predicators/agent_sdk/local_sandbox.py b/predicators/agent_sdk/local_sandbox.py index 3b87e9397..37b15453c 100644 --- a/predicators/agent_sdk/local_sandbox.py +++ b/predicators/agent_sdk/local_sandbox.py @@ -55,8 +55,8 @@ _DEADLINE_INTERRUPT_SLACK_S = 180 # Build local-sandbox-specific prompts from shared templates. -# CLAUDE.md is built per-instance with the phase tag so the agent reads -# phase-appropriate strategy guidance every turn (see build_claude_md). +# CLAUDE.md (sandbox mechanics only; see build_claude_md) is written +# into the sandbox when it is populated. _LOCAL_SANDBOX_SYSTEM_PROMPT = build_sandbox_system_prompt( env_description="a local sandbox environment", workspace_description="the current directory", diff --git a/predicators/agent_sdk/prompt_templates.py b/predicators/agent_sdk/prompt_templates.py new file mode 100644 index 000000000..ffa4e8bde --- /dev/null +++ b/predicators/agent_sdk/prompt_templates.py @@ -0,0 +1,137 @@ +"""Loader for the agent prompt templates in ``predicators/agent_sdk/prompts``. + +Every prompt the agent receives (per-phase system prompts, the solve +and explore query, the synthesis learn message, the sandbox CLAUDE.md) +is authored as Markdown in that directory and composed here, so the +text can be read, diffed, and reproduced as a document rather than +reassembled from string concatenation. + +Template format: + +- A file is split into named sections by marker lines of the form + ````. Text before the first marker is ignored + (it is the file's own header comment). +- ``__UPPER_SNAKE__`` placeholders are substituted at render time. + Rendering fails loudly when a template placeholder has no value, so + a renamed placeholder cannot silently ship as literal text. +- Prose is authored hard-wrapped; :func:`render` joins wrapped prose + lines into one line per paragraph while preserving headings, lists, + tables, code fences, and indented lines. +""" +import functools +import os +import re +from typing import Dict, List + +_PROMPTS_DIR = os.path.join(os.path.dirname(__file__), "prompts") +_SECTION_RE = re.compile(r"^[ \t]*$", + re.MULTILINE) +_PLACEHOLDER_RE = re.compile(r"__([A-Z][A-Z0-9_]*)__") +# Matches "1. ", "12) " etc. at the start of a stripped line. +_NUMBERED_ITEM_RE = re.compile(r"^\d+[.)]\s") + + +@functools.lru_cache(maxsize=None) +def load_sections(template: str) -> Dict[str, str]: + """Return ``{section_name: raw_text}`` for ``prompts/