Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion predicators/agent_sdk/sketch_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,21 @@ def build_solve_prompt(
"the mechanism worked - a short plan that exercises the "
"unknown beats a long one that spends the episode's steps "
"on what the model already predicts.\n\n"
"What a cycle's data must contain. Across a cycle's "
"episodes the real environment must see (a) at least one "
"attempt at the FULL goal - every goal atom, executed to "
"the end, with the parameters you believe most likely to "
"work in reality even where the belief model predicts "
"failure - and (b) the top-ranked open question's "
"experiment executed as it is specified (its option "
"sequence and parameters), not a variation of your own. "
"One episode usually carries both, because when the open "
"question is a mechanism the goal requires, the goal "
"attempt IS its experiment; when the budget forces a "
"choice, the cycle's first episode attempts the goal and a "
"later one runs the ledger's top experiment - the "
"scheduled-plans section below tells you what this cycle "
"already covers.\n\n"
"Experiment design - one episode, many measurements. Before "
"sketching, list the mechanisms the goal depends on and "
"mark each KNOWN (the belief model has predicted it "
Expand Down Expand Up @@ -266,7 +281,15 @@ def build_solve_prompt(
"domain's language suggests), so the first learning phase "
"sees each mechanism at least once, instead of spending the "
"episode polishing a single goal attempt whose failure "
"reveals only its first missing mechanism.\n\n"
"reveals only its first missing mechanism. Carry each "
"interaction to its CONSEQUENCE, not just its setup: bring "
"the prepared surfaces into actual contact, release, wait "
"long enough for a delayed effect, then probe the result "
"(lift, push, or move one body and watch whether the other "
"follows). An interaction the episode stages but never "
"consummates - glue applied to a face that touches nothing, "
"parts placed near but not against each other - leaves the "
"learner with no event to model, and the cycle is spent.\n\n"
"Ledger upkeep is part of the deliverable. Append "
"measurements to ./journal.md as you go (a short entry per "
"experiment - lead with the numbers), and when a result "
Expand Down
1 change: 1 addition & 0 deletions predicators/agent_sdk/synthesis_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def _publish_probe_fit(
fit_result: Optional[FitResult] = None,
sse: float = float("nan"),
applied_physical: Optional[Dict[str, float]] = None,
sigma_points: Optional[List[Dict[str, float]]] = None,
) -> None:
"""Deploy a canonical ``sim.fit`` result to the candidate probe."""

Expand Down
10 changes: 7 additions & 3 deletions predicators/agent_sdk/tools/params_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@ def __init__(self, params: Dict[str, float]) -> None:

def __getitem__(self, key: str) -> float:
if key not in self._params:
known = ", ".join(sorted(self._params)) or "<none loaded yet>"
raise KeyError(
f"params[{key!r}] accessed before any parameter fit; "
"call sim.fit() to "
"populate self._fitted_params first.")
f"params[{key!r}] is not among the current simulator "
f"parameters (available: {known}). Declare "
f"ParamSpec({key!r}, ...) in simulator.py, or update "
"the classifier to use a declared parameter; after a "
"structural edit the values refresh at the next sim "
"call (run sim.fit() to fit them).")
return self._params[key]

def __contains__(self, key: object) -> bool:
Expand Down
40 changes: 38 additions & 2 deletions predicators/agent_sdk/tools/synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def create_synthesis_tools(
fit_rule_parameters_latent
from predicators.code_sim_learning.grid_seed import grid_candidates
from predicators.code_sim_learning.identifiability import \
format_identifiability
format_identifiability, physics_sigma_points
from predicators.code_sim_learning.orchestrator import run_rollout_sysid
from predicators.code_sim_learning.rollout_env import \
physical_param_anchors
Expand Down Expand Up @@ -367,6 +367,30 @@ def _evaluate_rollout_fit(rules: list,
# applied - do NOT print fitted values or identifiability
# verdicts computed on zero surviving data (chaos makes
# the probe report "identified" for everything).
if not exploratory:
# Record the refusal as this file's canonical fit
# (pinned at the declared inits): the deployed model
# reuses it instead of re-running the same refused fit
# under a misleading FIT FALLBACK warning, and probe
# results stop saying UNFITTED after the agent did
# fit. Mirroring the joint-rollout fallback's
# no-survivor case, nothing is applied to the planning
# env and no sigma points are recorded.
trim_rule_names = {s.name for s in rule_specs}
approach._publish_probe_fit( # pylint: disable=protected-access
{
n: v
for n, v in
outcome.fit_result.point_estimate.items()
if n in trim_rule_names
},
version_tag,
simulator_file,
fit_result=outcome.fit_result,
sse=float("nan"))
if hasattr(approach, "_record_sysid_diagnostics"):
approach._record_sysid_diagnostics( # pylint: disable=protected-access
{}, physical_names, 0, len(rollouts), outcome.traj_rms)
rms_str = ", ".join(f"{r:.4g}" for r in outcome.traj_rms)
return "\n".join([
f"[{version_tag}] NO FIT RAN: all {len(rollouts)} "
Expand All @@ -377,6 +401,10 @@ def _evaluate_rollout_fit(rules: list,
"",
"Parameters were left at their baselines; nothing was "
"applied to the planning base env.",
("Recorded as this file's canonical fit (pinned at the "
"declared inits): the deployed model will use these "
"values without a harness refit." if not exploratory else
"Exploratory call: nothing recorded."),
"",
"This usually means the recorded interactions are not "
"repeatable under replay (prolonged scraping/jamming "
Expand Down Expand Up @@ -404,7 +432,15 @@ def _evaluate_rollout_fit(rules: list,
simulator_file,
fit_result=outcome.fit_result,
sse=post_sse,
applied_physical=dict(applied))
applied_physical=dict(applied),
# Physics-margin points for the capture gate, restored
# when this fit is deployed as the cycle's model.
sigma_points=physics_sigma_points(
applied,
ident_report,
physical_specs,
num_points=CFG.
agent_plan_validation_physics_margin_points))
if hasattr(approach, "_record_sysid_diagnostics"):
approach._record_sysid_diagnostics( # pylint: disable=protected-access
ident_report, physical_names, outcome.num_survivors,
Expand Down
44 changes: 35 additions & 9 deletions predicators/approaches/agent_sim_learning_approach.py
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,7 @@ def _publish_probe_fit(
fit_result: Optional[FitResult] = None,
sse: float = float("nan"),
applied_physical: Optional[Dict[str, float]] = None,
sigma_points: Optional[List[Dict[str, float]]] = None,
) -> None:
"""Deploy a canonical ``sim.fit`` result to the candidate probe.

Expand All @@ -1550,6 +1551,7 @@ def _publish_probe_fit(
state["fit_result"] = fit_result
state["sse"] = sse
state["applied_physical"] = dict(applied_physical or {})
state["sigma_points"] = list(sigma_points or [])
self._probe_model_cache().clear()
self._tool_context.probe_param_status = f"fitted ({version_tag})"
logger.info("Synthesis probe: sim.fit deployed %d params (%s).",
Expand Down Expand Up @@ -2322,14 +2324,28 @@ def _build_synthesis_learn_message(
Evidence discipline for rules that WRITE physical state (poses, \
velocities): ground them in recorded transitions the base sim \
mispredicts. A mechanism you suspect but have never observed \
end-to-end in the data is a HYPOTHESIS - record it in the decision \
record with the experiment that would confirm it (so the next \
exploration phase can run that experiment), instead of shipping a \
speculative rule; a speculative pose-writer fabricates states the \
environment never produces, and plans validated against it fail in \
reality. The converse error is just as costly: do not delete a rule \
whose mechanism you have confirmed merely because one fit metric is \
noisy - decide from the recorded evidence either way.
end-to-end in the data is a HYPOTHESIS, and what to do with it \
depends on whether the goal needs it. When the goal is reachable \
without it, record it in the decision record with the experiment \
that would confirm it and ship no rule: a speculative pose-writer \
fabricates states the environment never produces, and plans \
validated against it fail in reality. When the goal REQUIRES it - \
without the mechanism the goal is unreachable in your model (an \
assembly that must be carried as one body, a latch that must hold, \
an activation that must take effect) - omitting it is NOT the \
cautious choice: it turns "unknown" into "impossible" for every \
consumer of the model, so the explorer can no longer certify a goal \
attempt and the test session proves the goal unreachable and gives \
up. Ship a goal-required mechanism as a LABELLED HYPOTHESIS: a rule \
whose trigger geometry and timing are declared ParamSpecs at your \
best physical estimate with honest ranges (the ensemble spreads over \
them and the capture gate validates plans under that spread), a \
HYPOTHESIS marker on it in the decision record, and its confirming \
experiment as the FIRST entry of open_questions.md, written so that \
the next exploration's goal attempt exercises it. The converse error \
is just as costly: do not delete a rule whose mechanism you have \
confirmed merely because one fit metric is noisy - decide from the \
recorded evidence either way.

Work through EVERY divergence this cycle's new trajectories reveal \
in this one session: enumerate each mechanism the episodes \
Expand Down Expand Up @@ -2361,7 +2377,10 @@ def _build_synthesis_learn_message(
point to a learned threshold - compared against the measured \
execution scatter. NO-GO, or a margin thinner than the scatter, \
means the next test episode will likely fail: put exactly what is \
missing at the top of `./open_questions.md`.
missing at the top of `./open_questions.md`. A GO that rests on a \
hypothesised mechanism says so in the verdict; it is still a GO - \
the plan it certifies is the experiment that confirms or refutes \
the hypothesis in the real environment.

Also maintain `./open_questions.md`: a short RANKED ledger of the \
model's remaining uncertainties - mechanisms never observed, \
Expand Down Expand Up @@ -2513,7 +2532,14 @@ def _fit_params_after_synthesis(
len(expected), self._fit_sse)
applied = self._probe_fit_state().get("applied_physical")
if self._physical_param_specs and applied:
# Mirror _fit_parameters_joint_rollout's deploy: the
# cycle-level applied snapshot and the physics-margin
# sigma points come from the published fit (applying
# resets the points, so set them after).
self._apply_identified_physical_params(dict(applied))
self._cycle_applied_physical = dict(applied)
self._identified_physical_sigma_points = list(
self._probe_fit_state().get("sigma_points") or [])
else:
if CFG.agent_sim_learn_oracle_sim_program:
logger.info("Oracle sim program: fitting its "
Expand Down
8 changes: 5 additions & 3 deletions predicators/explorers/agent_bilevel_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,9 +432,11 @@ def _build_experiment_guidance(self) -> str:
"The learning phase left this ranked ledger of OPEN "
"QUESTIONS - uncertainties it could not settle from the "
"data collected so far, each with the experiment that "
"would settle it. Settling ledger entries is this "
"episode's highest-value use; design the episode to "
"cover as many as its step budget allows:\n" + ledger)
"would settle it. The TOP entry is mandatory for this "
"cycle: run its experiment as specified (its option "
"sequence and parameters) unless a plan already "
"scheduled this cycle covers it, and fold in as many "
"lower entries as the step budget allows:\n" + ledger)
if CFG.agent_explorer_info_seeking:
parts.append(
"Your explicit continuous parameters execute exactly as "
Expand Down
10 changes: 10 additions & 0 deletions predicators/ground_truth_models/skill_factories/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,16 @@ def build(self) -> ParameterizedOption:
def _initiable(self, state: State, memory: Dict, objects: Sequence[Object],
params: Array) -> bool:
del state, objects, params # unused
# A grounded option is re-executed by several callers (the
# explorer's certified-plan replay, execution-monitor suffix
# replans, validation rollouts). Everything this skill keeps in
# memory - the BiRRT waypoint cache, the learned aim offset,
# retry/dwell/stall counters, finger targets - describes ONE
# execution; carried into the next one it drives the phases from
# stale state (cached waypoints from a different start pose,
# exhausted retries, a correction offset for a landing that never
# happened). Start every execution clean.
memory.clear()
memory["phase_idx"] = 0
return True

Expand Down
20 changes: 19 additions & 1 deletion tests/agent_sdk/test_probe_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from predicators import utils
from predicators.agent_sdk.belief_probe import BeliefProbe, \
build_probe_namespace
from predicators.agent_sdk.tools import ToolContext, create_mcp_tools
from predicators.agent_sdk.tools import ToolContext, _ParamsView, \
create_mcp_tools
from predicators.approaches.agent_sim_learning_approach import \
AgentSimLearningApproach
from predicators.code_sim_learning.fit_space import FitResult
Expand Down Expand Up @@ -348,3 +349,20 @@ def test_probe_run_reports_subgoal_divergence() -> None:
rendered = repr(ProbeResult([step], False, [], task.init, []))
assert "SUBGOAL NOT REACHED: {WidgetAtFixture(widget0, fixture0)}" \
in rendered


def test_params_view_missing_key_names_available_parameters() -> None:
"""A dangling params[...] reference (spec deleted or never declared) names
the missing key and the available parameters instead of advising a
sim.fit() that cannot fix it (run_20260829 cycle 1: an agent deleted
ParamSpec("butt_gap_tol") while a predicate still read it and burned turns
on the old "call sim.fit()" advice)."""
view = _ParamsView({"k": 1.0, "gap": 0.2})
assert view["k"] == 1.0
with pytest.raises(KeyError) as excinfo:
view["butt_gap_tol"] # pylint: disable=pointless-statement
msg = str(excinfo.value)
assert "butt_gap_tol" in msg
assert "gap, k" in msg
assert "ParamSpec" in msg
assert "before any parameter fit" not in msg
14 changes: 14 additions & 0 deletions tests/agent_sdk/test_solve_prompt_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ def test_explore_mode_early_stop_note_credits_exploration_plans() -> None:
assert "truncated just after that step" not in prompt


def test_explore_mode_states_the_cycle_data_contract() -> None:
"""Every cycle must attempt the full goal at least once and run the
ledger's top experiment as specified, and first-cycle coverage must carry
each interaction to its consequence (run_20260829: four cycle-0 episodes
staged glue and neighbours without ever consummating a bond, so no learner
had the event to model)."""
utils.reset_config({"seed": 0})
prompt = _render_explore(_make_task(None))
assert "attempt at the FULL goal" in prompt
assert "top-ranked open question's experiment executed as it is " \
"specified" in prompt
assert "Carry each interaction to its CONSEQUENCE" in prompt


def test_domain_strategy_block_is_advisory() -> None:
"""strategy.md content renders as an advisory section; absent without."""
utils.reset_config({"seed": 0})
Expand Down
24 changes: 24 additions & 0 deletions tests/approaches/test_agent_sim_prompt_formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,3 +387,27 @@ def _task(evaluator=None):
assert "## Task objective (env ground-truth reward)" in out
assert "each blue costs 0.05" in out
assert "evaluate_trajectory" in out


def test_learn_message_ships_goal_required_mechanisms_as_hypotheses():
"""The learn message distinguishes a hypothesis the goal can do without
(record, do not ship) from one the goal REQUIRES (ship as a labelled
hypothesis with declared ParamSpecs and a first-ranked confirming
experiment).

The message is composed from live session state, so the guard reads
the template source: in run_20260829 both plan-arm learners followed
the older "never ship a never-observed mechanism" rule, the belief
then had no bond, and the test session proved the goal unreachable.
"""
import inspect

from predicators.approaches.agent_sim_learning_approach import \
AgentSimLearningApproach
src = inspect.getsource(
AgentSimLearningApproach._build_synthesis_learn_message)
assert "When the goal REQUIRES it" in src
assert "LABELLED HYPOTHESIS" in src
assert "FIRST entry of open_questions.md" in src
assert "A GO that rests on a" in src
assert "instead of shipping a speculative rule" not in src
Loading
Loading