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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,397 changes: 0 additions & 2,397 deletions agent_sdk_approach_overview.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/sysid/make_sysid_pipeline_fig.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
("D1", 3, 7.0, "Belief env", "applied to base env for planning;\n"
"fresh validation envs re-apply;\n"
"dropped params revert to registry", None),
("D2", 3, 10.0, "Capture gate", "evaluate_option_plan: parse ->\n"
("D2", 3, 10.0, "Capture gate", "submit_plan: parse ->\n"
"legitimacy -> 3x/6x decorrelated\n"
"validation -> 32-pt hull sweep\n"
"-> PARAM-SENSITIVE on failure", None),
Expand Down
117 changes: 78 additions & 39 deletions predicators/agent_sdk/belief_probe.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion predicators/agent_sdk/bilevel_sketch.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
strip_subgoal_annotations
from predicators.agent_sdk.sketch_prompts import build_solve_prompt
from predicators.agent_sdk.sketch_refinement import DeepestFailure, \
InfoScorer, RefineOutcome, StepProbeSuggestion, \
InfoScorer, RefineOutcome, StepProbeSuggestion, ground_step, \
refine_and_validate_report, refine_sketch, resolve_refine_timeout, \
sample_params, suggest_probes
from predicators.agent_sdk.sketch_types import GroundSampler, SketchStep
Expand All @@ -47,6 +47,7 @@
"format_plan_lines",
"format_sketch_lines",
"format_step_line",
"ground_step",
"parse_atoms",
"parse_region_annotations",
"parse_sketch_from_text",
Expand Down
39 changes: 9 additions & 30 deletions predicators/agent_sdk/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,15 @@ def from_cfg(cls) -> "SessionConfig":
class RefinementConfig:
"""Plan-sketch refinement: search budgets, gates, and ground samplers.

Consumed at handler entry by ``refine_plan_sketch`` /
``evaluate_option_plan`` (tools.py) and by the probe's ``refine``
(belief_probe.py).
Consumed at handler entry by ``submit_plan`` (tools/testing.py) and
by the probe's ``refine`` (belief_probe.py).
"""
ground_samplers: bool
refinement_timeout_per_step: float
refinement_timeout_min: float
max_samples_per_step: int
check_subgoals: bool
log_state: bool
refine_evaluator_attempts: int
use_llm_initial_params: bool

@classmethod
Expand All @@ -80,8 +78,6 @@ def from_cfg(cls) -> "RefinementConfig":
max_samples_per_step=CFG.agent_bilevel_max_samples_per_step,
check_subgoals=CFG.agent_bilevel_check_subgoals,
log_state=CFG.agent_bilevel_log_state,
refine_evaluator_attempts=(
CFG.agent_bilevel_refine_evaluator_attempts),
use_llm_initial_params=CFG.agent_bilevel_use_llm_initial_params,
)

Expand All @@ -90,9 +86,9 @@ def from_cfg(cls) -> "RefinementConfig":
class ValidationConfig:
"""Capture-validation rollouts and the cross-attempt journal.

Consumed at handler entry by ``evaluate_option_plan`` (tools.py) and
the probe's ``run(trials=N)`` (belief_probe.py); ``use_journal``
gates the ``record_journal`` tool.
Consumed at handler entry by ``submit_plan`` (tools.py) and the
probe's ``run(trials=N)`` (belief_probe.py); ``use_journal`` gates
the journal / attempt-log channel.
"""
rollouts: int
rollouts_after_flaky: int
Expand Down Expand Up @@ -120,36 +116,19 @@ def from_cfg(cls) -> "ValidationConfig":
class ToolSurfaceConfig:
"""Which optional tools a session offers, and their surface knobs.

Consumed by the tool builders in tools.py (gates + descriptions
baked at build time), the proposal handlers (call-time gates), image
sizing, and the sandbox CLAUDE.md builder (sandbox_prompts.py).
Consumed by the tool builders (descriptions baked at build time) and
image sizing.
"""
use_explore_python: bool
explore_python_keep_replaced_tools: bool
use_base_simulator: bool
explore_python_call_timeout: float
python_call_timeout: float
image_max_px: int
propose_types: bool
propose_predicates: bool
propose_processes: bool
propose_options: bool
propose_objects: bool

