Proof State Representation and Tactic Application in LeanAgent: A Deep Dive
LeanAgent represents proof states as plain-text strings containing the turnstile symbol (⊢) and applies tactics by calling Dojo.run_tac to interact with the Lean proof assistant, returning either a new state, completion, or error.
This article examines how the lean-dojo/leanagent repository handles proof state representation and tactic execution during automated theorem proving. Understanding these mechanisms is essential for extending the proof search or debugging tactic failures.
How LeanAgent Represents Proof States
The proof state representation in LeanAgent is designed to bridge the gap between Lean's native output and the neural generator's input requirements.
The Plain-Text State Format
At its core, a proof state is stored as a plain-text string that always contains the turnstile symbol ⊢ to separate goals. This string-based approach allows the generator model to consume the current proof obligations without parsing complex AST structures.
When Lean produces output, it often prefixes the state with metadata such as N goals. LeanAgent strips this prefix to ensure only the essential goal information is stored and displayed to the model.
Normalizing Raw Lean Output with format_state
The [common.py](https://github.com/lean-dojo/leanagent/blob/main/common.py#L91-L96) file contains the format_state function, which cleans raw Lean output by removing the "N goals" prefix:
def format_state(s: str) -> str:
m = re.match(r"\d+ goals", s)
if m is not None:
return s[m.end() :].strip()
else:
return s
This function guarantees that every stored state begins with the goal separator ⊢ and contains no markup symbols like « or ». The normalization ensures consistent prompting for the tactic generator regardless of how Lean formats its initial output.
The TacticState Wrapper
LeanAgent leverages the TacticState class from the external lean_dojo package to encapsulate state information. According to the source code in [proof_search.py](https://github.com/lean-dojo/leanagent/blob/main/prover/proof_search.py#L212-L215), a TacticState object exposes two critical attributes:
pp: The human-readable, pretty-printed version of the state used for prompting the generatorunsolved_tactic_state: The raw state string fed back to Lean during tactic execution
The prover selects the appropriate representation based on the node type:
if isinstance(search_node.state, TacticState):
ts = search_node.state.pp # text shown to the model
else:
ts = search_node.state.unsolved_tactic_state # raw Lean state
This dual representation allows LeanAgent to maintain both a clean interface for the neural model and a machine-readable format for the proof assistant.
Applying Tactics Through Lean
Tactic application follows a generate-then-validate pattern, where candidates are produced by a neural model and verified by Lean's kernel.
Generating Candidate Tactics
The generator model receives the current state (ts) along with theorem metadata (name, file path, position) and returns a list of (tactic, logprob) pairs. As implemented in the search loop, this occurs via:
suggestions = await self._generate_tactics(ts)
The generation logic resides in generator/model.py, which builds prompts embedding the state string as TACTIC_STATE.
Executing Tactics with Dojo.run_tac
The actual interaction with Lean occurs through Dojo.run_tac, called in [proof_search.py](https://github.com/lean-dojo/leanagent/blob/main/prover/proof_search.py#L265-L272):
response = self.dojo.run_tac(node.state, tactic)
This method sends the tactic string to the Lean server for execution against the current proof state. The run_tac function is part of the external lean_dojo package and handles the low-level communication protocol with the Lean proof assistant.
Handling Execution Results
The run_tac method returns one of several result types that determine the search trajectory:
ProofFinished: All goals are solved, indicating a complete proofLeanError: The tactic failed with a compilation or logical errorTimeoutError: Execution exceeded the configured time limitProofGivenUp: The proof was abandoned due to resource constraintsTacticState: Successful execution containing the new goal list
When a TacticState is returned, the prover creates a new search node with the updated state and cumulative log probability, then optionally enqueues it for further expansion.
End-to-End Tactic Application Flow
The following example illustrates the complete workflow from initial state to tactic application:
# Initialize Dojo and get the initial Lean goal state
with Dojo(theorem, timeout) as (dojo, init_state):
root = InternalNode(state=init_state, cumulative_logprob=0.0)
# Extract the printable state string for the model
ts = root.state.pp # or .unsolved_tactic_state for non-TacticState nodes
# Request candidate tactics from the generator
candidates = generator.generate(state=ts, theorem=theorem) # list of (tactic, logprob)
# Apply each tactic via Dojo
for tac, logp in candidates:
resp = dojo.run_tac(root.state, tac)
if isinstance(resp, ProofFinished):
# Proof complete - terminate search
break
elif isinstance(resp, TacticState):
# Create new node with updated goals for further search
new_node = InternalNode(state=resp, cumulative_logprob=root.cumulative_logprob + logp)
queue.append(new_node)
else:
# Handle LeanError, TimeoutError, or ProofGivenUp
continue
You can also inspect states and apply tactics manually for debugging:
# Inspect current goals
print(root.state.pp) # e.g., "⊢ Nat.succ n = m"
# Apply a tactic manually
new_state = dojo.run_tac(root.state, "apply Nat.succ_eq_add_one")
Summary
- Proof states are stored as plain-text strings containing the
⊢separator, normalized byformat_stateto remove Lean's "N goals" prefix. - Dual representation via
TacticState.pp(human-readable) andunsolved_tactic_state(machine-readable) supports both model prompting and Lean communication. - Tactic execution relies on
lean_dojo'sDojo.run_tacmethod, which returns structured results includingProofFinished, error types, or newTacticStateobjects. - Search integration combines neural generation with Lean verification, creating new nodes only when tactics successfully produce valid successor states.
Frequently Asked Questions
What format does LeanAgent use to store proof states internally?
LeanAgent stores proof states as plain-text strings that always include the turnstile symbol ⊢ to denote goals. The format_state function in common.py ensures these strings are cleaned of Lean's "N goals" prefix and special markup characters before storage or model consumption.
How does LeanAgent decide which proof state representation to show the generator?
The prover checks the node type in [proof_search.py](https://github.com/lean-dojo/leanagent/blob/main/prover/proof_search.py#L212-L215). For TacticState objects, it uses the pp (pretty-print) attribute for human-readable prompts. For other node types, it falls back to unsolved_tactic_state, which provides the raw format required by Lean's API.
What happens when a tactic fails during proof search?
When Dojo.run_tac encounters a failure, it returns either a LeanError (compilation failure), TimeoutError (execution timeout), or ProofGivenUp (resource exhaustion). The prover treats these as terminal nodes that do not expand the search tree, allowing the best-first search to explore alternative tactic sequences instead.
Can I manually apply tactics to a specific proof state outside the search loop?
Yes. You can directly call dojo.run_tac(state, tactic_string) where state is either a TacticState object or the initial state from a Dojo context. The method returns a new state, completion marker, or error, allowing interactive debugging or custom proof automation scripts independent of the main search algorithm.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →