This document tracks the module structure of LeanFlow and the in-progress decomposition of its
monoliths. It is the living companion to the refactoring program (see TODO.md "Refactoring
Plan" and the per-phase plan). Update it as modules move.
leanflow→leanflow_cli.main:main— the interactive shell / CLI.leanflow-agent→leanflow_agent:main— thin shim that seedsLEANFLOW_HOMEand callsrun_agent.main().
native_runner.py builds run_agent.AIAgent in-process (not via subprocess); the leanflow
shell spawns leanflow workflow … subprocesses for managed runs.
LeanFlow/
├── run_agent.py # AIAgent conversation loop (collaborators live under agent/)
├── leanflow_agent.py # leanflow-agent entry shim (seeds LEANFLOW_HOME + runs the legacy seed)
├── core/ # lowest layer (NO leanflow_cli deps): the home authority + shared kernel
│ ├── home.py # leanflow_home() + migrate_legacy_home() — single source of truth
│ ├── state.py time.py constants.py # SQLite session store / clock / endpoint constants
│ └── model_tools.py toolsets.py utils.py minisweagent_path.py
├── agent/ # AIAgent collaborators, grouped into cohesive subpackages:
│ # providers/ prompting/ compression/ execution/ display/ accounting/ runtime/
├── tools/ # agent tools, grouped: implementations/ utilities/ mcp/ environments/
│ # (registry.py + response.py kept at the top for self-registration)
└── leanflow_cli/ # shell UX + workflow orchestration, grouped:
# lean/ native/ formalization/ workflows/ cli/ runtime/
# (main.py shell.py config.py workflow.py kept at the top — entrypoint + patch targets)
Phase II reversed the earlier "keep everything flat" stance. The conservative first wave split the monoliths in place; Phase II then (a) grouped every extracted leaf into the subpackages above, (b) renamed the historical flat module names (e.g. the state store →
core.state) and consolidated to the singleLEANFLOW_env +~/.leanflowhome contract, and (c) completed the managed-run contract. The detailed extraction history further down lists modules by their original flat names — they now live under the subpackages here (e.g. theagent/collaborators are underagent/{accounting,execution,prompting,…}/; thenative_runner.py-era leaves underleanflow_cli/native/; thelean_*leaves underleanflow_cli/lean/).
Behavior-preserving throughout; each step gated by ruff+mypy+full pytest and committed separately.
- Risky-now fixes — locked concurrent workflow-state appends (
_locked_append), closed two shell-injection vectors intools/file_operations.py, narrowed a broad except. - Golden/characterization net —
tests/test_golden_cores.pypins the appendix-per-turn, callback-ordering, result-schema, and interrupt invariants before the DI work. - Deep restructure —
core/package introduced;agent/,tools/,leanflow_cli/each grouped into the subpackages above via collision-safe import rewrites (4 commits). - Env/home consolidation —
core.homesingle home authority (~/.leanflow); all runtime/session vars consolidated to the singleLEANFLOW_prefix; internal markers renamed. Kept: thescripts/install.sh→external Morph-template var contract. - Managed-run contract —
native_runnernow drives the post-tool-result appendix throughAIAgent.{stage,set,clear}_tool_result_appendixinstead of reaching into the private attribute. (The DI seams were already in place — 14+ injected collaborators; aPostToolResultAppendixBrokerwas evaluated and rejected because the raw attr is load-bearing for incompatible test semantics.) - Quality — safe ruff
B/SIMautofixes + 67try/except: pass→contextlib.suppress.
Line counts below are current (post-decomposition on refactor/leanflow-cores); the
"Target" column records what was carved off. The remaining bulk in each file is the coupled
core called out under "Deferred".
| File | Lines | Target |
|---|---|---|
leanflow_cli/native/native_runner.py |
11,671 → 8,962 | Phase 2: leaves → native_state boundary → cluster modules → proof_state_builder / verification_review / lean_module_paths / native_lean_files / queue_item_predicates; managed-conversation/follow-up core deferred |
run_agent.py (AIAgent) |
7,123 → 4,878 | Phase 4: 12 collaborators + collaborator_resolvers; Phase 4 module-level leaves workflow_events + runtime_helpers; run_conversation loop deferred |
leanflow_cli/lean/lean_services.py |
2,847 → 1,987 | Phase 5: lean_diagnostics / declarations / search_providers / automation / attempt_helpers / sorry_stats / proof_context_local + lean_backend wrapper + lean_models (result dataclasses) + lean_worker_dispatch; full backend abstraction deferred |
tools/implementations/web_tools.py |
1,670 → 1,309 | Phase 5: web_research_providers (arXiv/Semantic-Scholar/Crossref/Sourcegraph search + provider-ordering router + constants) split out |
agent/providers/auxiliary_client.py |
1,626 → 1,286 | Phase 5: auxiliary_adapters (routing) + model_capabilities (metadata+pricing) + auxiliary_rcp (RCP predicates) + auxiliary_nous (Nous auth/endpoint) split out |
tools/mcp/mcp_tool.py |
1,638 → 1,029 | Phase 5: mcp_transport (stdio/HTTP) + mcp_sampling (server-initiated LLM) + mcp_schema (schema/utility-schema/config-filter) + mcp_config (_load_mcp_config) split out |
leanflow_cli/workflows/workflow_state.py |
1,325 → 1,068 | Phase 3: activity_preview (event/status shaping) + workflow_state_paths (path-root discovery) + workflow_json_io (read/write JSON) split out |
leanflow_cli/formalization/formalization_documents.py |
1,512 → 536 | Phase 5: document_extraction + formalization_markdown + formalization_models + formalization_tex_discovery split out |
leanflow_cli/main.py |
1,331 → 426 | Phase 3: cli_handlers + shell_ui + shell (InteractiveShell REPL) split out; main.py is now a thin argparse dispatcher |
tools/implementations/lean_tool.py |
1,693 → 759 | Phase 5: lean_experts (advisor tools) + lean_patch (verified-patch apply) split out |
- mypy gate grown to 71 modules (all extracted leaves that pass cleanly).
- Silent-swallow logging: 10 highest-value
except: passsites now log (debug/warning) without changing control flow — persistence loads, checkpoint-before-mutation, telemetry writes. - ruff
UPmodernization applied tree-wide (~875 fixes: PEP585list/dict, PEP604X | None,datetime.UTC, OSError aliases) andUPis now enforced in the lint config (select=[F,I,UP], ignoring the unsafe/manualUP035/UP022/UP042). Requires Python ≥3.11 (already pinned). - ruff
B904exception chaining (raise … from exc) at 7 sites for better tracebacks. - Verified: full pytest suite green,
ruff+mypyclean, plus codex + a 4-reviewer adversarial pass (0 HIGH/0 MED findings).
- Public imports:
from run_agent import AIAgent;from model_tools import get_tool_definitions, handle_function_call, check_toolset_requirements;toolsets.*;utils.atomic_json_write. AIAgent.run_conversation()result schema (pinned bytests/test_run_conversation_schema.py):final_response, last_reasoning, messages, api_calls, usage, completed, exit_reason, partial, interrupted, response_previewed(+interrupt_messagewhen interrupted,erroron error).- Tool self-registration:
tools/*register at import viatools/registry.py;model_toolsimports tool modules by string name — keep names or update the discovery list. - Module-level imports used as patch targets / dynamic-access points (e.g.
tools.terminal_tool._interrupt_event) are part of the public surface even when they look "unused" — see the F401 note below.
- Characterize first (tests pinning current behavior).
- Move, don't rewrite — cut cohesive functions to a sibling module, fix imports only.
- Re-export shim from the original module path; keep
__all__accurate. Extracted modules must be leaves (no back-import into the monolith) —run_agent.pyhas ~46 lazy imports that make cycles easy to introduce. - Gate:
ruff check+ add the new module to mypy'sfileslist. - Verify: targeted tests → full suite →
--helpsmoke → ProveDemo workflow for runner changes. - One behavior-preserving extraction per commit/PR.
- ruff (
[tool.ruff.lint]):select = ["F", "I", "UP"](UP added in Phase 6),ignoreretainsF401/F841(re-export/patch-target safety) plus the unsafeUP035/UP022/UP042.- F401 (unused-import) and F841 (unused-variable) are deferred to Phase 6. F401 auto-removal
is unsafe here because module-level imports are re-exported as patch/dynamic-access targets
without
__all__; blanket removal silently breaks runtime and tests. Phase 6 handles them per-file with__all__/# noqa: F401.
- F401 (unused-import) and F841 (unused-variable) are deferred to Phase 6. F401 auto-removal
is unsafe here because module-level imports are re-exported as patch/dynamic-access targets
without
- mypy (
[tool.mypy]): incremental gate. Only the modules listed infiles = [...]are type-checked; the list grows as modules are extracted/cleaned. (Per-module overrides tune settings but do not select targets — the explicitfileslist does.) - CI runs ruff → mypy → pytest (
.github/workflows/tests.yml).
The full suite is green except one pre-existing xdist flake:
tests/tools/test_mcp_tool.py::TestMCPSelectiveToolLoading::test_existing_tool_names_reflect_registered_subset
fails only under full-parallel -n auto (parallel workers pollute the module-global tool
registry with mcp_lean_lsp_* entries); it passes in isolation and reproduces identically at the
branch base. Two earlier classes of local failure were FIXED on this branch: the
tests/agent/test_auxiliary_client.py failures (test-ordering pollution — a conftest autouse
fixture now snapshots/restores provider env between tests) and the test_non_quiet_logging
assertion (stale 1/180 fixture vs the default max_iterations=200).
The first wave (
refactor/leanflow-deep) was squashed and merged tomain; the follow-onrefactor/leanflow-coresbranch continues with further leaf extractions (theshell,lean_models,formalization_*,native_lean_files,queue_item_predicatesmodules below).
Behavior-preserving extractions completed so far (each: move verbatim → re-export shim from the
original module → ruff/mypy gate → full suite green → one commit). All extracted modules are leaf
modules (no back-import into their origin), keeping origin._name valid for callers and tests.
Phase 4 carved the AIAgent method clusters into single-responsibility collaborators under
agent/. AIAgent retains its public surface and now delegates: each collaborator is reached
through a lazy _resolve_*(agent) module-level accessor (which lazily constructs and caches the
collaborator if absent, so a bare-constructed or test-built agent still works), with thin method
wrappers and @property shims forwarding the old attribute/method names. This preserves the
patch/monkeypatch surface tests rely on while moving the logic out.
agent/token_accounting.py—TokenAccounter: cumulative token/cost counters.agent/provider_client.py—ProviderClientFactory: provider/OpenAI client construction + credential refresh.agent/tool_executor.py—ToolExecutor: concurrent/sequential tool-call dispatch for a turn.agent/conversation_manager.py—ConversationManager: session/message persistence + per-turn API-message shaping.agent/interrupt_controller.py—InterruptController:threading.Event-backed interrupt state (requested flag, message, children).agent/response_normalizer.py—ResponseNormalizer: raw provider response → normalized assistant response.agent/reasoning_processor.py—ReasoningProcessor: thinking/reasoning-block (mostly pure) text helpers.agent/prompt_manager.py—PromptManager: per-session system-prompt build/cache/invalidate lifecycle.agent/api_caller.py—ApiCaller: mediation between the agent loop and the provider API call.agent/compression_policy.py—CompressionPolicy: when/how context compression fires.agent/anthropic_messages.py—AnthropicMessagePreparer: Anthropic message-preparation cluster.agent/output_manager.py—OutputManager: conversation-start / token-usage / session-usage logging.agent/collaborator_resolvers.py— the lazy_resolve_X(agent)accessors that materialize/cache each collaborator on first use (re-exported onrun_agent).agent/command_safety.py— destructive-command detection (Phase 4).agent/log_formatting.py— pure tool arg/result log formatters (Phase 1).agent/managed_run.py— typed managed-run contract (Phase 1.5).
native_config.py— env/config readers (_read_native_env,_managed_home,_project_root, …).lean_parsing.py— pure Lean source/declaration text parsers (comment/string stripping, decl extraction).native_state.py— module-level mutable de-dup caches +_cache_once.queue_edit_guard.py— declaration-edit protect/restore guards.formalization_document_runner.py—/formalizeworkflow predicates + blueprint manifest parsers.manager_verification.py— verification-record/outcome + timeout/retry helpers.native_utils.py— shared leaf text/JSON/format helpers (_single_line,_extract_json_payload, …).project_prove_manager.py— file-level work-queue sizing/prioritization helpers.proof_state_builder.py— pure proof-state text/snapshot shaping helpers (safe subset).lean_diagnostic_feedback.py— pure diagnostic / goal text parsers (safe subset).native_checkpoints.py— workflow-state and checkpoint persistence helpers (safe subset).verification_review.py— verification-decision / advisory text parsers (safe subset).lean_module_paths.py— pure Lean module-name ↔ import-path ↔ on-disk-path translation helpers (safe subset).formalization_generated_lean.py— generated-Lean inspection helpers (safe subset).native_lean_files.py— active-file/target-symbol resolution + per-file/projectsorrycounting (imports the native_config / native_utils / lean_parsing leaves; no cycle).queue_item_predicates.py— pure queue-item classification predicates (sorry vs diagnostic blocker, current-item/status selection, attempted-proof shape).
lean_diagnostics.py— diagnostic/blocker/goal text parsers (incl. the backtracking-fixeddiagnostic_items).lean_declarations.py— pure path-based Lean declaration indexing / lookup helpers, plus the token-cheapdeclaration_outline/declaration_regionreaders backing thelean_outlinetool.lean_lemma_suggest.py— goal->candidate-lemma retriever: reads the assigned declaration's goal/hypotheses (vialean_proof_context/lean_inspect, resolved lazily offlean_services), derives targeted queries, runslean_searchacross modes, and dedupes/ranks candidates. Backs thelean_lemma_suggesttool.lean_search_providers.py— stateless Lean search-provider helpers.lean_automation.py— pure Lean auto-prove normalization / parsing helpers.lean_attempt_helpers.py— pure multi-attempt / path / comment text helpers.lean_sorry_stats.py— puresorry-counting helpers.lean_proof_context_local.py— pure local proof-context assembly helpers (safe subset).lean_models.py— the frozen Lean result/report dataclasses (LeanCapabilityReport,LeanSorryFinding,LeanInspection,LeanVerificationResult,LeanSearchResult,LeanAxiomReport,WorkflowRouteDecision,LeanWorkerRequest,LeanWorkerResult).lean_backend.py—LeanBackend, a thin façade forwarding to the LSP/MCP JSON tool invoker (_invoke_json_tool), the Lake/subprocess runner (_run_command), and a capability reader. A first, partial realization of the deferred backend abstraction: it wraps the existing primitives verbatim (resolving them lazily offlean_servicesfor monkeypatch safety) without owning backend state — the full LSP/REPL/Lake interface redesign remains deferred.
cli_handlers.py— argparse handler/formatter functions (_handle_config/_sandbox/_models, …).shell_ui.py— pure presentation helpers (prompt / bottom-toolbar formatters) that turn already-gathered shell state into display strings.shell.py— the fullInteractiveShellREPL (prompt-toolkit loop, slash-command dispatch, workflow launch/monitor, status rendering), re-exported frommainfor the historicalfrom leanflow_cli.main import InteractiveShellsurface. main.py is now a thin argparse dispatcher (~425 lines). Tests driving shell methods patch collaborators onleanflow_cli.shell; tests drivingmain()patch them onleanflow_cli.main(both import the names independently).- Shell slash-command routing is now unified in
commands.pybehind a singleCOMMAND_REGISTRY(tuple[WorkflowCommandSpec, …]), replacing the scattered per-command branches.
loogle_local.py— project-toolchain-matched local Loogle: per-toolchain cache resolution (loogle_cache_dir_for_project), the idempotent build (ensure_local_loogle_for_projectand its detached_asynclauncher, both under an exclusive<cache>/.loogle-build.lock), the fast no-build gate (local_loogle_needs_build), and thepatch_lean_lsp_loogle_build_lockpatch that makes lean-lsp-mcp take the same lock. It builds on the low-level primitives kept inmcp_bootstrap(managed_loogle_cache_dir,local_loogle_supported,_read_lean_toolchain,_lean_lsp_env_from_home);mcp_bootstrapreaches back only via lazy imports (managed_mcp_power_status,bootstrap_lean_mcp) to avoid a cycle. The lean-lsp server is pointed at the same per-toolchain dir bytools/mcp/mcp_transport._augment_lean_stdio_env— build, server, and status must agree (a test pins this). mypy-gated.
document_extraction.py— the text/LaTeX/PDF extraction layer: turns a resolved source file into a structured summary (theorem blocks, sections, references, extracted text). A closed set under "calls" that reaches no origin-mutable state, re-exported onformalization_documents.formalization_markdown.py— planner-context Markdown rendering (theorem blocks, sections, TeX-project discovery, source excerpt); imports only thedocument_extractionleaf.formalization_models.py— theFormalizationDocumentErrorexception + theFormalizationDocumentContext/_FormalizationDocumentSelectionfrozen dataclasses (shared data types, decoupled so the TeX-discovery leaf can use them without a cycle).formalization_tex_discovery.py— path resolution + TeX-project entrypoint/include/asset discovery (19 helpers + theTEX_PROJECT_*constants); imports only stdlib,document_extractionandformalization_models. formalization_documents.py is now a focused selection/context-prep module.
queue_models.py— theTheoremQueueManagerqueue dataclasses + legacy dict<->typed mapping (verbatim move).
activity_preview.py— pure activity/event-shaping helpers for managed-workflow status views.
agent/auxiliary_adapters.py— OpenAI-client-compatible provider adapters for the auxiliary router.- Model metadata (
agent/model_metadata.py) + pricing are unified behind theagent/model_capabilities.pyfaçade.
tools/lean_experts.py— auxiliary LLM-advisor Lean tools.tools/lean_patch.py— verified-patch application tool.
tools/mcp_transport.py— stdio/HTTP transport plumbing for MCP servers.tools/mcp_sampling.py—SamplingHandler: the server-initiatedsampling/createMessagecallback (server asks the agent's LLM to complete a message) plus its numeric-coercion / audit-path helpers; re-exported onmcp_tool.
- Test-pollution fix: importing
run_agentranload_leanflow_dotenv()at import time before the autouse_isolate_leanflow_homefixture had setLEANFLOW_HOME, leaking the developer's real.envprovider-resolution vars (LEANFLOW_*) into the session and breakingtests/agent/test_auxiliary_client.pywhenevertest_run_agentran first. The fixture now strips those vars (monkeypatch.delenv, auto-restored) so resolution starts clean regardless of test order. No production behavior changed. - Workflow-termination fixes: the headless stdin-exit guard now writes the pre-exit workflow
checkpoint (
force_filesystem_checkpoint=True) for autonomous workflows, restoring resumability on headless non-verified exits; the hard cycle-ceiling stop now labels its phase by_live_state_is_verified(verified vs stalled) instead of always "stalled"; andmcp_bootstrap._write_bootstrap_documentnow callsinvalidate_config_cache()after writing the managed config directly (it bypassesconfig.save_config), fixing a staleload_config()cache that returned the pre-bootstrap config later in the same process.
The still-coupled cores that resist behavior-preserving one-shot moves:
native_runner.py: the autonomous follow-up loop and_run_managed_conversation/main.AIAgent: therun_conversationmain loop and its retry/recovery orchestration.main.py:InteractiveShell(its callees are test-monkeypatched onmain).lean_services.py: the full backend abstraction (the LSP/REPL/Lake interface), which is a redesign rather than a move. TheLeanBackendwrapper (lean_backend.py) is a first partial step; routing all call sites through it is the remaining invasive work.
These are the next, more invasive refactoring steps.