@classmethod
def from_cfg(cls) -> "ToolSurfaceConfig":
"""Read the tool-surface flags from the live ``CFG``."""
# Flags keep their names for experiment-yaml compatibility.
return cls(
use_explore_python=CFG.agent_planner_use_explore_python,
explore_python_keep_replaced_tools=(
CFG.agent_planner_explore_python_keep_replaced_tools),
use_base_simulator=CFG.agent_planner_use_base_simulator,
explore_python_call_timeout=(
CFG.agent_sdk_explore_python_call_timeout),
python_call_timeout=(CFG.agent_sdk_python_call_timeout),
image_max_px=CFG.agent_sdk_image_max_px,
propose_types=CFG.agent_sdk_propose_types,
propose_predicates=CFG.agent_sdk_propose_predicates,
propose_processes=CFG.agent_sdk_propose_processes,
propose_options=CFG.agent_sdk_propose_options,
propose_objects=CFG.agent_sdk_propose_objects,
)
2 changes: 0 additions & 2 deletions predicators/agent_sdk/docker_agent_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ def _report_result(entry: Dict[str, Any]) -> None:

return {
"responses": collected,
"iteration_proposals": ctx.iteration_proposals,
}


Expand Down Expand Up @@ -318,7 +317,6 @@ def main() -> None:
"type": "error",
"error": str(e)
}],
"iteration_proposals": None,
}

# Save output
Expand Down
29 changes: 0 additions & 29 deletions predicators/agent_sdk/docker_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,35 +310,6 @@ def _stream_stderr() -> None:
query_output = pkl.load(f_in)

responses = query_output.get("responses", [])
proposals = query_output.get("iteration_proposals")

# 6. Merge proposals back into host ToolContext
if proposals is not None:
logger.info(
"Docker proposals: proposed_options=%s, "
"retract=%s",
[o.name for o in proposals.proposed_options],
sorted(proposals.retract_option_names),
)
self._tool_context.iteration_proposals = proposals
# Sync proposed/retracted options into ctx.options so
# the host-side parser can find them.
self._tool_context.options |= proposals.proposed_options
if proposals.retract_option_names:
self._tool_context.options = {
o
for o in self._tool_context.options
if o.name not in proposals.retract_option_names
}
logger.info(
"After Docker sync: tool_context.options=%s",
sorted(o.name for o in self._tool_context.options),
)
else:
logger.warning(
"Docker output has iteration_proposals=None; "
"no proposals synced.")

# Track costs/turns via the base delta accounting. Each
# docker query is a fresh in-container session whose
# cumulative cost restarts from zero, so reset the delta
Expand Down
124 changes: 69 additions & 55 deletions predicators/agent_sdk/journal.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
"""Persistent per-run solve journal.

One markdown file per run (``<sandbox>/journal.md``) that accumulates
knowledge across solve attempts and test tasks: the harness auto-records
each task's goal + initial state (one entry, at the top of the task's
section) and each attempt's outcome and captured or best refused plan,
and the agent records lessons via the ``record_journal`` MCP tool.
Fresh-context solve sessions read
the journal from their prompt, so knowledge travels through this curated
channel instead of raw transcript history (which also carries the wrong
conclusions of failed attempts - the anchoring failure mode).

Entries are size-capped and the tool guidance asks for facts and
measurements rather than verdicts: a recorded "X is impossible" from a
failed attempt would re-import exactly the anchoring the fresh context
is meant to shed, while "tried yaws 0-15 deg at x in [0.50, 0.54], all
stopped >=5 cm short" steers the next attempt without foreclosing it.

Phase lifecycle: learning-phase entries persist for the whole run and
accumulate across online-learning cycles, so every evaluation starts
from all learning knowledge so far. Test-phase entries live only for
their own evaluation: at ``end_test_phase`` the approach archives the
full journal to the run's log dir (outside the sandbox, so the agent
cannot read it) and rolls the file back to its pre-test content via
:func:`read_raw` / :func:`restore` - entries recorded while solving one
evaluation's test tasks must not leak into the next evaluation.
"""Persistent per-run solve journal and attempt log.

Two markdown files in the sandbox that carry knowledge across solve
attempts, test tasks, and learning cycles:

- ``journal.md`` is the AGENT's notebook. Solve and learn sessions
append to it with the ordinary file tools (no dedicated tool): short
factual entries - what was tried with exact parameters, what was
measured, what to try differently. The prompts ask for facts and
measurements rather than verdicts: a recorded "X is impossible"
from a failed attempt would re-import exactly the anchoring a fresh
context is meant to shed, while "tried yaws 0-15 deg at x in
[0.50, 0.54], all stopped >=5 cm short" steers the next attempt
without foreclosing it.
- ``attempts.md`` is the HARNESS's log, never edited by the agent:
each task's goal + initial state (once per task) and each
attempt's outcome and captured or best refused plan, so the
essentials of every attempt are on record even when the agent
writes nothing.

Fresh-context solve sessions read both from their prompt (tail-capped
so recent attempts stay intact), so knowledge travels through these
curated channels instead of raw transcript history.

Phase lifecycle: learning-phase content persists for the whole run
and accumulates across online-learning cycles, so every evaluation
starts from all learning knowledge so far. Test-phase additions live
only for their own evaluation: at ``end_test_phase`` the approach
archives both files to the run's log dir (outside the sandbox, so the
agent cannot read them) and rolls them back to their pre-test content
via :func:`read_raw` / :func:`restore` - entries written while
solving one evaluation's test tasks must not leak into the next.
"""

from __future__ import annotations
Expand All @@ -32,21 +38,19 @@
from typing import Optional

JOURNAL_FILENAME = "journal.md"

# Per-entry cap. Entries are meant to be skimmable bullet lists; a cap
# keeps one verbose attempt from crowding every later prompt.
MAX_ENTRY_CHARS = 2000

# Harness auto-entries get more room: the first entry per task embeds
# the init-state feature dict (the prompt's own representation) and a
# captured plan. The writer additionally orders the layout block last,
# so tail truncation at this cap can only ever cut layout, never the
# outcome or the captured plan.
MAX_AUTO_ENTRY_CHARS = 4000

# Cap on how much journal is injected into a solve prompt. Tail-biased:
# recent attempts (usually the same task) matter most.
MAX_PROMPT_CHARS = 8000
# The harness-owned attempt log (task contexts, attempt outcomes).
ATTEMPTS_FILENAME = "attempts.md"

# Per-entry cap for harness attempt-log entries: the first entry per
# task embeds the init-state feature dict (the prompt's own
# representation) and a captured plan. The writer orders the layout
# block last, so tail truncation at this cap can only ever cut layout,
# never the outcome or the captured plan.
MAX_ENTRY_CHARS = 4000
MAX_AUTO_ENTRY_CHARS = MAX_ENTRY_CHARS
# Cap on how much of each file is injected into a solve prompt.
# Tail-biased: recent attempts (usually the same task) matter most.
MAX_PROMPT_CHARS = 6000

# The learn-phase-maintained domain strategy document. Unlike the
# append-only journal (facts and measurements), strategy.md is a LIVING
Expand All @@ -66,6 +70,11 @@ def journal_path(sandbox_dir: str) -> str:
return os.path.join(sandbox_dir, JOURNAL_FILENAME)


def attempts_path(sandbox_dir: str) -> str:
"""Host path of the run's harness-owned attempt log."""
return os.path.join(sandbox_dir, ATTEMPTS_FILENAME)


def strategy_path(sandbox_dir: str) -> str:
"""Host path of the run's domain strategy document."""
return os.path.join(sandbox_dir, STRATEGY_FILENAME)
Expand Down Expand Up @@ -96,8 +105,9 @@ def read_strategy(sandbox_dir: Optional[str],
def append_entry(sandbox_dir: str,
header: str,
body: str,
max_chars: int = MAX_ENTRY_CHARS) -> Optional[str]:
"""Append one entry; returns a truncation notice or None.
max_chars: int = MAX_ENTRY_CHARS,
filename: str = ATTEMPTS_FILENAME) -> Optional[str]:
"""Append one harness entry; returns a truncation notice or None.

``header`` becomes a ``### <header>`` line; ``body`` is written
verbatim below it, truncated at ``max_chars`` (default
Expand All @@ -112,34 +122,37 @@ def append_entry(sandbox_dir: str,
body += "\n[entry truncated at the per-entry size cap]"
note = (f"entry truncated to {max_chars} chars - keep journal "
"entries short and factual")
with open(journal_path(sandbox_dir), "a", encoding="utf-8") as f:
with open(os.path.join(sandbox_dir, filename), "a", encoding="utf-8") as f:
f.write(f"### {header.strip()}\n{body}\n\n")
return note


def read_raw(sandbox_dir: Optional[str]) -> Optional[str]:
"""Exact journal file content, or None if no journal file exists.
def read_raw(sandbox_dir: Optional[str],
filename: str = JOURNAL_FILENAME) -> Optional[str]:
"""Exact file content, or None if the file does not exist.

Unlike :func:`read_journal` there is no prompt trimming and the
absent-file case is distinguishable from an empty file, so the
result is a faithful snapshot for :func:`restore`.
"""
if not sandbox_dir:
return None
path = journal_path(sandbox_dir)
path = os.path.join(sandbox_dir, filename)
if not os.path.isfile(path):
return None
with open(path, "r", encoding="utf-8") as f:
return f.read()


def restore(sandbox_dir: str, snapshot: Optional[str]) -> None:
"""Reset the journal file to a :func:`read_raw` snapshot.
def restore(sandbox_dir: str,
snapshot: Optional[str],
filename: str = JOURNAL_FILENAME) -> None:
"""Reset the file to a :func:`read_raw` snapshot.

A ``None`` snapshot means no journal file existed, so the file is
removed if present.
A ``None`` snapshot means the file did not exist, so it is removed
if present.
"""
path = journal_path(sandbox_dir)
path = os.path.join(sandbox_dir, filename)
if snapshot is None:
if os.path.isfile(path):
os.remove(path)
Expand All @@ -150,15 +163,16 @@ def restore(sandbox_dir: str, snapshot: Optional[str]) -> None:


def read_journal(sandbox_dir: Optional[str],
max_chars: int = MAX_PROMPT_CHARS) -> str:
"""Journal content for prompt injection ('' if absent or empty).
max_chars: int = MAX_PROMPT_CHARS,
filename: str = JOURNAL_FILENAME) -> str:
"""File content for prompt injection ('' if absent or empty).

Over ``max_chars`` the head is dropped at an entry boundary with a
truncation marker, keeping the most recent entries intact.
"""
if not sandbox_dir:
return ""
path = journal_path(sandbox_dir)
path = os.path.join(sandbox_dir, filename)
if not os.path.isfile(path):
return ""
with open(path, "r", encoding="utf-8") as f:
Expand Down
16 changes: 1 addition & 15 deletions predicators/agent_sdk/local_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ async def query(self,
await self.start_session()

# Wall-clock backstop for the solve attempt deadline: the probe
# and explore_python enforce it cooperatively (tool calls refuse
# and run_python enforce it cooperatively (tool calls refuse
# past the deadline), so normally the agent wraps up on its own;
# interrupt only if the turn stream is still going long after.
# The approach clears attempt_deadline before its final-submission
Expand Down Expand Up @@ -190,20 +190,6 @@ async def _maybe_interrupt_on_deadline(_entry: Dict[str, Any]) -> None:
kind=kind,
on_entry=_maybe_interrupt_on_deadline)

# Log proposals (matches Docker sandbox logging)
proposals = self._tool_context.iteration_proposals
if proposals.proposed_options or proposals.retract_option_names:
logger.info(
"Local sandbox proposals: proposed_options=%s, "
"retract=%s",
[o.name for o in proposals.proposed_options],
sorted(proposals.retract_option_names),
)
logger.info(
"After local sandbox query: tool_context.options=%s",
sorted(o.name for o in self._tool_context.options),
)

return collected

def _session_info_extras(self) -> Dict[str, Any]:
Expand Down
2 changes: 1 addition & 1 deletion predicators/agent_sdk/plan_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def execute_plan_forward(
"""Execute a fully-grounded plan step by step through the option model.

Shared forward-execution core behind ``validate_plan_forward`` (used
by ``refine_plan_sketch``) and the ``evaluate_option_plan`` tool.
by ``BeliefProbe.refine``) and the ``submit_plan`` tool.
State carries forward across options — matching how the real env
executes. Per step it mirrors ``run_backtracking_refinement``'s
fixed-plan path: check ``initiable``, call
Expand Down
Loading
Loading